@aztec/simulator 0.68.0 → 0.68.2
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/dest/avm/avm_memory_types.d.ts.map +1 -1
- package/dest/avm/avm_memory_types.js +21 -14
- package/dest/avm/avm_simulator.d.ts +1 -0
- package/dest/avm/avm_simulator.d.ts.map +1 -1
- package/dest/avm/avm_simulator.js +35 -18
- package/dest/avm/avm_tree.d.ts +0 -22
- package/dest/avm/avm_tree.d.ts.map +1 -1
- package/dest/avm/avm_tree.js +22 -81
- package/dest/avm/errors.d.ts +8 -1
- package/dest/avm/errors.d.ts.map +1 -1
- package/dest/avm/errors.js +13 -3
- package/dest/avm/journal/journal.d.ts +0 -4
- package/dest/avm/journal/journal.d.ts.map +1 -1
- package/dest/avm/journal/journal.js +1 -11
- package/dest/avm/journal/nullifiers.d.ts +0 -4
- package/dest/avm/journal/nullifiers.d.ts.map +1 -1
- package/dest/avm/journal/nullifiers.js +1 -11
- package/dest/avm/journal/public_storage.d.ts +1 -49
- package/dest/avm/journal/public_storage.d.ts.map +1 -1
- package/dest/avm/journal/public_storage.js +1 -19
- package/dest/avm/opcodes/addressing_mode.js +3 -3
- package/dest/avm/opcodes/ec_add.d.ts.map +1 -1
- package/dest/avm/opcodes/ec_add.js +5 -4
- package/dest/avm/opcodes/external_calls.js +2 -2
- package/dest/avm/opcodes/hashing.d.ts.map +1 -1
- package/dest/avm/opcodes/hashing.js +5 -5
- package/dest/avm/opcodes/misc.d.ts.map +1 -1
- package/dest/avm/opcodes/misc.js +3 -3
- package/dest/avm/opcodes/multi_scalar_mul.d.ts.map +1 -1
- package/dest/avm/opcodes/multi_scalar_mul.js +9 -6
- package/dest/public/bytecode_errors.d.ts +4 -0
- package/dest/public/bytecode_errors.d.ts.map +1 -0
- package/dest/public/bytecode_errors.js +7 -0
- package/dest/public/enqueued_call_side_effect_trace.d.ts +6 -1
- package/dest/public/enqueued_call_side_effect_trace.d.ts.map +1 -1
- package/dest/public/enqueued_call_side_effect_trace.js +58 -9
- package/dest/public/fixtures/index.d.ts +13 -8
- package/dest/public/fixtures/index.d.ts.map +1 -1
- package/dest/public/fixtures/index.js +97 -35
- package/dest/public/public_processor.d.ts +9 -3
- package/dest/public/public_processor.d.ts.map +1 -1
- package/dest/public/public_processor.js +49 -19
- package/dest/public/side_effect_errors.js +2 -2
- package/dest/public/unique_class_ids.d.ts +37 -0
- package/dest/public/unique_class_ids.d.ts.map +1 -0
- package/dest/public/unique_class_ids.js +66 -0
- package/package.json +10 -10
- package/src/avm/avm_memory_types.ts +29 -13
- package/src/avm/avm_simulator.ts +45 -19
- package/src/avm/avm_tree.ts +29 -91
- package/src/avm/errors.ts +13 -2
- package/src/avm/journal/journal.ts +0 -23
- package/src/avm/journal/nullifiers.ts +0 -11
- package/src/avm/journal/public_storage.ts +2 -21
- package/src/avm/opcodes/addressing_mode.ts +2 -2
- package/src/avm/opcodes/ec_add.ts +4 -3
- package/src/avm/opcodes/external_calls.ts +1 -1
- package/src/avm/opcodes/hashing.ts +6 -4
- package/src/avm/opcodes/misc.ts +4 -3
- package/src/avm/opcodes/multi_scalar_mul.ts +10 -5
- package/src/public/bytecode_errors.ts +6 -0
- package/src/public/enqueued_call_side_effect_trace.ts +75 -7
- package/src/public/fixtures/index.ts +143 -45
- package/src/public/public_processor.ts +79 -15
- package/src/public/side_effect_errors.ts +1 -1
- package/src/public/unique_class_ids.ts +80 -0
package/src/avm/avm_tree.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type IndexedTreeId, MerkleTreeId, type MerkleTreeReadOperations
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
//
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
|
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 = '
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
88
|
-
|
|
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.
|
|
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 `...`)
|
package/src/avm/opcodes/misc.ts
CHANGED
|
@@ -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
|
-
|
|
110
|
-
|
|
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.
|
|
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 */,
|
|
@@ -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
|
-
|
|
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
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
`
|
|
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
|
/**
|