@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.
Files changed (41) hide show
  1. package/README.md +48 -224
  2. package/SECURITY.md +38 -191
  3. package/docs/README.md +28 -0
  4. package/docs/advanced/contract-upgrades.md +537 -0
  5. package/docs/advanced/plugins.md +90 -25
  6. package/docs/api-reference/blockchain.md +48 -14
  7. package/docs/api-reference/storage.md +2 -111
  8. package/docs/contracts/op-net-base.md +22 -0
  9. package/docs/contracts/upgradeable.md +396 -0
  10. package/docs/core-concepts/blockchain-environment.md +0 -2
  11. package/docs/core-concepts/security.md +8 -111
  12. package/docs/core-concepts/storage-system.md +1 -32
  13. package/docs/examples/nft-with-reservations.md +8 -238
  14. package/docs/storage/memory-maps.md +1 -44
  15. package/docs/storage/stored-arrays.md +1 -65
  16. package/docs/storage/stored-maps.md +1 -73
  17. package/docs/storage/stored-primitives.md +2 -49
  18. package/docs/types/bytes-writer-reader.md +76 -0
  19. package/docs/types/safe-math.md +2 -45
  20. package/package.json +5 -5
  21. package/runtime/buffer/BytesReader.ts +90 -3
  22. package/runtime/buffer/BytesWriter.ts +81 -3
  23. package/runtime/contracts/OP721.ts +40 -4
  24. package/runtime/contracts/OP_NET.ts +83 -11
  25. package/runtime/contracts/Upgradeable.ts +242 -0
  26. package/runtime/env/BlockchainEnvironment.ts +124 -27
  27. package/runtime/env/global.ts +24 -0
  28. package/runtime/events/upgradeable/UpgradeableEvents.ts +41 -0
  29. package/runtime/generic/AddressMap.ts +20 -18
  30. package/runtime/generic/ExtendedAddressMap.ts +147 -0
  31. package/runtime/generic/MapUint8Array.ts +20 -18
  32. package/runtime/index.ts +8 -0
  33. package/runtime/plugins/Plugin.ts +34 -0
  34. package/runtime/plugins/UpgradeablePlugin.ts +279 -0
  35. package/runtime/storage/BaseStoredString.ts +1 -1
  36. package/runtime/storage/arrays/StoredPackedArray.ts +4 -0
  37. package/runtime/types/ExtendedAddress.ts +36 -24
  38. package/runtime/types/ExtendedAddressCache.ts +27 -0
  39. package/runtime/types/SafeMath.ts +109 -18
  40. package/runtime/types/SchnorrSignature.ts +44 -0
  41. 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 = -1;
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 or the check above)
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
- return DEAD_ADDRESS.clone();
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
- return ZERO_BITCOIN_ADDRESS.clone();
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 DEAD_ADDRESS, `false` otherwise
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] != DEAD_ADDRESS[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.