@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,242 @@
1
+ import { u256 } from '@btc-vision/as-bignum/assembly';
2
+ import { Blockchain } from '../env';
3
+ import { OP_NET } from './OP_NET';
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 { EMPTY_POINTER } from '../math/bytes';
10
+ import {
11
+ UpgradeSubmittedEvent,
12
+ UpgradeAppliedEvent,
13
+ UpgradeCancelledEvent,
14
+ } from '../events/upgradeable/UpgradeableEvents';
15
+
16
+ const pendingUpgradeAddressPointer: u16 = Blockchain.nextPointer;
17
+ const pendingUpgradeBlockPointer: u16 = Blockchain.nextPointer;
18
+
19
+ /**
20
+ * Upgradeable - Base contract for upgradeable contracts with timelock protection.
21
+ *
22
+ * This contract provides a secure upgrade mechanism with a configurable delay period.
23
+ * The pattern prevents instant malicious upgrades by requiring:
24
+ * 1. submitUpgrade() - Submit the source contract address, starts the timelock
25
+ * 2. Wait for the delay period to pass
26
+ * 3. applyUpgrade() - Apply the upgrade after the delay
27
+ *
28
+ * Users can monitor for UpgradeSubmitted events and exit if they distrust pending changes.
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * @final
33
+ * export class MyUpgradeableContract extends Upgradeable {
34
+ * // Set a 24-hour delay (144 blocks at 10 min/block)
35
+ * protected readonly upgradeDelay: u64 = 144;
36
+ *
37
+ * public override execute(method: Selector, calldata: Calldata): BytesWriter {
38
+ * switch (method) {
39
+ * case encodeSelector('submitUpgrade'):
40
+ * return this.submitUpgrade(calldata.readAddress());
41
+ * case encodeSelector('applyUpgrade'):
42
+ * const sourceAddress = calldata.readAddress();
43
+ * const updateCalldata = new BytesWriter(calldata.byteLength - ADDRESS_BYTE_LENGTH);
44
+ * // Copy remaining calldata for onUpdate
45
+ * return this.applyUpgrade(sourceAddress, updateCalldata);
46
+ * case encodeSelector('cancelUpgrade'):
47
+ * return this.cancelUpgrade();
48
+ * default:
49
+ * return super.execute(method, calldata);
50
+ * }
51
+ * }
52
+ * }
53
+ * ```
54
+ */
55
+ export class Upgradeable extends OP_NET {
56
+ /**
57
+ * The pending upgrade source address.
58
+ * Zero address means no pending upgrade.
59
+ */
60
+ protected readonly _pendingUpgradeAddress: StoredAddress;
61
+
62
+ /**
63
+ * The block number when the upgrade was submitted.
64
+ * Stored as u256, used as u64.
65
+ */
66
+ protected readonly _pendingUpgradeBlock: StoredU256;
67
+
68
+ /**
69
+ * The number of blocks to wait before an upgrade can be applied.
70
+ * Override this in derived contracts to set the delay.
71
+ *
72
+ * Common values:
73
+ * - 6 blocks = ~1 hour
74
+ * - 144 blocks = ~24 hours
75
+ * - 1008 blocks = ~1 week
76
+ */
77
+ protected readonly upgradeDelay: u64 = 144; // ~24 hours default
78
+
79
+ protected constructor() {
80
+ super();
81
+ this._pendingUpgradeAddress = new StoredAddress(pendingUpgradeAddressPointer);
82
+ this._pendingUpgradeBlock = new StoredU256(pendingUpgradeBlockPointer, EMPTY_POINTER);
83
+ }
84
+
85
+ /**
86
+ * Returns the pending upgrade source address.
87
+ * Returns zero address if no upgrade is pending.
88
+ */
89
+ public get pendingUpgradeAddress(): Address {
90
+ return this._pendingUpgradeAddress.value;
91
+ }
92
+
93
+ /**
94
+ * Returns the block number when the pending upgrade was submitted.
95
+ * Returns 0 if no upgrade is pending.
96
+ */
97
+ public get pendingUpgradeBlock(): u64 {
98
+ return this._pendingUpgradeBlock.value.lo1;
99
+ }
100
+
101
+ /**
102
+ * Returns the block number when the pending upgrade can be applied.
103
+ * Returns 0 if no upgrade is pending.
104
+ */
105
+ public get upgradeEffectiveBlock(): u64 {
106
+ const submitBlock = this.pendingUpgradeBlock;
107
+ if (submitBlock === 0) return 0;
108
+ return submitBlock + this.upgradeDelay;
109
+ }
110
+
111
+ /**
112
+ * Returns true if there is a pending upgrade.
113
+ */
114
+ public get hasPendingUpgrade(): bool {
115
+ return this.pendingUpgradeBlock !== 0;
116
+ }
117
+
118
+ /**
119
+ * Returns true if the pending upgrade can be applied (delay has passed).
120
+ */
121
+ public get canApplyUpgrade(): bool {
122
+ if (!this.hasPendingUpgrade) return false;
123
+ return Blockchain.block.number >= this.upgradeEffectiveBlock;
124
+ }
125
+
126
+ /**
127
+ * Submits an upgrade for timelock.
128
+ *
129
+ * The source address must be a deployed contract containing the new bytecode.
130
+ * After submission, the upgrade can only be applied after upgradeDelay blocks.
131
+ *
132
+ * Emits UpgradeSubmitted event.
133
+ *
134
+ * @param sourceAddress - The source contract address containing new bytecode
135
+ * @returns Empty response
136
+ * @throws If caller is not deployer
137
+ * @throws If source is not a deployed contract
138
+ * @throws If an upgrade is already pending
139
+ */
140
+ protected submitUpgrade(sourceAddress: Address): BytesWriter {
141
+ this.onlyDeployer(Blockchain.tx.sender);
142
+
143
+ // Check no pending upgrade
144
+ if (this.hasPendingUpgrade) {
145
+ throw new Revert('Upgrade already pending. Cancel first.');
146
+ }
147
+
148
+ // Validate source is a deployed contract
149
+ if (!Blockchain.isContract(sourceAddress)) {
150
+ throw new Revert('Source must be a deployed contract');
151
+ }
152
+
153
+ // Store pending upgrade
154
+ const currentBlock = Blockchain.block.number;
155
+ this._pendingUpgradeAddress.value = sourceAddress;
156
+ this._pendingUpgradeBlock.value = u256.fromU64(currentBlock);
157
+
158
+ // Emit event
159
+ const effectiveBlock = currentBlock + this.upgradeDelay;
160
+ Blockchain.emit(new UpgradeSubmittedEvent(sourceAddress, currentBlock, effectiveBlock));
161
+
162
+ return new BytesWriter(0);
163
+ }
164
+
165
+ /**
166
+ * Applies a pending upgrade after the timelock period has passed.
167
+ *
168
+ * The provided address must match the pending upgrade address as an
169
+ * additional security measure against front-running attacks.
170
+ *
171
+ * Emits UpgradeApplied event before the upgrade (new bytecode takes effect next block).
172
+ *
173
+ * @param sourceAddress - The source contract address (must match pending)
174
+ * @param calldata - The calldata to pass to onUpdate method of the new contract
175
+ * @returns Empty response
176
+ * @throws If caller is not deployer
177
+ * @throws If no upgrade is pending
178
+ * @throws If delay has not passed
179
+ * @throws If provided address does not match pending
180
+ */
181
+ protected applyUpgrade(sourceAddress: Address, calldata: BytesWriter): BytesWriter {
182
+ this.onlyDeployer(Blockchain.tx.sender);
183
+
184
+ // Check pending upgrade exists
185
+ if (!this.hasPendingUpgrade) {
186
+ throw new Revert('No pending upgrade');
187
+ }
188
+
189
+ // Check delay has passed
190
+ if (!this.canApplyUpgrade) {
191
+ throw new Revert('Upgrade delay not elapsed');
192
+ }
193
+
194
+ // Verify address matches pending
195
+ if (!sourceAddress.equals(this._pendingUpgradeAddress.value)) {
196
+ throw new Revert('Address does not match pending upgrade');
197
+ }
198
+
199
+ // Clear pending state before upgrade
200
+ this._pendingUpgradeAddress.value = Address.zero();
201
+ this._pendingUpgradeBlock.value = u256.Zero;
202
+
203
+ // Emit event
204
+ Blockchain.emit(new UpgradeAppliedEvent(sourceAddress, Blockchain.block.number));
205
+
206
+ // Perform upgrade - new bytecode takes effect next block
207
+ Blockchain.updateContractFromExisting(sourceAddress, calldata);
208
+
209
+ return new BytesWriter(0);
210
+ }
211
+
212
+ /**
213
+ * Cancels a pending upgrade.
214
+ *
215
+ * Can only be called by the deployer. Clears the pending upgrade state.
216
+ *
217
+ * Emits UpgradeCancelled event.
218
+ *
219
+ * @returns Empty response
220
+ * @throws If caller is not deployer
221
+ * @throws If no upgrade is pending
222
+ */
223
+ protected cancelUpgrade(): BytesWriter {
224
+ this.onlyDeployer(Blockchain.tx.sender);
225
+
226
+ // Check pending upgrade exists
227
+ if (!this.hasPendingUpgrade) {
228
+ throw new Revert('No pending upgrade');
229
+ }
230
+
231
+ const pendingAddress = this._pendingUpgradeAddress.value;
232
+
233
+ // Clear pending state
234
+ this._pendingUpgradeAddress.value = Address.zero();
235
+ this._pendingUpgradeBlock.value = u256.Zero;
236
+
237
+ // Emit event
238
+ Blockchain.emit(new UpgradeCancelledEvent(pendingAddress, Blockchain.block.number));
239
+
240
+ return new BytesWriter(0);
241
+ }
242
+ }
@@ -22,12 +22,12 @@ import {
22
22
  storePointer,
23
23
  tLoadPointer,
24
24
  tStorePointer,
25
+ updateFromAddress,
25
26
  validateBitcoinAddress,
26
27
  verifySignature,
27
28
  } from './global';
