From 12e2844f9af433beb1a586640b08ce284ad91095 Mon Sep 17 00:00:00 2001 From: Facundo Date: Mon, 25 Mar 2024 17:46:08 +0000 Subject: [PATCH] feat(avm): add AvmContextInputs (#5396) NOTE: I don't know why this triggered a change in the snapshot. --- This structure lets us easily pass things from the (TS) context to the constructor of the `AvmContext` in Noir, using `calldata` as a vehicle (effectively adding them as public inputs). The choice to add the structure to the function arguments as LAST and not first is because adding them first would break any non-noir-generated bytecode (since they would have to shift their expected calldata by a magic number `N = sizeof(AvmContextInputs)`). Putting it last, makes it transparent for them. A calldatacopy would still work. However, having this makes any external call always have `AvmContextInputs` in the calldata, bloating it (on chain) for non-noir-generated bytecode. This is not an issue now. For the moment, this is temporary, but might be useful long term. (Sean mentioned passing maybe env getters like this). --- Implemented `AvmContext.selector()` and `AvmContext.get_args_hash()` using the above. --- .../aztec-nr/aztec/src/context/avm_context.nr | 16 +- .../aztec-nr/aztec/src/context/inputs.nr | 2 + .../src/context/inputs/avm_context_inputs.nr | 4 + .../contracts/avm_test_contract/src/main.nr | 10 + .../aztec_macros/src/transforms/functions.rs | 9 +- .../simulator/src/avm/avm_context.test.ts | 8 +- .../src/avm/avm_execution_environment.test.ts | 11 +- .../src/avm/avm_execution_environment.ts | 21 +- .../simulator/src/avm/avm_simulator.test.ts | 1130 +++++++++-------- .../simulator/src/avm/fixtures/index.ts | 18 +- .../src/avm/opcodes/external_calls.test.ts | 16 +- .../simulator/src/avm/opcodes/memory.test.ts | 23 +- 12 files changed, 691 insertions(+), 577 deletions(-) create mode 100644 noir-projects/aztec-nr/aztec/src/context/inputs/avm_context_inputs.nr diff --git a/noir-projects/aztec-nr/aztec/src/context/avm_context.nr b/noir-projects/aztec-nr/aztec/src/context/avm_context.nr index 4f72dad449b..68925164b65 100644 --- a/noir-projects/aztec-nr/aztec/src/context/avm_context.nr +++ b/noir-projects/aztec-nr/aztec/src/context/avm_context.nr @@ -3,15 +3,17 @@ use dep::protocol_types::traits::Serialize; use dep::protocol_types::abis::function_selector::FunctionSelector; use dep::protocol_types::abis::public_circuit_public_inputs::PublicCircuitPublicInputs; use dep::protocol_types::constants::RETURN_VALUES_LENGTH; -use crate::context::inputs::PublicContextInputs; +use crate::context::inputs::avm_context_inputs::AvmContextInputs; use crate::context::interface::ContextInterface; use crate::context::interface::PublicContextInterface; -struct AVMContext {} +struct AVMContext { + inputs: AvmContextInputs, +} impl AVMContext { - pub fn new() -> Self { - AVMContext {} + pub fn new(inputs: AvmContextInputs) -> Self { + AVMContext { inputs } } pub fn origin(self) -> AztecAddress { @@ -190,16 +192,14 @@ impl ContextInterface for AVMContext { version() } fn selector(self) -> FunctionSelector { - assert(false, "'selector' not implemented!"); - FunctionSelector::zero() + FunctionSelector::from_field(self.inputs.selector) } fn get_header(self) -> Header { assert(false, "'get_header' not implemented!"); Header::empty() } fn get_args_hash(self) -> Field { - assert(false, "'get_args_hash' not implemented!"); - 0 + self.inputs.args_hash } } diff --git a/noir-projects/aztec-nr/aztec/src/context/inputs.nr b/noir-projects/aztec-nr/aztec/src/context/inputs.nr index 0d7c56574d5..86595969e1e 100644 --- a/noir-projects/aztec-nr/aztec/src/context/inputs.nr +++ b/noir-projects/aztec-nr/aztec/src/context/inputs.nr @@ -1,5 +1,7 @@ mod private_context_inputs; mod public_context_inputs; +mod avm_context_inputs; use crate::context::inputs::private_context_inputs::PrivateContextInputs; use crate::context::inputs::public_context_inputs::PublicContextInputs; +use crate::context::inputs::avm_context_inputs::AvmContextInputs; diff --git a/noir-projects/aztec-nr/aztec/src/context/inputs/avm_context_inputs.nr b/noir-projects/aztec-nr/aztec/src/context/inputs/avm_context_inputs.nr new file mode 100644 index 00000000000..ffd16b268ac --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/context/inputs/avm_context_inputs.nr @@ -0,0 +1,4 @@ +struct AvmContextInputs { + selector: Field, + args_hash: Field, +} 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 43a150ae1f8..96081c70768 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 @@ -232,6 +232,16 @@ contract AvmTest { // context.contract_call_depth() // } + #[aztec(public-vm)] + fn check_selector() { + assert(context.selector() == FunctionSelector::from_signature("check_selector()")); + } + + #[aztec(public-vm)] + fn get_args_hash(_a: u8, _fields: [Field; 3]) -> pub Field { + context.get_args_hash() + } + #[aztec(public-vm)] fn emit_unencrypted_log() { context.accumulate_unencrypted_logs(/*event_selector=*/ 5, /*message=*/ [10, 20, 30]); diff --git a/noir/noir-repo/aztec_macros/src/transforms/functions.rs b/noir/noir-repo/aztec_macros/src/transforms/functions.rs index 51a90c2ee28..5e42dccdd78 100644 --- a/noir/noir-repo/aztec_macros/src/transforms/functions.rs +++ b/noir/noir-repo/aztec_macros/src/transforms/functions.rs @@ -116,6 +116,10 @@ pub fn transform_vm_function( let create_context = create_avm_context()?; func.def.body.0.insert(0, create_context); + // Add the inputs to the params (first!) + let input = create_inputs("AvmContextInputs"); + func.def.parameters.insert(0, input); + // We want the function to be seen as a public function func.def.is_unconstrained = true; @@ -353,11 +357,14 @@ fn create_context(ty: &str, params: &[Param]) -> Result, AztecMac /// // ... /// } fn create_avm_context() -> Result { + // Create the inputs to the context + let inputs_expression = variable("inputs"); + let let_context = mutable_assignment( "context", // Assigned to call( variable_path(chained_dep!("aztec", "context", "AVMContext", "new")), // Path - vec![], // args + vec![inputs_expression], // args ), ); diff --git a/yarn-project/simulator/src/avm/avm_context.test.ts b/yarn-project/simulator/src/avm/avm_context.test.ts index e2a29c8be9b..089e1d3f4c4 100644 --- a/yarn-project/simulator/src/avm/avm_context.test.ts +++ b/yarn-project/simulator/src/avm/avm_context.test.ts @@ -1,6 +1,6 @@ import { AztecAddress, Fr } from '@aztec/circuits.js'; -import { allSameExcept, initContext } from './fixtures/index.js'; +import { allSameExcept, anyAvmContextInputs, initContext } from './fixtures/index.js'; describe('Avm Context', () => { it('New call should fork context correctly', () => { @@ -15,7 +15,8 @@ describe('Avm Context', () => { allSameExcept(context.environment, { address: newAddress, storageAddress: newAddress, - calldata: newCalldata, + // Calldata also includes AvmContextInputs + calldata: anyAvmContextInputs().concat(newCalldata), isStaticCall: false, }), ); @@ -41,7 +42,8 @@ describe('Avm Context', () => { allSameExcept(context.environment, { address: newAddress, storageAddress: newAddress, - calldata: newCalldata, + // 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 e2104cbad59..6aad1e2ea25 100644 --- a/yarn-project/simulator/src/avm/avm_execution_environment.test.ts +++ b/yarn-project/simulator/src/avm/avm_execution_environment.test.ts @@ -1,6 +1,6 @@ import { Fr } from '@aztec/foundation/fields'; -import { allSameExcept, initExecutionEnvironment } from './fixtures/index.js'; +import { allSameExcept, anyAvmContextInputs, initExecutionEnvironment } from './fixtures/index.js'; describe('Execution Environment', () => { const newAddress = new Fr(123456n); @@ -14,7 +14,8 @@ describe('Execution Environment', () => { allSameExcept(executionEnvironment, { address: newAddress, storageAddress: newAddress, - calldata, + // Calldata also includes AvmContextInputs + calldata: anyAvmContextInputs().concat(calldata), }), ); }); @@ -27,7 +28,8 @@ describe('Execution Environment', () => { allSameExcept(executionEnvironment, { address: newAddress, isDelegateCall: true, - calldata, + // Calldata also includes AvmContextInputs + calldata: anyAvmContextInputs().concat(calldata), }), ); }); @@ -41,7 +43,8 @@ describe('Execution Environment', () => { address: newAddress, storageAddress: newAddress, isStaticCall: true, - calldata, + // 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 23340dd42b8..d6ffd51d9ba 100644 --- a/yarn-project/simulator/src/avm/avm_execution_environment.ts +++ b/yarn-project/simulator/src/avm/avm_execution_environment.ts @@ -1,8 +1,19 @@ import { FunctionSelector, GlobalVariables } from '@aztec/circuits.js'; import { AztecAddress } from '@aztec/foundation/aztec-address'; +import { pedersenHash } from '@aztec/foundation/crypto'; import { EthAddress } from '@aztec/foundation/eth-address'; import { Fr } from '@aztec/foundation/fields'; +export class AvmContextInputs { + static readonly SIZE = 2; + + constructor(private selector: Fr, private argsHash: Fr) {} + + public toFields(): Fr[] { + return [this.selector, this.argsHash]; + } +} + /** * Contains variables that remain constant during AVM execution * These variables are provided by the public kernel circuit @@ -40,7 +51,15 @@ export class AvmExecutionEnvironment { // containing all functions, and function selector will become an application-level mechanism // (e.g. first few bytes of calldata + compiler-generated jump table) public readonly temporaryFunctionSelector: FunctionSelector, - ) {} + ) { + // We encode some extra inputs (AvmContextInputs) in calldata. + // This will have to go once we move away from one proof per call. + const inputs = new AvmContextInputs( + temporaryFunctionSelector.toField(), + pedersenHash(calldata.map(word => word.toBuffer())), + ); + this.calldata = [...inputs.toFields(), ...calldata]; + } public deriveEnvironmentForNestedCall( address: AztecAddress, diff --git a/yarn-project/simulator/src/avm/avm_simulator.test.ts b/yarn-project/simulator/src/avm/avm_simulator.test.ts index bf85390f1cb..e6d8a083fbe 100644 --- a/yarn-project/simulator/src/avm/avm_simulator.test.ts +++ b/yarn-project/simulator/src/avm/avm_simulator.test.ts @@ -1,5 +1,5 @@ import { UnencryptedL2Log } from '@aztec/circuit-types'; -import { EventSelector } from '@aztec/foundation/abi'; +import { EventSelector, FunctionSelector } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { keccak, pedersenHash, poseidonHash, sha256 } from '@aztec/foundation/crypto'; import { EthAddress } from '@aztec/foundation/eth-address'; @@ -12,6 +12,7 @@ import { strict as assert } from 'assert'; import { TypeTag } from './avm_memory_types.js'; import { AvmSimulator } from './avm_simulator.js'; import { + adjustCalldataIndex, initContext, initExecutionEnvironment, initGlobalVariables, @@ -20,13 +21,13 @@ import { import { Add, CalldataCopy, Return } from './opcodes/index.js'; import { encodeToBytecode } from './serialization/bytecode_serialization.js'; -describe('AVM simulator', () => { +describe('AVM simulator: injected bytecode', () => { it('Should execute bytecode that performs basic addition', async () => { const calldata: Fr[] = [new Fr(1), new Fr(2)]; // Construct bytecode const bytecode = encodeToBytecode([ - new CalldataCopy(/*indirect=*/ 0, /*cdOffset=*/ 0, /*copySize=*/ 2, /*dstOffset=*/ 0), + new CalldataCopy(/*indirect=*/ 0, /*cdOffset=*/ adjustCalldataIndex(0), /*copySize=*/ 2, /*dstOffset=*/ 0), new Add(/*indirect=*/ 0, TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 2), new Return(/*indirect=*/ 0, /*returnOffset=*/ 2, /*copySize=*/ 1), ]); @@ -37,663 +38,688 @@ describe('AVM simulator', () => { expect(results.reverted).toBe(false); 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)]; - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); +describe('AVM simulator: transpiled Noir contracts', () => { + it('addition', async () => { + const calldata: Fr[] = [new Fr(1), new Fr(2)]; + const context = initContext({ env: initExecutionEnvironment({ calldata }) }); + + const bytecode = getAvmTestContractBytecode('add_args_return'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); + + expect(results.reverted).toBe(false); + expect(results.output).toEqual([new Fr(3)]); + }); + + it('U128 addition', async () => { + const calldata: Fr[] = [ + // First U128 + new Fr(1), + new Fr(2), + // Second U128 + new Fr(3), + new Fr(4), + ]; + const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - const bytecode = getAvmTestContractBytecode('add_args_return'); + const bytecode = getAvmTestContractBytecode('add_u128'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); + + expect(results.reverted).toBe(false); + expect(results.output).toEqual([new Fr(4), new Fr(6)]); + }); + + describe.each([ + ['set_opcode_u8', 8n], + ['set_opcode_u32', 1n << 30n], + ['set_opcode_u64', 1n << 60n], + ['set_opcode_small_field', 0x001234567890abcdef1234567890abcdefn], + ['set_opcode_big_field', 0x991234567890abcdef1234567890abcdefn], + ])('SET functions', (name: string, res: bigint) => { + it(`function '${name}'`, async () => { + const context = initContext(); + const bytecode = getAvmTestContractBytecode(name); const results = await new AvmSimulator(context).executeBytecode(bytecode); expect(results.reverted).toBe(false); - expect(results.output).toEqual([new Fr(3)]); + expect(results.output).toEqual([new Fr(res)]); }); + }); + + describe.each([ + ['sha256_hash', sha256], + ['keccak_hash', keccak], + ])('Hashes with 2 fields returned in noir contracts', (name: string, hashFunction: (data: Buffer) => Buffer) => { + it(`Should execute contract function that performs ${name} hash`, async () => { + const calldata = [new Fr(1), new Fr(2), new Fr(3)]; + const hash = hashFunction(Buffer.concat(calldata.map(f => f.toBuffer()))); - it('Should execute contract function that performs U128 addition', async () => { - const calldata: Fr[] = [ - // First U128 - new Fr(1), - new Fr(2), - // Second U128 - new Fr(3), - new Fr(4), - ]; const context = initContext({ env: initExecutionEnvironment({ calldata }) }); + const bytecode = getAvmTestContractBytecode(name); + const results = await new AvmSimulator(context).executeBytecode(bytecode); - const bytecode = getAvmTestContractBytecode('add_u128'); + expect(results.reverted).toBe(false); + expect(results.output).toEqual([new Fr(hash.subarray(0, 16)), new Fr(hash.subarray(16, 32))]); + }); + }); + + describe.each([ + ['poseidon_hash', poseidonHash], + ['pedersen_hash', pedersenHash], + ])('Hashes with field returned in noir contracts', (name: string, hashFunction: (data: Buffer[]) => Fr) => { + it(`Should execute contract function that performs ${name} hash`, async () => { + const calldata = [new Fr(1), new Fr(2), new Fr(3)]; + const hash = hashFunction(calldata.map(f => f.toBuffer())); + + const context = initContext({ env: initExecutionEnvironment({ calldata }) }); + const bytecode = getAvmTestContractBytecode(name); const results = await new AvmSimulator(context).executeBytecode(bytecode); expect(results.reverted).toBe(false); - expect(results.output).toEqual([new Fr(4), new Fr(6)]); - }); - - describe.each([ - ['set_opcode_u8', 8n], - ['set_opcode_u32', 1n << 30n], - ['set_opcode_u64', 1n << 60n], - ['set_opcode_small_field', 0x001234567890abcdef1234567890abcdefn], - ['set_opcode_big_field', 0x991234567890abcdef1234567890abcdefn], - ])('Should execute contract SET functions', (name: string, res: bigint) => { - it(`Should execute contract function '${name}'`, async () => { - const context = initContext(); - const bytecode = getAvmTestContractBytecode(name); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - expect(results.reverted).toBe(false); - expect(results.output).toEqual([new Fr(res)]); - }); + expect(results.output).toEqual([new Fr(hash)]); }); + }); + + describe('Environment getters', () => { + const testEnvGetter = async (valueName: string, value: any, functionName: string, globalVar: boolean = false) => { + // Execute + let overrides = {}; + if (globalVar === true) { + const globals = initGlobalVariables({ [valueName]: value }); + overrides = { globals }; + } else { + overrides = { [valueName]: value }; + } + const context = initContext({ env: initExecutionEnvironment(overrides) }); + const bytecode = getAvmTestContractBytecode(functionName); + const results = await new AvmSimulator(context).executeBytecode(bytecode); - describe.each([ - ['sha256_hash', sha256], - ['keccak_hash', keccak], - ])('Hashes with 2 fields returned in noir contracts', (name: string, hashFunction: (data: Buffer) => Buffer) => { - it(`Should execute contract function that performs ${name} hash`, async () => { - const calldata = [new Fr(1), new Fr(2), new Fr(3)]; - const hash = hashFunction(Buffer.concat(calldata.map(f => f.toBuffer()))); + expect(results.reverted).toBe(false); - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - const bytecode = getAvmTestContractBytecode(name); - const results = await new AvmSimulator(context).executeBytecode(bytecode); + const returnData = results.output; + expect(returnData).toEqual([value.toField()]); + }; - expect(results.reverted).toBe(false); - expect(results.output).toEqual([new Fr(hash.subarray(0, 16)), new Fr(hash.subarray(16, 32))]); - }); + it('address', async () => { + const address = AztecAddress.fromField(new Fr(1)); + await testEnvGetter('address', address, 'get_address'); }); - describe.each([ - ['poseidon_hash', poseidonHash], - ['pedersen_hash', pedersenHash], - ])('Hashes with field returned in noir contracts', (name: string, hashFunction: (data: Buffer[]) => Fr) => { - it(`Should execute contract function that performs ${name} hash`, async () => { - const calldata = [new Fr(1), new Fr(2), new Fr(3)]; - const hash = hashFunction(calldata.map(f => f.toBuffer())); + it('storageAddress', async () => { + const storageAddress = AztecAddress.fromField(new Fr(1)); + await testEnvGetter('storageAddress', storageAddress, 'get_storage_address'); + }); - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - const bytecode = getAvmTestContractBytecode(name); - const results = await new AvmSimulator(context).executeBytecode(bytecode); + it('sender', async () => { + const sender = AztecAddress.fromField(new Fr(1)); + await testEnvGetter('sender', sender, 'get_sender'); + }); - expect(results.reverted).toBe(false); - expect(results.output).toEqual([new Fr(hash)]); - }); + it('origin', async () => { + const origin = AztecAddress.fromField(new Fr(1)); + await testEnvGetter('origin', origin, 'get_origin'); }); - describe('Test env getters from noir contract', () => { - const testEnvGetter = async (valueName: string, value: any, functionName: string, globalVar: boolean = false) => { - // Execute - let overrides = {}; - if (globalVar === true) { - const globals = initGlobalVariables({ [valueName]: value }); - overrides = { globals }; - } else { - overrides = { [valueName]: value }; - } - const context = initContext({ env: initExecutionEnvironment(overrides) }); - const bytecode = getAvmTestContractBytecode(functionName); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - expect(results.reverted).toBe(false); - - const returnData = results.output; - expect(returnData).toEqual([value.toField()]); - }; - - it('address', async () => { - const address = AztecAddress.fromField(new Fr(1)); - await testEnvGetter('address', address, 'get_address'); - }); + it('portal', async () => { + const portal = EthAddress.fromField(new Fr(1)); + await testEnvGetter('portal', portal, 'get_portal'); + }); - it('storageAddress', async () => { - const storageAddress = AztecAddress.fromField(new Fr(1)); - await testEnvGetter('storageAddress', storageAddress, 'get_storage_address'); - }); + it('getFeePerL1Gas', async () => { + const fee = new Fr(1); + await testEnvGetter('feePerL1Gas', fee, 'get_fee_per_l1_gas'); + }); - it('sender', async () => { - const sender = AztecAddress.fromField(new Fr(1)); - await testEnvGetter('sender', sender, 'get_sender'); - }); + it('getFeePerL2Gas', async () => { + const fee = new Fr(1); + await testEnvGetter('feePerL2Gas', fee, 'get_fee_per_l2_gas'); + }); - it('origin', async () => { - const origin = AztecAddress.fromField(new Fr(1)); - await testEnvGetter('origin', origin, 'get_origin'); - }); + it('getFeePerDaGas', async () => { + const fee = new Fr(1); + await testEnvGetter('feePerDaGas', fee, 'get_fee_per_da_gas'); + }); - it('portal', async () => { - const portal = EthAddress.fromField(new Fr(1)); - await testEnvGetter('portal', portal, 'get_portal'); - }); + it('chainId', async () => { + const chainId = new Fr(1); + await testEnvGetter('chainId', chainId, 'get_chain_id', /*globalVar=*/ true); + }); - it('getFeePerL1Gas', async () => { - const fee = new Fr(1); - await testEnvGetter('feePerL1Gas', fee, 'get_fee_per_l1_gas'); - }); + it('version', async () => { + const version = new Fr(1); + await testEnvGetter('version', version, 'get_version', /*globalVar=*/ true); + }); - it('getFeePerL2Gas', async () => { - const fee = new Fr(1); - await testEnvGetter('feePerL2Gas', fee, 'get_fee_per_l2_gas'); - }); + it('blockNumber', async () => { + const blockNumber = new Fr(1); + await testEnvGetter('blockNumber', blockNumber, 'get_block_number', /*globalVar=*/ true); + }); - it('getFeePerDaGas', async () => { - const fee = new Fr(1); - await testEnvGetter('feePerDaGas', fee, 'get_fee_per_da_gas'); - }); + it('timestamp', async () => { + const timestamp = new Fr(1); + await testEnvGetter('timestamp', timestamp, 'get_timestamp', /*globalVar=*/ true); + }); + }); - it('chainId', async () => { - const chainId = new Fr(1); - await testEnvGetter('chainId', chainId, 'get_chain_id', /*globalVar=*/ true); + describe('AvmContextInputs', () => { + it('selector', async () => { + const context = initContext({ + env: initExecutionEnvironment({ + temporaryFunctionSelector: FunctionSelector.fromSignature('check_selector()'), + }), }); + const bytecode = getAvmTestContractBytecode('check_selector'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); - it('version', async () => { - const version = new Fr(1); - await testEnvGetter('version', version, 'get_version', /*globalVar=*/ true); - }); + expect(results.reverted).toBe(false); + }); - it('blockNumber', async () => { - const blockNumber = new Fr(1); - await testEnvGetter('blockNumber', blockNumber, 'get_block_number', /*globalVar=*/ true); - }); + it('get_args_hash', async () => { + const calldata = [new Fr(8), new Fr(1), new Fr(2), new Fr(3)]; - it('timestamp', async () => { - const timestamp = new Fr(1); - await testEnvGetter('timestamp', timestamp, 'get_timestamp', /*globalVar=*/ true); - }); + const context = initContext({ env: initExecutionEnvironment({ calldata }) }); + const bytecode = getAvmTestContractBytecode('get_args_hash'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); + + expect(results.reverted).toBe(false); + expect(results.output).toEqual([pedersenHash(calldata.map(f => f.toBuffer()))]); }); + }); - describe('Test tree access functions from noir contract (notes & nullifiers)', () => { - it(`Should execute contract function that checks if a note hash exists (it does not)`, async () => { - const noteHash = new Fr(42); - const leafIndex = new Fr(7); - const calldata = [noteHash, leafIndex]; + 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); + 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)]); + expect(results.reverted).toBe(false); + expect(results.output).toEqual([/*exists=false*/ new Fr(0)]); - // Note hash existence check should be in trace - const trace = context.persistableState.flush(); - expect(trace.noteHashChecks).toEqual([expect.objectContaining({ noteHash, leafIndex, exists: false })]); - }); + // Note hash existence check should be in trace + 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]; - - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - // note hash exists! - jest - .spyOn(context.persistableState.hostStorage.commitmentsDb, 'getCommitmentIndex') - .mockReturnValue(Promise.resolve(BigInt(7))); - const bytecode = getAvmTestContractBytecode('note_hash_exists'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - expect(results.reverted).toBe(false); - expect(results.output).toEqual([/*exists=true*/ new Fr(1)]); - - // Note hash existence check should be in trace - const trace = context.persistableState.flush(); - expect(trace.noteHashChecks).toEqual([expect.objectContaining({ noteHash, leafIndex, exists: true })]); - }); + it(`Note hash exists (it does)`, async () => { + const noteHash = new Fr(42); + const leafIndex = new Fr(7); + const calldata = [noteHash, leafIndex]; - it(`Should execute contract function to 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(8), - Buffer.concat(expectedString.map(f => f.toBuffer())), - ), - new UnencryptedL2Log(context.environment.address, new EventSelector(10), expectedCompressedString), - ]); - }); + 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); - it(`Should execute contract function to emit note hash (should be traced)`, async () => { - const utxo = new Fr(42); - const calldata = [utxo]; + expect(results.reverted).toBe(false); + expect(results.output).toEqual([/*exists=true*/ new Fr(1)]); - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - const bytecode = getAvmTestContractBytecode('new_note_hash'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); + // Note hash existence check should be in trace + const trace = context.persistableState.flush(); + expect(trace.noteHashChecks).toEqual([expect.objectContaining({ noteHash, leafIndex, exists: true })]); + }); - expect(results.reverted).toBe(false); + 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(context.persistableState.flush().newNoteHashes).toEqual([utxo]); - }); + expect(results.reverted).toBe(false); - it(`Should execute contract function to emit nullifier (should be traced)`, async () => { - const utxo = new Fr(42); - const calldata = [utxo]; + 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(8), + Buffer.concat(expectedString.map(f => f.toBuffer())), + ), + new UnencryptedL2Log(context.environment.address, new EventSelector(10), expectedCompressedString), + ]); + }); - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - const bytecode = getAvmTestContractBytecode('new_nullifier'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); + it(`Emit note hash (should be traced)`, async () => { + const utxo = new Fr(42); + const calldata = [utxo]; - expect(results.reverted).toBe(false); + const context = initContext({ env: initExecutionEnvironment({ calldata }) }); + const bytecode = getAvmTestContractBytecode('new_note_hash'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); - expect(context.persistableState.flush().newNullifiers).toEqual([utxo]); - }); + expect(results.reverted).toBe(false); - it(`Should execute contract function that checks if a nullifier exists (it does not)`, async () => { - const utxo = new Fr(42); - const calldata = [utxo]; + expect(context.persistableState.flush().newNoteHashes).toEqual([utxo]); + }); - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - const bytecode = getAvmTestContractBytecode('nullifier_exists'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); + it(`Emit nullifier (should be traced)`, async () => { + const utxo = new Fr(42); + const calldata = [utxo]; - expect(results.reverted).toBe(false); - expect(results.output).toEqual([/*exists=false*/ new Fr(0)]); + const context = initContext({ env: initExecutionEnvironment({ calldata }) }); + const bytecode = getAvmTestContractBytecode('new_nullifier'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); - // Nullifier existence check should be in trace - const trace = context.persistableState.flush(); - expect(trace.nullifierChecks.length).toEqual(1); - expect(trace.nullifierChecks[0].exists).toEqual(false); - }); + expect(results.reverted).toBe(false); - it(`Should execute contract function that checks if a 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.length).toEqual(1); - expect(trace.nullifierChecks[0].exists).toEqual(true); - }); + expect(context.persistableState.flush().newNullifiers).toEqual([utxo]); + }); - it(`Should execute contract function that checks emits a nullifier and checks its existence`, async () => { - const utxo = new Fr(42); - const calldata = [utxo]; + it(`Nullifier exists (it does not)`, async () => { + const utxo = new Fr(42); + const calldata = [utxo]; - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - const bytecode = getAvmTestContractBytecode('emit_nullifier_and_check'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); + const context = initContext({ env: initExecutionEnvironment({ calldata }) }); + const bytecode = getAvmTestContractBytecode('nullifier_exists'); + 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([utxo]); - expect(trace.nullifierChecks.length).toEqual(1); - expect(trace.nullifierChecks[0].exists).toEqual(true); - }); + expect(results.reverted).toBe(false); + expect(results.output).toEqual([/*exists=false*/ new Fr(0)]); - it(`Should execute contract function that emits same nullifier twice (should fail)`, async () => { - const utxo = new Fr(42); - const calldata = [utxo]; + // Nullifier existence check should be in trace + const trace = context.persistableState.flush(); + expect(trace.nullifierChecks.length).toEqual(1); + expect(trace.nullifierChecks[0].exists).toEqual(false); + }); - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - const bytecode = getAvmTestContractBytecode('nullifier_collision'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); + it(`Nullifier exists (it does)`, async () => { + const utxo = new Fr(42); + const calldata = [utxo]; - 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]); - }); + 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.length).toEqual(1); + expect(trace.nullifierChecks[0].exists).toEqual(true); }); - 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]; + 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('l1_to_l2_msg_exists'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); + 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); - 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(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 checks if a message exists (it does)`, async () => { - const msgHash = new Fr(42); - const leafIndex = new Fr(24); - const calldata = [msgHash, leafIndex]; - - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - jest - .spyOn(context.persistableState.hostStorage.commitmentsDb, 'getL1ToL2MembershipWitness') - .mockResolvedValue(initL1ToL2MessageOracleInput(leafIndex.toBigInt())); - const bytecode = getAvmTestContractBytecode('l1_to_l2_msg_exists'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - expect(results.reverted).toBe(false); - expect(results.output).toEqual([/*exists=false*/ new Fr(1)]); - // Message existence check should be in trace - const trace = context.persistableState.flush(); - expect(trace.l1ToL2MessageChecks.length).toEqual(1); - expect(trace.l1ToL2MessageChecks[0].exists).toEqual(true); - }); + it(`Emits same nullifier twice (should fail)`, async () => { + const utxo = new Fr(42); + const calldata = [utxo]; + + const context = initContext({ env: initExecutionEnvironment({ calldata }) }); + const bytecode = getAvmTestContractBytecode('nullifier_collision'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); + + 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 nested external calls from noir contract', () => { - it(`Should execute contract function that makes a nested call`, async () => { - const calldata: Fr[] = [new Fr(1), new Fr(2)]; - const callBytecode = getAvmTestContractBytecode('raw_nested_call_to_add'); - const addBytecode = getAvmTestContractBytecode('add_args_return'); - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValueOnce(Promise.resolve(addBytecode)); + 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 results = await new AvmSimulator(context).executeBytecode(callBytecode); + 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([new Fr(3)]); - }); + 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 makes a nested call through the old interface`, 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') - .mockReturnValueOnce(Promise.resolve(addBytecode)); + it(`Message exists (it does)`, async () => { + const msgHash = new Fr(42); + const leafIndex = new Fr(24); + const calldata = [msgHash, leafIndex]; - const results = await new AvmSimulator(context).executeBytecode(callBytecode); + const context = initContext({ env: initExecutionEnvironment({ calldata }) }); + jest + .spyOn(context.persistableState.hostStorage.commitmentsDb, 'getL1ToL2MembershipWitness') + .mockResolvedValue(initL1ToL2MessageOracleInput(leafIndex.toBigInt())); + 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([new Fr(3)]); - }); + 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); + }); + }); - it(`Should execute contract function that makes a nested static call`, async () => { - const calldata: Fr[] = [new Fr(1), new Fr(2)]; - const callBytecode = getAvmTestContractBytecode('raw_nested_static_call_to_add'); - const addBytecode = getAvmTestContractBytecode('add_args_return'); - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValueOnce(Promise.resolve(addBytecode)); + describe('Nested external calls', () => { + it(`Nested call`, async () => { + const calldata: Fr[] = [new Fr(1), new Fr(2)]; + const callBytecode = getAvmTestContractBytecode('raw_nested_call_to_add'); + const addBytecode = getAvmTestContractBytecode('add_args_return'); + const context = initContext({ env: initExecutionEnvironment({ calldata }) }); + jest + .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') + .mockReturnValueOnce(Promise.resolve(addBytecode)); - const results = await new AvmSimulator(context).executeBytecode(callBytecode); + const results = await new AvmSimulator(context).executeBytecode(callBytecode); - expect(results.reverted).toBe(false); - expect(results.output).toEqual([/*result=*/ new Fr(3), /*success=*/ new Fr(1)]); - }); + expect(results.reverted).toBe(false); + expect(results.output).toEqual([new Fr(3)]); + }); - it(`Should execute contract function that makes a nested static call which modifies storage`, async () => { - const callBytecode = getAvmTestContractBytecode('raw_nested_static_call_to_set_storage'); - const nestedBytecode = getAvmTestContractBytecode('set_storage_single'); - const context = initContext(); - jest - .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') - .mockReturnValueOnce(Promise.resolve(nestedBytecode)); + it(`Nested call through the old interface`, 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') + .mockReturnValueOnce(Promise.resolve(addBytecode)); - const results = await new AvmSimulator(context).executeBytecode(callBytecode); + const results = await new AvmSimulator(context).executeBytecode(callBytecode); - expect(results.reverted).toBe(false); // The outer call should not revert. - expect(results.output).toEqual([new Fr(0)]); // The inner call should have reverted. - }); + expect(results.reverted).toBe(false); + expect(results.output).toEqual([new Fr(3)]); + }); - it(`Should execute contract function that makes a nested static call (old interface)`, 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') - .mockReturnValueOnce(Promise.resolve(addBytecode)); + it(`Nested static call`, async () => { + const calldata: Fr[] = [new Fr(1), new Fr(2)]; + const callBytecode = getAvmTestContractBytecode('raw_nested_static_call_to_add'); + const addBytecode = getAvmTestContractBytecode('add_args_return'); + const context = initContext({ env: initExecutionEnvironment({ calldata }) }); + jest + .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') + .mockReturnValueOnce(Promise.resolve(addBytecode)); - const results = await new AvmSimulator(context).executeBytecode(callBytecode); + const results = await new AvmSimulator(context).executeBytecode(callBytecode); - expect(results.reverted).toBe(false); - expect(results.output).toEqual([/*result=*/ new Fr(3)]); - }); + expect(results.reverted).toBe(false); + expect(results.output).toEqual([/*result=*/ new Fr(3), /*success=*/ new Fr(1)]); + }); - it(`Should execute contract function that makes a nested static call which modifies storage (old interface)`, 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') - .mockReturnValueOnce(Promise.resolve(nestedBytecode)); + it(`Nested static call which modifies storage`, async () => { + const callBytecode = getAvmTestContractBytecode('raw_nested_static_call_to_set_storage'); + const nestedBytecode = getAvmTestContractBytecode('set_storage_single'); + const context = initContext(); + jest + .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') + .mockReturnValueOnce(Promise.resolve(nestedBytecode)); - 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.reverted).toBe(false); // The outer call should not revert. + expect(results.output).toEqual([new Fr(0)]); // The inner call should have reverted. }); - 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]; + it(`Nested static call (old interface)`, 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') + .mockReturnValueOnce(Promise.resolve(addBytecode)); - const context = initContext({ - env: initExecutionEnvironment({ calldata, address, storageAddress: address }), - }); - const bytecode = getAvmTestContractBytecode('set_storage_single'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); + const results = await new AvmSimulator(context).executeBytecode(callBytecode); - expect(results.reverted).toBe(false); + expect(results.reverted).toBe(false); + expect(results.output).toEqual([/*result=*/ new Fr(3)]); + }); - // World state - const worldState = context.persistableState.flush(); - const storageSlot = worldState.currentStorageValue.get(address.toBigInt())!; - const adminSlotValue = storageSlot.get(slot); - expect(adminSlotValue).toEqual(value); + it(`Nested static call which modifies storage (old interface)`, 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') + .mockReturnValueOnce(Promise.resolve(nestedBytecode)); - // Tracing - const storageTrace = worldState.storageWrites.get(address.toBigInt())!; - const slotTrace = storageTrace.get(slot); - expect(slotTrace).toEqual([value]); - }); + const results = await new AvmSimulator(context).executeBytecode(callBytecode); + + expect(results.reverted).toBe(true); // The outer call should revert. + }); + }); + + 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]; - it('Should read value in storage (single)', async () => { - const slot = 1n; - const value = new Fr(12345); - const address = AztecAddress.fromField(new Fr(420)); - const storage = new Map([[slot, value]]); - - const context = initContext({ - env: initExecutionEnvironment({ storageAddress: address }), - }); - 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(); - const storageTrace = worldState.storageReads.get(address.toBigInt())!; - const slotTrace = storageTrace.get(slot); - expect(slotTrace).toEqual([value]); + const context = initContext({ + env: initExecutionEnvironment({ calldata, address, storageAddress: address }), }); + const bytecode = getAvmTestContractBytecode('set_storage_single'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); + + expect(results.reverted).toBe(false); + + // World state + const worldState = context.persistableState.flush(); + const storageSlot = worldState.currentStorageValue.get(address.toBigInt())!; + const adminSlotValue = storageSlot.get(slot); + expect(adminSlotValue).toEqual(value); + + // Tracing + const storageTrace = worldState.storageWrites.get(address.toBigInt())!; + const slotTrace = storageTrace.get(slot); + expect(slotTrace).toEqual([value]); + }); + + it('Should read value in storage (single)', async () => { + const slot = 1n; + const value = new Fr(12345); + const address = AztecAddress.fromField(new Fr(420)); + const storage = new Map([[slot, value]]); - it('Should set and read a value from storage (single)', async () => { - const slot = 1n; - const value = new Fr(12345); - const address = AztecAddress.fromField(new Fr(420)); - const calldata = [value]; - - const context = initContext({ - env: initExecutionEnvironment({ calldata, address, storageAddress: address }), - }); - const bytecode = getAvmTestContractBytecode('set_read_storage_single'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - expect(results.reverted).toBe(false); - expect(results.output).toEqual([value]); - - // Test read trace - const worldState = context.persistableState.flush(); - const storageReadTrace = worldState.storageReads.get(address.toBigInt())!; - const slotReadTrace = storageReadTrace.get(slot); - expect(slotReadTrace).toEqual([value]); - - // Test write trace - const storageWriteTrace = worldState.storageWrites.get(address.toBigInt())!; - const slotWriteTrace = storageWriteTrace.get(slot); - expect(slotWriteTrace).toEqual([value]); + const context = initContext({ + env: initExecutionEnvironment({ storageAddress: address }), }); + 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(); + const storageTrace = worldState.storageReads.get(address.toBigInt())!; + const slotTrace = storageTrace.get(slot); + expect(slotTrace).toEqual([value]); + }); + + 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 slot = 2n; - const sender = AztecAddress.fromField(new Fr(1)); - const address = AztecAddress.fromField(new Fr(420)); - const calldata = [new Fr(1), new Fr(2)]; - - const context = initContext({ - env: initExecutionEnvironment({ sender, address, calldata, storageAddress: address }), - }); - const bytecode = getAvmTestContractBytecode('set_storage_list'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - expect(results.reverted).toBe(false); - - const worldState = context.persistableState.flush(); - const storageSlot = worldState.currentStorageValue.get(address.toBigInt())!; - expect(storageSlot.get(slot)).toEqual(calldata[0]); - expect(storageSlot.get(slot + 1n)).toEqual(calldata[1]); - - // Tracing - const storageTrace = worldState.storageWrites.get(address.toBigInt())!; - expect(storageTrace.get(slot)).toEqual([calldata[0]]); - expect(storageTrace.get(slot + 1n)).toEqual([calldata[1]]); + const context = initContext({ + env: initExecutionEnvironment({ calldata, address, storageAddress: address }), }); + const bytecode = getAvmTestContractBytecode('set_read_storage_single'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); + + expect(results.reverted).toBe(false); + expect(results.output).toEqual([value]); + + // Test read trace + const worldState = context.persistableState.flush(); + const storageReadTrace = worldState.storageReads.get(address.toBigInt())!; + const slotReadTrace = storageReadTrace.get(slot); + expect(slotReadTrace).toEqual([value]); + + // Test write trace + const storageWriteTrace = worldState.storageWrites.get(address.toBigInt())!; + const slotWriteTrace = storageWriteTrace.get(slot); + expect(slotWriteTrace).toEqual([value]); + }); + + it('Should set a value in storage (list)', async () => { + const slot = 2n; + const sender = AztecAddress.fromField(new Fr(1)); + const address = AztecAddress.fromField(new Fr(420)); + const calldata = [new Fr(1), new Fr(2)]; - it('Should read a value in storage (list)', async () => { - const slot = 2n; - const address = AztecAddress.fromField(new Fr(420)); - const values = [new Fr(1), new Fr(2)]; - const storage = new Map([ - [slot, values[0]], - [slot + 1n, values[1]], - ]); - - const context = initContext({ - env: initExecutionEnvironment({ address, storageAddress: address }), - }); - jest - .spyOn(context.persistableState.hostStorage.publicStateDb, 'storageRead') - .mockImplementation((_address, slot) => Promise.resolve(storage.get(slot.toBigInt())!)); - const bytecode = getAvmTestContractBytecode('read_storage_list'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - expect(results.reverted).toBe(false); - expect(results.output).toEqual(values); - - // Tracing - const worldState = context.persistableState.flush(); - const storageTrace = worldState.storageReads.get(address.toBigInt())!; - expect(storageTrace.get(slot)).toEqual([values[0]]); - expect(storageTrace.get(slot + 1n)).toEqual([values[1]]); + const context = initContext({ + env: initExecutionEnvironment({ sender, address, calldata, storageAddress: address }), }); + const bytecode = getAvmTestContractBytecode('set_storage_list'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); - 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(results.reverted).toBe(false); + + const worldState = context.persistableState.flush(); + const storageSlot = worldState.currentStorageValue.get(address.toBigInt())!; + expect(storageSlot.get(slot)).toEqual(calldata[0]); + expect(storageSlot.get(slot + 1n)).toEqual(calldata[1]); + + // Tracing + const storageTrace = worldState.storageWrites.get(address.toBigInt())!; + expect(storageTrace.get(slot)).toEqual([calldata[0]]); + expect(storageTrace.get(slot + 1n)).toEqual([calldata[1]]); + }); + + it('Should read a value in storage (list)', async () => { + const slot = 2n; + const address = AztecAddress.fromField(new Fr(420)); + const values = [new Fr(1), new Fr(2)]; + const storage = new Map([ + [slot, values[0]], + [slot + 1n, values[1]], + ]); + + const context = initContext({ + env: initExecutionEnvironment({ address, storageAddress: address }), + }); + 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 context = initContext({ - env: initExecutionEnvironment({ address, calldata, storageAddress: address }), - }); - const bytecode = getAvmTestContractBytecode('set_storage_map'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); + expect(results.reverted).toBe(false); + expect(results.output).toEqual(values); - expect(results.reverted).toBe(false); - // returns the storage slot for modified key - const slotNumber = results.output[0].toBigInt(); + // Tracing + const worldState = context.persistableState.flush(); + const storageTrace = worldState.storageReads.get(address.toBigInt())!; + expect(storageTrace.get(slot)).toEqual([values[0]]); + expect(storageTrace.get(slot + 1n)).toEqual([values[1]]); + }); - const worldState = context.persistableState.flush(); - const storageSlot = worldState.currentStorageValue.get(address.toBigInt())!; - expect(storageSlot.get(slotNumber)).toEqual(value); + 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]; - // Tracing - const storageTrace = worldState.storageWrites.get(address.toBigInt())!; - expect(storageTrace.get(slotNumber)).toEqual([value]); + const context = initContext({ + env: initExecutionEnvironment({ address, calldata, storageAddress: address }), }); + 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); - it('Should read-add-set a value in storage (map)', async () => { - const address = AztecAddress.fromField(new Fr(420)); - const value = new Fr(12345); - const calldata = [address.toField(), value]; - - const context = initContext({ - env: initExecutionEnvironment({ address, calldata, storageAddress: address }), - }); - const bytecode = getAvmTestContractBytecode('add_storage_map'); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - expect(results.reverted).toBe(false); - // returns the storage slot for modified key - const slotNumber = results.output[0].toBigInt(); - - const worldState = context.persistableState.flush(); - const storageSlot = worldState.currentStorageValue.get(address.toBigInt())!; - expect(storageSlot.get(slotNumber)).toEqual(value); - - // Tracing - const storageReadTrace = worldState.storageReads.get(address.toBigInt())!; - expect(storageReadTrace.get(slotNumber)).toEqual([new Fr(0)]); - const storageWriteTrace = worldState.storageWrites.get(address.toBigInt())!; - expect(storageWriteTrace.get(slotNumber)).toEqual([value]); + // Tracing + const storageTrace = worldState.storageWrites.get(address.toBigInt())!; + expect(storageTrace.get(slotNumber)).toEqual([value]); + }); + + it('Should read-add-set a value in storage (map)', async () => { + const address = AztecAddress.fromField(new Fr(420)); + const value = new Fr(12345); + const calldata = [address.toField(), value]; + + const context = initContext({ + env: initExecutionEnvironment({ address, calldata, storageAddress: address }), }); + const bytecode = getAvmTestContractBytecode('add_storage_map'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); + + expect(results.reverted).toBe(false); + // returns the storage slot for modified key + const slotNumber = results.output[0].toBigInt(); + + const worldState = context.persistableState.flush(); + const storageSlot = worldState.currentStorageValue.get(address.toBigInt())!; + expect(storageSlot.get(slotNumber)).toEqual(value); + + // Tracing + const storageReadTrace = worldState.storageReads.get(address.toBigInt())!; + expect(storageReadTrace.get(slotNumber)).toEqual([new Fr(0)]); + const storageWriteTrace = worldState.storageWrites.get(address.toBigInt())!; + expect(storageWriteTrace.get(slotNumber)).toEqual([value]); + }); + + it('Should read value in storage (map)', async () => { + const value = new Fr(12345); + const address = AztecAddress.fromField(new Fr(420)); + const calldata = [address.toField()]; - it('Should read value in storage (map)', async () => { - const value = new Fr(12345); - const address = AztecAddress.fromField(new Fr(420)); - const calldata = [address.toField()]; - - const context = initContext({ - env: initExecutionEnvironment({ calldata, address, storageAddress: address }), - }); - 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(); - const storageTrace = worldState.storageReads.get(address.toBigInt())!; - expect([...storageTrace.values()]).toEqual([[value]]); + const context = initContext({ + env: initExecutionEnvironment({ calldata, address, storageAddress: address }), }); + 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(); + const storageTrace = worldState.storageReads.get(address.toBigInt())!; + expect([...storageTrace.values()]).toEqual([[value]]); }); }); }); diff --git a/yarn-project/simulator/src/avm/fixtures/index.ts b/yarn-project/simulator/src/avm/fixtures/index.ts index 14db8d632a3..2e75d31be4c 100644 --- a/yarn-project/simulator/src/avm/fixtures/index.ts +++ b/yarn-project/simulator/src/avm/fixtures/index.ts @@ -10,7 +10,7 @@ import merge from 'lodash.merge'; import { CommitmentsDB, MessageLoadOracleInputs, PublicContractsDB, PublicStateDB } from '../../index.js'; import { AvmContext } from '../avm_context.js'; -import { AvmExecutionEnvironment } from '../avm_execution_environment.js'; +import { AvmContextInputs, AvmExecutionEnvironment } from '../avm_execution_environment.js'; import { AvmMachineState } from '../avm_machine_state.js'; import { HostStorage } from '../journal/host_storage.js'; import { AvmPersistableStateManager } from '../journal/journal.js'; @@ -113,3 +113,19 @@ export function initL1ToL2MessageOracleInput( new SiblingPath(L1_TO_L2_MSG_TREE_HEIGHT, Array(L1_TO_L2_MSG_TREE_HEIGHT)), ); } + +/** + * Adjust the user index to account for the AvmContextInputs size. + * This is a hack for testing, and should go away once AvmContextInputs themselves go away. + */ +export function adjustCalldataIndex(userIndex: number): number { + return userIndex + AvmContextInputs.SIZE; +} + +export function anyAvmContextInputs() { + const tv = []; + for (let i = 0; i < AvmContextInputs.SIZE; i++) { + tv.push(expect.any(Fr)); + } + return tv; +} 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 1df77f9d0a6..d6112104d45 100644 --- a/yarn-project/simulator/src/avm/opcodes/external_calls.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/external_calls.test.ts @@ -6,7 +6,7 @@ import { mock } from 'jest-mock-extended'; import { CommitmentsDB, PublicContractsDB, PublicStateDB } from '../../index.js'; import { AvmContext } from '../avm_context.js'; import { Field, Uint8 } from '../avm_memory_types.js'; -import { initContext } from '../fixtures/index.js'; +import { adjustCalldataIndex, initContext } from '../fixtures/index.js'; import { HostStorage } from '../journal/host_storage.js'; import { AvmPersistableStateManager } from '../journal/journal.js'; import { encodeToBytecode } from '../serialization/bytecode_serialization.js'; @@ -70,7 +70,12 @@ describe('External Calls', () => { const retSize = 2; const successOffset = 7; const otherContextInstructionsBytecode = encodeToBytecode([ - new CalldataCopy(/*indirect=*/ 0, /*csOffset=*/ 0, /*copySize=*/ argsSize, /*dstOffset=*/ 0), + new CalldataCopy( + /*indirect=*/ 0, + /*csOffset=*/ adjustCalldataIndex(0), + /*copySize=*/ argsSize, + /*dstOffset=*/ 0, + ), new SStore(/*indirect=*/ 0, /*srcOffset=*/ 0, /*size=*/ 1, /*slotOffset=*/ 0), new Return(/*indirect=*/ 0, /*retOffset=*/ 0, /*size=*/ 2), ]); @@ -162,7 +167,12 @@ describe('External Calls', () => { context.machineState.memory.setSlice(2, args); const otherContextInstructions: Instruction[] = [ - new CalldataCopy(/*indirect=*/ 0, /*csOffset=*/ 0, /*copySize=*/ argsSize, /*dstOffset=*/ 0), + new CalldataCopy( + /*indirect=*/ 0, + /*csOffset=*/ adjustCalldataIndex(0), + /*copySize=*/ argsSize, + /*dstOffset=*/ 0, + ), new SStore(/*indirect=*/ 0, /*srcOffset=*/ 1, /*size=*/ 1, /*slotOffset=*/ 0), ]; diff --git a/yarn-project/simulator/src/avm/opcodes/memory.test.ts b/yarn-project/simulator/src/avm/opcodes/memory.test.ts index d7b691435a1..0b8102f98c7 100644 --- a/yarn-project/simulator/src/avm/opcodes/memory.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/memory.test.ts @@ -3,7 +3,7 @@ import { Fr } from '@aztec/foundation/fields'; import { AvmContext } from '../avm_context.js'; import { Field, TypeTag, Uint8, Uint16, Uint32, Uint64, Uint128 } from '../avm_memory_types.js'; import { InstructionExecutionError } from '../errors.js'; -import { initContext, initExecutionEnvironment } from '../fixtures/index.js'; +import { adjustCalldataIndex, initContext, initExecutionEnvironment } from '../fixtures/index.js'; import { Addressing, AddressingMode } from './addressing_mode.js'; import { CMov, CalldataCopy, Cast, Mov, Set } from './memory.js'; @@ -435,7 +435,12 @@ describe('Memory instructions', () => { context = initContext({ env: initExecutionEnvironment({ calldata }) }); context.machineState.memory.set(0, new Uint16(12)); // Some previous data to be overwritten - await new CalldataCopy(/*indirect=*/ 0, /*cdOffset=*/ 0, /*copySize=*/ 0, /*dstOffset=*/ 0).execute(context); + await new CalldataCopy( + /*indirect=*/ 0, + /*cdOffset=*/ adjustCalldataIndex(0), + /*copySize=*/ 0, + /*dstOffset=*/ 0, + ).execute(context); const actual = context.machineState.memory.get(0); expect(actual).toEqual(new Uint16(12)); @@ -446,7 +451,12 @@ describe('Memory instructions', () => { context = initContext({ env: initExecutionEnvironment({ calldata }) }); context.machineState.memory.set(0, new Uint16(12)); // Some previous data to be overwritten - await new CalldataCopy(/*indirect=*/ 0, /*cdOffset=*/ 0, /*copySize=*/ 3, /*dstOffset=*/ 0).execute(context); + await new CalldataCopy( + /*indirect=*/ 0, + /*cdOffset=*/ adjustCalldataIndex(0), + /*copySize=*/ 3, + /*dstOffset=*/ 0, + ).execute(context); const actual = context.machineState.memory.getSlice(/*offset=*/ 0, /*size=*/ 3); expect(actual).toEqual([new Field(1), new Field(2), new Field(3)]); @@ -457,7 +467,12 @@ describe('Memory instructions', () => { context = initContext({ env: initExecutionEnvironment({ calldata }) }); context.machineState.memory.set(0, new Uint16(12)); // Some previous data to be overwritten - await new CalldataCopy(/*indirect=*/ 0, /*cdOffset=*/ 1, /*copySize=*/ 2, /*dstOffset=*/ 0).execute(context); + await new CalldataCopy( + /*indirect=*/ 0, + /*cdOffset=*/ adjustCalldataIndex(1), + /*copySize=*/ 2, + /*dstOffset=*/ 0, + ).execute(context); const actual = context.machineState.memory.getSlice(/*offset=*/ 0, /*size=*/ 2); expect(actual).toEqual([new Field(2), new Field(3)]);