@aztec/simulator 0.68.0 → 0.68.1

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 (66) hide show
  1. package/dest/avm/avm_memory_types.d.ts.map +1 -1
  2. package/dest/avm/avm_memory_types.js +21 -14
  3. package/dest/avm/avm_simulator.d.ts +1 -0
  4. package/dest/avm/avm_simulator.d.ts.map +1 -1
  5. package/dest/avm/avm_simulator.js +35 -18
  6. package/dest/avm/avm_tree.d.ts +0 -22
  7. package/dest/avm/avm_tree.d.ts.map +1 -1
  8. package/dest/avm/avm_tree.js +22 -81
  9. package/dest/avm/errors.d.ts +8 -1
  10. package/dest/avm/errors.d.ts.map +1 -1
  11. package/dest/avm/errors.js +13 -3
  12. package/dest/avm/journal/journal.d.ts +0 -4
  13. package/dest/avm/journal/journal.d.ts.map +1 -1
  14. package/dest/avm/journal/journal.js +1 -11
  15. package/dest/avm/journal/nullifiers.d.ts +0 -4
  16. package/dest/avm/journal/nullifiers.d.ts.map +1 -1
  17. package/dest/avm/journal/nullifiers.js +1 -11
  18. package/dest/avm/journal/public_storage.d.ts +1 -49
  19. package/dest/avm/journal/public_storage.d.ts.map +1 -1
  20. package/dest/avm/journal/public_storage.js +1 -19
  21. package/dest/avm/opcodes/addressing_mode.js +3 -3
  22. package/dest/avm/opcodes/ec_add.d.ts.map +1 -1
  23. package/dest/avm/opcodes/ec_add.js +5 -4
  24. package/dest/avm/opcodes/external_calls.js +2 -2
  25. package/dest/avm/opcodes/hashing.d.ts.map +1 -1
  26. package/dest/avm/opcodes/hashing.js +5 -5
  27. package/dest/avm/opcodes/misc.d.ts.map +1 -1
  28. package/dest/avm/opcodes/misc.js +3 -3
  29. package/dest/avm/opcodes/multi_scalar_mul.d.ts.map +1 -1
  30. package/dest/avm/opcodes/multi_scalar_mul.js +9 -6
  31. package/dest/public/bytecode_errors.d.ts +4 -0
  32. package/dest/public/bytecode_errors.d.ts.map +1 -0
  33. package/dest/public/bytecode_errors.js +7 -0
  34. package/dest/public/enqueued_call_side_effect_trace.d.ts +6 -1
  35. package/dest/public/enqueued_call_side_effect_trace.d.ts.map +1 -1
  36. package/dest/public/enqueued_call_side_effect_trace.js +58 -9
  37. package/dest/public/fixtures/index.d.ts +13 -8
  38. package/dest/public/fixtures/index.d.ts.map +1 -1
  39. package/dest/public/fixtures/index.js +97 -35
  40. package/dest/public/public_processor.d.ts +9 -3
  41. package/dest/public/public_processor.d.ts.map +1 -1
  42. package/dest/public/public_processor.js +49 -19
  43. package/dest/public/side_effect_errors.js +2 -2
  44. package/dest/public/unique_class_ids.d.ts +37 -0
  45. package/dest/public/unique_class_ids.d.ts.map +1 -0
  46. package/dest/public/unique_class_ids.js +66 -0
  47. package/package.json +10 -10
  48. package/src/avm/avm_memory_types.ts +29 -13
  49. package/src/avm/avm_simulator.ts +45 -19
  50. package/src/avm/avm_tree.ts +29 -91
  51. package/src/avm/errors.ts +13 -2
  52. package/src/avm/journal/journal.ts +0 -23
  53. package/src/avm/journal/nullifiers.ts +0 -11
  54. package/src/avm/journal/public_storage.ts +2 -21
  55. package/src/avm/opcodes/addressing_mode.ts +2 -2
  56. package/src/avm/opcodes/ec_add.ts +4 -3
  57. package/src/avm/opcodes/external_calls.ts +1 -1
  58. package/src/avm/opcodes/hashing.ts +6 -4
  59. package/src/avm/opcodes/misc.ts +4 -3
  60. package/src/avm/opcodes/multi_scalar_mul.ts +10 -5
  61. package/src/public/bytecode_errors.ts +6 -0
  62. package/src/public/enqueued_call_side_effect_trace.ts +75 -7
  63. package/src/public/fixtures/index.ts +143 -45
  64. package/src/public/public_processor.ts +79 -15
  65. package/src/public/side_effect_errors.ts +1 -1
  66. package/src/public/unique_class_ids.ts +80 -0
