diff --git a/noir-projects/aztec-nr/aztec/src/oracle/enqueue_public_function_call.nr b/noir-projects/aztec-nr/aztec/src/oracle/enqueue_public_function_call.nr index 4bb26359cab0..d512a3bf0708 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/enqueue_public_function_call.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/enqueue_public_function_call.nr @@ -79,6 +79,7 @@ pub fn parse_public_call_stack_item_from_oracle(fields: [Field; ENQUEUE_PUBLIC_F // Note: Not using PublicCirclePublicInputs::deserialize here, because everything below args_hash is 0 and // there is no more data in fields because there is only ENQUEUE_PUBLIC_FUNCTION_CALL_RETURN_SIZE fields! + // WARNING: if updating, see comment in public_call_stack_item.ts's PublicCallStackItem.hash() let item = PublicCallStackItem { contract_address: AztecAddress::from_field(reader.read()), function_data: FunctionData { selector: FunctionSelector::from_field(reader.read()), is_private: false }, 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/noir-projects/noir-protocol-circuits/crates/types/src/abis/public_call_stack_item.nr b/noir-projects/noir-protocol-circuits/crates/types/src/abis/public_call_stack_item.nr index 5c98a5854419..9572f179dd1b 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/abis/public_call_stack_item.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/abis/public_call_stack_item.nr @@ -30,6 +30,7 @@ impl Hash for PublicCallStackItem { impl PublicCallStackItem { fn as_execution_request(self) -> Self { + // WARNING: if updating, see comment in public_call_stack_item.ts's `PublicCallStackItem.hash()` let public_inputs = self.public_inputs; let mut request_public_inputs = PublicCircuitPublicInputs::empty(); request_public_inputs.call_context = public_inputs.call_context; diff --git a/yarn-project/bb-prover/src/avm_proving.test.ts b/yarn-project/bb-prover/src/avm_proving.test.ts index 917850189ffc..53987df2e8cb 100644 --- a/yarn-project/bb-prover/src/avm_proving.test.ts +++ b/yarn-project/bb-prover/src/avm_proving.test.ts @@ -35,6 +35,7 @@ import { initContext, initExecutionEnvironment, initHostStorage, + initPersistableStateManager, } from '@aztec/simulator/avm/fixtures'; import { jest } from '@jest/globals'; @@ -43,11 +44,7 @@ import fs from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'path'; -import { AvmPersistableStateManager } from '../../simulator/src/avm/journal/journal.js'; -import { - convertAvmResultsToPxResult, - createPublicExecution, -} from '../../simulator/src/public/transitional_adaptors.js'; +import { PublicSideEffectTrace } from '../../simulator/src/public/side_effect_trace.js'; import { SerializableContractInstance } from '../../types/src/contracts/contract_instance.js'; import { type BBSuccess, BB_RESULT, generateAvmProof, verifyAvmProof } from './bb/execute.js'; import { extractVkData } from './verification_key/verification_key_data.js'; @@ -224,15 +221,13 @@ const proveAndVerifyAvmTestContract = async ( storageDb.storageRead.mockResolvedValue(Promise.resolve(storageValue)); const hostStorage = initHostStorage({ contractsDb }); - const persistableState = new AvmPersistableStateManager(hostStorage); + const trace = new PublicSideEffectTrace(startSideEffectCounter); + const persistableState = initPersistableStateManager({ hostStorage, trace }); const context = initContext({ env: environment, persistableState }); const nestedCallBytecode = getAvmTestContractBytecode('add_args_return'); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValue(Promise.resolve(nestedCallBytecode)); + jest.spyOn(hostStorage.contractsDb, 'getBytecode').mockResolvedValue(nestedCallBytecode); const startGas = new Gas(context.machineState.gasLeft.daGas, context.machineState.gasLeft.l2Gas); - const oldPublicExecution = createPublicExecution(startSideEffectCounter, environment, calldata); const internalLogger = createDebugLogger('aztec:avm-proving-test'); const logger = (msg: string, _data?: any) => internalLogger.verbose(msg); @@ -255,25 +250,21 @@ const proveAndVerifyAvmTestContract = async ( expect(avmResult.revertReason?.message).toContain(assertionErrString); } - const pxResult = convertAvmResultsToPxResult( - avmResult, - startSideEffectCounter, - oldPublicExecution, + const pxResult = trace.toPublicExecutionResult( + environment, startGas, - context, - simulator.getBytecode(), + /*endGasLeft=*/ Gas.from(context.machineState.gasLeft), + /*bytecode=*/ simulator.getBytecode()!, + avmResult, functionName, ); - // TODO(dbanks12): public inputs should not be empty.... Need to construct them from AvmContext? - const uncompressedBytecode = simulator.getBytecode()!; - const publicInputs = getPublicInputs(pxResult); const avmCircuitInputs = new AvmCircuitInputs( functionName, - uncompressedBytecode, - context.environment.calldata, - publicInputs, - pxResult.avmHints, + /*bytecode=*/ simulator.getBytecode()!, // uncompressed bytecode + /*calldata=*/ context.environment.calldata, + /*publicInputs=*/ getPublicInputs(pxResult), + /*avmHints=*/ pxResult.avmCircuitHints, ); // Then we prove. diff --git a/yarn-project/circuit-types/src/mocks.ts b/yarn-project/circuit-types/src/mocks.ts index 2ffb92850c31..a3a86bb6235b 100644 --- a/yarn-project/circuit-types/src/mocks.ts +++ b/yarn-project/circuit-types/src/mocks.ts @@ -203,8 +203,10 @@ export const randomContractArtifact = (): ContractArtifact => ({ notes: {}, }); -export const randomContractInstanceWithAddress = (opts: { contractClassId?: Fr } = {}): ContractInstanceWithAddress => - SerializableContractInstance.random(opts).withAddress(AztecAddress.random()); +export const randomContractInstanceWithAddress = ( + opts: { contractClassId?: Fr } = {}, + address: AztecAddress = AztecAddress.random(), +): ContractInstanceWithAddress => SerializableContractInstance.random(opts).withAddress(address); export const randomDeployedContract = () => { const artifact = randomContractArtifact(); diff --git a/yarn-project/circuits.js/src/structs/avm/avm.ts b/yarn-project/circuits.js/src/structs/avm/avm.ts index f33335f800cb..907e41ad4f2a 100644 --- a/yarn-project/circuits.js/src/structs/avm/avm.ts +++ b/yarn-project/circuits.js/src/structs/avm/avm.ts @@ -243,6 +243,7 @@ export class AvmContractInstanceHint { } } +// TODO(dbanks12): rename AvmCircuitHints export class AvmExecutionHints { public readonly storageValues: Vector; public readonly noteHashExists: Vector; @@ -267,6 +268,14 @@ export class AvmExecutionHints { this.contractInstances = new Vector(contractInstances); } + /** + * Return an empty instance. + * @returns an empty instance. + */ + empty() { + return new AvmExecutionHints([], [], [], [], [], []); + } + /** * Serializes the inputs to a buffer. * @returns - The inputs serialized to a buffer. diff --git a/yarn-project/circuits.js/src/structs/contract_storage_read.ts b/yarn-project/circuits.js/src/structs/contract_storage_read.ts index 5a679a75bf7e..56f0f95aa1dd 100644 --- a/yarn-project/circuits.js/src/structs/contract_storage_read.ts +++ b/yarn-project/circuits.js/src/structs/contract_storage_read.ts @@ -23,30 +23,19 @@ export class ContractStorageRead { /** * 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. ) {} - static from(args: { - /** - * Storage slot we are reading from. - */ - storageSlot: Fr; - /** - * Value read from the storage slot. - */ - currentValue: Fr; - /** - * Side effect counter tracking position of this event in tx execution. - */ - sideEffectCounter: number; - contractAddress?: AztecAddress; - }) { - return new ContractStorageRead(args.storageSlot, args.currentValue, args.sideEffectCounter, args.contractAddress); + static from(args: { storageSlot: Fr; currentValue: Fr; counter: number; contractAddress?: AztecAddress }) { + return new ContractStorageRead(args.storageSlot, args.currentValue, args.counter, args.contractAddress); } toBuffer() { - return serializeToBuffer(this.storageSlot, this.currentValue, new Fr(this.sideEffectCounter)); + return serializeToBuffer(this.storageSlot, this.currentValue, new Fr(this.counter)); } static fromBuffer(buffer: Buffer | BufferReader) { @@ -59,7 +48,7 @@ export class ContractStorageRead { } isEmpty() { - return this.storageSlot.isZero() && this.currentValue.isZero() && this.sideEffectCounter == 0; + return this.storageSlot.isZero() && this.currentValue.isZero() && this.counter == 0; } toFriendlyJSON() { @@ -67,7 +56,7 @@ export class ContractStorageRead { } toFields(): Fr[] { - const fields = [this.storageSlot, this.currentValue, new Fr(this.sideEffectCounter)]; + const fields = [this.storageSlot, this.currentValue, new Fr(this.counter)]; if (fields.length !== CONTRACT_STORAGE_READ_LENGTH) { throw new Error( `Invalid number of fields for ContractStorageRead. Expected ${CONTRACT_STORAGE_READ_LENGTH}, got ${fields.length}`, @@ -81,8 +70,8 @@ export class ContractStorageRead { const storageSlot = reader.readField(); const currentValue = reader.readField(); - const sideEffectCounter = reader.readField().toNumber(); + const counter = reader.readField().toNumber(); - return new ContractStorageRead(storageSlot, currentValue, sideEffectCounter); + return new ContractStorageRead(storageSlot, currentValue, counter); } } 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..4d7d3d665c07 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 @@ -22,14 +22,17 @@ export class ContractStorageUpdateRequest { */ public readonly newValue: Fr, /** - * Optional side effect counter tracking position of this event in tx execution. + * Side effect counter tracking position of this event in tx execution. + */ + public readonly counter: number, + /** + * Contract address whose storage is being read. */ - public readonly sideEffectCounter: number, 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/circuits.js/src/structs/public_call_stack_item.ts b/yarn-project/circuits.js/src/structs/public_call_stack_item.ts index 3223909d2878..170f3dc84b65 100644 --- a/yarn-project/circuits.js/src/structs/public_call_stack_item.ts +++ b/yarn-project/circuits.js/src/structs/public_call_stack_item.ts @@ -90,15 +90,24 @@ export class PublicCallStackItem { * @returns Hash. */ public hash() { + let publicInputsToHash = this.publicInputs; if (this.isExecutionRequest) { + // An execution request (such as an enqueued call from private) is hashed with + // only the publicInput members present in a PublicCallRequest. + // This allows us to check that the request (which is created/hashed before + // side-effects and output info are unknown for public calls) matches the call + // being processed by a kernel iteration. + // WARNING: This subset of publicInputs that is set here must align with + // `parse_public_call_stack_item_from_oracle` in enqueue_public_function_call.nr + // and `PublicCallStackItem::as_execution_request()` in public_call_stack_item.ts const { callContext, argsHash } = this.publicInputs; - this.publicInputs = PublicCircuitPublicInputs.empty(); - this.publicInputs.callContext = callContext; - this.publicInputs.argsHash = argsHash; + publicInputsToHash = PublicCircuitPublicInputs.empty(); + publicInputsToHash.callContext = callContext; + publicInputsToHash.argsHash = argsHash; } return pedersenHash( - [this.contractAddress, this.functionData.hash(), this.publicInputs.hash()], + [this.contractAddress, this.functionData.hash(), publicInputsToHash.hash()], GeneratorIndex.CALL_STACK_ITEM, ); } diff --git a/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts b/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts index cf6cd9fdcb16..ddbcceaca4f7 100644 --- a/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts +++ b/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts @@ -38,7 +38,7 @@ describe('e2e_avm_simulator', () => { }); it('PXE processes failed assertions and fills in the error message with the expression (even complex ones)', async () => { await expect(avmContract.methods.assert_nullifier_exists(123).simulate()).rejects.toThrow( - "Assertion failed: Nullifier doesn't exist! 'context.nullifier_exists(nullifier, context.this_address())'", + "Assertion failed: Nullifier doesn't exist! 'context.nullifier_exists(nullifier, context.storage_address())'", ); }); }); diff --git a/yarn-project/noir-protocol-circuits-types/src/index.ts b/yarn-project/noir-protocol-circuits-types/src/index.ts index 0685ef195c3c..b6ba9bd4097e 100644 --- a/yarn-project/noir-protocol-circuits-types/src/index.ts +++ b/yarn-project/noir-protocol-circuits-types/src/index.ts @@ -572,7 +572,7 @@ export function convertSimulatedPublicSetupInputsToWitnessMap(inputs: PublicKern } /** - * Converts the inputs of the public setup circuit into a witness map + * Converts the inputs of the public app logic circuit into a witness map * @param inputs - The public kernel inputs. * @returns The witness map */ diff --git a/yarn-project/noir-protocol-circuits-types/src/type_conversion.ts b/yarn-project/noir-protocol-circuits-types/src/type_conversion.ts index bc8db0da20f0..c276fc8a16d3 100644 --- a/yarn-project/noir-protocol-circuits-types/src/type_conversion.ts +++ b/yarn-project/noir-protocol-circuits-types/src/type_conversion.ts @@ -1826,7 +1826,7 @@ export function mapStorageUpdateRequestToNoir( return { storage_slot: mapFieldToNoir(storageUpdateRequest.storageSlot), new_value: mapFieldToNoir(storageUpdateRequest.newValue), - counter: mapNumberToNoir(storageUpdateRequest.sideEffectCounter), + counter: mapNumberToNoir(storageUpdateRequest.counter), }; } /** @@ -1855,7 +1855,7 @@ export function mapStorageReadToNoir(storageRead: ContractStorageRead): StorageR return { storage_slot: mapFieldToNoir(storageRead.storageSlot), current_value: mapFieldToNoir(storageRead.currentValue), - counter: mapNumberToNoir(storageRead.sideEffectCounter), + counter: mapNumberToNoir(storageRead.counter), }; } /** diff --git a/yarn-project/simulator/src/avm/avm_context.test.ts b/yarn-project/simulator/src/avm/avm_context.test.ts index bea44afec382..a96d88983061 100644 --- a/yarn-project/simulator/src/avm/avm_context.test.ts +++ b/yarn-project/simulator/src/avm/avm_context.test.ts @@ -16,6 +16,7 @@ describe('Avm Context', () => { allSameExcept(context.environment, { address: newAddress, storageAddress: newAddress, + contractCallDepth: Fr.ONE, // Calldata also includes AvmContextInputs calldata: anyAvmContextInputs().concat(newCalldata), isStaticCall: false, @@ -46,6 +47,7 @@ describe('Avm Context', () => { allSameExcept(context.environment, { address: newAddress, storageAddress: newAddress, + contractCallDepth: Fr.ONE, // Calldata also includes AvmContextInputs calldata: anyAvmContextInputs().concat(newCalldata), isStaticCall: true, diff --git a/yarn-project/simulator/src/avm/avm_execution_environment.test.ts b/yarn-project/simulator/src/avm/avm_execution_environment.test.ts index 68bde3962fb5..e13f3f248d71 100644 --- a/yarn-project/simulator/src/avm/avm_execution_environment.test.ts +++ b/yarn-project/simulator/src/avm/avm_execution_environment.test.ts @@ -16,6 +16,7 @@ describe('Execution Environment', () => { allSameExcept(executionEnvironment, { address: newAddress, storageAddress: newAddress, + contractCallDepth: Fr.ONE, // Calldata also includes AvmContextInputs calldata: anyAvmContextInputs().concat(calldata), }), @@ -30,6 +31,7 @@ describe('Execution Environment', () => { expect(newExecutionEnvironment).toEqual( allSameExcept(executionEnvironment, { address: newAddress, + contractCallDepth: Fr.ONE, isDelegateCall: true, // Calldata also includes AvmContextInputs calldata: anyAvmContextInputs().concat(calldata), @@ -49,6 +51,7 @@ describe('Execution Environment', () => { allSameExcept(executionEnvironment, { address: newAddress, storageAddress: newAddress, + contractCallDepth: Fr.ONE, isStaticCall: true, // Calldata also includes AvmContextInputs calldata: anyAvmContextInputs().concat(calldata), diff --git a/yarn-project/simulator/src/avm/avm_execution_environment.ts b/yarn-project/simulator/src/avm/avm_execution_environment.ts index 411b9d60ff49..c4794b1a02b7 100644 --- a/yarn-project/simulator/src/avm/avm_execution_environment.ts +++ b/yarn-project/simulator/src/avm/avm_execution_environment.ts @@ -19,6 +19,7 @@ export class AvmContextInputs { */ // TODO(https://github.com/AztecProtocol/aztec-packages/issues/3992): gas not implemented export class AvmExecutionEnvironment { + private readonly calldataPrefixLength; constructor( public readonly address: AztecAddress, public readonly storageAddress: AztecAddress, @@ -45,8 +46,9 @@ export class AvmExecutionEnvironment { temporaryFunctionSelector.toField(), computeVarArgsHash(calldata), isStaticCall, - ); - this.calldata = [...inputs.toFields(), ...calldata]; + ).toFields(); + this.calldata = [...inputs, ...calldata]; + this.calldataPrefixLength = inputs.length; } private deriveEnvironmentForNestedCallInternal( @@ -62,7 +64,7 @@ export class AvmExecutionEnvironment { /*sender=*/ this.address, this.feePerL2Gas, this.feePerDaGas, - this.contractCallDepth, + this.contractCallDepth.add(Fr.ONE), this.header, this.globals, isStaticCall, @@ -109,4 +111,9 @@ export class AvmExecutionEnvironment { ): AvmExecutionEnvironment { throw new Error('Delegate calls not supported!'); } + + public getCalldataWithoutPrefix(): Fr[] { + // clip off the first few entries + return this.calldata.slice(this.calldataPrefixLength); + } } diff --git a/yarn-project/simulator/src/avm/avm_simulator.test.ts b/yarn-project/simulator/src/avm/avm_simulator.test.ts index 1614305b0bea..e28d9d9b8e7a 100644 --- a/yarn-project/simulator/src/avm/avm_simulator.test.ts +++ b/yarn-project/simulator/src/avm/avm_simulator.test.ts @@ -1,15 +1,16 @@ -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 { mock } from 'jest-mock-extended'; +import { type PublicSideEffectTraceInterface } from '../public/side_effect_trace_interface.js'; import { isAvmBytecode, markBytecodeAsAvm } from '../public/transitional_adaptors.js'; +import { type AvmExecutionEnvironment } from './avm_execution_environment.js'; import { AvmMachineState } from './avm_machine_state.js'; import { type MemoryValue, TypeTag, type Uint8 } from './avm_memory_types.js'; import { AvmSimulator } from './avm_simulator.js'; @@ -19,12 +20,26 @@ import { initContext, initExecutionEnvironment, initGlobalVariables, + initHostStorage, initMachineState, + initPersistableStateManager, randomMemoryBytes, randomMemoryFields, } from './fixtures/index.js'; +import { type HostStorage } from './journal/host_storage.js'; +import { type AvmPersistableStateManager } from './journal/journal.js'; import { Add, CalldataCopy, Return } from './opcodes/index.js'; import { encodeToBytecode } from './serialization/bytecode_serialization.js'; +import { + mockGetBytecode, + mockGetContractInstance, + mockL1ToL2MessageExists, + mockNoteHashExists, + mockNullifierExists, + mockStorageRead, + mockStorageReadWithMap, + mockTraceFork, +} from './test_utils.js'; describe('AVM simulator: injected bytecode', () => { let calldata: Fr[]; @@ -314,634 +329,565 @@ 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)]); + it('conversions', async () => { + const calldata: Fr[] = [new Fr(0b1011101010100)]; + const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - // Note hash existence check should be in trace - const trace = context.persistableState.flush(); - expect(trace.noteHashChecks).toEqual([expect.objectContaining({ noteHash, leafIndex, exists: false })]); - }); + const bytecode = getAvmTestContractBytecode('to_radix_le'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); - it(`Note hash exists (it does)`, async () => { - const noteHash = new Fr(42); - const leafIndex = new Fr(7); - const calldata = [noteHash, leafIndex]; + 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())); - 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(resultBuffer.equals(expectedResults)).toBe(true); + }); - expect(results.reverted).toBe(false); - expect(results.output).toEqual([/*exists=true*/ new Fr(1)]); + 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); + + 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 }), + }); + }; - // Note hash existence check should be in trace - const trace = context.persistableState.flush(); - expect(trace.noteHashChecks).toEqual([expect.objectContaining({ noteHash, leafIndex, exists: true })]); + // Will check existence at leafIndex, but nothing may be found there and/or something may be found at mockAtLeafIndex + describe.each([ + [/*mockAtLeafIndex=*/ undefined], // doesn't exist at all + [/*mockAtLeafIndex=*/ leafIndex], // should be found! + [/*mockAtLeafIndex=*/ leafIndex.add(Fr.ONE)], // won't be found! (checking leafIndex+1, but it exists at leafIndex) + ])('Note hash checks', (mockAtLeafIndex?: Fr) => { + const expectFound = mockAtLeafIndex !== undefined && mockAtLeafIndex.equals(leafIndex); + const existsElsewhere = mockAtLeafIndex !== undefined && !mockAtLeafIndex.equals(leafIndex); + const existsStr = expectFound ? 'DOES exist' : 'does NOT exist'; + const foundAtStr = existsElsewhere + ? `at leafIndex=${mockAtLeafIndex.toNumber()} (exists at leafIndex=${leafIndex.toNumber()})` + : ''; + it(`Should return ${expectFound} (and be traced) when noteHash ${existsStr} ${foundAtStr}`, async () => { + const calldata = [value0, leafIndex]; + const context = createContext(calldata); + const bytecode = getAvmTestContractBytecode('note_hash_exists'); + if (mockAtLeafIndex !== undefined) { + mockNoteHashExists(hostStorage, mockAtLeafIndex, value0); + } + + const results = await new AvmSimulator(context).executeBytecode(bytecode); + expect(results.reverted).toBe(false); + expect(results.output).toEqual([expectFound ? Fr.ONE : Fr.ZERO]); + + expect(trace.traceNoteHashCheck).toHaveBeenCalledTimes(1); + expect(trace.traceNoteHashCheck).toHaveBeenCalledWith( + storageAddress, + /*noteHash=*/ value0, + leafIndex, + /*exists=*/ expectFound, + ); + }); }); - 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), - ]); + describe.each([[/*exists=*/ false], [/*exists=*/ true]])('Nullifier checks', (exists: boolean) => { + const existsStr = exists ? 'DOES exist' : 'does NOT exist'; + it(`Should return ${exists} (and be traced) when noteHash ${existsStr}`, async () => { + const calldata = [value0]; + const context = createContext(calldata); + const bytecode = getAvmTestContractBytecode('nullifier_exists'); + + if (exists) { + mockNullifierExists(hostStorage, leafIndex, value0); + } + + const results = await new AvmSimulator(context).executeBytecode(bytecode); + expect(results.reverted).toBe(false); + expect(results.output).toEqual([exists ? Fr.ONE : Fr.ZERO]); + + expect(trace.traceNullifierCheck).toHaveBeenCalledTimes(1); + const isPending = false; + // leafIndex is returned from DB call for nullifiers, so it is absent on DB miss + const tracedLeafIndex = exists && !isPending ? leafIndex : Fr.ZERO; + expect(trace.traceNullifierCheck).toHaveBeenCalledWith( + storageAddress, + value0, + tracedLeafIndex, + exists, + isPending, + ); + }); }); - 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, - }), - ]); + // Will check existence at leafIndex, but nothing may be found there and/or something may be found at mockAtLeafIndex + describe.each([ + [/*mockAtLeafIndex=*/ undefined], // doesn't exist at all + [/*mockAtLeafIndex=*/ leafIndex], // should be found! + [/*mockAtLeafIndex=*/ leafIndex.add(Fr.ONE)], // won't be found! (checking leafIndex+1, but it exists at leafIndex) + ])('L1ToL2 message checks', (mockAtLeafIndex?: Fr) => { + const expectFound = mockAtLeafIndex !== undefined && mockAtLeafIndex.equals(leafIndex); + const existsElsewhere = mockAtLeafIndex !== undefined && !mockAtLeafIndex.equals(leafIndex); + const existsStr = expectFound ? 'DOES exist' : 'does NOT exist'; + const foundAtStr = existsElsewhere + ? `at leafIndex=${mockAtLeafIndex.toNumber()} (exists at leafIndex=${leafIndex.toNumber()})` + : ''; + + it(`Should return ${expectFound} (and be traced) when noteHash ${existsStr} ${foundAtStr}`, async () => { + const calldata = [value0, leafIndex]; + const context = createContext(calldata); + const bytecode = getAvmTestContractBytecode('l1_to_l2_msg_exists'); + if (mockAtLeafIndex !== undefined) { + mockL1ToL2MessageExists(hostStorage, mockAtLeafIndex, value0, /*valueAtOtherIndices=*/ value1); + } + + const results = await new AvmSimulator(context).executeBytecode(bytecode); + expect(results.reverted).toBe(false); + expect(results.output).toEqual([expectFound ? Fr.ONE : Fr.ZERO]); + + expect(trace.traceL1ToL2MessageCheck).toHaveBeenCalledTimes(1); + expect(trace.traceL1ToL2MessageCheck).toHaveBeenCalledWith( + address, + /*noteHash=*/ value0, + leafIndex, + /*exists=*/ expectFound, + ); + }); }); - it(`Emit nullifier (should be traced)`, async () => { - const utxo = new Fr(42); - const calldata = [utxo]; + it('Should append a new note hash correctly', async () => { + const calldata = [value0]; + const context = createContext(calldata); + const bytecode = getAvmTestContractBytecode('new_note_hash'); - 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(results.output).toEqual([]); - expect(context.persistableState.flush().newNullifiers).toEqual([ - expect.objectContaining({ - storageAddress: context.environment.storageAddress, - nullifier: utxo, - }), - ]); + expect(trace.traceNewNoteHash).toHaveBeenCalledTimes(1); + expect(trace.traceNewNoteHash).toHaveBeenCalledWith( + expect.objectContaining(storageAddress), + /*nullifier=*/ value0, + ); }); - it(`Nullifier exists (it does not)`, async () => { - const utxo = new Fr(42); - const calldata = [utxo]; + it('Should append a new nullifier correctly', async () => { + const calldata = [value0]; + const context = createContext(calldata); + const bytecode = getAvmTestContractBytecode('new_nullifier'); - 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), - }), - ]); - }); + expect(results.output).toEqual([]); - 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), - }), - ]); + expect(trace.traceNewNullifier).toHaveBeenCalledTimes(1); + expect(trace.traceNewNullifier).toHaveBeenCalledWith( + expect.objectContaining(storageAddress), + /*nullifier=*/ value0, + ); }); - 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), - }), - ]); + 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 + expect(trace.traceNewNullifier).toHaveBeenCalledTimes(1); + expect(trace.traceNewNullifier).toHaveBeenCalledWith( + expect.objectContaining(storageAddress), + /*nullifier=*/ value0, + ); + expect(trace.traceNullifierCheck).toHaveBeenCalledTimes(1); + // leafIndex is returned from DB call for nullifiers, so it is absent on DB miss + expect(trace.traceNullifierCheck).toHaveBeenCalledWith( + storageAddress, + value0, + /*leafIndex=*/ Fr.ZERO, + /*exists=*/ true, + /*isPending=*/ true, + ); + }); + 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 + expect(trace.traceNewNullifier).toHaveBeenCalledTimes(1); + expect(trace.traceNewNullifier).toHaveBeenCalledWith( + expect.objectContaining(storageAddress), + /*nullifier=*/ value0, + ); + }); }); - it(`Emits same nullifier twice (should fail)`, async () => { - const utxo = new Fr(42); - const calldata = [utxo]; + describe('Unencrypted Logs', () => { + it(`Emit unencrypted logs (should be traced)`, async () => { + const context = createContext(); + const bytecode = getAvmTestContractBytecode('emit_unencrypted_log'); - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - const bytecode = getAvmTestContractBytecode('nullifier_collision'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); + const results = await new AvmSimulator(context).executeBytecode(bytecode); + expect(results.reverted).toBe(false); - 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, - }), - ]); - }); - }); + 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))); - 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); + expect(trace.traceUnencryptedLog).toHaveBeenCalledTimes(3); + expect(trace.traceUnencryptedLog).toHaveBeenCalledWith(address, eventSelector, expectedFields); + expect(trace.traceUnencryptedLog).toHaveBeenCalledWith(address, eventSelector, expectedString); + expect(trace.traceUnencryptedLog).toHaveBeenCalledWith(address, eventSelector, expectedCompressedString); + }); }); - it(`Message exists (it does)`, async () => { - const msgHash = new Fr(42); - const leafIndex = new Fr(24); - const calldata = [msgHash, leafIndex]; + describe('Public storage accesses', () => { + it('Should set value in storage (single)', async () => { + const calldata = [value0]; - 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); + const context = createContext(calldata); + const bytecode = getAvmTestContractBytecode('set_storage_single'); - 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); - }); - }); + const results = await new AvmSimulator(context).executeBytecode(bytecode); + expect(results.reverted).toBe(false); - 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]; + expect(await context.persistableState.peekStorage(storageAddress, slot)).toEqual(value0); - const context = initContext({ - env: initExecutionEnvironment({ calldata, address, storageAddress: address }), + expect(trace.tracePublicStorageWrite).toHaveBeenCalledTimes(1); + expect(trace.tracePublicStorageWrite).toHaveBeenCalledWith(storageAddress, slot, value0); }); - const bytecode = getAvmTestContractBytecode('set_storage_single'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - expect(results.reverted).toBe(false); + it('Should read value in storage (single)', async () => { + const context = createContext(); + mockStorageRead(hostStorage, value0); - // 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, - }), - ]); - }); + const bytecode = getAvmTestContractBytecode('read_storage_single'); - 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 results = await new AvmSimulator(context).executeBytecode(bytecode); + expect(results.reverted).toBe(false); + expect(results.output).toEqual([value0]); - const context = initContext({ - env: initExecutionEnvironment({ storageAddress: address }), + expect(trace.tracePublicStorageRead).toHaveBeenCalledTimes(1); + expect(trace.tracePublicStorageRead).toHaveBeenCalledWith( + storageAddress, + slot, + value0, + /*exists=*/ true, + /*cached=*/ false, + ); }); - 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 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]); + + expect(trace.tracePublicStorageWrite).toHaveBeenCalledTimes(1); + expect(trace.tracePublicStorageWrite).toHaveBeenCalledWith(storageAddress, slot, value0); + expect(trace.tracePublicStorageRead).toHaveBeenCalledTimes(1); + expect(trace.tracePublicStorageRead).toHaveBeenCalledWith( + storageAddress, + slot, + value0, + /*exists=*/ true, + /*cached=*/ 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]; + it('Should set a value in storage (list)', async () => { + const calldata = [value0, value1]; - const context = initContext({ - env: initExecutionEnvironment({ calldata, address, storageAddress: address }), - }); - const bytecode = getAvmTestContractBytecode('set_read_storage_single'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); + const context = createContext(calldata); + const bytecode = getAvmTestContractBytecode('set_storage_list'); - 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, - }), - ]); - }); + const results = await new AvmSimulator(context).executeBytecode(bytecode); + expect(results.reverted).toBe(false); - 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)]; + expect(await context.persistableState.peekStorage(address, listSlot0)).toEqual(calldata[0]); + expect(await context.persistableState.peekStorage(address, listSlot1)).toEqual(calldata[1]); - const context = initContext({ - env: initExecutionEnvironment({ sender, address, calldata, storageAddress: address }), + expect(trace.tracePublicStorageWrite).toHaveBeenCalledTimes(2); + expect(trace.tracePublicStorageWrite).toHaveBeenCalledWith(storageAddress, listSlot0, value0); + expect(trace.tracePublicStorageWrite).toHaveBeenCalledWith(storageAddress, listSlot1, value1); }); - const bytecode = getAvmTestContractBytecode('set_storage_list'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - expect(results.reverted).toBe(false); + it('Should read a value in storage (list)', async () => { + const context = createContext(); + const mockedStorage = new Map([ + [listSlot0.toBigInt(), value0], + [listSlot1.toBigInt(), value1], + ]); + mockStorageReadWithMap(hostStorage, mockedStorage); + + 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]); + + expect(trace.tracePublicStorageRead).toHaveBeenCalledWith( + storageAddress, + listSlot0, + value0, + /*exists=*/ true, + /*cached=*/ false, + ); + expect(trace.tracePublicStorageRead).toHaveBeenCalledWith( + storageAddress, + listSlot1, + value1, + /*exists=*/ true, + /*cached=*/ 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 set a value in storage (map)', async () => { + const calldata = [storageAddress, value0]; - 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 = createContext(calldata); + const bytecode = getAvmTestContractBytecode('set_storage_map'); - const context = initContext({ - env: initExecutionEnvironment({ address, storageAddress: address }), - }); - 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); + const results = await new AvmSimulator(context).executeBytecode(bytecode); + expect(results.reverted).toBe(false); - 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, - }), - ]); - }); + // returns the storage slot for modified key + const mapSlotNumber = results.output[0].toBigInt(); + const mapSlot = new Fr(mapSlotNumber); - 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]; + expect(await context.persistableState.peekStorage(storageAddress, mapSlot)).toEqual(value0); - const context = initContext({ - env: initExecutionEnvironment({ address, calldata, storageAddress: address }), + expect(trace.tracePublicStorageWrite).toHaveBeenCalledTimes(1); + expect(trace.tracePublicStorageWrite).toHaveBeenCalledWith(storageAddress, mapSlot, value0); }); - 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 calldata = [storageAddress, value0]; - 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 = createContext(calldata); + const bytecode = getAvmTestContractBytecode('add_storage_map'); - const context = initContext({ - env: initExecutionEnvironment({ address, calldata, storageAddress: address }), - }); - const bytecode = getAvmTestContractBytecode('add_storage_map'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); + const results = await new AvmSimulator(context).executeBytecode(bytecode); + expect(results.reverted).toBe(false); - 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, - }), - ]); - }); + // returns the storage slot for modified key + const mapSlotNumber = results.output[0].toBigInt(); + const mapSlot = new Fr(mapSlotNumber); - it('Should read value in storage (map)', async () => { - const value = new Fr(12345); - const address = AztecAddress.fromField(new Fr(420)); - const calldata = [address.toField()]; + expect(await context.persistableState.peekStorage(storageAddress, mapSlot)).toEqual(value0); - const context = initContext({ - env: initExecutionEnvironment({ calldata, address, storageAddress: address }), + expect(trace.tracePublicStorageRead).toHaveBeenCalledTimes(1); + expect(trace.tracePublicStorageRead).toHaveBeenCalledWith( + storageAddress, + mapSlot, + Fr.ZERO, + /*exists=*/ false, + /*cached=*/ false, + ); + expect(trace.tracePublicStorageWrite).toHaveBeenCalledTimes(1); + expect(trace.tracePublicStorageWrite).toHaveBeenCalledWith(storageAddress, mapSlot, value0); }); - 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, - }), - ]); + it('Should read value in storage (map)', async () => { + const calldata = [storageAddress]; + + const context = createContext(calldata); + mockStorageRead(hostStorage, 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]); + + expect(trace.tracePublicStorageRead).toHaveBeenCalledTimes(1); + // slot is the result of a pedersen hash and is therefore not known in the test + expect(trace.tracePublicStorageRead).toHaveBeenCalledWith( + storageAddress, + expect.anything(), + value0, + /*exists=*/ true, + /*cached=*/ false, + ); + }); }); - }); - - 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), - }; - 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('Contract Instance Retrieval', () => { + it(`Can getContractInstance`, async () => { + const context = createContext(); + // Contract instance must match noir + 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), + }; + mockGetContractInstance(hostStorage, contractInstance); + + const bytecode = getAvmTestContractBytecode('test_get_contract_instance_raw'); + + const results = await new AvmSimulator(context).executeBytecode(bytecode); + expect(results.reverted).toBe(false); + + expect(trace.traceGetContractInstance).toHaveBeenCalledTimes(1); + expect(trace.traceGetContractInstance).toHaveBeenCalledWith({ exists: true, ...contractInstance }); + }); }); - }); - - 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); + describe('Nested external calls', () => { + const expectTracedNestedCall = ( + environment: AvmExecutionEnvironment, + nestedTrace: PublicSideEffectTraceInterface, + isStaticCall: boolean = false, + ) => { + expect(trace.traceNestedCall).toHaveBeenCalledTimes(1); + expect(trace.traceNestedCall).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 + /*functionName=*/ expect.anything(), + ); + }; - // 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 = [value0, value1]; + const context = createContext(calldata); + const callBytecode = getAvmTestContractBytecode('nested_call_to_add'); + const addBytecode = getAvmTestContractBytecode('add_args_return'); + mockGetBytecode(hostStorage, addBytecode); + const nestedTrace = mock(); + mockTraceFork(trace, nestedTrace); - 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([value0.add(value1)]); - const results = await new AvmSimulator(context).executeBytecode(callBytecode); + expectTracedNestedCall(context.environment, nestedTrace); + }); - expect(results.reverted).toBe(false); - expect(results.output).toEqual([new Fr(3)]); - }); + 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'); + mockGetBytecode(hostStorage, addBytecode); + const nestedTrace = mock(); + mockTraceFork(trace, nestedTrace); - 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([value0.add(value1)]); - const results = await new AvmSimulator(context).executeBytecode(callBytecode); + expectTracedNestedCall(context.environment, nestedTrace, /*isStaticCall=*/ true); + }); - expect(results.reverted).toBe(false); - expect(results.output).toEqual([/*result=*/ new Fr(3)]); - }); + 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'); + mockGetBytecode(hostStorage, addBytecode); + mockTraceFork(trace); + + 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 + expect(trace.traceNestedCall).toHaveBeenCalledTimes(0); + }); - 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)); + 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'); + mockGetBytecode(hostStorage, nestedBytecode); + mockTraceFork(trace); - const results = await new AvmSimulator(context).executeBytecode(callBytecode); + 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', - ); - }); + 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)); + // 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); - const results = await new AvmSimulator(context).executeBytecode(callBytecode); + // Nested call should NOT have been able to write storage + expect(trace.tracePublicStorageWrite).toHaveBeenCalledTimes(0); + }); - expect(results.reverted).toBe(true); // The outer call should revert. - expect(results.revertReason?.message).toEqual('Assertion failed: Values are not equal'); + 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'); + mockGetBytecode(hostStorage, 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/avm_simulator.ts b/yarn-project/simulator/src/avm/avm_simulator.ts index 6d0eb154332b..64d13a2ffbe9 100644 --- a/yarn-project/simulator/src/avm/avm_simulator.ts +++ b/yarn-project/simulator/src/avm/avm_simulator.ts @@ -29,10 +29,9 @@ export class AvmSimulator { * Fetch the bytecode and execute it in the current context. */ public async execute(): Promise { - const selector = this.context.environment.temporaryFunctionSelector; - const bytecode = await this.context.persistableState.hostStorage.contractsDb.getBytecode( + const bytecode = await this.context.persistableState.getBytecode( this.context.environment.address, - selector, + this.context.environment.temporaryFunctionSelector, ); // This assumes that we will not be able to send messages to accounts without code diff --git a/yarn-project/simulator/src/avm/fixtures/index.ts b/yarn-project/simulator/src/avm/fixtures/index.ts index b96be7f003c3..d7926c28dfe0 100644 --- a/yarn-project/simulator/src/avm/fixtures/index.ts +++ b/yarn-project/simulator/src/avm/fixtures/index.ts @@ -4,20 +4,21 @@ import { AztecAddress } from '@aztec/foundation/aztec-address'; import { EthAddress } from '@aztec/foundation/eth-address'; import { Fr } from '@aztec/foundation/fields'; import { AvmTestContractArtifact } from '@aztec/noir-contracts.js'; -import { SerializableContractInstance } from '@aztec/types/contracts'; import { strict as assert } from 'assert'; import { mock } from 'jest-mock-extended'; import merge from 'lodash.merge'; import { type CommitmentsDB, type PublicContractsDB, type PublicStateDB } from '../../index.js'; +import { type PublicSideEffectTraceInterface } from '../../public/side_effect_trace_interface.js'; import { AvmContext } from '../avm_context.js'; import { AvmContextInputs, AvmExecutionEnvironment } from '../avm_execution_environment.js'; 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 { NullifierManager } from '../journal/nullifiers.js'; +import { PublicStorage } from '../journal/public_storage.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(), ); @@ -47,9 +48,20 @@ export function initHostStorage(overrides?: { ); } -/** Creates an empty state manager with mocked storage. */ -export function initMockPersistableStateManager(): AvmPersistableStateManager { - return new AvmPersistableStateManager(initHostStorage()); +/** Creates an empty state manager with mocked host storage. */ +export function initPersistableStateManager(overrides?: { + hostStorage?: HostStorage; + trace?: PublicSideEffectTraceInterface; + publicStorage?: PublicStorage; + nullifiers?: NullifierManager; +}): AvmPersistableStateManager { + const hostStorage = overrides?.hostStorage || initHostStorage(); + return new AvmPersistableStateManager( + hostStorage, + overrides?.trace || mock(), + overrides?.publicStorage || new PublicStorage(hostStorage.publicStateDb), + overrides?.nullifiers || new NullifierManager(hostStorage.commitmentsDb), + ); } /** @@ -138,14 +150,3 @@ export function getAvmTestContractBytecode(functionName: string): Buffer { ); return artifact.bytecode; } - -export function randomTracedContractInstance(): TracedContractInstance { - const instance = SerializableContractInstance.random(); - const address = AztecAddress.random(); - return { exists: true, ...instance, address }; -} - -export function emptyTracedContractInstance(withAddress?: AztecAddress): TracedContractInstance { - const instance = SerializableContractInstance.empty().withAddress(withAddress ?? AztecAddress.zero()); - return { exists: false, ...instance }; -} diff --git a/yarn-project/simulator/src/avm/journal/journal.test.ts b/yarn-project/simulator/src/avm/journal/journal.test.ts index 77b7b3732b68..7d001d3ee6ac 100644 --- a/yarn-project/simulator/src/avm/journal/journal.test.ts +++ b/yarn-project/simulator/src/avm/journal/journal.test.ts @@ -1,445 +1,431 @@ -import { UnencryptedL2Log } from '@aztec/circuit-types'; -import { AztecAddress, EthAddress } from '@aztec/circuits.js'; -import { EventSelector } from '@aztec/foundation/abi'; +import { randomContractInstanceWithAddress } from '@aztec/circuit-types'; import { Fr } from '@aztec/foundation/fields'; - -import { type MockProxy, mock } from 'jest-mock-extended'; - -import { type CommitmentsDB, type PublicContractsDB, type PublicStateDB } from '../../index.js'; -import { emptyTracedContractInstance, randomTracedContractInstance } from '../fixtures/index.js'; -import { HostStorage } from './host_storage.js'; -import { AvmPersistableStateManager, type JournalData } from './journal.js'; +import { SerializableContractInstance } from '@aztec/types/contracts'; + +import { mock } from 'jest-mock-extended'; + +import { type PublicSideEffectTraceInterface } from '../../public/side_effect_trace_interface.js'; +import { initHostStorage, initPersistableStateManager } from '../fixtures/index.js'; +import { + mockGetContractInstance, + mockL1ToL2MessageExists, + mockNoteHashExists, + mockNullifierExists, + mockStorageRead, +} from '../test_utils.js'; +import { type HostStorage } from './host_storage.js'; +import { type AvmPersistableStateManager } from './journal.js'; describe('journal', () => { - let publicDb: MockProxy; - let contractsDb: MockProxy; - let commitmentsDb: MockProxy; - let journal: AvmPersistableStateManager; + const address = Fr.random(); + const utxo = Fr.random(); + const leafIndex = Fr.random(); - beforeEach(() => { - publicDb = mock(); - commitmentsDb = mock(); - contractsDb = mock(); + let hostStorage: HostStorage; + let trace: PublicSideEffectTraceInterface; + let persistableState: AvmPersistableStateManager; - const hostStorage = new HostStorage(publicDb, contractsDb, commitmentsDb); - journal = new AvmPersistableStateManager(hostStorage); + beforeEach(() => { + hostStorage = initHostStorage(); + trace = mock(); + persistableState = initPersistableStateManager({ hostStorage, trace }); }); describe('Public Storage', () => { it('When reading from storage, should check the cache first, and be appended to read/write journal', async () => { // Store a different value in storage vs the cache, and make sure the cache is returned - const contractAddress = new Fr(1); - const key = new Fr(2); + const slot = new Fr(2); const storedValue = new Fr(420); const cachedValue = new Fr(69); - publicDb.storageRead.mockResolvedValue(Promise.resolve(storedValue)); + mockStorageRead(hostStorage, storedValue); // Get the cache first - const cacheMissResult = await journal.readStorage(contractAddress, key); + const cacheMissResult = await persistableState.readStorage(address, slot); expect(cacheMissResult).toEqual(storedValue); // Write to storage - journal.writeStorage(contractAddress, key, cachedValue); + persistableState.writeStorage(address, slot, cachedValue); // Get the storage value - const cachedResult = await journal.readStorage(contractAddress, key); + const cachedResult = await persistableState.readStorage(address, slot); expect(cachedResult).toEqual(cachedValue); + // confirm that peek works + expect(await persistableState.peekStorage(address, slot)).toEqual(cachedResult); // We expect the journal to store the access in [storedVal, cachedVal] - [time0, time1] - const { storageReads, storageWrites }: JournalData = journal.flush(); - expect(storageReads).toEqual([ - expect.objectContaining({ - storageAddress: contractAddress, - exists: true, - slot: key, - value: storedValue, - }), - expect.objectContaining({ - storageAddress: contractAddress, - exists: true, - slot: key, - value: cachedValue, - }), - ]); - expect(storageWrites).toEqual([ - expect.objectContaining({ - storageAddress: contractAddress, - slot: key, - value: cachedValue, - }), - ]); + expect(trace.tracePublicStorageRead).toHaveBeenCalledTimes(2); + expect(trace.tracePublicStorageRead).toHaveBeenNthCalledWith( + /*nthCall=*/ 1, + address, + slot, + storedValue, + /*exists=*/ true, + /*cached=*/ false, + ); + expect(trace.tracePublicStorageRead).toHaveBeenNthCalledWith( + /*nthCall=*/ 2, + address, + slot, + cachedValue, + /*exists=*/ true, + /*cached=*/ true, + ); }); }); describe('UTXOs & messages', () => { - it('Should maintain commitments', () => { - const utxo = new Fr(1); - const address = new Fr(1234); - journal.writeNoteHash(address, utxo); - - const journalUpdates = journal.flush(); - expect(journalUpdates.newNoteHashes).toEqual([ - expect.objectContaining({ noteHash: utxo, storageAddress: address }), - ]); - }); - it('checkNullifierExists works for missing nullifiers', async () => { - const contractAddress = new Fr(1); - const utxo = new Fr(2); - const exists = await journal.checkNullifierExists(contractAddress, utxo); + it('checkNoteHashExists works for missing note hashes', async () => { + const exists = await persistableState.checkNoteHashExists(address, utxo, leafIndex); expect(exists).toEqual(false); - - const journalUpdates = journal.flush(); - expect(journalUpdates.nullifierChecks).toEqual([expect.objectContaining({ nullifier: utxo, exists: false })]); + expect(trace.traceNoteHashCheck).toHaveBeenCalledTimes(1); + expect(trace.traceNoteHashCheck).toHaveBeenCalledWith(address, utxo, leafIndex, exists); }); - it('checkNullifierExists works for existing nullifiers', async () => { - const contractAddress = new Fr(1); - const utxo = new Fr(2); - const storedLeafIndex = BigInt(42); - commitmentsDb.getNullifierIndex.mockResolvedValue(Promise.resolve(storedLeafIndex)); - const exists = await journal.checkNullifierExists(contractAddress, utxo); + it('checkNoteHashExists works for existing note hashes', async () => { + mockNoteHashExists(hostStorage, leafIndex, utxo); + const exists = await persistableState.checkNoteHashExists(address, utxo, leafIndex); expect(exists).toEqual(true); - - const journalUpdates = journal.flush(); - expect(journalUpdates.nullifierChecks).toEqual([expect.objectContaining({ nullifier: utxo, exists: true })]); + expect(trace.traceNoteHashCheck).toHaveBeenCalledTimes(1); + expect(trace.traceNoteHashCheck).toHaveBeenCalledWith(address, utxo, leafIndex, exists); }); - it('Should maintain nullifiers', async () => { - const contractAddress = new Fr(1); - const utxo = new Fr(2); - await journal.writeNullifier(contractAddress, utxo); - - const journalUpdates = journal.flush(); - expect(journalUpdates.newNullifiers).toEqual([ - expect.objectContaining({ storageAddress: contractAddress, nullifier: utxo }), - ]); + + it('writeNoteHash works', () => { + persistableState.writeNoteHash(address, utxo); + expect(trace.traceNewNoteHash).toHaveBeenCalledTimes(1); + expect(trace.traceNewNoteHash).toHaveBeenCalledWith(expect.objectContaining(address), /*noteHash=*/ utxo); }); - it('checkL1ToL2MessageExists works for missing message', async () => { - const msgHash = new Fr(2); - const leafIndex = new Fr(42); - const exists = await journal.checkL1ToL2MessageExists(msgHash, leafIndex); + it('checkNullifierExists works for missing nullifiers', async () => { + const exists = await persistableState.checkNullifierExists(address, utxo); expect(exists).toEqual(false); - - const journalUpdates = journal.flush(); - expect(journalUpdates.l1ToL2MessageChecks).toEqual([ - expect.objectContaining({ leafIndex: leafIndex, msgHash, exists: false }), - ]); + expect(trace.traceNullifierCheck).toHaveBeenCalledTimes(1); + expect(trace.traceNullifierCheck).toHaveBeenCalledWith( + address, + utxo, + /*leafIndex=*/ Fr.ZERO, + exists, + /*isPending=*/ false, + ); }); - it('checkL1ToL2MessageExists works for existing msgHash', async () => { - const msgHash = new Fr(2); - const leafIndex = new Fr(42); - commitmentsDb.getL1ToL2LeafValue.mockResolvedValue(msgHash); - const exists = await journal.checkL1ToL2MessageExists(msgHash, leafIndex); + it('checkNullifierExists works for existing nullifiers', async () => { + mockNullifierExists(hostStorage, leafIndex, utxo); + const exists = await persistableState.checkNullifierExists(address, utxo); expect(exists).toEqual(true); + expect(trace.traceNullifierCheck).toHaveBeenCalledTimes(1); + expect(trace.traceNullifierCheck).toHaveBeenCalledWith(address, utxo, leafIndex, exists, /*isPending=*/ false); + }); - const journalUpdates = journal.flush(); - expect(journalUpdates.l1ToL2MessageChecks).toEqual([ - expect.objectContaining({ leafIndex: leafIndex, msgHash, exists: true }), - ]); + it('writeNullifier works', async () => { + await persistableState.writeNullifier(address, utxo); + expect(trace.traceNewNullifier).toHaveBeenCalledWith(expect.objectContaining(address), /*nullifier=*/ utxo); }); - it('Should maintain nullifiers', async () => { - const contractAddress = new Fr(1); - const utxo = new Fr(2); - await journal.writeNullifier(contractAddress, utxo); - - const journalUpdates = journal.flush(); - expect(journalUpdates.newNullifiers).toEqual([ - expect.objectContaining({ storageAddress: contractAddress, nullifier: utxo }), - ]); + + it('checkL1ToL2MessageExists works for missing message', async () => { + const exists = await persistableState.checkL1ToL2MessageExists(address, utxo, leafIndex); + expect(exists).toEqual(false); + expect(trace.traceL1ToL2MessageCheck).toHaveBeenCalledTimes(1); + expect(trace.traceL1ToL2MessageCheck).toHaveBeenCalledWith(address, utxo, leafIndex, exists); }); - it('Should maintain l1 messages', () => { - const recipient = EthAddress.fromField(new Fr(1)); - const msgHash = new Fr(2); - journal.writeL1Message(recipient, msgHash); - const journalUpdates = journal.flush(); - expect(journalUpdates.newL1Messages).toEqual([expect.objectContaining({ recipient, content: msgHash })]); + it('checkL1ToL2MessageExists works for existing message', async () => { + mockL1ToL2MessageExists(hostStorage, leafIndex, utxo); + const exists = await persistableState.checkL1ToL2MessageExists(address, utxo, leafIndex); + expect(exists).toEqual(true); + expect(trace.traceL1ToL2MessageCheck).toHaveBeenCalledTimes(1); + expect(trace.traceL1ToL2MessageCheck).toHaveBeenCalledWith(address, utxo, leafIndex, exists); }); - describe('Getting contract instances', () => { - it('Should get contract instance', async () => { - const contractAddress = AztecAddress.fromField(new Fr(2)); - const instance = randomTracedContractInstance(); - instance.exists = true; - contractsDb.getContractInstance.mockResolvedValue(Promise.resolve(instance)); - await journal.getContractInstance(contractAddress); - expect(journal.trace.gotContractInstances).toEqual([instance]); - }); - it('Can get undefined contract instance', async () => { - const contractAddress = AztecAddress.fromField(new Fr(2)); - await journal.getContractInstance(contractAddress); - const emptyInstance = emptyTracedContractInstance(AztecAddress.fromField(contractAddress)); - expect(journal.trace.gotContractInstances).toEqual([emptyInstance]); - }); + it('Should maintain l1 messages', () => { + const recipient = new Fr(1); + persistableState.writeL2ToL1Message(recipient, utxo); + expect(trace.traceNewL2ToL1Message).toHaveBeenCalledTimes(1); + expect(trace.traceNewL2ToL1Message).toHaveBeenCalledWith(recipient, utxo); }); }); - it('Should merge two successful journals together', async () => { - // Fundamentally checking that insert ordering of public storage is preserved upon journal merge - // time | journal | op | value - // t0 -> journal0 -> write | 1 - // t1 -> journal1 -> write | 2 - // merge journals - // t2 -> journal0 -> read | 2 - - const contractAddress = new Fr(1); - const aztecContractAddress = AztecAddress.fromField(contractAddress); - const key = new Fr(2); - const value = new Fr(1); - const valueT1 = new Fr(2); - const recipient = EthAddress.fromField(new Fr(42)); - const commitment = new Fr(10); - const commitmentT1 = new Fr(20); - const log = { address: 10n, selector: 5, data: [new Fr(5), new Fr(6)] }; - const logT1 = { address: 20n, selector: 8, data: [new Fr(7), new Fr(8)] }; - const index = new Fr(42); - const indexT1 = new Fr(24); - const instance = emptyTracedContractInstance(aztecContractAddress); - - 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); - await journal.writeNullifier(contractAddress, commitment); - await journal.checkNullifierExists(contractAddress, commitment); - await journal.checkL1ToL2MessageExists(commitment, index); - await journal.getContractInstance(aztecContractAddress); - - const childJournal = new AvmPersistableStateManager(journal.hostStorage, 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); - await childJournal.writeNullifier(contractAddress, commitmentT1); - await childJournal.checkNullifierExists(contractAddress, commitmentT1); - await childJournal.checkL1ToL2MessageExists(commitmentT1, indexT1); - await childJournal.getContractInstance(aztecContractAddress); - - journal.acceptNestedCallState(childJournal); - - const result = await journal.readStorage(contractAddress, key); - expect(result).toEqual(valueT1); - - // Check that the storage is merged by reading from the journal - // Check that the UTXOs are merged - const journalUpdates: JournalData = journal.flush(); - - // Check storage reads order is preserved upon merge - // We first read value from t0, then value from t1 - expect(journalUpdates.storageReads).toEqual([ - expect.objectContaining({ - storageAddress: contractAddress, - exists: true, - slot: key, - value: value, - }), - expect.objectContaining({ - storageAddress: contractAddress, - exists: true, - slot: key, - value: valueT1, - }), - // Read a third time to check storage - expect.objectContaining({ - storageAddress: contractAddress, - exists: true, - slot: key, - value: valueT1, - }), - ]); - - // We first write value from t0, then value from t1 - expect(journalUpdates.storageWrites).toEqual([ - expect.objectContaining({ - storageAddress: contractAddress, - slot: key, - value: value, - }), - expect.objectContaining({ - storageAddress: contractAddress, - slot: key, - value: valueT1, - }), - ]); - - expect(journalUpdates.newNoteHashes).toEqual([ - expect.objectContaining({ noteHash: commitment, storageAddress: contractAddress }), - expect.objectContaining({ noteHash: commitmentT1, storageAddress: contractAddress }), - ]); - expect(journalUpdates.newLogs).toEqual([ - new UnencryptedL2Log( - AztecAddress.fromBigInt(log.address), - new EventSelector(log.selector), - Buffer.concat(log.data.map(f => f.toBuffer())), - ), - new UnencryptedL2Log( - AztecAddress.fromBigInt(logT1.address), - new EventSelector(logT1.selector), - Buffer.concat(logT1.data.map(f => f.toBuffer())), - ), - ]); - expect(journalUpdates.newL1Messages).toEqual([ - expect.objectContaining({ recipient, content: commitment }), - expect.objectContaining({ recipient, content: commitmentT1 }), - ]); - expect(journalUpdates.nullifierChecks).toEqual([ - expect.objectContaining({ nullifier: commitment, exists: true }), - expect.objectContaining({ nullifier: commitmentT1, exists: true }), - ]); - expect(journalUpdates.newNullifiers).toEqual([ - expect.objectContaining({ - storageAddress: contractAddress, - nullifier: commitment, - }), - expect.objectContaining({ - storageAddress: contractAddress, - nullifier: commitmentT1, - }), - ]); - expect(journalUpdates.l1ToL2MessageChecks).toEqual([ - expect.objectContaining({ leafIndex: index, msgHash: commitment, exists: false }), - expect.objectContaining({ leafIndex: indexT1, msgHash: commitmentT1, exists: false }), - ]); - expect(journal.trace.gotContractInstances).toEqual([instance, instance]); - }); + describe('Getting contract instances', () => { + it('Should get contract instance', async () => { + const contractInstance = randomContractInstanceWithAddress(/*(base instance) opts=*/ {}, /*address=*/ address); + mockGetContractInstance(hostStorage, contractInstance); + await persistableState.getContractInstance(address); + expect(trace.traceGetContractInstance).toHaveBeenCalledTimes(1); + expect(trace.traceGetContractInstance).toHaveBeenCalledWith({ exists: true, ...contractInstance }); + }); + it('Can get undefined contract instance', async () => { + const emptyContractInstance = SerializableContractInstance.empty().withAddress(address); + await persistableState.getContractInstance(address); - it('Should merge failed journals together', async () => { - // Checking public storage update journals are preserved upon journal merge, - // But the latest state is not - - // time | journal | op | value - // t0 -> journal0 -> write | 1 - // t1 -> journal1 -> write | 2 - // merge journals - // t2 -> journal0 -> read | 1 - - const contractAddress = new Fr(1); - const aztecContractAddress = AztecAddress.fromField(contractAddress); - const key = new Fr(2); - const value = new Fr(1); - const valueT1 = new Fr(2); - const recipient = EthAddress.fromField(new Fr(42)); - const commitment = new Fr(10); - const commitmentT1 = new Fr(20); - const log = { address: 10n, selector: 5, data: [new Fr(5), new Fr(6)] }; - const logT1 = { address: 20n, selector: 8, data: [new Fr(7), new Fr(8)] }; - const index = new Fr(42); - const indexT1 = new Fr(24); - const instance = emptyTracedContractInstance(aztecContractAddress); - - journal.writeStorage(contractAddress, key, value); - await journal.readStorage(contractAddress, key); - journal.writeNoteHash(contractAddress, commitment); - 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); - await journal.getContractInstance(aztecContractAddress); - - const childJournal = new AvmPersistableStateManager(journal.hostStorage, journal); - childJournal.writeStorage(contractAddress, key, valueT1); - await childJournal.readStorage(contractAddress, key); - childJournal.writeNoteHash(contractAddress, commitmentT1); - 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); - await childJournal.getContractInstance(aztecContractAddress); - - journal.rejectNestedCallState(childJournal); - - // Check that the storage is reverted by reading from the journal - const result = await journal.readStorage(contractAddress, key); - expect(result).toEqual(value); // rather than valueT1 - - const journalUpdates: JournalData = journal.flush(); - - // Reads and writes should be preserved - // Check storage reads order is preserved upon merge - // We first read value from t0, then value from t1 - expect(journalUpdates.storageReads).toEqual([ - expect.objectContaining({ - storageAddress: contractAddress, - exists: true, - slot: key, - value: value, - }), - expect.objectContaining({ - storageAddress: contractAddress, - exists: true, - slot: key, - value: valueT1, - }), - // Read a third time to check storage - expect.objectContaining({ - storageAddress: contractAddress, - exists: true, - slot: key, - value: value, - }), - ]); - - // We first write value from t0, then value from t1 - expect(journalUpdates.storageWrites).toEqual([ - expect.objectContaining({ - storageAddress: contractAddress, - slot: key, - value: value, - }), - expect.objectContaining({ - storageAddress: contractAddress, - slot: key, - value: valueT1, - }), - ]); - - // Check that the world state _traces_ are merged even on rejection - expect(journalUpdates.newNoteHashes).toEqual([ - expect.objectContaining({ noteHash: commitment, storageAddress: contractAddress }), - expect.objectContaining({ noteHash: commitmentT1, storageAddress: contractAddress }), - ]); - expect(journalUpdates.nullifierChecks).toEqual([ - expect.objectContaining({ nullifier: commitment, exists: true }), - expect.objectContaining({ nullifier: commitmentT1, exists: true }), - ]); - expect(journalUpdates.newNullifiers).toEqual([ - expect.objectContaining({ - storageAddress: contractAddress, - nullifier: commitment, - }), - expect.objectContaining({ - storageAddress: contractAddress, - nullifier: commitmentT1, - }), - ]); - expect(journalUpdates.l1ToL2MessageChecks).toEqual([ - expect.objectContaining({ leafIndex: index, msgHash: commitment, exists: false }), - expect.objectContaining({ leafIndex: indexT1, msgHash: commitmentT1, exists: false }), - ]); - - // Check that rejected Accrued Substate is absent - expect(journalUpdates.newLogs).toEqual([ - new UnencryptedL2Log( - AztecAddress.fromBigInt(log.address), - new EventSelector(log.selector), - Buffer.concat(log.data.map(f => f.toBuffer())), - ), - ]); - expect(journalUpdates.newL1Messages).toEqual([expect.objectContaining({ recipient, content: commitment })]); - expect(journal.trace.gotContractInstances).toEqual([instance, instance]); + expect(trace.traceGetContractInstance).toHaveBeenCalledTimes(1); + expect(trace.traceGetContractInstance).toHaveBeenCalledWith({ exists: false, ...emptyContractInstance }); + }); }); - it('Can fork and merge journals', () => { - const rootJournal = new AvmPersistableStateManager(journal.hostStorage); - const childJournal = rootJournal.fork(); - - expect(() => rootJournal.acceptNestedCallState(childJournal)); - expect(() => rootJournal.rejectNestedCallState(childJournal)); - }); + //it('Should merge two successful journals together', async () => { + // // Fundamentally checking that insert ordering of public storage is preserved upon journal merge + // // time | journal | op | value + // // t0 -> journal0 -> write | 1 + // // t1 -> journal1 -> write | 2 + // // merge journals + // // t2 -> journal0 -> read | 2 + + // const contractAddress = new Fr(1); + // const aztecContractAddress = AztecAddress.fromField(contractAddress); + // const key = new Fr(2); + // const value = new Fr(1); + // const valueT1 = new Fr(2); + // const recipient = EthAddress.fromField(new Fr(42)); + // const commitment = new Fr(10); + // const commitmentT1 = new Fr(20); + // const log = { address: 10n, selector: 5, data: [new Fr(5), new Fr(6)] }; + // const logT1 = { address: 20n, selector: 8, data: [new Fr(7), new Fr(8)] }; + // const index = new Fr(42); + // const indexT1 = new Fr(24); + // const instance = emptyTracedContractInstance(aztecContractAddress); + + // persistableState.writeStorage(contractAddress, key, value); + // await persistableState.readStorage(contractAddress, key); + // persistableState.writeNoteHash(contractAddress, commitment); + // persistableState.writeUnencryptedLog(new Fr(log.address), new Fr(log.selector), log.data); + // persistableState.writeL2ToL1Message(recipient, commitment); + // await persistableState.writeNullifier(contractAddress, commitment); + // await persistableState.checkNullifierExists(contractAddress, commitment); + // await persistableState.checkL1ToL2MessageExists(commitment, index); + // await persistableState.getContractInstance(aztecContractAddress); + + // const childJournal = new AvmPersistableStateManager(persistableState.hostStorage, persistableState); + // childJournal.writeStorage(contractAddress, key, valueT1); + // await childJournal.readStorage(contractAddress, key); + // childJournal.writeNoteHash(contractAddress, 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); + // await childJournal.getContractInstance(aztecContractAddress); + + // persistableState.acceptNestedCallState(childJournal); + + // const result = await persistableState.readStorage(contractAddress, key); + // expect(result).toEqual(valueT1); + + // // Check that the storage is merged by reading from the journal + // // Check that the UTXOs are merged + // const journalUpdates: JournalData = persistableState.getTrace()(); + + // // Check storage reads order is preserved upon merge + // // We first read value from t0, then value from t1 + // expect(journalUpdates.storageReads).toEqual([ + // expect.objectContaining({ + // storageAddress: contractAddress, + // exists: true, + // slot: key, + // value: value, + // }), + // expect.objectContaining({ + // storageAddress: contractAddress, + // exists: true, + // slot: key, + // value: valueT1, + // }), + // // Read a third time to check storage + // expect.objectContaining({ + // storageAddress: contractAddress, + // exists: true, + // slot: key, + // value: valueT1, + // }), + // ]); + + // // We first write value from t0, then value from t1 + // expect(journalUpdates.storageWrites).toEqual([ + // expect.objectContaining({ + // storageAddress: contractAddress, + // slot: key, + // value: value, + // }), + // expect.objectContaining({ + // storageAddress: contractAddress, + // slot: key, + // value: valueT1, + // }), + // ]); + + // expect(journalUpdates.newNoteHashes).toEqual([ + // expect.objectContaining({ noteHash: commitment, storageAddress: contractAddress }), + // expect.objectContaining({ noteHash: commitmentT1, storageAddress: contractAddress }), + // ]); + // expect(journalUpdates.newLogs).toEqual([ + // new UnencryptedL2Log( + // AztecAddress.fromBigInt(log.address), + // new EventSelector(log.selector), + // Buffer.concat(log.data.map(f => f.toBuffer())), + // ), + // new UnencryptedL2Log( + // AztecAddress.fromBigInt(logT1.address), + // new EventSelector(logT1.selector), + // Buffer.concat(logT1.data.map(f => f.toBuffer())), + // ), + // ]); + // expect(journalUpdates.newL1Messages).toEqual([ + // expect.objectContaining({ recipient, content: commitment }), + // expect.objectContaining({ recipient, content: commitmentT1 }), + // ]); + // expect(journalUpdates.nullifierChecks).toEqual([ + // expect.objectContaining({ nullifier: commitment, exists: true }), + // expect.objectContaining({ nullifier: commitmentT1, exists: true }), + // ]); + // expect(journalUpdates.newNullifiers).toEqual([ + // expect.objectContaining({ + // storageAddress: contractAddress, + // nullifier: commitment, + // }), + // expect.objectContaining({ + // storageAddress: contractAddress, + // nullifier: commitmentT1, + // }), + // ]); + // expect(journalUpdates.l1ToL2MessageChecks).toEqual([ + // expect.objectContaining({ leafIndex: index, msgHash: commitment, exists: false }), + // expect.objectContaining({ leafIndex: indexT1, msgHash: commitmentT1, exists: false }), + // ]); + // expect(persistableState.trace.gotContractInstances).toEqual([instance, instance]); + //}); + + //it('Should merge failed journals together', async () => { + // // Checking public storage update journals are preserved upon journal merge, + // // But the latest state is not + + // // time | journal | op | value + // // t0 -> journal0 -> write | 1 + // // t1 -> journal1 -> write | 2 + // // merge journals + // // t2 -> journal0 -> read | 1 + + // const contractAddress = new Fr(1); + // const aztecContractAddress = AztecAddress.fromField(contractAddress); + // const key = new Fr(2); + // const value = new Fr(1); + // const valueT1 = new Fr(2); + // const recipient = EthAddress.fromField(new Fr(42)); + // const commitment = new Fr(10); + // const commitmentT1 = new Fr(20); + // const log = { address: 10n, selector: 5, data: [new Fr(5), new Fr(6)] }; + // const logT1 = { address: 20n, selector: 8, data: [new Fr(7), new Fr(8)] }; + // const index = new Fr(42); + // const indexT1 = new Fr(24); + // const instance = emptyTracedContractInstance(aztecContractAddress); + + // persistableState.writeStorage(contractAddress, key, value); + // await persistableState.readStorage(contractAddress, key); + // persistableState.writeNoteHash(contractAddress, commitment); + // await persistableState.writeNullifier(contractAddress, commitment); + // await persistableState.checkNullifierExists(contractAddress, commitment); + // await persistableState.checkL1ToL2MessageExists(commitment, index); + // persistableState.writeUnencryptedLog(new Fr(log.address), new Fr(log.selector), log.data); + // persistableState.writeL2ToL1Message(recipient, commitment); + // await persistableState.getContractInstance(aztecContractAddress); + + // const childJournal = new AvmPersistableStateManager(persistableState.hostStorage, persistableState); + // childJournal.writeStorage(contractAddress, key, valueT1); + // await childJournal.readStorage(contractAddress, key); + // childJournal.writeNoteHash(contractAddress, commitmentT1); + // await childJournal.writeNullifier(contractAddress, commitmentT1); + // await childJournal.checkNullifierExists(contractAddress, commitmentT1); + // await persistableState.checkL1ToL2MessageExists(commitmentT1, indexT1); + // childJournal.writeUnencryptedLog(new Fr(logT1.address), new Fr(logT1.selector), logT1.data); + // childJournal.writeL2ToL1Message(recipient, commitmentT1); + // await childJournal.getContractInstance(aztecContractAddress); + + // persistableState.rejectNestedCallState(childJournal); + + // // Check that the storage is reverted by reading from the journal + // const result = await persistableState.readStorage(contractAddress, key); + // expect(result).toEqual(value); // rather than valueT1 + + // const journalUpdates: JournalData = persistableState.getTrace()(); + + // // Reads and writes should be preserved + // // Check storage reads order is preserved upon merge + // // We first read value from t0, then value from t1 + // expect(journalUpdates.storageReads).toEqual([ + // expect.objectContaining({ + // storageAddress: contractAddress, + // exists: true, + // slot: key, + // value: value, + // }), + // expect.objectContaining({ + // storageAddress: contractAddress, + // exists: true, + // slot: key, + // value: valueT1, + // }), + // // Read a third time to check storage + // expect.objectContaining({ + // storageAddress: contractAddress, + // exists: true, + // slot: key, + // value: value, + // }), + // ]); + + // // We first write value from t0, then value from t1 + // expect(journalUpdates.storageWrites).toEqual([ + // expect.objectContaining({ + // storageAddress: contractAddress, + // slot: key, + // value: value, + // }), + // expect.objectContaining({ + // storageAddress: contractAddress, + // slot: key, + // value: valueT1, + // }), + // ]); + + // // Check that the world state _traces_ are merged even on rejection + // expect(journalUpdates.newNoteHashes).toEqual([ + // expect.objectContaining({ noteHash: commitment, storageAddress: contractAddress }), + // expect.objectContaining({ noteHash: commitmentT1, storageAddress: contractAddress }), + // ]); + // expect(journalUpdates.nullifierChecks).toEqual([ + // expect.objectContaining({ nullifier: commitment, exists: true }), + // expect.objectContaining({ nullifier: commitmentT1, exists: true }), + // ]); + // expect(journalUpdates.newNullifiers).toEqual([ + // expect.objectContaining({ + // storageAddress: contractAddress, + // nullifier: commitment, + // }), + // expect.objectContaining({ + // storageAddress: contractAddress, + // nullifier: commitmentT1, + // }), + // ]); + // expect(journalUpdates.l1ToL2MessageChecks).toEqual([ + // expect.objectContaining({ leafIndex: index, msgHash: commitment, exists: false }), + // expect.objectContaining({ leafIndex: indexT1, msgHash: commitmentT1, exists: false }), + // ]); + + // // Check that rejected Accrued Substate is absent + // expect(journalUpdates.newLogs).toEqual([ + // new UnencryptedL2Log( + // AztecAddress.fromBigInt(log.address), + // new EventSelector(log.selector), + // Buffer.concat(log.data.map(f => f.toBuffer())), + // ), + // ]); + // expect(journalUpdates.newL1Messages).toEqual([expect.objectContaining({ recipient, content: commitment })]); + // expect(persistableState.trace.gotContractInstances).toEqual([instance, instance]); + //}); + + //it('Can fork and merge journals', () => { + // const rootJournal = new AvmPersistableStateManager(persistableState.hostStorage); + // const childJournal = rootJournal.fork(); + + // expect(() => rootJournal.acceptNestedCallState(childJournal)); + // expect(() => rootJournal.rejectNestedCallState(childJournal)); + //}); }); diff --git a/yarn-project/simulator/src/avm/journal/journal.ts b/yarn-project/simulator/src/avm/journal/journal.ts index dd028a63db97..06e6465385fc 100644 --- a/yarn-project/simulator/src/avm/journal/journal.ts +++ b/yarn-project/simulator/src/avm/journal/journal.ts @@ -1,139 +1,69 @@ -// 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 { Fr } from '@aztec/foundation/fields'; +import { AztecAddress, type FunctionSelector, type Gas } from '@aztec/circuits.js'; +import { type 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 TracedContractInstance } from '../../public/side_effect_trace.js'; +import { type PublicSideEffectTraceInterface } from '../../public/side_effect_trace_interface.js'; +import { type AvmExecutionEnvironment } from '../avm_execution_environment.js'; +import { type AvmContractCallResults } from '../avm_message_call_result.js'; import { type HostStorage } from './host_storage.js'; -import { Nullifiers } from './nullifiers.js'; +import { NullifierManager } 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[]; -}; /** * 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 */ - /** 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; + constructor( + /** Reference to node storage */ + private hostStorage: HostStorage, + /** Side effect trace */ + private trace: PublicSideEffectTraceInterface, + /** Public storage, including cached writes */ + public readonly publicStorage: PublicStorage, + /** Nullifier set, including cached/recently-emitted nullifiers */ + private readonly nullifiers: NullifierManager, + ) {} - /** 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; - 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.accessCounter, - newNullifiers: [], - contractStorageReads: [], - contractStorageUpdateRequests: [], - unencryptedLogsHashes: [], - unencryptedLogs: [], - allUnencryptedLogs: [], - nestedExecutions: [], - }; + /** + * Create a new state manager with some preloaded pending siloed nullifiers + */ + public static newWithPendingSiloedNullifiers( + hostStorage: HostStorage, + trace: PublicSideEffectTraceInterface, + pendingSiloedNullifiers: Fr[], + ) { + const parentNullifiers = NullifierManager.newWithPendingSiloedNullifiers( + hostStorage.commitmentsDb, + pendingSiloedNullifiers, + ); + return new AvmPersistableStateManager( + hostStorage, + trace, + /*publicStorage=*/ new PublicStorage(hostStorage.publicStateDb), + /*nullifiers=*/ parentNullifiers.fork(), + ); } /** * 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.publicStorage.fork(), + this.nullifiers.fork(), + ); } /** @@ -147,13 +77,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.accessCounter, storageAddress), - ); - - // Trace all storage writes (even reverted ones) this.trace.tracePublicStorageWrite(storageAddress, slot, value); } @@ -169,14 +92,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.accessCounter, 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 +124,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.accessCounter)); - - this.trace.traceNoteHashCheck(storageAddress, noteHash, exists, leafIndex); + this.trace.traceNoteHashCheck(storageAddress, noteHash, leafIndex, exists); return Promise.resolve(exists); } @@ -206,9 +133,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.accessCounter)); - this.log.debug(`noteHashes(${storageAddress}) += @${noteHash}.`); this.trace.traceNewNoteHash(storageAddress, noteHash); } @@ -222,19 +146,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.accessCounter)); - } else { - this.transitionalExecutionResult.nullifierNonExistentReadRequests.push( - new ReadRequest(nullifier, this.trace.accessCounter), - ); - } - - this.trace.traceNullifierCheck(storageAddress, nullifier, exists, isPending, leafIndex); + this.trace.traceNullifierCheck(storageAddress, nullifier, leafIndex, exists, isPending); return Promise.resolve(exists); } @@ -244,11 +158,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.accessCounter, /*noteHash=*/ Fr.ZERO), - ); - this.log.debug(`nullifiers(${storageAddress}) += ${nullifier}.`); // Cache pending nullifiers for later access await this.nullifiers.append(storageAddress, nullifier); @@ -262,16 +171,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.accessCounter)); - - this.trace.traceL1ToL2MessageCheck(msgHash, msgLeafIndex, exists); + this.trace.traceL1ToL2MessageCheck(contractAddress, msgHash, msgLeafIndex, exists); return Promise.resolve(exists); } @@ -280,40 +186,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.accessCounter, 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); @@ -322,59 +215,57 @@ export class AvmPersistableStateManager { instance = SerializableContractInstance.empty().withAddress(aztecAddress); exists = false; } + this.log.debug( + `Get Contract instance (address=${contractAddress}): exists=${exists}, instance=${JSON.stringify(instance)}`, + ); const tracedInstance = { ...instance, exists }; this.trace.traceGetContractInstance(tracedInstance); return Promise.resolve(tracedInstance); } /** - * 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 + * Get a contract's bytecode from the contracts DB */ - public rejectNestedCallState(nestedJournal: AvmPersistableStateManager) { - // Merge World State Access Trace - this.trace.acceptAndMerge(nestedJournal.trace); + public async getBytecode(contractAddress: AztecAddress, selector: FunctionSelector): Promise { + return await this.hostStorage.contractsDb.getBytecode(contractAddress, selector); } - // TODO:(5818): do we need this type anymore? /** - * Access the current state of the journal - * - * @returns a JournalData object + * Accept the nested call's state and trace the nested call */ - 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.accessCounter, - }; + public async processNestedCall( + nestedState: AvmPersistableStateManager, + success: boolean, + nestedEnvironment: AvmExecutionEnvironment, + startGasLeft: Gas, + endGasLeft: Gas, + bytecode: Buffer, + avmCallResults: AvmContractCallResults, + ) { + if (success) { + this.acceptNestedCallState(nestedState); + } + const functionName = + (await nestedState.hostStorage.contractsDb.getDebugFunctionName( + nestedEnvironment.address, + nestedEnvironment.temporaryFunctionSelector, + )) ?? `${nestedEnvironment.address}:${nestedEnvironment.temporaryFunctionSelector}`; + this.trace.traceNestedCall( + nestedState.trace, + nestedEnvironment, + startGasLeft, + endGasLeft, + bytecode, + avmCallResults, + functionName, + ); } } diff --git a/yarn-project/simulator/src/avm/journal/nullifiers.test.ts b/yarn-project/simulator/src/avm/journal/nullifiers.test.ts index f8cec85bd92b..8a215a542288 100644 --- a/yarn-project/simulator/src/avm/journal/nullifiers.test.ts +++ b/yarn-project/simulator/src/avm/journal/nullifiers.test.ts @@ -3,15 +3,15 @@ import { Fr } from '@aztec/foundation/fields'; import { type MockProxy, mock } from 'jest-mock-extended'; import { type CommitmentsDB } from '../../index.js'; -import { Nullifiers } from './nullifiers.js'; +import { NullifierManager } from './nullifiers.js'; describe('avm nullifier caching', () => { let commitmentsDb: MockProxy; - let nullifiers: Nullifiers; + let nullifiers: NullifierManager; beforeEach(() => { commitmentsDb = mock(); - nullifiers = new Nullifiers(commitmentsDb); + nullifiers = new NullifierManager(commitmentsDb); }); describe('Nullifier caching and existence checks', () => { @@ -42,7 +42,7 @@ describe('avm nullifier caching', () => { const nullifier = new Fr(2); const storedLeafIndex = BigInt(420); - commitmentsDb.getNullifierIndex.mockResolvedValue(Promise.resolve(storedLeafIndex)); + commitmentsDb.getNullifierIndex.mockResolvedValue(storedLeafIndex); const [exists, isPending, gotIndex] = await nullifiers.checkExists(contractAddress, nullifier); // exists (in host), not pending, tree index retrieved from host @@ -53,7 +53,7 @@ describe('avm nullifier caching', () => { it('Existence check works on fallback to parent (gets value, exists, is pending)', async () => { const contractAddress = new Fr(1); const nullifier = new Fr(2); - const childNullifiers = new Nullifiers(commitmentsDb, nullifiers); + const childNullifiers = nullifiers.fork(); // Write to parent cache await nullifiers.append(contractAddress, nullifier); @@ -67,8 +67,8 @@ describe('avm nullifier caching', () => { it('Existence check works on fallback to grandparent (gets value, exists, is pending)', async () => { const contractAddress = new Fr(1); const nullifier = new Fr(2); - const childNullifiers = new Nullifiers(commitmentsDb, nullifiers); - const grandChildNullifiers = new Nullifiers(commitmentsDb, childNullifiers); + const childNullifiers = nullifiers.fork(); + const grandChildNullifiers = childNullifiers.fork(); // Write to parent cache await nullifiers.append(contractAddress, nullifier); @@ -99,7 +99,7 @@ describe('avm nullifier caching', () => { // Append a nullifier to parent await nullifiers.append(contractAddress, nullifier); - const childNullifiers = new Nullifiers(commitmentsDb, nullifiers); + const childNullifiers = nullifiers.fork(); // Can't append again in child await expect(childNullifiers.append(contractAddress, nullifier)).rejects.toThrow( `Nullifier ${nullifier} at contract ${contractAddress} already exists in parent cache or host.`, @@ -111,7 +111,7 @@ describe('avm nullifier caching', () => { const storedLeafIndex = BigInt(420); // Nullifier exists in host - commitmentsDb.getNullifierIndex.mockResolvedValue(Promise.resolve(storedLeafIndex)); + commitmentsDb.getNullifierIndex.mockResolvedValue(storedLeafIndex); // Can't append to cache await expect(nullifiers.append(contractAddress, nullifier)).rejects.toThrow( `Nullifier ${nullifier} at contract ${contractAddress} already exists in parent cache or host.`, @@ -128,7 +128,7 @@ describe('avm nullifier caching', () => { // Append a nullifier to parent await nullifiers.append(contractAddress, nullifier0); - const childNullifiers = new Nullifiers(commitmentsDb, nullifiers); + const childNullifiers = nullifiers.fork(); // Append a nullifier to child await childNullifiers.append(contractAddress, nullifier1); @@ -149,7 +149,7 @@ describe('avm nullifier caching', () => { await nullifiers.append(contractAddress, nullifier); // Create child cache, don't derive from parent so we can concoct a collision on merge - const childNullifiers = new Nullifiers(commitmentsDb); + const childNullifiers = new NullifierManager(commitmentsDb); // Append a nullifier to child await childNullifiers.append(contractAddress, nullifier); diff --git a/yarn-project/simulator/src/avm/journal/nullifiers.ts b/yarn-project/simulator/src/avm/journal/nullifiers.ts index e580c1a885c1..a4d23a357e21 100644 --- a/yarn-project/simulator/src/avm/journal/nullifiers.ts +++ b/yarn-project/simulator/src/avm/journal/nullifiers.ts @@ -9,17 +9,29 @@ import type { CommitmentsDB } from '../../index.js'; * Maintains a nullifier cache, and ensures that existence checks fall back to the correct source. * When a contract call completes, its cached nullifier set can be merged into its parent's. */ -export class Nullifiers { - /** Cached nullifiers. */ - public cache: NullifierCache; - +export class NullifierManager { constructor( /** Reference to node storage. Checked on parent cache-miss. */ private readonly hostNullifiers: CommitmentsDB, - /** Parent's nullifiers. Checked on this' cache-miss. */ - private readonly parent?: Nullifiers | undefined, - ) { - this.cache = new NullifierCache(); + /** Cached nullifiers. */ + private readonly cache: NullifierCache = new NullifierCache(), + /** Parent nullifier manager to fall back on */ + private readonly parent?: NullifierManager, + ) {} + + /** + * Create a new nullifiers manager with some preloaded pending siloed nullifiers + */ + public static newWithPendingSiloedNullifiers(hostNullifiers: CommitmentsDB, pendingSiloedNullifiers: Fr[]) { + const cache = new NullifierCache(pendingSiloedNullifiers); + return new NullifierManager(hostNullifiers, cache); + } + + /** + * Create a new nullifiers manager forked from this one + */ + public fork() { + return new NullifierManager(this.hostNullifiers, new NullifierCache(), this); } /** @@ -92,7 +104,7 @@ export class Nullifiers { * * @param incomingNullifiers - the incoming cached nullifiers to merge into this instance's */ - public acceptAndMerge(incomingNullifiers: Nullifiers) { + public acceptAndMerge(incomingNullifiers: NullifierManager) { this.cache.acceptAndMerge(incomingNullifiers.cache); } } @@ -111,6 +123,15 @@ export class NullifierCache { private cachePerContract: Map> = new Map(); private siloedNullifiers: Set = new Set(); + /** + * @parem siloedNullifierFrs: optional list of pending siloed nullifiers to initialize this cache with + */ + constructor(siloedNullifierFrs?: Fr[]) { + if (siloedNullifierFrs !== undefined) { + siloedNullifierFrs.forEach(nullifier => this.siloedNullifiers.add(nullifier.toBigInt())); + } + } + /** * Check whether a nullifier exists in the cache. * @@ -147,10 +168,6 @@ export class NullifierCache { nullifiersForContract.add(nullifier.toBigInt()); } - public appendSiloed(siloedNullifier: Fr) { - this.siloedNullifiers.add(siloedNullifier.toBigInt()); - } - /** * Merge another cache's nullifiers into this instance's. * diff --git a/yarn-project/simulator/src/avm/journal/public_storage.test.ts b/yarn-project/simulator/src/avm/journal/public_storage.test.ts index 1d6359caef95..3b20b5cae3ba 100644 --- a/yarn-project/simulator/src/avm/journal/public_storage.test.ts +++ b/yarn-project/simulator/src/avm/journal/public_storage.test.ts @@ -44,7 +44,7 @@ describe('avm public storage', () => { const slot = new Fr(2); const storedValue = new Fr(420); // ensure that fallback to host gets a value - publicDb.storageRead.mockResolvedValue(Promise.resolve(storedValue)); + publicDb.storageRead.mockResolvedValue(storedValue); const { exists, value: gotValue, cached } = await publicStorage.read(contractAddress, slot); // it exists in the host, so it must've been written before @@ -90,7 +90,7 @@ describe('avm public storage', () => { const parentValue = new Fr(69); const cachedValue = new Fr(1337); - publicDb.storageRead.mockResolvedValue(Promise.resolve(storedValue)); + publicDb.storageRead.mockResolvedValue(storedValue); const childStorage = new PublicStorage(publicDb, publicStorage); // Cache miss falls back to host diff --git a/yarn-project/simulator/src/avm/journal/public_storage.ts b/yarn-project/simulator/src/avm/journal/public_storage.ts index 6019934c201d..4dee472ab240 100644 --- a/yarn-project/simulator/src/avm/journal/public_storage.ts +++ b/yarn-project/simulator/src/avm/journal/public_storage.ts @@ -27,6 +27,13 @@ export class PublicStorage { this.cache = new PublicStorageCache(); } + /** + * Create a new public storage manager forked from this one + */ + public fork() { + return new PublicStorage(this.hostPublicStorage, this); + } + /** * Get the pending storage. */ @@ -71,6 +78,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 deleted file mode 100644 index a143ce4e3be4..000000000000 --- a/yarn-project/simulator/src/avm/journal/trace.test.ts +++ /dev/null @@ -1,294 +0,0 @@ -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'; - -describe('world state access trace', () => { - let trace: WorldStateAccessTrace; - - beforeEach(() => { - trace = new WorldStateAccessTrace(); - }); - - 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); - - 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.getAccessCounter()).toBe(1); - }); - it('Should trace note hashes', () => { - const contractAddress = new Fr(1); - const utxo = new Fr(2); - - trace.traceNewNoteHash(contractAddress, utxo); - - expect(trace.newNoteHashes).toEqual([ - expect.objectContaining({ storageAddress: contractAddress, noteHash: utxo }), - ]); - expect(trace.getAccessCounter()).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); - 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.getAccessCounter()).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]); - expect(trace.getAccessCounter()).toEqual(1); - }); - it('Should trace L1ToL2 Message checks', () => { - const utxo = new Fr(2); - 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.getAccessCounter()).toEqual(1); - }); - it('Should trace get contract instance', () => { - const instance = randomTracedContractInstance(); - trace.traceGetContractInstance(instance); - expect(trace.gotContractInstances).toEqual([instance]); - expect(trace.getAccessCounter()).toEqual(1); - }); - }); - - it('Access counter should properly count accesses', () => { - const contractAddress = new Fr(1); - const slot = new Fr(2); - const value = new Fr(1); - const nullifier = new Fr(20); - const nullifierExists = false; - const nullifierIsPending = false; - const nullifierLeafIndex = Fr.ZERO; - const noteHash = new Fr(10); - const noteHashLeafIndex = new Fr(88); - const noteHashExists = false; - const msgExists = false; - const msgLeafIndex = Fr.ZERO; - const msgHash = new Fr(10); - const instance = randomTracedContractInstance(); - - let counter = 0; - trace.tracePublicStorageWrite(contractAddress, slot, value); - counter++; - trace.tracePublicStorageRead(contractAddress, slot, value, /*exists=*/ true, /*cached=*/ true); - counter++; - trace.traceNoteHashCheck(contractAddress, noteHash, noteHashExists, noteHashLeafIndex); - counter++; - trace.traceNewNoteHash(contractAddress, noteHash); - counter++; - trace.traceNullifierCheck(contractAddress, nullifier, nullifierExists, nullifierIsPending, nullifierLeafIndex); - counter++; - trace.traceNewNullifier(contractAddress, nullifier); - counter++; - trace.traceL1ToL2MessageCheck(msgHash, msgLeafIndex, msgExists); - counter++; - trace.tracePublicStorageWrite(contractAddress, slot, value); - counter++; - trace.tracePublicStorageRead(contractAddress, slot, value, /*exists=*/ true, /*cached=*/ true); - counter++; - trace.traceNewNoteHash(contractAddress, noteHash); - counter++; - trace.traceNullifierCheck(contractAddress, nullifier, nullifierExists, nullifierIsPending, nullifierLeafIndex); - counter++; - trace.traceNewNullifier(contractAddress, nullifier); - counter++; - trace.traceL1ToL2MessageCheck(msgHash, msgLeafIndex, msgExists); - counter++; - trace.traceGetContractInstance(instance); - counter++; - expect(trace.getAccessCounter()).toEqual(counter); - }); - - it('Should merge two traces together', () => { - const contractAddress = new Fr(1); - const slot = new Fr(2); - const value = new Fr(1); - const valueT1 = new Fr(2); - - const noteHash = new Fr(10); - const noteHashExists = false; - const noteHashLeafIndex = new Fr(88); - const noteHashT1 = new Fr(11); - const noteHashExistsT1 = true; - const noteHashLeafIndexT1 = new Fr(7); - - const nullifierExists = false; - const nullifierIsPending = false; - const nullifierLeafIndex = Fr.ZERO; - const nullifier = new Fr(10); - const nullifierT1 = new Fr(20); - const nullifierExistsT1 = true; - const nullifierIsPendingT1 = false; - const nullifierLeafIndexT1 = new Fr(42); - - const msgExists = false; - const msgLeafIndex = Fr.ZERO; - const msgHash = new Fr(10); - const msgHashT1 = new Fr(20); - const msgExistsT1 = true; - const msgLeafIndexT1 = new Fr(42); - - const instance = randomTracedContractInstance(); - const instanceT1 = randomTracedContractInstance(); - - const expectedMessageCheck = { - leafIndex: msgLeafIndex, - msgHash: msgHash, - exists: msgExists, - }; - const expectedMessageCheckT1 = { - leafIndex: msgLeafIndexT1, - msgHash: msgHashT1, - exists: msgExistsT1, - }; - - trace.tracePublicStorageWrite(contractAddress, slot, value); - trace.tracePublicStorageRead(contractAddress, slot, value, /*exists=*/ true, /*cached=*/ true); - trace.traceNoteHashCheck(contractAddress, noteHash, noteHashExists, noteHashLeafIndex); - trace.traceNewNoteHash(contractAddress, noteHash); - trace.traceNullifierCheck(contractAddress, nullifier, nullifierExists, nullifierIsPending, nullifierLeafIndex); - trace.traceNewNullifier(contractAddress, nullifier); - trace.traceL1ToL2MessageCheck(msgHash, msgLeafIndex, msgExists); - trace.traceGetContractInstance(instance); - - const childTrace = new WorldStateAccessTrace(trace); - childTrace.tracePublicStorageWrite(contractAddress, slot, valueT1); - childTrace.tracePublicStorageRead(contractAddress, slot, valueT1, /*exists=*/ true, /*cached=*/ true); - childTrace.traceNoteHashCheck(contractAddress, noteHashT1, noteHashExistsT1, noteHashLeafIndexT1); - childTrace.traceNewNoteHash(contractAddress, nullifierT1); - childTrace.traceNullifierCheck( - contractAddress, - nullifierT1, - nullifierExistsT1, - nullifierIsPendingT1, - nullifierLeafIndexT1, - ); - childTrace.traceNewNullifier(contractAddress, nullifierT1); - childTrace.traceL1ToL2MessageCheck(msgHashT1, msgLeafIndexT1, msgExistsT1); - childTrace.traceGetContractInstance(instanceT1); - - const childCounterBeforeMerge = childTrace.getAccessCounter(); - trace.acceptAndMerge(childTrace); - expect(trace.getAccessCounter()).toEqual(childCounterBeforeMerge); - - expect(trace.publicStorageReads).toEqual([ - expect.objectContaining({ - storageAddress: contractAddress, - slot: slot, - value: value, - exists: true, - cached: true, - }), - expect.objectContaining({ - storageAddress: contractAddress, - slot: slot, - value: valueT1, - exists: true, - cached: true, - }), - ]); - expect(trace.publicStorageWrites).toEqual([ - expect.objectContaining({ storageAddress: contractAddress, slot: slot, value: value }), - expect.objectContaining({ storageAddress: contractAddress, slot: slot, value: valueT1 }), - ]); - expect(trace.newNoteHashes).toEqual([ - expect.objectContaining({ - storageAddress: contractAddress, - noteHash: nullifier, - }), - expect.objectContaining({ - storageAddress: contractAddress, - noteHash: nullifierT1, - }), - ]); - expect(trace.newNullifiers).toEqual([ - expect.objectContaining({ - storageAddress: contractAddress, - nullifier: nullifier, - }), - expect.objectContaining({ - storageAddress: contractAddress, - nullifier: nullifierT1, - }), - ]); - expect(trace.nullifierChecks).toEqual([ - expect.objectContaining({ - nullifier: nullifier, - exists: nullifierExists, - isPending: nullifierIsPending, - leafIndex: nullifierLeafIndex, - }), - expect.objectContaining({ - nullifier: nullifierT1, - exists: nullifierExistsT1, - isPending: nullifierIsPendingT1, - leafIndex: nullifierLeafIndexT1, - }), - ]); - expect(trace.noteHashChecks).toEqual([ - expect.objectContaining({ noteHash: noteHash, exists: noteHashExists, leafIndex: noteHashLeafIndex }), - expect.objectContaining({ noteHash: noteHashT1, exists: noteHashExistsT1, leafIndex: noteHashLeafIndexT1 }), - ]); - expect( - trace.l1ToL2MessageChecks.map(c => ({ - leafIndex: c.leafIndex, - msgHash: c.msgHash, - exists: c.exists, - })), - ).toEqual([expectedMessageCheck, expectedMessageCheckT1]); - expect(trace.l1ToL2MessageChecks).toEqual([ - expect.objectContaining({ leafIndex: msgLeafIndex, msgHash: msgHash, exists: msgExists }), - expect.objectContaining({ leafIndex: msgLeafIndexT1, msgHash: msgHashT1, exists: msgExistsT1 }), - ]); - expect(trace.gotContractInstances).toEqual([instance, instanceT1]); - }); -}); 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 608f738ccc34..000000000000 --- a/yarn-project/simulator/src/avm/journal/trace.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { Fr } from '@aztec/foundation/fields'; - -import { - type TracedContractInstance, - type TracedL1toL2MessageCheck, - type TracedNoteHash, - type TracedNoteHashCheck, - type TracedNullifier, - type TracedNullifierCheck, - type TracedPublicStorageRead, - type TracedPublicStorageWrite, - type TracedUnencryptedL2Log, -} from './trace_types.js'; - -export class WorldStateAccessTrace { - public accessCounter: number; - - public publicStorageReads: TracedPublicStorageRead[] = []; - public publicStorageWrites: TracedPublicStorageWrite[] = []; - - public noteHashChecks: TracedNoteHashCheck[] = []; - public newNoteHashes: TracedNoteHash[] = []; - public nullifierChecks: TracedNullifierCheck[] = []; - public newNullifiers: TracedNullifier[] = []; - public l1ToL2MessageChecks: TracedL1toL2MessageCheck[] = []; - public newLogsHashes: TracedUnencryptedL2Log[] = []; - public gotContractInstances: TracedContractInstance[] = []; - - //public contractCalls: TracedContractCall[] = []; - //public archiveChecks: TracedArchiveLeafCheck[] = []; - - constructor(parentTrace?: WorldStateAccessTrace) { - this.accessCounter = parentTrace ? parentTrace.accessCounter : 0; - // TODO(4805): consider tracking the parent's trace vector lengths so we can enforce limits - } - - public getAccessCounter() { - return this.accessCounter; - } - - public tracePublicStorageRead(storageAddress: Fr, slot: Fr, value: Fr, exists: boolean, cached: boolean) { - // TODO(4805): check if some threshold is reached for max storage reads - // (need access to parent length, or trace needs to be initialized with parent's contents) - const traced: TracedPublicStorageRead = { - // callPointer: Fr.ZERO, - storageAddress, - slot, - value, - exists, - cached, - counter: new Fr(this.accessCounter), - // endLifetime: Fr.ZERO, - }; - this.publicStorageReads.push(traced); - 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, - storageAddress, - slot, - value, - counter: new Fr(this.accessCounter), - // 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.accessCounter), - // 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.accessCounter), - // 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.accessCounter), - // 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.accessCounter), - // 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.accessCounter), - //endLifetime: Fr.ZERO, // FIXME - }; - this.l1ToL2MessageChecks.push(traced); - this.incrementAccessCounter(); - } - - public traceNewLog(logHash: Fr) { - const traced: TracedUnencryptedL2Log = { - logHash, - counter: new Fr(this.accessCounter), - }; - this.newLogsHashes.push(traced); - this.incrementAccessCounter(); - } - - public traceGetContractInstance(instance: TracedContractInstance) { - this.gotContractInstances.push(instance); - this.incrementAccessCounter(); - } - - private incrementAccessCounter() { - this.accessCounter++; - } - - /** - * 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.accessCounter = incomingTrace.accessCounter; - } -} 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 db57e53998ba..000000000000 --- a/yarn-project/simulator/src/avm/journal/trace_types.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { type Fr } from '@aztec/foundation/fields'; -import { type ContractInstanceWithAddress } from '@aztec/types/contracts'; - -//export type TracedContractCall = { -// callPointer: Fr; -// address: Fr; -// storageAddress: Fr; -// endLifetime: Fr; -//}; - -export type TracedPublicStorageRead = { - // callPointer: Fr; - storageAddress: Fr; - exists: boolean; - cached: boolean; - slot: Fr; - value: Fr; - counter: Fr; - // endLifetime: Fr; -}; - -export type TracedPublicStorageWrite = { - // callPointer: Fr; - storageAddress: Fr; - slot: Fr; - value: Fr; - counter: Fr; - // endLifetime: Fr; -}; - -export type TracedNoteHashCheck = { - // callPointer: Fr; - storageAddress: Fr; - leafIndex: Fr; - noteHash: Fr; - exists: boolean; - counter: Fr; - // endLifetime: Fr; -}; - -export type TracedNoteHash = { - // callPointer: Fr; - storageAddress: Fr; - noteHash: Fr; - counter: Fr; - // endLifetime: Fr; -}; - -export type TracedNullifierCheck = { - // callPointer: Fr; - 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 = { - // callPointer: Fr; - storageAddress: Fr; - nullifier: Fr; - counter: Fr; - // endLifetime: Fr; -}; - -export type TracedL1toL2MessageCheck = { - //callPointer: Fr; - leafIndex: Fr; - msgHash: Fr; - exists: boolean; - counter: Fr; - //endLifetime: Fr; -}; - -export type TracedUnencryptedL2Log = { - //callPointer: Fr; - logHash: Fr; - counter: Fr; - //endLifetime: Fr; -}; - -//export type TracedArchiveLeafCheck = { -// leafIndex: Fr; -// leaf: 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..9f71a34a6f62 100644 --- a/yarn-project/simulator/src/avm/opcodes/accrued_substate.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/accrued_substate.test.ts @@ -1,15 +1,20 @@ -import { UnencryptedL2Log } from '@aztec/circuit-types'; -import { EthAddress, Fr } from '@aztec/circuits.js'; -import { EventSelector } from '@aztec/foundation/abi'; +import { Fr } from '@aztec/circuits.js'; import { mock } from 'jest-mock-extended'; -import { type CommitmentsDB } from '../../index.js'; +import { type PublicSideEffectTraceInterface } from '../../public/side_effect_trace_interface.js'; import { type AvmContext } from '../avm_context.js'; import { Field, Uint8, Uint32 } from '../avm_memory_types.js'; import { InstructionExecutionError, StaticCallAlterationError } from '../errors.js'; -import { initContext, initExecutionEnvironment, initHostStorage } from '../fixtures/index.js'; -import { AvmPersistableStateManager } from '../journal/journal.js'; +import { + initContext, + initExecutionEnvironment, + initHostStorage, + initPersistableStateManager, +} from '../fixtures/index.js'; +import { type HostStorage } from '../journal/host_storage.js'; +import { type AvmPersistableStateManager } from '../journal/journal.js'; +import { mockL1ToL2MessageExists, mockNoteHashExists, mockNullifierExists } from '../test_utils.js'; import { EmitNoteHash, EmitNullifier, @@ -21,10 +26,27 @@ import { } from './accrued_substate.js'; describe('Accrued Substate', () => { + let hostStorage: HostStorage; + let trace: PublicSideEffectTraceInterface; + let persistableState: AvmPersistableStateManager; let context: AvmContext; + const address = new Fr(1); + const storageAddress = new Fr(2); + const sender = new Fr(42); + const value0 = new Fr(69); // noteHash or nullifier... + const value0Offset = 100; + const value1 = new Fr(420); + const value1Offset = 200; + const leafIndex = new Fr(7); + const leafIndexOffset = 1; + const existsOffset = 2; + beforeEach(() => { - context = initContext(); + hostStorage = initHostStorage(); + trace = mock(); + persistableState = initPersistableStateManager({ hostStorage, trace }); + context = initContext({ persistableState, env: initExecutionEnvironment({ address, storageAddress, sender }) }); }); describe('NoteHashExists', () => { @@ -47,82 +69,43 @@ describe('Accrued Substate', () => { expect(inst.serialize()).toEqual(buf); }); - it('Should correctly return false when noteHash does not exist', async () => { - const noteHash = new Field(69n); - const noteHashOffset = 0; - const leafIndex = new Field(7n); - const leafIndexOffset = 1; - const existsOffset = 2; - - // mock host storage this so that persistable state's getCommitmentIndex returns UNDEFINED - const commitmentsDb = mock(); - commitmentsDb.getCommitmentIndex.mockResolvedValue(Promise.resolve(undefined)); - const hostStorage = initHostStorage({ commitmentsDb }); - context = initContext({ persistableState: new AvmPersistableStateManager(hostStorage) }); - - context.machineState.memory.set(noteHashOffset, noteHash); - context.machineState.memory.set(leafIndexOffset, leafIndex); - await new NoteHashExists(/*indirect=*/ 0, noteHashOffset, leafIndexOffset, existsOffset).execute(context); - - const exists = context.machineState.memory.getAs(existsOffset); - expect(exists).toEqual(new Uint8(0)); - - const journalState = context.persistableState.flush(); - expect(journalState.noteHashChecks).toEqual([ - expect.objectContaining({ exists: false, leafIndex: leafIndex.toFr(), noteHash: noteHash.toFr() }), - ]); - }); - - it('Should correctly return false when note hash exists at a different leaf index', async () => { - const noteHash = new Field(69n); - const noteHashOffset = 0; - const leafIndex = new Field(7n); - const storedLeafIndex = 88n; - const leafIndexOffset = 1; - const existsOffset = 2; - - const commitmentsDb = mock(); - commitmentsDb.getCommitmentIndex.mockResolvedValue(Promise.resolve(storedLeafIndex)); - const hostStorage = initHostStorage({ commitmentsDb }); - context = initContext({ persistableState: new AvmPersistableStateManager(hostStorage) }); - - context.machineState.memory.set(noteHashOffset, noteHash); - context.machineState.memory.set(leafIndexOffset, leafIndex); - await new NoteHashExists(/*indirect=*/ 0, noteHashOffset, leafIndexOffset, existsOffset).execute(context); - - const exists = context.machineState.memory.getAs(existsOffset); - expect(exists).toEqual(new Uint8(0)); - - const journalState = context.persistableState.flush(); - expect(journalState.noteHashChecks).toEqual([ - expect.objectContaining({ exists: false, leafIndex: leafIndex.toFr(), noteHash: noteHash.toFr() }), - ]); - }); - - it('Should correctly return true when note hash exists at the given leaf index', async () => { - const noteHash = new Field(69n); - const noteHashOffset = 0; - const leafIndex = new Field(7n); - const storedLeafIndex = 7n; - const leafIndexOffset = 1; - const existsOffset = 2; - - const commitmentsDb = mock(); - commitmentsDb.getCommitmentIndex.mockResolvedValue(Promise.resolve(storedLeafIndex)); - const hostStorage = initHostStorage({ commitmentsDb }); - context = initContext({ persistableState: new AvmPersistableStateManager(hostStorage) }); - - context.machineState.memory.set(noteHashOffset, noteHash); - context.machineState.memory.set(leafIndexOffset, leafIndex); - await new NoteHashExists(/*indirect=*/ 0, noteHashOffset, leafIndexOffset, existsOffset).execute(context); - - const exists = context.machineState.memory.getAs(existsOffset); - expect(exists).toEqual(new Uint8(1)); - - const journalState = context.persistableState.flush(); - expect(journalState.noteHashChecks).toEqual([ - expect.objectContaining({ exists: true, leafIndex: leafIndex.toFr(), noteHash: noteHash.toFr() }), - ]); + // Will check existence at leafIndex, but nothing may be found there and/or something may be found at mockAtLeafIndex + describe.each([ + [/*mockAtLeafIndex=*/ undefined], // doesn't exist at all + [/*mockAtLeafIndex=*/ leafIndex], // should be found! + [/*mockAtLeafIndex=*/ leafIndex.add(Fr.ONE)], // won't be found! (checking leafIndex+1, but it exists at leafIndex) + ])('Note hash checks', (mockAtLeafIndex?: Fr) => { + const expectFound = mockAtLeafIndex !== undefined && mockAtLeafIndex.equals(leafIndex); + const existsElsewhere = mockAtLeafIndex !== undefined && !mockAtLeafIndex.equals(leafIndex); + const existsStr = expectFound ? 'DOES exist' : 'does NOT exist'; + const foundAtStr = existsElsewhere + ? `at leafIndex=${mockAtLeafIndex.toNumber()} (exists at leafIndex=${leafIndex.toNumber()})` + : ''; + it(`Should return ${expectFound} (and be traced) when noteHash ${existsStr} ${foundAtStr}`, async () => { + if (mockAtLeafIndex !== undefined) { + mockNoteHashExists(hostStorage, mockAtLeafIndex, value0); + } + + context.machineState.memory.set(value0Offset, new Field(value0)); // noteHash + context.machineState.memory.set(leafIndexOffset, new Field(leafIndex)); + await new NoteHashExists( + /*indirect=*/ 0, + /*noteHashOffset=*/ value0Offset, + leafIndexOffset, + existsOffset, + ).execute(context); + + const gotExists = context.machineState.memory.getAs(existsOffset); + expect(gotExists).toEqual(new Uint8(expectFound ? 1 : 0)); + + expect(trace.traceNoteHashCheck).toHaveBeenCalledTimes(1); + expect(trace.traceNoteHashCheck).toHaveBeenCalledWith( + storageAddress, + /*noteHash=*/ value0, + leafIndex, + /*exists=*/ expectFound, + ); + }); }); }); @@ -140,18 +123,13 @@ describe('Accrued Substate', () => { }); it('Should append a new note hash correctly', async () => { - const value = new Field(69n); - context.machineState.memory.set(0, value); - - await new EmitNoteHash(/*indirect=*/ 0, /*offset=*/ 0).execute(context); - - const journalState = context.persistableState.flush(); - expect(journalState.newNoteHashes).toEqual([ - expect.objectContaining({ - storageAddress: context.environment.storageAddress, - noteHash: value.toFr(), - }), - ]); + context.machineState.memory.set(value0Offset, new Field(value0)); + await new EmitNoteHash(/*indirect=*/ 0, /*offset=*/ value0Offset).execute(context); + expect(trace.traceNewNoteHash).toHaveBeenCalledTimes(1); + expect(trace.traceNewNoteHash).toHaveBeenCalledWith( + expect.objectContaining(storageAddress), + /*noteHash=*/ value0, + ); }); }); @@ -175,57 +153,39 @@ describe('Accrued Substate', () => { expect(inst.serialize()).toEqual(buf); }); - it('Should correctly show false when nullifier does not exist', async () => { - const value = new Field(69n); - const nullifierOffset = 0; - const addressOffset = 1; - const existsOffset = 2; - - // mock host storage this so that persistable state's checkNullifierExists returns UNDEFINED - const commitmentsDb = mock(); - commitmentsDb.getNullifierIndex.mockResolvedValue(Promise.resolve(undefined)); - const hostStorage = initHostStorage({ commitmentsDb }); - context = initContext({ persistableState: new AvmPersistableStateManager(hostStorage) }); - const address = new Field(context.environment.storageAddress.toField()); - - context.machineState.memory.set(nullifierOffset, value); - context.machineState.memory.set(addressOffset, address); - await new NullifierExists(/*indirect=*/ 0, nullifierOffset, addressOffset, existsOffset).execute(context); - - const exists = context.machineState.memory.getAs(existsOffset); - expect(exists).toEqual(new Uint8(0)); - - const journalState = context.persistableState.flush(); - expect(journalState.nullifierChecks).toEqual([ - expect.objectContaining({ nullifier: value.toFr(), storageAddress: address.toFr(), exists: false }), - ]); - }); - - it('Should correctly show true when nullifier exists', async () => { - const value = new Field(69n); - const nullifierOffset = 0; - const addressOffset = 1; - const existsOffset = 2; - const storedLeafIndex = BigInt(42); - - // mock host storage this so that persistable state's checkNullifierExists returns true - const commitmentsDb = mock(); - commitmentsDb.getNullifierIndex.mockResolvedValue(Promise.resolve(storedLeafIndex)); - const hostStorage = initHostStorage({ commitmentsDb }); - context = initContext({ persistableState: new AvmPersistableStateManager(hostStorage) }); - const address = new Field(context.environment.storageAddress.toField()); - - context.machineState.memory.set(nullifierOffset, value); - context.machineState.memory.set(addressOffset, address); - await new NullifierExists(/*indirect=*/ 0, nullifierOffset, addressOffset, existsOffset).execute(context); - - const exists = context.machineState.memory.getAs(existsOffset); - expect(exists).toEqual(new Uint8(1)); - - const journalState = context.persistableState.flush(); - expect(journalState.nullifierChecks).toEqual([ - expect.objectContaining({ nullifier: value.toFr(), storageAddress: address.toFr(), exists: true }), - ]); + describe.each([[/*exists=*/ false], [/*exists=*/ true]])('Nullifier checks', (exists: boolean) => { + const existsStr = exists ? 'DOES exist' : 'does NOT exist'; + it(`Should return ${exists} (and be traced) when noteHash ${existsStr}`, async () => { + const storageAddressOffset = 1; + + if (exists) { + mockNullifierExists(hostStorage, leafIndex, value0); + } + + context.machineState.memory.set(value0Offset, new Field(value0)); // nullifier + context.machineState.memory.set(storageAddressOffset, new Field(storageAddress)); + await new NullifierExists( + /*indirect=*/ 0, + /*nullifierOffset=*/ value0Offset, + storageAddressOffset, + existsOffset, + ).execute(context); + + const gotExists = context.machineState.memory.getAs(existsOffset); + expect(gotExists).toEqual(new Uint8(exists ? 1 : 0)); + + expect(trace.traceNullifierCheck).toHaveBeenCalledTimes(1); + const isPending = false; + // leafIndex is returned from DB call for nullifiers, so it is absent on DB miss + const tracedLeafIndex = exists && !isPending ? leafIndex : Fr.ZERO; + expect(trace.traceNullifierCheck).toHaveBeenCalledWith( + storageAddress, + value0, + tracedLeafIndex, + exists, + isPending, + ); + }); }); }); @@ -243,52 +203,39 @@ describe('Accrued Substate', () => { }); it('Should append a new nullifier correctly', async () => { - const value = new Field(69n); - context.machineState.memory.set(0, value); - - await new EmitNullifier(/*indirect=*/ 0, /*offset=*/ 0).execute(context); - - const journalState = context.persistableState.flush(); - expect(journalState.newNullifiers).toEqual([ - expect.objectContaining({ - storageAddress: context.environment.storageAddress.toField(), - nullifier: value.toFr(), - }), - ]); + context.machineState.memory.set(value0Offset, new Field(value0)); + await new EmitNullifier(/*indirect=*/ 0, /*offset=*/ value0Offset).execute(context); + expect(trace.traceNewNullifier).toHaveBeenCalledTimes(1); + expect(trace.traceNewNullifier).toHaveBeenCalledWith( + expect.objectContaining(storageAddress), + /*nullifier=*/ value0, + ); }); it('Nullifier collision reverts (same nullifier emitted twice)', async () => { - const value = new Field(69n); - context.machineState.memory.set(0, value); - - await new EmitNullifier(/*indirect=*/ 0, /*offset=*/ 0).execute(context); - await expect(new EmitNullifier(/*indirect=*/ 0, /*offset=*/ 0).execute(context)).rejects.toThrow( + context.machineState.memory.set(value0Offset, new Field(value0)); + await new EmitNullifier(/*indirect=*/ 0, /*offset=*/ value0Offset).execute(context); + await expect(new EmitNullifier(/*indirect=*/ 0, /*offset=*/ value0Offset).execute(context)).rejects.toThrow( new InstructionExecutionError( - `Attempted to emit duplicate nullifier ${value.toFr()} (storage address: ${ - context.environment.storageAddress - }).`, + `Attempted to emit duplicate nullifier ${value0} (storage address: ${storageAddress}).`, ), ); + expect(trace.traceNewNullifier).toHaveBeenCalledTimes(1); + expect(trace.traceNewNullifier).toHaveBeenCalledWith( + expect.objectContaining(storageAddress), + /*nullifier=*/ value0, + ); }); it('Nullifier collision reverts (nullifier exists in host state)', async () => { - const value = new Field(69n); - const storedLeafIndex = BigInt(42); - - // Mock the nullifiers db to return a stored leaf index - const commitmentsDb = mock(); - commitmentsDb.getNullifierIndex.mockResolvedValue(Promise.resolve(storedLeafIndex)); - const hostStorage = initHostStorage({ commitmentsDb }); - context = initContext({ persistableState: new AvmPersistableStateManager(hostStorage) }); - - context.machineState.memory.set(0, value); - await expect(new EmitNullifier(/*indirect=*/ 0, /*offset=*/ 0).execute(context)).rejects.toThrow( + mockNullifierExists(hostStorage, leafIndex); // db will say that nullifier already exists + context.machineState.memory.set(value0Offset, new Field(value0)); + await expect(new EmitNullifier(/*indirect=*/ 0, /*offset=*/ value0Offset).execute(context)).rejects.toThrow( new InstructionExecutionError( - `Attempted to emit duplicate nullifier ${value.toFr()} (storage address: ${ - context.environment.storageAddress - }).`, + `Attempted to emit duplicate nullifier ${value0} (storage address: ${storageAddress}).`, ), ); + expect(trace.traceNewNullifier).toHaveBeenCalledTimes(0); // the only attempt should fail before tracing }); }); @@ -312,77 +259,44 @@ describe('Accrued Substate', () => { expect(inst.serialize()).toEqual(buf); }); - it('Should correctly show false when L1ToL2 message does not exist', async () => { - const msgHash = new Field(69n); - const leafIndex = new Field(42n); - const msgHashOffset = 0; - const msgLeafIndexOffset = 1; - const existsOffset = 2; - - context.machineState.memory.set(msgHashOffset, msgHash); - context.machineState.memory.set(msgLeafIndexOffset, leafIndex); - await new L1ToL2MessageExists(/*indirect=*/ 0, msgHashOffset, msgLeafIndexOffset, existsOffset).execute(context); - - // never created, doesn't exist! - const exists = context.machineState.memory.getAs(existsOffset); - expect(exists).toEqual(new Uint8(0)); - - const journalState = context.persistableState.flush(); - expect(journalState.l1ToL2MessageChecks).toEqual([ - expect.objectContaining({ leafIndex: leafIndex.toFr(), msgHash: msgHash.toFr(), exists: false }), - ]); - }); - - it('Should correctly show true when L1ToL2 message exists', async () => { - const msgHash = new Field(69n); - const leafIndex = new Field(42n); - const msgHashOffset = 0; - const msgLeafIndexOffset = 1; - const existsOffset = 2; - - // mock commitments db to show message exists - const commitmentsDb = mock(); - commitmentsDb.getL1ToL2LeafValue.mockResolvedValue(msgHash.toFr()); - const hostStorage = initHostStorage({ commitmentsDb }); - context = initContext({ persistableState: new AvmPersistableStateManager(hostStorage) }); - - context.machineState.memory.set(msgHashOffset, msgHash); - context.machineState.memory.set(msgLeafIndexOffset, leafIndex); - await new L1ToL2MessageExists(/*indirect=*/ 0, msgHashOffset, msgLeafIndexOffset, existsOffset).execute(context); - - const exists = context.machineState.memory.getAs(existsOffset); - expect(exists).toEqual(new Uint8(1)); - - const journalState = context.persistableState.flush(); - expect(journalState.l1ToL2MessageChecks).toEqual([ - expect.objectContaining({ leafIndex: leafIndex.toFr(), msgHash: msgHash.toFr(), exists: true }), - ]); - }); - - it('Should correctly show false when another L1ToL2 message exists at that index', async () => { - const msgHash = new Field(69n); - const leafIndex = new Field(42n); - const msgHashOffset = 0; - const msgLeafIndexOffset = 1; - const existsOffset = 2; - - const commitmentsDb = mock(); - commitmentsDb.getL1ToL2LeafValue.mockResolvedValue(Fr.ZERO); - const hostStorage = initHostStorage({ commitmentsDb }); - context = initContext({ persistableState: new AvmPersistableStateManager(hostStorage) }); - - context.machineState.memory.set(msgHashOffset, msgHash); - context.machineState.memory.set(msgLeafIndexOffset, leafIndex); - await new L1ToL2MessageExists(/*indirect=*/ 0, msgHashOffset, msgLeafIndexOffset, existsOffset).execute(context); - - // never created, doesn't exist! - const exists = context.machineState.memory.getAs(existsOffset); - expect(exists).toEqual(new Uint8(0)); - - const journalState = context.persistableState.flush(); - expect(journalState.l1ToL2MessageChecks).toEqual([ - expect.objectContaining({ leafIndex: leafIndex.toFr(), msgHash: msgHash.toFr(), exists: false }), - ]); + // Will check existence at leafIndex, but nothing may be found there and/or something may be found at mockAtLeafIndex + describe.each([ + [/*mockAtLeafIndex=*/ undefined], // doesn't exist at all + [/*mockAtLeafIndex=*/ leafIndex], // should be found! + [/*mockAtLeafIndex=*/ leafIndex.add(Fr.ONE)], // won't be found! (checking leafIndex+1, but it exists at leafIndex) + ])('L1ToL2 message checks', (mockAtLeafIndex?: Fr) => { + const expectFound = mockAtLeafIndex !== undefined && mockAtLeafIndex.equals(leafIndex); + const existsElsewhere = mockAtLeafIndex !== undefined && !mockAtLeafIndex.equals(leafIndex); + const existsStr = expectFound ? 'DOES exist' : 'does NOT exist'; + const foundAtStr = existsElsewhere + ? `at leafIndex=${mockAtLeafIndex.toNumber()} (exists at leafIndex=${leafIndex.toNumber()})` + : ''; + + it(`Should return ${expectFound} (and be traced) when noteHash ${existsStr} ${foundAtStr}`, async () => { + if (mockAtLeafIndex !== undefined) { + mockL1ToL2MessageExists(hostStorage, mockAtLeafIndex, value0, /*valueAtOtherIndices=*/ value1); + } + + context.machineState.memory.set(value0Offset, new Field(value0)); // noteHash + context.machineState.memory.set(leafIndexOffset, new Field(leafIndex)); + await new L1ToL2MessageExists( + /*indirect=*/ 0, + /*msgHashOffset=*/ value0Offset, + leafIndexOffset, + existsOffset, + ).execute(context); + + const gotExists = context.machineState.memory.getAs(existsOffset); + expect(gotExists).toEqual(new Uint8(expectFound ? 1 : 0)); + + expect(trace.traceL1ToL2MessageCheck).toHaveBeenCalledTimes(1); + expect(trace.traceL1ToL2MessageCheck).toHaveBeenCalledWith( + address, + /*noteHash=*/ value0, + leafIndex, + /*exists=*/ expectFound, + ); + }); }); }); @@ -408,12 +322,15 @@ describe('Accrued Substate', () => { it('Should append unencrypted logs correctly', async () => { const startOffset = 0; - const eventSelector = 5; + const eventSelector = new Fr(5); const eventSelectorOffset = 10; const logSizeOffset = 20; - const values = [new Field(69n), new Field(420n), new Field(Field.MODULUS - 1n)]; - context.machineState.memory.setSlice(startOffset, values); + const values = [new Fr(69n), new Fr(420n), new Fr(Fr.MODULUS - 1n)]; + context.machineState.memory.setSlice( + startOffset, + values.map(f => new Field(f)), + ); context.machineState.memory.set(eventSelectorOffset, new Field(eventSelector)); context.machineState.memory.set(logSizeOffset, new Uint32(values.length)); @@ -424,11 +341,8 @@ describe('Accrued Substate', () => { logSizeOffset, ).execute(context); - const journalState = context.persistableState.flush(); - const expectedLog = Buffer.concat(values.map(v => v.toFr().toBuffer())); - expect(journalState.newLogs).toEqual([ - new UnencryptedL2Log(context.environment.address, new EventSelector(eventSelector), expectedLog), - ]); + expect(trace.traceUnencryptedLog).toHaveBeenCalledTimes(1); + expect(trace.traceUnencryptedLog).toHaveBeenCalledWith(address, eventSelector, values); }); }); @@ -450,25 +364,18 @@ describe('Accrued Substate', () => { expect(inst.serialize()).toEqual(buf); }); - it('Should append l2 to l1 messages correctly', async () => { - const recipientOffset = 0; - const recipient = new Fr(42); - const contentOffset = 1; - const content = new Fr(69); - - context.machineState.memory.set(recipientOffset, new Field(recipient)); - context.machineState.memory.set(contentOffset, new Field(content)); - + it('Should append l2 to l1 message correctly', async () => { + // recipient: value0 + // content: value1 + context.machineState.memory.set(value0Offset, new Field(value0)); + context.machineState.memory.set(value1Offset, new Field(value1)); await new SendL2ToL1Message( /*indirect=*/ 0, - /*recipientOffset=*/ recipientOffset, - /*contentOffset=*/ contentOffset, + /*recipientOffset=*/ value0Offset, + /*contentOffset=*/ value1Offset, ).execute(context); - - const journalState = context.persistableState.flush(); - expect(journalState.newL1Messages).toEqual([ - expect.objectContaining({ recipient: EthAddress.fromField(recipient), content }), - ]); + expect(trace.traceNewL2ToL1Message).toHaveBeenCalledTimes(1); + expect(trace.traceNewL2ToL1Message).toHaveBeenCalledWith(/*recipient=*/ value0, /*content=*/ value1); }); }); diff --git a/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts b/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts index c227710208fd..97a21cf14409 100644 --- a/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts +++ b/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts @@ -201,7 +201,11 @@ 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 +256,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 +289,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/contract.test.ts b/yarn-project/simulator/src/avm/opcodes/contract.test.ts index 105d9ef579c5..ced3a000d64f 100644 --- a/yarn-project/simulator/src/avm/opcodes/contract.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/contract.test.ts @@ -1,21 +1,31 @@ -import { AztecAddress, Fr } from '@aztec/circuits.js'; -import { type ContractInstanceWithAddress } from '@aztec/types/contracts'; +import { randomContractInstanceWithAddress } from '@aztec/circuit-types'; +import { AztecAddress } from '@aztec/circuits.js'; +import { SerializableContractInstance } from '@aztec/types/contracts'; import { mock } from 'jest-mock-extended'; -import { type PublicContractsDB } from '../../public/db_interfaces.js'; +import { type PublicSideEffectTraceInterface } from '../../public/side_effect_trace_interface.js'; import { type AvmContext } from '../avm_context.js'; import { Field } from '../avm_memory_types.js'; -import { initContext, initHostStorage } from '../fixtures/index.js'; -import { AvmPersistableStateManager } from '../journal/journal.js'; +import { initContext, initHostStorage, initPersistableStateManager } from '../fixtures/index.js'; +import { type HostStorage } from '../journal/host_storage.js'; +import { type AvmPersistableStateManager } from '../journal/journal.js'; +import { mockGetContractInstance } from '../test_utils.js'; import { GetContractInstance } from './contract.js'; describe('Contract opcodes', () => { - let context: AvmContext; const address = AztecAddress.random(); - beforeEach(async () => { - context = initContext(); + let hostStorage: HostStorage; + let trace: PublicSideEffectTraceInterface; + let persistableState: AvmPersistableStateManager; + let context: AvmContext; + + beforeEach(() => { + hostStorage = initHostStorage(); + trace = mock(); + persistableState = initPersistableStateManager({ hostStorage, trace }); + context = initContext({ persistableState }); }); describe('GETCONTRACTINSTANCE', () => { @@ -37,22 +47,10 @@ describe('Contract opcodes', () => { }); it('should copy contract instance to memory if found', async () => { - context.machineState.memory.set(0, new Field(address.toField())); - - const contractInstance = { - address: address, - version: 1 as const, - salt: new Fr(20), - contractClassId: new Fr(30), - initializationHash: new Fr(40), - publicKeysHash: new Fr(50), - deployer: AztecAddress.random(), - } as ContractInstanceWithAddress; - - const contractsDb = mock(); - contractsDb.getContractInstance.mockResolvedValue(Promise.resolve(contractInstance)); - context.persistableState = new AvmPersistableStateManager(initHostStorage({ contractsDb })); + const contractInstance = randomContractInstanceWithAddress(/*(base instance) opts=*/ {}, /*address=*/ address); + mockGetContractInstance(hostStorage, contractInstance); + context.machineState.memory.set(0, new Field(address.toField())); await new GetContractInstance(/*indirect=*/ 0, /*addressOffset=*/ 0, /*dstOffset=*/ 1).execute(context); const actual = context.machineState.memory.getSlice(1, 6); @@ -64,9 +62,13 @@ describe('Contract opcodes', () => { new Field(contractInstance.initializationHash), new Field(contractInstance.publicKeysHash), ]); + + expect(trace.traceGetContractInstance).toHaveBeenCalledTimes(1); + expect(trace.traceGetContractInstance).toHaveBeenCalledWith({ exists: true, ...contractInstance }); }); it('should return zeroes if not found', async () => { + const emptyContractInstance = SerializableContractInstance.empty().withAddress(address); context.machineState.memory.set(0, new Field(address.toField())); await new GetContractInstance(/*indirect=*/ 0, /*addressOffset=*/ 0, /*dstOffset=*/ 1).execute(context); @@ -80,6 +82,9 @@ describe('Contract opcodes', () => { new Field(0), new Field(0), ]); + + expect(trace.traceGetContractInstance).toHaveBeenCalledTimes(1); + expect(trace.traceGetContractInstance).toHaveBeenCalledWith({ exists: false, ...emptyContractInstance }); }); }); }); 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..19da62cc3a19 100644 --- a/yarn-project/simulator/src/avm/opcodes/external_calls.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/external_calls.test.ts @@ -1,16 +1,16 @@ import { Fr } from '@aztec/foundation/fields'; -import { jest } from '@jest/globals'; import { mock } from 'jest-mock-extended'; -import { type CommitmentsDB, type PublicContractsDB, type PublicStateDB } from '../../index.js'; +import { type PublicSideEffectTraceInterface } from '../../public/side_effect_trace_interface.js'; import { markBytecodeAsAvm } from '../../public/transitional_adaptors.js'; import { type AvmContext } from '../avm_context.js'; import { Field, Uint8, Uint32 } from '../avm_memory_types.js'; -import { adjustCalldataIndex, initContext } from '../fixtures/index.js'; -import { HostStorage } from '../journal/host_storage.js'; -import { AvmPersistableStateManager } from '../journal/journal.js'; +import { adjustCalldataIndex, initContext, initHostStorage, initPersistableStateManager } from '../fixtures/index.js'; +import { type HostStorage } from '../journal/host_storage.js'; +import { type AvmPersistableStateManager } from '../journal/journal.js'; import { encodeToBytecode } from '../serialization/bytecode_serialization.js'; +import { mockGetBytecode, mockTraceFork } from '../test_utils.js'; import { L2GasLeft } from './context_getters.js'; import { Call, Return, Revert, StaticCall } from './external_calls.js'; import { type Instruction } from './instruction.js'; @@ -19,14 +19,16 @@ import { SStore } from './storage.js'; describe('External Calls', () => { let context: AvmContext; + let hostStorage: HostStorage; + let trace: PublicSideEffectTraceInterface; + let persistableState: AvmPersistableStateManager; beforeEach(() => { - const contractsDb = mock(); - const commitmentsDb = mock(); - const publicStateDb = mock(); - const hostStorage = new HostStorage(publicStateDb, contractsDb, commitmentsDb); - const journal = new AvmPersistableStateManager(hostStorage); - context = initContext({ persistableState: journal }); + hostStorage = initHostStorage(); + trace = mock(); + persistableState = initPersistableStateManager({ hostStorage, trace }); + context = initContext({ persistableState: persistableState }); + mockTraceFork(trace); // make sure trace.fork() works on nested call }); describe('Call', () => { @@ -66,11 +68,16 @@ describe('External Calls', () => { const addrOffset = 2; const addr = new Fr(123456n); const argsOffset = 3; - const args = [new Field(1n), new Field(2n), new Field(3n)]; + const valueToStore = new Fr(42); + const valueOffset = 0; // 0th entry in calldata to nested call + const slot = new Fr(100); + const slotOffset = 1; // 1st entry in calldata to nested call + const args = [new Field(valueToStore), new Field(slot), new Field(3n)]; const argsSize = args.length; const argsSizeOffset = 20; const retOffset = 7; const retSize = 2; + const expectedRetValue = args.slice(0, retSize); const successOffset = 6; // const otherContextInstructionsL2GasCost = 780; // Includes the cost of the call itself @@ -82,10 +89,11 @@ describe('External Calls', () => { /*copySize=*/ argsSize, /*dstOffset=*/ 0, ), - new SStore(/*indirect=*/ 0, /*srcOffset=*/ 0, /*size=*/ 1, /*slotOffset=*/ 0), + new SStore(/*indirect=*/ 0, /*srcOffset=*/ valueOffset, /*size=*/ 1, /*slotOffset=*/ slotOffset), new Return(/*indirect=*/ 0, /*retOffset=*/ 0, /*size=*/ 2), ]), ); + mockGetBytecode(hostStorage, otherContextInstructionsBytecode); const { l2GasLeft: initialL2Gas, daGasLeft: initialDaGas } = context.machineState; @@ -94,9 +102,6 @@ describe('External Calls', () => { context.machineState.memory.set(2, new Field(addr)); context.machineState.memory.set(argsSizeOffset, new Uint32(argsSize)); context.machineState.memory.setSlice(3, args); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValue(Promise.resolve(otherContextInstructionsBytecode)); const instruction = new Call( /*indirect=*/ 0, @@ -115,18 +120,10 @@ describe('External Calls', () => { expect(successValue).toEqual(new Uint8(1n)); const retValue = context.machineState.memory.getSlice(retOffset, retSize); - expect(retValue).toEqual([new Field(1n), new Field(2n)]); + expect(retValue).toEqual(expectedRetValue); // Check that the storage call has been merged into the parent journal - const { currentStorageValue } = context.persistableState.flush(); - expect(currentStorageValue.size).toEqual(1); - - const nestedContractWrites = currentStorageValue.get(addr.toBigInt()); - expect(nestedContractWrites).toBeDefined(); - - const slotNumber = 1n; - const expectedStoredValue = new Fr(1n); - expect(nestedContractWrites!.get(slotNumber)).toEqual(expectedStoredValue); + expect(await context.persistableState.peekStorage(addr, slot)).toEqual(valueToStore); expect(context.machineState.l2GasLeft).toBeLessThan(initialL2Gas); expect(context.machineState.daGasLeft).toEqual(initialDaGas); @@ -150,6 +147,7 @@ describe('External Calls', () => { new Return(/*indirect=*/ 0, /*retOffset=*/ 0, /*size=*/ 1), ]), ); + mockGetBytecode(hostStorage, otherContextInstructionsBytecode); const { l2GasLeft: initialL2Gas, daGasLeft: initialDaGas } = context.machineState; @@ -157,9 +155,6 @@ describe('External Calls', () => { context.machineState.memory.set(1, new Field(daGas)); context.machineState.memory.set(2, new Field(addr)); context.machineState.memory.set(argsSizeOffset, new Uint32(argsSize)); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValue(Promise.resolve(otherContextInstructionsBytecode)); const instruction = new Call( /*indirect=*/ 0, @@ -239,10 +234,7 @@ describe('External Calls', () => { ]; const otherContextInstructionsBytecode = markBytecodeAsAvm(encodeToBytecode(otherContextInstructions)); - - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValue(Promise.resolve(otherContextInstructionsBytecode)); + mockGetBytecode(hostStorage, otherContextInstructionsBytecode); const instruction = new StaticCall( /*indirect=*/ 0, diff --git a/yarn-project/simulator/src/avm/opcodes/external_calls.ts b/yarn-project/simulator/src/avm/opcodes/external_calls.ts index 20f72557b3ce..3830d4db0e98 100644 --- a/yarn-project/simulator/src/avm/opcodes/external_calls.ts +++ b/yarn-project/simulator/src/avm/opcodes/external_calls.ts @@ -1,7 +1,6 @@ import { FunctionSelector, Gas } from '@aztec/circuits.js'; import { padArrayEnd } from '@aztec/foundation/collection'; -import { convertAvmResultsToPxResult, createPublicExecution } 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'; @@ -24,7 +23,6 @@ abstract class ExternalCall extends Instruction { OperandType.UINT32, OperandType.UINT32, OperandType.UINT32, - /* temporary function selector */ OperandType.UINT32, ]; @@ -37,8 +35,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 +79,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,38 +86,9 @@ abstract class ExternalCall extends Instruction { callType, FunctionSelector.fromField(functionSelector), ); - const startSideEffectCounter = nestedContext.persistableState.trace.accessCounter; - const oldStyleExecution = createPublicExecution(startSideEffectCounter, nestedContext.environment, calldata); const simulator = new AvmSimulator(nestedContext); const nestedCallResults: AvmContractCallResults = await simulator.execute(); - const functionName = - (await nestedContext.persistableState.hostStorage.contractsDb.getDebugFunctionName( - nestedContext.environment.address, - nestedContext.environment.temporaryFunctionSelector, - )) ?? `${nestedContext.environment.address}:${nestedContext.environment.temporaryFunctionSelector}`; - const pxResults = convertAvmResultsToPxResult( - nestedCallResults, - startSideEffectCounter, - oldStyleExecution, - Gas.from(allocatedGas), - nestedContext, - simulator.getBytecode(), - functionName, - ); - // 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. @@ -149,12 +117,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 + await 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/avm/opcodes/storage.test.ts b/yarn-project/simulator/src/avm/opcodes/storage.test.ts index 2bd18ebc1972..7ddaa9cb5bbe 100644 --- a/yarn-project/simulator/src/avm/opcodes/storage.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/storage.test.ts @@ -12,13 +12,13 @@ import { SLoad, SStore } from './storage.js'; describe('Storage Instructions', () => { let context: AvmContext; - let journal: MockProxy; + let persistableState: MockProxy; const address = AztecAddress.random(); beforeEach(async () => { - journal = mock(); + persistableState = mock(); context = initContext({ - persistableState: journal, + persistableState: persistableState, env: initExecutionEnvironment({ address, storageAddress: address }), }); }); @@ -52,12 +52,12 @@ describe('Storage Instructions', () => { await new SStore(/*indirect=*/ 0, /*srcOffset=*/ 1, /*size=*/ 1, /*slotOffset=*/ 0).execute(context); - expect(journal.writeStorage).toHaveBeenCalledWith(address, new Fr(a.toBigInt()), new Fr(b.toBigInt())); + expect(persistableState.writeStorage).toHaveBeenCalledWith(address, new Fr(a.toBigInt()), new Fr(b.toBigInt())); }); it('Should not be able to write to storage in a static call', async () => { context = initContext({ - persistableState: journal, + persistableState: persistableState, env: initExecutionEnvironment({ address, storageAddress: address, isStaticCall: true }), }); @@ -96,7 +96,7 @@ describe('Storage Instructions', () => { it('Sload should Read into storage', async () => { // Mock response const expectedResult = new Fr(1n); - journal.readStorage.mockReturnValueOnce(Promise.resolve(expectedResult)); + persistableState.readStorage.mockResolvedValueOnce(expectedResult); const a = new Field(1n); const b = new Field(2n); @@ -106,7 +106,7 @@ describe('Storage Instructions', () => { await new SLoad(/*indirect=*/ 0, /*slotOffset=*/ 0, /*size=*/ 1, /*dstOffset=*/ 1).execute(context); - expect(journal.readStorage).toHaveBeenCalledWith(address, new Fr(a.toBigInt())); + expect(persistableState.readStorage).toHaveBeenCalledWith(address, new Fr(a.toBigInt())); const actual = context.machineState.memory.get(1); expect(actual).toEqual(new Field(expectedResult)); diff --git a/yarn-project/simulator/src/avm/test_utils.ts b/yarn-project/simulator/src/avm/test_utils.ts new file mode 100644 index 000000000000..ce65116d5b87 --- /dev/null +++ b/yarn-project/simulator/src/avm/test_utils.ts @@ -0,0 +1,53 @@ +import { Fr } from '@aztec/circuits.js'; +import { type ContractInstanceWithAddress } from '@aztec/types/contracts'; + +import { type jest } from '@jest/globals'; +import { mock } from 'jest-mock-extended'; + +import { type CommitmentsDB, type PublicContractsDB, type PublicStateDB } from '../public/db_interfaces.js'; +import { type PublicSideEffectTraceInterface } from '../public/side_effect_trace_interface.js'; +import { type HostStorage } from './journal/host_storage.js'; + +export function mockGetBytecode(hs: HostStorage, bytecode: Buffer) { + (hs as jest.Mocked).contractsDb.getBytecode.mockResolvedValue(bytecode); +} + +export function mockTraceFork(trace: PublicSideEffectTraceInterface, nestedTrace?: PublicSideEffectTraceInterface) { + (trace as jest.Mocked).fork.mockReturnValue( + nestedTrace ?? mock(), + ); +} + +export function mockStorageRead(hs: HostStorage, value: Fr) { + (hs.publicStateDb as jest.Mocked).storageRead.mockResolvedValue(value); +} + +export function mockStorageReadWithMap(hs: HostStorage, mockedStorage: Map) { + (hs.publicStateDb as jest.Mocked).storageRead.mockImplementation((_address, slot) => + Promise.resolve(mockedStorage.get(slot.toBigInt()) ?? Fr.ZERO), + ); +} + +export function mockNoteHashExists(hs: HostStorage, leafIndex: Fr, _value?: Fr) { + (hs.commitmentsDb as jest.Mocked).getCommitmentIndex.mockResolvedValue(leafIndex.toBigInt()); +} + +export function mockNullifierExists(hs: HostStorage, leafIndex: Fr, _value?: Fr) { + (hs.commitmentsDb as jest.Mocked).getNullifierIndex.mockResolvedValue(leafIndex.toBigInt()); +} + +export function mockL1ToL2MessageExists(hs: HostStorage, leafIndex: Fr, value: Fr, valueAtOtherIndices?: Fr) { + (hs.commitmentsDb as jest.Mocked).getL1ToL2LeafValue.mockImplementation((index: bigint) => { + if (index == leafIndex.toBigInt()) { + return Promise.resolve(value); + } else { + // any indices other than mockAtLeafIndex will return a different value + // (or undefined if no value is specified for other indices) + return Promise.resolve(valueAtOtherIndices!); + } + }); +} + +export function mockGetContractInstance(hs: HostStorage, contractInstance: ContractInstanceWithAddress) { + (hs.contractsDb as jest.Mocked).getContractInstance.mockResolvedValue(contractInstance); +} diff --git a/yarn-project/simulator/src/mocks/fixtures.ts b/yarn-project/simulator/src/mocks/fixtures.ts index 9c51ebbc1844..7bbd49b1f7a5 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(), functionName: 'unknown', ...overrides, }; diff --git a/yarn-project/simulator/src/public/abstract_phase_manager.ts b/yarn-project/simulator/src/public/abstract_phase_manager.ts index 1b2833e58751..fecc49988d2f 100644 --- a/yarn-project/simulator/src/public/abstract_phase_manager.ts +++ b/yarn-project/simulator/src/public/abstract_phase_manager.ts @@ -266,13 +266,15 @@ export abstract class AbstractPhaseManager { const isExecutionRequest = !isPublicExecutionResult(current); const result = isExecutionRequest ? await this.publicExecutor.simulate( - current, + /*executionRequest=*/ current, this.globalVariables, /*availableGas=*/ this.getAvailableGas(tx, kernelPublicOutput), tx.data.constants.txContext, /*pendingNullifiers=*/ this.getSiloedPendingNullifiers(kernelPublicOutput), transactionFee, /*startSideEffectCounter=*/ AbstractPhaseManager.getMaxSideEffectCounter(kernelPublicOutput) + 1, + // NOTE: startSideEffectCounter is not the same as the executionRequest's sideEffectCounter + // (which counts the request itself) ) : current; @@ -320,7 +322,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 2d28731f6214..e5ca2cecd53b 100644 --- a/yarn-project/simulator/src/public/execution.ts +++ b/yarn-project/simulator/src/public/execution.ts @@ -20,16 +20,37 @@ import { type Gas } from '../avm/avm_gas.js'; export interface PublicExecutionResult { /** The execution that triggered this result. */ execution: PublicExecution; + + /** 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 contract storage reads performed by the function. */ + contractStorageReads: ContractStorageRead[]; + /** The contract storage update requests performed by the function. */ + contractStorageUpdateRequests: ContractStorageUpdateRequest[]; /** 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. */ @@ -40,12 +61,6 @@ export interface PublicExecutionResult { nullifierNonExistentReadRequests: ReadRequest[]; /** L1 to L2 message read requests emitted in this call. */ l1ToL2MsgReadRequests: ReadRequest[]; - /** The contract storage reads performed by the function. */ - contractStorageReads: ContractStorageRead[]; - /** The contract storage update requests performed by the function. */ - contractStorageUpdateRequests: ContractStorageUpdateRequest[]; - /** The results of nested calls. */ - nestedExecutions: this[]; /** * The hashed logs with side effect counter. * Note: required as we don't track the counter anywhere else. @@ -61,22 +76,15 @@ 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[]; + + // TODO(dbanks12): add contract instance read requests + + /** The results of nested calls. */ + nestedExecutions: this[]; + /** Hints for proving AVM execution. */ - avmHints: AvmExecutionHints; + avmCircuitHints: AvmExecutionHints; + /** The name of the function that was executed. Only used for logging. */ functionName: string; } diff --git a/yarn-project/simulator/src/public/executor.ts b/yarn-project/simulator/src/public/executor.ts index 45885d23de62..8486fb8d80e8 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 { PublicSideEffectTrace } from './side_effect_trace.js'; +import { createAvmExecutionEnvironment } from './transitional_adaptors.js'; /** * Handles execution of public functions. @@ -27,54 +28,57 @@ 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 pendingSiloedNullifiers - 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, - pendingNullifiers: Nullifier[], + pendingSiloedNullifiers: Nullifier[], transactionFee: Fr = Fr.ZERO, startSideEffectCounter: number = 0, ): Promise { - const address = execution.contractAddress; - const selector = execution.functionSelector; - const startGas = availableGas; + const address = executionRequest.contractAddress; + const selector = executionRequest.functionSelector; const fnName = (await this.contractsDb.getDebugFunctionName(address, selector)) ?? `${address}:${selector}`; PublicExecutor.log.verbose(`[AVM] Executing public external function ${fnName}.`); const timer = new Timer(); - // Temporary code to construct the AVM context - // 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 trace = new PublicSideEffectTrace(startSideEffectCounter); + const avmPersistableState = AvmPersistableStateManager.newWithPendingSiloedNullifiers( + hostStorage, + trace, + pendingSiloedNullifiers.map(n => n.value), + ); - const worldStateJournal = new AvmPersistableStateManager(hostStorage); - for (const nullifier of pendingNullifiers) { - worldStateJournal.nullifiers.cache.appendSiloed(nullifier.value); - } - worldStateJournal.trace.accessCounter = 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(avmPersistableState, 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): this should be unnecessary here or should be exposed by state manager await avmContext.persistableState.publicStorage.commitToDB(); PublicExecutor.log.verbose( @@ -89,28 +93,30 @@ export class PublicExecutor { } satisfies AvmSimulationStats, ); - const executionResult = convertAvmResultsToPxResult( - avmResult, - startSideEffectCounter, - execution, - startGas, - avmContext, + const publicExecutionResult = trace.toPublicExecutionResult( + avmExecutionEnv, + /*startGasLeft=*/ availableGas, + /*endGasLeft=*/ Gas.from(avmContext.machineState.gasLeft), bytecode, + avmResult, fnName, + /*requestSideEffectCounter=*/ executionRequest.callContext.sideEffectCounter, + // NOTE: startSideEffectCounter is not the same as the executionRequest's sideEffectCounter + // (which counts the request itself) ); // 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.test.ts b/yarn-project/simulator/src/public/side_effect_trace.test.ts new file mode 100644 index 000000000000..fbfb42b2e5f4 --- /dev/null +++ b/yarn-project/simulator/src/public/side_effect_trace.test.ts @@ -0,0 +1,284 @@ +import { UnencryptedL2Log } from '@aztec/circuit-types'; +import { AztecAddress, EthAddress, Gas, L2ToL1Message } from '@aztec/circuits.js'; +import { EventSelector } from '@aztec/foundation/abi'; +import { Fr } from '@aztec/foundation/fields'; +import { SerializableContractInstance } from '@aztec/types/contracts'; + +import { randomBytes, randomInt } from 'crypto'; + +import { Selector } from '../../../foundation/src/abi/selector.js'; +import { AvmContractCallResults } from '../avm/avm_message_call_result.js'; +import { initExecutionEnvironment } from '../avm/fixtures/index.js'; +import { PublicSideEffectTrace, type TracedContractInstance } from './side_effect_trace.js'; + +function randomTracedContractInstance(): TracedContractInstance { + const instance = SerializableContractInstance.random(); + const address = AztecAddress.random(); + return { exists: true, ...instance, address }; +} + +describe('Side Effect Trace', () => { + const address = Fr.random(); + const utxo = Fr.random(); + const leafIndex = Fr.random(); + const slot = Fr.random(); + const value = Fr.random(); + const recipient = Fr.random(); + const content = Fr.random(); + const event = new Fr(randomBytes(Selector.SIZE).readUint32BE()); + const log = [Fr.random(), Fr.random(), Fr.random()]; + + const startGasLeft = Gas.fromFields([new Fr(randomInt(10000)), new Fr(randomInt(10000))]); + const endGasLeft = Gas.fromFields([new Fr(randomInt(10000)), new Fr(randomInt(10000))]); + const transactionFee = Fr.random(); + const calldata = [Fr.random(), Fr.random(), Fr.random(), Fr.random()]; + const bytecode = randomBytes(100); + const returnValues = [Fr.random(), Fr.random()]; + + const avmEnvironment = initExecutionEnvironment({ + address, + calldata, + transactionFee, + }); + const reverted = false; + const avmCallResults = new AvmContractCallResults(reverted, returnValues); + + let startCounter: number; + let startCounterFr: Fr; + let startCounterPlus1: number; + let trace: PublicSideEffectTrace; + + beforeEach(() => { + startCounter = randomInt(/*max=*/ 1000000); + startCounterFr = new Fr(startCounter); + startCounterPlus1 = startCounter + 1; + trace = new PublicSideEffectTrace(startCounter); + }); + + const toPxResult = (trc: PublicSideEffectTrace) => { + return trc.toPublicExecutionResult(avmEnvironment, startGasLeft, endGasLeft, bytecode, avmCallResults); + }; + + it('Should trace storage reads', () => { + const exists = true; + const cached = false; + trace.tracePublicStorageRead(address, slot, value, exists, cached); + expect(trace.getCounter()).toBe(startCounterPlus1); + + const pxResult = toPxResult(trace); + expect(pxResult.contractStorageReads).toEqual([ + { + storageSlot: slot, + currentValue: value, + counter: startCounter, + contractAddress: AztecAddress.fromField(address), + //exists: exists, + //cached: cached, + }, + ]); + expect(pxResult.avmCircuitHints.storageValues.items).toEqual([{ key: startCounterFr, value: value }]); + }); + + it('Should trace storage writes', () => { + trace.tracePublicStorageWrite(address, slot, value); + expect(trace.getCounter()).toBe(startCounterPlus1); + + const pxResult = toPxResult(trace); + expect(pxResult.contractStorageUpdateRequests).toEqual([ + { + storageSlot: slot, + newValue: value, + counter: startCounter, + contractAddress: AztecAddress.fromField(address), + }, + ]); + }); + + it('Should trace note hash checks', () => { + const exists = true; + trace.traceNoteHashCheck(address, utxo, leafIndex, exists); + expect(trace.getCounter()).toBe(startCounterPlus1); + + const pxResult = toPxResult(trace); + expect(pxResult.noteHashReadRequests).toEqual([ + { + //storageAddress: contractAddress, + value: utxo, + //exists: exists, + counter: startCounter, + //leafIndex: leafIndex, + }, + ]); + expect(pxResult.avmCircuitHints.noteHashExists.items).toEqual([{ key: startCounterFr, value: new Fr(exists) }]); + }); + + it('Should trace note hashes', () => { + trace.traceNewNoteHash(address, utxo); + expect(trace.getCounter()).toBe(startCounterPlus1); + + const pxResult = toPxResult(trace); + expect(pxResult.newNoteHashes).toEqual([ + { + //storageAddress: contractAddress, + value: utxo, + counter: startCounter, + }, + ]); + }); + + it('Should trace nullifier checks', () => { + const exists = true; + const isPending = false; + trace.traceNullifierCheck(address, utxo, leafIndex, exists, isPending); + expect(trace.getCounter()).toBe(startCounterPlus1); + + const pxResult = toPxResult(trace); + expect(pxResult.nullifierReadRequests).toEqual([ + { + value: utxo, + counter: startCounter, + }, + ]); + expect(pxResult.nullifierNonExistentReadRequests).toEqual([]); + expect(pxResult.avmCircuitHints.nullifierExists.items).toEqual([{ key: startCounterFr, value: new Fr(exists) }]); + }); + + it('Should trace non-existent nullifier checks', () => { + const exists = false; + const isPending = false; + trace.traceNullifierCheck(address, utxo, leafIndex, exists, isPending); + expect(trace.getCounter()).toBe(startCounterPlus1); + + const pxResult = toPxResult(trace); + expect(pxResult.nullifierReadRequests).toEqual([]); + expect(pxResult.nullifierNonExistentReadRequests).toEqual([ + { + value: utxo, + counter: startCounter, + }, + ]); + expect(pxResult.avmCircuitHints.nullifierExists.items).toEqual([{ key: startCounterFr, value: new Fr(exists) }]); + }); + + it('Should trace nullifiers', () => { + trace.traceNewNullifier(address, utxo); + expect(trace.getCounter()).toBe(startCounterPlus1); + + const pxResult = toPxResult(trace); + expect(pxResult.newNullifiers).toEqual([ + { + value: utxo, + counter: startCounter, + noteHash: Fr.ZERO, + }, + ]); + }); + + it('Should trace L1ToL2 Message checks', () => { + const exists = true; + trace.traceL1ToL2MessageCheck(address, utxo, leafIndex, exists); + expect(trace.getCounter()).toBe(startCounterPlus1); + + const pxResult = toPxResult(trace); + expect(pxResult.l1ToL2MsgReadRequests).toEqual([ + { + value: utxo, + counter: startCounter, + }, + ]); + expect(pxResult.avmCircuitHints.l1ToL2MessageExists.items).toEqual([ + { + key: startCounterFr, + value: new Fr(exists), + }, + ]); + }); + + it('Should trace new L2ToL1 messages', () => { + trace.traceNewL2ToL1Message(recipient, content); + expect(trace.getCounter()).toBe(startCounterPlus1); + + const pxResult = toPxResult(trace); + expect(pxResult.newL2ToL1Messages).toEqual([ + new L2ToL1Message(EthAddress.fromField(recipient), content, startCounter), + ]); + }); + + it('Should trace new unencrypted logs', () => { + trace.traceUnencryptedLog(address, event, log); + expect(trace.getCounter()).toBe(startCounterPlus1); + + const pxResult = toPxResult(trace); + const expectLog = new UnencryptedL2Log( + AztecAddress.fromField(address), + EventSelector.fromField(event), + Buffer.concat(log.map(f => f.toBuffer())), + ); + expect(pxResult.unencryptedLogs.logs).toEqual([expectLog]); + expect(pxResult.allUnencryptedLogs.logs).toEqual([expectLog]); + expect(pxResult.unencryptedLogsHashes).toEqual([ + expect.objectContaining({ + counter: startCounter, + }), + ]); + }); + + it('Should trace get contract instance', () => { + const instance = randomTracedContractInstance(); + const { version: _, ...instanceWithoutVersion } = instance; + trace.traceGetContractInstance(instance); + expect(trace.getCounter()).toBe(startCounterPlus1); + + const pxResult = toPxResult(trace); + // TODO(dbanks12): process contract instance read requests in public kernel + //expect(pxResult.gotContractInstances).toEqual([instance]); + expect(pxResult.avmCircuitHints.contractInstances.items).toEqual([ + { + // hint omits "version" and has "exists" as an Fr + ...instanceWithoutVersion, + exists: new Fr(instance.exists), + }, + ]); + }); + + it('Should trace nested calls', () => { + const existsDefault = true; + const cached = false; + const isPending = false; + + const nestedTrace = new PublicSideEffectTrace(startCounter); + let testCounter = startCounter; + nestedTrace.tracePublicStorageRead(address, slot, value, existsDefault, cached); + testCounter++; + nestedTrace.tracePublicStorageWrite(address, slot, value); + testCounter++; + nestedTrace.traceNoteHashCheck(address, utxo, leafIndex, existsDefault); + testCounter++; + nestedTrace.traceNewNoteHash(address, utxo); + testCounter++; + nestedTrace.traceNullifierCheck(address, utxo, leafIndex, /*exists=*/ true, isPending); + testCounter++; + nestedTrace.traceNullifierCheck(address, utxo, leafIndex, /*exists=*/ false, isPending); + testCounter++; + nestedTrace.traceNewNullifier(address, utxo); + testCounter++; + nestedTrace.traceL1ToL2MessageCheck(address, utxo, leafIndex, existsDefault); + testCounter++; + nestedTrace.traceNewL2ToL1Message(recipient, content); + testCounter++; + nestedTrace.traceUnencryptedLog(address, event, log); + testCounter++; + + trace.traceNestedCall(nestedTrace, avmEnvironment, startGasLeft, endGasLeft, bytecode, avmCallResults); + // parent trace adopts nested call's counter + expect(trace.getCounter()).toBe(testCounter); + + // get parent trace as result + const parentPxResult = toPxResult(trace); + const childPxResult = toPxResult(nestedTrace); + expect(parentPxResult.nestedExecutions).toEqual([childPxResult]); + + // parent absorb's child's unencryptedLogs into all* + expect(parentPxResult.allUnencryptedLogs).toEqual(childPxResult.allUnencryptedLogs); + }); +}); 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..64e32718a599 --- /dev/null +++ b/yarn-project/simulator/src/public/side_effect_trace.ts @@ -0,0 +1,323 @@ +import { UnencryptedFunctionL2Logs, UnencryptedL2Log } from '@aztec/circuit-types'; +import { + AvmContractInstanceHint, + AvmExecutionHints, + AvmExternalCallHint, + AvmKeyValueHint, + AztecAddress, + CallContext, + ContractStorageRead, + ContractStorageUpdateRequest, + EthAddress, + Gas, + L2ToL1Message, + LogHash, + NoteHash, + Nullifier, + ReadRequest, +} from '@aztec/circuits.js'; +import { EventSelector } from '@aztec/foundation/abi'; +import { Fr } from '@aztec/foundation/fields'; +import { type ContractInstanceWithAddress } from '@aztec/types/contracts'; + +import { type AvmExecutionEnvironment } from '../avm/avm_execution_environment.js'; +import { type AvmContractCallResults } from '../avm/avm_message_call_result.js'; +import { createSimulationError } from '../common/errors.js'; +import { type PublicExecution, type PublicExecutionResult } from './execution.js'; +import { type PublicSideEffectTraceInterface } from './side_effect_trace_interface.js'; + +export type TracedContractInstance = { exists: boolean } & ContractInstanceWithAddress; + +export class PublicSideEffectTrace 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 PublicSideEffectTrace(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: PublicSideEffectTrace, + /** 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, + /** Function name for logging */ + functionName: string = 'unknown', + ) { + const result = nestedCallTrace.toPublicExecutionResult( + nestedEnvironment, + startGasLeft, + endGasLeft, + bytecode, + avmCallResults, + functionName, + ); + 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. */ + avmEnvironment: 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, + /** Function name for logging */ + functionName: string = 'unknown', + /** The side effect counter of the execution request itself */ + requestSideEffectCounter: number = this.startSideEffectCounter, + ): PublicExecutionResult { + return { + execution: createPublicExecutionRequest(requestSideEffectCounter, avmEnvironment), + + startSideEffectCounter: new Fr(this.startSideEffectCounter), + endSideEffectCounter: new Fr(this.sideEffectCounter), + startGasLeft, + endGasLeft, + transactionFee: avmEnvironment.transactionFee, + + bytecode, + calldata: avmEnvironment.calldata, + returnValues: avmCallResults.output, + reverted: avmCallResults.reverted, + revertReason: avmCallResults.revertReason ? createSimulationError(avmCallResults.revertReason) : undefined, + + 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, + // TODO(dbanks12): process contract instance read requests in public kernel + //gotContractInstances: this.gotContractInstances, + + nestedExecutions: this.nestedExecutions, + + avmCircuitHints: this.avmCircuitHints, + + functionName, + }; + } +} + +/** + * Helper function to create a public execution request from an AVM execution environment + */ +function createPublicExecutionRequest( + requestSideEffectCounter: number, + avmEnvironment: AvmExecutionEnvironment, +): PublicExecution { + const callContext = CallContext.from({ + msgSender: avmEnvironment.sender, + storageContractAddress: avmEnvironment.storageAddress, + functionSelector: avmEnvironment.temporaryFunctionSelector, + isDelegateCall: avmEnvironment.isDelegateCall, + isStaticCall: avmEnvironment.isStaticCall, + sideEffectCounter: requestSideEffectCounter, + }); + const execution: PublicExecution = { + contractAddress: avmEnvironment.address, + functionSelector: avmEnvironment.temporaryFunctionSelector, + callContext, + // execution request does not contain AvmContextInputs prefix + args: avmEnvironment.getCalldataWithoutPrefix(), + }; + 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..60dd0b1107d4 --- /dev/null +++ b/yarn-project/simulator/src/public/side_effect_trace_interface.ts @@ -0,0 +1,41 @@ +import { type Gas } from '@aztec/circuits.js'; +import { type Fr } from '@aztec/foundation/fields'; + +import { type AvmExecutionEnvironment } from '../avm/avm_execution_environment.js'; +import { type AvmContractCallResults } from '../avm/avm_message_call_result.js'; +import { type TracedContractInstance } from './side_effect_trace.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, + /** Function name */ + functionName: string, + ): void; +} diff --git a/yarn-project/simulator/src/public/transitional_adaptors.ts b/yarn-project/simulator/src/public/transitional_adaptors.ts index 36d0f2ade12b..9cea3c780753 100644 --- a/yarn-project/simulator/src/public/transitional_adaptors.ts +++ b/yarn-project/simulator/src/public/transitional_adaptors.ts @@ -1,29 +1,13 @@ // 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, -} from '@aztec/circuits.js'; +import { type GasSettings, type GlobalVariables, type Header } from '@aztec/circuits.js'; 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,90 +41,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, - functionName: string, -): PublicExecutionResult { - const endPersistableState = endAvmContext.persistableState; - const endMachineState = endAvmContext.machineState; - - return { - ...endPersistableState.transitionalExecutionResult, // includes nestedExecutions - functionName: functionName, - execution: fromPx, - returnValues: avmResult.output, - startSideEffectCounter: new Fr(startSideEffectCounter), - endSideEffectCounter: new Fr(endPersistableState.trace.accessCounter), - 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