From 4102e8db459dc648e759b45dc55376f2e22fc8df Mon Sep 17 00:00:00 2001 From: fcarreiro Date: Wed, 6 Mar 2024 14:33:36 +0000 Subject: [PATCH] based on david's --- avm-transpiler/src/opcodes.rs | 22 ---- avm-transpiler/src/transpile.rs | 86 ++++++++++++++- .../aztec-nr/aztec/src/context/avm.nr | 38 ++++++- .../contracts/avm_test_contract/src/main.nr | 35 ++++++ .../src/brillig/brillig_gen/brillig_block.rs | 5 +- .../noirc_evaluator/src/brillig/brillig_ir.rs | 25 ++--- .../simulator/src/avm/avm_context.test.ts | 6 +- yarn-project/simulator/src/avm/avm_context.ts | 26 ++++- .../src/avm/avm_execution_environment.ts | 39 ++++--- .../simulator/src/avm/avm_memory_types.ts | 39 ++++++- .../simulator/src/avm/avm_simulator.test.ts | 17 +++ .../simulator/src/avm/avm_simulator.ts | 13 ++- .../simulator/src/avm/opcodes/arithmetic.ts | 6 +- .../simulator/src/avm/opcodes/comparators.ts | 10 +- .../src/avm/opcodes/control_flow.test.ts | 72 +------------ .../simulator/src/avm/opcodes/control_flow.ts | 48 +-------- .../src/avm/opcodes/external_calls.test.ts | 79 +++++++++++++- .../src/avm/opcodes/external_calls.ts | 102 +++++++++++++++--- .../bytecode_serialization.test.ts | 4 + .../instruction_serialization.ts | 1 - 20 files changed, 459 insertions(+), 214 deletions(-) diff --git a/avm-transpiler/src/opcodes.rs b/avm-transpiler/src/opcodes.rs index b297958b498..6c4fa503a9f 100644 --- a/avm-transpiler/src/opcodes.rs +++ b/avm-transpiler/src/opcodes.rs @@ -2,27 +2,20 @@ /// Keep updated with TS and yellow paper! #[derive(Copy, Clone)] pub enum AvmOpcode { - // Compute - // Compute - Arithmetic ADD, SUB, MUL, DIV, - // Compute - Comparators EQ, LT, LTE, - // Compute - Bitwise AND, OR, XOR, NOT, SHL, SHR, - // Compute - Type Conversions CAST, - - // Execution Environment ADDRESS, STORAGEADDRESS, ORIGIN, @@ -32,7 +25,6 @@ pub enum AvmOpcode { FEEPERL2GAS, FEEPERDAGAS, CONTRACTCALLDEPTH, - // Execution Environment - Globals CHAINID, VERSION, BLOCKNUMBER, @@ -41,25 +33,17 @@ pub enum AvmOpcode { BLOCKL1GASLIMIT, BLOCKL2GASLIMIT, BLOCKDAGASLIMIT, - // Execution Environment - Calldata CALLDATACOPY, - - // Machine State - // Machine State - Gas L1GASLEFT, L2GASLEFT, DAGASLEFT, - // Machine State - Internal Control Flow JUMP, JUMPI, INTERNALCALL, INTERNALRETURN, - // Machine State - Memory SET, MOV, CMOV, - - // World State SLOAD, // Public Storage SSTORE, // Public Storage NOTEHASHEXISTS, // Notes & Nullifiers @@ -68,19 +52,13 @@ pub enum AvmOpcode { EMITNULLIFIER, // Notes & Nullifiers L1TOL2MSGEXISTS, // Messages HEADERMEMBER, // Archive tree & Headers - - // Accrued Substate EMITUNENCRYPTEDLOG, SENDL2TOL1MSG, - - // Control Flow - Contract Calls CALL, STATICCALL, DELEGATECALL, RETURN, REVERT, - - // Gadgets KECCAK, POSEIDON, SHA256, diff --git a/avm-transpiler/src/transpile.rs b/avm-transpiler/src/transpile.rs index a5ca11a5a47..9165fd78d80 100644 --- a/avm-transpiler/src/transpile.rs +++ b/avm-transpiler/src/transpile.rs @@ -62,6 +62,7 @@ pub fn brillig_to_avm(brillig: &Brillig) -> Vec { lhs, rhs, } => { + assert!(is_integral_bit_size(*bit_size), "BinaryIntOp::{:?} bit_size must be integral, got {:?}", op, bit_size); let avm_opcode = match op { BinaryIntOp::Add => AvmOpcode::ADD, BinaryIntOp::Sub => AvmOpcode::SUB, @@ -179,7 +180,7 @@ pub fn brillig_to_avm(brillig: &Brillig) -> Vec { BrilligOpcode::Stop { return_data_offset, return_data_size } => { avm_instrs.push(AvmInstruction { opcode: AvmOpcode::RETURN, - indirect: Some(ALL_DIRECT), + indirect: Some(ZEROTH_OPERAND_INDIRECT), operands: vec![ AvmOperand::U32 { value: *return_data_offset as u32 }, AvmOperand::U32 { value: *return_data_size as u32 }, @@ -238,6 +239,7 @@ fn handle_foreign_call( inputs: &Vec, ) { match function { + "avmOpcodeCall" => handle_external_call(avm_instrs, destinations, inputs), "amvOpcodeEmitUnencryptedLog" => { handle_emit_unencrypted_log(avm_instrs, destinations, inputs) }, @@ -271,6 +273,81 @@ fn handle_foreign_call( } } +/// Handle an AVM CALL +/// (an external 'call' brillig foreign call was encountered) +/// Adds the new instruction to the avm instructions list. +fn handle_external_call( + avm_instrs: &mut Vec, + destinations: &Vec, + inputs: &Vec, +) { + if destinations.len() != 2 || inputs.len() != 4 { + panic!( + "Transpiler expects ForeignCall::CALL to have 2 destinations and 4 inputs, got {} and {}. + Make sure your call instructions's input/return arrays have static length (`[Field; ]`)!", + destinations.len(), + inputs.len() + ); + } + let gas_offset_maybe = inputs[0]; + let gas_offset = match gas_offset_maybe { + ValueOrArray::HeapArray(HeapArray { pointer, size }) => { + assert!(size == 3, "Call instruction's gas input should be a HeapArray of size 3 (`[l1Gas, l2Gas, daGas]`)"); + pointer.0 as u32 + } + ValueOrArray::HeapVector(_) => panic!("Call instruction's gas input must be a HeapArray, not a HeapVector. Make sure you are explicitly defining its size as 3 (`[l1Gas, l2Gas, daGas]`)!"), + _ => panic!("Call instruction's gas input should be a HeapArray"), + }; + let address_offset = match &inputs[1] { + ValueOrArray::MemoryAddress(offset) => offset.to_usize() as u32, + _ => panic!("Call instruction's target address input should be a basic MemoryAddress",), + }; + let args_offset_maybe = inputs[2]; + let (args_offset, args_size) = match args_offset_maybe { + ValueOrArray::HeapArray(HeapArray { pointer, size }) => (pointer.0 as u32, size as u32), + ValueOrArray::HeapVector(_) => panic!("Call instruction's args must be a HeapArray, not a HeapVector. Make sure you are explicitly defining its size (`[arg0, arg1, ... argN]`)!"), + _ => panic!("Call instruction's args input should be a HeapArray input"), + }; + let temporary_function_selector_offset = match &inputs[3] { + ValueOrArray::MemoryAddress(offset) => offset.to_usize() as u32, + _ => panic!( + "Call instruction's temporary function selector input should be a basic MemoryAddress", + ), + }; + + let ret_offset_maybe = destinations[0]; + let (ret_offset, ret_size) = match ret_offset_maybe { + ValueOrArray::HeapArray(HeapArray { pointer, size }) => (pointer.0 as u32, size as u32), + ValueOrArray::HeapVector(_) => panic!("Call instruction's return data must be a HeapArray, not a HeapVector. Make sure you are explicitly defining its size (`let returnData: [Field; ] = ...`)!"), + _ => panic!("Call instruction's returnData destination should be a HeapArray input"), + }; + let success_offset = match &destinations[1] { + ValueOrArray::MemoryAddress(offset) => offset.to_usize() as u32, + _ => panic!("Call instruction's success destination should be a basic MemoryAddress",), + }; + avm_instrs.push(AvmInstruction { + opcode: AvmOpcode::CALL, + indirect: Some(0b01101), // (left to right) selector direct, ret offset INDIRECT, args offset INDIRECT, address offset direct, gas offset INDIRECT + operands: vec![ + AvmOperand::U32 { value: gas_offset }, + AvmOperand::U32 { + value: address_offset, + }, + AvmOperand::U32 { value: args_offset }, + AvmOperand::U32 { value: args_size }, + AvmOperand::U32 { value: ret_offset }, + AvmOperand::U32 { value: ret_size }, + AvmOperand::U32 { + value: success_offset, + }, + AvmOperand::U32 { + value: temporary_function_selector_offset, + }, + ], + ..Default::default() + }); +} + /// Handle an AVM NOTEHASHEXISTS instruction /// Adds the new instruction to the avm instructions list. fn handle_note_hash_exists( @@ -913,6 +990,13 @@ fn map_brillig_pcs_to_avm_pcs(initial_offset: usize, brillig: &Brillig) -> Vec bool { + match bit_size { + 1 | 8 | 16 | 32 | 64 | 128 => true, + _ => false, + } +} + fn tag_from_bit_size(bit_size: u32) -> AvmTypeTag { match bit_size { 1 => AvmTypeTag::UINT8, // temp workaround diff --git a/noir-projects/aztec-nr/aztec/src/context/avm.nr b/noir-projects/aztec-nr/aztec/src/context/avm.nr index 0d2c98e82e2..2548c31c862 100644 --- a/noir-projects/aztec-nr/aztec/src/context/avm.nr +++ b/noir-projects/aztec-nr/aztec/src/context/avm.nr @@ -1,5 +1,6 @@ use dep::protocol_types::{address::{AztecAddress, EthAddress}, constants::L1_TO_L2_MESSAGE_LENGTH}; use dep::protocol_types::traits::{Serialize}; +use dep::protocol_types::abis::function_selector::FunctionSelector; // Getters that will be converted by the transpiler into their // own opcodes @@ -80,9 +81,19 @@ impl AVMContext { #[oracle(avmOpcodeSendL2ToL1Msg)] pub fn send_l2_to_l1_msg(self, recipient: EthAddress, content: Field) {} - /////////////////////////////////////////////////////////////////////////// - // The functions below allow interface-equivalence with PrivateContext - /////////////////////////////////////////////////////////////////////////// + #[oracle(avmOpcodeCall)] + fn call( + self, + gas: [Field; 3], // gas allocation: [l1Gas, l2Gas, daGas] + address: AztecAddress, + args: [Field; ARGS_COUNT], + temporary_function_selector: Field + ) -> ([Field; RET_SIZE], u8) {} + // ^ return data ^ success + + //////////////////////////////////////////////////////////////////////////////// + // The functions below allow interface-equivalence with current public/private + //////////////////////////////////////////////////////////////////////////////// pub fn this_address(self) -> AztecAddress { self.address() } @@ -107,4 +118,25 @@ impl AVMContext { // Cannot nullify pending commitments in AVM, so `nullified_commitment` is not used self.emit_nullifier(nullifier); } + + pub fn call_public_function( + self: Self, + contract_address: AztecAddress, + temporary_function_selector: FunctionSelector, + args: [Field; ARGS_COUNT] + ) -> [Field; RET_SIZE] { + let gas = [/*l1Gas*/42, /*l2Gas*/24, /*daGas*/420]; + + let results = self.call( + gas, + contract_address, + args, + temporary_function_selector.to_field() + ); + let returnData: [Field; RET_SIZE] = results.0; + let success: u8 = results.1; + assert(success == 1, "Nested call failed!"); + + returnData + } } 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 f039bb367e3..a1a11bc9338 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 @@ -2,6 +2,7 @@ contract AvmTest { // Libs use dep::aztec::state_vars::PublicMutable; use dep::aztec::protocol_types::{address::{AztecAddress, EthAddress}, constants::L1_TO_L2_MESSAGE_LENGTH}; + use dep::aztec::protocol_types::abis::function_selector::FunctionSelector; use dep::compressed_string::CompressedString; // avm lib @@ -214,4 +215,38 @@ contract AvmTest { fn send_l2_to_l1_msg(recipient: EthAddress, content: Field) { context.message_portal(recipient, content) } + + // Directly call the external call opcode to initiate a nested call to the add function + #[aztec(public-vm)] + fn raw_nested_call_to_add(argA: Field, argB: Field) -> pub Field { + let selector = FunctionSelector::from_signature("avm_addArgsReturn(Field,Field)").to_field(); + let gas = [/*l1Gas*/42, /*l2Gas*/24, /*daGas*/420]; + + // Nested call + let results = context.call(gas, context.address(), [argA, argB], selector); + let returnData: [Field; 1] = results.0; + // this explicit size ^ is necessary to ensure that retSize is compile-time + // (ensure the returnData is in a HeapArray not a HeapVector) + let success: u8 = results.1; + + assert(success == 1, "Call failed"); + + let addResult = returnData[0]; + // assert(addResult == argA + argB, "Return value from nested 'add' call incorrect"); + addResult + } + + // Use the `call_public_function` wrapper to initiate a nested call to the add function + #[aztec(public-vm)] + fn nested_call_to_add(argA: Field, argB: Field) -> pub Field { + let selector = FunctionSelector::from_signature("avm_addArgsReturn(Field,Field)"); + + // Nested call using standard context interface function + let returnData: [Field; 1] = context.call_public_function(context.address(), selector, [argA, argB]); + // this explicit size ^ is necessary to ensure that retSize is compile-time + // (ensure the returnData is in a HeapArray not a HeapVector) + + let addResult = returnData[0]; + addResult + } } diff --git a/noir/noir-repo/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_block.rs b/noir/noir-repo/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_block.rs index c04d8475f08..37d58fe45bc 100644 --- a/noir/noir-repo/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_block.rs +++ b/noir/noir-repo/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_block.rs @@ -831,14 +831,15 @@ impl<'block> BrilligBlock<'block> { _ => unreachable!("ICE: array set on non-array"), }; - let one = self.brillig_context.make_usize_constant(1_usize.into()); + let bit_size: u32 = 64; // REFERENCE_COUNT_SIZE? + let one = self.brillig_context.make_constant(1_usize.into(), bit_size); let condition = self.brillig_context.allocate_register(); self.brillig_context.binary_instruction( reference_count, one, condition, - BrilligBinaryOp::Field { op: BinaryFieldOp::Equals }, + BrilligBinaryOp::Integer { op: BinaryIntOp::Equals, bit_size } ); self.brillig_context.branch_instruction(condition, |ctx, cond| { diff --git a/noir/noir-repo/compiler/noirc_evaluator/src/brillig/brillig_ir.rs b/noir/noir-repo/compiler/noirc_evaluator/src/brillig/brillig_ir.rs index 662dc074d98..a03b279afec 100644 --- a/noir/noir-repo/compiler/noirc_evaluator/src/brillig/brillig_ir.rs +++ b/noir/noir-repo/compiler/noirc_evaluator/src/brillig/brillig_ir.rs @@ -27,7 +27,6 @@ use acvm::{ FieldElement, }; use debug_show::DebugShow; -use num_bigint::BigUint; /// The Brillig VM does not apply a limit to the memory address space, /// As a convention, we take use 64 bits. This means that we assume that @@ -217,7 +216,7 @@ impl BrilligContext { array_ptr, index, index_of_element_in_memory, - BrilligBinaryOp::Field { op: BinaryFieldOp::Add }, + BrilligBinaryOp::Integer { op: BinaryIntOp::Add, bit_size: BRILLIG_MEMORY_ADDRESSING_BIT_SIZE }, ); self.load_instruction(result, index_of_element_in_memory); @@ -239,7 +238,7 @@ impl BrilligContext { array_ptr, index, index_of_element_in_memory, - BrilligBinaryOp::Field { op: BinaryFieldOp::Add }, + BrilligBinaryOp::Integer { op: BinaryIntOp::Add, bit_size: BRILLIG_MEMORY_ADDRESSING_BIT_SIZE }, ); self.store_instruction(index_of_element_in_memory, value); @@ -744,20 +743,12 @@ impl BrilligContext { value_to_truncate.bit_size ); - let mask = BigUint::from(2_u32).pow(bit_size) - BigUint::from(1_u32); - let mask_constant = self.make_constant( - FieldElement::from_be_bytes_reduce(&mask.to_bytes_be()).into(), - value_to_truncate.bit_size, - ); - - self.binary_instruction( - value_to_truncate.address, - mask_constant, - destination_of_truncated_value.address, - BrilligBinaryOp::Integer { op: BinaryIntOp::And, bit_size: value_to_truncate.bit_size }, - ); - - self.deallocate_register(mask_constant); + // We cast back and forth to ensure that the value is truncated. + let intermediate_register = + SingleAddrVariable { address: self.allocate_register(), bit_size }; + self.cast_instruction(intermediate_register, value_to_truncate); + self.cast_instruction(destination_of_truncated_value, intermediate_register); + self.deallocate_register(intermediate_register.address); } /// Emits a stop instruction diff --git a/yarn-project/simulator/src/avm/avm_context.test.ts b/yarn-project/simulator/src/avm/avm_context.test.ts index e2a29c8be9b..90b74583cbf 100644 --- a/yarn-project/simulator/src/avm/avm_context.test.ts +++ b/yarn-project/simulator/src/avm/avm_context.test.ts @@ -1,4 +1,4 @@ -import { AztecAddress, Fr } from '@aztec/circuits.js'; +import { AztecAddress, Fr, FunctionSelector } from '@aztec/circuits.js'; import { allSameExcept, initContext } from './fixtures/index.js'; @@ -9,7 +9,7 @@ describe('Avm Context', () => { const newAddress = AztecAddress.random(); const newCalldata = [new Fr(1), new Fr(2)]; - const newContext = context.createNestedContractCallContext(newAddress, newCalldata); + const newContext = context.createNestedContractCallContext(newAddress, newCalldata, FunctionSelector.empty()); expect(newContext.environment).toEqual( allSameExcept(context.environment, { @@ -35,7 +35,7 @@ describe('Avm Context', () => { const newAddress = AztecAddress.random(); const newCalldata = [new Fr(1), new Fr(2)]; - const newContext = context.createNestedContractStaticCallContext(newAddress, newCalldata); + const newContext = context.createNestedContractStaticCallContext(newAddress, newCalldata, FunctionSelector.empty()); expect(newContext.environment).toEqual( allSameExcept(context.environment, { diff --git a/yarn-project/simulator/src/avm/avm_context.ts b/yarn-project/simulator/src/avm/avm_context.ts index 1a20c101ce7..70cb91db98c 100644 --- a/yarn-project/simulator/src/avm/avm_context.ts +++ b/yarn-project/simulator/src/avm/avm_context.ts @@ -1,4 +1,4 @@ -import { AztecAddress } from '@aztec/circuits.js'; +import { AztecAddress, FunctionSelector } from '@aztec/circuits.js'; import { Fr } from '@aztec/foundation/fields'; import { AvmExecutionEnvironment } from './avm_execution_environment.js'; @@ -35,8 +35,16 @@ export class AvmContext { * @param calldata - Data/arguments for nested call * @returns new AvmContext instance */ - public createNestedContractCallContext(address: AztecAddress, calldata: Fr[]): AvmContext { - const newExecutionEnvironment = this.environment.deriveEnvironmentForNestedCall(address, calldata); + public createNestedContractCallContext( + address: AztecAddress, + calldata: Fr[], + temporaryFunctionSelector: FunctionSelector, + ): AvmContext { + const newExecutionEnvironment = this.environment.deriveEnvironmentForNestedCall( + address, + calldata, + temporaryFunctionSelector, + ); const forkedWorldState = this.persistableState.fork(); const machineState = AvmMachineState.fromState(this.machineState); return new AvmContext(forkedWorldState, newExecutionEnvironment, machineState); @@ -54,8 +62,16 @@ export class AvmContext { * @param calldata - Data/arguments for nested call * @returns new AvmContext instance */ - public createNestedContractStaticCallContext(address: AztecAddress, calldata: Fr[]): AvmContext { - const newExecutionEnvironment = this.environment.deriveEnvironmentForNestedStaticCall(address, calldata); + public createNestedContractStaticCallContext( + address: AztecAddress, + calldata: Fr[], + temporaryFunctionSelector: FunctionSelector, + ): AvmContext { + const newExecutionEnvironment = this.environment.deriveEnvironmentForNestedStaticCall( + address, + calldata, + temporaryFunctionSelector, + ); const forkedWorldState = this.persistableState.fork(); const machineState = AvmMachineState.fromState(this.machineState); return new AvmContext(forkedWorldState, newExecutionEnvironment, machineState); diff --git a/yarn-project/simulator/src/avm/avm_execution_environment.ts b/yarn-project/simulator/src/avm/avm_execution_environment.ts index 1317cc71fdd..23340dd42b8 100644 --- a/yarn-project/simulator/src/avm/avm_execution_environment.ts +++ b/yarn-project/simulator/src/avm/avm_execution_environment.ts @@ -36,12 +36,19 @@ export class AvmExecutionEnvironment { public readonly calldata: Fr[], + // Function selector is temporary since eventually public contract bytecode will be one blob + // containing all functions, and function selector will become an application-level mechanism + // (e.g. first few bytes of calldata + compiler-generated jump table) public readonly temporaryFunctionSelector: FunctionSelector, ) {} - public deriveEnvironmentForNestedCall(address: AztecAddress, calldata: Fr[]): AvmExecutionEnvironment { + public deriveEnvironmentForNestedCall( + address: AztecAddress, + calldata: Fr[], + temporaryFunctionSelector: FunctionSelector = FunctionSelector.empty(), + ): AvmExecutionEnvironment { return new AvmExecutionEnvironment( - /*address=*/ address, + address, /*storageAddress=*/ address, this.origin, this.sender, @@ -53,14 +60,18 @@ export class AvmExecutionEnvironment { this.globals, this.isStaticCall, this.isDelegateCall, - /*calldata=*/ calldata, - this.temporaryFunctionSelector, + calldata, + temporaryFunctionSelector, ); } - public deriveEnvironmentForNestedStaticCall(address: AztecAddress, calldata: Fr[]): AvmExecutionEnvironment { + public deriveEnvironmentForNestedStaticCall( + address: AztecAddress, + calldata: Fr[], + temporaryFunctionSelector: FunctionSelector = FunctionSelector.empty(), + ): AvmExecutionEnvironment { return new AvmExecutionEnvironment( - /*address=*/ address, + address, /*storageAddress=*/ address, this.origin, this.sender, @@ -72,14 +83,18 @@ export class AvmExecutionEnvironment { this.globals, /*isStaticCall=*/ true, this.isDelegateCall, - /*calldata=*/ calldata, - this.temporaryFunctionSelector, + calldata, + temporaryFunctionSelector, ); } - public newDelegateCall(address: AztecAddress, calldata: Fr[]): AvmExecutionEnvironment { + public newDelegateCall( + address: AztecAddress, + calldata: Fr[], + temporaryFunctionSelector: FunctionSelector = FunctionSelector.empty(), + ): AvmExecutionEnvironment { return new AvmExecutionEnvironment( - /*address=*/ address, + address, this.storageAddress, this.origin, this.sender, @@ -91,8 +106,8 @@ export class AvmExecutionEnvironment { this.globals, this.isStaticCall, /*isDelegateCall=*/ true, - /*calldata=*/ calldata, - this.temporaryFunctionSelector, + calldata, + temporaryFunctionSelector, ); } } diff --git a/yarn-project/simulator/src/avm/avm_memory_types.ts b/yarn-project/simulator/src/avm/avm_memory_types.ts index 48e56ed3026..0645280a548 100644 --- a/yarn-project/simulator/src/avm/avm_memory_types.ts +++ b/yarn-project/simulator/src/avm/avm_memory_types.ts @@ -1,5 +1,6 @@ import { toBufferBE } from '@aztec/foundation/bigint-buffer'; import { Fr } from '@aztec/foundation/fields'; +import { DebugLogger, createDebugLogger } from '@aztec/foundation/log'; import { strict as assert } from 'assert'; @@ -28,6 +29,10 @@ export abstract class MemoryValue { public toFr(): Fr { return new Fr(this.toBigInt()); } + + public toString(): string { + return `${this.constructor.name}(0x${this.toBigInt().toString(16)})`; + } } /** IntegralValue gathers the common operations for all integral memory types. */ @@ -189,6 +194,8 @@ export enum TypeTag { // TODO: Consider automatic conversion when getting undefined values. export class TaggedMemory { + private log: DebugLogger = createDebugLogger('aztec:avm_simulator:memory'); + // FIXME: memory should be 2^32, but TS doesn't allow for arrays that big. static readonly MAX_MEMORY_SIZE = Number((1n << 32n) - 2n); private _mem: MemoryValue[]; @@ -200,25 +207,29 @@ export class TaggedMemory { public get(offset: number): MemoryValue { assert(offset < TaggedMemory.MAX_MEMORY_SIZE); - return this.getAs(offset); + const value = this.getAs(offset); + return value; } public getAs(offset: number): T { assert(offset < TaggedMemory.MAX_MEMORY_SIZE); const word = this._mem[offset]; + this.log(`get(${offset}) = ${word}`); return word as T; } public getSlice(offset: number, size: number): MemoryValue[] { assert(offset < TaggedMemory.MAX_MEMORY_SIZE); assert(offset + size < TaggedMemory.MAX_MEMORY_SIZE); - return this._mem.slice(offset, offset + size); + const value = this._mem.slice(offset, offset + size); + this.log(`getSlice(${offset}, ${size}) = ${value}`); + return value; } public getSliceAs(offset: number, size: number): T[] { assert(offset < TaggedMemory.MAX_MEMORY_SIZE); assert(offset + size < TaggedMemory.MAX_MEMORY_SIZE); - return this._mem.slice(offset, offset + size) as T[]; + return this.getSlice(offset, size) as T[]; } public getSliceTags(offset: number, size: number): TypeTag[] { @@ -230,6 +241,7 @@ export class TaggedMemory { public set(offset: number, v: MemoryValue) { assert(offset < TaggedMemory.MAX_MEMORY_SIZE); this._mem[offset] = v; + this.log(`set(${offset}, ${v})`); } public setSlice(offset: number, vs: MemoryValue[]) { @@ -240,6 +252,7 @@ export class TaggedMemory { this._mem.length = offset + vs.length; } this._mem.splice(offset, vs.length, ...vs); + this.log(`setSlice(${offset}, ${vs})`); } public getTag(offset: number): TypeTag { @@ -327,4 +340,24 @@ export class TaggedMemory { throw new Error(`${TypeTag[tag]} is not a valid integral type.`); } } + + // Does not truncate. Type constructor will check that it fits. + public static buildFromTagOrDie(v: bigint | number, tag: TypeTag): MemoryValue { + switch (tag) { + case TypeTag.UINT8: + return new Uint8(v); + case TypeTag.UINT16: + return new Uint16(v); + case TypeTag.UINT32: + return new Uint32(v); + case TypeTag.UINT64: + return new Uint64(v); + case TypeTag.UINT128: + return new Uint128(v); + case TypeTag.FIELD: + return new Field(v); + default: + throw new Error(`${TypeTag[tag]} is not a valid integral type.`); + } + } } diff --git a/yarn-project/simulator/src/avm/avm_simulator.test.ts b/yarn-project/simulator/src/avm/avm_simulator.test.ts index e10905caa36..853b7111799 100644 --- a/yarn-project/simulator/src/avm/avm_simulator.test.ts +++ b/yarn-project/simulator/src/avm/avm_simulator.test.ts @@ -394,6 +394,23 @@ describe('AVM simulator', () => { }); }); + 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('avm_raw_nested_call_to_add'); + const addBytecode = getAvmTestContractBytecode('avm_addArgsReturn'); + 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); + + expect(results.reverted).toBe(false); + expect(results.output).toEqual([new Fr(3)]); + }); + }); + describe('Storage accesses', () => { it('Should set a single value in storage', async () => { // We are setting the owner diff --git a/yarn-project/simulator/src/avm/avm_simulator.ts b/yarn-project/simulator/src/avm/avm_simulator.ts index 7cbb99b0dc5..8ef55049780 100644 --- a/yarn-project/simulator/src/avm/avm_simulator.ts +++ b/yarn-project/simulator/src/avm/avm_simulator.ts @@ -9,9 +9,13 @@ import type { Instruction } from './opcodes/index.js'; import { decodeFromBytecode } from './serialization/bytecode_serialization.js'; export class AvmSimulator { - private log: DebugLogger = createDebugLogger('aztec:avm_simulator'); + private log: DebugLogger; - constructor(private context: AvmContext) {} + constructor(private context: AvmContext) { + this.log = createDebugLogger( + `aztec:avm_simulator:core(f:${context.environment.temporaryFunctionSelector.toString()})`, + ); + } /** * Fetch the bytecode and execute it in the current context. @@ -52,7 +56,10 @@ export class AvmSimulator { // continuing until the machine state signifies a halt while (!this.context.machineState.halted) { const instruction = instructions[this.context.machineState.pc]; - assert(!!instruction); // This should never happen + assert( + !!instruction, + 'AVM attempted to execute non-existent instruction. This should never happen (invalid bytecode or AVM simulator bug)!', + ); this.log.debug(`@${this.context.machineState.pc} ${instruction.toString()}`); // Execute the instruction. diff --git a/yarn-project/simulator/src/avm/opcodes/arithmetic.ts b/yarn-project/simulator/src/avm/opcodes/arithmetic.ts index 91c5a44a6dc..d31976969b0 100644 --- a/yarn-project/simulator/src/avm/opcodes/arithmetic.ts +++ b/yarn-project/simulator/src/avm/opcodes/arithmetic.ts @@ -1,4 +1,5 @@ import type { AvmContext } from '../avm_context.js'; +import { TaggedMemory } from '../avm_memory_types.js'; import { Opcode } from '../serialization/instruction_serialization.js'; import { ThreeOperandInstruction } from './instruction_impl.js'; @@ -11,10 +12,13 @@ export class Add extends ThreeOperandInstruction { } async execute(context: AvmContext): Promise { + context.machineState.memory.checkTags(this.inTag, this.aOffset, this.bOffset); + const a = context.machineState.memory.get(this.aOffset); const b = context.machineState.memory.get(this.bOffset); - const dest = a.add(b); + // const dest = a.add(b); + const dest = TaggedMemory.buildFromTagOrDie(a.toBigInt() + b.toBigInt(), this.inTag); context.machineState.memory.set(this.dstOffset, dest); context.machineState.incrementPc(); diff --git a/yarn-project/simulator/src/avm/opcodes/comparators.ts b/yarn-project/simulator/src/avm/opcodes/comparators.ts index 3f896248738..00cb33708d5 100644 --- a/yarn-project/simulator/src/avm/opcodes/comparators.ts +++ b/yarn-project/simulator/src/avm/opcodes/comparators.ts @@ -1,4 +1,5 @@ import type { AvmContext } from '../avm_context.js'; +import { TaggedMemory } from '../avm_memory_types.js'; import { Opcode } from '../serialization/instruction_serialization.js'; import { ThreeOperandInstruction } from './instruction_impl.js'; @@ -16,8 +17,7 @@ export class Eq extends ThreeOperandInstruction { const a = context.machineState.memory.get(this.aOffset); const b = context.machineState.memory.get(this.bOffset); - // Result will be of the same type as 'a'. - const dest = a.build(a.equals(b) ? 1n : 0n); + const dest = TaggedMemory.buildFromTagOrDie(a.equals(b) ? 1n : 0n, this.inTag); context.machineState.memory.set(this.dstOffset, dest); context.machineState.incrementPc(); @@ -38,8 +38,7 @@ export class Lt extends ThreeOperandInstruction { const a = context.machineState.memory.get(this.aOffset); const b = context.machineState.memory.get(this.bOffset); - // Result will be of the same type as 'a'. - const dest = a.build(a.lt(b) ? 1n : 0n); + const dest = TaggedMemory.buildFromTagOrDie(a.lt(b) ? 1n : 0n, this.inTag); context.machineState.memory.set(this.dstOffset, dest); context.machineState.incrementPc(); @@ -60,8 +59,7 @@ export class Lte extends ThreeOperandInstruction { const a = context.machineState.memory.get(this.aOffset); const b = context.machineState.memory.get(this.bOffset); - // Result will be of the same type as 'a'. - const dest = a.build(a.equals(b) || a.lt(b) ? 1n : 0n); + const dest = TaggedMemory.buildFromTagOrDie(a.lt(b) || a.equals(b) ? 1n : 0n, this.inTag); context.machineState.memory.set(this.dstOffset, dest); context.machineState.incrementPc(); diff --git a/yarn-project/simulator/src/avm/opcodes/control_flow.test.ts b/yarn-project/simulator/src/avm/opcodes/control_flow.test.ts index fee81ec3402..660c561a3e5 100644 --- a/yarn-project/simulator/src/avm/opcodes/control_flow.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/control_flow.test.ts @@ -1,10 +1,8 @@ -import { Fr } from '@aztec/foundation/fields'; - import { AvmContext } from '../avm_context.js'; -import { Field, Uint16 } from '../avm_memory_types.js'; +import { Uint16 } from '../avm_memory_types.js'; import { InstructionExecutionError } from '../errors.js'; import { initContext } from '../fixtures/index.js'; -import { InternalCall, InternalReturn, Jump, JumpI, Return, Revert } from './control_flow.js'; +import { InternalCall, InternalReturn, Jump, JumpI } from './control_flow.js'; describe('Control Flow Opcodes', () => { let context: AvmContext; @@ -82,7 +80,7 @@ describe('Control Flow Opcodes', () => { }); }); - describe('INTERNALCALL and RETURN', () => { + describe('INTERNALCALL and INTERNALRETURN', () => { it('INTERNALCALL Should (de)serialize correctly', () => { const buf = Buffer.from([ InternalCall.opcode, // opcode @@ -151,68 +149,4 @@ describe('Control Flow Opcodes', () => { } }); }); - - describe('RETURN', () => { - it('Should (de)serialize correctly', () => { - const buf = Buffer.from([ - Return.opcode, // opcode - 0x01, // indirect - ...Buffer.from('12345678', 'hex'), // returnOffset - ...Buffer.from('a2345678', 'hex'), // copySize - ]); - const inst = new Return(/*indirect=*/ 0x01, /*returnOffset=*/ 0x12345678, /*copySize=*/ 0xa2345678); - - expect(Return.deserialize(buf)).toEqual(inst); - expect(inst.serialize()).toEqual(buf); - }); - - it('Should return data from the return opcode', async () => { - const returnData = [new Fr(1n), new Fr(2n), new Fr(3n)]; - - context.machineState.memory.set(0, new Field(1n)); - context.machineState.memory.set(1, new Field(2n)); - context.machineState.memory.set(2, new Field(3n)); - - const instruction = new Return(/*indirect=*/ 0, /*returnOffset=*/ 0, returnData.length); - await instruction.execute(context); - - expect(context.machineState.halted).toBe(true); - expect(context.machineState.getResults()).toEqual({ - reverted: false, - output: returnData, - }); - }); - }); - - describe('REVERT', () => { - it('Should (de)serialize correctly', () => { - const buf = Buffer.from([ - Revert.opcode, // opcode - 0x01, // indirect - ...Buffer.from('12345678', 'hex'), // returnOffset - ...Buffer.from('a2345678', 'hex'), // retSize - ]); - const inst = new Revert(/*indirect=*/ 0x01, /*returnOffset=*/ 0x12345678, /*retSize=*/ 0xa2345678); - - expect(Revert.deserialize(buf)).toEqual(inst); - expect(inst.serialize()).toEqual(buf); - }); - - it('Should return data and revert from the revert opcode', async () => { - const returnData = [new Fr(1n), new Fr(2n), new Fr(3n)]; - - context.machineState.memory.set(0, new Field(1n)); - context.machineState.memory.set(1, new Field(2n)); - context.machineState.memory.set(2, new Field(3n)); - - const instruction = new Revert(/*indirect=*/ 0, /*returnOffset=*/ 0, returnData.length); - await instruction.execute(context); - - expect(context.machineState.halted).toBe(true); - expect(context.machineState.getResults()).toEqual({ - reverted: true, - output: returnData, - }); - }); - }); }); diff --git a/yarn-project/simulator/src/avm/opcodes/control_flow.ts b/yarn-project/simulator/src/avm/opcodes/control_flow.ts index a3a8faaf619..0ebc96b6ccb 100644 --- a/yarn-project/simulator/src/avm/opcodes/control_flow.ts +++ b/yarn-project/simulator/src/avm/opcodes/control_flow.ts @@ -4,52 +4,6 @@ import { InstructionExecutionError } from '../errors.js'; import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; import { Instruction } from './instruction.js'; -export class Return extends Instruction { - static type: string = 'RETURN'; - static readonly opcode: Opcode = Opcode.RETURN; - // Informs (de)serialization. See Instruction.deserialize. - static readonly wireFormat: OperandType[] = [ - OperandType.UINT8, - OperandType.UINT8, - OperandType.UINT32, - OperandType.UINT32, - ]; - - constructor(private indirect: number, private returnOffset: number, private copySize: number) { - super(); - } - - async execute(context: AvmContext): Promise { - const output = context.machineState.memory.getSlice(this.returnOffset, this.copySize).map(word => word.toFr()); - - context.machineState.return(output); - } -} - -export class Revert extends Instruction { - static type: string = 'RETURN'; - static readonly opcode: Opcode = Opcode.REVERT; - // Informs (de)serialization. See Instruction.deserialize. - static readonly wireFormat: OperandType[] = [ - OperandType.UINT8, - OperandType.UINT8, - OperandType.UINT32, - OperandType.UINT32, - ]; - - constructor(private indirect: number, private returnOffset: number, private retSize: number) { - super(); - } - - async execute(context: AvmContext): Promise { - const output = context.machineState.memory - .getSlice(this.returnOffset, this.returnOffset + this.retSize) - .map(word => word.toFr()); - - context.machineState.revert(output); - } -} - export class Jump extends Instruction { static type: string = 'JUMP'; static readonly opcode: Opcode = Opcode.JUMP; @@ -122,7 +76,7 @@ export class InternalReturn extends Instruction { async execute(context: AvmContext): Promise { const jumpOffset = context.machineState.internalCallStack.pop(); if (jumpOffset === undefined) { - throw new InstructionExecutionError('Internal call empty!'); + throw new InstructionExecutionError('Internal call stack empty!'); } context.machineState.pc = jumpOffset; } 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 2f14a7a2398..6316a61155b 100644 --- a/yarn-project/simulator/src/avm/opcodes/external_calls.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/external_calls.test.ts @@ -5,13 +5,12 @@ import { mock } from 'jest-mock-extended'; import { CommitmentsDB, PublicContractsDB, PublicStateDB } from '../../index.js'; import { AvmContext } from '../avm_context.js'; -import { Field } from '../avm_memory_types.js'; +import { Field, Uint8 } from '../avm_memory_types.js'; import { 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'; -import { Return } from './control_flow.js'; -import { Call, StaticCall } from './external_calls.js'; +import { Call, Return, Revert, StaticCall } from './external_calls.js'; import { Instruction } from './instruction.js'; import { CalldataCopy } from './memory.js'; import { SStore } from './storage.js'; @@ -40,6 +39,7 @@ describe('External Calls', () => { ...Buffer.from('d2345678', 'hex'), // retOffset ...Buffer.from('e2345678', 'hex'), // retSize ...Buffer.from('f2345678', 'hex'), // successOffset + ...Buffer.from('f3345678', 'hex'), // temporaryFunctionSelectorOffset ]); const inst = new Call( /*indirect=*/ 0x01, @@ -50,6 +50,7 @@ describe('External Calls', () => { /*retOffset=*/ 0xd2345678, /*retSize=*/ 0xe2345678, /*successOffset=*/ 0xf2345678, + /*temporaryFunctionSelectorOffset=*/ 0xf3345678, ); expect(Call.deserialize(buf)).toEqual(inst); @@ -90,11 +91,12 @@ describe('External Calls', () => { retOffset, retSize, successOffset, + /*temporaryFunctionSelector=*/ 0, ); await instruction.execute(context); const successValue = context.machineState.memory.get(successOffset); - expect(successValue).toEqual(new Field(1n)); + expect(successValue).toEqual(new Uint8(1n)); const retValue = context.machineState.memory.getSlice(retOffset, retSize); expect(retValue).toEqual([new Field(1n), new Field(2n)]); @@ -124,6 +126,7 @@ describe('External Calls', () => { ...Buffer.from('d2345678', 'hex'), // retOffset ...Buffer.from('e2345678', 'hex'), // retSize ...Buffer.from('f2345678', 'hex'), // successOffset + ...Buffer.from('f3345678', 'hex'), // temporaryFunctionSelectorOffset ]); const inst = new StaticCall( /*indirect=*/ 0x01, @@ -134,6 +137,7 @@ describe('External Calls', () => { /*retOffset=*/ 0xd2345678, /*retSize=*/ 0xe2345678, /*successOffset=*/ 0xf2345678, + /*temporaryFunctionSelectorOffset=*/ 0xf3345678, ); expect(StaticCall.deserialize(buf)).toEqual(inst); @@ -177,12 +181,77 @@ describe('External Calls', () => { retOffset, retSize, successOffset, + /*temporaryFunctionSelector=*/ 0, ); await instruction.execute(context); // No revert has occurred, but the nested execution has failed const successValue = context.machineState.memory.get(successOffset); - expect(successValue).toEqual(new Field(0n)); + expect(successValue).toEqual(new Uint8(0n)); + }); + }); + + describe('RETURN', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + Return.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // returnOffset + ...Buffer.from('a2345678', 'hex'), // copySize + ]); + const inst = new Return(/*indirect=*/ 0x01, /*returnOffset=*/ 0x12345678, /*copySize=*/ 0xa2345678); + + expect(Return.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + + it('Should return data from the return opcode', async () => { + const returnData = [new Fr(1n), new Fr(2n), new Fr(3n)]; + + context.machineState.memory.set(0, new Field(1n)); + context.machineState.memory.set(1, new Field(2n)); + context.machineState.memory.set(2, new Field(3n)); + + const instruction = new Return(/*indirect=*/ 0, /*returnOffset=*/ 0, returnData.length); + await instruction.execute(context); + + expect(context.machineState.halted).toBe(true); + expect(context.machineState.getResults()).toEqual({ + reverted: false, + output: returnData, + }); + }); + }); + + describe('REVERT', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + Revert.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // returnOffset + ...Buffer.from('a2345678', 'hex'), // retSize + ]); + const inst = new Revert(/*indirect=*/ 0x01, /*returnOffset=*/ 0x12345678, /*retSize=*/ 0xa2345678); + + expect(Revert.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + + it('Should return data and revert from the revert opcode', async () => { + const returnData = [new Fr(1n), new Fr(2n), new Fr(3n)]; + + context.machineState.memory.set(0, new Field(1n)); + context.machineState.memory.set(1, new Field(2n)); + context.machineState.memory.set(2, new Field(3n)); + + const instruction = new Revert(/*indirect=*/ 0, /*returnOffset=*/ 0, returnData.length); + await instruction.execute(context); + + expect(context.machineState.halted).toBe(true); + expect(context.machineState.getResults()).toEqual({ + reverted: true, + output: returnData, + }); }); }); }); diff --git a/yarn-project/simulator/src/avm/opcodes/external_calls.ts b/yarn-project/simulator/src/avm/opcodes/external_calls.ts index e576af103be..0c1f5abdc0b 100644 --- a/yarn-project/simulator/src/avm/opcodes/external_calls.ts +++ b/yarn-project/simulator/src/avm/opcodes/external_calls.ts @@ -1,9 +1,10 @@ -import { Fr } from '@aztec/foundation/fields'; +import { FunctionSelector } from '@aztec/circuits.js'; import type { AvmContext } from '../avm_context.js'; -import { Field } from '../avm_memory_types.js'; +import { Field, Uint8 } from '../avm_memory_types.js'; import { AvmSimulator } from '../avm_simulator.js'; import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; +import { Addressing } from './addressing_mode.js'; import { Instruction } from './instruction.js'; export class Call extends Instruction { @@ -20,6 +21,8 @@ export class Call extends Instruction { OperandType.UINT32, OperandType.UINT32, OperandType.UINT32, + /* temporary function selector */ + OperandType.UINT32, ]; constructor( @@ -31,16 +34,30 @@ export class Call extends Instruction { private retOffset: number, private retSize: number, private successOffset: number, + // Function selector is temporary since eventually public contract bytecode will be one blob + // containing all functions, and function selector will become an application-level mechanism + // (e.g. first few bytes of calldata + compiler-generated jump table) + private temporaryFunctionSelectorOffset: number, ) { super(); } // TODO(https://github.com/AztecProtocol/aztec-packages/issues/3992): there is no concept of remaining / available gas at this moment async execute(context: AvmContext): Promise { - const callAddress = context.machineState.memory.getAs(this.addrOffset); - const calldata = context.machineState.memory.getSlice(this.argsOffset, this.argsSize).map(f => f.toFr()); + const [_gasOffset, addrOffset, argsOffset, retOffset, successOffset] = Addressing.fromWire(this.indirect).resolve( + [this._gasOffset, this.addrOffset, this.argsOffset, this.retOffset, this.successOffset], + context.machineState.memory, + ); + + const callAddress = context.machineState.memory.getAs(addrOffset); + const calldata = context.machineState.memory.getSlice(argsOffset, this.argsSize).map(f => f.toFr()); + const functionSelector = context.machineState.memory.getAs(this.temporaryFunctionSelectorOffset).toFr(); - const nestedContext = context.createNestedContractCallContext(callAddress.toFr(), calldata); + const nestedContext = context.createNestedContractCallContext( + callAddress.toFr(), + calldata, + FunctionSelector.fromField(functionSelector), + ); const nestedCallResults = await new AvmSimulator(nestedContext).execute(); const success = !nestedCallResults.reverted; @@ -50,8 +67,8 @@ export class Call extends Instruction { const convertedReturnData = returnData.map(f => new Field(f)); // Write our return data into memory - context.machineState.memory.set(this.successOffset, new Field(success ? 1 : 0)); - context.machineState.memory.setSlice(this.retOffset, convertedReturnData); + context.machineState.memory.set(successOffset, new Uint8(success ? 1 : 0)); + context.machineState.memory.setSlice(retOffset, convertedReturnData); if (success) { context.persistableState.acceptNestedCallState(nestedContext.persistableState); @@ -77,6 +94,8 @@ export class StaticCall extends Instruction { OperandType.UINT32, OperandType.UINT32, OperandType.UINT32, + /* temporary function selector */ + OperandType.UINT32, ]; constructor( @@ -88,17 +107,26 @@ export class StaticCall extends Instruction { private retOffset: number, private retSize: number, private successOffset: number, + private temporaryFunctionSelectorOffset: number, ) { super(); } async execute(context: AvmContext): Promise { - const callAddress = context.machineState.memory.get(this.addrOffset); - const calldata = context.machineState.memory - .getSlice(this.argsOffset, this.argsSize) - .map(f => new Fr(f.toBigInt())); + const [_gasOffset, addrOffset, argsOffset, retOffset, successOffset] = Addressing.fromWire(this.indirect).resolve( + [this._gasOffset, this.addrOffset, this.argsOffset, this.retOffset, this.successOffset], + context.machineState.memory, + ); - const nestedContext = context.createNestedContractStaticCallContext(callAddress.toFr(), calldata); + const callAddress = context.machineState.memory.get(addrOffset); + const calldata = context.machineState.memory.getSlice(argsOffset, this.argsSize).map(f => f.toFr()); + const functionSelector = context.machineState.memory.getAs(this.temporaryFunctionSelectorOffset).toFr(); + + const nestedContext = context.createNestedContractStaticCallContext( + callAddress.toFr(), + calldata, + FunctionSelector.fromField(functionSelector), + ); const nestedCallResults = await new AvmSimulator(nestedContext).execute(); const success = !nestedCallResults.reverted; @@ -108,8 +136,8 @@ export class StaticCall extends Instruction { const convertedReturnData = returnData.map(f => new Field(f)); // Write our return data into memory - context.machineState.memory.set(this.successOffset, new Field(success ? 1 : 0)); - context.machineState.memory.setSlice(this.retOffset, convertedReturnData); + context.machineState.memory.set(successOffset, new Uint8(success ? 1 : 0)); + context.machineState.memory.setSlice(retOffset, convertedReturnData); if (success) { context.persistableState.acceptNestedCallState(nestedContext.persistableState); @@ -120,3 +148,49 @@ export class StaticCall extends Instruction { context.machineState.incrementPc(); } } + +export class Return extends Instruction { + static type: string = 'RETURN'; + static readonly opcode: Opcode = Opcode.RETURN; + // Informs (de)serialization. See Instruction.deserialize. + static readonly wireFormat: OperandType[] = [ + OperandType.UINT8, + OperandType.UINT8, + OperandType.UINT32, + OperandType.UINT32, + ]; + + constructor(private indirect: number, private returnOffset: number, private copySize: number) { + super(); + } + + async execute(context: AvmContext): Promise { + const output = context.machineState.memory.getSlice(this.returnOffset, this.copySize).map(word => word.toFr()); + + context.machineState.return(output); + } +} + +export class Revert extends Instruction { + static type: string = 'RETURN'; + static readonly opcode: Opcode = Opcode.REVERT; + // Informs (de)serialization. See Instruction.deserialize. + static readonly wireFormat: OperandType[] = [ + OperandType.UINT8, + OperandType.UINT8, + OperandType.UINT32, + OperandType.UINT32, + ]; + + constructor(private indirect: number, private returnOffset: number, private retSize: number) { + super(); + } + + async execute(context: AvmContext): Promise { + const output = context.machineState.memory + .getSlice(this.returnOffset, this.returnOffset + this.retSize) + .map(word => word.toFr()); + + context.machineState.revert(output); + } +} diff --git a/yarn-project/simulator/src/avm/serialization/bytecode_serialization.test.ts b/yarn-project/simulator/src/avm/serialization/bytecode_serialization.test.ts index 26e75171b65..a9d8f08bec2 100644 --- a/yarn-project/simulator/src/avm/serialization/bytecode_serialization.test.ts +++ b/yarn-project/simulator/src/avm/serialization/bytecode_serialization.test.ts @@ -84,6 +84,7 @@ describe('Bytecode Serialization', () => { /*retOffset=*/ 0xd2345678, /*retSize=*/ 0xe2345678, /*successOffset=*/ 0xf2345678, + /*temporaryFunctionSelectorOffset=*/ 0xf3345678, ), new StaticCall( /*indirect=*/ 0x01, @@ -94,6 +95,7 @@ describe('Bytecode Serialization', () => { /*retOffset=*/ 0xd2345678, /*retSize=*/ 0xe2345678, /*successOffset=*/ 0xf2345678, + /*temporaryFunctionSelectorOffset=*/ 0xf3345678, ), ]; const bytecode = Buffer.concat(instructions.map(i => i.serialize())); @@ -117,6 +119,7 @@ describe('Bytecode Serialization', () => { /*retOffset=*/ 0xd2345678, /*retSize=*/ 0xe2345678, /*successOffset=*/ 0xf2345678, + /*temporaryFunctionSelectorOffset=*/ 0xf3345678, ), new StaticCall( /*indirect=*/ 0x01, @@ -127,6 +130,7 @@ describe('Bytecode Serialization', () => { /*retOffset=*/ 0xd2345678, /*retSize=*/ 0xe2345678, /*successOffset=*/ 0xf2345678, + /*temporaryFunctionSelectorOffset=*/ 0xf3345678, ), ]; diff --git a/yarn-project/simulator/src/avm/serialization/instruction_serialization.ts b/yarn-project/simulator/src/avm/serialization/instruction_serialization.ts index 963c457c299..684db9c6505 100644 --- a/yarn-project/simulator/src/avm/serialization/instruction_serialization.ts +++ b/yarn-project/simulator/src/avm/serialization/instruction_serialization.ts @@ -66,7 +66,6 @@ export enum Opcode { REVERT, KECCAK, POSEIDON, - // Add new opcodes before this SHA256, // temp - may be removed, but alot of contracts rely on it PEDERSEN, // temp - may be removed, but alot of contracts rely on it TOTAL_OPCODES_NUMBER,