@@ -1,4 +1,4 @@
1
- import { type IndexedTreeId, MerkleTreeId, type MerkleTreeReadOperations, getTreeHeight } from '@aztec/circuit-types';
1
+ import { type IndexedTreeId, MerkleTreeId, type MerkleTreeReadOperations } from '@aztec/circuit-types';
2
2
  import { AppendOnlyTreeSnapshot, NullifierLeafPreimage, PublicDataTreeLeafPreimage } from '@aztec/circuits.js';
3
3
  import { poseidon2Hash } from '@aztec/foundation/crypto';
4
4
  import { Fr } from '@aztec/foundation/fields';
@@ -398,35 +398,36 @@ export class AvmEphemeralForest {
398
398
  // First, search the indexed updates (no DB fallback) to find
399
399
  // the leafIndex of the leaf with the largest key <= the specified key.
400
400
  const minIndexedLeafIndex = this._getLeafIndexOrNextLowestInIndexedUpdates(treeId, key);
401
+
402
+ // Then we search on the external DB
403
+ const leafOrLowLeafWitnessFromExternalDb: GetLeafResult<T> = await this._getLeafOrLowLeafWitnessInExternalDb(
404
+ treeId,
405
+ bigIntKey,
406
+ );
407
+
408
+ // If the indexed updates are empty, we can return the leaf from the DB
401
409
  if (minIndexedLeafIndex === -1n) {
402
- // No leaf is present in the indexed updates that is <= the key,
403
- // so retrieve the leaf or low leaf from the underlying DB.
404
- const leafOrLowLeafPreimage: GetLeafResult<T> = await this._getLeafOrLowLeafWitnessInExternalDb(
405
- treeId,
406
- bigIntKey,
407
- );
408
- return [leafOrLowLeafPreimage, /*pathAbsentInEphemeralTree=*/ true];
410
+ return [leafOrLowLeafWitnessFromExternalDb, /*pathAbsentInEphemeralTree=*/ true];
411
+ }
412
+
413
+ // Otherwise, we return the closest one. First fetch the leaf from the indexed updates.
414
+ const minIndexedUpdate: T = this.getIndexedUpdate(treeId, minIndexedLeafIndex);
415
+
416
+ // And get both keys
417
+ const keyFromIndexed = minIndexedUpdate.getKey();
418
+ const keyFromExternal = leafOrLowLeafWitnessFromExternalDb.preimage.getKey();
419
+
420
+ if (keyFromExternal > keyFromIndexed) {
421
+ // this.log.debug(`Using leaf from external DB for ${MerkleTreeId[treeId]}`);
422
+ return [leafOrLowLeafWitnessFromExternalDb, /*pathAbsentInEphemeralTree=*/ true];
409
423
  } else {
410
- // A leaf was found in the indexed updates that is <= the key
411
- const minPreimage: T = this.getIndexedUpdate(treeId, minIndexedLeafIndex);
412
- if (minPreimage.getKey() === bigIntKey) {
413
- // the index found is an exact match, no need to search further
414
- const leafInfo = { preimage: minPreimage, index: minIndexedLeafIndex, alreadyPresent: true };
415
- return [leafInfo, /*pathAbsentInEphemeralTree=*/ false];
416
- } else {
417
- // We are starting with the leaf with largest key <= the specified key
418
- // Starting at that "min leaf", search for specified key in both the indexed updates
419
- // and the underlying DB. If not found, return its low leaf.
420
- const [leafOrLowLeafInfo, pathAbsentInEphemeralTree] = await this._searchForLeafOrLowLeaf<ID, T>(
421
- treeId,
422
- bigIntKey,
423
- minPreimage,
424
- minIndexedLeafIndex,
425
- );
426
- // We did not find it - this is unexpected... the leaf OR low leaf should always be present
427
- assert(leafOrLowLeafInfo !== undefined, 'Could not find leaf or low leaf. This should not happen!');
428
- return [leafOrLowLeafInfo, pathAbsentInEphemeralTree];
429
- }
424
+ // this.log.debug(`Using leaf from indexed DB for ${MerkleTreeId[treeId]}`);
425
+ const leafInfo = {
426
+ preimage: minIndexedUpdate,
427
+ index: minIndexedLeafIndex,
428
+ alreadyPresent: keyFromIndexed === bigIntKey,
429
+ };
430
+ return [leafInfo, /*pathAbsentInEphemeralTree=*/ false];
430
431
  }
431
432
  }
432
433
 
@@ -480,69 +481,6 @@ export class AvmEphemeralForest {
480
481
  return { preimage: leafPreimage as T, index: leafIndex, alreadyPresent };
481
482
  }