28
29
  import { eqUint, MapUint8Array } from '../generic/MapUint8Array';
29
30
  import { EMPTY_BUFFER } from '../math/bytes';
30
- import { Plugin } from '../plugins/Plugin';
31
31
  import { Calldata } from '../types';
32
32
  import { Revert } from '../types/Revert';
33
33
  import { Selector } from '../math/abi';
@@ -75,7 +75,6 @@ export class BlockchainEnvironment {
75
75
  private storage: MapUint8Array = new MapUint8Array();
76
76
  private transientStorage: MapUint8Array = new MapUint8Array();
77
77
  private _selfContract: Potential<OP_NET> = null;
78
- private _plugins: Plugin[] = [];
79
78
  private _network: Networks = Networks.Unknown;
80
79
 
81
80
  /**
@@ -268,32 +267,29 @@ export class BlockchainEnvironment {
268
267
  }
269
268
 
270
269
  /**
271
- * Registers a plugin to extend contract functionality.
270
+ * Handles contract deployment initialization.
272
271
  *
273
- * @param plugin - Plugin instance to register
272
+ * @param calldata - Deployment parameters
274
273
  *
275
274
  * @remarks
276
- * Plugins execute in registration order and have full access to contract state.
275
+ * Called once during deployment. Delegates to the contract's onDeployment
276
+ * which handles plugin notification.
277
277
  */
278
- public registerPlugin(plugin: Plugin): void {
279
- this._plugins.push(plugin);
278
+ public onDeployment(calldata: Calldata): void {
279
+ this.contract.onDeployment(calldata);
280
280
  }
281
281
 
282
282
  /**
283
- * Handles contract deployment initialization.
283
+ * Handles contract bytecode update.
284
284
  *
285
- * @param calldata - Deployment parameters
285
+ * @param calldata - Update parameters passed to updateContractFromExisting
286
286
  *
287
287
  * @remarks
288
- * Called once during deployment. State changes here are permanent.
288
+ * Called when the contract's bytecode is updated. Delegates to the contract's
289
+ * onUpdate which handles plugin notification and migration logic.
289
290
  */
290
- public onDeployment(calldata: Calldata): void {
291
- const len = this._plugins.length;
292
- for (let i: i32 = 0; i < len; i++) {
293
- // Unchecked access for speed
294
- unchecked(this._plugins[i].onDeployment(calldata));
295
- }
296
- this.contract.onDeployment(calldata);
291
+ public onUpdate(calldata: Calldata): void {
292
+ this.contract.onUpdate(calldata);
297
293
  }
298
294
 
299
295
  /**
@@ -303,13 +299,9 @@ export class BlockchainEnvironment {
303
299
  * @param calldata - Method parameters
304
300
  *
305
301
  * @remarks
306
- * Used for access control, reentrancy guards, and validation.
302
+ * Delegates to the contract's onExecutionStarted which handles plugin notification.
307
303
  */
308
304
  public onExecutionStarted(selector: Selector, calldata: Calldata): void {
309
- const len = this._plugins.length;
310
- for (let i: i32 = 0; i < len; i++) {
311
- unchecked(this._plugins[i].onExecutionStarted(selector, calldata));
312
- }
313
305
  this.contract.onExecutionStarted(selector, calldata);
314
306
  }
315
307
 
@@ -320,13 +312,9 @@ export class BlockchainEnvironment {
320
312
  * @param calldata - Method parameters that were passed
321
313
  *
322
314
  * @remarks
323
- * Only called on successful execution. Used for cleanup and events.
315
+ * Delegates to the contract's onExecutionCompleted which handles plugin notification.
324
316
  */
325
317
  public onExecutionCompleted(selector: Selector, calldata: Calldata): void {
326
- const len = this._plugins.length;
327
- for (let i: i32 = 0; i < len; i++) {
328
- unchecked(this._plugins[i].onExecutionCompleted(selector, calldata));
329
- }
330
318
  this.contract.onExecutionCompleted(selector, calldata);
331
319
  }
332
320
 
@@ -591,6 +579,115 @@ export class BlockchainEnvironment {
591
579
  return contractAddressReader.readAddress();
592
580
  }
593
581
 
582
+ /**
583
+ * Updates this contract's bytecode from an existing deployed contract.
584
+ *
585
+ * This method triggers a bytecode replacement where the calling contract's execution
586
+ * logic is replaced with the bytecode from the source contract. The new bytecode
587
+ * takes effect at the next block.
588
+ *
589
+ * @param sourceAddress - Address of the contract containing the new bytecode
590
+ * @param [calldata] - Optional parameters passed to the new bytecode's onUpdate method
591
+ * @throws {Revert} When the source address is invalid or the update fails
592
+ *
593
+ * @warning This is a privileged operation with significant implications:
594
+ * - Storage layout compatibility is entirely the developer's responsibility
595
+ * - The contract address and all storage slots persist unchanged
596
+ * - Only the execution logic changes
597
+ * - The source contract must be an already-deployed contract
598
+ *
599
+ * @remarks
600
+ * Contracts should implement their own permission checks and optional timelock
601
+ * patterns before calling this method. A recommended pattern is:
602
+ *
603
+ * 1. `submitUpdate(address)` - Logs source address and block number, validates
604
+ * that the address is an existing deployed contract
605
+ * 2. `applyUpdate(address)` - Can only be called after a configured delay,
606
+ * verifies address matches the submitted one, then calls this method
607
+ *
608
+ * This pattern gives users time to assess pending changes and exit if needed.
609
+ *
610
+ * @example
611
+ * ```typescript
612
+ * // Simple immediate update (not recommended for production)
613
+ * public upgrade(calldata: Calldata): BytesWriter {
614
+ * this.onlyDeployer(Blockchain.tx.sender);
615
+ * const newBytecodeAddress = calldata.readAddress();
616
+ * Blockchain.updateContractFromExisting(newBytecodeAddress);
617
+ * return new BytesWriter(0);
618
+ * }
619
+ * ```
620
+ *
621
+ * @example
622
+ * ```typescript
623
+ * // Timelock pattern (recommended)
624
+ * private pendingUpdatePointer: u16 = Blockchain.nextPointer;
625
+ * private pendingUpdate: StoredAddress = new StoredAddress(this.pendingUpdatePointer);
626
+ *
627
+ * private pendingUpdateBlockPointer: u16 = Blockchain.nextPointer;
628
+ * private pendingUpdateBlock: StoredU64 = new StoredU64(this.pendingUpdateBlockPointer);
629
+ *
630
+ * private readonly UPDATE_DELAY: u64 = 144; // ~1 day in blocks
631
+ *
632
+ * public submitUpdate(calldata: Calldata): BytesWriter {
633
+ * this.onlyDeployer(Blockchain.tx.sender);
634
+ * const sourceAddress = calldata.readAddress();
635
+ *
636
+ * // Validate source is an existing contract
637
+ * if (!Blockchain.isContract(sourceAddress)) {
638
+ * throw new Revert('Source must be a deployed contract');
639
+ * }
640
+ *
641
+ * this.pendingUpdate.value = sourceAddress;
642
+ * this.pendingUpdateBlock.value = Blockchain.block.number;
643
+ * return new BytesWriter(0);
644
+ * }
645
+ *
646
+ * public applyUpdate(calldata: Calldata): BytesWriter {
647
+ * this.onlyDeployer(Blockchain.tx.sender);
648
+ * const sourceAddress = calldata.readAddress();
649
+ *
650
+ * // Verify address matches pending update
651
+ * if (!sourceAddress.equals(this.pendingUpdate.value)) {
652
+ * throw new Revert('Address does not match pending update');
653
+ * }
654
+ *
655
+ * // Verify delay has passed
656
+ * const submitBlock = this.pendingUpdateBlock.value;
657
+ * if (Blockchain.block.number < submitBlock + this.UPDATE_DELAY) {
658
+ * throw new Revert('Update delay not elapsed');
659
+ * }
660
+ *
661
+ * Blockchain.updateContractFromExisting(sourceAddress);
662
+ * return new BytesWriter(0);
663
+ * }
664
+ * ```
665
+ */
666
+ public updateContractFromExisting(
667
+ sourceAddress: Address,
668
+ calldata: BytesWriter | null = null,
669
+ ): void {
670
+ if (!sourceAddress) {
671
+ throw new Revert('Source address is required');
672
+ }
673
+
674
+ if (!this.isContract(sourceAddress)) {
675
+ throw new Revert('Source address must be a deployed contract');
676
+ }
677
+
678
+ const callDataBuffer = calldata ? calldata.getBuffer().buffer : new ArrayBuffer(0);
679
+
680
+ const status = updateFromAddress(
681
+ sourceAddress.buffer,
682
+ callDataBuffer,
683
+ callDataBuffer.byteLength,
684
+ );
685
+
686
+ if (status !== 0) {
687
+ throw new Revert('Failed to update contract bytecode');
688
+ }
689
+ }
690
+
594
691
  /**
595
692
  * Reads a value from persistent storage.
596
693
  *
@@ -65,6 +65,30 @@ export declare function tStorePointer(key: ArrayBuffer, value: ArrayBuffer): voi
65
65
  @external('env', 'deployFromAddress')
66
66
  export declare function deployFromAddress(originAddress: ArrayBuffer, salt: ArrayBuffer, calldata: ArrayBuffer, calldataLength: u32, resultAddress: ArrayBuffer): u32;
67
67
 
68
+ /**
69
+ * Updates the calling contract's bytecode from an existing contract.
70
+ *
71
+ * This VM opcode enables bytecode replacement where a contract can replace its own
72
+ * execution logic by referencing another deployed contract containing the new WASM bytecode.
73
+ * The new bytecode takes effect at the next block.
74
+ *
75
+ * @param {ArrayBuffer} sourceAddress - The address of the contract containing the new bytecode.
76
+ * @param {ArrayBuffer} calldata - The calldata for the update (passed to onUpdate if implemented).
77
+ * @param {u32} calldataLength - The length of the calldata.
78
+ * @returns {u32} - Status code (0 = success, non-zero = failure).
79
+ *
80
+ * @remarks
81
+ * - The source contract must be an already-deployed contract
82
+ * - Storage layout compatibility is the developer's responsibility
83
+ * - The contract address and all storage slots persist unchanged
84
+ * - Only the execution logic changes
85
+ *
86
+ * @warning This is a privileged operation. Contracts should implement their own
87
+ * permission checks and optional timelock patterns before calling this.
88
+ */
89
+ @external('env', 'updateFromAddress')
90
+ export declare function updateFromAddress(sourceAddress: ArrayBuffer, calldata: ArrayBuffer, calldataLength: u32): u32;
91
+
68
92
  /**
69
93
  * Calls a contract.
70
94
  * @param {ArrayBuffer} address - The address of the contract to call.
@@ -0,0 +1,41 @@
1
+ import { NetEvent } from '../NetEvent';
2
+ import { BytesWriter } from '../../buffer/BytesWriter';
3
+ import { Address } from '../../types/Address';
4
+ import { ADDRESS_BYTE_LENGTH } from '../../utils';
5
+
6
+ /**
7
+ * Event emitted when an upgrade is submitted for timelock.
8
+ */
9
+ export class UpgradeSubmittedEvent extends NetEvent {
10
+ constructor(sourceAddress: Address, submitBlock: u64, effectiveBlock: u64) {
11
+ const data = new BytesWriter(ADDRESS_BYTE_LENGTH + 16);
12
+ data.writeAddress(sourceAddress);
13
+ data.writeU64(submitBlock);
14
+ data.writeU64(effectiveBlock);
15
+ super('UpgradeSubmitted', data);
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Event emitted when an upgrade is applied.
21
+ */
22
+ export class UpgradeAppliedEvent extends NetEvent {
23
+ constructor(sourceAddress: Address, appliedAtBlock: u64) {
24
+ const data = new BytesWriter(ADDRESS_BYTE_LENGTH + 8);
25
+ data.writeAddress(sourceAddress);
26
+ data.writeU64(appliedAtBlock);
27
+ super('UpgradeApplied', data);
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Event emitted when a pending upgrade is cancelled.
33
+ */
34
+ export class UpgradeCancelledEvent extends NetEvent {
35
+ constructor(sourceAddress: Address, cancelledAtBlock: u64) {
36
+ const data = new BytesWriter(ADDRESS_BYTE_LENGTH + 8);
37
+ data.writeAddress(sourceAddress);
38
+ data.writeU64(cancelledAtBlock);
39
+ super('UpgradeCancelled', data);
40
+ }
41
+ }
@@ -26,23 +26,7 @@ export class AddressMap<V> implements IMap<Address, V> {
26
26
  }
27
27
 
28
28
  public set(key: Address, value: V): this {
29
- let index = -1;
30
-
31
- // Check Cache (Fastest)
32
- if (this._lastIndex !== -1) {
33
- const cachedKey = unchecked(this._keys[this._lastIndex]);
34
- // Check length first, then full content equality
35
- if (cachedKey.length === key.length) {
36
- if (memory.compare(cachedKey.dataStart, key.dataStart, key.length) === 0) {
37
- index = this._lastIndex;
38
- }
39
- }
40
- }
41
-
42
- // Full Scan if cache missed
43
- if (index === -1) {
44
- index = this.indexOf(key);
45
- }
29
+ const index = this.indexOf(key);
46
30
 
47
31
  if (index === -1) {
48
32
  this._keys.push(key);
@@ -50,7 +34,7 @@ export class AddressMap<V> implements IMap<Address, V> {
50
34
  this._lastIndex = this._keys.length - 1;
51
35
  } else {
52
36
  unchecked((this._values[index] = value));
53
- // Cache is already pointing to this index (from indexOf or the check above)
37
+ // Cache is already pointing to this index (from indexOf)
54
38
  this._lastIndex = index;
55
39
  }
56
40
 
@@ -63,6 +47,10 @@ export class AddressMap<V> implements IMap<Address, V> {
63
47
  * Uses a "Prefix Filter" to skip expensive memory comparisons.
64
48
  */
65
49
  public indexOf(pointerHash: Address): i32 {
50
+ if (this.isLastIndex(pointerHash)) {
51
+ return this._lastIndex;
52
+ }
53
+
66
54
  const len = this._keys.length;
67
55
  if (len === 0) return -1;
68
56
 
@@ -111,6 +99,20 @@ export class AddressMap<V> implements IMap<Address, V> {
111
99
  return -1;
112
100
  }
113
101
 
102
+ private isLastIndex(key: Uint8Array): bool {
103
+ if (this._lastIndex !== -1) {
104
+ const cachedKey = unchecked(this._keys[this._lastIndex]);
105
+ // Check length first, then full content equality
106
+ if (cachedKey.length === key.length) {
107
+ if (memory.compare(cachedKey.dataStart, key.dataStart, key.length) === 0) {
108
+ return true;
109
+ }
110
+ }
111
+ }
112
+
113
+ return false;
114
+ }
115
+
114
116
  @inline
115
117
  public has(key: Address): bool {
116
118
  return this.indexOf(key) !== -1;