From 1a2a632ebecd1677bd119ffaadf0326c9755bb79 Mon Sep 17 00:00:00 2001 From: fcarreiro Date: Tue, 5 Mar 2024 11:31:03 +0000 Subject: [PATCH] chore(avm-simulator): test improvements --- .../contracts/avm_test_contract/src/main.nr | 2 +- .../simulator/src/avm/avm_simulator.test.ts | 264 ++++-------------- .../simulator/src/avm/avm_simulator.ts | 44 ++- 3 files changed, 80 insertions(+), 230 deletions(-) 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 09f6a6b23de..f5134cb8940 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 @@ -168,7 +168,7 @@ contract AvmTest { context.push_new_nullifier(nullifier, 0); } - // Use the standard context interface to emit a new nullifier + // Use the standard context interface to check for a nullifier #[aztec(public-vm)] fn nullifier_exists(nullifier: Field) -> pub u8 { context.nullifier_exists(nullifier) diff --git a/yarn-project/simulator/src/avm/avm_simulator.test.ts b/yarn-project/simulator/src/avm/avm_simulator.test.ts index ea5a7ed378e..5c2ce65376c 100644 --- a/yarn-project/simulator/src/avm/avm_simulator.test.ts +++ b/yarn-project/simulator/src/avm/avm_simulator.test.ts @@ -7,6 +7,7 @@ import { Fr } from '@aztec/foundation/fields'; import { AvmTestContractArtifact } from '@aztec/noir-contracts.js'; import { jest } from '@jest/globals'; +import { strict as assert } from 'assert'; import { TypeTag } from './avm_memory_types.js'; import { AvmSimulator } from './avm_simulator.js'; @@ -19,6 +20,15 @@ import { import { Add, CalldataCopy, Return } from './opcodes/index.js'; import { encodeToBytecode } from './serialization/bytecode_serialization.js'; +function getAvmTestContractBytecode(functionName: string): Buffer { + const artifact = AvmTestContractArtifact.functions.find(f => f.name === functionName)!; + assert( + !!artifact.bytecode, + `No bytecode found for function ${functionName}. Try re-running bootstraph.sh on the repository root.`, + ); + return Buffer.from(artifact.bytecode, 'base64'); +} + describe('AVM simulator', () => { it('Should execute bytecode that performs basic addition', async () => { const calldata: Fr[] = [new Fr(1), new Fr(2)]; @@ -31,40 +41,22 @@ describe('AVM simulator', () => { ]); const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValue(Promise.resolve(bytecode)); - - const results = await new AvmSimulator(context).execute(); + const results = await new AvmSimulator(context).executeBytecode(bytecode); expect(results.reverted).toBe(false); - - const returnData = results.output; - expect(returnData.length).toBe(1); - expect(returnData).toEqual([new Fr(3)]); + expect(results.output).toEqual([new Fr(3)]); }); describe('Transpiled Noir contracts', () => { it('Should execute contract function that performs addition', async () => { const calldata: Fr[] = [new Fr(1), new Fr(2)]; - - // Get contract function artifact - const addArtifact = AvmTestContractArtifact.functions.find(f => f.name === 'avm_addArgsReturn')!; - - // Decode bytecode into instructions - const bytecode = Buffer.from(addArtifact.bytecode, 'base64'); - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValue(Promise.resolve(bytecode)); - const results = await new AvmSimulator(context).execute(); + const bytecode = getAvmTestContractBytecode('avm_addArgsReturn'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); expect(results.reverted).toBe(false); - - const returnData = results.output; - expect(returnData).toEqual([new Fr(3)]); + expect(results.output).toEqual([new Fr(3)]); }); describe.each([ @@ -76,21 +68,12 @@ describe('AVM simulator', () => { ['avm_setOpcodeSmallField', 200n], ])('Should execute contract SET functions', (name: string, res: bigint) => { it(`Should execute contract function '${name}'`, async () => { - // Decode bytecode into instructions - const artifact = AvmTestContractArtifact.functions.find(f => f.name === name)!; - const bytecode = Buffer.from(artifact.bytecode, 'base64'); - const context = initContext(); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValue(Promise.resolve(bytecode)); - - const results = await new AvmSimulator(context).execute(); + const bytecode = getAvmTestContractBytecode(name); + const results = await new AvmSimulator(context).executeBytecode(bytecode); expect(results.reverted).toBe(false); - - const returnData = results.output; - expect(returnData).toEqual([new Fr(res)]); + expect(results.output).toEqual([new Fr(res)]); }); }); @@ -102,27 +85,12 @@ describe('AVM simulator', () => { const calldata = [new Fr(1), new Fr(2), new Fr(3)]; const hash = hashFunction(Buffer.concat(calldata.map(f => f.toBuffer()))); - // Get contract function artifact - const artifact = AvmTestContractArtifact.functions.find(f => f.name === name)!; - - // Decode bytecode into instructions - const bytecode = Buffer.from(artifact.bytecode, 'base64'); - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValue(Promise.resolve(bytecode)); - - const results = await new AvmSimulator(context).execute(); + const bytecode = getAvmTestContractBytecode(name); + const results = await new AvmSimulator(context).executeBytecode(bytecode); expect(results.reverted).toBe(false); - - const returnData = results.output; - const reconstructedHash = Buffer.concat([ - returnData[0].toBuffer().subarray(16, 32), - returnData[1].toBuffer().subarray(16, 32), - ]); - expect(reconstructedHash).toEqual(hash); + expect(results.output).toEqual([new Fr(hash.subarray(0, 16)), new Fr(hash.subarray(16, 32))]); }); }); @@ -134,30 +102,17 @@ describe('AVM simulator', () => { const calldata = [new Fr(1), new Fr(2), new Fr(3)]; const hash = hashFunction(calldata.map(f => f.toBuffer())); - // Get contract function artifact - const artifact = AvmTestContractArtifact.functions.find(f => f.name === name)!; - - // Decode bytecode into instructions - const bytecode = Buffer.from(artifact.bytecode, 'base64'); - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValue(Promise.resolve(bytecode)); - - const results = await new AvmSimulator(context).execute(); + const bytecode = getAvmTestContractBytecode(name); + const results = await new AvmSimulator(context).executeBytecode(bytecode); expect(results.reverted).toBe(false); - - const returnData = results.output; - expect(returnData).toEqual([new Fr(hash)]); + expect(results.output).toEqual([new Fr(hash)]); }); }); describe('Test env getters from noir contract', () => { const testEnvGetter = async (valueName: string, value: any, functionName: string, globalVar: boolean = false) => { - const getterArtifact = AvmTestContractArtifact.functions.find(f => f.name === functionName)!; - // Execute let overrides = {}; if (globalVar === true) { @@ -167,15 +122,8 @@ describe('AVM simulator', () => { overrides = { [valueName]: value }; } const context = initContext({ env: initExecutionEnvironment(overrides) }); - - // Decode bytecode into instructions - const bytecode = Buffer.from(getterArtifact.bytecode, 'base64'); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValue(Promise.resolve(bytecode)); - // Execute - - const results = await new AvmSimulator(context).execute(); + const bytecode = getAvmTestContractBytecode(functionName); + const results = await new AvmSimulator(context).executeBytecode(bytecode); expect(results.reverted).toBe(false); @@ -250,19 +198,10 @@ describe('AVM simulator', () => { const leafIndex = new Fr(7); const calldata = [noteHash, leafIndex]; - // Get contract function artifact - const artifact = AvmTestContractArtifact.functions.find(f => f.name === 'avm_note_hash_exists')!; - - // Decode bytecode into instructions - const bytecode = Buffer.from(artifact.bytecode, 'base64'); - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValue(Promise.resolve(bytecode)); + const bytecode = getAvmTestContractBytecode('avm_note_hash_exists'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); - await new AvmSimulator(context).execute(); - const results = await new AvmSimulator(context).execute(); expect(results.reverted).toBe(false); expect(results.output).toEqual([/*exists=false*/ new Fr(0)]); @@ -270,29 +209,20 @@ describe('AVM simulator', () => { const trace = context.persistableState.flush(); expect(trace.noteHashChecks).toEqual([expect.objectContaining({ noteHash, leafIndex, exists: false })]); }); + it(`Should execute contract function that checks if a note hash exists (it does)`, async () => { const noteHash = new Fr(42); const leafIndex = new Fr(7); const calldata = [noteHash, leafIndex]; - // Get contract function artifact - const artifact = AvmTestContractArtifact.functions.find(f => f.name === 'avm_note_hash_exists')!; - - // Decode bytecode into instructions - const bytecode = Buffer.from(artifact.bytecode, 'base64'); - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValue(Promise.resolve(bytecode)); - // note hash exists! jest .spyOn(context.persistableState.hostStorage.commitmentsDb, 'getCommitmentIndex') .mockReturnValue(Promise.resolve(BigInt(7))); + const bytecode = getAvmTestContractBytecode('avm_note_hash_exists'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); - await new AvmSimulator(context).execute(); - const results = await new AvmSimulator(context).execute(); expect(results.reverted).toBe(false); expect(results.output).toEqual([/*exists=true*/ new Fr(1)]); @@ -300,19 +230,11 @@ describe('AVM simulator', () => { const trace = context.persistableState.flush(); expect(trace.noteHashChecks).toEqual([expect.objectContaining({ noteHash, leafIndex, exists: true })]); }); - it(`Should execute contract function to emit unencrypted logs (should be traced)`, async () => { - // Get contract function artifact - const artifact = AvmTestContractArtifact.functions.find(f => f.name === 'avm_emit_unencrypted_log')!; - - // Decode bytecode into instructions - const bytecode = Buffer.from(artifact.bytecode, 'base64'); + it(`Should execute contract function to emit unencrypted logs (should be traced)`, async () => { const context = initContext(); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValue(Promise.resolve(bytecode)); - - const results = await new AvmSimulator(context).execute(); + const bytecode = getAvmTestContractBytecode('avm_emit_unencrypted_log'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); expect(results.reverted).toBe(false); @@ -338,65 +260,41 @@ describe('AVM simulator', () => { // ), ]); }); + it(`Should execute contract function to emit note hash (should be traced)`, async () => { const utxo = new Fr(42); const calldata = [utxo]; - // Get contract function artifact - const artifact = AvmTestContractArtifact.functions.find(f => f.name === 'avm_new_note_hash')!; - - // Decode bytecode into instructions - const bytecode = Buffer.from(artifact.bytecode, 'base64'); - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValue(Promise.resolve(bytecode)); - - const results = await new AvmSimulator(context).execute(); + const bytecode = getAvmTestContractBytecode('avm_new_note_hash'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); expect(results.reverted).toBe(false); expect(context.persistableState.flush().newNoteHashes).toEqual([utxo]); }); + it(`Should execute contract function to emit nullifier (should be traced)`, async () => { const utxo = new Fr(42); const calldata = [utxo]; - // Get contract function artifact - const artifact = AvmTestContractArtifact.functions.find(f => f.name === 'avm_new_nullifier')!; - - // Decode bytecode into instructions - const bytecode = Buffer.from(artifact.bytecode, 'base64'); - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValue(Promise.resolve(bytecode)); - - const results = await new AvmSimulator(context).execute(); + const bytecode = getAvmTestContractBytecode('avm_new_nullifier'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); expect(results.reverted).toBe(false); expect(context.persistableState.flush().newNullifiers).toEqual([utxo]); }); + it(`Should execute contract function that checks if a nullifier exists (it does not)`, async () => { const utxo = new Fr(42); const calldata = [utxo]; - // Get contract function artifact - const artifact = AvmTestContractArtifact.functions.find(f => f.name === 'avm_nullifier_exists')!; - - // Decode bytecode into instructions - const bytecode = Buffer.from(artifact.bytecode, 'base64'); - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValue(Promise.resolve(bytecode)); + const bytecode = getAvmTestContractBytecode('avm_nullifier_exists'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); - await new AvmSimulator(context).execute(); - const results = await new AvmSimulator(context).execute(); expect(results.reverted).toBe(false); expect(results.output).toEqual([/*exists=false*/ new Fr(0)]); @@ -405,28 +303,19 @@ describe('AVM simulator', () => { expect(trace.nullifierChecks.length).toEqual(1); expect(trace.nullifierChecks[0].exists).toEqual(false); }); + it(`Should execute contract function that checks if a nullifier exists (it does)`, async () => { const utxo = new Fr(42); const calldata = [utxo]; - // Get contract function artifact - const artifact = AvmTestContractArtifact.functions.find(f => f.name === 'avm_nullifier_exists')!; - - // Decode bytecode into instructions - const bytecode = Buffer.from(artifact.bytecode, 'base64'); - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValue(Promise.resolve(bytecode)); - // nullifier exists! jest .spyOn(context.persistableState.hostStorage.commitmentsDb, 'getNullifierIndex') .mockReturnValue(Promise.resolve(BigInt(42))); + const bytecode = getAvmTestContractBytecode('avm_nullifier_exists'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); - await new AvmSimulator(context).execute(); - const results = await new AvmSimulator(context).execute(); expect(results.reverted).toBe(false); expect(results.output).toEqual([/*exists=true*/ new Fr(1)]); @@ -435,106 +324,69 @@ describe('AVM simulator', () => { expect(trace.nullifierChecks.length).toEqual(1); expect(trace.nullifierChecks[0].exists).toEqual(true); }); + it(`Should execute contract function that checks emits a nullifier and checks its existence`, async () => { const utxo = new Fr(42); const calldata = [utxo]; - // Get contract function artifact - const artifact = AvmTestContractArtifact.functions.find(f => f.name === 'avm_emit_nullifier_and_check')!; - - // Decode bytecode into instructions - const bytecode = Buffer.from(artifact.bytecode, 'base64'); - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValue(Promise.resolve(bytecode)); + const bytecode = getAvmTestContractBytecode('avm_emit_nullifier_and_check'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); - await new AvmSimulator(context).execute(); - const results = await new AvmSimulator(context).execute(); expect(results.reverted).toBe(false); - // Nullifier existence check should be in trace const trace = context.persistableState.flush(); expect(trace.newNullifiers).toEqual([utxo]); expect(trace.nullifierChecks.length).toEqual(1); expect(trace.nullifierChecks[0].exists).toEqual(true); }); + it(`Should execute contract function that emits same nullifier twice (should fail)`, async () => { const utxo = new Fr(42); const calldata = [utxo]; - // Get contract function artifact - const artifact = AvmTestContractArtifact.functions.find(f => f.name === 'avm_nullifier_collision')!; - - // Decode bytecode into instructions - const bytecode = Buffer.from(artifact.bytecode, 'base64'); - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValue(Promise.resolve(bytecode)); + const bytecode = getAvmTestContractBytecode('avm_nullifier_collision'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); - await new AvmSimulator(context).execute(); - const results = await new AvmSimulator(context).execute(); expect(results.reverted).toBe(true); - // Only the first nullifier should be in the trace, second one failed to add expect(context.persistableState.flush().newNullifiers).toEqual([utxo]); }); }); + describe('Test tree access functions from noir contract (l1ToL2 messages)', () => { it(`Should execute contract function that checks if a message exists (it does not)`, async () => { const msgHash = new Fr(42); const leafIndex = new Fr(24); const calldata = [msgHash, leafIndex]; - // Get contract function artifact - const artifact = AvmTestContractArtifact.functions.find(f => f.name === 'avm_l1_to_l2_msg_exists')!; - - // Decode bytecode into instructions - const bytecode = Buffer.from(artifact.bytecode, 'base64'); - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValue(Promise.resolve(bytecode)); + const bytecode = getAvmTestContractBytecode('avm_l1_to_l2_msg_exists'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); - await new AvmSimulator(context).execute(); - const results = await new AvmSimulator(context).execute(); expect(results.reverted).toBe(false); expect(results.output).toEqual([/*exists=false*/ new Fr(0)]); - // Message existence check should be in trace const trace = context.persistableState.flush(); expect(trace.l1ToL2MessageChecks.length).toEqual(1); expect(trace.l1ToL2MessageChecks[0].exists).toEqual(false); }); + it(`Should execute contract function that checks if a message exists (it does)`, async () => { const msgHash = new Fr(42); const leafIndex = new Fr(24); const calldata = [msgHash, leafIndex]; - // Get contract function artifact - const artifact = AvmTestContractArtifact.functions.find(f => f.name === 'avm_l1_to_l2_msg_exists')!; - - // Decode bytecode into instructions - const bytecode = Buffer.from(artifact.bytecode, 'base64'); - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValue(Promise.resolve(bytecode)); - jest .spyOn(context.persistableState.hostStorage.commitmentsDb, 'getL1ToL2MembershipWitness') .mockResolvedValue(initL1ToL2MessageOracleInput(leafIndex.toBigInt())); + const bytecode = getAvmTestContractBytecode('avm_l1_to_l2_msg_exists'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); - await new AvmSimulator(context).execute(); - const results = await new AvmSimulator(context).execute(); 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); diff --git a/yarn-project/simulator/src/avm/avm_simulator.ts b/yarn-project/simulator/src/avm/avm_simulator.ts index 4289e5deeda..7cbb99b0dc5 100644 --- a/yarn-project/simulator/src/avm/avm_simulator.ts +++ b/yarn-project/simulator/src/avm/avm_simulator.ts @@ -17,8 +17,27 @@ export class AvmSimulator { * Fetch the bytecode and execute it in the current context. */ public async execute(): Promise { - const instructions = await this.fetchAndDecodeBytecode(); - return this.executeInstructions(instructions); + const selector = this.context.environment.temporaryFunctionSelector; + const bytecode = await this.context.persistableState.hostStorage.contractsDb.getBytecode( + this.context.environment.address, + selector, + ); + + // This assumes that we will not be able to send messages to accounts without code + // Pending classes and instances impl details + if (!bytecode) { + throw new NoBytecodeForContractError(this.context.environment.address); + } + + return await this.executeBytecode(bytecode); + } + + /** + * Executes the provided bytecode in the current context. + * This method is useful for testing and debugging. + */ + public async executeBytecode(bytecode: Buffer): Promise { + return await this.executeInstructions(decodeFromBytecode(bytecode)); } /** @@ -65,25 +84,4 @@ export class AvmSimulator { return results; } } - - /** - * Fetch contract bytecode from world state and decode into executable instructions. - */ - private async fetchAndDecodeBytecode(): Promise { - // NOTE: the following is mocked as getPublicBytecode does not exist yet - - const selector = this.context.environment.temporaryFunctionSelector; - const bytecode = await this.context.persistableState.hostStorage.contractsDb.getBytecode( - this.context.environment.address, - selector, - ); - - // This assumes that we will not be able to send messages to accounts without code - // Pending classes and instances impl details - if (!bytecode) { - throw new NoBytecodeForContractError(this.context.environment.address); - } - - return decodeFromBytecode(bytecode); - } }