482
483
 
483
- /**
484
- * Search for the leaf for the specified key.
485
- * Some leaf with key <= the specified key is expected to be present in the ephemeral tree's "indexed updates".
486
- * While searching, this function bounces between our local indexedUpdates and the external DB.
487
- *
488
- * @param key - The key for which we are look up the leaf or low leaf for.
489
- * @param minPreimage - The leaf with the largest key <= the specified key. Expected to be present in local indexedUpdates.
490
- * @param minIndex - The index of the leaf with the largest key <= the specified key.
491
- * @param T - The type of the preimage (PublicData or Nullifier)
492
- * @returns [
493
- * GetLeafResult | undefined - The leaf or low leaf info (preimage & leaf index),
494
- * pathAbsentInEphemeralTree - whether its sibling path is absent in the ephemeral tree (useful during insertions)
495
- * ]
496
- *
497
- * @details We look for the low element by bouncing between our local indexedUpdates map or the external DB
498
- * The conditions we are looking for are:
499
- * (1) Exact Match: curr.nextKey == key (this is only valid for public data tree)
500
- * (2) Sandwich Match: curr.nextKey > key and curr.key < key
501
- * (3) Max Condition: curr.next_index == 0 and curr.key < key
502
- * Note the min condition does not need to be handled since indexed trees are prefilled with at least the 0 element
503
- */
504
- private async _searchForLeafOrLowLeaf<ID extends IndexedTreeId, T extends IndexedTreeLeafPreimage>(
505
- treeId: ID,
506
- key: bigint,
507
- minPreimage: T,
508
- minIndex: bigint,
509
- ): Promise<[GetLeafResult<T> | undefined, /*pathAbsentInEphemeralTree=*/ boolean]> {
510
- let found = false;
511
- let curr = minPreimage as T;
512
- let result: GetLeafResult<T> | undefined = undefined;
513
- // Temp to avoid infinite loops - the limit is the number of leaves we may have to read
514
- const LIMIT = 2n ** BigInt(getTreeHeight(treeId)) - 1n;
515
- let counter = 0n;
516
- let lowPublicDataIndex = minIndex;
517
- let pathAbsentInEphemeralTree = false;
518
- while (!found && counter < LIMIT) {
519
- const bigIntKey = key;
520
- if (curr.getKey() === bigIntKey) {
521
- // We found an exact match - therefore this is an update
522
- found = true;
523
- result = { preimage: curr, index: lowPublicDataIndex, alreadyPresent: true };
524
- } else if (curr.getKey() < bigIntKey && (curr.getNextIndex() === 0n || curr.getNextKey() > bigIntKey)) {
525
- // We found it via sandwich or max condition, this is a low nullifier
526
- found = true;
527
- result = { preimage: curr, index: lowPublicDataIndex, alreadyPresent: false };
528
- }
529
- // Update the the values for the next iteration
530
- else {
531
- lowPublicDataIndex = curr.getNextIndex();
532
- if (this.hasLocalUpdates(treeId, lowPublicDataIndex)) {
533
- curr = this.getIndexedUpdate(treeId, lowPublicDataIndex)!;
534
- pathAbsentInEphemeralTree = false;
535
- } else {
536
- const preimage: IndexedTreeLeafPreimage = (await this.treeDb.getLeafPreimage(treeId, lowPublicDataIndex))!;
537
- curr = preimage as T;
538
- pathAbsentInEphemeralTree = true;
539
- }
540
- }
541
- counter++;
542
- }
543
- return [result, pathAbsentInEphemeralTree];
544
- }
545
-
546
484
  /**
547
485
  * This hashes the preimage to a field element
548
486
  */
