From 1ceccadc09915e30b1932b3e9fa552ef05c36483 Mon Sep 17 00:00:00 2001 From: dbanks12 Date: Fri, 23 Feb 2024 15:38:49 +0000 Subject: [PATCH] feat(avm-simulator): create cache for pending nullifiers and existence checks --- .../src/sequencer/public_processor.ts | 16 +- .../src/simulator/public_executor.ts | 13 +- .../simulator/src/avm/fixtures/index.ts | 9 +- .../simulator/src/avm/journal/host_storage.ts | 19 +-- .../simulator/src/avm/journal/journal.test.ts | 29 ++-- .../simulator/src/avm/journal/journal.ts | 18 +- .../src/avm/journal/nullifiers.test.ts | 145 ++++++++++++++++ .../simulator/src/avm/journal/nullifiers.ts | 156 ++++++++++++++++++ .../src/avm/opcodes/accrued_substate.ts | 2 +- .../src/avm/opcodes/external_calls.test.ts | 5 +- yarn-project/simulator/src/public/db.ts | 9 + yarn-project/simulator/src/public/executor.ts | 5 +- .../simulator/src/public/index.test.ts | 31 ++-- 13 files changed, 399 insertions(+), 58 deletions(-) create mode 100644 yarn-project/simulator/src/avm/journal/nullifiers.test.ts create mode 100644 yarn-project/simulator/src/avm/journal/nullifiers.ts diff --git a/yarn-project/sequencer-client/src/sequencer/public_processor.ts b/yarn-project/sequencer-client/src/sequencer/public_processor.ts index 99189c376a1b..fcb00b60335a 100644 --- a/yarn-project/sequencer-client/src/sequencer/public_processor.ts +++ b/yarn-project/sequencer-client/src/sequencer/public_processor.ts @@ -9,7 +9,12 @@ import { MerkleTreeOperations } from '@aztec/world-state'; import { EmptyPublicProver } from '../prover/empty.js'; import { PublicProver } from '../prover/index.js'; import { PublicKernelCircuitSimulator } from '../simulator/index.js'; -import { ContractsDataSourcePublicDB, WorldStateDB, WorldStatePublicDB } from '../simulator/public_executor.js'; +import { + ContractsDataSourcePublicDB, + NullifiersDB, + WorldStateDB, + WorldStatePublicDB, +} from '../simulator/public_executor.js'; import { RealPublicKernelCircuitSimulator } from '../simulator/public_kernel.js'; import { AbstractPhaseManager } from './abstract_phase_manager.js'; import { PhaseManagerFactory } from './phase_manager_factory.js'; @@ -41,7 +46,14 @@ export class PublicProcessorFactory { const publicContractsDB = new ContractsDataSourcePublicDB(this.contractDataSource); const worldStatePublicDB = new WorldStatePublicDB(this.merkleTree); const worldStateDB = new WorldStateDB(this.merkleTree, this.l1Tol2MessagesDataSource); - const publicExecutor = new PublicExecutor(worldStatePublicDB, publicContractsDB, worldStateDB, historicalHeader); + const nullifiersDB = new NullifiersDB(this.merkleTree); + const publicExecutor = new PublicExecutor( + worldStatePublicDB, + publicContractsDB, + worldStateDB, + nullifiersDB, + historicalHeader, + ); return new PublicProcessor( this.merkleTree, publicExecutor, diff --git a/yarn-project/sequencer-client/src/simulator/public_executor.ts b/yarn-project/sequencer-client/src/simulator/public_executor.ts index a707355b7512..e9c2518b7366 100644 --- a/yarn-project/sequencer-client/src/simulator/public_executor.ts +++ b/yarn-project/sequencer-client/src/simulator/public_executor.ts @@ -8,7 +8,7 @@ import { PublicDataTreeLeafPreimage, } from '@aztec/circuits.js'; import { computePublicDataTreeLeafSlot } from '@aztec/circuits.js/hash'; -import { CommitmentsDB, MessageLoadOracleInputs, PublicContractsDB, PublicStateDB } from '@aztec/simulator'; +import { CommitmentsDB, MessageLoadOracleInputs, NullifiersDB as NDB, PublicContractsDB, PublicStateDB } from '@aztec/simulator'; import { MerkleTreeOperations } from '@aztec/world-state'; /** @@ -167,3 +167,14 @@ export class WorldStateDB implements CommitmentsDB { return await this.db.findLeafIndex(MerkleTreeId.NOTE_HASH_TREE, commitment.toBuffer()); } } + +/** + * Implements Nullifier db. + */ +export class NullifiersDB implements NDB { + constructor(private db: MerkleTreeOperations) {} + + public async getNullifierIndex(nullifier: Fr): Promise { + return await this.db.findLeafIndex(MerkleTreeId.NULLIFIER_TREE, nullifier.toBuffer()); + } +} \ No newline at end of file diff --git a/yarn-project/simulator/src/avm/fixtures/index.ts b/yarn-project/simulator/src/avm/fixtures/index.ts index bc5487099ca2..fab71636a615 100644 --- a/yarn-project/simulator/src/avm/fixtures/index.ts +++ b/yarn-project/simulator/src/avm/fixtures/index.ts @@ -7,7 +7,7 @@ import { Fr } from '@aztec/foundation/fields'; import { mock } from 'jest-mock-extended'; import merge from 'lodash.merge'; -import { CommitmentsDB, PublicContractsDB, PublicStateDB } from '../../index.js'; +import { CommitmentsDB, NullifiersDB, PublicContractsDB, PublicStateDB } from '../../index.js'; import { AvmContext } from '../avm_context.js'; import { AvmExecutionEnvironment } from '../avm_execution_environment.js'; import { AvmMachineState } from '../avm_machine_state.js'; @@ -31,7 +31,12 @@ export function initContext(overrides?: { /** Creates an empty world state with mocked storage. */ export function initMockWorldStateJournal(): AvmPersistableStateManager { - const hostStorage = new HostStorage(mock(), mock(), mock()); + const hostStorage = new HostStorage( + mock(), + mock(), + mock(), + mock(), + ); return new AvmPersistableStateManager(hostStorage); } diff --git a/yarn-project/simulator/src/avm/journal/host_storage.ts b/yarn-project/simulator/src/avm/journal/host_storage.ts index 330f8bf0189a..2b69bb3a413e 100644 --- a/yarn-project/simulator/src/avm/journal/host_storage.ts +++ b/yarn-project/simulator/src/avm/journal/host_storage.ts @@ -1,4 +1,4 @@ -import { CommitmentsDB, PublicContractsDB, PublicStateDB } from '../../public/db.js'; +import { CommitmentsDB, NullifiersDB, PublicContractsDB, PublicStateDB } from '../../public/db.js'; /** * Host storage @@ -6,15 +6,10 @@ import { CommitmentsDB, PublicContractsDB, PublicStateDB } from '../../public/db * A wrapper around the node dbs */ export class HostStorage { - public readonly publicStateDb: PublicStateDB; - - public readonly contractsDb: PublicContractsDB; - - public readonly commitmentsDb: CommitmentsDB; - - constructor(publicStateDb: PublicStateDB, contractsDb: PublicContractsDB, commitmentsDb: CommitmentsDB) { - this.publicStateDb = publicStateDb; - this.contractsDb = contractsDb; - this.commitmentsDb = commitmentsDb; - } + constructor( + public readonly publicStateDb: PublicStateDB, + public readonly contractsDb: PublicContractsDB, + public readonly commitmentsDb: CommitmentsDB, + public readonly nullifiersDb: NullifiersDB, + ) {} } diff --git a/yarn-project/simulator/src/avm/journal/journal.test.ts b/yarn-project/simulator/src/avm/journal/journal.test.ts index 0530fe20c1dc..faf551e95358 100644 --- a/yarn-project/simulator/src/avm/journal/journal.test.ts +++ b/yarn-project/simulator/src/avm/journal/journal.test.ts @@ -2,7 +2,7 @@ import { Fr } from '@aztec/foundation/fields'; import { MockProxy, mock } from 'jest-mock-extended'; -import { CommitmentsDB, PublicContractsDB, PublicStateDB } from '../../index.js'; +import { CommitmentsDB, NullifiersDB, PublicContractsDB, PublicStateDB } from '../../index.js'; import { HostStorage } from './host_storage.js'; import { AvmPersistableStateManager, JournalData } from './journal.js'; @@ -12,10 +12,11 @@ describe('journal', () => { beforeEach(() => { publicDb = mock(); - const commitmentsDb = mock(); const contractsDb = mock(); + const commitmentsDb = mock(); + const nullifiersDb = mock(); - const hostStorage = new HostStorage(publicDb, contractsDb, commitmentsDb); + const hostStorage = new HostStorage(publicDb, contractsDb, commitmentsDb, nullifiersDb); journal = new AvmPersistableStateManager(hostStorage); }); @@ -60,7 +61,13 @@ describe('journal', () => { const journalUpdates = journal.flush(); expect(journalUpdates.newNoteHashes).toEqual([utxo]); }); + it('Should maintain nullifiers', async () => { + const utxo = new Fr(1); + await journal.writeNullifier(utxo); + const journalUpdates = journal.flush(); + expect(journalUpdates.newNullifiers).toEqual([utxo]); + }); it('Should maintain l1 messages', () => { const utxo = [new Fr(1)]; journal.writeL1Message(utxo); @@ -68,14 +75,6 @@ describe('journal', () => { const journalUpdates = journal.flush(); expect(journalUpdates.newL1Messages).toEqual([utxo]); }); - - it('Should maintain nullifiers', () => { - const utxo = new Fr(1); - journal.writeNullifier(utxo); - - const journalUpdates = journal.flush(); - expect(journalUpdates.newNullifiers).toEqual([utxo]); - }); }); it('Should merge two successful journals together', async () => { @@ -100,7 +99,7 @@ describe('journal', () => { journal.writeNoteHash(commitment); journal.writeLog(logs); journal.writeL1Message(logs); - journal.writeNullifier(commitment); + await journal.writeNullifier(commitment); const childJournal = new AvmPersistableStateManager(journal.hostStorage, journal); childJournal.writeStorage(contractAddress, key, valueT1); @@ -108,7 +107,7 @@ describe('journal', () => { childJournal.writeNoteHash(commitmentT1); childJournal.writeLog(logsT1); childJournal.writeL1Message(logsT1); - childJournal.writeNullifier(commitmentT1); + await childJournal.writeNullifier(commitmentT1); journal.acceptNestedCallState(childJournal); @@ -158,7 +157,7 @@ describe('journal', () => { journal.writeStorage(contractAddress, key, value); await journal.readStorage(contractAddress, key); journal.writeNoteHash(commitment); - journal.writeNullifier(commitment); + await journal.writeNullifier(commitment); journal.writeLog(logs); journal.writeL1Message(logs); @@ -166,7 +165,7 @@ describe('journal', () => { childJournal.writeStorage(contractAddress, key, valueT1); await childJournal.readStorage(contractAddress, key); childJournal.writeNoteHash(commitmentT1); - childJournal.writeNullifier(commitmentT1); + await childJournal.writeNullifier(commitmentT1); childJournal.writeLog(logsT1); childJournal.writeL1Message(logsT1); diff --git a/yarn-project/simulator/src/avm/journal/journal.ts b/yarn-project/simulator/src/avm/journal/journal.ts index fab9ec5c9144..17b5bc84a605 100644 --- a/yarn-project/simulator/src/avm/journal/journal.ts +++ b/yarn-project/simulator/src/avm/journal/journal.ts @@ -1,6 +1,7 @@ import { Fr } from '@aztec/foundation/fields'; import { HostStorage } from './host_storage.js'; +import { Nullifiers } from './nullifiers.js'; import { PublicStorage } from './public_storage.js'; import { WorldStateAccessTrace } from './trace.js'; @@ -10,6 +11,7 @@ import { WorldStateAccessTrace } from './trace.js'; export type JournalData = { newNoteHashes: Fr[]; newNullifiers: Fr[]; + newL1Messages: Fr[][]; newLogs: Fr[][]; @@ -38,8 +40,8 @@ export class AvmPersistableStateManager { /** World State */ /** Public storage, including cached writes */ private publicStorage: PublicStorage; - ///** Nullifier set, including cached/recently-emitted nullifiers */ - //private nullifiers: NullifiersDB; + /** Nullifier set, including cached/recently-emitted nullifiers */ + private nullifiers: Nullifiers; /** World State Access Trace */ private trace: WorldStateAccessTrace; @@ -51,6 +53,7 @@ export class AvmPersistableStateManager { constructor(hostStorage: HostStorage, parent?: AvmPersistableStateManager) { this.hostStorage = hostStorage; this.publicStorage = new PublicStorage(hostStorage.publicStateDb, parent?.publicStorage); + this.nullifiers = new Nullifiers(hostStorage.nullifiersDb, parent?.nullifiers); this.trace = new WorldStateAccessTrace(parent?.trace); } @@ -69,8 +72,9 @@ export class AvmPersistableStateManager { * @param value - the value being written to the slot */ public writeStorage(storageAddress: Fr, slot: Fr, value: Fr) { + // Cache storage writes for later reference/reads this.publicStorage.write(storageAddress, slot, value); - // We want to keep track of all performed writes in the journal + // Trace all storage writes (even reverted ones) this.trace.tracePublicStorageWrite(storageAddress, slot, value); } @@ -83,7 +87,7 @@ export class AvmPersistableStateManager { */ public async readStorage(storageAddress: Fr, slot: Fr): Promise { const [_exists, value] = await this.publicStorage.read(storageAddress, slot); - // We want to keep track of all performed reads + // We want to keep track of all performed reads (even reverted ones) this.trace.tracePublicStorageRead(storageAddress, slot, value); return Promise.resolve(value); } @@ -92,8 +96,10 @@ export class AvmPersistableStateManager { this.trace.traceNewNoteHash(/*storageAddress*/ Fr.ZERO, noteHash); } - public writeNullifier(nullifier: Fr) { - // TODO track pending nullifiers in set per-contract + public async writeNullifier(nullifier: Fr) { + // Cache pending nullifiers for later access + await this.nullifiers.append(/*storageAddress*/ Fr.ZERO, nullifier); + // Trace all nullifier creations (even reverted ones) this.trace.traceNewNullifier(/*storageAddress*/ Fr.ZERO, nullifier); } diff --git a/yarn-project/simulator/src/avm/journal/nullifiers.test.ts b/yarn-project/simulator/src/avm/journal/nullifiers.test.ts new file mode 100644 index 000000000000..bebee082a95e --- /dev/null +++ b/yarn-project/simulator/src/avm/journal/nullifiers.test.ts @@ -0,0 +1,145 @@ +import { Fr } from '@aztec/foundation/fields'; + +import { MockProxy, mock } from 'jest-mock-extended'; + +import { NullifiersDB } from '../../index.js'; +import { Nullifiers } from './nullifiers.js'; + +describe('avm nullifier caching', () => { + let nullifiersDb: MockProxy; + let nullifiers: Nullifiers; + + beforeEach(() => { + nullifiersDb = mock(); + nullifiers = new Nullifiers(nullifiersDb); + }); + + describe('Nullifier caching and existence checks', () => { + it('Reading a non-existent nullifier works (gets zero & DNE)', async () => { + const contractAddress = new Fr(1); + const nullifier = new Fr(2); + // never written! + const [exists, isPending, gotIndex] = await nullifiers.getNullifierIndex(contractAddress, nullifier); + // doesn't exist, not pending, index is zero (non-existent) + expect(exists).toEqual(false); + expect(isPending).toEqual(false); + expect(gotIndex).toEqual(Fr.ZERO); + }); + it('Should cache pending nullifier, existence check works after creation', async () => { + const contractAddress = new Fr(1); + const nullifier = new Fr(2); + + // Write to cache + await nullifiers.append(contractAddress, nullifier); + const [exists, isPending, gotIndex] = await nullifiers.getNullifierIndex(contractAddress, nullifier); + // exists (in cache), isPending, index is zero (not in tree) + expect(exists).toEqual(true); + expect(isPending).toEqual(true); + expect(gotIndex).toEqual(Fr.ZERO); + }); + it('Existence check works on fallback to host (gets index, exists, not-pending)', async () => { + const contractAddress = new Fr(1); + const nullifier = new Fr(2); + const storedLeafIndex = BigInt(420); + + nullifiersDb.getNullifierIndex.mockResolvedValue(Promise.resolve(storedLeafIndex)); + + const [exists, isPending, gotIndex] = await nullifiers.getNullifierIndex(contractAddress, nullifier); + // exists (in host), not pending, tree index retrieved from host + expect(exists).toEqual(true); + expect(isPending).toEqual(false); + expect(gotIndex).toEqual(gotIndex); + }); + it('Existence check works on fallback to parent (gets value, exists, is pending)', async () => { + const contractAddress = new Fr(1); + const nullifier = new Fr(2); + const childNullifiers = new Nullifiers(nullifiersDb, nullifiers); + + // Write to parent cache + await nullifiers.append(contractAddress, nullifier); + // Get from child cache + const [exists, isPending, gotIndex] = await childNullifiers.getNullifierIndex(contractAddress, nullifier); + // exists (in parent), isPending, index is zero (not in tree) + expect(exists).toEqual(true); + expect(isPending).toEqual(true); + expect(gotIndex).toEqual(Fr.ZERO); + }); + }); + + describe('Nullifier collision failures', () => { + it('Cant append nullifier that already exists in cache', async () => { + const contractAddress = new Fr(1); + const nullifier = new Fr(2); // same nullifier for both! + + // Append a nullifier to cache + await nullifiers.append(contractAddress, nullifier); + // Can't append again + await expect(nullifiers.append(contractAddress, nullifier)).rejects.toThrowError( + `Nullifier ${nullifier} at contract ${contractAddress} already exists in parent cache or host.`, + ); + }); + it('Cant append nullifier that already exists in parent cache', async () => { + const contractAddress = new Fr(1); + const nullifier = new Fr(2); // same nullifier for both! + + // Append a nullifier to parent + await nullifiers.append(contractAddress, nullifier); + const childNullifiers = new Nullifiers(nullifiersDb, nullifiers); + // Can't append again in child + await expect(childNullifiers.append(contractAddress, nullifier)).rejects.toThrowError( + `Nullifier ${nullifier} at contract ${contractAddress} already exists in parent cache or host.`, + ); + }); + it('Cant append nullifier that already exist in host', async () => { + const contractAddress = new Fr(1); + const nullifier = new Fr(2); // same nullifier for both! + const storedLeafIndex = BigInt(420); + + // Nullifier exists in host + nullifiersDb.getNullifierIndex.mockResolvedValue(Promise.resolve(storedLeafIndex)); + // Can't append to cache + await expect(nullifiers.append(contractAddress, nullifier)).rejects.toThrowError( + `Nullifier ${nullifier} at contract ${contractAddress} already exists in parent cache or host.`, + ); + }); + }); + + describe('Nullifier cache merging', () => { + it('Should be able to merge two nullifier caches together', async () => { + const contractAddress = new Fr(1); + const nullifier0 = new Fr(2); + const nullifier1 = new Fr(3); + + // Append a nullifier to parent + await nullifiers.append(contractAddress, nullifier0); + + const childNullifiers = new Nullifiers(nullifiersDb, nullifiers); + // Append a nullifier to child + await childNullifiers.append(contractAddress, nullifier1); + + // Parent accepts child's nullifiers + nullifiers.acceptAndMerge(childNullifiers); + + // After merge, parent has both nullifiers + const results0 = await nullifiers.getNullifierIndex(contractAddress, nullifier0); + expect(results0).toEqual([/*exists=*/ true, /*isPending=*/ true, /*leafIndex=*/ Fr.ZERO]); + const results1 = await nullifiers.getNullifierIndex(contractAddress, nullifier1); + expect(results1).toEqual([/*exists=*/ true, /*isPending=*/ true, /*leafIndex=*/ Fr.ZERO]); + }); + it('Cant merge two nullifier caches with colliding entries', async () => { + const contractAddress = new Fr(1); + const nullifier = new Fr(2); + + // Append a nullifier to parent + await nullifiers.append(contractAddress, nullifier); + + // Create child cache, don't derive from parent so we can concoct a collision on merge + const childNullifiers = new Nullifiers(nullifiersDb); + // Append a nullifier to child + await childNullifiers.append(contractAddress, nullifier); + + // Parent accepts child's nullifiers + expect(() => nullifiers.acceptAndMerge(childNullifiers)).toThrowError(`Failed to accept child call's nullifiers. Nullifier ${nullifier.toBigInt()} already exists at contract ${contractAddress.toBigInt()}.`); + }); + }); +}); diff --git a/yarn-project/simulator/src/avm/journal/nullifiers.ts b/yarn-project/simulator/src/avm/journal/nullifiers.ts new file mode 100644 index 000000000000..b3dacbbf55f2 --- /dev/null +++ b/yarn-project/simulator/src/avm/journal/nullifiers.ts @@ -0,0 +1,156 @@ +import { Fr } from '@aztec/foundation/fields'; + +import type { NullifiersDB } from '../../index.js'; + +/** + * A class to manage new nullifier staging and existence checks during a contract call's AVM simulation. + * Maintains a pending nullifier cache, and ensures that existence checks fall back to the correct source. + * When a contract call completes, its pending nullifier set can be merged into its parent's. + */ +export class Nullifiers { + /** Cached nullifiers. */ + private cache: PendingNullifiers; + /** Parent's nullifier cache. Checked on cache-miss. */ + private readonly parentCache: PendingNullifiers | undefined; + /** Reference to node storage. Checked on parent cache-miss. */ + private readonly hostNullifiers: NullifiersDB; + + constructor(hostNullifiers: NullifiersDB, parent?: Nullifiers) { + this.hostNullifiers = hostNullifiers; + this.parentCache = parent?.cache; + this.cache = new PendingNullifiers(); + } + + /** + * Get a nullifier's leaf index and existence status. + * 1. Check cache. + * 2. Check parent's cache. + * 3. Fall back to the host state. + * 4. Not found! Nullifier does not exist. + * + * @param storageAddress - the address of the contract whose storage is being read from + * @param nullifier - the nullifier to check for + * @returns exists: whether the nullifier exists at all, + * isPending: whether the nullifier was found in a cache, + * leafIndex: the nullifier's leaf index if it exists and is not pending. + */ + public async getNullifierIndex( + storageAddress: Fr, + nullifier: Fr, + ): Promise<[/*exists=*/ boolean, /*isPending=*/ boolean, /*leafIndex=*/ Fr]> { + // First check this pending nullifiers + let existsAsPending = this.cache.exists(storageAddress, nullifier); + // Then check parent's pending nullifiers + if (!existsAsPending && this.parentCache) { + existsAsPending = this.parentCache?.exists(storageAddress, nullifier); + } + // Finally try the host's Aztec state (a trip to the database) + // If the value is found in the database, it will be associated with a leaf index! + let leafIndex: bigint | undefined = undefined; + if (!existsAsPending) { + leafIndex = await this.hostNullifiers.getNullifierIndex(nullifier); + } + const exists = existsAsPending || leafIndex !== undefined; + leafIndex = leafIndex === undefined ? BigInt(0) : leafIndex; + return Promise.resolve([exists, existsAsPending, new Fr(leafIndex)]); + } + + /** + * Stage a new nullifier (append it to the cache). + * + * @param storageAddress - the address of the contract that the nullifier is associated with + * @param nullifier - the nullifier to stage + */ + public async append(storageAddress: Fr, nullifier: Fr) { + const [exists, ,] = await this.getNullifierIndex(storageAddress, nullifier); + if (exists) { + throw new Error(`Nullifier ${nullifier} at contract ${storageAddress} already exists in parent cache or host.`); + } + this.cache.append(storageAddress, nullifier); + } + + /** + * Merges another Nullifiers' cache (pending nullifiers) into this one. + * + * @param incomingNullifiers - the incoming pending nullifiers to merge into this instance's + */ + public acceptAndMerge(incomingNullifiers: Nullifiers) { + this.cache.acceptAndMerge(incomingNullifiers.cache); + } +} + +/** + * A class to cache nullifiers created during a contract call's AVM simulation. + * "append" updates a map, "exists" checks that map. + * An instance of this class can merge another instance's pending nullifiers into its own. + */ +export class PendingNullifiers { + /** + * Map for staging pending nullifiers. + * One inner-set per contract storage address, + * each entry being a nullifier. + */ + private cachePerContract: Map> = new Map(); + + /** + * Check whether a pending nullifier exists in the cache. + * + * @param storageAddress - the address of the contract that the nullifier is associated with + * @param nullifier - the nullifier to check existence of + * @returns whether the nullifier is found in the cache + */ + public exists(storageAddress: Fr, nullifier: Fr): boolean { + const exists = this.cachePerContract.get(storageAddress.toBigInt())?.has(nullifier.toBigInt()); + return exists ? true : false; + } + + /** + * Stage a new nullifier (append it to the cache). + * + * @param storageAddress - the address of the contract that the nullifier is associated with + * @param nullifier - the nullifier to stage + */ + public append(storageAddress: Fr, nullifier: Fr) { + let nullifiersForContract = this.cachePerContract.get(storageAddress.toBigInt()); + // If this contract's nullifier set has no pending nullifiers, create a new Set to store them + if (!nullifiersForContract) { + nullifiersForContract = new Set(); + this.cachePerContract.set(storageAddress.toBigInt(), nullifiersForContract); + } + if (nullifiersForContract.has(nullifier.toBigInt())) { + throw new Error(`Nullifier ${nullifier} at contract ${storageAddress} already exists in cache.`); + } + nullifiersForContract.add(nullifier.toBigInt()); + } + + /** + * Merges a nested/child call's pending nullifiers into the current/parent. + * Merge another cache's pending nullifiers into this instance's. + * + * Pending nullifiers in "incoming" must not collide with any present in "this". + * + * In practice, "this" is a parent call's pending nullifiers, and "incoming" is a nested call's. + * + * @param incomingNullifiers - the incoming pending nullifiers to merge into this instance's + */ + public acceptAndMerge(incomingNullifiers: PendingNullifiers) { + // Iterate over all contracts with staged writes in the child. + for (const [incomingAddress, incomingCacheAtContract] of incomingNullifiers.cachePerContract) { + const thisCacheAtContract = this.cachePerContract.get(incomingAddress); + if (!thisCacheAtContract) { + // This contract has no pending nullifiers staged here + // so just accept incoming cache as-is for this contract. + this.cachePerContract.set(incomingAddress, incomingCacheAtContract); + } else { + // "Incoming" and "this" both have pending nullifiers for this contract. + // Merge in incoming nullifiers, erroring if there are any duplicates. + for (const nullifier of incomingCacheAtContract) { + if (thisCacheAtContract.has(nullifier)) { + throw new Error(`Failed to accept child call's nullifiers. Nullifier ${nullifier} already exists at contract ${incomingAddress}.`); + } + thisCacheAtContract.add(nullifier); + } + } + } + } +} diff --git a/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts b/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts index c7d1a13e86a5..eaba65fcee78 100644 --- a/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts +++ b/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts @@ -41,7 +41,7 @@ export class EmitNullifier extends Instruction { } const nullifier = context.machineState.memory.get(this.nullifierOffset).toFr(); - context.persistableState.writeNullifier(nullifier); + await context.persistableState.writeNullifier(nullifier); context.machineState.incrementPc(); } diff --git a/yarn-project/simulator/src/avm/opcodes/external_calls.test.ts b/yarn-project/simulator/src/avm/opcodes/external_calls.test.ts index a32678a3c806..fd4c6538a3e1 100644 --- a/yarn-project/simulator/src/avm/opcodes/external_calls.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/external_calls.test.ts @@ -3,7 +3,7 @@ import { Fr } from '@aztec/foundation/fields'; import { jest } from '@jest/globals'; import { mock } from 'jest-mock-extended'; -import { CommitmentsDB, PublicContractsDB, PublicStateDB } from '../../index.js'; +import { CommitmentsDB, NullifiersDB, PublicContractsDB, PublicStateDB } from '../../index.js'; import { AvmContext } from '../avm_context.js'; import { Field } from '../avm_memory_types.js'; import { initContext } from '../fixtures/index.js'; @@ -23,7 +23,8 @@ describe('External Calls', () => { const contractsDb = mock(); const commitmentsDb = mock(); const publicStateDb = mock(); - const hostStorage = new HostStorage(publicStateDb, contractsDb, commitmentsDb); + const nullifiersDb = mock(); + const hostStorage = new HostStorage(publicStateDb, contractsDb, commitmentsDb, nullifiersDb); const journal = new AvmPersistableStateManager(hostStorage); context = initContext({ worldState: journal }); }); diff --git a/yarn-project/simulator/src/public/db.ts b/yarn-project/simulator/src/public/db.ts index 26923cc366e4..caeaf85ea0a5 100644 --- a/yarn-project/simulator/src/public/db.ts +++ b/yarn-project/simulator/src/public/db.ts @@ -83,3 +83,12 @@ export interface CommitmentsDB { */ getCommitmentIndex(commitment: Fr): Promise; } + +export interface NullifiersDB { + /** + * Gets the index of a nullifier in the nullifier tree. + * @param nullifier - The nullifier. + * @returns - The index of the nullifier. Undefined if it does not exist in the tree. + */ + getNullifierIndex(nullifier: Fr): Promise; +} diff --git a/yarn-project/simulator/src/public/executor.ts b/yarn-project/simulator/src/public/executor.ts index dfc7b1b6c1f5..ea49cf3593c4 100644 --- a/yarn-project/simulator/src/public/executor.ts +++ b/yarn-project/simulator/src/public/executor.ts @@ -15,7 +15,7 @@ import { ExecutionError, createSimulationError } from '../common/errors.js'; import { SideEffectCounter } from '../common/index.js'; import { PackedArgsCache } from '../common/packed_args_cache.js'; import { AcirSimulator } from '../index.js'; -import { CommitmentsDB, PublicContractsDB, PublicStateDB } from './db.js'; +import { CommitmentsDB, NullifiersDB, PublicContractsDB, PublicStateDB } from './db.js'; import { PublicExecution, PublicExecutionResult, checkValidStaticCall } from './execution.js'; import { PublicExecutionContext } from './public_execution_context.js'; @@ -91,6 +91,7 @@ export class PublicExecutor { private readonly stateDb: PublicStateDB, private readonly contractsDb: PublicContractsDB, private readonly commitmentsDb: CommitmentsDB, + private readonly nullifiersDb: NullifiersDB, private readonly header: Header, ) {} @@ -157,7 +158,7 @@ export class PublicExecutor { ): Promise { // Temporary code to construct the AVM context // These data structures will permiate across the simulator when the public executor is phased out - const hostStorage = new HostStorage(this.stateDb, this.contractsDb, this.commitmentsDb); + const hostStorage = new HostStorage(this.stateDb, this.contractsDb, this.commitmentsDb, this.nullifiersDb); const worldStateJournal = new AvmPersistableStateManager(hostStorage); const executionEnv = temporaryCreateAvmExecutionEnvironment(execution, globalVariables); const machineState = new AvmMachineState(0, 0, 0); diff --git a/yarn-project/simulator/src/public/index.test.ts b/yarn-project/simulator/src/public/index.test.ts index 7171d3656de1..793f2fea0a2c 100644 --- a/yarn-project/simulator/src/public/index.test.ts +++ b/yarn-project/simulator/src/public/index.test.ts @@ -26,7 +26,7 @@ import { getFunctionSelector } from 'viem'; import { MessageLoadOracleInputs } from '../index.js'; import { buildL1ToL2Message } from '../test/utils.js'; import { computeSlotForMapping } from '../utils.js'; -import { CommitmentsDB, PublicContractsDB, PublicStateDB } from './db.js'; +import { CommitmentsDB, NullifiersDB, PublicContractsDB, PublicStateDB } from './db.js'; import { PublicExecution } from './execution.js'; import { PublicExecutor } from './executor.js'; @@ -36,6 +36,7 @@ describe('ACIR public execution simulator', () => { let publicState: MockProxy; let publicContracts: MockProxy; let commitmentsDb: MockProxy; + let nullifiersDb: MockProxy; let executor: PublicExecutor; let header: Header; @@ -47,7 +48,7 @@ describe('ACIR public execution simulator', () => { const randomInt = Math.floor(Math.random() * 1000000); header = makeHeader(randomInt); - executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, header); + executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, nullifiersDb, header); }, 10000); describe('Token contract', () => { @@ -498,7 +499,7 @@ describe('ACIR public execution simulator', () => { globalVariables = computeGlobalVariables(); const execution: PublicExecution = { contractAddress, functionData, args, callContext }; - executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, header); + executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, nullifiersDb, header); const result = await executor.simulate(execution, globalVariables); expect(result.newNullifiers.length).toEqual(1); }); @@ -516,7 +517,7 @@ describe('ACIR public execution simulator', () => { globalVariables = computeGlobalVariables(); const execution: PublicExecution = { contractAddress, functionData, args, callContext }; - executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, header); + executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, nullifiersDb, header); await expect(executor.simulate(execution, globalVariables)).rejects.toThrowError( 'Message not matching requested key', ); @@ -534,7 +535,7 @@ describe('ACIR public execution simulator', () => { globalVariables = computeGlobalVariables(); const execution: PublicExecution = { contractAddress, functionData, args, callContext }; - executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, header); + executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, nullifiersDb, header); await expect(executor.simulate(execution, globalVariables)).rejects.toThrowError('Message not in state'); }); @@ -549,7 +550,7 @@ describe('ACIR public execution simulator', () => { globalVariables = computeGlobalVariables(); const execution: PublicExecution = { contractAddress, functionData, args, callContext }; - executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, header); + executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, nullifiersDb, header); await expect(executor.simulate(execution, globalVariables)).rejects.toThrowError('Invalid recipient'); }); @@ -564,7 +565,7 @@ describe('ACIR public execution simulator', () => { globalVariables = computeGlobalVariables(); const execution: PublicExecution = { contractAddress, functionData, args, callContext }; - executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, header); + executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, nullifiersDb, header); await expect(executor.simulate(execution, globalVariables)).rejects.toThrowError('Invalid sender'); }); @@ -579,7 +580,7 @@ describe('ACIR public execution simulator', () => { globalVariables.chainId = Fr.random(); const execution: PublicExecution = { contractAddress, functionData, args, callContext }; - executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, header); + executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, nullifiersDb, header); await expect(executor.simulate(execution, globalVariables)).rejects.toThrowError('Invalid Chainid'); }); @@ -594,7 +595,7 @@ describe('ACIR public execution simulator', () => { globalVariables.version = Fr.random(); const execution: PublicExecution = { contractAddress, functionData, args, callContext }; - executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, header); + executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, nullifiersDb, header); await expect(executor.simulate(execution, globalVariables)).rejects.toThrowError('Invalid Version'); }); @@ -610,7 +611,7 @@ describe('ACIR public execution simulator', () => { globalVariables = computeGlobalVariables(); const execution: PublicExecution = { contractAddress, functionData, args, callContext }; - executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, header); + executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, nullifiersDb, header); await expect(executor.simulate(execution, globalVariables)).rejects.toThrowError('Invalid Content'); }); @@ -626,7 +627,7 @@ describe('ACIR public execution simulator', () => { globalVariables = computeGlobalVariables(); const execution: PublicExecution = { contractAddress, functionData, args, callContext }; - executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, header); + executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, nullifiersDb, header); await expect(executor.simulate(execution, globalVariables)).rejects.toThrowError('Invalid message secret'); }); }); @@ -696,7 +697,7 @@ describe('ACIR public execution simulator', () => { } const execution: PublicExecution = { contractAddress, functionData, args, callContext }; - executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, header); + executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, nullifiersDb, header); expect(() => executor.simulate(execution, globalVariables)).not.toThrow(); }); @@ -712,7 +713,7 @@ describe('ACIR public execution simulator', () => { } const execution: PublicExecution = { contractAddress, functionData, args, callContext }; - executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, header); + executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, nullifiersDb, header); await expect(executor.simulate(execution, globalVariables)).rejects.toThrowError( `Invalid ${description.toLowerCase()}`, @@ -752,7 +753,7 @@ describe('ACIR public execution simulator', () => { const args = encodeArguments(assertHeaderPublicArtifact, [header.hash()]); const execution: PublicExecution = { contractAddress, functionData, args, callContext }; - executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, header); + executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, nullifiersDb, header); expect(() => executor.simulate(execution, GlobalVariables.empty())).not.toThrow(); }); @@ -762,7 +763,7 @@ describe('ACIR public execution simulator', () => { const args = encodeArguments(assertHeaderPublicArtifact, [unexpectedHeaderHash]); const execution: PublicExecution = { contractAddress, functionData, args, callContext }; - executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, header); + executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, nullifiersDb, header); await expect(executor.simulate(execution, GlobalVariables.empty())).rejects.toThrowError('Invalid header hash'); });