diff --git a/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts b/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts index 435bc3e8be0..6691ad45c34 100644 --- a/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts +++ b/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts @@ -1,4 +1,4 @@ -import { type AccountWallet, AztecAddress, Fr, FunctionSelector, TxStatus } from '@aztec/aztec.js'; +import { type AccountWallet, AztecAddress, BatchCall, Fr, FunctionSelector, TxStatus } from '@aztec/aztec.js'; import { GasSettings } from '@aztec/circuits.js'; import { AvmAcvmInteropTestContract, @@ -60,6 +60,19 @@ describe('e2e_avm_simulator', () => { await avmContract.methods.add_storage_map(address, 100).send().wait(); expect(await avmContract.methods.view_storage_map(address).simulate()).toEqual(200n); }); + + it('Preserves storage across enqueued public calls', async () => { + const address = AztecAddress.fromBigInt(9090n); + // This will create 1 tx with 2 public calls in it. + await new BatchCall(wallet, [ + avmContract.methods.set_storage_map(address, 100).request(), + avmContract.methods.add_storage_map(address, 100).request(), + ]) + .send() + .wait(); + // On a separate tx, we check the result. + expect(await avmContract.methods.view_storage_map(address).simulate()).toEqual(200n); + }); }); describe('Contract instance', () => { @@ -85,6 +98,18 @@ describe('e2e_avm_simulator', () => { tx = await avmContract.methods.assert_nullifier_exists(nullifier).send().wait(); expect(tx.status).toEqual(TxStatus.MINED); }); + + it('Emit and check in separate enqueued calls but same tx', async () => { + const nullifier = new Fr(123456); + + // This will create 1 tx with 2 public calls in it. + await new BatchCall(wallet, [ + avmContract.methods.new_nullifier(nullifier).request(), + avmContract.methods.assert_nullifier_exists(nullifier).request(), + ]) + .send() + .wait(); + }); }); }); diff --git a/yarn-project/prover-client/src/mocks/test_context.ts b/yarn-project/prover-client/src/mocks/test_context.ts index 75abb24ada6..7dcdcf6ea93 100644 --- a/yarn-project/prover-client/src/mocks/test_context.ts +++ b/yarn-project/prover-client/src/mocks/test_context.ts @@ -1,5 +1,5 @@ import { type BlockProver, type ProcessedTx, type Tx, type TxValidator } from '@aztec/circuit-types'; -import { type Gas, GlobalVariables, Header, type TxContext } from '@aztec/circuits.js'; +import { type Gas, GlobalVariables, Header, type Nullifier, type TxContext } from '@aztec/circuits.js'; import { type Fr } from '@aztec/foundation/fields'; import { type DebugLogger } from '@aztec/foundation/log'; import { openTmpStore } from '@aztec/kv-store/utils'; @@ -129,6 +129,7 @@ export class TestContext { _globalVariables: GlobalVariables, availableGas: Gas, _txContext: TxContext, + _pendingNullifiers: Nullifier[], transactionFee?: Fr, _sideEffectCounter?: number, ) => { @@ -166,6 +167,7 @@ export class TestContext { globalVariables: GlobalVariables, availableGas: Gas, txContext: TxContext, + pendingNullifiers: Nullifier[], transactionFee?: Fr, sideEffectCounter?: number, ) => Promise, diff --git a/yarn-project/simulator/src/avm/journal/journal.ts b/yarn-project/simulator/src/avm/journal/journal.ts index 175ed4db2d1..3a47c4adebb 100644 --- a/yarn-project/simulator/src/avm/journal/journal.ts +++ b/yarn-project/simulator/src/avm/journal/journal.ts @@ -50,6 +50,8 @@ export type JournalData = { newLogsHashes: TracedUnencryptedL2Log[]; /** contract address -\> key -\> value */ currentStorageValue: Map>; + + sideEffectCounter: number; }; // TRANSITIONAL: This should be removed once the kernel handles and entire enqueued call per circuit @@ -143,6 +145,15 @@ export class AvmPersistableStateManager { this.publicStorage.write(storageAddress, slot, value); // TRANSITIONAL: This should be removed once the kernel handles and entire enqueued call per circuit + // The current info to the kernel clears any previous read or write request. + this.transitionalExecutionResult.contractStorageReads = + this.transitionalExecutionResult.contractStorageReads.filter( + read => !read.storageSlot.equals(slot) || !read.contractAddress!.equals(storageAddress), + ); + this.transitionalExecutionResult.contractStorageUpdateRequests = + this.transitionalExecutionResult.contractStorageUpdateRequests.filter( + update => !update.storageSlot.equals(slot) || !update.contractAddress!.equals(storageAddress), + ); this.transitionalExecutionResult.contractStorageUpdateRequests.push( new ContractStorageUpdateRequest(slot, value, this.trace.accessCounter, storageAddress), ); @@ -159,16 +170,24 @@ export class AvmPersistableStateManager { * @returns the latest value written to slot, or 0 if never written to before */ public async readStorage(storageAddress: Fr, slot: Fr): Promise { - const [exists, value] = await this.publicStorage.read(storageAddress, slot); - this.log.debug(`storage(${storageAddress})@${slot} ?? value: ${value}, exists: ${exists}.`); + const { value, exists, cached } = await this.publicStorage.read(storageAddress, slot); + this.log.debug(`storage(${storageAddress})@${slot} ?? value: ${value}, exists: ${exists}, cached: ${cached}.`); // TRANSITIONAL: This should be removed once the kernel handles and entire enqueued call per circuit - this.transitionalExecutionResult.contractStorageReads.push( - new ContractStorageRead(slot, value, this.trace.accessCounter, storageAddress), - ); + // The current info to the kernel kernel does not consider cached reads. + if (!cached) { + // The current info to the kernel removes any previous reads to the same slot. + this.transitionalExecutionResult.contractStorageReads = + this.transitionalExecutionResult.contractStorageReads.filter( + read => !read.storageSlot.equals(slot) || !read.contractAddress!.equals(storageAddress), + ); + this.transitionalExecutionResult.contractStorageReads.push( + new ContractStorageRead(slot, value, this.trace.accessCounter, storageAddress), + ); + } // We want to keep track of all performed reads (even reverted ones) - this.trace.tracePublicStorageRead(storageAddress, slot, value, exists); + this.trace.tracePublicStorageRead(storageAddress, slot, value, exists, cached); return Promise.resolve(value); } @@ -348,6 +367,7 @@ export class AvmPersistableStateManager { currentStorageValue: this.publicStorage.getCache().cachePerContract, storageReads: this.trace.publicStorageReads, storageWrites: this.trace.publicStorageWrites, + sideEffectCounter: this.trace.accessCounter, }; } } diff --git a/yarn-project/simulator/src/avm/journal/nullifiers.ts b/yarn-project/simulator/src/avm/journal/nullifiers.ts index f8374f6f8a7..99d12757e60 100644 --- a/yarn-project/simulator/src/avm/journal/nullifiers.ts +++ b/yarn-project/simulator/src/avm/journal/nullifiers.ts @@ -1,3 +1,4 @@ +import { AztecAddress } from '@aztec/circuits.js'; import { siloNullifier } from '@aztec/circuits.js/hash'; import { Fr } from '@aztec/foundation/fields'; @@ -10,7 +11,7 @@ import type { CommitmentsDB } from '../../index.js'; */ export class Nullifiers { /** Cached nullifiers. */ - private cache: NullifierCache; + public cache: NullifierCache; /** Parent's nullifier cache. Checked on cache-miss. */ private readonly parentCache: NullifierCache | undefined; /** Reference to node storage. Checked on parent cache-miss. */ @@ -95,6 +96,7 @@ export class NullifierCache { * each entry being a nullifier. */ private cachePerContract: Map> = new Map(); + private siloedNullifiers: Set = new Set(); /** * Check whether a nullifier exists in the cache. @@ -104,8 +106,10 @@ export class NullifierCache { * @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; + const exists = + this.cachePerContract.get(storageAddress.toBigInt())?.has(nullifier.toBigInt()) || + this.siloedNullifiers.has(siloNullifier(AztecAddress.fromField(storageAddress), nullifier).toBigInt()); + return !!exists; } /** @@ -115,20 +119,25 @@ export class NullifierCache { * @param nullifier - the nullifier to stage */ public append(storageAddress: Fr, nullifier: Fr) { + if (this.exists(storageAddress, nullifier)) { + throw new NullifierCollisionError( + `Nullifier ${nullifier} at contract ${storageAddress} already exists in cache.`, + ); + } + let nullifiersForContract = this.cachePerContract.get(storageAddress.toBigInt()); // If this contract's nullifier set has no cached 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 NullifierCollisionError( - `Nullifier ${nullifier} at contract ${storageAddress} already exists in cache.`, - ); - } nullifiersForContract.add(nullifier.toBigInt()); } + public appendSiloed(siloedNullifier: Fr) { + this.siloedNullifiers.add(siloedNullifier.toBigInt()); + } + /** * Merge another cache's nullifiers into this instance's. * @@ -139,6 +148,8 @@ export class NullifierCache { * @param incomingNullifiers - the incoming cached nullifiers to merge into this instance's */ public acceptAndMerge(incomingNullifiers: NullifierCache) { + // Merge siloed nullifiers. + this.siloedNullifiers = new Set([...this.siloedNullifiers, ...incomingNullifiers.siloedNullifiers]); // Iterate over all contracts with staged writes in the child. for (const [incomingAddress, incomingCacheAtContract] of incomingNullifiers.cachePerContract) { const thisCacheAtContract = this.cachePerContract.get(incomingAddress); diff --git a/yarn-project/simulator/src/avm/journal/public_storage.test.ts b/yarn-project/simulator/src/avm/journal/public_storage.test.ts index 33c747977d7..54633e40f96 100644 --- a/yarn-project/simulator/src/avm/journal/public_storage.test.ts +++ b/yarn-project/simulator/src/avm/journal/public_storage.test.ts @@ -19,22 +19,26 @@ describe('avm public storage', () => { const contractAddress = new Fr(1); const slot = new Fr(2); // never written! - const [exists, gotValue] = await publicStorage.read(contractAddress, slot); + const { exists, value: gotValue, cached } = await publicStorage.read(contractAddress, slot); // doesn't exist, value is zero expect(exists).toEqual(false); expect(gotValue).toEqual(Fr.ZERO); + expect(cached).toEqual(false); }); + it('Should cache storage write, reading works after write', async () => { const contractAddress = new Fr(1); const slot = new Fr(2); const value = new Fr(3); // Write to cache publicStorage.write(contractAddress, slot, value); - const [exists, gotValue] = await publicStorage.read(contractAddress, slot); + const { exists, value: gotValue, cached } = await publicStorage.read(contractAddress, slot); // exists because it was previously written expect(exists).toEqual(true); expect(gotValue).toEqual(value); + expect(cached).toEqual(true); }); + it('Reading works on fallback to host (gets value & exists)', async () => { const contractAddress = new Fr(1); const slot = new Fr(2); @@ -42,11 +46,13 @@ describe('avm public storage', () => { // ensure that fallback to host gets a value publicDb.storageRead.mockResolvedValue(Promise.resolve(storedValue)); - const [exists, gotValue] = await publicStorage.read(contractAddress, slot); + const { exists, value: gotValue, cached } = await publicStorage.read(contractAddress, slot); // it exists in the host, so it must've been written before expect(exists).toEqual(true); expect(gotValue).toEqual(storedValue); + expect(cached).toEqual(false); }); + it('Reading works on fallback to parent (gets value & exists)', async () => { const contractAddress = new Fr(1); const slot = new Fr(2); @@ -54,11 +60,13 @@ describe('avm public storage', () => { const childStorage = new PublicStorage(publicDb, publicStorage); publicStorage.write(contractAddress, slot, value); - const [exists, gotValue] = await childStorage.read(contractAddress, slot); + const { exists, value: gotValue, cached } = await childStorage.read(contractAddress, slot); // exists because it was previously written! expect(exists).toEqual(true); expect(gotValue).toEqual(value); + expect(cached).toEqual(true); }); + it('When reading from storage, should check cache, then parent, then host', async () => { // Store a different value in storage vs the cache, and make sure the cache is returned const contractAddress = new Fr(1); @@ -71,21 +79,24 @@ describe('avm public storage', () => { const childStorage = new PublicStorage(publicDb, publicStorage); // Cache miss falls back to host - const [, cacheMissResult] = await childStorage.read(contractAddress, slot); - expect(cacheMissResult).toEqual(storedValue); + const { cached: cachedCacheMiss, value: valueCacheMiss } = await childStorage.read(contractAddress, slot); + expect(valueCacheMiss).toEqual(storedValue); + expect(cachedCacheMiss).toEqual(false); // Write to storage publicStorage.write(contractAddress, slot, parentValue); // Reading from child should give value written in parent - const [, valueFromParent] = await childStorage.read(contractAddress, slot); + const { cached: cachedValueFromParent, value: valueFromParent } = await childStorage.read(contractAddress, slot); expect(valueFromParent).toEqual(parentValue); + expect(cachedValueFromParent).toEqual(true); // Now write a value directly in child childStorage.write(contractAddress, slot, cachedValue); // Reading should now give the value written in child - const [, cachedResult] = await childStorage.read(contractAddress, slot); + const { cached: cachedChild, value: cachedResult } = await childStorage.read(contractAddress, slot); expect(cachedResult).toEqual(cachedValue); + expect(cachedChild).toEqual(true); }); }); @@ -109,7 +120,7 @@ describe('avm public storage', () => { publicStorage.acceptAndMerge(childStorage); // Read from parent gives latest value written in child before merge (valueT1) - const [exists, result] = await publicStorage.read(contractAddress, slot); + const { exists, value: result } = await publicStorage.read(contractAddress, slot); expect(exists).toEqual(true); expect(result).toEqual(valueT1); }); diff --git a/yarn-project/simulator/src/avm/journal/public_storage.ts b/yarn-project/simulator/src/avm/journal/public_storage.ts index f6b3b7a35d8..30c59e421b3 100644 --- a/yarn-project/simulator/src/avm/journal/public_storage.ts +++ b/yarn-project/simulator/src/avm/journal/public_storage.ts @@ -1,7 +1,14 @@ +import { AztecAddress } from '@aztec/circuits.js'; import { Fr } from '@aztec/foundation/fields'; import type { PublicStateDB } from '../../index.js'; +type PublicStorageReadResult = { + value: Fr; + exists: boolean; + cached: boolean; +}; + /** * A class to manage public storage reads and writes during a contract call's AVM simulation. * Maintains a storage write cache, and ensures that reads fall back to the correct source. @@ -39,7 +46,8 @@ export class PublicStorage { * @param slot - the slot in the contract's storage being read from * @returns exists: whether the slot has EVER been written to before, value: the latest value written to slot, or 0 if never written to before */ - public async read(storageAddress: Fr, slot: Fr): Promise<[/*exists=*/ boolean, /*value=*/ Fr]> { + public async read(storageAddress: Fr, slot: Fr): Promise { + let cached = false; // First try check this storage cache let value = this.cache.read(storageAddress, slot); // Then try parent's storage cache (if it exists / written to earlier in this TX) @@ -49,11 +57,13 @@ export class PublicStorage { // Finally try the host's Aztec state (a trip to the database) if (!value) { value = await this.hostPublicStorage.storageRead(storageAddress, slot); + } else { + cached = true; } // if value is undefined, that means this slot has never been written to! const exists = value !== undefined; const valueOrZero = exists ? value : Fr.ZERO; - return Promise.resolve([exists, valueOrZero]); + return Promise.resolve({ value: valueOrZero, exists, cached }); } /** @@ -75,6 +85,17 @@ export class PublicStorage { public acceptAndMerge(incomingPublicStorage: PublicStorage) { this.cache.acceptAndMerge(incomingPublicStorage.cache); } + + /** + * Commits ALL staged writes to the host's state. + */ + public async commitToDB() { + for (const [storageAddress, cacheAtContract] of this.cache.cachePerContract) { + for (const [slot, value] of cacheAtContract) { + await this.hostPublicStorage.storageWrite(AztecAddress.fromBigInt(storageAddress), new Fr(slot), value); + } + } + } } /** diff --git a/yarn-project/simulator/src/avm/journal/trace.test.ts b/yarn-project/simulator/src/avm/journal/trace.test.ts index e19c14dc706..f8692e01c84 100644 --- a/yarn-project/simulator/src/avm/journal/trace.test.ts +++ b/yarn-project/simulator/src/avm/journal/trace.test.ts @@ -110,7 +110,7 @@ describe('world state access trace', () => { let counter = 0; trace.tracePublicStorageWrite(contractAddress, slot, value); counter++; - trace.tracePublicStorageRead(contractAddress, slot, value, /*exists=*/ true); + trace.tracePublicStorageRead(contractAddress, slot, value, /*exists=*/ true, /*cached=*/ true); counter++; trace.traceNoteHashCheck(contractAddress, noteHash, noteHashExists, noteHashLeafIndex); counter++; @@ -124,7 +124,7 @@ describe('world state access trace', () => { counter++; trace.tracePublicStorageWrite(contractAddress, slot, value); counter++; - trace.tracePublicStorageRead(contractAddress, slot, value, /*exists=*/ true); + trace.tracePublicStorageRead(contractAddress, slot, value, /*exists=*/ true, /*cached=*/ true); counter++; trace.traceNewNoteHash(contractAddress, noteHash); counter++; @@ -178,7 +178,7 @@ describe('world state access trace', () => { }; trace.tracePublicStorageWrite(contractAddress, slot, value); - trace.tracePublicStorageRead(contractAddress, slot, value, /*exists=*/ true); + trace.tracePublicStorageRead(contractAddress, slot, value, /*exists=*/ true, /*cached=*/ true); trace.traceNoteHashCheck(contractAddress, noteHash, noteHashExists, noteHashLeafIndex); trace.traceNewNoteHash(contractAddress, noteHash); trace.traceNullifierCheck(contractAddress, nullifier, nullifierExists, nullifierIsPending, nullifierLeafIndex); @@ -187,7 +187,7 @@ describe('world state access trace', () => { const childTrace = new WorldStateAccessTrace(trace); childTrace.tracePublicStorageWrite(contractAddress, slot, valueT1); - childTrace.tracePublicStorageRead(contractAddress, slot, valueT1, /*exists=*/ true); + childTrace.tracePublicStorageRead(contractAddress, slot, valueT1, /*exists=*/ true, /*cached=*/ true); childTrace.traceNoteHashCheck(contractAddress, noteHashT1, noteHashExistsT1, noteHashLeafIndexT1); childTrace.traceNewNoteHash(contractAddress, nullifierT1); childTrace.traceNullifierCheck( @@ -205,8 +205,20 @@ describe('world state access trace', () => { expect(trace.getAccessCounter()).toEqual(childCounterBeforeMerge); expect(trace.publicStorageReads).toEqual([ - expect.objectContaining({ storageAddress: contractAddress, slot: slot, value: value, exists: true }), - expect.objectContaining({ storageAddress: contractAddress, slot: slot, value: valueT1, exists: true }), + expect.objectContaining({ + storageAddress: contractAddress, + slot: slot, + value: value, + exists: true, + cached: true, + }), + expect.objectContaining({ + storageAddress: contractAddress, + slot: slot, + value: valueT1, + exists: true, + cached: true, + }), ]); expect(trace.publicStorageWrites).toEqual([ expect.objectContaining({ storageAddress: contractAddress, slot: slot, value: value }), diff --git a/yarn-project/simulator/src/avm/journal/trace.ts b/yarn-project/simulator/src/avm/journal/trace.ts index 5ca5be9dbc1..da857f984fc 100644 --- a/yarn-project/simulator/src/avm/journal/trace.ts +++ b/yarn-project/simulator/src/avm/journal/trace.ts @@ -36,7 +36,7 @@ export class WorldStateAccessTrace { return this.accessCounter; } - public tracePublicStorageRead(storageAddress: Fr, slot: Fr, value: Fr, exists: boolean) { + public tracePublicStorageRead(storageAddress: Fr, slot: Fr, value: Fr, exists: boolean, cached: boolean) { // TODO(4805): check if some threshold is reached for max storage reads // (need access to parent length, or trace needs to be initialized with parent's contents) const traced: TracedPublicStorageRead = { @@ -45,6 +45,7 @@ export class WorldStateAccessTrace { slot, value, exists, + cached, counter: new Fr(this.accessCounter), // endLifetime: Fr.ZERO, }; diff --git a/yarn-project/simulator/src/avm/journal/trace_types.ts b/yarn-project/simulator/src/avm/journal/trace_types.ts index 3c93649cefd..4c292146a7b 100644 --- a/yarn-project/simulator/src/avm/journal/trace_types.ts +++ b/yarn-project/simulator/src/avm/journal/trace_types.ts @@ -11,6 +11,7 @@ export type TracedPublicStorageRead = { // callPointer: Fr; storageAddress: Fr; exists: boolean; + cached: boolean; slot: Fr; value: Fr; counter: Fr; diff --git a/yarn-project/simulator/src/public/abstract_phase_manager.ts b/yarn-project/simulator/src/public/abstract_phase_manager.ts index e3084193263..f6a7d1848ad 100644 --- a/yarn-project/simulator/src/public/abstract_phase_manager.ts +++ b/yarn-project/simulator/src/public/abstract_phase_manager.ts @@ -249,6 +249,7 @@ export abstract class AbstractPhaseManager { // TODO(6052): Extract correct new counter from nested calls const sideEffectCounter = lastSideEffectCounter(tx) + 1; const availableGas = this.getAvailableGas(tx, kernelOutput); + const pendingNullifiers = this.getSiloedPendingNullifiers(kernelOutput); const result = isExecutionRequest ? await this.publicExecutor.simulate( @@ -256,6 +257,7 @@ export abstract class AbstractPhaseManager { this.globalVariables, availableGas, tx.data.constants.txContext, + pendingNullifiers, transactionFee, sideEffectCounter, ) @@ -323,6 +325,11 @@ export abstract class AbstractPhaseManager { return [publicKernelInputs, kernelOutput, kernelProof, newUnencryptedFunctionLogs, undefined, returns]; } + /** Returns all pending private and public nullifiers. */ + private getSiloedPendingNullifiers(ko: PublicKernelCircuitPublicInputs) { + return [...ko.end.newNullifiers, ...ko.endNonRevertibleData.newNullifiers].filter(n => !n.isEmpty()); + } + protected getAvailableGas(tx: Tx, previousPublicKernelOutput: PublicKernelCircuitPublicInputs) { return tx.data.constants.txContext.gasSettings .getLimits() // No need to subtract teardown limits since they are already included in end.gasUsed diff --git a/yarn-project/simulator/src/public/executor.ts b/yarn-project/simulator/src/public/executor.ts index d00ea70af12..89951608404 100644 --- a/yarn-project/simulator/src/public/executor.ts +++ b/yarn-project/simulator/src/public/executor.ts @@ -4,6 +4,7 @@ import { Gas, type GlobalVariables, type Header, + type Nullifier, PublicCircuitPublicInputs, type TxContext, } from '@aztec/circuits.js'; @@ -46,7 +47,7 @@ export async function executePublicFunction( } if (isAvmBytecode(bytecode)) { - return await executeTopLevelPublicFunctionAvm(context); + return await executeTopLevelPublicFunctionAvm(context, bytecode); } else { return await executePublicFunctionAcvm(context, bytecode, nested); } @@ -58,6 +59,7 @@ export async function executePublicFunction( */ async function executeTopLevelPublicFunctionAvm( executionContext: PublicExecutionContext, + bytecode: Buffer, ): Promise { const address = executionContext.execution.contractAddress; const selector = executionContext.execution.functionData.selector; @@ -77,6 +79,9 @@ async function executeTopLevelPublicFunctionAvm( // or modify the PersistableStateManager to manage rollbacks across enqueued-calls and transactions. const worldStateJournal = new AvmPersistableStateManager(hostStorage); const startSideEffectCounter = executionContext.execution.callContext.sideEffectCounter; + for (const nullifier of executionContext.pendingNullifiers) { + worldStateJournal.nullifiers.cache.appendSiloed(nullifier.value); + } worldStateJournal.trace.accessCounter = startSideEffectCounter; const executionEnv = createAvmExecutionEnvironment( @@ -91,7 +96,12 @@ async function executeTopLevelPublicFunctionAvm( const avmContext = new AvmContext(worldStateJournal, executionEnv, machineState); const simulator = new AvmSimulator(avmContext); - const avmResult = await simulator.execute(); + const avmResult = await simulator.executeBytecode(bytecode); + + // Commit the journals state to the DBs since this is a top-level execution. + // Observe that this will write all the state changes to the DBs, not only the latest for each slot. + // However, the underlying DB keep a cache and will only write the latest state to disk. + await avmContext.persistableState.publicStorage.commitToDB(); log.verbose( `[AVM] ${address.toString()}:${selector} returned, reverted: ${avmResult.reverted}, reason: ${ @@ -99,8 +109,12 @@ async function executeTopLevelPublicFunctionAvm( }.`, ); - return Promise.resolve( - convertAvmResultsToPxResult(avmResult, startSideEffectCounter, executionContext.execution, startGas, avmContext), + return convertAvmResultsToPxResult( + avmResult, + startSideEffectCounter, + executionContext.execution, + startGas, + avmContext, ); } @@ -279,6 +293,7 @@ export class PublicExecutor { globalVariables: GlobalVariables, availableGas: Gas, txContext: TxContext, + pendingNullifiers: Nullifier[], transactionFee: Fr = Fr.ZERO, sideEffectCounter: number = 0, ): Promise { @@ -298,6 +313,7 @@ export class PublicExecutor { availableGas, transactionFee, txContext.gasSettings, + pendingNullifiers, ); const executionResult = await executePublicFunction(context, /*nested=*/ false); diff --git a/yarn-project/simulator/src/public/index.test.ts b/yarn-project/simulator/src/public/index.test.ts index 14995aa727a..81dd93a2712 100644 --- a/yarn-project/simulator/src/public/index.test.ts +++ b/yarn-project/simulator/src/public/index.test.ts @@ -99,7 +99,7 @@ describe('ACIR public execution simulator', () => { }); const simulate = (execution: PublicExecution, globalVariables: GlobalVariables) => - executor.simulate(execution, globalVariables, Gas.test(), makeTxContext(), Fr.ZERO); + executor.simulate(execution, globalVariables, Gas.test(), makeTxContext(), /*pendingNullifiers=*/ [], Fr.ZERO); describe('Token contract', () => { let recipient: AztecAddress; diff --git a/yarn-project/simulator/src/public/public_execution_context.ts b/yarn-project/simulator/src/public/public_execution_context.ts index bcd468aa568..c6279209ecf 100644 --- a/yarn-project/simulator/src/public/public_execution_context.ts +++ b/yarn-project/simulator/src/public/public_execution_context.ts @@ -7,6 +7,7 @@ import { type GasSettings, type GlobalVariables, type Header, + type Nullifier, PublicContextInputs, } from '@aztec/circuits.js'; import { type AztecAddress } from '@aztec/foundation/aztec-address'; @@ -45,6 +46,7 @@ export class PublicExecutionContext extends TypedOracle { public readonly availableGas: Gas, public readonly transactionFee: Fr, public readonly gasSettings: GasSettings, + public readonly pendingNullifiers: Nullifier[], // Unencrypted logs emitted during this call AND any nested calls // Useful for maintaining correct ordering in ts private allUnencryptedLogs: UnencryptedL2Log[] = [], @@ -239,6 +241,7 @@ export class PublicExecutionContext extends TypedOracle { this.availableGas, this.transactionFee, this.gasSettings, + /*pendingNullifiers=*/ [], this.allUnencryptedLogs, this.log, ); diff --git a/yarn-project/simulator/src/public/public_processor.test.ts b/yarn-project/simulator/src/public/public_processor.test.ts index 94e666fd524..33c25cab600 100644 --- a/yarn-project/simulator/src/public/public_processor.test.ts +++ b/yarn-project/simulator/src/public/public_processor.test.ts @@ -764,6 +764,7 @@ describe('public_processor', () => { expect.anything(), // GlobalVariables Gas.from(availableGas), expect.anything(), // TxContext + expect.anything(), // pendingNullifiers new Fr(txFee), expect.anything(), // SideEffectCounter ]; diff --git a/yarn-project/simulator/src/public/transitional_adaptors.ts b/yarn-project/simulator/src/public/transitional_adaptors.ts index 09b2e2d99fc..5e241a6a315 100644 --- a/yarn-project/simulator/src/public/transitional_adaptors.ts +++ b/yarn-project/simulator/src/public/transitional_adaptors.ts @@ -1,31 +1,21 @@ // All code in this file needs to die once the public executor is phased out in favor of the AVM. -import { UnencryptedFunctionL2Logs, UnencryptedL2Log } from '@aztec/circuit-types'; +import { UnencryptedFunctionL2Logs } from '@aztec/circuit-types'; import { CallContext, - ContractStorageRead, - ContractStorageUpdateRequest, FunctionData, type Gas, type GasSettings, type GlobalVariables, type Header, - L2ToL1Message, - NoteHash, - Nullifier, - ReadRequest, - SideEffect, } from '@aztec/circuits.js'; import { Fr } from '@aztec/foundation/fields'; import { type AvmContext } from '../avm/avm_context.js'; import { AvmExecutionEnvironment } from '../avm/avm_execution_environment.js'; -import { type AvmMachineState } from '../avm/avm_machine_state.js'; -import { AvmContractCallResults } from '../avm/avm_message_call_result.js'; -import { type JournalData } from '../avm/journal/journal.js'; +import { type AvmContractCallResults } from '../avm/avm_message_call_result.js'; import { Mov } from '../avm/opcodes/memory.js'; import { createSimulationError } from '../common/errors.js'; import { type PublicExecution, type PublicExecutionResult } from './execution.js'; -import { type PublicExecutionContext } from './public_execution_context.js'; /** * Convert a PublicExecution(Environment) object to an AvmExecutionEnvironment @@ -109,97 +99,6 @@ export function convertAvmResultsToPxResult( }; } -/** - * Convert the result of an AVM contract call to a PublicExecutionResult for the public kernel - * - * @param execution - * @param newWorldState - * @param result - * @returns - */ -export async function convertAvmResults( - executionContext: PublicExecutionContext, - newWorldState: JournalData, - result: AvmContractCallResults, - endMachineState: AvmMachineState, -): Promise { - const execution = executionContext.execution; - - const contractStorageReads: ContractStorageRead[] = newWorldState.storageReads.map( - read => new ContractStorageRead(read.slot, read.value, read.counter.toNumber(), read.storageAddress), - ); - const contractStorageUpdateRequests: ContractStorageUpdateRequest[] = newWorldState.storageWrites.map( - write => new ContractStorageUpdateRequest(write.slot, write.value, write.counter.toNumber(), write.storageAddress), - ); - // We need to write the storage updates to the DB, because that's what the ACVM expects. - // Assumes the updates are in the right order. - for (const write of newWorldState.storageWrites) { - await executionContext.stateDb.storageWrite(write.storageAddress, write.slot, write.value); - } - - const newNoteHashes = newWorldState.newNoteHashes.map( - noteHash => new NoteHash(noteHash.noteHash, noteHash.counter.toNumber()), - ); - const nullifierReadRequests: ReadRequest[] = newWorldState.nullifierChecks - .filter(nullifierCheck => nullifierCheck.exists) - .map(nullifierCheck => new ReadRequest(nullifierCheck.nullifier, nullifierCheck.counter.toNumber())); - const nullifierNonExistentReadRequests: ReadRequest[] = newWorldState.nullifierChecks - .filter(nullifierCheck => !nullifierCheck.exists) - .map(nullifierCheck => new ReadRequest(nullifierCheck.nullifier, nullifierCheck.counter.toNumber())); - const newNullifiers: Nullifier[] = newWorldState.newNullifiers.map( - tracedNullifier => - new Nullifier( - /*value=*/ tracedNullifier.nullifier, - tracedNullifier.counter.toNumber(), - /*noteHash=*/ Fr.ZERO, // NEEDED? - ), - ); - const unencryptedLogs: UnencryptedFunctionL2Logs = new UnencryptedFunctionL2Logs( - newWorldState.newLogs.map(log => new UnencryptedL2Log(log.contractAddress, log.selector, log.data)), - ); - const unencryptedLogsHashes = newWorldState.newLogsHashes.map( - logHash => new SideEffect(logHash.logHash, logHash.counter), - ); - const newL2ToL1Messages = newWorldState.newL1Messages.map(m => new L2ToL1Message(m.recipient, m.content)); - - const returnValues = result.output; - - // TODO: Support nested executions. - const nestedExecutions: PublicExecutionResult[] = []; - const allUnencryptedLogs = unencryptedLogs; - // TODO keep track of side effect counters - const startSideEffectCounter = Fr.ZERO; - const endSideEffectCounter = Fr.ZERO; - - return { - execution, - nullifierReadRequests, - nullifierNonExistentReadRequests, - newNoteHashes, - newL2ToL1Messages, - startSideEffectCounter, - endSideEffectCounter, - newNullifiers, - contractStorageReads, - contractStorageUpdateRequests, - returnValues, - nestedExecutions, - unencryptedLogsHashes, - unencryptedLogs, - unencryptedLogPreimagesLength: new Fr(unencryptedLogs.getSerializedLength()), - allUnencryptedLogs, - reverted: result.reverted, - revertReason: result.revertReason ? createSimulationError(result.revertReason) : undefined, - startGasLeft: executionContext.availableGas, - endGasLeft: endMachineState.gasLeft, - transactionFee: executionContext.transactionFee, - }; -} - -export function convertPublicExecutionResult(res: PublicExecutionResult): AvmContractCallResults { - return new AvmContractCallResults(res.reverted, res.returnValues, res.revertReason); -} - const AVM_MAGIC_SUFFIX = Buffer.from([ Mov.opcode, // opcode 0x00, // indirect