package/src/avm/errors.ts CHANGED
@@ -102,10 +102,21 @@ export class TagCheckError extends AvmExecutionError {
102
102
  * Error is thrown when a relative memory address resolved to an offset which
103
103
  * is out of range, i.e, greater than maxUint32.
104
104
  */
105
- export class AddressOutOfRangeError extends AvmExecutionError {
105
+ export class RelativeAddressOutOfRangeError extends AvmExecutionError {
106
106
  constructor(baseAddr: number, relOffset: number) {
107
107
  super(`Address out of range. Base address ${baseAddr}, relative offset ${relOffset}`);
108
- this.name = 'AddressOutOfRangeError';
108
+ this.name = 'RelativeAddressOutOfRangeError';
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Error is thrown when a memory slice contains addresses which are
114
+ * out of range, i.e, greater than maxUint32.
115
+ */
116
+ export class MemorySliceOutOfRangeError extends AvmExecutionError {
117
+ constructor(baseAddr: number, size: number) {
118
+ super(`Memory slice is out of range. Base address ${baseAddr}, size ${size}`);
119
+ this.name = 'MemorySliceOutOfRangeError';
109
120
  }
110
121
  }
111
122
 
@@ -64,29 +64,6 @@ export class AvmPersistableStateManager {
64
64
  public readonly txHash: TxHash,
65
65
  ) {}
66
66
 
67
- /**
68
- * Create a new state manager with some preloaded pending siloed nullifiers
69
- */
70
- public static async newWithPendingSiloedNullifiers(
71
- worldStateDB: WorldStateDB,
72
- trace: PublicSideEffectTraceInterface,
73
- pendingSiloedNullifiers: Fr[],
74
- doMerkleOperations: boolean = false,
75
- txHash: TxHash,
76
- ) {
77
- const parentNullifiers = NullifierManager.newWithPendingSiloedNullifiers(worldStateDB, pendingSiloedNullifiers);
78
- const ephemeralForest = await AvmEphemeralForest.create(worldStateDB.getMerkleInterface());
79
- return new AvmPersistableStateManager(
80
- worldStateDB,
81
- trace,
82
- /*publicStorage=*/ new PublicStorage(worldStateDB),
83
- /*nullifiers=*/ parentNullifiers.fork(),
84
- doMerkleOperations,
85
- ephemeralForest,
86
- txHash,
87
- );
88
- }
89
-
90
67
  /**
91
68
  * Create a new state manager
92
69
  */
@@ -17,17 +17,6 @@ export class NullifierManager {
17
17
  private readonly parent?: NullifierManager,
18
18
  ) {}
19
19
 
20
- /**
21
- * Create a new nullifiers manager with some preloaded pending siloed nullifiers
22
- */
23
- public static newWithPendingSiloedNullifiers(hostNullifiers: CommitmentsDB, pendingSiloedNullifiers?: Fr[]) {
24
- const cachedSiloedNullifiers = new Set<bigint>();
25
- if (pendingSiloedNullifiers !== undefined) {
26
- pendingSiloedNullifiers.forEach(nullifier => cachedSiloedNullifiers.add(nullifier.toBigInt()));
27
- }
28
- return new NullifierManager(hostNullifiers, cachedSiloedNullifiers);
29
- }
30
-
31
20
  /**
32
21
  * Create a new nullifiers manager forked from this one
33
22
  */
@@ -1,4 +1,4 @@
1
- import { AztecAddress } from '@aztec/circuits.js';
1
+ import { type AztecAddress } from '@aztec/circuits.js';
2
2
  import { Fr } from '@aztec/foundation/fields';
3
3
 
4
4
  import type { PublicStateDB } from '../../index.js';
@@ -33,13 +33,6 @@ export class PublicStorage {
33
33
  return new PublicStorage(this.hostPublicStorage, this);
34
34
  }
35
35
 
36
- /**
37
- * Get the pending storage.
38
- */
39
- public getCache() {
40
- return this.cache;
41
- }
42
-
43
36
  /**
44
37
  * Read a storage value from this' cache or parent's (recursively).
45
38
  * DOES NOT CHECK HOST STORAGE!
@@ -108,17 +101,6 @@ export class PublicStorage {
108
101
  public acceptAndMerge(incomingPublicStorage: PublicStorage) {
109
102
  this.cache.acceptAndMerge(incomingPublicStorage.cache);
110
103
  }
111
-
112
- /**
113
- * Commits ALL staged writes to the host's state.
114
- */
115
- public async commitToDB() {
116
- for (const [contractAddress, cacheAtContract] of this.cache.cachePerContract) {
117
- for (const [slot, value] of cacheAtContract) {
118
- await this.hostPublicStorage.storageWrite(AztecAddress.fromBigInt(contractAddress), new Fr(slot), value);
119
- }
120
- }
121
- }
122
104
  }
123
105
 
124
106
  /**
@@ -132,8 +114,7 @@ class PublicStorageCache {
132
114
  * One inner-map per contract storage address,
133
115
  * mapping storage slot to latest staged write value.
134
116
  */
135
- public cachePerContract: Map<bigint, Map<bigint, Fr>> = new Map();
136
- // FIXME: storage ^ should be private, but its value is used in commitToDB
117
+ private cachePerContract: Map<bigint, Map<bigint, Fr>> = new Map();
137
118
 
138
119
  /**
139
120
  * Read a staged value from storage, if it has been previously written to.
@@ -1,7 +1,7 @@
1
1
  import { strict as assert } from 'assert';
2
2
 
3
3
  import { TaggedMemory, type TaggedMemoryInterface } from '../avm_memory_types.js';
4
- import { AddressOutOfRangeError } from '../errors.js';
4
+ import { RelativeAddressOutOfRangeError } from '../errors.js';
5
5
 
6
6
  export enum AddressingMode {
7
7
  DIRECT = 0,
@@ -67,7 +67,7 @@ export class Addressing {
67
67
  const baseAddr = Number(mem.get(0).toBigInt());
68
68
  resolved[i] += baseAddr;
69
69
  if (resolved[i] >= TaggedMemory.MAX_MEMORY_SIZE) {
70
- throw new AddressOutOfRangeError(baseAddr, offset);
70
+ throw new RelativeAddressOutOfRangeError(baseAddr, offset);
71
71
  }
72
72
  }
73
73
  if (mode & AddressingMode.INDIRECT) {
@@ -84,10 +84,11 @@ export class EcAdd extends Instruction {
84
84
  dest = grumpkin.add(p1, p2);
85
85
  }
86
86
 
87
- memory.set(dstOffset, new Field(dest.x));
88
- memory.set(dstOffset + 1, new Field(dest.y));
87
+ // Important to use setSlice() and not set() in the two following statements as
88
+ // this checks that the offsets lie within memory range.
89
+ memory.setSlice(dstOffset, [new Field(dest.x), new Field(dest.y)]);
89
90
  // Check representation of infinity for grumpkin
90
- memory.set(dstOffset + 2, new Uint1(dest.equals(Point.ZERO) ? 1 : 0));
91
+ memory.setSlice(dstOffset + 2, [new Uint1(dest.equals(Point.ZERO) ? 1 : 0)]);
91
92
 
92
93
  memory.assert({ reads: 6, writes: 3, addressing });
93
94
  }
@@ -39,10 +39,10 @@ abstract class ExternalCall extends Instruction {
39
39
  memory.checkTag(TypeTag.UINT32, argsSizeOffset);
40
40
 
41
41
  const calldataSize = memory.get(argsSizeOffset).toNumber();
42
+ const calldata = memory.getSlice(argsOffset, calldataSize).map(f => f.toFr());
42
43
  memory.checkTagsRange(TypeTag.FIELD, argsOffset, calldataSize);
43
44
 
44
45
  const callAddress = memory.getAs<Field>(addrOffset);
45
- const calldata = memory.getSlice(argsOffset, calldataSize).map(f => f.toFr());
46
46
  // If we are already in a static call, we propagate the environment.
47
47
  const callType = context.environment.isStaticCall ? 'STATICCALL' : this.type;
48
48
 
@@ -30,9 +30,10 @@ export class Poseidon2 extends Instruction {
30
30
  const operands = [this.inputStateOffset, this.outputStateOffset];
31
31
  const addressing = Addressing.fromWire(this.indirect, operands.length);
32
32
  const [inputOffset, outputOffset] = addressing.resolve(operands, memory);
33
- memory.checkTagsRange(TypeTag.FIELD, inputOffset, Poseidon2.stateSize);
34
33
 
35
34
  const inputState = memory.getSlice(inputOffset, Poseidon2.stateSize);
35
+ memory.checkTagsRange(TypeTag.FIELD, inputOffset, Poseidon2.stateSize);
36
+
36
37
  const outputState = poseidon2Permutation(inputState);
37
38
  memory.setSlice(
38
39
  outputOffset,
@@ -68,9 +69,9 @@ export class KeccakF1600 extends Instruction {
68
69
  const [dstOffset, inputOffset] = addressing.resolve(operands, memory);
69
70
  context.machineState.consumeGas(this.gasCost());
70
71
 
72
+ const stateData = memory.getSlice(inputOffset, inputSize).map(word => word.toBigInt());
71
73
  memory.checkTagsRange(TypeTag.UINT64, inputOffset, inputSize);
72
74
 
73
- const stateData = memory.getSlice(inputOffset, inputSize).map(word => word.toBigInt());
74
75
  const updatedState = keccakf1600(stateData);
75
76
 
76
77
  const res = updatedState.map(word => new Uint64(word));
@@ -113,11 +114,12 @@ export class Sha256Compression extends Instruction {
113
114
 
114
115
  // Note: size of output is same as size of state
115
116
  context.machineState.consumeGas(this.gasCost());
117
+ const inputs = Uint32Array.from(memory.getSlice(inputsOffset, INPUTS_SIZE).map(word => word.toNumber()));
118
+ const state = Uint32Array.from(memory.getSlice(stateOffset, STATE_SIZE).map(word => word.toNumber()));
119
+
116
120
  memory.checkTagsRange(TypeTag.UINT32, inputsOffset, INPUTS_SIZE);
117
121
  memory.checkTagsRange(TypeTag.UINT32, stateOffset, STATE_SIZE);
118
122
 
119
- const state = Uint32Array.from(memory.getSlice(stateOffset, STATE_SIZE).map(word => word.toNumber()));
120
- const inputs = Uint32Array.from(memory.getSlice(inputsOffset, INPUTS_SIZE).map(word => word.toNumber()));
121
123
  const output = sha256Compression(state, inputs);
122
124
 
123
125
  // Conversion required from Uint32Array to Uint32[] (can't map directly, need `...`)
@@ -39,14 +39,15 @@ export class DebugLog extends Instruction {
39
39
 
40
40
  memory.checkTag(TypeTag.UINT32, fieldsSizeOffset);
41
41
  const fieldsSize = memory.get(fieldsSizeOffset).toNumber();
42
+
43
+ const rawMessage = memory.getSlice(messageOffset, this.messageSize);
44
+ const fields = memory.getSlice(fieldsOffset, fieldsSize);
45
+
42
46
  memory.checkTagsRange(TypeTag.UINT8, messageOffset, this.messageSize);
43
47
  memory.checkTagsRange(TypeTag.FIELD, fieldsOffset, fieldsSize);
44
48
 
45
49
  context.machineState.consumeGas(this.gasCost(this.messageSize + fieldsSize));
46
50
 
47
- const rawMessage = memory.getSlice(messageOffset, this.messageSize);
48
- const fields = memory.getSlice(fieldsOffset, fieldsSize);
49
-
50
51
  // Interpret str<N> = [u8; N] to string.
51
52
  const messageAsStr = rawMessage.map(field => String.fromCharCode(field.toNumber())).join('');
52
53
  const formattedStr = applyStringFormatting(
@@ -46,6 +46,12 @@ export class MultiScalarMul extends Instruction {
46
46
  if (pointsReadLength % 3 !== 0) {
47
47
  throw new InstructionExecutionError(`Points vector offset should be a multiple of 3, was ${pointsReadLength}`);
48
48
  }
49
+
50
+ // Get the unrolled (x, y, inf) representing the points
51
+ // Important to perform this before tag validation, as getSlice() first checks
52
+ // that the slice is not out of memory range. This needs to be aligned with circuit.
53
+ const pointsVector = memory.getSlice(pointsOffset, pointsReadLength);
54
+
49
55
  // Divide by 3 since each point is represented as a triplet to get the number of points
50
56
  const numPoints = pointsReadLength / 3;
51
57
  // The tag for each triplet will be (Field, Field, Uint8)
@@ -56,8 +62,6 @@ export class MultiScalarMul extends Instruction {
56
62
  // Check Uint1 (inf flag)
57
63
  memory.checkTag(TypeTag.UINT1, offset + 2);
58
64
  }
59
- // Get the unrolled (x, y, inf) representing the points
60
- const pointsVector = memory.getSlice(pointsOffset, pointsReadLength);
61
65
 
62
66
  // The size of the scalars vector is twice the NUMBER of points because of the scalar limb decomposition
63
67
  const scalarReadLength = numPoints * 2;
@@ -106,10 +110,11 @@ export class MultiScalarMul extends Instruction {
106
110
  }
107
111
  }, grumpkin.mul(firstBaseScalarPair[0], firstBaseScalarPair[1]));
108
112
 
109
- memory.set(outputOffset, new Field(outputPoint.x));
110
- memory.set(outputOffset + 1, new Field(outputPoint.y));
113
+ // Important to use setSlice() and not set() in the two following statements as
114
+ // this checks that the offsets lie within memory range.
115
+ memory.setSlice(outputOffset, [new Field(outputPoint.x), new Field(outputPoint.y)]);
111
116
  // Check representation of infinity for grumpkin
112
- memory.set(outputOffset + 2, new Uint1(outputPoint.equals(Point.ZERO) ? 1 : 0));
117
+ memory.setSlice(outputOffset + 2, [new Uint1(outputPoint.equals(Point.ZERO) ? 1 : 0)]);
113
118
 
114
119
  memory.assert({
115
120
  reads: 1 + pointsReadLength + scalarReadLength /* points and scalars */,
@@ -0,0 +1,6 @@
1
+ export class ContractClassBytecodeError extends Error {
2
+ constructor(contractAddress: string) {
3
+ super(`Failed to get bytecode for contract at address ${contractAddress}`);
4
+ this.name = 'ContractClassBytecodeError';
5
+ }
6
+ }
@@ -24,6 +24,7 @@ import {
24
24
  MAX_L2_TO_L1_MSGS_PER_TX,
25
25
  MAX_NOTE_HASHES_PER_TX,
26
26
  MAX_NULLIFIERS_PER_TX,
27
+ MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS,
27
28
  MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX,
28
29
  MAX_TOTAL_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX,
29
30
  MAX_UNENCRYPTED_LOGS_PER_TX,
@@ -58,6 +59,7 @@ import { type AvmExecutionEnvironment } from '../avm/avm_execution_environment.j
58
59
  import { type EnqueuedPublicCallExecutionResultWithSideEffects, type PublicFunctionCallResult } from './execution.js';
59
60
  import { SideEffectLimitReachedError } from './side_effect_errors.js';
60
61
  import { type PublicSideEffectTraceInterface } from './side_effect_trace_interface.js';
62
+ import { UniqueClassIds } from './unique_class_ids.js';
61
63
 
62
64
  const emptyPublicDataPath = () => new Array(PUBLIC_DATA_TREE_HEIGHT).fill(Fr.zero());
63
65
  const emptyNoteHashPath = () => new Array(NOTE_HASH_TREE_HEIGHT).fill(Fr.zero());
@@ -128,6 +130,8 @@ export class PublicEnqueuedCallSideEffectTrace implements PublicSideEffectTraceI
128
130
  * otherwise the public kernel can fail to prove because TX limits are breached.
129
131
  */
130
132
  private readonly previousSideEffectArrayLengths: SideEffectArrayLengths = SideEffectArrayLengths.empty(),
133
+ /** We need to track the set of class IDs used for bytecode retrieval to deduplicate and enforce limits. */
134
+ private gotBytecodeFromClassIds: UniqueClassIds = new UniqueClassIds(),
131
135
  ) {
132
136
  this.log.debug(`Creating trace instance with startSideEffectCounter: ${startSideEffectCounter}`);
133
137
  this.sideEffectCounter = startSideEffectCounter;
@@ -145,6 +149,7 @@ export class PublicEnqueuedCallSideEffectTrace implements PublicSideEffectTraceI
145
149
  this.previousSideEffectArrayLengths.l2ToL1Msgs + this.l2ToL1Messages.length,
146
150
  this.previousSideEffectArrayLengths.unencryptedLogs + this.unencryptedLogs.length,
147
151
  ),
152
+ this.gotBytecodeFromClassIds.fork(),
148
153
  );
149
154
  }
150
155
 
@@ -152,7 +157,7 @@ export class PublicEnqueuedCallSideEffectTrace implements PublicSideEffectTraceI
152
157
  // sanity check to avoid merging the same forked trace twice
153
158
  assert(
154
159
  !forkedTrace.alreadyMergedIntoParent,
155
- 'Cannot merge a forked trace that has already been merged into its parent!',
160
+ 'Bug! Cannot merge a forked trace that has already been merged into its parent!',
156
161
  );
157
162
  forkedTrace.alreadyMergedIntoParent = true;
158
163
 
@@ -171,10 +176,21 @@ export class PublicEnqueuedCallSideEffectTrace implements PublicSideEffectTraceI
171
176
  }
