@btc-vision/btc-runtime 1.10.11 → 1.11.0-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -224
- package/SECURITY.md +38 -191
- package/docs/README.md +28 -0
- package/docs/advanced/contract-upgrades.md +537 -0
- package/docs/advanced/plugins.md +90 -25
- package/docs/api-reference/blockchain.md +48 -14
- package/docs/api-reference/storage.md +2 -111
- package/docs/contracts/op-net-base.md +22 -0
- package/docs/contracts/upgradeable.md +396 -0
- package/docs/core-concepts/blockchain-environment.md +0 -2
- package/docs/core-concepts/security.md +8 -111
- package/docs/core-concepts/storage-system.md +1 -32
- package/docs/examples/nft-with-reservations.md +8 -238
- package/docs/storage/memory-maps.md +1 -44
- package/docs/storage/stored-arrays.md +1 -65
- package/docs/storage/stored-maps.md +1 -73
- package/docs/storage/stored-primitives.md +2 -49
- package/docs/types/bytes-writer-reader.md +76 -0
- package/docs/types/safe-math.md +2 -45
- package/package.json +5 -5
- package/runtime/buffer/BytesReader.ts +90 -3
- package/runtime/buffer/BytesWriter.ts +81 -3
- package/runtime/contracts/OP721.ts +40 -4
- package/runtime/contracts/OP_NET.ts +83 -11
- package/runtime/contracts/Upgradeable.ts +242 -0
- package/runtime/env/BlockchainEnvironment.ts +124 -27
- package/runtime/env/global.ts +24 -0
- package/runtime/events/upgradeable/UpgradeableEvents.ts +41 -0
- package/runtime/generic/AddressMap.ts +20 -18
- package/runtime/generic/ExtendedAddressMap.ts +147 -0
- package/runtime/generic/MapUint8Array.ts +20 -18
- package/runtime/index.ts +8 -0
- package/runtime/plugins/Plugin.ts +34 -0
- package/runtime/plugins/UpgradeablePlugin.ts +279 -0
- package/runtime/storage/BaseStoredString.ts +1 -1
- package/runtime/storage/arrays/StoredPackedArray.ts +4 -0
- package/runtime/types/ExtendedAddress.ts +36 -24
- package/runtime/types/ExtendedAddressCache.ts +27 -0
- package/runtime/types/SafeMath.ts +109 -18
- package/runtime/types/SchnorrSignature.ts +44 -0
- package/runtime/utils/lengths.ts +2 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { ExtendedAddress } from '../types/ExtendedAddress';
|
|
2
|
+
import { Revert } from '../types/Revert';
|
|
3
|
+
import { IMap } from './Map';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A map implementation using ExtendedAddress (64 bytes) as keys.
|
|
7
|
+
* Uses hyper-optimized search with prefix filtering for performance.
|
|
8
|
+
*/
|
|
9
|
+
@final
|
|
10
|
+
export class ExtendedAddressMap<V> implements IMap<ExtendedAddress, V> {
|
|
11
|
+
protected _keys: ExtendedAddress[] = [];
|
|
12
|
+
protected _values: V[] = [];
|
|
13
|
+
|
|
14
|
+
// CACHE: Stores the index of the last successful lookup to make repeated access O(1)
|
|
15
|
+
private _lastIndex: i32 = -1;
|
|
16
|
+
|
|
17
|
+
@inline
|
|
18
|
+
public get size(): i32 {
|
|
19
|
+
return this._keys.length;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@inline
|
|
23
|
+
public keys(): ExtendedAddress[] {
|
|
24
|
+
return this._keys;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@inline
|
|
28
|
+
public values(): V[] {
|
|
29
|
+
return this._values;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public set(key: ExtendedAddress, value: V): this {
|
|
33
|
+
const index = this.indexOf(key);
|
|
34
|
+
|
|
35
|
+
if (index === -1) {
|
|
36
|
+
this._keys.push(key);
|
|
37
|
+
this._values.push(value);
|
|
38
|
+
this._lastIndex = this._keys.length - 1;
|
|
39
|
+
} else {
|
|
40
|
+
unchecked((this._values[index] = value));
|
|
41
|
+
this._lastIndex = index;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* HYPER-OPTIMIZED SEARCH
|
|
49
|
+
* Compares both the ML-DSA key hash (inherited) and tweakedPublicKey.
|
|
50
|
+
*/
|
|
51
|
+
public indexOf(searchKey: ExtendedAddress): i32 {
|
|
52
|
+
if (this.isLastIndex(searchKey)) {
|
|
53
|
+
return this._lastIndex;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const len = this._keys.length;
|
|
57
|
+
if (len === 0) return -1;
|
|
58
|
+
|
|
59
|
+
const searchMldsaData = searchKey.dataStart;
|
|
60
|
+
const searchTweakedData = searchKey.tweakedPublicKey.dataStart;
|
|
61
|
+
|
|
62
|
+
// Loop Backwards (finding most recently added items first)
|
|
63
|
+
for (let i = len - 1; i >= 0; i--) {
|
|
64
|
+
const key = unchecked(this._keys[i]);
|
|
65
|
+
|
|
66
|
+
// Quick prefix check on ML-DSA key hash (first 8 bytes)
|
|
67
|
+
if (load<u64>(key.dataStart) !== load<u64>(searchMldsaData)) continue;
|
|
68
|
+
|
|
69
|
+
// Quick prefix check on tweaked public key (first 8 bytes)
|
|
70
|
+
if (
|
|
71
|
+
load<u64>(key.tweakedPublicKey.dataStart) !== load<u64>(searchTweakedData)
|
|
72
|
+
)
|
|
73
|
+
continue;
|
|
74
|
+
|
|
75
|
+
// Full comparison of ML-DSA key hash (32 bytes)
|
|
76
|
+
if (memory.compare(key.dataStart, searchMldsaData, 32) !== 0) continue;
|
|
77
|
+
|
|
78
|
+
// Full comparison of tweaked public key (32 bytes)
|
|
79
|
+
if (memory.compare(key.tweakedPublicKey.dataStart, searchTweakedData, 32) === 0) {
|
|
80
|
+
this._lastIndex = i;
|
|
81
|
+
return i;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return -1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private isLastIndex(key: ExtendedAddress): bool {
|
|
89
|
+
if (this._lastIndex !== -1) {
|
|
90
|
+
const cachedKey = unchecked(this._keys[this._lastIndex]);
|
|
91
|
+
|
|
92
|
+
// Check ML-DSA key hash equality
|
|
93
|
+
if (memory.compare(cachedKey.dataStart, key.dataStart, 32) !== 0) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check tweaked public key equality
|
|
98
|
+
if (
|
|
99
|
+
memory.compare(
|
|
100
|
+
cachedKey.tweakedPublicKey.dataStart,
|
|
101
|
+
key.tweakedPublicKey.dataStart,
|
|
102
|
+
32,
|
|
103
|
+
) === 0
|
|
104
|
+
) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@inline
|
|
113
|
+
public has(key: ExtendedAddress): bool {
|
|
114
|
+
return this.indexOf(key) !== -1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public get(key: ExtendedAddress): V {
|
|
118
|
+
const index = this.indexOf(key);
|
|
119
|
+
if (index === -1) {
|
|
120
|
+
throw new Revert('Key not found in map');
|
|
121
|
+
}
|
|
122
|
+
return unchecked(this._values[index]);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
public delete(key: ExtendedAddress): bool {
|
|
126
|
+
const index = this.indexOf(key);
|
|
127
|
+
if (index === -1) return false;
|
|
128
|
+
|
|
129
|
+
this._keys.splice(index, 1);
|
|
130
|
+
this._values.splice(index, 1);
|
|
131
|
+
|
|
132
|
+
this._lastIndex = -1;
|
|
133
|
+
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@inline
|
|
138
|
+
public clear(): void {
|
|
139
|
+
this._keys = [];
|
|
140
|
+
this._values = [];
|
|
141
|
+
this._lastIndex = -1;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
public toString(): string {
|
|
145
|
+
return `ExtendedAddressMap(size=${this._keys.length})`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -34,23 +34,7 @@ export class MapUint8Array implements IMap<Uint8Array, Uint8Array> {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
public set(key: Uint8Array, value: Uint8Array): this {
|
|
37
|
-
let index =
|
|
38
|
-
|
|
39
|
-
// Check Cache (Fastest)
|
|
40
|
-
if (this._lastIndex !== -1) {
|
|
41
|
-
const cachedKey = unchecked(this._keys[this._lastIndex]);
|
|
42
|
-
// Check length first, then full content equality
|
|
43
|
-
if (cachedKey.length === key.length) {
|
|
44
|
-
if (memory.compare(cachedKey.dataStart, key.dataStart, key.length) === 0) {
|
|
45
|
-
index = this._lastIndex;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Full Scan if cache missed
|
|
51
|
-
if (index === -1) {
|
|
52
|
-
index = this.indexOf(key);
|
|
53
|
-
}
|
|
37
|
+
let index = this.indexOf(key);
|
|
54
38
|
|
|
55
39
|
if (index === -1) {
|
|
56
40
|
this._keys.push(key);
|
|
@@ -58,7 +42,7 @@ export class MapUint8Array implements IMap<Uint8Array, Uint8Array> {
|
|
|
58
42
|
this._lastIndex = this._keys.length - 1;
|
|
59
43
|
} else {
|
|
60
44
|
unchecked((this._values[index] = value));
|
|
61
|
-
// Cache is already pointing to this index (from indexOf
|
|
45
|
+
// Cache is already pointing to this index (from indexOf)
|
|
62
46
|
this._lastIndex = index;
|
|
63
47
|
}
|
|
64
48
|
|
|
@@ -71,6 +55,10 @@ export class MapUint8Array implements IMap<Uint8Array, Uint8Array> {
|
|
|
71
55
|
* Uses a "Prefix Filter" to skip expensive memory comparisons.
|
|
72
56
|
*/
|
|
73
57
|
public indexOf(pointerHash: Uint8Array): i32 {
|
|
58
|
+
if (this.isLastIndex(pointerHash)) {
|
|
59
|
+
return this._lastIndex;
|
|
60
|
+
}
|
|
61
|
+
|
|
74
62
|
const len = this._keys.length;
|
|
75
63
|
if (len === 0) return -1;
|
|
76
64
|
|
|
@@ -119,6 +107,20 @@ export class MapUint8Array implements IMap<Uint8Array, Uint8Array> {
|
|
|
119
107
|
return -1;
|
|
120
108
|
}
|
|
121
109
|
|
|
110
|
+
private isLastIndex(key: Uint8Array): bool {
|
|
111
|
+
if (this._lastIndex !== -1) {
|
|
112
|
+
const cachedKey = unchecked(this._keys[this._lastIndex]);
|
|
113
|
+
// Check length first, then full content equality
|
|
114
|
+
if (cachedKey.length === key.length) {
|
|
115
|
+
if (memory.compare(cachedKey.dataStart, key.dataStart, key.length) === 0) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
122
124
|
@inline
|
|
123
125
|
public has(key: Uint8Array): bool {
|
|
124
126
|
return this.indexOf(key) !== -1;
|
package/runtime/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ export * from './interfaces/IBTC';
|
|
|
20
20
|
export * from './events/NetEvent';
|
|
21
21
|
export * from './events/predefined';
|
|
22
22
|
export * from './events/op20s/OP20SEvents';
|
|
23
|
+
export * from './events/upgradeable/UpgradeableEvents';
|
|
23
24
|
|
|
24
25
|
/** Env */
|
|
25
26
|
export * from './env/classes/UTXO';
|
|
@@ -35,6 +36,7 @@ export * from './env/consensus/MLDSAMetadata';
|
|
|
35
36
|
export * from './generic/Map';
|
|
36
37
|
export * from './generic/MapU256';
|
|
37
38
|
export * from './generic/AddressMap';
|
|
39
|
+
export * from './generic/ExtendedAddressMap';
|
|
38
40
|
|
|
39
41
|
/** Types */
|
|
40
42
|
export * from './types';
|
|
@@ -43,6 +45,7 @@ export * from './types';
|
|
|
43
45
|
export * from './lang/Definitions';
|
|
44
46
|
export * from './types/Address';
|
|
45
47
|
export * from './types/ExtendedAddress';
|
|
48
|
+
export * from './types/SchnorrSignature';
|
|
46
49
|
export * from './types/Revert';
|
|
47
50
|
export * from './types/SafeMath';
|
|
48
51
|
export * from './types/SafeMathI128';
|
|
@@ -106,4 +109,9 @@ export * from './contracts/interfaces/IOP721';
|
|
|
106
109
|
export * from './contracts/interfaces/IOP1155';
|
|
107
110
|
export * from './contracts/interfaces/OP721InitParameters';
|
|
108
111
|
export * from './contracts/ReentrancyGuard';
|
|
112
|
+
export * from './contracts/Upgradeable';
|
|
109
113
|
export * from './contracts/interfaces/OP1155InitParameters';
|
|
114
|
+
|
|
115
|
+
/** Plugins */
|
|
116
|
+
export * from './plugins/Plugin';
|
|
117
|
+
export * from './plugins/UpgradeablePlugin';
|
|
@@ -1,10 +1,44 @@
|
|
|
1
1
|
import { Calldata } from '../types';
|
|
2
2
|
import { Selector } from '../math/abi';
|
|
3
|
+
import { BytesWriter } from '../buffer/BytesWriter';
|
|
3
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Base class for plugins that can extend contract functionality.
|
|
7
|
+
*
|
|
8
|
+
* Plugins can be registered with OP_NET contracts to automatically
|
|
9
|
+
* handle method selectors without requiring manual delegation in execute().
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* class MyPlugin extends Plugin {
|
|
14
|
+
* public execute(method: Selector, calldata: Calldata): BytesWriter | null {
|
|
15
|
+
* switch (method) {
|
|
16
|
+
* case encodeSelector('myMethod()'):
|
|
17
|
+
* return this.myMethod();
|
|
18
|
+
* default:
|
|
19
|
+
* return null; // Not handled
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
4
25
|
export class Plugin {
|
|
5
26
|
public onDeployment(_calldata: Calldata): void {}
|
|
6
27
|
|
|
28
|
+
public onUpdate(_calldata: Calldata): void {}
|
|
29
|
+
|
|
7
30
|
public onExecutionStarted(_selector: Selector, _calldata: Calldata): void {}
|
|
8
31
|
|
|
9
32
|
public onExecutionCompleted(_selector: Selector, _calldata: Calldata): void {}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Attempts to execute a method.
|
|
36
|
+
*
|
|
37
|
+
* @param _method - The method selector
|
|
38
|
+
* @param _calldata - The calldata
|
|
39
|
+
* @returns BytesWriter response if handled, null if not handled
|
|
40
|
+
*/
|
|
41
|
+
public execute(_method: Selector, _calldata: Calldata): BytesWriter | null {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
10
44
|
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { u256 } from '@btc-vision/as-bignum/assembly';
|
|
2
|
+
import { Blockchain } from '../env';
|
|
3
|
+
import { Plugin } from './Plugin';
|
|
4
|
+
import { StoredAddress } from '../storage/StoredAddress';
|
|
5
|
+
import { StoredU256 } from '../storage/StoredU256';
|
|
6
|
+
import { Address } from '../types/Address';
|
|
7
|
+
import { Revert } from '../types/Revert';
|
|
8
|
+
import { BytesWriter } from '../buffer/BytesWriter';
|
|
9
|
+
import { encodeSelector, Selector } from '../math/abi';
|
|
10
|
+
import { ADDRESS_BYTE_LENGTH } from '../utils';
|
|
11
|
+
import { Calldata } from '../types';
|
|
12
|
+
import { EMPTY_POINTER } from '../math/bytes';
|
|
13
|
+
import {
|
|
14
|
+
UpgradeSubmittedEvent,
|
|
15
|
+
UpgradeAppliedEvent,
|
|
16
|
+
UpgradeCancelledEvent,
|
|
17
|
+
} from '../events/upgradeable/UpgradeableEvents';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* UpgradeablePlugin - Plugin for upgradeable contracts with timelock protection.
|
|
21
|
+
*
|
|
22
|
+
* This plugin provides a secure upgrade mechanism with a configurable delay period.
|
|
23
|
+
* Unlike extending the Upgradeable base class, this plugin can be added to any contract.
|
|
24
|
+
*
|
|
25
|
+
* The pattern prevents instant malicious upgrades by requiring:
|
|
26
|
+
* 1. submitUpgrade() - Submit the source contract address, starts the timelock
|
|
27
|
+
* 2. Wait for the delay period to pass
|
|
28
|
+
* 3. applyUpgrade() - Apply the upgrade after the delay
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* @final
|
|
33
|
+
* export class MyContract extends OP_NET {
|
|
34
|
+
* public constructor() {
|
|
35
|
+
* super();
|
|
36
|
+
* // 144 blocks = ~24 hours
|
|
37
|
+
* this.registerPlugin(new UpgradeablePlugin(144));
|
|
38
|
+
* }
|
|
39
|
+
*
|
|
40
|
+
* // No need to modify execute() - the plugin handles upgrade methods automatically!
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export class UpgradeablePlugin extends Plugin {
|
|
45
|
+
// Method selectors
|
|
46
|
+
public static get SUBMIT_UPGRADE_SELECTOR(): Selector {
|
|
47
|
+
return encodeSelector('submitUpgrade(address)');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public static get APPLY_UPGRADE_SELECTOR(): Selector {
|
|
51
|
+
return encodeSelector('applyUpgrade(address)');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public static get CANCEL_UPGRADE_SELECTOR(): Selector {
|
|
55
|
+
return encodeSelector('cancelUpgrade()');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public static get PENDING_UPGRADE_SELECTOR(): Selector {
|
|
59
|
+
return encodeSelector('pendingUpgrade()');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public static get UPGRADE_DELAY_SELECTOR(): Selector {
|
|
63
|
+
return encodeSelector('upgradeDelay()');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private readonly _pendingUpgradeAddress: StoredAddress;
|
|
67
|
+
private readonly _pendingUpgradeBlock: StoredU256;
|
|
68
|
+
private readonly _upgradeDelay: u64;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Creates a new UpgradeablePlugin.
|
|
72
|
+
*
|
|
73
|
+
* @param upgradeDelay - Number of blocks to wait before upgrade can be applied.
|
|
74
|
+
* Default: 144 blocks (~24 hours)
|
|
75
|
+
* Common values:
|
|
76
|
+
* - 6 blocks = ~1 hour
|
|
77
|
+
* - 144 blocks = ~24 hours
|
|
78
|
+
* - 1008 blocks = ~1 week
|
|
79
|
+
* @param addressPointer - Storage pointer for pending upgrade address
|
|
80
|
+
* @param blockPointer - Storage pointer for pending upgrade block
|
|
81
|
+
*/
|
|
82
|
+
public constructor(
|
|
83
|
+
upgradeDelay: u64 = 144,
|
|
84
|
+
addressPointer: u16 = Blockchain.nextPointer,
|
|
85
|
+
blockPointer: u16 = Blockchain.nextPointer,
|
|
86
|
+
) {
|
|
87
|
+
super();
|
|
88
|
+
this._upgradeDelay = upgradeDelay;
|
|
89
|
+
this._pendingUpgradeAddress = new StoredAddress(addressPointer);
|
|
90
|
+
this._pendingUpgradeBlock = new StoredU256(blockPointer, EMPTY_POINTER);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Returns the pending upgrade source address.
|
|
95
|
+
*/
|
|
96
|
+
public get pendingUpgradeAddress(): Address {
|
|
97
|
+
return this._pendingUpgradeAddress.value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Returns the block number when the pending upgrade was submitted.
|
|
102
|
+
*/
|
|
103
|
+
public get pendingUpgradeBlock(): u64 {
|
|
104
|
+
return this._pendingUpgradeBlock.value.lo1;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Returns the configured upgrade delay in blocks.
|
|
109
|
+
*/
|
|
110
|
+
public get upgradeDelay(): u64 {
|
|
111
|
+
return this._upgradeDelay;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Returns the block number when the pending upgrade can be applied.
|
|
116
|
+
*/
|
|
117
|
+
public get upgradeEffectiveBlock(): u64 {
|
|
118
|
+
const submitBlock = this.pendingUpgradeBlock;
|
|
119
|
+
if (submitBlock === 0) return 0;
|
|
120
|
+
return submitBlock + this._upgradeDelay;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Returns true if there is a pending upgrade.
|
|
125
|
+
*/
|
|
126
|
+
public get hasPendingUpgrade(): bool {
|
|
127
|
+
return this.pendingUpgradeBlock !== 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Returns true if the pending upgrade can be applied (delay has passed).
|
|
132
|
+
*/
|
|
133
|
+
public get canApplyUpgrade(): bool {
|
|
134
|
+
if (!this.hasPendingUpgrade) return false;
|
|
135
|
+
return Blockchain.block.number >= this.upgradeEffectiveBlock;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Attempts to execute an upgrade-related method.
|
|
140
|
+
* Returns the response if the method was handled, or null if not.
|
|
141
|
+
*
|
|
142
|
+
* @param method - The method selector
|
|
143
|
+
* @param calldata - The calldata
|
|
144
|
+
* @returns BytesWriter response if handled, null otherwise
|
|
145
|
+
*/
|
|
146
|
+
public override execute(method: Selector, calldata: Calldata): BytesWriter | null {
|
|
147
|
+
switch (method) {
|
|
148
|
+
case UpgradeablePlugin.SUBMIT_UPGRADE_SELECTOR:
|
|
149
|
+
return this.submitUpgrade(calldata);
|
|
150
|
+
case UpgradeablePlugin.APPLY_UPGRADE_SELECTOR:
|
|
151
|
+
return this.applyUpgrade(calldata);
|
|
152
|
+
case UpgradeablePlugin.CANCEL_UPGRADE_SELECTOR:
|
|
153
|
+
return this.cancelUpgrade();
|
|
154
|
+
case UpgradeablePlugin.PENDING_UPGRADE_SELECTOR:
|
|
155
|
+
return this.getPendingUpgrade();
|
|
156
|
+
case UpgradeablePlugin.UPGRADE_DELAY_SELECTOR:
|
|
157
|
+
return this.getUpgradeDelay();
|
|
158
|
+
default:
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Submits an upgrade for timelock.
|
|
165
|
+
*/
|
|
166
|
+
private submitUpgrade(calldata: Calldata): BytesWriter {
|
|
167
|
+
this.onlyDeployer();
|
|
168
|
+
|
|
169
|
+
if (this.hasPendingUpgrade) {
|
|
170
|
+
throw new Revert('Upgrade already pending. Cancel first.');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const sourceAddress = calldata.readAddress();
|
|
174
|
+
|
|
175
|
+
if (!Blockchain.isContract(sourceAddress)) {
|
|
176
|
+
throw new Revert('Source must be a deployed contract');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const currentBlock = Blockchain.block.number;
|
|
180
|
+
this._pendingUpgradeAddress.value = sourceAddress;
|
|
181
|
+
this._pendingUpgradeBlock.value = u256.fromU64(currentBlock);
|
|
182
|
+
|
|
183
|
+
const effectiveBlock = currentBlock + this._upgradeDelay;
|
|
184
|
+
Blockchain.emit(new UpgradeSubmittedEvent(sourceAddress, currentBlock, effectiveBlock));
|
|
185
|
+
|
|
186
|
+
return new BytesWriter(0);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Applies a pending upgrade after the timelock period has passed.
|
|
191
|
+
* Any remaining calldata after the source address is passed to onUpdate.
|
|
192
|
+
*/
|
|
193
|
+
private applyUpgrade(calldata: Calldata): BytesWriter {
|
|
194
|
+
this.onlyDeployer();
|
|
195
|
+
|
|
196
|
+
if (!this.hasPendingUpgrade) {
|
|
197
|
+
throw new Revert('No pending upgrade');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!this.canApplyUpgrade) {
|
|
201
|
+
throw new Revert('Upgrade delay not elapsed');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const sourceAddress = calldata.readAddress();
|
|
205
|
+
const pendingAddress = this._pendingUpgradeAddress.value;
|
|
206
|
+
|
|
207
|
+
if (!sourceAddress.equals(pendingAddress)) {
|
|
208
|
+
throw new Revert('Address does not match pending upgrade');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Clear pending state before upgrade
|
|
212
|
+
this._pendingUpgradeAddress.value = Address.zero();
|
|
213
|
+
this._pendingUpgradeBlock.value = u256.Zero;
|
|
214
|
+
|
|
215
|
+
Blockchain.emit(new UpgradeAppliedEvent(sourceAddress, Blockchain.block.number));
|
|
216
|
+
|
|
217
|
+
// Extract remaining calldata for onUpdate
|
|
218
|
+
const remainingLength = calldata.byteLength - calldata.getOffset();
|
|
219
|
+
const updateCalldata = new BytesWriter(remainingLength);
|
|
220
|
+
if (remainingLength > 0) {
|
|
221
|
+
const remainingBytes = calldata.readBytes(remainingLength);
|
|
222
|
+
updateCalldata.writeBytes(remainingBytes);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Perform upgrade - new bytecode takes effect next block
|
|
226
|
+
Blockchain.updateContractFromExisting(sourceAddress, updateCalldata);
|
|
227
|
+
|
|
228
|
+
return new BytesWriter(0);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Cancels a pending upgrade.
|
|
233
|
+
*/
|
|
234
|
+
private cancelUpgrade(): BytesWriter {
|
|
235
|
+
this.onlyDeployer();
|
|
236
|
+
|
|
237
|
+
if (!this.hasPendingUpgrade) {
|
|
238
|
+
throw new Revert('No pending upgrade');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const pendingAddress = this._pendingUpgradeAddress.value;
|
|
242
|
+
|
|
243
|
+
this._pendingUpgradeAddress.value = Address.zero();
|
|
244
|
+
this._pendingUpgradeBlock.value = u256.Zero;
|
|
245
|
+
|
|
246
|
+
Blockchain.emit(new UpgradeCancelledEvent(pendingAddress, Blockchain.block.number));
|
|
247
|
+
|
|
248
|
+
return new BytesWriter(0);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Returns the pending upgrade info.
|
|
253
|
+
*/
|
|
254
|
+
private getPendingUpgrade(): BytesWriter {
|
|
255
|
+
const response = new BytesWriter(ADDRESS_BYTE_LENGTH + 16);
|
|
256
|
+
response.writeAddress(this._pendingUpgradeAddress.value);
|
|
257
|
+
response.writeU64(this.pendingUpgradeBlock);
|
|
258
|
+
response.writeU64(this.upgradeEffectiveBlock);
|
|
259
|
+
return response;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Returns the upgrade delay.
|
|
264
|
+
*/
|
|
265
|
+
private getUpgradeDelay(): BytesWriter {
|
|
266
|
+
const response = new BytesWriter(8);
|
|
267
|
+
response.writeU64(this._upgradeDelay);
|
|
268
|
+
return response;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Validates that the caller is the contract deployer.
|
|
273
|
+
*/
|
|
274
|
+
private onlyDeployer(): void {
|
|
275
|
+
if (Blockchain.contractDeployer !== Blockchain.tx.sender) {
|
|
276
|
+
throw new Revert('Only deployer can call this method');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -83,7 +83,7 @@ export abstract class BaseStoredString {
|
|
|
83
83
|
let chunkCount: u64 = 1;
|
|
84
84
|
|
|
85
85
|
// In the header slot, we can store up to 28 bytes of data.
|
|
86
|
-
const remaining = oldLength > 28 ? oldLength - 28 : 0;
|
|
86
|
+
const remaining: u64 = oldLength > 28 ? oldLength - 28 : 0;
|
|
87
87
|
if (remaining > 0) {
|
|
88
88
|
// Each additional chunk is 32 bytes.
|
|
89
89
|
// Use integer math ceiling: (remaining + 32 - 1) / 32
|
|
@@ -353,6 +353,10 @@ export abstract class StoredPackedArray<T> {
|
|
|
353
353
|
|
|
354
354
|
@inline
|
|
355
355
|
public setMultiple(startIndex: u32, values: T[]): void {
|
|
356
|
+
if (startIndex > u32.MAX_VALUE - <u32>values.length) {
|
|
357
|
+
throw new Revert('setMultiple: end index overflow (packed array)');
|
|
358
|
+
}
|
|
359
|
+
|
|
356
360
|
const end = startIndex + <u32>values.length;
|
|
357
361
|
if (end > this._length) {
|
|
358
362
|
throw new Revert('setMultiple: out of range (packed array)');
|
|
@@ -4,6 +4,14 @@ import { Blockchain } from '../env';
|
|
|
4
4
|
import { Network } from '../script/Networks';
|
|
5
5
|
import { Address } from './Address';
|
|
6
6
|
import { BitcoinAddresses } from '../script/BitcoinAddresses';
|
|
7
|
+
import {
|
|
8
|
+
DEAD_ARRAY,
|
|
9
|
+
getCachedDeadAddress,
|
|
10
|
+
getCachedZeroAddress,
|
|
11
|
+
setCachedDeadAddress,
|
|
12
|
+
setCachedZeroAddress,
|
|
13
|
+
ZERO_ARRAY,
|
|
14
|
+
} from './ExtendedAddressCache';
|
|
7
15
|
|
|
8
16
|
/**
|
|
9
17
|
* Extended address implementation for Bitcoin with dual-key support.
|
|
@@ -86,7 +94,13 @@ export class ExtendedAddress extends Address {
|
|
|
86
94
|
* ```
|
|
87
95
|
*/
|
|
88
96
|
public static dead(): ExtendedAddress {
|
|
89
|
-
|
|
97
|
+
let cached = getCachedDeadAddress();
|
|
98
|
+
if (cached === 0) {
|
|
99
|
+
const addr = new ExtendedAddress(DEAD_ARRAY, ZERO_ARRAY);
|
|
100
|
+
cached = changetype<usize>(addr);
|
|
101
|
+
setCachedDeadAddress(cached);
|
|
102
|
+
}
|
|
103
|
+
return changetype<ExtendedAddress>(cached).clone();
|
|
90
104
|
}
|
|
91
105
|
|
|
92
106
|
/**
|
|
@@ -103,7 +117,13 @@ export class ExtendedAddress extends Address {
|
|
|
103
117
|
* ```
|
|
104
118
|
*/
|
|
105
119
|
public static zero(): ExtendedAddress {
|
|
106
|
-
|
|
120
|
+
let cached = getCachedZeroAddress();
|
|
121
|
+
if (cached === 0) {
|
|
122
|
+
const addr = new ExtendedAddress(ZERO_ARRAY, ZERO_ARRAY);
|
|
123
|
+
cached = changetype<usize>(addr);
|
|
124
|
+
setCachedZeroAddress(cached);
|
|
125
|
+
}
|
|
126
|
+
return changetype<ExtendedAddress>(cached).clone();
|
|
107
127
|
}
|
|
108
128
|
|
|
109
129
|
/**
|
|
@@ -299,7 +319,7 @@ export class ExtendedAddress extends Address {
|
|
|
299
319
|
/**
|
|
300
320
|
* Checks if this address equals the canonical dead address.
|
|
301
321
|
*
|
|
302
|
-
* @returns `true` if this address matches
|
|
322
|
+
* @returns `true` if this address matches the dead address, `false` otherwise
|
|
303
323
|
*
|
|
304
324
|
* @example
|
|
305
325
|
* ```typescript
|
|
@@ -310,8 +330,18 @@ export class ExtendedAddress extends Address {
|
|
|
310
330
|
* ```
|
|
311
331
|
*/
|
|
312
332
|
public isDead(): bool {
|
|
333
|
+
// Use cached dead address for comparison
|
|
334
|
+
const deadAddr = ExtendedAddress.dead();
|
|
335
|
+
|
|
336
|
+
// Compare both ML-DSA key hash (this) and tweaked key
|
|
313
337
|
for (let i = 0; i < this.length; i++) {
|
|
314
|
-
if (this[i] !=
|
|
338
|
+
if (this[i] != deadAddr[i]) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
for (let i = 0; i < this.tweakedPublicKey.length; i++) {
|
|
344
|
+
if (this.tweakedPublicKey[i] != deadAddr.tweakedPublicKey[i]) {
|
|
315
345
|
return false;
|
|
316
346
|
}
|
|
317
347
|
}
|
|
@@ -393,32 +423,14 @@ export class ExtendedAddress extends Address {
|
|
|
393
423
|
* Pre-initialized zero ExtendedAddress constant.
|
|
394
424
|
* Both the tweaked key and ML-DSA key hash are all zeros.
|
|
395
425
|
*/
|
|
396
|
-
export const ZERO_BITCOIN_ADDRESS: ExtendedAddress = new ExtendedAddress(
|
|
397
|
-
[
|
|
398
|
-
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
399
|
-
0,
|
|
400
|
-
],
|
|
401
|
-
[
|
|
402
|
-
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
403
|
-
0,
|
|
404
|
-
],
|
|
405
|
-
);
|
|
426
|
+
export const ZERO_BITCOIN_ADDRESS: ExtendedAddress = new ExtendedAddress(ZERO_ARRAY, ZERO_ARRAY);
|
|
406
427
|
|
|
407
428
|
/**
|
|
408
429
|
* Pre-initialized dead ExtendedAddress constant.
|
|
409
430
|
* The tweaked key is zero while the ML-DSA key hash represents the canonical dead address.
|
|
410
431
|
* Hash: 284ae4acdb32a99ba3ebfa66a91ddb41a7b7a1d2fef415399922cd8a04485c02
|
|
411
432
|
*/
|
|
412
|
-
export const DEAD_ADDRESS: ExtendedAddress = new ExtendedAddress(
|
|
413
|
-
[
|
|
414
|
-
40, 74, 228, 172, 219, 50, 169, 155, 163, 235, 250, 102, 169, 29, 219, 65, 167, 183, 161,
|
|
415
|
-
210, 254, 244, 21, 57, 153, 34, 205, 138, 4, 72, 92, 2,
|
|
416
|
-
],
|
|
417
|
-
[
|
|
418
|
-
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
419
|
-
0,
|
|
420
|
-
],
|
|
421
|
-
);
|
|
433
|
+
export const DEAD_ADDRESS: ExtendedAddress = new ExtendedAddress(DEAD_ARRAY, ZERO_ARRAY);
|
|
422
434
|
|
|
423
435
|
/**
|
|
424
436
|
* Type alias for nullable ExtendedAddress references.
|