diff --git a/noir-projects/noir-contracts/contracts/avm_test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/avm_test_contract/src/main.nr index d870e8564f88..a9dd932bdace 100644 --- a/noir-projects/noir-contracts/contracts/avm_test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/avm_test_contract/src/main.nr @@ -363,19 +363,19 @@ contract AvmTest { // Use the standard context interface to check for a nullifier #[aztec(public)] fn nullifier_exists(nullifier: Field) -> bool { - context.nullifier_exists(nullifier, context.this_address()) + context.nullifier_exists(nullifier, context.storage_address()) } #[aztec(public)] fn assert_nullifier_exists(nullifier: Field) { - assert(context.nullifier_exists(nullifier, context.this_address()), "Nullifier doesn't exist!"); + assert(context.nullifier_exists(nullifier, context.storage_address()), "Nullifier doesn't exist!"); } // Use the standard context interface to emit a new nullifier #[aztec(public)] fn emit_nullifier_and_check(nullifier: Field) { context.push_new_nullifier(nullifier, 0); - let exists = context.nullifier_exists(nullifier, context.this_address()); + let exists = context.nullifier_exists(nullifier, context.storage_address()); assert(exists, "Nullifier was just created, but its existence wasn't detected!"); } diff --git a/yarn-project/circuits.js/src/structs/contract_storage_update_request.ts b/yarn-project/circuits.js/src/structs/contract_storage_update_request.ts index 04be2dd24f9d..00fcf5b743ca 100644 --- a/yarn-project/circuits.js/src/structs/contract_storage_update_request.ts +++ b/yarn-project/circuits.js/src/structs/contract_storage_update_request.ts @@ -24,12 +24,15 @@ export class ContractStorageUpdateRequest { /** * Optional side effect counter tracking position of this event in tx execution. */ - public readonly sideEffectCounter: number, + public readonly counter: number, + /** + * Contract address whose storage is being read. + */ public contractAddress?: AztecAddress, // TODO: Should not be optional. This is a temporary hack to silo the storage slot with the correct address for nested executions. ) {} toBuffer() { - return serializeToBuffer(this.storageSlot, this.newValue, this.sideEffectCounter); + return serializeToBuffer(this.storageSlot, this.newValue, this.counter); } static fromBuffer(buffer: Buffer | BufferReader) { @@ -52,7 +55,7 @@ export class ContractStorageUpdateRequest { * @returns The array. */ static getFields(fields: FieldsOf) { - return [fields.storageSlot, fields.newValue, fields.sideEffectCounter, fields.contractAddress] as const; + return [fields.storageSlot, fields.newValue, fields.counter, fields.contractAddress] as const; } static empty() { @@ -65,12 +68,12 @@ export class ContractStorageUpdateRequest { toFriendlyJSON() { return `Slot=${this.storageSlot.toFriendlyJSON()}: ${this.newValue.toFriendlyJSON()}, sideEffectCounter=${ - this.sideEffectCounter + this.counter }`; } toFields(): Fr[] { - const fields = [this.storageSlot, this.newValue, new Fr(this.sideEffectCounter)]; + const fields = [this.storageSlot, this.newValue, new Fr(this.counter)]; if (fields.length !== CONTRACT_STORAGE_UPDATE_REQUEST_LENGTH) { throw new Error( `Invalid number of fields for ContractStorageUpdateRequest. Expected ${CONTRACT_STORAGE_UPDATE_REQUEST_LENGTH}, got ${fields.length}`, diff --git a/yarn-project/end-to-end/src/e2e_block_building.test.ts b/yarn-project/end-to-end/src/e2e_block_building.test.ts index 9a308a2e523a..0bc3a357826a 100644 --- a/yarn-project/end-to-end/src/e2e_block_building.test.ts +++ b/yarn-project/end-to-end/src/e2e_block_building.test.ts @@ -17,8 +17,6 @@ import { StatefulTestContractArtifact } from '@aztec/noir-contracts.js'; import { TestContract } from '@aztec/noir-contracts.js/Test'; import { TokenContract } from '@aztec/noir-contracts.js/Token'; -import 'jest-extended'; - import { TaggedLog } from '../../circuit-types/src/logs/l1_payload/tagged_log.js'; import { DUPLICATE_NULLIFIER_ERROR } from './fixtures/fixtures.js'; import { setup } from './fixtures/utils.js'; diff --git a/yarn-project/simulator/src/avm/avm_execution_environment.ts b/yarn-project/simulator/src/avm/avm_execution_environment.ts index 411b9d60ff49..f0898695fbd3 100644 --- a/yarn-project/simulator/src/avm/avm_execution_environment.ts +++ b/yarn-project/simulator/src/avm/avm_execution_environment.ts @@ -62,7 +62,7 @@ export class AvmExecutionEnvironment { /*sender=*/ this.address, this.feePerL2Gas, this.feePerDaGas, - this.contractCallDepth, + this.contractCallDepth.add(Fr.ONE), this.header, this.globals, isStaticCall, diff --git a/yarn-project/simulator/src/avm/avm_simulator.test.ts b/yarn-project/simulator/src/avm/avm_simulator.test.ts index 1614305b0bea..f5a17fc70cda 100644 --- a/yarn-project/simulator/src/avm/avm_simulator.test.ts +++ b/yarn-project/simulator/src/avm/avm_simulator.test.ts @@ -1,13 +1,13 @@ -import { UnencryptedL2Log } from '@aztec/circuit-types'; import { Grumpkin } from '@aztec/circuits.js/barretenberg'; import { computeVarArgsHash } from '@aztec/circuits.js/hash'; -import { EventSelector, FunctionSelector } from '@aztec/foundation/abi'; +import { FunctionSelector } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { keccak256, pedersenHash, poseidon2Hash, sha256 } from '@aztec/foundation/crypto'; import { Fq, Fr } from '@aztec/foundation/fields'; import { type Fieldable } from '@aztec/foundation/serialize'; import { jest } from '@jest/globals'; +import 'jest-extended'; import { isAvmBytecode, markBytecodeAsAvm } from '../public/transitional_adaptors.js'; import { AvmMachineState } from './avm_machine_state.js'; @@ -19,12 +19,21 @@ import { initContext, initExecutionEnvironment, initGlobalVariables, + initHostStorage, initMachineState, + initPersistableStateManager, randomMemoryBytes, randomMemoryFields, } from './fixtures/index.js'; import { Add, CalldataCopy, Return } from './opcodes/index.js'; import { encodeToBytecode } from './serialization/bytecode_serialization.js'; +import { mock } from 'jest-mock-extended'; +import { PublicSideEffectTraceInterface } from '../public/side_effect_trace_interface.js'; +import { AvmPersistableStateManager } from './journal/journal.js'; +import { HostStorage } from './journal/host_storage.js'; +import { AvmContractCallResults } from './avm_message_call_result.js'; +import { AvmExecutionEnvironment } from './avm_execution_environment.js'; +import { CommitmentsDB } from '../public/db_interfaces.js'; describe('AVM simulator: injected bytecode', () => { let calldata: Fr[]; @@ -314,634 +323,500 @@ describe('AVM simulator: transpiled Noir contracts', () => { }); }); - describe('Tree access (notes & nullifiers)', () => { - it(`Note hash exists (it does not)`, async () => { - const noteHash = new Fr(42); - const leafIndex = new Fr(7); - const calldata = [noteHash, leafIndex]; - - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - const bytecode = getAvmTestContractBytecode('note_hash_exists'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - expect(results.reverted).toBe(false); - expect(results.output).toEqual([/*exists=false*/ new Fr(0)]); - - // Note hash existence check should be in trace - const trace = context.persistableState.flush(); - expect(trace.noteHashChecks).toEqual([expect.objectContaining({ noteHash, leafIndex, exists: false })]); - }); - - it(`Note hash exists (it does)`, async () => { - const noteHash = new Fr(42); - const leafIndex = new Fr(7); - const calldata = [noteHash, leafIndex]; - - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - // note hash exists! - jest - .spyOn(context.persistableState.hostStorage.commitmentsDb, 'getCommitmentIndex') - .mockReturnValue(Promise.resolve(BigInt(7))); - const bytecode = getAvmTestContractBytecode('note_hash_exists'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - expect(results.reverted).toBe(false); - expect(results.output).toEqual([/*exists=true*/ new Fr(1)]); - - // Note hash existence check should be in trace - const trace = context.persistableState.flush(); - expect(trace.noteHashChecks).toEqual([expect.objectContaining({ noteHash, leafIndex, exists: true })]); - }); - - it(`Emit unencrypted logs (should be traced)`, async () => { - const context = initContext(); - const bytecode = getAvmTestContractBytecode('emit_unencrypted_log'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - expect(results.reverted).toBe(false); - - const expectedFields = [new Fr(10), new Fr(20), new Fr(30)]; - const expectedString = 'Hello, world!'.split('').map(c => new Fr(c.charCodeAt(0))); - const expectedCompressedString = Buffer.from( - '\0A long time ago, in a galaxy fa' + '\0r far away...\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0', - ); - expect(context.persistableState.flush().newLogs).toEqual([ - new UnencryptedL2Log( - context.environment.address, - new EventSelector(5), - Buffer.concat(expectedFields.map(f => f.toBuffer())), - ), - new UnencryptedL2Log( - context.environment.address, - new EventSelector(5), - Buffer.concat(expectedString.map(f => f.toBuffer())), - ), - new UnencryptedL2Log(context.environment.address, new EventSelector(5), expectedCompressedString), - ]); - }); - - it(`Emit note hash (should be traced)`, async () => { - const utxo = new Fr(42); - const calldata = [utxo]; - - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - const bytecode = getAvmTestContractBytecode('new_note_hash'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - expect(results.reverted).toBe(false); - - expect(context.persistableState.flush().newNoteHashes).toEqual([ - expect.objectContaining({ - storageAddress: context.environment.storageAddress, - noteHash: utxo, - }), - ]); - }); - - it(`Emit nullifier (should be traced)`, async () => { - const utxo = new Fr(42); - const calldata = [utxo]; - - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - const bytecode = getAvmTestContractBytecode('new_nullifier'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - expect(results.reverted).toBe(false); - - expect(context.persistableState.flush().newNullifiers).toEqual([ - expect.objectContaining({ - storageAddress: context.environment.storageAddress, - nullifier: utxo, - }), - ]); - }); - - it(`Nullifier exists (it does not)`, async () => { - const utxo = new Fr(42); - const calldata = [utxo]; - - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - const bytecode = getAvmTestContractBytecode('nullifier_exists'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - expect(results.reverted).toBe(false); - expect(results.output).toEqual([/*exists=false*/ new Fr(0)]); - - // Nullifier existence check should be in trace - const trace = context.persistableState.flush(); - expect(trace.nullifierChecks).toEqual([ - expect.objectContaining({ - storageAddress: context.environment.storageAddress, - nullifier: utxo, - exists: false, - counter: expect.any(Fr), - isPending: false, - leafIndex: expect.any(Fr), - }), - ]); - }); - - it(`Nullifier exists (it does)`, async () => { - const utxo = new Fr(42); - const calldata = [utxo]; - - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - // nullifier exists! - jest - .spyOn(context.persistableState.hostStorage.commitmentsDb, 'getNullifierIndex') - .mockReturnValue(Promise.resolve(BigInt(42))); - const bytecode = getAvmTestContractBytecode('nullifier_exists'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - expect(results.reverted).toBe(false); - expect(results.output).toEqual([/*exists=true*/ new Fr(1)]); - - // Nullifier existence check should be in trace - const trace = context.persistableState.flush(); - expect(trace.nullifierChecks).toEqual([ - expect.objectContaining({ - storageAddress: context.environment.storageAddress, - nullifier: utxo, - exists: true, - counter: expect.any(Fr), - isPending: false, - leafIndex: expect.any(Fr), - }), - ]); - }); - - it(`Emits a nullifier and checks its existence`, async () => { - const utxo = new Fr(42); - const calldata = [utxo]; - - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - const bytecode = getAvmTestContractBytecode('emit_nullifier_and_check'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - expect(results.reverted).toBe(false); - // Nullifier existence check should be in trace - const trace = context.persistableState.flush(); - expect(trace.newNullifiers).toEqual([ - expect.objectContaining({ - storageAddress: context.environment.storageAddress, - nullifier: utxo, - }), - ]); - expect(trace.nullifierChecks).toEqual([ - expect.objectContaining({ - storageAddress: context.environment.storageAddress, - nullifier: utxo, - exists: true, - counter: expect.any(Fr), - isPending: true, - leafIndex: expect.any(Fr), - }), - ]); - }); + it('conversions', async () => { + const calldata: Fr[] = [new Fr(0b1011101010100)]; + const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - it(`Emits same nullifier twice (should fail)`, async () => { - const utxo = new Fr(42); - const calldata = [utxo]; + const bytecode = getAvmTestContractBytecode('to_radix_le'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - const bytecode = getAvmTestContractBytecode('nullifier_collision'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); + expect(results.reverted).toBe(false); + const expectedResults = Buffer.concat('0010101011'.split('').map(c => new Fr(Number(c)).toBuffer())); + const resultBuffer = Buffer.concat(results.output.map(f => f.toBuffer())); - expect(results.reverted).toBe(true); - expect(results.revertReason?.message).toMatch(/Attempted to emit duplicate nullifier/); - // Only the first nullifier should be in the trace, second one failed to add - expect(context.persistableState.flush().newNullifiers).toEqual([ - expect.objectContaining({ - storageAddress: context.environment.storageAddress, - nullifier: utxo, - }), - ]); - }); + expect(resultBuffer.equals(expectedResults)).toBe(true); }); - describe('Test tree access (l1ToL2 messages)', () => { - it(`Message exists (it does not)`, async () => { - const msgHash = new Fr(42); - const leafIndex = new Fr(24); - const calldata = [msgHash, leafIndex]; - - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - const bytecode = getAvmTestContractBytecode('l1_to_l2_msg_exists'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - expect(results.reverted).toBe(false); - expect(results.output).toEqual([/*exists=false*/ new Fr(0)]); - // Message existence check should be in trace - const trace = context.persistableState.flush(); - expect(trace.l1ToL2MessageChecks.length).toEqual(1); - expect(trace.l1ToL2MessageChecks[0].exists).toEqual(false); - }); - - it(`Message exists (it does)`, async () => { - const msgHash = new Fr(42); - const leafIndex = new Fr(24); - const calldata = [msgHash, leafIndex]; - - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - jest.spyOn(context.persistableState.hostStorage.commitmentsDb, 'getL1ToL2LeafValue').mockResolvedValue(msgHash); - const bytecode = getAvmTestContractBytecode('l1_to_l2_msg_exists'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - expect(results.reverted).toBe(false); - expect(results.output).toEqual([/*exists=false*/ new Fr(1)]); - // Message existence check should be in trace - const trace = context.persistableState.flush(); - expect(trace.l1ToL2MessageChecks.length).toEqual(1); - expect(trace.l1ToL2MessageChecks[0].exists).toEqual(true); - }); - }); + describe('Side effects, world state, nested calls', () => { + const address = new Fr(1); + // TODO(dbanks12): should be able to make address and storage address different + const storageAddress = new Fr(1); + const sender = new Fr(42); + const leafIndex = new Fr(7); + const slotNumber = 1; // must update Noir contract if changing this + const slot = new Fr(slotNumber); + const listSlotNumber0 = 2; // must update Noir contract if changing this + const listSlotNumber1 = listSlotNumber0 + 1; + const listSlot0 = new Fr(listSlotNumber0); + const listSlot1 = new Fr(listSlotNumber1); + const value0 = new Fr(420); + const value1 = new Fr(69); + const sum = value0.add(value1); + + let hostStorage: HostStorage; + let trace: PublicSideEffectTraceInterface; + let persistableState: AvmPersistableStateManager; + + beforeEach(() => { + hostStorage = initHostStorage(); + trace = mock(); + persistableState = initPersistableStateManager({ hostStorage, trace }); + }); + + const createContext = (calldata: Fr[] = []) => { + return initContext({ persistableState, env: initExecutionEnvironment({ address, storageAddress, sender, calldata }) }); + }; - describe('Storage accesses', () => { - it('Should set value in storage (single)', async () => { - const slot = 1n; - const address = AztecAddress.fromField(new Fr(420)); - const value = new Fr(88); - const calldata = [value]; + type SideEffectTestMode = { + traceFn: keyof PublicSideEffectTraceInterface; + mockDbCall: (leafIndex: Fr, value: Fr) => void; + traceArgs: (exists: boolean, isPending?: boolean) => any[]; + output: (exists: boolean) => Fr[]; + }; + + const sideEffectTestModes: Record = { + 'note_hash_exists': { + traceFn: 'traceNoteHashCheck', + mockDbCall: (leafIndex: Fr, _value: Fr) => jest.spyOn(hostStorage.commitmentsDb, 'getCommitmentIndex').mockResolvedValue(leafIndex.toBigInt()), + // leafIndex is provided by user, so it is present in trace call even on DB miss + traceArgs: (exists: boolean) => [storageAddress, value0, leafIndex, exists], + output: (exists: boolean) => [new Fr(exists)], + }, + 'nullifier_exists': { + traceFn: 'traceNullifierCheck', + mockDbCall: (leafIndex: Fr, _value: Fr) => jest.spyOn(hostStorage.commitmentsDb, 'getNullifierIndex').mockResolvedValue(leafIndex.toBigInt()), + traceArgs: (exists: boolean, isPending: boolean = false) => { + // leafIndex is returned from DB call for nullifiers, so it is absent on DB miss + const tracedLeafIndex = (exists && !isPending) ? leafIndex : Fr.ZERO; + return [storageAddress, value0, tracedLeafIndex, exists, isPending]; + }, + output: (exists: boolean) => [new Fr(exists)], + }, + 'l1_to_l2_msg_exists': { + traceFn: 'traceL1ToL2MessageCheck', + mockDbCall: (_leafIndex:Fr, value: Fr) => jest.spyOn(hostStorage.commitmentsDb, 'getL1ToL2LeafValue').mockResolvedValue(value), + // leafIndex is provided by user, so it is present in trace call even on DB miss + traceArgs: (exists: boolean) => [address, value0, leafIndex, exists], + output: (exists: boolean) => [new Fr(exists)], + }, + 'new_note_hash': { + traceFn: 'traceNewNoteHash', + mockDbCall: () => {}, // there is no db call + traceArgs: () => [expect.objectContaining(storageAddress), value0], + output: () => [], // there is no output + }, + 'new_nullifier': { + traceFn: 'traceNewNullifier', + mockDbCall: () => {}, // there is no db call + traceArgs: () => [expect.objectContaining(storageAddress), value0], + output: () => [], // there is no output + }, + }; - const context = initContext({ - env: initExecutionEnvironment({ calldata, address, storageAddress: address }), + const expectTraced = (mode: string, exists: boolean = false, isPending: boolean = false) => { + const testMode = sideEffectTestModes[mode]; + if (!testMode) { + throw new Error(`Invalid side effect test mode in expectTraced: ${mode}`); + } + const spy = jest.spyOn(trace, testMode.traceFn); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(...testMode.traceArgs(exists, isPending)); + } + + describe.each([ + ['note_hash_exists', [value0, leafIndex], /*exists=*/false], + ['note_hash_exists', [value0, leafIndex], /*exists=*/true], + ['nullifier_exists', [value0], /*exists=*/false], + ['nullifier_exists', [value0], /*exists=*/true], + ['l1_to_l2_msg_exists', [value0, leafIndex], /*exists=*/false], + ['l1_to_l2_msg_exists', [value0, leafIndex], /*exists=*/true], + ['new_note_hash', [value0]], + ['new_nullifier', [value0]], + ])('Basic tree access', (noirFnName: string, calldata: Fr[], exists?: boolean) => { + it(noirFnName + (exists !== undefined ? ` - existence check (exists: ${exists})` : ''), async () => { + const context = createContext(calldata); + const bytecode = getAvmTestContractBytecode(noirFnName); + + const testMode = sideEffectTestModes[/*mode=*/noirFnName]; + if (!testMode) { + throw new Error(`Invalid side effect test mode: ${noirFnName}`); + } + + if (exists) testMode.mockDbCall(leafIndex, value0); + + const results = await new AvmSimulator(context).executeBytecode(bytecode); + expect(results.reverted).toBe(false); + expect(results.output).toEqual(testMode.output(!!exists)); + + expectTraced(/*mode=*/noirFnName, exists); }); - const bytecode = getAvmTestContractBytecode('set_storage_single'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - expect(results.reverted).toBe(false); - - // World state - const worldState = context.persistableState.flush(); - const storageSlot = worldState.currentStorageValue.get(address.toBigInt())!; - const adminSlotValue = storageSlot.get(slot); - expect(adminSlotValue).toEqual(value); - - // Tracing - expect(worldState.storageWrites).toEqual([ - expect.objectContaining({ - storageAddress: address, - slot: new Fr(slot), - value: value, - }), - ]); }); - it('Should read value in storage (single)', async () => { - const slot = 1n; - const value = new Fr(12345); - const address = AztecAddress.fromField(new Fr(420)); - const storage = new Map([[slot, value]]); - - const context = initContext({ - env: initExecutionEnvironment({ storageAddress: address }), + describe('Cached nullifiers', () => { + it(`Emits a nullifier and checks its existence`, async () => { + const calldata = [value0]; + + const context = createContext(calldata); + const bytecode = getAvmTestContractBytecode('emit_nullifier_and_check'); + + const results = await new AvmSimulator(context).executeBytecode(bytecode); + expect(results.reverted).toBe(false); + + // New nullifier and nullifier existence check should be traced + expectTraced('new_nullifier'); + expectTraced('nullifier_exists', /*exists=*/true, /*isPending=*/true); }); - jest - .spyOn(context.persistableState.hostStorage.publicStateDb, 'storageRead') - .mockImplementation((_address, slot) => Promise.resolve(storage.get(slot.toBigInt())!)); - const bytecode = getAvmTestContractBytecode('read_storage_single'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - // Get contract function artifact - expect(results.reverted).toBe(false); - expect(results.output).toEqual([value]); - - // Tracing - const worldState = context.persistableState.flush(); - expect(worldState.storageReads).toEqual([ - expect.objectContaining({ - storageAddress: address, - slot: new Fr(slot), - value: value, - exists: true, - }), - ]); - }); - - it('Should set and read a value from storage (single)', async () => { - const slot = 1n; - const value = new Fr(12345); - const address = AztecAddress.fromField(new Fr(420)); - const calldata = [value]; - - const context = initContext({ - env: initExecutionEnvironment({ calldata, address, storageAddress: address }), + it(`Emits same nullifier twice (expect failure)`, async () => { + const calldata = [value0]; + + const context = createContext(calldata); + const bytecode = getAvmTestContractBytecode('nullifier_collision'); + + const results = await new AvmSimulator(context).executeBytecode(bytecode); + expect(results.reverted).toBe(true); + expect(results.revertReason?.message).toMatch(/Attempted to emit duplicate nullifier/); + + // Nullifier should be traced exactly once + expectTraced('new_nullifier'); }); - const bytecode = getAvmTestContractBytecode('set_read_storage_single'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - expect(results.reverted).toBe(false); - expect(results.output).toEqual([value]); - - // Test read trace - const worldState = context.persistableState.flush(); - expect(worldState.storageReads).toEqual([ - expect.objectContaining({ - storageAddress: address, - slot: new Fr(slot), - value: value, - exists: true, - }), - ]); - expect(worldState.storageWrites).toEqual([ - expect.objectContaining({ - storageAddress: address, - slot: new Fr(slot), - value: value, - }), - ]); }); - - it('Should set a value in storage (list)', async () => { - const slot = 2n; - const sender = AztecAddress.fromField(new Fr(1)); - const address = AztecAddress.fromField(new Fr(420)); - const calldata = [new Fr(1), new Fr(2)]; - - const context = initContext({ - env: initExecutionEnvironment({ sender, address, calldata, storageAddress: address }), + + describe('Unencrypted Logs', () => { + it(`Emit unencrypted logs (should be traced)`, async () => { + const context = createContext(); + const bytecode = getAvmTestContractBytecode('emit_unencrypted_log'); + + const results = await new AvmSimulator(context).executeBytecode(bytecode); + expect(results.reverted).toBe(false); + + const eventSelector = new Fr(5); + const expectedFields = [new Fr(10), new Fr(20), new Fr(30)]; + const expectedString = 'Hello, world!'.split('').map(c => new Fr(c.charCodeAt(0))); + const expectedCompressedString = ['\0A long time ago, in a galaxy fa', '\0r far away...\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0'].map(s => new Fr(Buffer.from(s))); + + const spy = jest.spyOn(trace, 'traceUnencryptedLog'); + expect(spy).toHaveBeenCalledTimes(3); + expect(spy).toHaveBeenCalledWith(address, eventSelector, expectedFields); + expect(spy).toHaveBeenCalledWith(address, eventSelector, expectedString); + expect(spy).toHaveBeenCalledWith(address, eventSelector, expectedCompressedString); }); - const bytecode = getAvmTestContractBytecode('set_storage_list'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - expect(results.reverted).toBe(false); - - const worldState = context.persistableState.flush(); - const storageSlot = worldState.currentStorageValue.get(address.toBigInt())!; - expect(storageSlot.get(slot)).toEqual(calldata[0]); - expect(storageSlot.get(slot + 1n)).toEqual(calldata[1]); - - // Tracing - expect(worldState.storageWrites).toEqual([ - expect.objectContaining({ - storageAddress: address, - slot: new Fr(slot), - value: calldata[0], - }), - expect.objectContaining({ - storageAddress: address, - slot: new Fr(slot + 1n), - value: calldata[1], - }), - ]); }); - - it('Should read a value in storage (list)', async () => { - const slot = 2n; - const address = AztecAddress.fromField(new Fr(420)); - const values = [new Fr(1), new Fr(2)]; - const storage = new Map([ - [slot, values[0]], - [slot + 1n, values[1]], - ]); - - const context = initContext({ - env: initExecutionEnvironment({ address, storageAddress: address }), + + describe('Public storage accesses', () => { + it('Should set value in storage (single)', async () => { + const calldata = [value0]; + + const context = createContext(calldata); + const bytecode = getAvmTestContractBytecode('set_storage_single'); + + const results = await new AvmSimulator(context).executeBytecode(bytecode); + expect(results.reverted).toBe(false); + + expect(await context.persistableState.peekStorage(storageAddress, slot)).toEqual(value0); + + const spy = jest.spyOn(trace, 'tracePublicStorageWrite'); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(storageAddress, slot, value0); }); - jest - .spyOn(context.persistableState.hostStorage.publicStateDb, 'storageRead') - .mockImplementation((_address, slot) => Promise.resolve(storage.get(slot.toBigInt())!)); - const bytecode = getAvmTestContractBytecode('read_storage_list'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - expect(results.reverted).toBe(false); - expect(results.output).toEqual(values); - - // Tracing - const worldState = context.persistableState.flush(); - expect(worldState.storageReads).toEqual([ - expect.objectContaining({ - storageAddress: address, - slot: new Fr(slot), - value: values[0], - exists: true, - }), - expect.objectContaining({ - storageAddress: address, - slot: new Fr(slot + 1n), - value: values[1], - exists: true, - }), - ]); - }); - - it('Should set a value in storage (map)', async () => { - const address = AztecAddress.fromField(new Fr(420)); - const value = new Fr(12345); - const calldata = [address.toField(), value]; - - const context = initContext({ - env: initExecutionEnvironment({ address, calldata, storageAddress: address }), + + it('Should read value in storage (single)', async () => { + const storage = new Map([[slot.toBigInt(), value0]]); + + const context = createContext(); + jest + .spyOn(hostStorage.publicStateDb, 'storageRead') + .mockImplementation((_address, slot) => Promise.resolve(storage.get(slot.toBigInt())!)); + const bytecode = getAvmTestContractBytecode('read_storage_single'); + + const results = await new AvmSimulator(context).executeBytecode(bytecode); + expect(results.reverted).toBe(false); + expect(results.output).toEqual([value0]); + + const spy = jest.spyOn(trace, 'tracePublicStorageRead'); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(storageAddress, slot, value0, /*exists=*/true, /*cached=*/false); }); - const bytecode = getAvmTestContractBytecode('set_storage_map'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - expect(results.reverted).toBe(false); - // returns the storage slot for modified key - const slotNumber = results.output[0].toBigInt(); - - const worldState = context.persistableState.flush(); - const storageSlot = worldState.currentStorageValue.get(address.toBigInt())!; - expect(storageSlot.get(slotNumber)).toEqual(value); - - // Tracing - expect(worldState.storageWrites).toEqual([ - expect.objectContaining({ - storageAddress: address, - slot: new Fr(slotNumber), - value: value, - }), - ]); - }); - - it('Should read-add-set a value in storage (map)', async () => { - const address = AztecAddress.fromField(new Fr(420)); - const value = new Fr(12345); - const calldata = [address.toField(), value]; - - const context = initContext({ - env: initExecutionEnvironment({ address, calldata, storageAddress: address }), + + it('Should set and read a value from storage (single)', async () => { + const calldata = [value0]; + + const context = createContext(calldata); + const bytecode = getAvmTestContractBytecode('set_read_storage_single'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); + + expect(results.reverted).toBe(false); + expect(results.output).toEqual([value0]); + + const writeSpy = jest.spyOn(trace, 'tracePublicStorageWrite'); + expect(writeSpy).toHaveBeenCalledTimes(1); + expect(writeSpy).toHaveBeenCalledWith(storageAddress, slot, value0); + + const readSpy = jest.spyOn(trace, 'tracePublicStorageRead'); + expect(readSpy).toHaveBeenCalledTimes(1); + expect(readSpy).toHaveBeenCalledWith(storageAddress, slot, value0, /*exists=*/true, /*cached=*/true); + }); + + it('Should set a value in storage (list)', async () => { + const calldata = [value0, value1]; + + const context = createContext(calldata); + const bytecode = getAvmTestContractBytecode('set_storage_list'); + + const results = await new AvmSimulator(context).executeBytecode(bytecode); + expect(results.reverted).toBe(false); + + const trace = context.persistableState.getTrace(); + expect(await context.persistableState.peekStorage(address, listSlot0)).toEqual(calldata[0]); + expect(await context.persistableState.peekStorage(address, listSlot1)).toEqual(calldata[1]); + + const spy = jest.spyOn(trace, 'tracePublicStorageWrite'); + expect(spy).toHaveBeenCalledTimes(2); + expect(spy).toHaveBeenCalledWith(storageAddress, listSlot0, value0); + expect(spy).toHaveBeenCalledWith(storageAddress, listSlot1, value1); + }); + + it('Should read a value in storage (list)', async () => { + const storage = new Map([ + [listSlot0.toBigInt(), value0], + [listSlot1.toBigInt(), value1], + ]); + + const context = createContext(); + jest + .spyOn(hostStorage.publicStateDb, 'storageRead') + .mockImplementation((_address, slot) => Promise.resolve(storage.get(slot.toBigInt())!)); + const bytecode = getAvmTestContractBytecode('read_storage_list'); + + const results = await new AvmSimulator(context).executeBytecode(bytecode); + expect(results.reverted).toBe(false); + expect(results.output).toEqual([value0, value1]); + + const spy = jest.spyOn(trace, 'tracePublicStorageRead'); + expect(spy).toHaveBeenCalledTimes(2); + expect(spy).toHaveBeenCalledWith(storageAddress, listSlot0, value0, /*exists=*/true, /*cached=*/false); + expect(spy).toHaveBeenCalledWith(storageAddress, listSlot1, value1, /*exists=*/true, /*cached=*/false); + }); + + it('Should set a value in storage (map)', async () => { + const calldata = [storageAddress, value0]; + + const context = createContext(calldata); + const bytecode = getAvmTestContractBytecode('set_storage_map'); + + const results = await new AvmSimulator(context).executeBytecode(bytecode); + expect(results.reverted).toBe(false); + + // returns the storage slot for modified key + const mapSlotNumber = results.output[0].toBigInt(); + const mapSlot = new Fr(mapSlotNumber); + + expect(await context.persistableState.peekStorage(storageAddress, mapSlot)).toEqual(value0); + + const spy = jest.spyOn(trace, 'tracePublicStorageWrite'); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(storageAddress, mapSlot, value0); + }); + + it('Should read-add-set a value in storage (map)', async () => { + const calldata = [storageAddress, value0]; + + const context = createContext(calldata); + const bytecode = getAvmTestContractBytecode('add_storage_map'); + + const results = await new AvmSimulator(context).executeBytecode(bytecode); + expect(results.reverted).toBe(false); + + // returns the storage slot for modified key + const mapSlotNumber = results.output[0].toBigInt(); + const mapSlot = new Fr(mapSlotNumber); + + expect(await context.persistableState.peekStorage(storageAddress, mapSlot)).toEqual(value0); + + const readSpy = jest.spyOn(trace, 'tracePublicStorageRead'); + expect(readSpy).toHaveBeenCalledTimes(1); + expect(readSpy).toHaveBeenCalledWith(storageAddress, mapSlot, Fr.ZERO, /*exists=*/false, /*cached=*/false); + + const writeSpy = jest.spyOn(trace, 'tracePublicStorageWrite'); + expect(writeSpy).toHaveBeenCalledTimes(1); + expect(writeSpy).toHaveBeenCalledWith(storageAddress, mapSlot, value0); + }); + + it('Should read value in storage (map)', async () => { + const calldata = [storageAddress]; + + const context = createContext(calldata); + jest + .spyOn(hostStorage.publicStateDb, 'storageRead') + .mockResolvedValue(value0); + const bytecode = getAvmTestContractBytecode('read_storage_map'); + + const results = await new AvmSimulator(context).executeBytecode(bytecode); + expect(results.reverted).toBe(false); + expect(results.output).toEqual([value0]); + + const spy = jest.spyOn(trace, 'tracePublicStorageRead'); + expect(spy).toHaveBeenCalledTimes(1); + // slot is the result of a pedersen hash and is therefore not known in the test + expect(spy).toHaveBeenCalledWith(storageAddress, expect.anything(), value0, /*exists=*/true, /*cached=*/false); }); - const bytecode = getAvmTestContractBytecode('add_storage_map'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - expect(results.reverted).toBe(false); - // returns the storage slot for modified key - const slotNumber = results.output[0].toBigInt(); - - const worldState = context.persistableState.flush(); - const storageSlot = worldState.currentStorageValue.get(address.toBigInt())!; - expect(storageSlot.get(slotNumber)).toEqual(value); - - // Tracing - expect(worldState.storageReads).toEqual([ - expect.objectContaining({ - storageAddress: address, - slot: new Fr(slotNumber), - value: Fr.ZERO, - exists: false, - }), - ]); - expect(worldState.storageWrites).toEqual([ - expect.objectContaining({ - storageAddress: address, - slot: new Fr(slotNumber), - value: value, - }), - ]); }); - - it('Should read value in storage (map)', async () => { - const value = new Fr(12345); - const address = AztecAddress.fromField(new Fr(420)); - const calldata = [address.toField()]; - - const context = initContext({ - env: initExecutionEnvironment({ calldata, address, storageAddress: address }), + + describe('Contract Instance Retrieval', () => { + it(`Can getContractInstance`, async () => { + const context = createContext(); + const contractInstance = { + address: AztecAddress.random(), + version: 1 as const, + salt: new Fr(0x123), + deployer: AztecAddress.fromBigInt(0x456n), + contractClassId: new Fr(0x789), + initializationHash: new Fr(0x101112), + publicKeysHash: new Fr(0x161718), + }; + + jest + .spyOn(hostStorage.contractsDb, 'getContractInstance') + .mockResolvedValue(contractInstance); + const bytecode = getAvmTestContractBytecode('test_get_contract_instance_raw'); + + const results = await new AvmSimulator(context).executeBytecode(bytecode); + expect(results.reverted).toBe(false); + + const spy = jest.spyOn(trace, 'traceGetContractInstance'); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(expect.objectContaining(contractInstance)); }); - jest - .spyOn(context.persistableState.hostStorage.publicStateDb, 'storageRead') - .mockReturnValue(Promise.resolve(value)); - const bytecode = getAvmTestContractBytecode('read_storage_map'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - // Get contract function artifact - expect(results.reverted).toBe(false); - expect(results.output).toEqual([value]); - - // Tracing - const worldState = context.persistableState.flush(); - expect(worldState.storageReads).toEqual([ - expect.objectContaining({ - storageAddress: address, - // slot depends on pedersen hash of key, etc. - value: value, - exists: true, - }), - ]); }); - }); - - describe('Contract', () => { - it(`GETCONTRACTINSTANCE deserializes correctly`, async () => { - const context = initContext(); - const contractInstance = { - address: AztecAddress.random(), - version: 1 as const, - salt: new Fr(0x123), - deployer: AztecAddress.fromBigInt(0x456n), - contractClassId: new Fr(0x789), - initializationHash: new Fr(0x101112), - publicKeysHash: new Fr(0x161718), + + describe('Nested external calls', () => { + const expectTracedNestedCall = ( + environment: AvmExecutionEnvironment, + nestedTrace: PublicSideEffectTraceInterface, + isStaticCall: boolean = false) => { + const spy = jest.spyOn(trace, 'traceNestedCall'); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith( + /*nestedCallTrace=*/nestedTrace, + /*nestedEnvironment=*/expect.objectContaining({ + sender: environment.address, // sender is top-level call + contractCallDepth: new Fr(1), // top call is depth 0, nested is depth 1 + header: environment.header, // just confirming that nested env looks roughly right + globals: environment.globals, // just confirming that nested env looks roughly right + isStaticCall: isStaticCall, + // TODO(7121): can't check calldata like this since it is modified on environment construction + // with AvmContextInputs. These should eventually go away. + //calldata: expect.arrayContaining(environment.calldata), // top-level call forwards args + }), + /*startGasLeft=*/expect.anything(), + /*endGasLeft=*/expect.anything(), + /*bytecode=*/expect.anything(),//decompressBytecodeIfCompressed(addBytecode), + /*avmCallResults=*/expect.anything(), // we don't have the NESTED call's results to check + ); }; + + it(`Nested call`, async () => { + const calldata = [value0, value1]; + const context = createContext(calldata); + const callBytecode = getAvmTestContractBytecode('nested_call_to_add'); + const addBytecode = getAvmTestContractBytecode('add_args_return'); + jest + .spyOn(hostStorage.contractsDb, 'getBytecode') + .mockResolvedValue(addBytecode); + const nestedTrace = mock(); + jest.spyOn(trace, 'fork').mockReturnValue(nestedTrace); + + const results = await new AvmSimulator(context).executeBytecode(callBytecode); + expect(results.reverted).toBe(false); + expect(results.output).toEqual([sum]); + + expectTracedNestedCall(context.environment, nestedTrace); + }); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getContractInstance') - .mockReturnValue(Promise.resolve(contractInstance)); - const bytecode = getAvmTestContractBytecode('test_get_contract_instance_raw'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - expect(results.reverted).toBe(false); - }); - }); - - describe('Nested external calls', () => { - it(`Nested call with not enough gas`, async () => { - const gas = [/*l2=*/ 5, /*da=*/ 10000].map(g => new Fr(g)); - const calldata: Fr[] = [new Fr(1), new Fr(2), ...gas]; - const callBytecode = getAvmTestContractBytecode('nested_call_to_add_with_gas'); - const addBytecode = getAvmTestContractBytecode('add_args_return'); - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValue(Promise.resolve(addBytecode)); - - const results = await new AvmSimulator(context).executeBytecode(callBytecode); - - // TODO: change this once we don't force rethrowing of exceptions. - // Outer frame should not revert, but inner should, so the forwarded return value is 0 - // expect(results.revertReason).toBeUndefined(); - // expect(results.reverted).toBe(false); - expect(results.reverted).toBe(true); - expect(results.revertReason?.message).toEqual('Not enough L2GAS gas left'); - }); - - it(`Nested call`, async () => { - const calldata: Fr[] = [new Fr(1), new Fr(2)]; - const callBytecode = getAvmTestContractBytecode('nested_call_to_add'); - const addBytecode = getAvmTestContractBytecode('add_args_return'); - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValue(Promise.resolve(addBytecode)); - - const results = await new AvmSimulator(context).executeBytecode(callBytecode); - - expect(results.reverted).toBe(false); - expect(results.output).toEqual([new Fr(3)]); - }); - - it(`Nested static call`, async () => { - const calldata: Fr[] = [new Fr(1), new Fr(2)]; - const callBytecode = getAvmTestContractBytecode('nested_static_call_to_add'); - const addBytecode = getAvmTestContractBytecode('add_args_return'); - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValue(Promise.resolve(addBytecode)); - - const results = await new AvmSimulator(context).executeBytecode(callBytecode); - - expect(results.reverted).toBe(false); - expect(results.output).toEqual([/*result=*/ new Fr(3)]); - }); - - it(`Nested static call which modifies storage`, async () => { - const callBytecode = getAvmTestContractBytecode('nested_static_call_to_set_storage'); - const nestedBytecode = getAvmTestContractBytecode('set_storage_single'); - const context = initContext(); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValue(Promise.resolve(nestedBytecode)); - - const results = await new AvmSimulator(context).executeBytecode(callBytecode); - - expect(results.reverted).toBe(true); // The outer call should revert. - expect(results.revertReason?.message).toEqual( - 'Static call cannot update the state, emit L2->L1 messages or generate logs', - ); - }); - - it(`Nested calls rethrow exceptions`, async () => { - const calldata: Fr[] = [new Fr(1), new Fr(2)]; - const callBytecode = getAvmTestContractBytecode('nested_call_to_add'); - // We actually don't pass the function ADD, but it's ok because the signature is the same. - const nestedBytecode = getAvmTestContractBytecode('assert_same'); - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValue(Promise.resolve(nestedBytecode)); - - const results = await new AvmSimulator(context).executeBytecode(callBytecode); - - expect(results.reverted).toBe(true); // The outer call should revert. - expect(results.revertReason?.message).toEqual('Assertion failed: Values are not equal'); + it(`Nested static call`, async () => { + const calldata = [value0, value1]; + const context = createContext(calldata); + const callBytecode = getAvmTestContractBytecode('nested_static_call_to_add'); + const addBytecode = getAvmTestContractBytecode('add_args_return'); + jest + .spyOn(hostStorage.contractsDb, 'getBytecode') + .mockResolvedValue(addBytecode); + const nestedTrace = mock(); + jest.spyOn(trace, 'fork').mockReturnValue(nestedTrace); + + const results = await new AvmSimulator(context).executeBytecode(callBytecode); + expect(results.reverted).toBe(false); + expect(results.output).toEqual([sum]); + + expectTracedNestedCall(context.environment, nestedTrace, /*isStaticCall=*/true); + }); + + it(`Nested call with not enough gas (expect failure)`, async () => { + const gas = [/*l2=*/ 5, /*da=*/ 10000].map(g => new Fr(g)); + const calldata: Fr[] = [value0, value1, ...gas]; + const context = createContext(calldata); + const callBytecode = getAvmTestContractBytecode('nested_call_to_add_with_gas'); + const addBytecode = getAvmTestContractBytecode('add_args_return'); + jest + .spyOn(hostStorage.contractsDb, 'getBytecode') + .mockResolvedValue(addBytecode); + const nestedTrace = mock(); + jest.spyOn(trace, 'fork').mockReturnValue(nestedTrace); + + const results = await new AvmSimulator(context).executeBytecode(callBytecode); + // TODO(7141): change this once we don't force rethrowing of exceptions. + // Outer frame should not revert, but inner should, so the forwarded return value is 0 + // expect(results.revertReason).toBeUndefined(); + // expect(results.reverted).toBe(false); + expect(results.reverted).toBe(true); + expect(results.revertReason?.message).toEqual('Not enough L2GAS gas left'); + + // Nested call should NOT have been made and therefore should not be traced + const spy = jest.spyOn(trace, 'traceNestedCall'); + expect(spy).toHaveBeenCalledTimes(0); + }); + + it(`Nested static call which modifies storage (expect failure)`, async () => { + const context = createContext(); + const callBytecode = getAvmTestContractBytecode('nested_static_call_to_set_storage'); + const nestedBytecode = getAvmTestContractBytecode('set_storage_single'); + jest + .spyOn(hostStorage.contractsDb, 'getBytecode') + .mockResolvedValue(nestedBytecode); + const nestedTrace = mock(); + jest.spyOn(trace, 'fork').mockReturnValue(nestedTrace); + + const results = await new AvmSimulator(context).executeBytecode(callBytecode); + + expect(results.reverted).toBe(true); // The outer call should revert. + expect(results.revertReason?.message).toEqual( + 'Static call cannot update the state, emit L2->L1 messages or generate logs', + ); + + // TODO(7141): external call doesn't recover from nested exception until + // we support recoverability of reverts (here and in kernel) + //expectTracedNestedCall(context.environment, results, nestedTrace, /*isStaticCall=*/true); + + // Nested call should NOT have been able to write storage + const spy = jest.spyOn(nestedTrace, 'tracePublicStorageWrite'); + expect(spy).toHaveBeenCalledTimes(0); + }); + + it(`Nested calls rethrow exceptions`, async () => { + const calldata = [value0, value1]; + const context = createContext(calldata); + const callBytecode = getAvmTestContractBytecode('nested_call_to_add'); + // We actually don't pass the function ADD, but it's ok because the signature is the same. + const nestedBytecode = getAvmTestContractBytecode('assert_same'); + jest + .spyOn(hostStorage.contractsDb, 'getBytecode') + .mockReturnValue(Promise.resolve(nestedBytecode)); + + const results = await new AvmSimulator(context).executeBytecode(callBytecode); + expect(results.reverted).toBe(true); // The outer call should revert. + expect(results.revertReason?.message).toEqual('Assertion failed: Values are not equal'); + }); }); }); - - it('conversions', async () => { - const calldata: Fr[] = [new Fr(0b1011101010100)]; - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - - const bytecode = getAvmTestContractBytecode('to_radix_le'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - expect(results.reverted).toBe(false); - const expectedResults = Buffer.concat('0010101011'.split('').map(c => new Fr(Number(c)).toBuffer())); - const resultBuffer = Buffer.concat(results.output.map(f => f.toBuffer())); - - expect(resultBuffer.equals(expectedResults)).toBe(true); - }); }); function sha256FromMemoryBytes(bytes: Uint8[]): Fr[] { diff --git a/yarn-project/simulator/src/avm/fixtures/index.ts b/yarn-project/simulator/src/avm/fixtures/index.ts index b96be7f003c3..c2c765a2da3c 100644 --- a/yarn-project/simulator/src/avm/fixtures/index.ts +++ b/yarn-project/simulator/src/avm/fixtures/index.ts @@ -17,7 +17,8 @@ import { AvmMachineState } from '../avm_machine_state.js'; import { Field, Uint8 } from '../avm_memory_types.js'; import { HostStorage } from '../journal/host_storage.js'; import { AvmPersistableStateManager } from '../journal/journal.js'; -import { type TracedContractInstance } from '../journal/trace_types.js'; +import { PublicSideEffectTraceInterface } from '../../public/side_effect_trace_interface.js'; +import { TracedContractInstance } from '../../public/side_effect_trace.js'; /** * Create a new AVM context with default values. @@ -28,7 +29,7 @@ export function initContext(overrides?: { machineState?: AvmMachineState; }): AvmContext { return new AvmContext( - overrides?.persistableState || initMockPersistableStateManager(), + overrides?.persistableState || initPersistableStateManager(), overrides?.env || initExecutionEnvironment(), overrides?.machineState || initMachineState(), ); @@ -48,8 +49,14 @@ export function initHostStorage(overrides?: { } /** Creates an empty state manager with mocked storage. */ -export function initMockPersistableStateManager(): AvmPersistableStateManager { - return new AvmPersistableStateManager(initHostStorage()); +export function initPersistableStateManager(overrides?: { + hostStorage?: HostStorage; + trace?: PublicSideEffectTraceInterface; +}): AvmPersistableStateManager { + return new AvmPersistableStateManager( + overrides?.hostStorage || initHostStorage(), + overrides?.trace || mock() + ); } /** diff --git a/yarn-project/simulator/src/avm/journal/journal.test.ts b/yarn-project/simulator/src/avm/journal/journal.test.ts index 77b7b3732b68..11b38237efde 100644 --- a/yarn-project/simulator/src/avm/journal/journal.test.ts +++ b/yarn-project/simulator/src/avm/journal/journal.test.ts @@ -47,7 +47,7 @@ describe('journal', () => { expect(cachedResult).toEqual(cachedValue); // We expect the journal to store the access in [storedVal, cachedVal] - [time0, time1] - const { storageReads, storageWrites }: JournalData = journal.flush(); + const { storageReads, storageWrites }: JournalData = journal.getTrace()(); expect(storageReads).toEqual([ expect.objectContaining({ storageAddress: contractAddress, @@ -78,7 +78,7 @@ describe('journal', () => { const address = new Fr(1234); journal.writeNoteHash(address, utxo); - const journalUpdates = journal.flush(); + const journalUpdates = journal.getTrace()(); expect(journalUpdates.newNoteHashes).toEqual([ expect.objectContaining({ noteHash: utxo, storageAddress: address }), ]); @@ -89,7 +89,7 @@ describe('journal', () => { const exists = await journal.checkNullifierExists(contractAddress, utxo); expect(exists).toEqual(false); - const journalUpdates = journal.flush(); + const journalUpdates = journal.getTrace()(); expect(journalUpdates.nullifierChecks).toEqual([expect.objectContaining({ nullifier: utxo, exists: false })]); }); it('checkNullifierExists works for existing nullifiers', async () => { @@ -101,7 +101,7 @@ describe('journal', () => { const exists = await journal.checkNullifierExists(contractAddress, utxo); expect(exists).toEqual(true); - const journalUpdates = journal.flush(); + const journalUpdates = journal.getTrace()(); expect(journalUpdates.nullifierChecks).toEqual([expect.objectContaining({ nullifier: utxo, exists: true })]); }); it('Should maintain nullifiers', async () => { @@ -109,7 +109,7 @@ describe('journal', () => { const utxo = new Fr(2); await journal.writeNullifier(contractAddress, utxo); - const journalUpdates = journal.flush(); + const journalUpdates = journal.getTrace()(); expect(journalUpdates.newNullifiers).toEqual([ expect.objectContaining({ storageAddress: contractAddress, nullifier: utxo }), ]); @@ -121,7 +121,7 @@ describe('journal', () => { const exists = await journal.checkL1ToL2MessageExists(msgHash, leafIndex); expect(exists).toEqual(false); - const journalUpdates = journal.flush(); + const journalUpdates = journal.getTrace()(); expect(journalUpdates.l1ToL2MessageChecks).toEqual([ expect.objectContaining({ leafIndex: leafIndex, msgHash, exists: false }), ]); @@ -134,7 +134,7 @@ describe('journal', () => { const exists = await journal.checkL1ToL2MessageExists(msgHash, leafIndex); expect(exists).toEqual(true); - const journalUpdates = journal.flush(); + const journalUpdates = journal.getTrace()(); expect(journalUpdates.l1ToL2MessageChecks).toEqual([ expect.objectContaining({ leafIndex: leafIndex, msgHash, exists: true }), ]); @@ -144,7 +144,7 @@ describe('journal', () => { const utxo = new Fr(2); await journal.writeNullifier(contractAddress, utxo); - const journalUpdates = journal.flush(); + const journalUpdates = journal.getTrace()(); expect(journalUpdates.newNullifiers).toEqual([ expect.objectContaining({ storageAddress: contractAddress, nullifier: utxo }), ]); @@ -152,9 +152,9 @@ describe('journal', () => { it('Should maintain l1 messages', () => { const recipient = EthAddress.fromField(new Fr(1)); const msgHash = new Fr(2); - journal.writeL1Message(recipient, msgHash); + journal.writeL2ToL1Message(recipient, msgHash); - const journalUpdates = journal.flush(); + const journalUpdates = journal.getTrace()(); expect(journalUpdates.newL1Messages).toEqual([expect.objectContaining({ recipient, content: msgHash })]); }); @@ -201,8 +201,8 @@ describe('journal', () => { journal.writeStorage(contractAddress, key, value); await journal.readStorage(contractAddress, key); journal.writeNoteHash(contractAddress, commitment); - journal.writeLog(new Fr(log.address), new Fr(log.selector), log.data); - journal.writeL1Message(recipient, commitment); + journal.writeUnencryptedLog(new Fr(log.address), new Fr(log.selector), log.data); + journal.writeL2ToL1Message(recipient, commitment); await journal.writeNullifier(contractAddress, commitment); await journal.checkNullifierExists(contractAddress, commitment); await journal.checkL1ToL2MessageExists(commitment, index); @@ -212,8 +212,8 @@ describe('journal', () => { childJournal.writeStorage(contractAddress, key, valueT1); await childJournal.readStorage(contractAddress, key); childJournal.writeNoteHash(contractAddress, commitmentT1); - childJournal.writeLog(new Fr(logT1.address), new Fr(logT1.selector), logT1.data); - childJournal.writeL1Message(recipient, commitmentT1); + childJournal.writeUnencryptedLog(new Fr(logT1.address), new Fr(logT1.selector), logT1.data); + childJournal.writeL2ToL1Message(recipient, commitmentT1); await childJournal.writeNullifier(contractAddress, commitmentT1); await childJournal.checkNullifierExists(contractAddress, commitmentT1); await childJournal.checkL1ToL2MessageExists(commitmentT1, indexT1); @@ -226,7 +226,7 @@ describe('journal', () => { // Check that the storage is merged by reading from the journal // Check that the UTXOs are merged - const journalUpdates: JournalData = journal.flush(); + const journalUpdates: JournalData = journal.getTrace()(); // Check storage reads order is preserved upon merge // We first read value from t0, then value from t1 @@ -337,8 +337,8 @@ describe('journal', () => { await journal.writeNullifier(contractAddress, commitment); await journal.checkNullifierExists(contractAddress, commitment); await journal.checkL1ToL2MessageExists(commitment, index); - journal.writeLog(new Fr(log.address), new Fr(log.selector), log.data); - journal.writeL1Message(recipient, commitment); + journal.writeUnencryptedLog(new Fr(log.address), new Fr(log.selector), log.data); + journal.writeL2ToL1Message(recipient, commitment); await journal.getContractInstance(aztecContractAddress); const childJournal = new AvmPersistableStateManager(journal.hostStorage, journal); @@ -348,8 +348,8 @@ describe('journal', () => { await childJournal.writeNullifier(contractAddress, commitmentT1); await childJournal.checkNullifierExists(contractAddress, commitmentT1); await journal.checkL1ToL2MessageExists(commitmentT1, indexT1); - childJournal.writeLog(new Fr(logT1.address), new Fr(logT1.selector), logT1.data); - childJournal.writeL1Message(recipient, commitmentT1); + childJournal.writeUnencryptedLog(new Fr(logT1.address), new Fr(logT1.selector), logT1.data); + childJournal.writeL2ToL1Message(recipient, commitmentT1); await childJournal.getContractInstance(aztecContractAddress); journal.rejectNestedCallState(childJournal); @@ -358,7 +358,7 @@ describe('journal', () => { const result = await journal.readStorage(contractAddress, key); expect(result).toEqual(value); // rather than valueT1 - const journalUpdates: JournalData = journal.flush(); + const journalUpdates: JournalData = journal.getTrace()(); // Reads and writes should be preserved // Check storage reads order is preserved upon merge diff --git a/yarn-project/simulator/src/avm/journal/journal.ts b/yarn-project/simulator/src/avm/journal/journal.ts index c1495874b42a..a142d99fb5c1 100644 --- a/yarn-project/simulator/src/avm/journal/journal.ts +++ b/yarn-project/simulator/src/avm/journal/journal.ts @@ -1,139 +1,50 @@ -// TODO(5818): Rename file and all uses of "journal" -import { UnencryptedL2Log } from '@aztec/circuit-types'; -import { - AztecAddress, - ContractStorageRead, - ContractStorageUpdateRequest, - EthAddress, - L2ToL1Message, - LogHash, - NoteHash, - Nullifier, - ReadRequest, -} from '@aztec/circuits.js'; -import { EventSelector } from '@aztec/foundation/abi'; +import { AztecAddress, Gas } from '@aztec/circuits.js'; import { Fr } from '@aztec/foundation/fields'; import { type DebugLogger, createDebugLogger } from '@aztec/foundation/log'; import { SerializableContractInstance } from '@aztec/types/contracts'; -import { type PublicExecutionResult } from '../../index.js'; import { type HostStorage } from './host_storage.js'; import { Nullifiers } from './nullifiers.js'; import { PublicStorage } from './public_storage.js'; -import { WorldStateAccessTrace } from './trace.js'; -import { - type TracedContractInstance, - type TracedL1toL2MessageCheck, - type TracedNoteHash, - type TracedNoteHashCheck, - type TracedNullifier, - type TracedNullifierCheck, - type TracedPublicStorageRead, - type TracedPublicStorageWrite, - type TracedUnencryptedL2Log, -} from './trace_types.js'; - -// TODO:(5818): do we need this type anymore? -/** - * Data held within the journal - */ -export type JournalData = { - storageWrites: TracedPublicStorageWrite[]; - storageReads: TracedPublicStorageRead[]; - - noteHashChecks: TracedNoteHashCheck[]; - newNoteHashes: TracedNoteHash[]; - nullifierChecks: TracedNullifierCheck[]; - newNullifiers: TracedNullifier[]; - l1ToL2MessageChecks: TracedL1toL2MessageCheck[]; - - newL1Messages: L2ToL1Message[]; - newLogs: UnencryptedL2Log[]; - 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 -export type PartialPublicExecutionResult = { - noteHashReadRequests: ReadRequest[]; - nullifierReadRequests: ReadRequest[]; - nullifierNonExistentReadRequests: ReadRequest[]; - l1ToL2MsgReadRequests: ReadRequest[]; - newNoteHashes: NoteHash[]; - newL2ToL1Messages: L2ToL1Message[]; - startSideEffectCounter: number; - newNullifiers: Nullifier[]; - contractStorageReads: ContractStorageRead[]; - contractStorageUpdateRequests: ContractStorageUpdateRequest[]; - unencryptedLogsHashes: LogHash[]; - unencryptedLogs: UnencryptedL2Log[]; - allUnencryptedLogs: UnencryptedL2Log[]; - nestedExecutions: PublicExecutionResult[]; -}; +import { PublicSideEffectTraceInterface } from '../../public/side_effect_trace_interface.js'; +import { AvmExecutionEnvironment } from '../avm_execution_environment.js'; +import { AvmContractCallResults } from '../avm_message_call_result.js'; +import { TracedContractInstance } from '../../public/side_effect_trace.js'; /** * A class to manage persistable AVM state for contract calls. * Maintains a cache of the current world state, - * a trace of all world state accesses, and a list of accrued substate items. + * a trace of all side effects. * - * The simulator should make any world state and accrued substate queries through this object. + * The simulator should make any world state / tree queries through this object. * * Manages merging of successful/reverted child state into current state. */ export class AvmPersistableStateManager { private readonly log: DebugLogger = createDebugLogger('aztec:avm_simulator:state_manager'); - /** Reference to node storage */ - public readonly hostStorage: HostStorage; - // TODO(5818): make members private once this is not used in transitional_adaptors.ts. - /** World State */ + // TODO(dbanks12): make these private once hack is removed in executor.ts /** Public storage, including cached writes */ public publicStorage: PublicStorage; /** Nullifier set, including cached/recently-emitted nullifiers */ public nullifiers: Nullifiers; - /** World State Access Trace */ - public trace: WorldStateAccessTrace; - - /** Accrued Substate **/ - public newL1Messages: L2ToL1Message[] = []; - public newLogs: UnencryptedL2Log[] = []; - - // TRANSITIONAL: This should be removed once the kernel handles and entire enqueued call per circuit - public transitionalExecutionResult: PartialPublicExecutionResult; - - constructor(hostStorage: HostStorage, parent?: AvmPersistableStateManager) { - this.hostStorage = hostStorage; + constructor( + /** Reference to node storage */ + private hostStorage: HostStorage, + /** Side Effect Trace */ + private trace: PublicSideEffectTraceInterface, + parent?: AvmPersistableStateManager + ) { this.publicStorage = new PublicStorage(hostStorage.publicStateDb, parent?.publicStorage); this.nullifiers = new Nullifiers(hostStorage.commitmentsDb, parent?.nullifiers); - this.trace = new WorldStateAccessTrace(parent?.trace); - - this.transitionalExecutionResult = { - noteHashReadRequests: [], - nullifierReadRequests: [], - nullifierNonExistentReadRequests: [], - l1ToL2MsgReadRequests: [], - newNoteHashes: [], - newL2ToL1Messages: [], - startSideEffectCounter: this.trace.counter, - newNullifiers: [], - contractStorageReads: [], - contractStorageUpdateRequests: [], - unencryptedLogsHashes: [], - unencryptedLogs: [], - allUnencryptedLogs: [], - nestedExecutions: [], - }; } /** * Create a new state manager forked from this one */ public fork() { - return new AvmPersistableStateManager(this.hostStorage, this); + return new AvmPersistableStateManager(this.hostStorage, this.trace.fork(), this); } /** @@ -147,13 +58,6 @@ export class AvmPersistableStateManager { this.log.debug(`Storage write (address=${storageAddress}, slot=${slot}): value=${value}`); // Cache storage writes for later reference/reads this.publicStorage.write(storageAddress, slot, value); - - // TRANSITIONAL: This should be removed once the kernel handles and entire enqueued call per circuit - this.transitionalExecutionResult.contractStorageUpdateRequests.push( - new ContractStorageUpdateRequest(slot, value, this.trace.counter, storageAddress), - ); - - // Trace all storage writes (even reverted ones) this.trace.tracePublicStorageWrite(storageAddress, slot, value); } @@ -169,14 +73,22 @@ export class AvmPersistableStateManager { this.log.debug( `Storage read (address=${storageAddress}, slot=${slot}): value=${value}, exists=${exists}, cached=${cached}`, ); + this.trace.tracePublicStorageRead(storageAddress, slot, value, exists, cached); + return Promise.resolve(value); + } - // 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.counter, storageAddress), + /** + * Read from public storage, don't trace the read. + * + * @param storageAddress - the address of the contract whose storage is being read from + * @param slot - the slot in the contract's storage being read from + * @returns the latest value written to slot, or 0 if never written to before + */ + public async peekStorage(storageAddress: Fr, slot: Fr): Promise { + const { value, exists, cached } = await this.publicStorage.read(storageAddress, slot); + this.log.debug( + `Storage peek (address=${storageAddress}, slot=${slot}): value=${value}, exists=${exists}, cached=${cached}`, ); - - // We want to keep track of all performed reads (even reverted ones) - this.trace.tracePublicStorageRead(storageAddress, slot, value, exists, cached); return Promise.resolve(value); } @@ -193,11 +105,7 @@ export class AvmPersistableStateManager { const gotLeafIndex = await this.hostStorage.commitmentsDb.getCommitmentIndex(noteHash); const exists = gotLeafIndex === leafIndex.toBigInt(); this.log.debug(`noteHashes(${storageAddress})@${noteHash} ?? leafIndex: ${leafIndex}, exists: ${exists}.`); - - // TODO: include exists here also - This can for sure come from the trace??? - this.transitionalExecutionResult.noteHashReadRequests.push(new ReadRequest(noteHash, this.trace.counter)); - - this.trace.traceNoteHashCheck(storageAddress, noteHash, exists, leafIndex); + this.trace.traceNoteHashCheck(storageAddress, noteHash, leafIndex, exists); return Promise.resolve(exists); } @@ -206,9 +114,6 @@ export class AvmPersistableStateManager { * @param noteHash - the unsiloed note hash to write */ public writeNoteHash(storageAddress: Fr, noteHash: Fr) { - // TRANSITIONAL: This should be removed once the kernel handles and entire enqueued call per circuit - this.transitionalExecutionResult.newNoteHashes.push(new NoteHash(noteHash, this.trace.counter)); - this.log.debug(`noteHashes(${storageAddress}) += @${noteHash}.`); this.trace.traceNewNoteHash(storageAddress, noteHash); } @@ -222,19 +127,9 @@ export class AvmPersistableStateManager { public async checkNullifierExists(storageAddress: Fr, nullifier: Fr): Promise { const [exists, isPending, leafIndex] = await this.nullifiers.checkExists(storageAddress, nullifier); this.log.debug( - `nullifiers(${storageAddress})@${nullifier} ?? leafIndex: ${leafIndex}, pending: ${isPending}, exists: ${exists}.`, + `nullifiers(${storageAddress})@${nullifier} ?? leafIndex: ${leafIndex}, exists: ${exists}, pending: ${isPending}.`, ); - - // TRANSITIONAL: This should be removed once the kernel handles and entire enqueued call per circuit - if (exists) { - this.transitionalExecutionResult.nullifierReadRequests.push(new ReadRequest(nullifier, this.trace.counter)); - } else { - this.transitionalExecutionResult.nullifierNonExistentReadRequests.push( - new ReadRequest(nullifier, this.trace.counter), - ); - } - - this.trace.traceNullifierCheck(storageAddress, nullifier, exists, isPending, leafIndex); + this.trace.traceNullifierCheck(storageAddress, nullifier, leafIndex, exists, isPending); return Promise.resolve(exists); } @@ -244,11 +139,6 @@ export class AvmPersistableStateManager { * @param nullifier - the unsiloed nullifier to write */ public async writeNullifier(storageAddress: Fr, nullifier: Fr) { - // TRANSITIONAL: This should be removed once the kernel handles and entire enqueued call per circuit - this.transitionalExecutionResult.newNullifiers.push( - new Nullifier(nullifier, this.trace.counter, /*noteHash=*/ Fr.ZERO), - ); - this.log.debug(`nullifiers(${storageAddress}) += ${nullifier}.`); // Cache pending nullifiers for later access await this.nullifiers.append(storageAddress, nullifier); @@ -262,16 +152,13 @@ export class AvmPersistableStateManager { * @param msgLeafIndex - the message leaf index to use in the check * @returns exists - whether the message exists in the L1 to L2 Messages tree */ - public async checkL1ToL2MessageExists(msgHash: Fr, msgLeafIndex: Fr): Promise { + public async checkL1ToL2MessageExists(contractAddress: Fr, msgHash: Fr, msgLeafIndex: Fr): Promise { const valueAtIndex = await this.hostStorage.commitmentsDb.getL1ToL2LeafValue(msgLeafIndex.toBigInt()); const exists = valueAtIndex?.equals(msgHash) ?? false; this.log.debug( `l1ToL2Messages(@${msgLeafIndex}) ?? exists: ${exists}, expected: ${msgHash}, found: ${valueAtIndex}.`, ); - - this.transitionalExecutionResult.l1ToL2MsgReadRequests.push(new ReadRequest(msgHash, this.trace.counter)); - - this.trace.traceL1ToL2MessageCheck(msgHash, msgLeafIndex, exists); + this.trace.traceL1ToL2MessageCheck(contractAddress, msgHash, msgLeafIndex, exists); return Promise.resolve(exists); } @@ -280,40 +167,27 @@ export class AvmPersistableStateManager { * @param recipient - L1 contract address to send the message to. * @param content - Message content. */ - public writeL1Message(recipient: EthAddress | Fr, content: Fr) { + public writeL2ToL1Message(recipient: Fr, content: Fr) { this.log.debug(`L1Messages(${recipient}) += ${content}.`); - const recipientAddress = recipient instanceof EthAddress ? recipient : EthAddress.fromField(recipient); - const message = new L2ToL1Message(recipientAddress, content, 0); - this.newL1Messages.push(message); - - // TRANSITIONAL: This should be removed once the kernel handles and entire enqueued call per circuit - this.transitionalExecutionResult.newL2ToL1Messages.push(message); + this.trace.traceNewL2ToL1Message(recipient, content); } - public writeLog(contractAddress: Fr, event: Fr, log: Fr[]) { + /** + * Write an unencrypted log + * @param contractAddress - address of the contract that emitted the log + * @param event - log event selector + * @param log - log contents + */ + public writeUnencryptedLog(contractAddress: Fr, event: Fr, log: Fr[]) { this.log.debug(`UnencryptedL2Log(${contractAddress}) += event ${event} with ${log.length} fields.`); - const ulog = new UnencryptedL2Log( - AztecAddress.fromField(contractAddress), - EventSelector.fromField(event), - Buffer.concat(log.map(f => f.toBuffer())), - ); - const logHash = Fr.fromBuffer(ulog.hash()); - - // TRANSITIONAL: This should be removed once the kernel handles and entire enqueued call per circuit - this.transitionalExecutionResult.unencryptedLogs.push(ulog); - this.transitionalExecutionResult.allUnencryptedLogs.push(ulog); - // this duplicates exactly what happens in the trace just for the purpose of transitional integration with the kernel - this.transitionalExecutionResult.unencryptedLogsHashes.push( - // TODO(6578): explain magic number 4 here - new LogHash(logHash, this.trace.counter, new Fr(ulog.length + 4)), - ); - // TODO(6206): likely need to track this here and not just in the transitional logic. - - // TODO(6205): why are logs pushed here but logs hashes are traced? - this.newLogs.push(ulog); - this.trace.traceNewLog(logHash); + this.trace.traceUnencryptedLog(contractAddress, event, log); } + /** + * Get a contract instance. + * @param contractAddress - address of the contract instance to retrieve. + * @returns the contract instance with an "exists" flag + */ public async getContractInstance(contractAddress: Fr): Promise { let exists = true; const aztecAddress = AztecAddress.fromField(contractAddress); @@ -328,53 +202,39 @@ export class AvmPersistableStateManager { } /** - * Accept nested world state modifications, merging in its trace and accrued substate + * Accept nested world state modifications */ - public acceptNestedCallState(nestedJournal: AvmPersistableStateManager) { - // Merge Public Storage - this.publicStorage.acceptAndMerge(nestedJournal.publicStorage); - - // Merge World State Access Trace - this.trace.acceptAndMerge(nestedJournal.trace); - - // Accrued Substate - this.newL1Messages.push(...nestedJournal.newL1Messages); - this.newLogs.push(...nestedJournal.newLogs); - - // TRANSITIONAL: This should be removed once the kernel handles and entire enqueued call per circuit - this.transitionalExecutionResult.allUnencryptedLogs.push( - ...nestedJournal.transitionalExecutionResult.allUnencryptedLogs, - ); + public acceptNestedCallState(nestedState: AvmPersistableStateManager) { + this.publicStorage.acceptAndMerge(nestedState.publicStorage); + this.nullifiers.acceptAndMerge(nestedState.nullifiers); } /** - * Reject nested world state, merging in its trace, but not accepting any state modifications + * Accept the nested call's state and trace the nested call */ - public rejectNestedCallState(nestedJournal: AvmPersistableStateManager) { - // Merge World State Access Trace - this.trace.acceptAndMerge(nestedJournal.trace); + public processNestedCall( + nestedState: AvmPersistableStateManager, + success: boolean, + nestedEnvironment: AvmExecutionEnvironment, + startGasLeft: Gas, + endGasLeft: Gas, + bytecode: Buffer, + avmCallResults: AvmContractCallResults, + ) { + if (success) { + this.acceptNestedCallState(nestedState); + } + this.trace.traceNestedCall( + nestedState.getTrace(), + nestedEnvironment, + startGasLeft, + endGasLeft, + bytecode, + avmCallResults, + ); } - // TODO:(5818): do we need this type anymore? - /** - * Access the current state of the journal - * - * @returns a JournalData object - */ - public flush(): JournalData { - return { - noteHashChecks: this.trace.noteHashChecks, - newNoteHashes: this.trace.newNoteHashes, - nullifierChecks: this.trace.nullifierChecks, - newNullifiers: this.trace.newNullifiers, - l1ToL2MessageChecks: this.trace.l1ToL2MessageChecks, - newL1Messages: this.newL1Messages, - newLogs: this.newLogs, - newLogsHashes: this.trace.newLogsHashes, - currentStorageValue: this.publicStorage.getCache().cachePerContract, - storageReads: this.trace.publicStorageReads, - storageWrites: this.trace.publicStorageWrites, - sideEffectCounter: this.trace.counter, - }; + public getTrace(): PublicSideEffectTraceInterface { + return this.trace; } } diff --git a/yarn-project/simulator/src/avm/journal/nullifiers.ts b/yarn-project/simulator/src/avm/journal/nullifiers.ts index e580c1a885c1..c5ba68b4a260 100644 --- a/yarn-project/simulator/src/avm/journal/nullifiers.ts +++ b/yarn-project/simulator/src/avm/journal/nullifiers.ts @@ -59,6 +59,7 @@ export class Nullifiers { ): Promise<[/*exists=*/ boolean, /*isPending=*/ boolean, /*leafIndex=*/ Fr]> { // Check this cache and parent's (recursively) const existsAsPending = this.checkExistsHereOrParent(storageAddress, nullifier); + console.log(`Nullifier ${nullifier} exists as pending at contract ${storageAddress}?: existsAsPending: ${existsAsPending}`); // 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; @@ -78,6 +79,7 @@ export class Nullifiers { * @param nullifier - the nullifier to stage */ public async append(storageAddress: Fr, nullifier: Fr) { + console.log(`Nullifier ${nullifier} created at contract ${storageAddress}`); const [exists, ,] = await this.checkExists(storageAddress, nullifier); if (exists) { throw new NullifierCollisionError( @@ -122,6 +124,7 @@ export class NullifierCache { const exists = this.cachePerContract.get(storageAddress.toBigInt())?.has(nullifier.toBigInt()) || this.siloedNullifiers.has(siloNullifier(AztecAddress.fromField(storageAddress), nullifier).toBigInt()); + console.log(`In cache, nullifier ${nullifier} exists as pending at contract ${storageAddress}?: ${exists}`); return !!exists; } diff --git a/yarn-project/simulator/src/avm/journal/public_storage.ts b/yarn-project/simulator/src/avm/journal/public_storage.ts index 6019934c201d..624a68ef465d 100644 --- a/yarn-project/simulator/src/avm/journal/public_storage.ts +++ b/yarn-project/simulator/src/avm/journal/public_storage.ts @@ -71,6 +71,9 @@ export class PublicStorage { // Finally try the host's Aztec state (a trip to the database) if (!value) { value = await this.hostPublicStorage.storageRead(storageAddress, slot); + // TODO(dbanks12): if value retrieved from host storage, we can cache it here + // any future reads to the same slot can read from cache instead of more expensive + // DB access } else { cached = true; } diff --git a/yarn-project/simulator/src/avm/journal/trace.test.ts b/yarn-project/simulator/src/avm/journal/trace.test.ts index eb1b19030bc0..22a8e77ff7bb 100644 --- a/yarn-project/simulator/src/avm/journal/trace.test.ts +++ b/yarn-project/simulator/src/avm/journal/trace.test.ts @@ -1,81 +1,130 @@ import { Fr } from '@aztec/foundation/fields'; import { randomTracedContractInstance } from '../fixtures/index.js'; -import { WorldStateAccessTrace } from './trace.js'; -import { type TracedL1toL2MessageCheck, type TracedNullifier, type TracedNullifierCheck } from './trace_types.js'; +import { AvmSideEffectTrace } from './trace.js'; describe('world state access trace', () => { - let trace: WorldStateAccessTrace; + let trace: AvmSideEffectTrace; beforeEach(() => { - trace = new WorldStateAccessTrace(); + trace = new AvmSideEffectTrace(); }); describe('Basic tracing', () => { - it('Should trace note hash checks', () => { - const contractAddress = new Fr(1); - const noteHash = new Fr(2); - const exists = true; - const leafIndex = new Fr(42); + //it('Should trace note hash checks', () => { + // const contractAddress = new Fr(1); + // const noteHash = new Fr(2); + // const exists = true; + // const leafIndex = new Fr(42); - trace.traceNoteHashCheck(contractAddress, noteHash, exists, leafIndex); + // trace.traceNoteHashCheck(contractAddress, noteHash, exists, leafIndex); - expect(trace.noteHashChecks).toEqual([ - { - // callPointer: expect.any(Fr), - storageAddress: contractAddress, - noteHash: noteHash, - exists: exists, - counter: Fr.ZERO, // 0th access - // endLifetime: expect.any(Fr), - leafIndex: leafIndex, - }, - ]); - expect(trace.getCounter()).toBe(1); - }); - it('Should trace note hashes', () => { - const contractAddress = new Fr(1); - const utxo = new Fr(2); + // expect(trace.noteHashReadRequests).toEqual([ + // { + // storageAddress: contractAddress, + // noteHash: noteHash, + // exists: exists, + // counter: Fr.ZERO, // 0th access + // leafIndex: leafIndex, + // }, + // ]); + // expect(trace.getCounter()).toBe(1); + //}); + //it('Should trace note hashes', () => { + // const contractAddress = new Fr(1); + // const utxo = new Fr(2); - trace.traceNewNoteHash(contractAddress, utxo); + // trace.traceNewNoteHash(contractAddress, utxo); - expect(trace.newNoteHashes).toEqual([ - expect.objectContaining({ storageAddress: contractAddress, noteHash: utxo }), - ]); - expect(trace.getCounter()).toEqual(1); - }); - it('Should trace nullifier checks', () => { - const contractAddress = new Fr(1); - const utxo = new Fr(2); - const exists = true; - const isPending = false; + // expect(trace.newNoteHashes).toEqual([ + // expect.objectContaining({ storageAddress: contractAddress, noteHash: utxo }), + // ]); + // expect(trace.getCounter()).toEqual(1); + //}); + //it('Should trace nullifier checks', () => { + // const contractAddress = new Fr(1); + // const utxo = new Fr(2); + // const exists = true; + // const isPending = false; + // const leafIndex = new Fr(42); + // trace.traceNullifierCheck(contractAddress, utxo, exists, isPending, leafIndex); + // expect(trace.nullifierReadRequests).toEqual([ + // expect.objectContaining({ + // value: utxo, + // counter: Fr.ZERO, + // }), + // ]); + // expect(trace.nullifierNonExistentReadRequests).toEqual([]); + // expect(trace.avmCircuitHints.nullifierExists.items[0].value).toEqual(new Fr(1)); // exists true + // expect(trace.getCounter()).toEqual(1); + //}); + //it('Should trace non-existent nullifier checks', () => { + // const contractAddress = new Fr(1); + // const utxo = new Fr(2); + // const exists = false; + // const isPending = false; + // const leafIndex = new Fr(42); + // trace.traceNullifierCheck(contractAddress, utxo, exists, isPending, leafIndex); + // expect(trace.nullifierReadRequests).toEqual([]); + // expect(trace.nullifierNonExistentReadRequests).toEqual([ + // expect.objectContaining({ + // value: utxo, + // counter: Fr.ZERO, + // }), + // ]); + // expect(trace.avmCircuitHints.nullifierExists.items[0].value).toEqual(new Fr(0)); // exists false + // expect(trace.getCounter()).toEqual(1); + //}); + //it('Should trace nullifiers', () => { + // const contractAddress = new Fr(1); + // const utxo = new Fr(2); + // trace.traceNewNullifier(contractAddress, utxo); + // expect(trace.newNullifiers).toEqual([ + // expect.objectContaining({ + // value: utxo, + // counter: Fr.ZERO, + // }), + // ]); + // expect(trace.getCounter()).toEqual(1); + //}); + + //const noteHashCheckMembers = { + // traceIt: trace.traceNoteHashCheck, + // readRequests: trace.noteHashReadRequests, + // existsHints: trace.avmCircuitHints.noteHashExists.items, + //}; + const noteHashCheckMembers = { + traceIt: (t: AvmSideEffectTrace) => t.traceNoteHashCheck, + readRequests: (t: AvmSideEffectTrace) => t.noteHashReadRequests, + existsHints: (t: AvmSideEffectTrace) => t.avmCircuitHints.noteHashExists.items, + }; + + + it.each([ + ['noteHash', true, noteHashCheckMembers], + //['l1ToL2Message', true, 'traceL1ToL2MessageCheck', 'l1ToL2MsgReadRequests', 'l1ToL2MessageExists'], + ])('Should trace %s checks (read requests) (exist: %s)', + (_, exists, {traceIt, readRequests, existsHints}) => { + const traceIt = traceIt(trace); + const readRequests = readRequests(trace); + const existsHints = existsHints(trace); + + const address = new Fr(1); + const value = new Fr(2); const leafIndex = new Fr(42); - trace.traceNullifierCheck(contractAddress, utxo, exists, isPending, leafIndex); - const expectedCheck: TracedNullifierCheck = { - // callPointer: Fr.ZERO, - storageAddress: contractAddress, - nullifier: utxo, - exists: exists, - counter: Fr.ZERO, // 0th access - // endLifetime: Fr.ZERO, - isPending: isPending, - leafIndex: leafIndex, - }; - expect(trace.nullifierChecks).toEqual([expectedCheck]); - expect(trace.getCounter()).toEqual(1); - }); - it('Should trace nullifiers', () => { - const contractAddress = new Fr(1); - const utxo = new Fr(2); - trace.traceNewNullifier(contractAddress, utxo); - const expectedNullifier: TracedNullifier = { - // callPointer: Fr.ZERO, - storageAddress: contractAddress, - nullifier: utxo, - counter: new Fr(0), - // endLifetime: Fr.ZERO, - }; - expect(trace.newNullifiers).toEqual([expectedNullifier]); + traceIt(address, value, leafIndex, exists); + expect(readRequests).toEqual([ + expect.objectContaining({ + value, + counter: Fr.ZERO, + }), + ]); + expect(existsHints[0]).toEqual( + expect.objectContaining({ + key: Fr.ZERO, + value, + }), + ); expect(trace.getCounter()).toEqual(1); }); it('Should trace L1ToL2 Message checks', () => { @@ -83,13 +132,12 @@ describe('world state access trace', () => { const exists = true; const leafIndex = new Fr(42); trace.traceL1ToL2MessageCheck(utxo, leafIndex, exists); - const expectedCheck: TracedL1toL2MessageCheck = { - leafIndex: leafIndex, - msgHash: utxo, - exists: exists, - counter: new Fr(0), - }; - expect(trace.l1ToL2MessageChecks).toEqual([expectedCheck]); + expect(trace.l1ToL2MsgReadRequests).toEqual([ + expect.objectContaining({ + value: utxo, + counter: Fr.ZERO, + }), + ]); expect(trace.getCounter()).toEqual(1); }); it('Should trace get contract instance', () => { @@ -200,7 +248,7 @@ describe('world state access trace', () => { trace.traceL1ToL2MessageCheck(msgHash, msgLeafIndex, msgExists); trace.traceGetContractInstance(instance); - const childTrace = new WorldStateAccessTrace(trace); + const childTrace = new AvmSideEffectTrace(trace); childTrace.tracePublicStorageWrite(contractAddress, slot, valueT1); childTrace.tracePublicStorageRead(contractAddress, slot, valueT1, /*exists=*/ true, /*cached=*/ true); childTrace.traceNoteHashCheck(contractAddress, noteHashT1, noteHashExistsT1, noteHashLeafIndexT1); @@ -217,10 +265,10 @@ describe('world state access trace', () => { childTrace.traceGetContractInstance(instanceT1); const childCounterBeforeMerge = childTrace.getCounter(); - trace.acceptAndMerge(childTrace); + trace.processFinishedChildTrace(childTrace); expect(trace.getCounter()).toEqual(childCounterBeforeMerge); - expect(trace.publicStorageReads).toEqual([ + expect(trace.contractStorageReads).toEqual([ expect.objectContaining({ storageAddress: contractAddress, slot: slot, @@ -236,7 +284,7 @@ describe('world state access trace', () => { cached: true, }), ]); - expect(trace.publicStorageWrites).toEqual([ + expect(trace.contractStorageUpdateRequests).toEqual([ expect.objectContaining({ storageAddress: contractAddress, slot: slot, value: value }), expect.objectContaining({ storageAddress: contractAddress, slot: slot, value: valueT1 }), ]); @@ -260,7 +308,7 @@ describe('world state access trace', () => { nullifier: nullifierT1, }), ]); - expect(trace.nullifierChecks).toEqual([ + expect(trace.nullifierReadRequests).toEqual([ expect.objectContaining({ nullifier: nullifier, exists: nullifierExists, @@ -274,18 +322,18 @@ describe('world state access trace', () => { leafIndex: nullifierLeafIndexT1, }), ]); - expect(trace.noteHashChecks).toEqual([ + expect(trace.noteHashReadRequests).toEqual([ expect.objectContaining({ noteHash: noteHash, exists: noteHashExists, leafIndex: noteHashLeafIndex }), expect.objectContaining({ noteHash: noteHashT1, exists: noteHashExistsT1, leafIndex: noteHashLeafIndexT1 }), ]); expect( - trace.l1ToL2MessageChecks.map(c => ({ + trace.l1ToL2MsgReadRequests.map(c => ({ leafIndex: c.leafIndex, msgHash: c.msgHash, exists: c.exists, })), ).toEqual([expectedMessageCheck, expectedMessageCheckT1]); - expect(trace.l1ToL2MessageChecks).toEqual([ + expect(trace.l1ToL2MsgReadRequests).toEqual([ expect.objectContaining({ leafIndex: msgLeafIndex, msgHash: msgHash, exists: msgExists }), expect.objectContaining({ leafIndex: msgLeafIndexT1, msgHash: msgHashT1, exists: msgExistsT1 }), ]); diff --git a/yarn-project/simulator/src/avm/journal/trace.ts b/yarn-project/simulator/src/avm/journal/trace.ts deleted file mode 100644 index ca4d9f508f3b..000000000000 --- a/yarn-project/simulator/src/avm/journal/trace.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { Fr } from '@aztec/foundation/fields'; - -import { - type TracedContractInstance, - type TracedL1toL2MessageCheck, - type TracedNoteHash, - type TracedNoteHashCheck, - type TracedNullifier, - type TracedNullifierCheck, - type TracedPublicStorageWrite, - type TracedUnencryptedL2Log, -} from './trace_types.js'; -import { AvmExecutionHints, AvmKeyValueHint, AztecAddress, ContractStorageRead } from '@aztec/circuits.js'; - -export class WorldStateAccessTrace { - public counter: number; - - public publicStorageReads: ContractStorageRead[] = []; - public publicStorageWrites: TracedPublicStorageWrite[] = []; - - public noteHashChecks: TracedNoteHashCheck[] = []; - public newNoteHashes: TracedNoteHash[] = []; - public nullifierChecks: TracedNullifierCheck[] = []; - public newNullifiers: TracedNullifier[] = []; - public l1ToL2MessageChecks: TracedL1toL2MessageCheck[] = []; - public newLogsHashes: TracedUnencryptedL2Log[] = []; - public gotContractInstances: TracedContractInstance[] = []; - - // TODO(dbanks12): use this - public circuitHints: AvmExecutionHints; - - constructor(parentTrace?: WorldStateAccessTrace) { - this.counter = parentTrace ? parentTrace.counter : 0; - // TODO(4805): consider tracking the parent's trace vector lengths so we can enforce limits - this.circuitHints = AvmExecutionHints.empty(); - } - - public getCounter() { - return this.counter; - } - - 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) - // NOTE: exists and cached are unused for now but may be used for optimizations or kernel hints later - this.publicStorageReads.push( - new ContractStorageRead(slot, value, this.counter, AztecAddress.fromField(storageAddress)) - ); - this.circuitHints.storageValues.items.push(new AvmKeyValueHint(/*key=*/new Fr(this.counter), /*value=*/value)); - this.incrementAccessCounter(); - } - - public tracePublicStorageWrite(storageAddress: Fr, slot: Fr, value: Fr) { - // TODO(4805): check if some threshold is reached for max storage writes - // (need access to parent length, or trace needs to be initialized with parent's contents) - const traced: TracedPublicStorageWrite = { - // callPointer: Fr.ZERO, - contractAddress: storageAddress, - slot, - value, - counter: new Fr(this.counter), - // endLifetime: Fr.ZERO, - }; - this.publicStorageWrites.push(traced); - this.incrementAccessCounter(); - } - - public traceNoteHashCheck(storageAddress: Fr, noteHash: Fr, exists: boolean, leafIndex: Fr) { - const traced: TracedNoteHashCheck = { - // callPointer: Fr.ZERO, - storageAddress, - noteHash, - exists, - counter: new Fr(this.counter), - // endLifetime: Fr.ZERO, - leafIndex, - }; - this.noteHashChecks.push(traced); - this.incrementAccessCounter(); - } - - public traceNewNoteHash(storageAddress: Fr, noteHash: Fr) { - // TODO(4805): check if some threshold is reached for max new note hash - const traced: TracedNoteHash = { - // callPointer: Fr.ZERO, - storageAddress, - noteHash, - counter: new Fr(this.counter), - // endLifetime: Fr.ZERO, - }; - this.newNoteHashes.push(traced); - this.incrementAccessCounter(); - } - - public traceNullifierCheck(storageAddress: Fr, nullifier: Fr, exists: boolean, isPending: boolean, leafIndex: Fr) { - // TODO(4805): check if some threshold is reached for max new nullifier - const traced: TracedNullifierCheck = { - // callPointer: Fr.ZERO, - storageAddress, - nullifier, - exists, - counter: new Fr(this.counter), - // endLifetime: Fr.ZERO, - isPending, - leafIndex, - }; - this.nullifierChecks.push(traced); - this.incrementAccessCounter(); - } - - public traceNewNullifier(storageAddress: Fr, nullifier: Fr) { - // TODO(4805): check if some threshold is reached for max new nullifier - const tracedNullifier: TracedNullifier = { - // callPointer: Fr.ZERO, - storageAddress, - nullifier, - counter: new Fr(this.counter), - // endLifetime: Fr.ZERO, - }; - this.newNullifiers.push(tracedNullifier); - this.incrementAccessCounter(); - } - - public traceL1ToL2MessageCheck(msgHash: Fr, msgLeafIndex: Fr, exists: boolean) { - // TODO(4805): check if some threshold is reached for max message reads - const traced: TracedL1toL2MessageCheck = { - //callPointer: Fr.ZERO, // FIXME - leafIndex: msgLeafIndex, - msgHash: msgHash, - exists: exists, - counter: new Fr(this.counter), - //endLifetime: Fr.ZERO, // FIXME - }; - this.l1ToL2MessageChecks.push(traced); - this.incrementAccessCounter(); - } - - public traceNewLog(logHash: Fr) { - const traced: TracedUnencryptedL2Log = { - logHash, - counter: new Fr(this.counter), - }; - this.newLogsHashes.push(traced); - this.incrementAccessCounter(); - } - - public traceGetContractInstance(instance: TracedContractInstance) { - this.gotContractInstances.push(instance); - this.incrementAccessCounter(); - } - - private incrementAccessCounter() { - this.counter++; - } - - /** - * Merges another trace into this one - * - * @param incomingTrace - the incoming trace to merge into this instance - */ - public acceptAndMerge(incomingTrace: WorldStateAccessTrace) { - // Merge storage read and write journals - this.publicStorageReads.push(...incomingTrace.publicStorageReads); - this.publicStorageWrites.push(...incomingTrace.publicStorageWrites); - // Merge new note hashes and nullifiers - this.noteHashChecks.push(...incomingTrace.noteHashChecks); - this.newNoteHashes.push(...incomingTrace.newNoteHashes); - this.nullifierChecks.push(...incomingTrace.nullifierChecks); - this.newNullifiers.push(...incomingTrace.newNullifiers); - this.l1ToL2MessageChecks.push(...incomingTrace.l1ToL2MessageChecks); - this.newLogsHashes.push(...incomingTrace.newLogsHashes); - this.gotContractInstances.push(...incomingTrace.gotContractInstances); - // it is assumed that the incoming trace was initialized with this as parent, so accept counter - this.counter = incomingTrace.counter; - } -} diff --git a/yarn-project/simulator/src/avm/journal/trace_types.ts b/yarn-project/simulator/src/avm/journal/trace_types.ts deleted file mode 100644 index ecabd6033c33..000000000000 --- a/yarn-project/simulator/src/avm/journal/trace_types.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { type Fr } from '@aztec/foundation/fields'; -import { type ContractInstanceWithAddress } from '@aztec/types/contracts'; - -export type TracedPublicStorageWrite = { - contractAddress: Fr; - slot: Fr; - value: Fr; - counter: Fr; - // endLifetime: Fr; -}; - -export type TracedNoteHashCheck = { - storageAddress: Fr; - leafIndex: Fr; - noteHash: Fr; - exists: boolean; - counter: Fr; - // endLifetime: Fr; -}; - -export type TracedNoteHash = { - storageAddress: Fr; - noteHash: Fr; - counter: Fr; - // endLifetime: Fr; -}; - -export type TracedNullifierCheck = { - storageAddress: Fr; - nullifier: Fr; - exists: boolean; - counter: Fr; - // endLifetime: Fr; - // the fields below are relevant only to the public kernel - // and are therefore omitted from VM inputs - isPending: boolean; - leafIndex: Fr; -}; - -export type TracedNullifier = { - storageAddress: Fr; - nullifier: Fr; - counter: Fr; - // endLifetime: Fr; -}; - -export type TracedL1toL2MessageCheck = { - leafIndex: Fr; - msgHash: Fr; - exists: boolean; - counter: Fr; - //endLifetime: Fr; -}; - -export type TracedUnencryptedL2Log = { - logHash: Fr; - counter: Fr; - //endLifetime: Fr; -}; - -export type TracedContractInstance = { exists: boolean } & ContractInstanceWithAddress; diff --git a/yarn-project/simulator/src/avm/opcodes/accrued_substate.test.ts b/yarn-project/simulator/src/avm/opcodes/accrued_substate.test.ts index 5f4ac1eae0da..bb1d8e5fa8e8 100644 --- a/yarn-project/simulator/src/avm/opcodes/accrued_substate.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/accrued_substate.test.ts @@ -67,8 +67,8 @@ describe('Accrued Substate', () => { const exists = context.machineState.memory.getAs(existsOffset); expect(exists).toEqual(new Uint8(0)); - const journalState = context.persistableState.flush(); - expect(journalState.noteHashChecks).toEqual([ + const trace = context.persistableState.getTrace(); + expect(trace.noteHashChecks).toEqual([ expect.objectContaining({ exists: false, leafIndex: leafIndex.toFr(), noteHash: noteHash.toFr() }), ]); }); @@ -93,8 +93,8 @@ describe('Accrued Substate', () => { const exists = context.machineState.memory.getAs(existsOffset); expect(exists).toEqual(new Uint8(0)); - const journalState = context.persistableState.flush(); - expect(journalState.noteHashChecks).toEqual([ + const trace = context.persistableState.getTrace(); + expect(trace.noteHashChecks).toEqual([ expect.objectContaining({ exists: false, leafIndex: leafIndex.toFr(), noteHash: noteHash.toFr() }), ]); }); @@ -119,8 +119,8 @@ describe('Accrued Substate', () => { const exists = context.machineState.memory.getAs(existsOffset); expect(exists).toEqual(new Uint8(1)); - const journalState = context.persistableState.flush(); - expect(journalState.noteHashChecks).toEqual([ + const trace = context.persistableState.getTrace(); + expect(trace.noteHashChecks).toEqual([ expect.objectContaining({ exists: true, leafIndex: leafIndex.toFr(), noteHash: noteHash.toFr() }), ]); }); @@ -145,8 +145,8 @@ describe('Accrued Substate', () => { await new EmitNoteHash(/*indirect=*/ 0, /*offset=*/ 0).execute(context); - const journalState = context.persistableState.flush(); - expect(journalState.newNoteHashes).toEqual([ + const trace = context.persistableState.getTrace(); + expect(trace.newNoteHashes).toEqual([ expect.objectContaining({ storageAddress: context.environment.storageAddress, noteHash: value.toFr(), @@ -195,8 +195,8 @@ describe('Accrued Substate', () => { const exists = context.machineState.memory.getAs(existsOffset); expect(exists).toEqual(new Uint8(0)); - const journalState = context.persistableState.flush(); - expect(journalState.nullifierChecks).toEqual([ + const trace = context.persistableState.getTrace(); + expect(trace.nullifierChecks).toEqual([ expect.objectContaining({ nullifier: value.toFr(), storageAddress: address.toFr(), exists: false }), ]); }); @@ -222,8 +222,8 @@ describe('Accrued Substate', () => { const exists = context.machineState.memory.getAs(existsOffset); expect(exists).toEqual(new Uint8(1)); - const journalState = context.persistableState.flush(); - expect(journalState.nullifierChecks).toEqual([ + const trace = context.persistableState.getTrace(); + expect(trace.nullifierChecks).toEqual([ expect.objectContaining({ nullifier: value.toFr(), storageAddress: address.toFr(), exists: true }), ]); }); @@ -248,8 +248,8 @@ describe('Accrued Substate', () => { await new EmitNullifier(/*indirect=*/ 0, /*offset=*/ 0).execute(context); - const journalState = context.persistableState.flush(); - expect(journalState.newNullifiers).toEqual([ + const trace = context.persistableState.getTrace(); + expect(trace.newNullifiers).toEqual([ expect.objectContaining({ storageAddress: context.environment.storageAddress.toField(), nullifier: value.toFr(), @@ -327,8 +327,8 @@ describe('Accrued Substate', () => { const exists = context.machineState.memory.getAs(existsOffset); expect(exists).toEqual(new Uint8(0)); - const journalState = context.persistableState.flush(); - expect(journalState.l1ToL2MessageChecks).toEqual([ + const trace = context.persistableState.getTrace(); + expect(trace.l1ToL2MessageChecks).toEqual([ expect.objectContaining({ leafIndex: leafIndex.toFr(), msgHash: msgHash.toFr(), exists: false }), ]); }); @@ -353,8 +353,8 @@ describe('Accrued Substate', () => { const exists = context.machineState.memory.getAs(existsOffset); expect(exists).toEqual(new Uint8(1)); - const journalState = context.persistableState.flush(); - expect(journalState.l1ToL2MessageChecks).toEqual([ + const trace = context.persistableState.getTrace(); + expect(trace.l1ToL2MessageChecks).toEqual([ expect.objectContaining({ leafIndex: leafIndex.toFr(), msgHash: msgHash.toFr(), exists: true }), ]); }); @@ -379,8 +379,8 @@ describe('Accrued Substate', () => { const exists = context.machineState.memory.getAs(existsOffset); expect(exists).toEqual(new Uint8(0)); - const journalState = context.persistableState.flush(); - expect(journalState.l1ToL2MessageChecks).toEqual([ + const trace = context.persistableState.getTrace(); + expect(trace.l1ToL2MessageChecks).toEqual([ expect.objectContaining({ leafIndex: leafIndex.toFr(), msgHash: msgHash.toFr(), exists: false }), ]); }); @@ -424,9 +424,9 @@ describe('Accrued Substate', () => { logSizeOffset, ).execute(context); - const journalState = context.persistableState.flush(); + const trace = context.persistableState.getTrace(); const expectedLog = Buffer.concat(values.map(v => v.toFr().toBuffer())); - expect(journalState.newLogs).toEqual([ + expect(trace.newLogs).toEqual([ new UnencryptedL2Log(context.environment.address, new EventSelector(eventSelector), expectedLog), ]); }); @@ -465,8 +465,8 @@ describe('Accrued Substate', () => { /*contentOffset=*/ contentOffset, ).execute(context); - const journalState = context.persistableState.flush(); - expect(journalState.newL1Messages).toEqual([ + const trace = context.persistableState.getTrace(); + expect(trace.newL1Messages).toEqual([ expect.objectContaining({ recipient: EthAddress.fromField(recipient), content }), ]); }); diff --git a/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts b/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts index c227710208fd..8c4f7c149c08 100644 --- a/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts +++ b/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts @@ -201,7 +201,7 @@ export class L1ToL2MessageExists extends Instruction { const msgHash = memory.get(msgHashOffset).toFr(); const msgLeafIndex = memory.get(msgLeafIndexOffset).toFr(); - const exists = await context.persistableState.checkL1ToL2MessageExists(msgHash, msgLeafIndex); + const exists = await context.persistableState.checkL1ToL2MessageExists(context.environment.address, msgHash, msgLeafIndex); memory.set(existsOffset, exists ? new Uint8(1) : new Uint8(0)); memory.assert(memoryOperations); @@ -252,7 +252,7 @@ export class EmitUnencryptedLog extends Instruction { const memoryOperations = { reads: 2 + logSize, indirect: this.indirect }; context.machineState.consumeGas(this.gasCost(memoryOperations)); const log = memory.getSlice(logOffset, logSize).map(f => f.toFr()); - context.persistableState.writeLog(contractAddress, event, log); + context.persistableState.writeUnencryptedLog(contractAddress, event, log); memory.assert(memoryOperations); context.machineState.incrementPc(); @@ -285,7 +285,7 @@ export class SendL2ToL1Message extends Instruction { const recipient = memory.get(recipientOffset).toFr(); const content = memory.get(contentOffset).toFr(); - context.persistableState.writeL1Message(recipient, content); + context.persistableState.writeL2ToL1Message(recipient, content); memory.assert(memoryOperations); 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 6dd086bc78d3..1297a0e76199 100644 --- a/yarn-project/simulator/src/avm/opcodes/external_calls.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/external_calls.test.ts @@ -118,7 +118,7 @@ describe('External Calls', () => { expect(retValue).toEqual([new Field(1n), new Field(2n)]); // Check that the storage call has been merged into the parent journal - const { currentStorageValue } = context.persistableState.flush(); + const { currentStorageValue } = context.persistableState.getTrace()(); expect(currentStorageValue.size).toEqual(1); const nestedContractWrites = currentStorageValue.get(addr.toBigInt()); diff --git a/yarn-project/simulator/src/avm/opcodes/external_calls.ts b/yarn-project/simulator/src/avm/opcodes/external_calls.ts index c89c84c44ebd..448246a74f16 100644 --- a/yarn-project/simulator/src/avm/opcodes/external_calls.ts +++ b/yarn-project/simulator/src/avm/opcodes/external_calls.ts @@ -1,7 +1,7 @@ import { FunctionSelector, Gas } from '@aztec/circuits.js'; import { padArrayEnd } from '@aztec/foundation/collection'; -import { convertAvmResultsToPxResult, createPublicExecution } from '../../public/transitional_adaptors.js'; +import { createPublicExecutionRequest } from '../../public/transitional_adaptors.js'; import type { AvmContext } from '../avm_context.js'; import { gasLeftToGas } from '../avm_gas.js'; import { Field, TypeTag, Uint8 } from '../avm_memory_types.js'; @@ -11,6 +11,7 @@ import { RethrownError } from '../errors.js'; import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; import { Addressing } from './addressing_mode.js'; import { Instruction } from './instruction.js'; +import { createSimulationError } from '../../common/errors.js'; abstract class ExternalCall extends Instruction { // Informs (de)serialization. See Instruction.deserialize. @@ -24,7 +25,6 @@ abstract class ExternalCall extends Instruction { OperandType.UINT32, OperandType.UINT32, OperandType.UINT32, - /* temporary function selector */ OperandType.UINT32, ]; @@ -37,8 +37,8 @@ abstract class ExternalCall extends Instruction { private retOffset: number, private retSize: number, private successOffset: number, - // Function selector is temporary since eventually public contract bytecode will be one blob - // containing all functions, and function selector will become an application-level mechanism + // NOTE: Function selector is likely temporary since eventually public contract bytecode will be one + // blob containing all functions, and function selector will become an application-level mechanism // (e.g. first few bytes of calldata + compiler-generated jump table) private functionSelectorOffset: number, ) { @@ -81,7 +81,6 @@ abstract class ExternalCall extends Instruction { const allocatedGas = { l2Gas: allocatedL2Gas, daGas: allocatedDaGas }; context.machineState.consumeGas(allocatedGas); - // TRANSITIONAL: This should be removed once the kernel handles and entire enqueued call per circuit const nestedContext = context.createNestedContractCallContext( callAddress.toFr(), calldata, @@ -89,32 +88,9 @@ abstract class ExternalCall extends Instruction { callType, FunctionSelector.fromField(functionSelector), ); - const startSideEffectCounter = nestedContext.persistableState.trace.counter; - const oldStyleExecution = createPublicExecution(startSideEffectCounter, nestedContext.environment, calldata); const simulator = new AvmSimulator(nestedContext); const nestedCallResults: AvmContractCallResults = await simulator.execute(); - const pxResults = convertAvmResultsToPxResult( - nestedCallResults, - startSideEffectCounter, - oldStyleExecution, - Gas.from(allocatedGas), - nestedContext, - simulator.getBytecode(), - ); - // store the old PublicExecutionResult object to maintain a recursive data structure for the old kernel - context.persistableState.transitionalExecutionResult.nestedExecutions.push(pxResults); - // END TRANSITIONAL - - // const nestedContext = context.createNestedContractCallContext( - // callAddress.toFr(), - // calldata, - // allocatedGas, - // this.type, - // FunctionSelector.fromField(functionSelector), - // ); - // const nestedCallResults: AvmContractCallResults = await new AvmSimulator(nestedContext).execute(); - const success = !nestedCallResults.reverted; // TRANSITIONAL: We rethrow here so that the MESSAGE gets propagated. @@ -143,12 +119,16 @@ abstract class ExternalCall extends Instruction { // Refund unused gas context.machineState.refundGas(gasLeftToGas(nestedContext.machineState)); - // TODO: Should we merge the changes from a nested call in the case of a STATIC call? - if (success) { - context.persistableState.acceptNestedCallState(nestedContext.persistableState); - } else { - context.persistableState.rejectNestedCallState(nestedContext.persistableState); - } + // Accept the nested call's state and trace the nested call + context.persistableState.processNestedCall( + /*nestedState=*/nestedContext.persistableState, + /*success=*/success, + /*nestedEnvironment=*/nestedContext.environment, + /*startGasLeft=*/Gas.from(allocatedGas), + /*endGasLeft=*/Gas.from(nestedContext.machineState.gasLeft), + /*bytecode=*/simulator.getBytecode()!, + /*avmCallResults=*/nestedCallResults, + ); memory.assert(memoryOperations); context.machineState.incrementPc(); diff --git a/yarn-project/simulator/src/mocks/fixtures.ts b/yarn-project/simulator/src/mocks/fixtures.ts index 088cdec820a6..971968259ca7 100644 --- a/yarn-project/simulator/src/mocks/fixtures.ts +++ b/yarn-project/simulator/src/mocks/fixtures.ts @@ -143,7 +143,7 @@ export class PublicExecutionResultBuilder { endGasLeft: Gas.test(), transactionFee: Fr.ZERO, calldata: [], - avmHints: AvmExecutionHints.empty(), + avmCircuitHints: AvmExecutionHints.empty(), ...overrides, }; } diff --git a/yarn-project/simulator/src/public/abstract_phase_manager.ts b/yarn-project/simulator/src/public/abstract_phase_manager.ts index 3dd7016612d8..edefd1c7b486 100644 --- a/yarn-project/simulator/src/public/abstract_phase_manager.ts +++ b/yarn-project/simulator/src/public/abstract_phase_manager.ts @@ -313,7 +313,7 @@ export abstract class AbstractPhaseManager { calldata: result.calldata, bytecode: result.bytecode!, inputs: privateInputs, - avmHints: result.avmHints, + avmHints: result.avmCircuitHints, }; provingInformationList.push(publicProvingInformation); diff --git a/yarn-project/simulator/src/public/execution.ts b/yarn-project/simulator/src/public/execution.ts index bb83e9605b77..d71e5c9fca9e 100644 --- a/yarn-project/simulator/src/public/execution.ts +++ b/yarn-project/simulator/src/public/execution.ts @@ -20,16 +20,35 @@ import { type Gas } from '../avm/avm_gas.js'; export interface PublicExecutionResult { /** The execution that triggered this result. */ execution: PublicExecution; + /** The results of nested calls. */ + nestedExecutions: this[]; + + /** The side effect counter at the start of the function call. */ + startSideEffectCounter: Fr; + /** The side effect counter after executing this function call */ + endSideEffectCounter: Fr; + /** How much gas was available for this public execution. */ + startGasLeft: Gas; + /** How much gas was left after this public execution. */ + endGasLeft: Gas; + /** Transaction fee set for this tx. */ + transactionFee: Fr; + + /** Bytecode used for this execution. */ + bytecode?: Buffer; + /** Calldata used for this execution. */ + calldata: Fr[]; /** The return values of the function. */ returnValues: Fr[]; + /** Whether the execution reverted. */ + reverted: boolean; + /** The revert reason if the execution reverted. */ + revertReason?: SimulationError; + /** The new note hashes to be inserted into the note hashes tree. */ newNoteHashes: NoteHash[]; /** The new l2 to l1 messages generated in this call. */ newL2ToL1Messages: L2ToL1Message[]; - /** The side effect counter at the start of the function call. */ - startSideEffectCounter: Fr; - /** The side effect counter after executing this function call */ - endSideEffectCounter: Fr; /** The new nullifiers to be inserted into the nullifier tree. */ newNullifiers: Nullifier[]; /** The note hash read requests emitted in this call. */ @@ -44,8 +63,7 @@ export interface PublicExecutionResult { contractStorageReads: ContractStorageRead[]; /** The contract storage update requests performed by the function. */ contractStorageUpdateRequests: ContractStorageUpdateRequest[]; - /** The results of nested calls. */ - nestedExecutions: this[]; + // TODO(dbanks12): add contractInstance ReadRequests /** * The hashed logs with side effect counter. * Note: required as we don't track the counter anywhere else. @@ -61,22 +79,9 @@ export interface PublicExecutionResult { * Useful for maintaining correct ordering in ts. */ allUnencryptedLogs: UnencryptedFunctionL2Logs; - /** Whether the execution reverted. */ - reverted: boolean; - /** The revert reason if the execution reverted. */ - revertReason?: SimulationError; - /** How much gas was available for this public execution. */ - startGasLeft: Gas; - /** How much gas was left after this public execution. */ - endGasLeft: Gas; - /** Transaction fee set for this tx. */ - transactionFee: Fr; - /** Bytecode used for this execution. */ - bytecode?: Buffer; - /** Calldata used for this execution. */ - calldata: Fr[]; + /** Hints for proving AVM execution. */ - avmHints: AvmExecutionHints; + avmCircuitHints: AvmExecutionHints; } /** diff --git a/yarn-project/simulator/src/public/executor.ts b/yarn-project/simulator/src/public/executor.ts index a9f2d3c22498..9d0e222fa8a3 100644 --- a/yarn-project/simulator/src/public/executor.ts +++ b/yarn-project/simulator/src/public/executor.ts @@ -1,5 +1,5 @@ import { type AvmSimulationStats } from '@aztec/circuit-types/stats'; -import { Fr, type Gas, type GlobalVariables, type Header, type Nullifier, type TxContext } from '@aztec/circuits.js'; +import { Fr, Gas, type GlobalVariables, type Header, type Nullifier, type TxContext } from '@aztec/circuits.js'; import { createDebugLogger } from '@aztec/foundation/log'; import { Timer } from '@aztec/foundation/timer'; @@ -10,7 +10,8 @@ import { HostStorage } from '../avm/journal/host_storage.js'; import { AvmPersistableStateManager } from '../avm/journal/index.js'; import { type CommitmentsDB, type PublicContractsDB, type PublicStateDB } from './db_interfaces.js'; import { type PublicExecution, type PublicExecutionResult, checkValidStaticCall } from './execution.js'; -import { convertAvmResultsToPxResult, createAvmExecutionEnvironment } from './transitional_adaptors.js'; +import { createAvmExecutionEnvironment } from './transitional_adaptors.js'; +import { AvmSideEffectTrace } from './side_effect_trace.js'; /** * Handles execution of public functions. @@ -27,12 +28,17 @@ export class PublicExecutor { /** * Executes a public execution request. - * @param execution - The execution to run. + * @param executionRequest - The execution to run. * @param globalVariables - The global variables to use. - * @returns The result of the run plus all nested runs. + * @param availableGas - The gas available at the start of this enqueued call. + * @param txContext - Transaction context. + * @param pendingNullifiers - The pending nullifier set from earlier parts of this TX. + * @param transactionFee - Fee offered for this TX. + * @param startSideEffectCounter - The counter of the first side-effect generated by this simulation. + * @returns The result of execution, including the results of all nested calls. */ public async simulate( - execution: PublicExecution, + executionRequest: PublicExecution, globalVariables: GlobalVariables, availableGas: Gas, txContext: TxContext, @@ -40,11 +46,7 @@ export class PublicExecutor { transactionFee: Fr = Fr.ZERO, startSideEffectCounter: number = 0, ): Promise { - const address = execution.contractAddress; - const selector = execution.functionSelector; - const startGas = availableGas; - const fnName = await this.contractsDb.getDebugFunctionName(address, selector); - + const fnName = await this.contractsDb.getDebugFunctionName(executionRequest.contractAddress, executionRequest.functionSelector); PublicExecutor.log.verbose(`[AVM] Executing public external function ${fnName}.`); const timer = new Timer(); @@ -52,29 +54,34 @@ export class PublicExecutor { // These data structures will permeate across the simulator when the public executor is phased out const hostStorage = new HostStorage(this.stateDb, this.contractsDb, this.commitmentsDb); - const worldStateJournal = new AvmPersistableStateManager(hostStorage); + const trace = new AvmSideEffectTrace(startSideEffectCounter); + + const persistableState = new AvmPersistableStateManager(hostStorage, trace); + // TODO(dbanks12): pass in parent state manager instead of doing this hack for (const nullifier of pendingNullifiers) { - worldStateJournal.nullifiers.cache.appendSiloed(nullifier.value); + persistableState.nullifiers.cache.appendSiloed(nullifier.value); } - worldStateJournal.trace.counter = startSideEffectCounter; - const executionEnv = createAvmExecutionEnvironment( - execution, + const avmExecutionEnv = createAvmExecutionEnvironment( + executionRequest, this.header, globalVariables, txContext.gasSettings, transactionFee, ); - const machineState = new AvmMachineState(startGas); - const avmContext = new AvmContext(worldStateJournal, executionEnv, machineState); + const avmMachineState = new AvmMachineState(availableGas); + const avmContext = new AvmContext(persistableState, avmExecutionEnv, avmMachineState); const simulator = new AvmSimulator(avmContext); const avmResult = await simulator.execute(); - const bytecode = simulator.getBytecode(); + const bytecode = simulator.getBytecode()!; // 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. + // TODO(dbanks12): consider moving into some journal-level function? Or maybe this + // becomes unnecessary if all enqueued calls see previous one as parent? + // But what about setup and teardown which need to have their state committed even if the app logic reverts. await avmContext.persistableState.publicStorage.commitToDB(); PublicExecutor.log.verbose( @@ -89,27 +96,26 @@ export class PublicExecutor { } satisfies AvmSimulationStats, ); - const executionResult = convertAvmResultsToPxResult( - avmResult, - startSideEffectCounter, - execution, - startGas, - avmContext, - bytecode, + const publicExecutionResult = trace.toPublicExecutionResult( + /*avmEnvironment=*/avmExecutionEnv, + /*startGasLeft=*/availableGas, + /*endGasLeft=*/Gas.from(avmContext.machineState.gasLeft), + /*bytecode=*/bytecode, + /*avmCallResults=*/avmResult, ); // TODO(https://github.com/AztecProtocol/aztec-packages/issues/5818): is this really needed? // should already be handled in simulation. - if (execution.callContext.isStaticCall) { + if (executionRequest.callContext.isStaticCall) { checkValidStaticCall( - executionResult.newNoteHashes, - executionResult.newNullifiers, - executionResult.contractStorageUpdateRequests, - executionResult.newL2ToL1Messages, - executionResult.unencryptedLogs, + publicExecutionResult.newNoteHashes, + publicExecutionResult.newNullifiers, + publicExecutionResult.contractStorageUpdateRequests, + publicExecutionResult.newL2ToL1Messages, + publicExecutionResult.unencryptedLogs, ); } - return executionResult; + return publicExecutionResult; } } diff --git a/yarn-project/simulator/src/public/side_effect_trace.ts b/yarn-project/simulator/src/public/side_effect_trace.ts new file mode 100644 index 000000000000..f1328fc4f6ff --- /dev/null +++ b/yarn-project/simulator/src/public/side_effect_trace.ts @@ -0,0 +1,296 @@ +import { AvmContractInstanceHint, AvmExecutionHints, AvmExternalCallHint, AvmKeyValueHint, AztecAddress, CallContext, ContractStorageRead, ContractStorageUpdateRequest, EthAddress, Gas, L2ToL1Message, LogHash, NoteHash, Nullifier, ReadRequest } from '@aztec/circuits.js'; +import { UnencryptedFunctionL2Logs, UnencryptedL2Log } from '@aztec/circuit-types'; +import { ContractInstanceWithAddress } from '@aztec/types/contracts'; +import { EventSelector } from '@aztec/foundation/abi'; +import { Fr } from '@aztec/foundation/fields'; + +import { + type PublicExecution, + type PublicExecutionResult, +} from './execution.js'; +import { PublicSideEffectTraceInterface } from './side_effect_trace_interface.js'; +import { createSimulationError } from '../common/errors.js'; +import { AvmExecutionEnvironment } from '../avm/avm_execution_environment.js'; +import { AvmContractCallResults } from '../avm/avm_message_call_result.js'; + +export type TracedContractInstance = { exists: boolean } & ContractInstanceWithAddress; + +export class AvmSideEffectTrace implements PublicSideEffectTraceInterface { + /** The side effect counter increments with every call to the trace. */ + private sideEffectCounter: number; // kept as number until finalized for efficiency + + private contractStorageReads: ContractStorageRead[] = []; + private contractStorageUpdateRequests: ContractStorageUpdateRequest[] = []; + + private noteHashReadRequests: ReadRequest[] = []; + private newNoteHashes: NoteHash[] = []; + + private nullifierReadRequests: ReadRequest[] = []; + private nullifierNonExistentReadRequests: ReadRequest[] = []; + private newNullifiers: Nullifier[] = []; + + private l1ToL2MsgReadRequests: ReadRequest[] = []; + private newL2ToL1Messages: L2ToL1Message[] = []; + + private unencryptedLogs: UnencryptedL2Log[] = []; + private allUnencryptedLogs: UnencryptedL2Log[] = []; + private unencryptedLogsHashes: LogHash[] = []; + + private gotContractInstances: ContractInstanceWithAddress[] = []; + + private nestedExecutions: PublicExecutionResult[] = []; + + private avmCircuitHints: AvmExecutionHints; + + constructor( + /** The counter of this trace's first side effect. */ + public readonly startSideEffectCounter: number = 0, + ) { + this.sideEffectCounter = startSideEffectCounter; + this.avmCircuitHints = AvmExecutionHints.empty(); + } + + public fork() { + return new AvmSideEffectTrace(this.sideEffectCounter); + } + + public getCounter() { + return this.sideEffectCounter; + } + + private incrementSideEffectCounter() { + this.sideEffectCounter++; + } + + 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) + // NOTE: exists and cached are unused for now but may be used for optimizations or kernel hints later + this.contractStorageReads.push( + new ContractStorageRead(slot, value, this.sideEffectCounter, AztecAddress.fromField(storageAddress)) + ); + this.avmCircuitHints.storageValues.items.push(new AvmKeyValueHint(/*key=*/new Fr(this.sideEffectCounter), /*value=*/value)); + this.incrementSideEffectCounter(); + } + + public tracePublicStorageWrite(storageAddress: Fr, slot: Fr, value: Fr) { + // TODO(4805): check if some threshold is reached for max storage writes + // (need access to parent length, or trace needs to be initialized with parent's contents) + this.contractStorageUpdateRequests.push( + new ContractStorageUpdateRequest(slot, value, this.sideEffectCounter, storageAddress) + ); + this.incrementSideEffectCounter(); + } + + public traceNoteHashCheck(_storageAddress: Fr, noteHash: Fr, _leafIndex: Fr, exists: boolean) { + // TODO(4805): check if some threshold is reached for max note hash checks + // NOTE: storageAddress is unused but will be important when an AVM circuit processes an entire enqueued call + // TODO(dbanks12): leafIndex is unused for now but later must be used by kernel to constrain that the kernel + // is in fact checking the leaf indicated by the user + this.noteHashReadRequests.push( + new ReadRequest(noteHash, this.sideEffectCounter) + ); + this.avmCircuitHints.noteHashExists.items.push(new AvmKeyValueHint(/*key=*/new Fr(this.sideEffectCounter), /*value=*/new Fr(exists ? 1 : 0))); + this.incrementSideEffectCounter(); + } + + public traceNewNoteHash(_storageAddress: Fr, noteHash: Fr) { + // TODO(4805): check if some threshold is reached for max new note hash + // NOTE: storageAddress is unused but will be important when an AVM circuit processes an entire enqueued call + // TODO(dbanks12): non-existent note hashes should emit a read request of the note hash that actually + // IS there, and the AVM circuit should accept THAT noteHash as a hint. The circuit will then compare + // the noteHash against the one provided by the user code to determine what to return to the user (exists or not), + // and will then propagate the actually-present noteHash to its public inputs. + this.newNoteHashes.push(new NoteHash(noteHash, this.sideEffectCounter)); + this.incrementSideEffectCounter(); + } + + public traceNullifierCheck(_storageAddress: Fr, nullifier: Fr, _leafIndex: Fr, exists: boolean, _isPending: boolean) { + // TODO(4805): check if some threshold is reached for max new nullifier + // NOTE: storageAddress is unused but will be important when an AVM circuit processes an entire enqueued call + // NOTE: isPending and leafIndex are unused for now but may be used for optimizations or kernel hints later + const readRequest = new ReadRequest(nullifier, this.sideEffectCounter); + if (exists) { + this.nullifierReadRequests.push(readRequest); + } else { + this.nullifierNonExistentReadRequests.push(readRequest); + } + this.avmCircuitHints.nullifierExists.items.push(new AvmKeyValueHint(/*key=*/new Fr(this.sideEffectCounter), /*value=*/new Fr(exists ? 1 : 0))); + this.incrementSideEffectCounter(); + } + + public traceNewNullifier(_storageAddress: Fr, nullifier: Fr) { + // TODO(4805): check if some threshold is reached for max new nullifier + // NOTE: storageAddress is unused but will be important when an AVM circuit processes an entire enqueued call + this.newNullifiers.push( + new Nullifier(nullifier, this.sideEffectCounter, /*noteHash=*/ Fr.ZERO), + ); + this.incrementSideEffectCounter(); + } + + public traceL1ToL2MessageCheck(_contractAddress: Fr, msgHash: Fr, _msgLeafIndex: Fr, exists: boolean) { + // TODO(4805): check if some threshold is reached for max message reads + // NOTE: contractAddress is unused but will be important when an AVM circuit processes an entire enqueued call + // TODO(dbanks12): leafIndex is unused for now but later must be used by kernel to constrain that the kernel + // is in fact checking the leaf indicated by the user + this.l1ToL2MsgReadRequests.push( + new ReadRequest(msgHash, this.sideEffectCounter) + ); + this.avmCircuitHints.l1ToL2MessageExists.items.push(new AvmKeyValueHint(/*key=*/new Fr(this.sideEffectCounter), /*value=*/new Fr(exists ? 1 : 0))); + this.incrementSideEffectCounter(); + } + + public traceNewL2ToL1Message(recipient: Fr, content: Fr) { + // TODO(4805): check if some threshold is reached for max messages + const recipientAddress = EthAddress.fromField(recipient); + this.newL2ToL1Messages.push(new L2ToL1Message(recipientAddress, content, this.sideEffectCounter)); + this.incrementSideEffectCounter(); + } + + public traceUnencryptedLog(contractAddress: Fr, event: Fr, log: Fr[]) { + // TODO(4805): check if some threshold is reached for max logs + const ulog = new UnencryptedL2Log( + AztecAddress.fromField(contractAddress), + EventSelector.fromField(event), + Buffer.concat(log.map(f => f.toBuffer())), + ); + const basicLogHash = Fr.fromBuffer(ulog.hash()); + this.unencryptedLogs.push(ulog); + this.allUnencryptedLogs.push(ulog); + // TODO(6578): explain magic number 4 here + this.unencryptedLogsHashes.push(new LogHash(basicLogHash, this.sideEffectCounter, new Fr(ulog.length + 4))); + this.incrementSideEffectCounter(); + } + + public traceGetContractInstance(instance: TracedContractInstance) { + // TODO(4805): check if some threshold is reached for max contract instance retrievals + this.gotContractInstances.push(instance); + this.avmCircuitHints.contractInstances.items.push( + new AvmContractInstanceHint( + instance.address, + new Fr(instance.exists ? 1 : 0), + instance.salt, + instance.deployer, + instance.contractClassId, + instance.initializationHash, + instance.publicKeysHash, + ) + ); + this.incrementSideEffectCounter(); + } + + /** + * Trace a nested call. + * Accept some results from a finished nested call's trace into this one. + */ + public traceNestedCall( + /** The trace of the nested call. */ + nestedCallTrace: AvmSideEffectTrace, + /** The execution environment of the nested call. */ + nestedEnvironment: AvmExecutionEnvironment, + /** How much gas was available for this public execution. */ + startGasLeft: Gas, + /** How much gas was left after this public execution. */ + endGasLeft: Gas, + /** Bytecode used for this execution. */ + bytecode: Buffer, + /** The call's results */ + avmCallResults: AvmContractCallResults, + ) { + const result = nestedCallTrace.toPublicExecutionResult( + nestedEnvironment, + startGasLeft, + endGasLeft, + bytecode, + avmCallResults, + ); + this.sideEffectCounter = result.endSideEffectCounter.toNumber(); + // when a nested call returns, caller accepts its updated counter + this.allUnencryptedLogs.push(...result.allUnencryptedLogs.logs); + // NOTE: eventually if the AVM circuit processes an entire enqueued call, + // this function will accept all of the nested's side effects into this instance + this.nestedExecutions.push(result); + + const gasUsed = new Gas( + result.startGasLeft.daGas - result.endGasLeft.daGas, + result.startGasLeft.l2Gas - result.endGasLeft.l2Gas, + ); + this.avmCircuitHints.externalCalls.items.push(new AvmExternalCallHint(/*success=*/ new Fr(result.reverted ? 0 : 1), result.returnValues, gasUsed)); + } + + /** + * Convert this trace to a PublicExecutionResult for use externally to the simulator. + */ + public toPublicExecutionResult( + /** The execution environment of the nested call. */ + nestedEnvironment: AvmExecutionEnvironment, + /** How much gas was available for this public execution. */ + startGasLeft: Gas, + /** How much gas was left after this public execution. */ + endGasLeft: Gas, + /** Bytecode used for this execution. */ + bytecode: Buffer, + /** The call's results */ + avmCallResults: AvmContractCallResults, + ): PublicExecutionResult { + return { + execution: createPublicExecutionRequest(this.startSideEffectCounter, nestedEnvironment), + + startSideEffectCounter: new Fr(this.startSideEffectCounter), + endSideEffectCounter: new Fr(this.sideEffectCounter), + startGasLeft, + endGasLeft, + transactionFee: nestedEnvironment.transactionFee, + + bytecode, + calldata: nestedEnvironment.calldata, + returnValues: avmCallResults.output, + reverted: avmCallResults.reverted, + revertReason: avmCallResults.revertReason ? createSimulationError(avmCallResults.revertReason) : undefined, + + // TODO(dbanks12): get args to match ordering of PublicExecutionResults' fields + contractStorageReads: this.contractStorageReads, + contractStorageUpdateRequests: this.contractStorageUpdateRequests, + noteHashReadRequests: this.noteHashReadRequests, + newNoteHashes: this.newNoteHashes, + nullifierReadRequests: this.nullifierReadRequests, + nullifierNonExistentReadRequests: this.nullifierNonExistentReadRequests, + newNullifiers: this.newNullifiers, + l1ToL2MsgReadRequests: this.l1ToL2MsgReadRequests, + newL2ToL1Messages: this.newL2ToL1Messages, + // correct the type on these now that they are finalized (lists won't grow) + unencryptedLogs: new UnencryptedFunctionL2Logs(this.unencryptedLogs), + allUnencryptedLogs: new UnencryptedFunctionL2Logs(this.allUnencryptedLogs), + unencryptedLogsHashes: this.unencryptedLogsHashes, + //gotContractInstances: this.gotContractInstances, + + nestedExecutions: this.nestedExecutions, + + avmCircuitHints: this.avmCircuitHints, + }; + } +} + +/** + * Helper function to create a public execution request from an AVM execution environment + */ +function createPublicExecutionRequest( + startSideEffectCounter: number, + avmEnvironment: AvmExecutionEnvironment, +): PublicExecution { + const callContext = CallContext.from({ + msgSender: avmEnvironment.sender, + storageContractAddress: avmEnvironment.storageAddress, + functionSelector: avmEnvironment.temporaryFunctionSelector, + isDelegateCall: avmEnvironment.isDelegateCall, + isStaticCall: avmEnvironment.isStaticCall, + sideEffectCounter: startSideEffectCounter, + }); + const execution: PublicExecution = { + contractAddress: avmEnvironment.address, + callContext, + args: avmEnvironment.calldata, + functionSelector: avmEnvironment.temporaryFunctionSelector, + }; + return execution; +} diff --git a/yarn-project/simulator/src/public/side_effect_trace_interface.ts b/yarn-project/simulator/src/public/side_effect_trace_interface.ts new file mode 100644 index 000000000000..1a890c37185c --- /dev/null +++ b/yarn-project/simulator/src/public/side_effect_trace_interface.ts @@ -0,0 +1,97 @@ +import { Fr } from '@aztec/foundation/fields'; + +import { TracedContractInstance } from './side_effect_trace.js'; +import { Gas } from '@aztec/circuits.js'; +import { AvmExecutionEnvironment } from '../avm/avm_execution_environment.js'; +import { AvmContractCallResults } from '../avm/avm_message_call_result.js'; + +export interface PublicSideEffectTraceInterface { + fork(): PublicSideEffectTraceInterface; + getCounter(): number; + tracePublicStorageRead(storageAddress: Fr, slot: Fr, value: Fr, exists: boolean, cached: boolean): void; + tracePublicStorageWrite(storageAddress: Fr, slot: Fr, value: Fr): void; + traceNoteHashCheck(storageAddress: Fr, noteHash: Fr, leafIndex: Fr, exists: boolean): void; + traceNewNoteHash(storageAddress: Fr, noteHash: Fr): void; + traceNullifierCheck(storageAddress: Fr, nullifier: Fr, leafIndex: Fr, exists: boolean, isPending: boolean): void; + traceNewNullifier(storageAddress: Fr, nullifier: Fr): void; + traceL1ToL2MessageCheck(contractAddress: Fr, msgHash: Fr, msgLeafIndex: Fr, exists: boolean): void; + // TODO(dbanks12): should new message accept contract address as arg? + traceNewL2ToL1Message(recipient: Fr, content: Fr): void; + traceUnencryptedLog(contractAddress: Fr, event: Fr, log: Fr[]): void; + // TODO(dbanks12): odd that getContractInstance is a one-off in that it accepts an entire object instead of components + traceGetContractInstance(instance: TracedContractInstance): void; + traceNestedCall( + /** The trace of the nested call. */ + nestedCallTrace: PublicSideEffectTraceInterface, + /** The execution environment of the nested call. */ + nestedEnvironment: AvmExecutionEnvironment, + /** How much gas was available for this public execution. */ + // TODO(dbanks12): consider moving to AvmExecutionEnvironment + startGasLeft: Gas, + /** How much gas was left after this public execution. */ + // TODO(dbanks12): consider moving to AvmContractCallResults + endGasLeft: Gas, + /** Bytecode used for this execution. */ + bytecode: Buffer, + /** The call's results */ + avmCallResults: AvmContractCallResults, + ): void; +} + +//export abstract class PublicSideEffectTrace { +// constructor(_parent: any) {} +// fork(): PublicSideEffectTraceInterface { +// throw new Error('Not implemented'); +// } +// getCounter(): number { +// throw new Error('Not implemented'); +// } +// tracePublicStorageRead(_storageAddress: Fr, _slot: Fr, _value: Fr, _exists: boolean, _cached: boolean): void { +// throw new Error('Not implemented'); +// } +// tracePublicStorageWrite(_storageAddress: Fr, _slot: Fr, _value: Fr): void { +// throw new Error('Not implemented'); +// } +// traceNoteHashCheck(_storageAddress: Fr, _noteHash: Fr, _leafIndex: Fr, _exists: boolean): void { +// throw new Error('Not implemented'); +// } +// traceNewNoteHash(_storageAddress: Fr, _noteHash: Fr): void { +// throw new Error('Not implemented'); +// } +// traceNullifierCheck(_storageAddress: Fr, _nullifier: Fr, _leafIndex: Fr, _exists: boolean, _isPending: boolean): void { +// throw new Error('Not implemented'); +// } +// traceNewNullifier(_storageAddress: Fr, _nullifier: Fr): void { +// throw new Error('Not implemented'); +// } +// traceL1ToL2MessageCheck(_storageAddress: Fr, _msgHash: Fr, _msgLeafIndex: Fr, _exists: boolean): void { +// throw new Error('Not implemented'); +// } +// traceNewL2ToL1Message(_recipient: Fr, _content: Fr): void { +// throw new Error('Not implemented'); +// } +// traceUnencryptedLog(_contractAddress: Fr, _event: Fr, _log: Fr[]): void { +// throw new Error('Not implemented'); +// } +// traceGetContractInstance(_instance: TracedContractInstance): void { +// throw new Error('Not implemented'); +// } +// traceNestedCall( +// /** The trace of the nested call. */ +// _nestedCallTrace: PublicSideEffectTraceInterface, +// /** The execution that triggered this result. */ +// _avmEnvironment: AvmExecutionEnvironment, +// /** How much gas was available for this public execution. */ +// // TODO(dbanks12): consider moving to AvmExecutionEnvironment +// _startGasLeft: Gas, +// /** How much gas was left after this public execution. */ +// // TODO(dbanks12): consider moving to AvmContractCallResults +// _endGasLeft: Gas, +// /** Bytecode used for this execution. */ +// _bytecode: Buffer, +// /** The call's results */ +// _avmCallResults: AvmContractCallResults, +// ): void { +// throw new Error('Not implemented'); +// } +//} diff --git a/yarn-project/simulator/src/public/transitional_adaptors.ts b/yarn-project/simulator/src/public/transitional_adaptors.ts index 5ecd77cd5d02..82e377c2a833 100644 --- a/yarn-project/simulator/src/public/transitional_adaptors.ts +++ b/yarn-project/simulator/src/public/transitional_adaptors.ts @@ -1,12 +1,6 @@ // All code in this file needs to die once the public executor is phased out in favor of the AVM. -import { UnencryptedFunctionL2Logs } from '@aztec/circuit-types'; import { - AvmContractInstanceHint, - AvmExecutionHints, - AvmExternalCallHint, - AvmKeyValueHint, CallContext, - Gas, type GasSettings, type GlobalVariables, type Header, @@ -16,14 +10,9 @@ import { Fr } from '@aztec/foundation/fields'; import { promisify } from 'util'; import { gunzip } from 'zlib'; -import { type AvmContext } from '../avm/avm_context.js'; import { AvmExecutionEnvironment } from '../avm/avm_execution_environment.js'; -import { type AvmContractCallResults } from '../avm/avm_message_call_result.js'; -import { type PartialPublicExecutionResult } from '../avm/journal/journal.js'; -import { type WorldStateAccessTrace } from '../avm/journal/trace.js'; import { Mov } from '../avm/opcodes/memory.js'; -import { createSimulationError } from '../common/errors.js'; -import { type PublicExecution, type PublicExecutionResult } from './execution.js'; +import { type PublicExecution } from './execution.js'; /** * Convert a PublicExecution(Environment) object to an AvmExecutionEnvironment @@ -57,88 +46,6 @@ export function createAvmExecutionEnvironment( ); } -export function createPublicExecution( - startSideEffectCounter: number, - avmEnvironment: AvmExecutionEnvironment, - calldata: Fr[], -): PublicExecution { - const callContext = CallContext.from({ - msgSender: avmEnvironment.sender, - storageContractAddress: avmEnvironment.storageAddress, - functionSelector: avmEnvironment.temporaryFunctionSelector, - isDelegateCall: avmEnvironment.isDelegateCall, - isStaticCall: avmEnvironment.isStaticCall, - sideEffectCounter: startSideEffectCounter, - }); - const execution: PublicExecution = { - contractAddress: avmEnvironment.address, - callContext, - args: calldata, - functionSelector: avmEnvironment.temporaryFunctionSelector, - }; - return execution; -} - -function computeHints(trace: WorldStateAccessTrace, executionResult: PartialPublicExecutionResult): AvmExecutionHints { - return new AvmExecutionHints( - trace.publicStorageReads.map(read => new AvmKeyValueHint(read.counter, read.value)), - trace.noteHashChecks.map(check => new AvmKeyValueHint(check.counter, new Fr(check.exists ? 1 : 0))), - trace.nullifierChecks.map(check => new AvmKeyValueHint(check.counter, new Fr(check.exists ? 1 : 0))), - trace.l1ToL2MessageChecks.map(check => new AvmKeyValueHint(check.counter, new Fr(check.exists ? 1 : 0))), - executionResult.nestedExecutions.map(nested => { - const gasUsed = new Gas( - nested.startGasLeft.daGas - nested.endGasLeft.daGas, - nested.startGasLeft.l2Gas - nested.endGasLeft.l2Gas, - ); - return new AvmExternalCallHint(/*success=*/ new Fr(nested.reverted ? 0 : 1), nested.returnValues, gasUsed); - }), - trace.gotContractInstances.map( - instance => - new AvmContractInstanceHint( - instance.address, - new Fr(instance.exists ? 1 : 0), - instance.salt, - instance.deployer, - instance.contractClassId, - instance.initializationHash, - instance.publicKeysHash, - ), - ), - ); -} - -export function convertAvmResultsToPxResult( - avmResult: AvmContractCallResults, - startSideEffectCounter: number, - fromPx: PublicExecution, - startGas: Gas, - endAvmContext: AvmContext, - bytecode: Buffer | undefined, -): PublicExecutionResult { - const endPersistableState = endAvmContext.persistableState; - const endMachineState = endAvmContext.machineState; - - return { - ...endPersistableState.transitionalExecutionResult, // includes nestedExecutions - execution: fromPx, - returnValues: avmResult.output, - startSideEffectCounter: new Fr(startSideEffectCounter), - endSideEffectCounter: new Fr(endPersistableState.trace.counter), - unencryptedLogs: new UnencryptedFunctionL2Logs(endPersistableState.transitionalExecutionResult.unencryptedLogs), - allUnencryptedLogs: new UnencryptedFunctionL2Logs( - endPersistableState.transitionalExecutionResult.allUnencryptedLogs, - ), - reverted: avmResult.reverted, - revertReason: avmResult.revertReason ? createSimulationError(avmResult.revertReason) : undefined, - startGasLeft: startGas, - endGasLeft: endMachineState.gasLeft, - transactionFee: endAvmContext.environment.transactionFee, - bytecode: bytecode, - calldata: endAvmContext.environment.calldata, - avmHints: computeHints(endPersistableState.trace, endPersistableState.transitionalExecutionResult), - }; -} - const AVM_MAGIC_SUFFIX = Buffer.from([ Mov.opcode, // opcode 0x00, // indirect