172
177
 
173
178
  private mergeHints(forkedTrace: this) {
179
+ this.gotBytecodeFromClassIds.acceptAndMerge(forkedTrace.gotBytecodeFromClassIds);
180
+
174
181
  this.avmCircuitHints.enqueuedCalls.items.push(...forkedTrace.avmCircuitHints.enqueuedCalls.items);
175
182
 
176
183
  this.avmCircuitHints.contractInstances.items.push(...forkedTrace.avmCircuitHints.contractInstances.items);
177
- this.avmCircuitHints.contractBytecodeHints.items.push(...forkedTrace.avmCircuitHints.contractBytecodeHints.items);
184
+
185
+ // merge in contract bytecode hints
186
+ // UniqueClassIds should prevent duplication
187
+ for (const [contractClassId, bytecodeHint] of forkedTrace.avmCircuitHints.contractBytecodeHints) {
188
+ assert(
189
+ !this.avmCircuitHints.contractBytecodeHints.has(contractClassId),
190
+ 'Bug preventing duplication of contract bytecode hints',
191
+ );
192
+ this.avmCircuitHints.contractBytecodeHints.set(contractClassId, bytecodeHint);
193
+ }
178
194
 
179
195
  this.avmCircuitHints.publicDataReads.items.push(...forkedTrace.avmCircuitHints.publicDataReads.items);
180
196
  this.avmCircuitHints.publicDataWrites.items.push(...forkedTrace.avmCircuitHints.publicDataWrites.items);
@@ -405,6 +421,14 @@ export class PublicEnqueuedCallSideEffectTrace implements PublicSideEffectTraceI
405
421
  lowLeafIndex: Fr = Fr.zero(),
406
422
  lowLeafPath: Fr[] = emptyNullifierPath(),
407
423
  ) {
424
+ // FIXME: The way we are hinting contract bytecodes is fundamentally broken.
425
+ // We are mapping contract class ID to a bytecode hint
426
+ // But a bytecode hint is tied to a contract INSTANCE.
427
+ // What if you encounter another contract instance with the same class ID?
428
+ // We can't hint that instance too since there is already an entry in the hints set that class ID.
429
+ // But without that instance hinted, the circuit can't prove that the called contract address
430
+ // actually corresponds to any class ID.
431
+
408
432
  const membershipHint = new AvmNullifierReadTreeHint(lowLeafPreimage, lowLeafIndex, lowLeafPath);
409
433
  const instance = new AvmContractInstanceHint(
410
434
  contractAddress,
@@ -416,13 +440,57 @@ export class PublicEnqueuedCallSideEffectTrace implements PublicSideEffectTraceI
416
440
  contractInstance.publicKeys,
417
441
  membershipHint,
418
442
  );
419
- // We need to deduplicate the contract instances based on addresses
420
- this.avmCircuitHints.contractBytecodeHints.items.push(
421
- new AvmContractBytecodeHints(bytecode, instance, contractClass),
422
- );
443
+
444
+ // Always hint the contract instance separately from the bytecode hint.
445
+ // Since the bytecode hints are keyed by class ID, we need to hint the instance separately
446
+ // since there might be multiple instances hinted for the same class ID.
447
+ this.avmCircuitHints.contractInstances.items.push(instance);
423
448
  this.log.debug(
424
- `Bytecode retrieval for contract execution traced: exists=${exists}, instance=${jsonStringify(contractInstance)}`,
449
+ `Tracing contract instance for bytecode retrieval: exists=${exists}, instance=${jsonStringify(contractInstance)}`,
450
+ );
451
+
452
+ if (!exists) {
453
+ // this ensures there are no duplicates
454
+ this.log.debug(`Contract address ${contractAddress} does not exist. Not tracing bytecode & class ID.`);
455
+ return;
456
+ }
457
+ // We already hinted this bytecode. No need to
458
+ // Don't we still need to hint if the class ID already exists?
459
+ // Because the circuit needs to prove that the called contract address corresponds to the class ID.
460
+ // To do so, the circuit needs to know the class ID in the
461
+ if (this.gotBytecodeFromClassIds.has(contractInstance.contractClassId.toString())) {
462
+ // this ensures there are no duplicates
463
+ this.log.debug(
464
+ `Contract class id ${contractInstance.contractClassId.toString()} already exists in previous hints`,
465
+ );
466
+ return;
467
+ }
468
+
469
+ // If we could actually allow contract calls after the limit was reached, we would hint even if we have
470
+ // surpassed the limit of unique class IDs (still trace the failed bytecode retrieval)
471
+ // because the circuit needs to know the class ID to know when the limit is hit.
472
+ // BUT, the issue with this approach is that the sequencer could lie and say "this call was to a new class ID",
473
+ // and the circuit cannot prove that it's not true without deriving the class ID from bytecode,
474
+ // proving that it corresponds to the called contract address, and proving that the class ID wasn't already
475
+ // present/used. That would require more bytecode hashing which is exactly what this limit exists to avoid.
476
+ if (this.gotBytecodeFromClassIds.size() >= MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS) {
477
+ this.log.debug(
478
+ `Bytecode retrieval failure for contract class ID ${contractInstance.contractClassId.toString()} (limit reached)`,
479
+ );
480
+ throw new SideEffectLimitReachedError(
481
+ 'contract calls to unique class IDs',
482
+ MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS,
483
+ );
484
+ }
485
+
486
+ this.log.debug(`Tracing bytecode & contract class for bytecode retrieval: class=${jsonStringify(contractClass)}`);
487
+ this.avmCircuitHints.contractBytecodeHints.set(
488
+ contractInstance.contractClassId.toString(),
489
+ new AvmContractBytecodeHints(bytecode, instance, contractClass),
425
490
  );
491
+ // After adding the bytecode hint, mark the classId as retrieved to avoid duplication.
492
+ // The above map alone isn't sufficient because we need to check the parent trace's (and its parent) as well.
493
+ this.gotBytecodeFromClassIds.add(contractInstance.contractClassId.toString());
426
494
  }
427
495
 
428
496
  /**