diff --git a/avm-transpiler/src/transpile.rs b/avm-transpiler/src/transpile.rs index 0cfa1889a4a..078160d5693 100644 --- a/avm-transpiler/src/transpile.rs +++ b/avm-transpiler/src/transpile.rs @@ -395,9 +395,6 @@ fn handle_foreign_call( "avmOpcodeNullifierExists" => handle_nullifier_exists(avm_instrs, destinations, inputs), "avmOpcodeL1ToL2MsgExists" => handle_l1_to_l2_msg_exists(avm_instrs, destinations, inputs), "avmOpcodeSendL2ToL1Msg" => handle_send_l2_to_l1_msg(avm_instrs, destinations, inputs), - "avmOpcodeGetContractInstance" => { - handle_get_contract_instance(avm_instrs, destinations, inputs); - } "avmOpcodeCalldataCopy" => handle_calldata_copy(avm_instrs, destinations, inputs), "avmOpcodeReturn" => handle_return(avm_instrs, destinations, inputs), "avmOpcodeRevert" => handle_revert(avm_instrs, destinations, inputs), @@ -408,6 +405,10 @@ fn handle_foreign_call( _ if inputs.is_empty() && destinations.len() == 1 => { handle_getter_instruction(avm_instrs, function, destinations, inputs); } + // Get contract instance variations. + _ if function.starts_with("avmOpcodeGetContractInstance") => { + handle_get_contract_instance(avm_instrs, function, destinations, inputs); + } // Anything else. _ => panic!("Transpiler doesn't know how to process ForeignCall function {}", function), } @@ -1304,22 +1305,42 @@ fn handle_storage_write( /// Emit a GETCONTRACTINSTANCE opcode fn handle_get_contract_instance( avm_instrs: &mut Vec, + function: &str, destinations: &Vec, inputs: &Vec, ) { + enum ContractInstanceMember { + DEPLOYER, + CLASS_ID, + INIT_HASH, + } + assert!(inputs.len() == 1); - assert!(destinations.len() == 1); + assert!(destinations.len() == 2); + + let member_idx = match function { + "avmOpcodeGetContractInstanceDeployer" => ContractInstanceMember::DEPLOYER, + "avmOpcodeGetContractInstanceClassId" => ContractInstanceMember::CLASS_ID, + "avmOpcodeGetContractInstanceInitializationHash" => ContractInstanceMember::INIT_HASH, + _ => panic!("Transpiler doesn't know how to process function {:?}", function), + }; let address_offset_maybe = inputs[0]; let address_offset = match address_offset_maybe { - ValueOrArray::MemoryAddress(slot_offset) => slot_offset, + ValueOrArray::MemoryAddress(offset) => offset, _ => panic!("GETCONTRACTINSTANCE address should be a single value"), }; let dest_offset_maybe = destinations[0]; let dest_offset = match dest_offset_maybe { - ValueOrArray::HeapArray(HeapArray { pointer, .. }) => pointer, - _ => panic!("GETCONTRACTINSTANCE destination should be an array"), + ValueOrArray::MemoryAddress(offset) => offset, + _ => panic!("GETCONTRACTINSTANCE dst destination should be a single value"), + }; + + let exists_offset_maybe = destinations[1]; + let exists_offset = match exists_offset_maybe { + ValueOrArray::MemoryAddress(offset) => offset, + _ => panic!("GETCONTRACTINSTANCE exists destination should be a single value"), }; avm_instrs.push(AvmInstruction { @@ -1327,12 +1348,15 @@ fn handle_get_contract_instance( indirect: Some( AddressingModeBuilder::default() .direct_operand(&address_offset) - .indirect_operand(&dest_offset) + .direct_operand(&dest_offset) + .direct_operand(&exists_offset) .build(), ), operands: vec![ - AvmOperand::U32 { value: address_offset.to_usize() as u32 }, - AvmOperand::U32 { value: dest_offset.to_usize() as u32 }, + AvmOperand::U8 { value: member_idx as u8 }, + AvmOperand::U16 { value: address_offset.to_usize() as u16 }, + AvmOperand::U16 { value: dest_offset.to_usize() as u16 }, + AvmOperand::U16 { value: exists_offset.to_usize() as u16 }, ], ..Default::default() }); diff --git a/barretenberg/cpp/pil/avm/main.pil b/barretenberg/cpp/pil/avm/main.pil index 753cd19c4f5..b2bbf684c29 100644 --- a/barretenberg/cpp/pil/avm/main.pil +++ b/barretenberg/cpp/pil/avm/main.pil @@ -351,9 +351,8 @@ namespace main(256); // op_err * (sel_op_fdiv + sel_op_XXX + ... - 1) == 0 // Note that the above is even a stronger constraint, as it shows // that exactly one sel_op_XXX must be true. - // At this time, we have only division producing an error. #[SUBOP_ERROR_RELEVANT_OP] - op_err * ((sel_op_fdiv + sel_op_div) - 1) = 0; + op_err * ((sel_op_fdiv + sel_op_div + sel_op_get_contract_instance) - 1) = 0; // TODO: constraint that we stop execution at the first error (tag_err or op_err) // An error can only happen at the last sub-operation row. diff --git a/barretenberg/cpp/src/barretenberg/vm/avm/generated/relations/main.hpp b/barretenberg/cpp/src/barretenberg/vm/avm/generated/relations/main.hpp index 8869a0fb847..91cc7ded614 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm/generated/relations/main.hpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm/generated/relations/main.hpp @@ -567,7 +567,9 @@ template class mainImpl { } { using Accumulator = typename std::tuple_element_t<79, ContainerOverSubrelations>; - auto tmp = (new_term.main_op_err * ((new_term.main_sel_op_fdiv + new_term.main_sel_op_div) - FF(1))); + auto tmp = (new_term.main_op_err * (((new_term.main_sel_op_fdiv + new_term.main_sel_op_div) + + new_term.main_sel_op_get_contract_instance) - + FF(1))); tmp *= scaling_factor; std::get<79>(evals) += typename Accumulator::View(tmp); } diff --git a/barretenberg/cpp/src/barretenberg/vm/avm/tests/execution.test.cpp b/barretenberg/cpp/src/barretenberg/vm/avm/tests/execution.test.cpp index 7ca67efbdbd..250b2d5dd45 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm/tests/execution.test.cpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm/tests/execution.test.cpp @@ -1245,7 +1245,7 @@ TEST_F(AvmExecutionTests, msmOpCode) } // Positive test for Kernel Input opcodes -TEST_F(AvmExecutionTests, kernelInputOpcodes) +TEST_F(AvmExecutionTests, getEnvOpcode) { std::string bytecode_hex = to_hex(OpCode::GETENVVAR_16) + // opcode GETENVVAR_16 @@ -1497,6 +1497,32 @@ TEST_F(AvmExecutionTests, kernelInputOpcodes) validate_trace(std::move(trace), convert_public_inputs(public_inputs_vec), calldata, returndata); } +// TODO(9395): allow this intruction to raise error flag in main.pil +// TEST_F(AvmExecutionTests, getEnvOpcodeBadEnum) +//{ +// std::string bytecode_hex = +// to_hex(OpCode::GETENVVAR_16) + // opcode GETENVVAR_16 +// "00" // Indirect flag +// + to_hex(static_cast(EnvironmentVariable::MAX_ENV_VAR)) + // envvar ADDRESS +// "0001"; // dst_offset +// +// auto bytecode = hex_to_bytes(bytecode_hex); +// auto instructions = Deserialization::parse(bytecode); +// +// // Public inputs for the circuit +// std::vector calldata; +// std::vector returndata; +// ExecutionHints execution_hints; +// auto trace = gen_trace(bytecode, calldata, public_inputs_vec, returndata, execution_hints); +// +// // Bad enum should raise error flag +// auto address_row = +// std::ranges::find_if(trace.begin(), trace.end(), [](Row r) { return r.main_sel_op_address == 1; }); +// EXPECT_EQ(address_row->main_op_err, FF(1)); +// +// validate_trace(std::move(trace), convert_public_inputs(public_inputs_vec), calldata, returndata); +//} + // Positive test for L2GASLEFT opcode TEST_F(AvmExecutionTests, l2GasLeft) { @@ -2110,43 +2136,10 @@ TEST_F(AvmExecutionTests, opCallOpcodes) validate_trace(std::move(trace), public_inputs, calldata, returndata); } -TEST_F(AvmExecutionTests, opGetContractInstanceOpcodes) +TEST_F(AvmExecutionTests, opGetContractInstanceOpcode) { - std::string bytecode_hex = to_hex(OpCode::SET_8) + // opcode SET - "00" // Indirect flag - + to_hex(AvmMemoryTag::U32) + - "00" // val - "00" // dst_offset - + to_hex(OpCode::SET_8) + // opcode SET - "00" // Indirect flag - + to_hex(AvmMemoryTag::U32) + - "01" // val - "01" + - to_hex(OpCode::CALLDATACOPY) + // opcode CALLDATACOPY for addr - "00" // Indirect flag - "0000" // cd_offset - "0001" // copy_size - "0001" // dst_offset, (i.e. where we store the addr) - + to_hex(OpCode::SET_8) + // opcode SET for the indirect dst offset - "00" // Indirect flag - + to_hex(AvmMemoryTag::U32) + - "03" // val i - "02" + // dst_offset 2 - to_hex(OpCode::GETCONTRACTINSTANCE) + // opcode CALL - "02" // Indirect flag - "00000001" // address offset - "00000002" // dst offset - + to_hex(OpCode::RETURN) + // opcode RETURN - "00" // Indirect flag - "0003" // ret offset 3 - "0006"; // ret size 6 - - auto bytecode = hex_to_bytes(bytecode_hex); - auto instructions = Deserialization::parse(bytecode); - - FF address = 10; - std::vector calldata = { address }; - std::vector returndata = {}; + const uint8_t address_byte = 0x42; + const FF address(address_byte); // Generate Hint for call operation // Note: opcode does not write 'address' into memory @@ -2158,14 +2151,96 @@ TEST_F(AvmExecutionTests, opGetContractInstanceOpcodes) grumpkin::g1::affine_element::random_element(), grumpkin::g1::affine_element::random_element(), }; - auto execution_hints = - ExecutionHints().with_contract_instance_hints({ { address, { address, 1, 2, 3, 4, 5, public_keys_hints } } }); + const ContractInstanceHint instance = ContractInstanceHint{ + .address = address, + .exists = true, + .salt = 2, + .deployer_addr = 42, + .contract_class_id = 66, + .initialisation_hash = 99, + .public_keys = public_keys_hints, + }; + auto execution_hints = ExecutionHints().with_contract_instance_hints({ { address, instance } }); + + std::string bytecode_hex = to_hex(OpCode::SET_8) + // opcode SET + "00" // Indirect flag + + to_hex(AvmMemoryTag::U8) + to_hex(address_byte) + // val + "01" // dst_offset 0 + + to_hex(OpCode::GETCONTRACTINSTANCE) + // opcode GETCONTRACTINSTANCE + "00" // Indirect flag + + to_hex(static_cast(ContractInstanceMember::DEPLOYER)) + // member enum + "0001" // address offset + "0010" // dst offset + "0011" // exists offset + + to_hex(OpCode::GETCONTRACTINSTANCE) + // opcode GETCONTRACTINSTANCE + "00" // Indirect flag + + to_hex(static_cast(ContractInstanceMember::CLASS_ID)) + // member enum + "0001" // address offset + "0012" // dst offset + "0013" // exists offset + + to_hex(OpCode::GETCONTRACTINSTANCE) + // opcode GETCONTRACTINSTANCE + "00" // Indirect flag + + to_hex(static_cast(ContractInstanceMember::INIT_HASH)) + // member enum + "0001" // address offset + "0014" // dst offset + "0015" // exists offset + + to_hex(OpCode::RETURN) + // opcode RETURN + "00" // Indirect flag + "0010" // ret offset 1 + "0006"; // ret size 6 (dst & exists for all 3) + + auto bytecode = hex_to_bytes(bytecode_hex); + auto instructions = Deserialization::parse(bytecode); + + ASSERT_THAT(instructions, SizeIs(5)); + + std::vector const calldata{}; + // alternating member value, exists bool + std::vector const expected_returndata = { + instance.deployer_addr, 1, instance.contract_class_id, 1, instance.initialisation_hash, 1, + }; + std::vector returndata{}; auto trace = gen_trace(bytecode, calldata, public_inputs_vec, returndata, execution_hints); - EXPECT_EQ(returndata, std::vector({ 1, 2, 3, 4, 5, returned_point.x })); // The first one represents true validate_trace(std::move(trace), public_inputs, calldata, returndata); + + // Validate returndata + EXPECT_EQ(returndata, expected_returndata); } + +TEST_F(AvmExecutionTests, opGetContractInstanceOpcodeBadEnum) +{ + const uint8_t address_byte = 0x42; + const FF address(address_byte); + + std::string bytecode_hex = to_hex(OpCode::SET_8) + // opcode SET + "00" // Indirect flag + + to_hex(AvmMemoryTag::U8) + to_hex(address_byte) + // val + "01" // dst_offset 0 + + to_hex(OpCode::GETCONTRACTINSTANCE) + // opcode GETCONTRACTINSTANCE + "00" // Indirect flag + + to_hex(static_cast(ContractInstanceMember::MAX_MEMBER)) + // member enum + "0001" // address offset + "0010" // dst offset + "0011"; // exists offset + + auto bytecode = hex_to_bytes(bytecode_hex); + auto instructions = Deserialization::parse(bytecode); + + std::vector calldata; + std::vector returndata; + ExecutionHints execution_hints; + auto trace = gen_trace(bytecode, calldata, public_inputs_vec, returndata, execution_hints); + + // Bad enum should raise error flag + auto address_row = std::ranges::find_if( + trace.begin(), trace.end(), [](Row r) { return r.main_sel_op_get_contract_instance == 1; }); + EXPECT_EQ(address_row->main_op_err, FF(1)); + + validate_trace(std::move(trace), public_inputs, calldata, returndata); +} + // Negative test detecting an invalid opcode byte. TEST_F(AvmExecutionTests, invalidOpcode) { diff --git a/barretenberg/cpp/src/barretenberg/vm/avm/trace/deserialization.cpp b/barretenberg/cpp/src/barretenberg/vm/avm/trace/deserialization.cpp index 88be6bca8df..779eb0c21e8 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm/trace/deserialization.cpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm/trace/deserialization.cpp @@ -136,7 +136,8 @@ const std::unordered_map> OPCODE_WIRE_FORMAT = OperandType::UINT16, /*TODO: leafIndexOffset is not constrained*/ OperandType::UINT16, OperandType::UINT16 } }, - { OpCode::GETCONTRACTINSTANCE, { OperandType::INDIRECT8, OperandType::UINT32, OperandType::UINT32 } }, + { OpCode::GETCONTRACTINSTANCE, + { OperandType::INDIRECT8, OperandType::UINT8, OperandType::UINT16, OperandType::UINT16, OperandType::UINT16 } }, { OpCode::EMITUNENCRYPTEDLOG, { OperandType::INDIRECT8, diff --git a/barretenberg/cpp/src/barretenberg/vm/avm/trace/execution.cpp b/barretenberg/cpp/src/barretenberg/vm/avm/trace/execution.cpp index 1a2a5357e8b..6d1978806a4 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm/trace/execution.cpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm/trace/execution.cpp @@ -604,8 +604,10 @@ std::vector Execution::gen_trace(std::vector const& calldata, break; case OpCode::GETCONTRACTINSTANCE: trace_builder.op_get_contract_instance(std::get(inst.operands.at(0)), - std::get(inst.operands.at(1)), - std::get(inst.operands.at(2))); + std::get(inst.operands.at(1)), + std::get(inst.operands.at(2)), + std::get(inst.operands.at(3)), + std::get(inst.operands.at(4))); break; // Accrued Substate diff --git a/barretenberg/cpp/src/barretenberg/vm/avm/trace/opcode.hpp b/barretenberg/cpp/src/barretenberg/vm/avm/trace/opcode.hpp index 4aef84ee5bf..b0c5efd9a84 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm/trace/opcode.hpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm/trace/opcode.hpp @@ -128,6 +128,14 @@ enum class EnvironmentVariable { MAX_ENV_VAR }; +enum class ContractInstanceMember { + DEPLOYER, + CLASS_ID, + INIT_HASH, + // sentinel + MAX_MEMBER, +}; + class Bytecode { public: static bool is_valid(uint8_t byte); diff --git a/barretenberg/cpp/src/barretenberg/vm/avm/trace/trace.cpp b/barretenberg/cpp/src/barretenberg/vm/avm/trace/trace.cpp index a4d17e13f75..d5400f5c8f5 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm/trace/trace.cpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm/trace/trace.cpp @@ -1261,51 +1261,70 @@ Row AvmTraceBuilder::create_kernel_lookup_opcode(uint8_t indirect, uint32_t dst_ void AvmTraceBuilder::op_get_env_var(uint8_t indirect, uint8_t env_var, uint32_t dst_offset) { - ASSERT(env_var < static_cast(EnvironmentVariable::MAX_ENV_VAR)); - EnvironmentVariable var = static_cast(env_var); - - switch (var) { - case EnvironmentVariable::ADDRESS: - op_address(indirect, dst_offset); - break; - case EnvironmentVariable::SENDER: - op_sender(indirect, dst_offset); - break; - case EnvironmentVariable::FUNCTIONSELECTOR: - op_function_selector(indirect, dst_offset); - break; - case EnvironmentVariable::TRANSACTIONFEE: - op_transaction_fee(indirect, dst_offset); - break; - case EnvironmentVariable::CHAINID: - op_chain_id(indirect, dst_offset); - break; - case EnvironmentVariable::VERSION: - op_version(indirect, dst_offset); - break; - case EnvironmentVariable::BLOCKNUMBER: - op_block_number(indirect, dst_offset); - break; - case EnvironmentVariable::TIMESTAMP: - op_timestamp(indirect, dst_offset); - break; - case EnvironmentVariable::FEEPERL2GAS: - op_fee_per_l2_gas(indirect, dst_offset); - break; - case EnvironmentVariable::FEEPERDAGAS: - op_fee_per_da_gas(indirect, dst_offset); - break; - case EnvironmentVariable::ISSTATICCALL: - op_is_static_call(indirect, dst_offset); - break; - case EnvironmentVariable::L2GASLEFT: - op_l2gasleft(indirect, dst_offset); - break; - case EnvironmentVariable::DAGASLEFT: - op_dagasleft(indirect, dst_offset); - break; - default: - throw std::runtime_error("Invalid environment variable"); + if (env_var >= static_cast(EnvironmentVariable::MAX_ENV_VAR)) { + // Error, bad enum operand + // TODO(9395): constrain this via range check + auto const clk = static_cast(main_trace.size()) + 1; + const auto row = Row{ + .main_clk = clk, + .main_call_ptr = call_ptr, + .main_internal_return_ptr = internal_return_ptr, + .main_op_err = FF(1), + .main_pc = pc++, + .main_sel_op_address = FF(1), // TODO(9407): what selector should this be? + }; + + // Constrain gas cost + gas_trace_builder.constrain_gas(static_cast(row.main_clk), OpCode::GETENVVAR_16); + + main_trace.push_back(row); + } else { + EnvironmentVariable var = static_cast(env_var); + + switch (var) { + case EnvironmentVariable::ADDRESS: + op_address(indirect, dst_offset); + break; + case EnvironmentVariable::SENDER: + op_sender(indirect, dst_offset); + break; + case EnvironmentVariable::FUNCTIONSELECTOR: + op_function_selector(indirect, dst_offset); + break; + case EnvironmentVariable::TRANSACTIONFEE: + op_transaction_fee(indirect, dst_offset); + break; + case EnvironmentVariable::CHAINID: + op_chain_id(indirect, dst_offset); + break; + case EnvironmentVariable::VERSION: + op_version(indirect, dst_offset); + break; + case EnvironmentVariable::BLOCKNUMBER: + op_block_number(indirect, dst_offset); + break; + case EnvironmentVariable::TIMESTAMP: + op_timestamp(indirect, dst_offset); + break; + case EnvironmentVariable::FEEPERL2GAS: + op_fee_per_l2_gas(indirect, dst_offset); + break; + case EnvironmentVariable::FEEPERDAGAS: + op_fee_per_da_gas(indirect, dst_offset); + break; + case EnvironmentVariable::ISSTATICCALL: + op_is_static_call(indirect, dst_offset); + break; + case EnvironmentVariable::L2GASLEFT: + op_l2gasleft(indirect, dst_offset); + break; + case EnvironmentVariable::DAGASLEFT: + op_dagasleft(indirect, dst_offset); + break; + default: + throw std::runtime_error("Invalid environment variable"); + break; + } } } @@ -2359,48 +2378,100 @@ void AvmTraceBuilder::op_l1_to_l2_msg_exists(uint8_t indirect, debug("l1_to_l2_msg_exists side-effect cnt: ", side_effect_counter); } -void AvmTraceBuilder::op_get_contract_instance(uint8_t indirect, uint32_t address_offset, uint32_t dst_offset) +void AvmTraceBuilder::op_get_contract_instance( + uint8_t indirect, uint8_t member_enum, uint16_t address_offset, uint16_t dst_offset, uint16_t exists_offset) { auto clk = static_cast(main_trace.size()) + 1; + // Constrain gas cost + gas_trace_builder.constrain_gas(clk, OpCode::GETCONTRACTINSTANCE); - auto [resolved_address_offset, resolved_dst_offset] = - Addressing<2>::fromWire(indirect, call_ptr).resolve({ address_offset, dst_offset }, mem_trace_builder); + if (member_enum >= static_cast(ContractInstanceMember::MAX_MEMBER)) { + // Error, bad enum operand + // TODO(9393): constrain this via range check + const auto row = Row{ + .main_clk = clk, + .main_call_ptr = call_ptr, + .main_internal_return_ptr = internal_return_ptr, + .main_op_err = FF(1), + .main_pc = pc++, + .main_sel_op_get_contract_instance = FF(1), + }; + main_trace.push_back(row); - auto read_address = constrained_read_from_memory( - call_ptr, clk, resolved_address_offset, AvmMemoryTag::FF, AvmMemoryTag::FF, IntermRegister::IA); - bool tag_match = read_address.tag_match; + } else { - // Constrain gas cost - gas_trace_builder.constrain_gas(clk, OpCode::GETCONTRACTINSTANCE); + ContractInstanceMember chosen_member = static_cast(member_enum); + + auto [resolved_address_offset, resolved_dst_offset, resolved_exists_offset] = + Addressing<3>::fromWire(indirect, call_ptr) + .resolve({ address_offset, dst_offset, exists_offset }, mem_trace_builder); + + auto read_address = constrained_read_from_memory( + call_ptr, clk, resolved_address_offset, AvmMemoryTag::FF, AvmMemoryTag::FF, IntermRegister::IA); + bool tag_match = read_address.tag_match; + + // Read the contract instance + ContractInstanceHint instance = execution_hints.contract_instance_hints.at(read_address.val); + + FF member_value; + switch (chosen_member) { + case ContractInstanceMember::DEPLOYER: + member_value = instance.deployer_addr; + break; + case ContractInstanceMember::CLASS_ID: + member_value = instance.contract_class_id; + break; + case ContractInstanceMember::INIT_HASH: + member_value = instance.initialisation_hash; + break; + default: + member_value = 0; + break; + } - main_trace.push_back(Row{ - .main_clk = clk, - .main_ia = read_address.val, - .main_ind_addr_a = FF(read_address.indirect_address), - .main_internal_return_ptr = FF(internal_return_ptr), - .main_mem_addr_a = FF(read_address.direct_address), - .main_pc = FF(pc++), - .main_r_in_tag = FF(static_cast(AvmMemoryTag::FF)), - .main_sel_mem_op_a = FF(1), - .main_sel_op_get_contract_instance = FF(1), - .main_sel_resolve_ind_addr_a = FF(static_cast(read_address.is_indirect)), - .main_tag_err = FF(static_cast(!tag_match)), - }); + // TODO:(8603): once instructions can have multiple different tags for writes, write dst as FF and exists as U1 + // auto write_dst = constrained_write_to_memory(call_ptr, clk, resolved_dst_offset, member_value, + // AvmMemoryTag::FF, AvmMemoryTag::FF, IntermRegister::IC); auto write_exists = + // constrained_write_to_memory(call_ptr, clk, resolved_exists_offset, instance.instance_found_in_address, + // AvmMemoryTag::FF, AvmMemoryTag::FF, IntermRegister::ID); - // Read the contract instance - ContractInstanceHint contract_instance = execution_hints.contract_instance_hints.at(read_address.val); - std::vector public_key_fields = contract_instance.public_keys.to_fields(); - // NOTE: we don't write the first entry (the contract instance's address/key) to memory - std::vector contract_instance_vec = { contract_instance.exists ? FF::one() : FF::zero(), - contract_instance.salt, - contract_instance.deployer_addr, - contract_instance.contract_class_id, - contract_instance.initialisation_hash }; - contract_instance_vec.insert(contract_instance_vec.end(), public_key_fields.begin(), public_key_fields.end()); - write_slice_to_memory(resolved_dst_offset, AvmMemoryTag::FF, contract_instance_vec); - - debug("contract_instance cnt: ", side_effect_counter); - side_effect_counter++; + main_trace.push_back(Row{ + .main_clk = clk, + .main_call_ptr = call_ptr, + .main_ia = read_address.val, + // TODO:(8603): uncomment this and below blocks once instructions can have multiple different tags for + // writes + //.main_ic = write_dst.val, + //.main_id = write_exists.val, + .main_ind_addr_a = FF(read_address.indirect_address), + //.main_ind_addr_c = FF(write_dst.indirect_address), + //.main_ind_addr_d = FF(write_exists.indirect_address), + .main_internal_return_ptr = FF(internal_return_ptr), + .main_mem_addr_a = FF(read_address.direct_address), + //.main_mem_addr_c = FF(write_dst.direct_address), + //.main_mem_addr_d = FF(write_exists.direct_address), + .main_pc = FF(pc++), + .main_r_in_tag = FF(static_cast(AvmMemoryTag::FF)), + .main_sel_mem_op_a = FF(1), + //.main_sel_mem_op_c = FF(1), + //.main_sel_mem_op_d = FF(1), + .main_sel_op_get_contract_instance = FF(1), + .main_sel_resolve_ind_addr_a = FF(static_cast(read_address.is_indirect)), + //.main_sel_resolve_ind_addr_c = FF(static_cast(write_dst.is_indirect)), + //.main_sel_resolve_ind_addr_d = FF(static_cast(write_exists.is_indirect)), + .main_tag_err = FF(static_cast(!tag_match)), + }); + + // TODO:(8603): once instructions can have multiple different tags for writes, remove this and do a constrained + // writes + write_to_memory(resolved_dst_offset, member_value, AvmMemoryTag::FF); + write_to_memory(resolved_exists_offset, FF(static_cast(instance.exists)), AvmMemoryTag::U1); + + // TODO(dbanks12): compute contract address nullifier from instance preimage and perform membership check + + debug("contract_instance cnt: ", side_effect_counter); + side_effect_counter++; + } } /************************************************************************************************** diff --git a/barretenberg/cpp/src/barretenberg/vm/avm/trace/trace.hpp b/barretenberg/cpp/src/barretenberg/vm/avm/trace/trace.hpp index 59c4fb2e2c0..c1095864326 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm/trace/trace.hpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm/trace/trace.hpp @@ -113,7 +113,8 @@ class AvmTraceBuilder { uint32_t log_offset, uint32_t leaf_index_offset, uint32_t dest_offset); - void op_get_contract_instance(uint8_t indirect, uint32_t address_offset, uint32_t dst_offset); + void op_get_contract_instance( + uint8_t indirect, uint8_t member_enum, uint16_t address_offset, uint16_t dst_offset, uint16_t exists_offset); // Accrued Substate void op_emit_unencrypted_log(uint8_t indirect, uint32_t log_offset, uint32_t log_size_offset); diff --git a/noir-projects/aztec-nr/aztec/src/context/public_context.nr b/noir-projects/aztec-nr/aztec/src/context/public_context.nr index 9456483faf4..bd1bcb4d8dc 100644 --- a/noir-projects/aztec-nr/aztec/src/context/public_context.nr +++ b/noir-projects/aztec-nr/aztec/src/context/public_context.nr @@ -210,13 +210,14 @@ unconstrained fn address() -> AztecAddress { unconstrained fn sender() -> AztecAddress { sender_opcode() } +// TODO(9396): Remove. unconstrained fn portal() -> EthAddress { portal_opcode() } -// UNUSED: Remove. -// unconstrained fn function_selector() -> u32 { -// function_selector_opcode() -// } +// TODO(9396): Remove. +//unconstrained fn function_selector() -> u32 { +// function_selector_opcode() +//} unconstrained fn transaction_fee() -> Field { transaction_fee_opcode() } @@ -325,9 +326,9 @@ unconstrained fn sender_opcode() -> AztecAddress {} #[oracle(avmOpcodePortal)] unconstrained fn portal_opcode() -> EthAddress {} -// UNUSED: Remove. -// #[oracle(avmOpcodeFunctionSelector)] -// unconstrained fn function_selector_opcode() -> u32 {} +// TODO(9396): Remove. +//#[oracle(avmOpcodeFunctionSelector)] +//unconstrained fn function_selector_opcode() -> u32 {} #[oracle(avmOpcodeTransactionFee)] unconstrained fn transaction_fee_opcode() -> Field {} diff --git a/noir-projects/aztec-nr/aztec/src/initializer.nr b/noir-projects/aztec-nr/aztec/src/initializer.nr index 71509340e71..36dc2b41893 100644 --- a/noir-projects/aztec-nr/aztec/src/initializer.nr +++ b/noir-projects/aztec-nr/aztec/src/initializer.nr @@ -5,7 +5,10 @@ use dep::protocol_types::{ use crate::{ context::{PrivateContext, PublicContext}, - oracle::get_contract_instance::{get_contract_instance, get_contract_instance_avm}, + oracle::get_contract_instance::{ + get_contract_instance, get_contract_instance_deployer_avm, + get_contract_instance_initialization_hash_avm, + }, }; pub fn mark_as_initialized_public(context: &mut PublicContext) { @@ -36,11 +39,12 @@ fn compute_unsiloed_contract_initialization_nullifier(address: AztecAddress) -> pub fn assert_initialization_matches_address_preimage_public(context: PublicContext) { let address = context.this_address(); - let instance = get_contract_instance_avm(address).unwrap(); + let deployer = get_contract_instance_deployer_avm(address).unwrap(); + let initialization_hash = get_contract_instance_initialization_hash_avm(address).unwrap(); let expected_init = compute_initialization_hash(context.selector(), context.get_args_hash()); - assert(instance.initialization_hash == expected_init, "Initialization hash does not match"); + assert(initialization_hash == expected_init, "Initialization hash does not match"); assert( - (instance.deployer.is_zero()) | (instance.deployer == context.msg_sender()), + (deployer.is_zero()) | (deployer == context.msg_sender()), "Initializer address is not the contract deployer", ); } diff --git a/noir-projects/aztec-nr/aztec/src/oracle/get_contract_instance.nr b/noir-projects/aztec-nr/aztec/src/oracle/get_contract_instance.nr index 7958bfd819c..0b427393d5e 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/get_contract_instance.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/get_contract_instance.nr @@ -1,31 +1,22 @@ use dep::protocol_types::{ - address::AztecAddress, constants::CONTRACT_INSTANCE_LENGTH, contract_instance::ContractInstance, - utils::reader::Reader, + address::AztecAddress, constants::CONTRACT_INSTANCE_LENGTH, contract_class_id::ContractClassId, + contract_instance::ContractInstance, }; +// NOTE: this is for use in private only #[oracle(getContractInstance)] unconstrained fn get_contract_instance_oracle( _address: AztecAddress, ) -> [Field; CONTRACT_INSTANCE_LENGTH] {} -// Returns a ContractInstance plus a boolean indicating whether the instance was found. -#[oracle(avmOpcodeGetContractInstance)] -unconstrained fn get_contract_instance_oracle_avm( - _address: AztecAddress, -) -> [Field; CONTRACT_INSTANCE_LENGTH + 1] {} - +// NOTE: this is for use in private only unconstrained fn get_contract_instance_internal( address: AztecAddress, ) -> [Field; CONTRACT_INSTANCE_LENGTH] { get_contract_instance_oracle(address) } -pub unconstrained fn get_contract_instance_internal_avm( - address: AztecAddress, -) -> [Field; CONTRACT_INSTANCE_LENGTH + 1] { - get_contract_instance_oracle_avm(address) -} - +// NOTE: this is for use in private only pub fn get_contract_instance(address: AztecAddress) -> ContractInstance { let instance = unsafe { ContractInstance::deserialize(get_contract_instance_internal(address)) }; @@ -36,12 +27,58 @@ pub fn get_contract_instance(address: AztecAddress) -> ContractInstance { instance } -pub fn get_contract_instance_avm(address: AztecAddress) -> Option { - let mut reader = Reader::new(get_contract_instance_internal_avm(address)); - let found = reader.read(); - if found == 0 { +// These oracles each return a ContractInstance member +// plus a boolean indicating whether the instance was found. +#[oracle(avmOpcodeGetContractInstanceDeployer)] +unconstrained fn get_contract_instance_deployer_oracle_avm( + _address: AztecAddress, +) -> (Field, bool) {} +#[oracle(avmOpcodeGetContractInstanceClassId)] +unconstrained fn get_contract_instance_class_id_oracle_avm( + _address: AztecAddress, +) -> (Field, bool) {} +#[oracle(avmOpcodeGetContractInstanceInitializationHash)] +unconstrained fn get_contract_instance_initialization_hash_oracle_avm( + _address: AztecAddress, +) -> (Field, bool) {} + +pub unconstrained fn get_contract_instance_deployer_internal_avm( + address: AztecAddress, +) -> (Field, bool) { + get_contract_instance_deployer_oracle_avm(address) +} +pub unconstrained fn get_contract_instance_class_id_internal_avm( + address: AztecAddress, +) -> (Field, bool) { + get_contract_instance_class_id_oracle_avm(address) +} +pub unconstrained fn get_contract_instance_initialization_hash_internal_avm( + address: AztecAddress, +) -> (Field, bool) { + get_contract_instance_initialization_hash_oracle_avm(address) +} + +pub fn get_contract_instance_deployer_avm(address: AztecAddress) -> Option { + let (member, exists) = get_contract_instance_deployer_internal_avm(address); + if exists { + Option::some(AztecAddress::from_field(member)) + } else { + Option::none() + } +} +pub fn get_contract_instance_class_id_avm(address: AztecAddress) -> Option { + let (member, exists) = get_contract_instance_class_id_internal_avm(address); + if exists { + Option::some(ContractClassId::from_field(member)) + } else { Option::none() + } +} +pub fn get_contract_instance_initialization_hash_avm(address: AztecAddress) -> Option { + let (member, exists) = get_contract_instance_initialization_hash_internal_avm(address); + if exists { + Option::some(member) } else { - Option::some(reader.read_struct(ContractInstance::deserialize)) + Option::none() } } 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 e0e1a2833ee..4f7402b954c 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 @@ -32,11 +32,13 @@ contract AvmTest { use dep::aztec::context::gas::GasOpts; use dep::aztec::macros::{functions::{private, public}, storage::storage}; use dep::aztec::oracle::get_contract_instance::{ - get_contract_instance_avm, get_contract_instance_internal_avm, + get_contract_instance_class_id_avm, get_contract_instance_deployer_avm, + get_contract_instance_initialization_hash_avm, }; use dep::aztec::prelude::Map; use dep::aztec::protocol_types::{ - abis::function_selector::FunctionSelector, storage::map::derive_storage_slot_in_map, + abis::function_selector::FunctionSelector, contract_class_id::ContractClassId, + storage::map::derive_storage_slot_in_map, }; use dep::aztec::protocol_types::{ address::{AztecAddress, EthAddress}, @@ -297,33 +299,46 @@ contract AvmTest { * Contract instance ************************************************************************/ #[public] - fn test_get_contract_instance_raw() { - let fields = get_contract_instance_internal_avm(context.this_address()); - // The values here should match those in `avm_simulator.test.ts>Contract>GETCONTRACTINSTANCE deserializes correctly` - assert(fields.len() == CONTRACT_INSTANCE_LENGTH + 1); - assert(fields[0] == 0x1); - assert(fields[1] == 0x123); - assert(fields[2] == 0x456); - assert(fields[3] == 0x789); - assert(fields[4] == 0x101112); - assert(fields[5] == 0x131415); - assert(fields[6] == 0x161718); - assert(fields[7] == 0x00); - assert(fields[8] == 0x192021); - assert(fields[9] == 0x222324); - assert(fields[10] == 0x00); - assert(fields[11] == 0x252627); - assert(fields[12] == 0x282930); - assert(fields[13] == 0x00); - assert(fields[14] == 0x313233); - assert(fields[15] == 0x343536); - assert(fields[16] == 0x00); - } - - #[public] - fn test_get_contract_instance() { - let ci = get_contract_instance_avm(context.this_address()); - assert(ci.is_some(), "Contract instance not found!"); + fn test_get_contract_instance(address: AztecAddress) { + let deployer = get_contract_instance_deployer_avm(address); + let class_id = get_contract_instance_class_id_avm(address); + let initialization_hash = get_contract_instance_initialization_hash_avm(address); + + assert(deployer.is_some(), "Contract instance not found when getting DEPLOYER!"); + assert(class_id.is_some(), "Contract instance not found when getting CLASS_ID!"); + assert( + initialization_hash.is_some(), + "Contract instance not found when getting INIT_HASH!", + ); + + // The values here should match those in `avm_simulator.test.ts` + assert(deployer.unwrap().eq(AztecAddress::from_field(0x456))); + assert(class_id.unwrap().eq(ContractClassId::from_field(0x789))); + assert(initialization_hash.unwrap() == 0x101112); + } + + #[public] + fn test_get_contract_instance_matches( + address: AztecAddress, + expected_deployer: AztecAddress, + expected_class_id: ContractClassId, + expected_initialization_hash: Field, + ) { + let deployer = get_contract_instance_deployer_avm(address); + let class_id = get_contract_instance_class_id_avm(address); + let initialization_hash = get_contract_instance_initialization_hash_avm(address); + + assert(deployer.is_some(), "Contract instance not found when getting DEPLOYER!"); + assert(class_id.is_some(), "Contract instance not found when getting CLASS_ID!"); + assert( + initialization_hash.is_some(), + "Contract instance not found when getting INIT_HASH!", + ); + + // The values here should match those in `avm_simulator.test.ts` + assert(deployer.unwrap().eq(expected_deployer)); + assert(class_id.unwrap().eq(expected_class_id)); + assert(initialization_hash.unwrap().eq(expected_initialization_hash)); } /************************************************************************ @@ -563,7 +578,7 @@ contract AvmTest { dep::aztec::oracle::debug_log::debug_log("pedersen_hash_with_index"); let _ = pedersen_hash_with_index(args_field); dep::aztec::oracle::debug_log::debug_log("test_get_contract_instance"); - test_get_contract_instance(); + test_get_contract_instance(context.this_address()); dep::aztec::oracle::debug_log::debug_log("get_address"); let _ = get_address(); dep::aztec::oracle::debug_log::debug_log("get_sender"); diff --git a/yarn-project/bb-prover/src/avm_proving.test.ts b/yarn-project/bb-prover/src/avm_proving.test.ts index 72b5f3ce510..935a6f448fc 100644 --- a/yarn-project/bb-prover/src/avm_proving.test.ts +++ b/yarn-project/bb-prover/src/avm_proving.test.ts @@ -66,6 +66,7 @@ const proveAndVerifyAvmTestContract = async ( globals.timestamp = TIMESTAMP; const worldStateDB = mock(); + // // Top level contract call const bytecode = getAvmTestContractBytecode('public_dispatch'); const fnSelector = getAvmTestContractFunctionSelector('public_dispatch'); @@ -73,6 +74,7 @@ const proveAndVerifyAvmTestContract = async ( const contractClass = makeContractClassPublic(0, publicFn); const contractInstance = makeContractInstanceFromClassId(contractClass.id); + // The values here should match those in `avm_simulator.test.ts` const instanceGet = new SerializableContractInstance({ version: 1, salt: new Fr(0x123), @@ -89,7 +91,9 @@ const proveAndVerifyAvmTestContract = async ( worldStateDB.getContractInstance .mockResolvedValueOnce(contractInstance) - .mockResolvedValueOnce(instanceGet) + .mockResolvedValueOnce(instanceGet) // test gets deployer + .mockResolvedValueOnce(instanceGet) // test gets class id + .mockResolvedValueOnce(instanceGet) // test gets init hash .mockResolvedValue(contractInstance); worldStateDB.getContractClass.mockResolvedValue(contractClass); diff --git a/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts b/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts index bff43d40840..c594663f8de 100644 --- a/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts +++ b/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts @@ -95,7 +95,15 @@ describe('e2e_avm_simulator', () => { describe('Contract instance', () => { it('Works', async () => { - const tx = await avmContract.methods.test_get_contract_instance().send().wait(); + const tx = await avmContract.methods + .test_get_contract_instance_matches( + avmContract.address, + avmContract.instance.deployer, + avmContract.instance.contractClassId, + avmContract.instance.initializationHash, + ) + .send() + .wait(); expect(tx.status).toEqual(TxStatus.SUCCESS); }); }); diff --git a/yarn-project/ivc-integration/src/avm_integration.test.ts b/yarn-project/ivc-integration/src/avm_integration.test.ts index 9aa3016ded5..e3404915972 100644 --- a/yarn-project/ivc-integration/src/avm_integration.test.ts +++ b/yarn-project/ivc-integration/src/avm_integration.test.ts @@ -183,7 +183,9 @@ const proveAvmTestContract = async ( worldStateDB.getContractInstance .mockResolvedValueOnce(contractInstance) - .mockResolvedValueOnce(instanceGet) + .mockResolvedValueOnce(instanceGet) // test gets deployer + .mockResolvedValueOnce(instanceGet) // test gets class id + .mockResolvedValueOnce(instanceGet) // test gets init hash .mockResolvedValue(contractInstance); worldStateDB.getContractClass.mockResolvedValue(contractClass); diff --git a/yarn-project/simulator/src/avm/avm_simulator.test.ts b/yarn-project/simulator/src/avm/avm_simulator.test.ts index 85ba0d0c014..613c7bf68a0 100644 --- a/yarn-project/simulator/src/avm/avm_simulator.test.ts +++ b/yarn-project/simulator/src/avm/avm_simulator.test.ts @@ -1,4 +1,4 @@ -import { GasFees, PublicKeys } from '@aztec/circuits.js'; +import { GasFees, PublicKeys, SerializableContractInstance } from '@aztec/circuits.js'; import { Grumpkin } from '@aztec/circuits.js/barretenberg'; import { computeVarArgsHash } from '@aztec/circuits.js/hash'; import { makeContractClassPublic, makeContractInstanceFromClassId } from '@aztec/circuits.js/testing'; @@ -826,10 +826,10 @@ describe('AVM simulator: transpiled Noir contracts', () => { describe('Contract Instance Retrieval', () => { it(`Can getContractInstance`, async () => { - const context = createContext(); + const calldata = [address]; + const context = createContext(calldata); // Contract instance must match noir - const contractInstance = { - address: AztecAddress.random(), + const contractInstance = new SerializableContractInstance({ version: 1 as const, salt: new Fr(0x123), deployer: AztecAddress.fromBigInt(0x456n), @@ -841,16 +841,16 @@ describe('AVM simulator: transpiled Noir contracts', () => { new Point(new Fr(0x252627), new Fr(0x282930), false), new Point(new Fr(0x313233), new Fr(0x343536), false), ), - }; - mockGetContractInstance(worldStateDB, contractInstance); + }); + mockGetContractInstance(worldStateDB, contractInstance.withAddress(address)); - const bytecode = getAvmTestContractBytecode('test_get_contract_instance_raw'); + const bytecode = getAvmTestContractBytecode('test_get_contract_instance'); const results = await new AvmSimulator(context).executeBytecode(bytecode); expect(results.reverted).toBe(false); - expect(trace.traceGetContractInstance).toHaveBeenCalledTimes(1); - expect(trace.traceGetContractInstance).toHaveBeenCalledWith({ exists: true, ...contractInstance }); + expect(trace.traceGetContractInstance).toHaveBeenCalledTimes(3); // called for each enum value + expect(trace.traceGetContractInstance).toHaveBeenCalledWith(address, /*exists=*/ true, contractInstance); }); }); diff --git a/yarn-project/simulator/src/avm/journal/journal.test.ts b/yarn-project/simulator/src/avm/journal/journal.test.ts index e3feaf9ee53..47288f52805 100644 --- a/yarn-project/simulator/src/avm/journal/journal.test.ts +++ b/yarn-project/simulator/src/avm/journal/journal.test.ts @@ -1,4 +1,3 @@ -import { randomContractInstanceWithAddress } from '@aztec/circuit-types'; import { SerializableContractInstance } from '@aztec/circuits.js'; import { Fr } from '@aztec/foundation/fields'; @@ -147,18 +146,17 @@ describe('journal', () => { describe('Getting contract instances', () => { it('Should get contract instance', async () => { - const contractInstance = randomContractInstanceWithAddress(/*(base instance) opts=*/ {}, /*address=*/ address); - mockGetContractInstance(worldStateDB, contractInstance); + const contractInstance = SerializableContractInstance.default(); + mockGetContractInstance(worldStateDB, contractInstance.withAddress(address)); + mockGetContractInstance(worldStateDB, contractInstance.withAddress(address)); await persistableState.getContractInstance(address); expect(trace.traceGetContractInstance).toHaveBeenCalledTimes(1); - expect(trace.traceGetContractInstance).toHaveBeenCalledWith({ exists: true, ...contractInstance }); + expect(trace.traceGetContractInstance).toHaveBeenCalledWith(address, /*exists=*/ true, contractInstance); }); it('Can get undefined contract instance', async () => { - const defaultContractInstance = SerializableContractInstance.default().withAddress(address); await persistableState.getContractInstance(address); - expect(trace.traceGetContractInstance).toHaveBeenCalledTimes(1); - expect(trace.traceGetContractInstance).toHaveBeenCalledWith({ exists: false, ...defaultContractInstance }); + expect(trace.traceGetContractInstance).toHaveBeenCalledWith(address, /*exists=*/ false); }); }); diff --git a/yarn-project/simulator/src/avm/journal/journal.ts b/yarn-project/simulator/src/avm/journal/journal.ts index 4a19de59e7a..abf8d067a90 100644 --- a/yarn-project/simulator/src/avm/journal/journal.ts +++ b/yarn-project/simulator/src/avm/journal/journal.ts @@ -11,7 +11,6 @@ import assert from 'assert'; import { getPublicFunctionDebugName } from '../../common/debug_fn_name.js'; import { type WorldStateDB } from '../../public/public_db_sources.js'; -import { type TracedContractInstance } from '../../public/side_effect_trace.js'; import { type PublicSideEffectTraceInterface } from '../../public/side_effect_trace_interface.js'; import { type AvmContractCallResult } from '../avm_contract_call_result.js'; import { type AvmExecutionEnvironment } from '../avm_execution_environment.js'; @@ -217,22 +216,26 @@ export class AvmPersistableStateManager { /** * Get a contract instance. * @param contractAddress - address of the contract instance to retrieve. - * @returns the contract instance with an "exists" flag + * @returns the contract instance or undefined if it does not exist. */ - public async getContractInstance(contractAddress: Fr): Promise { - let exists = true; - const aztecAddress = AztecAddress.fromField(contractAddress); - let instance = await this.worldStateDB.getContractInstance(aztecAddress); - if (instance === undefined) { - instance = SerializableContractInstance.default().withAddress(aztecAddress); - exists = false; + public async getContractInstance(contractAddress: Fr): Promise { + this.log.debug(`Getting contract instance for address ${contractAddress}`); + const instanceWithAddress = await this.worldStateDB.getContractInstance(AztecAddress.fromField(contractAddress)); + const exists = instanceWithAddress !== undefined; + + if (exists) { + const instance = new SerializableContractInstance(instanceWithAddress); + this.log.debug( + `Got contract instance (address=${contractAddress}): exists=${exists}, instance=${JSON.stringify(instance)}`, + ); + this.trace.traceGetContractInstance(contractAddress, exists, instance); + + return Promise.resolve(instance); + } else { + this.log.debug(`Contract instance NOT FOUND (address=${contractAddress})`); + this.trace.traceGetContractInstance(contractAddress, exists); + return Promise.resolve(undefined); } - this.log.debug( - `Get Contract instance (address=${contractAddress}): exists=${exists}, instance=${JSON.stringify(instance)}`, - ); - const tracedInstance = { ...instance, exists }; - this.trace.traceGetContractInstance(tracedInstance); - return Promise.resolve(tracedInstance); } /** @@ -247,35 +250,40 @@ export class AvmPersistableStateManager { * Get a contract's bytecode from the contracts DB, also trace the contract class and instance */ public async getBytecode(contractAddress: AztecAddress): Promise { - let exists = true; - let contractInstance = await this.worldStateDB.getContractInstance(contractAddress); - // If the contract instance is not found, we assume it has not been deployed. - // It doesnt matter what the values of the contract instance are in this case, as long as we tag it with exists=false. - // This will hint to the avm circuit to just perform the non-membership check on the address and disregard the bytecode hash - if (!contractInstance) { - contractInstance = SerializableContractInstance.default().withAddress(contractAddress); - exists = false; - const defaultBytecode = Buffer.alloc(0); + this.log.debug(`Getting bytecode for contract address ${contractAddress}`); + const instanceWithAddress = await this.worldStateDB.getContractInstance(contractAddress); + const exists = instanceWithAddress !== undefined; + + if (exists) { + const instance = new SerializableContractInstance(instanceWithAddress); + + const contractClass = await this.worldStateDB.getContractClass(instance.contractClassId); + assert( + contractClass, + `Contract class not found in DB, but a contract instance was found with this class ID (${instance.contractClassId}). This should not happen!`, + ); + const contractClassPreimage = { - artifactHash: Fr.ZERO, - privateFunctionsRoot: Fr.ZERO, - publicBytecodeCommitment: Fr.ZERO, + artifactHash: contractClass.artifactHash, + privateFunctionsRoot: contractClass.privateFunctionsRoot, + publicBytecodeCommitment: computePublicBytecodeCommitment(contractClass.packedBytecode), }; - this.trace.traceGetBytecode(defaultBytecode, { exists, ...contractInstance }, contractClassPreimage); - } - const contractClass = await this.worldStateDB.getContractClass(contractInstance.contractClassId); - assert( - contractClass, - `Contract class not found in DB, but a contract instance was found with this class ID (${contractInstance.contractClassId}). This should not happen!`, - ); - const contractClassPreimage = { - artifactHash: contractClass.artifactHash, - privateFunctionsRoot: contractClass.privateFunctionsRoot, - publicBytecodeCommitment: computePublicBytecodeCommitment(contractClass.packedBytecode), - }; - this.trace.traceGetBytecode(contractClass.packedBytecode, { exists, ...contractInstance }, contractClassPreimage); - return contractClass.packedBytecode; + this.trace.traceGetBytecode( + contractAddress, + exists, + contractClass.packedBytecode, + instance, + contractClassPreimage, + ); + return contractClass.packedBytecode; + } else { + // If the contract instance is not found, we assume it has not been deployed. + // It doesnt matter what the values of the contract instance are in this case, as long as we tag it with exists=false. + // This will hint to the avm circuit to just perform the non-membership check on the address and disregard the bytecode hash + this.trace.traceGetBytecode(contractAddress, exists); // bytecode, instance, class undefined + return undefined; + } } /** diff --git a/yarn-project/simulator/src/avm/opcodes/contract.test.ts b/yarn-project/simulator/src/avm/opcodes/contract.test.ts index 1ba6ff2c2c1..818ff2844b0 100644 --- a/yarn-project/simulator/src/avm/opcodes/contract.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/contract.test.ts @@ -1,19 +1,22 @@ -import { randomContractInstanceWithAddress } from '@aztec/circuit-types'; -import { AztecAddress, PublicKeys, SerializableContractInstance } from '@aztec/circuits.js'; +import { Fr, SerializableContractInstance } from '@aztec/circuits.js'; import { mock } from 'jest-mock-extended'; import { type WorldStateDB } from '../../public/public_db_sources.js'; import { type PublicSideEffectTraceInterface } from '../../public/side_effect_trace_interface.js'; import { type AvmContext } from '../avm_context.js'; -import { Field } from '../avm_memory_types.js'; +import { Field, TypeTag, Uint1 } from '../avm_memory_types.js'; import { initContext, initPersistableStateManager } from '../fixtures/index.js'; import { type AvmPersistableStateManager } from '../journal/journal.js'; import { mockGetContractInstance } from '../test_utils.js'; -import { GetContractInstance } from './contract.js'; +import { ContractInstanceMember, GetContractInstance } from './contract.js'; describe('Contract opcodes', () => { - const address = AztecAddress.random(); + const address = Fr.random(); + const contractInstance = SerializableContractInstance.random(); + const deployer = contractInstance.deployer; + const contractClassId = contractInstance.contractClassId; + const initializationHash = contractInstance.initializationHash; let worldStateDB: WorldStateDB; let trace: PublicSideEffectTraceInterface; @@ -32,61 +35,102 @@ describe('Contract opcodes', () => { const buf = Buffer.from([ GetContractInstance.opcode, // opcode 0x01, // indirect - ...Buffer.from('12345678', 'hex'), // addressOffset - ...Buffer.from('a2345678', 'hex'), // dstOffset + 0x02, // memberEnum (immediate) + ...Buffer.from('1234', 'hex'), // addressOffset + ...Buffer.from('a234', 'hex'), // dstOffset + ...Buffer.from('b234', 'hex'), // existsOffset ]); const inst = new GetContractInstance( /*indirect=*/ 0x01, - /*addressOffset=*/ 0x12345678, - /*dstOffset=*/ 0xa2345678, + /*memberEnum=*/ 0x02, + /*addressOffset=*/ 0x1234, + /*dstOffset=*/ 0xa234, + /*existsOffset=*/ 0xb234, ); expect(GetContractInstance.deserialize(buf)).toEqual(inst); expect(inst.serialize()).toEqual(buf); }); - it('should copy contract instance to memory if found', async () => { - const contractInstance = randomContractInstanceWithAddress(/*(base instance) opts=*/ {}, /*address=*/ address); - mockGetContractInstance(worldStateDB, contractInstance); - - context.machineState.memory.set(0, new Field(address.toField())); - await new GetContractInstance(/*indirect=*/ 0, /*addressOffset=*/ 0, /*dstOffset=*/ 1).execute(context); - - const actual = context.machineState.memory.getSlice(1, 17); - - expect(actual).toEqual([ - new Field(1), // found - new Field(contractInstance.salt), - new Field(contractInstance.deployer), - new Field(contractInstance.contractClassId), - new Field(contractInstance.initializationHash), - ...contractInstance.publicKeys.toFields().map(f => new Field(f)), - ]); - - expect(trace.traceGetContractInstance).toHaveBeenCalledTimes(1); - expect(trace.traceGetContractInstance).toHaveBeenCalledWith({ exists: true, ...contractInstance }); + describe.each([ + [ContractInstanceMember.DEPLOYER, deployer.toField()], + [ContractInstanceMember.CLASS_ID, contractClassId.toField()], + [ContractInstanceMember.INIT_HASH, initializationHash.toField()], + ])('GETCONTRACTINSTANCE member instruction ', (memberEnum: ContractInstanceMember, value: Fr) => { + it(`Should read '${ContractInstanceMember[memberEnum]}' correctly`, async () => { + mockGetContractInstance(worldStateDB, contractInstance.withAddress(address)); + + context.machineState.memory.set(0, new Field(address)); + await new GetContractInstance( + /*indirect=*/ 0, + memberEnum, + /*addressOffset=*/ 0, + /*dstOffset=*/ 1, + /*existsOffset=*/ 2, + ).execute(context); + + // value should be right + expect(context.machineState.memory.getTag(1)).toBe(TypeTag.FIELD); + const actual = context.machineState.memory.get(1); + expect(actual).toEqual(new Field(value)); + + // exists should be true + expect(context.machineState.memory.getTag(2)).toBe(TypeTag.UINT1); + const exists = context.machineState.memory.get(2); + expect(exists).toEqual(new Uint1(1)); + + expect(trace.traceGetContractInstance).toHaveBeenCalledTimes(1); + expect(trace.traceGetContractInstance).toHaveBeenCalledWith(address, /*exists=*/ true, contractInstance); + }); }); - it('should return zeroes if not found', async () => { - const defaultContractInstance = SerializableContractInstance.default().withAddress(address); - context.machineState.memory.set(0, new Field(address.toField())); - - await new GetContractInstance(/*indirect=*/ 0, /*addressOffset=*/ 0, /*dstOffset=*/ 1).execute(context); - - const actual = context.machineState.memory.getSlice(1, 17); - expect(actual).toEqual([ - new Field(0), // found - new Field(0), - new Field(0), - new Field(0), - new Field(0), - ...PublicKeys.default() - .toFields() - .map(f => new Field(f)), - ]); + describe.each([ + [ContractInstanceMember.DEPLOYER], + [ContractInstanceMember.CLASS_ID], + [ContractInstanceMember.INIT_HASH], + ])( + 'GETCONTRACTINSTANCE member instruction works when contract does not exist', + (memberEnum: ContractInstanceMember) => { + it(`'${ContractInstanceMember[memberEnum]}' should be 0 when contract does not exist `, async () => { + context.machineState.memory.set(0, new Field(address)); + await new GetContractInstance( + /*indirect=*/ 0, + memberEnum, + /*addressOffset=*/ 0, + /*dstOffset=*/ 1, + /*existsOffset=*/ 2, + ).execute(context); + + // value should be 0 + expect(context.machineState.memory.getTag(1)).toBe(TypeTag.FIELD); + const actual = context.machineState.memory.get(1); + expect(actual).toEqual(new Field(0)); + + // exists should be false + expect(context.machineState.memory.getTag(2)).toBe(TypeTag.UINT1); + const exists = context.machineState.memory.get(2); + expect(exists).toEqual(new Uint1(0)); + + expect(trace.traceGetContractInstance).toHaveBeenCalledTimes(1); + expect(trace.traceGetContractInstance).toHaveBeenCalledWith(address, /*exists=*/ false); + }); + }, + ); + + it(`GETCONTRACTINSTANCE reverts for bad enum operand`, async () => { + const invalidEnum = 255; + const instruction = new GetContractInstance( + /*indirect=*/ 0, + /*memberEnum=*/ invalidEnum, + /*addressOffset=*/ 0, + /*dstOffset=*/ 1, + /*existsOffset=*/ 2, + ); + await expect(instruction.execute(context)).rejects.toThrow( + `Invalid GETCONSTRACTINSTANCE member enum ${invalidEnum}`, + ); - expect(trace.traceGetContractInstance).toHaveBeenCalledTimes(1); - expect(trace.traceGetContractInstance).toHaveBeenCalledWith({ exists: false, ...defaultContractInstance }); + expect(trace.traceGetContractInstance).toHaveBeenCalledTimes(0); }); }); }); diff --git a/yarn-project/simulator/src/avm/opcodes/contract.ts b/yarn-project/simulator/src/avm/opcodes/contract.ts index 4ee050e1c9e..76f821755cb 100644 --- a/yarn-project/simulator/src/avm/opcodes/contract.ts +++ b/yarn-project/simulator/src/avm/opcodes/contract.ts @@ -1,23 +1,36 @@ -import { Fr } from '@aztec/circuits.js'; - import type { AvmContext } from '../avm_context.js'; -import { Field, TypeTag } from '../avm_memory_types.js'; +import { Field, TypeTag, Uint1 } from '../avm_memory_types.js'; +import { InstructionExecutionError } from '../errors.js'; import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; import { Addressing } from './addressing_mode.js'; import { Instruction } from './instruction.js'; +export enum ContractInstanceMember { + DEPLOYER, + CLASS_ID, + INIT_HASH, +} + export class GetContractInstance extends Instruction { static readonly type: string = 'GETCONTRACTINSTANCE'; static readonly opcode: Opcode = Opcode.GETCONTRACTINSTANCE; // Informs (de)serialization. See Instruction.deserialize. static readonly wireFormat: OperandType[] = [ - OperandType.UINT8, - OperandType.UINT8, - OperandType.UINT32, - OperandType.UINT32, + OperandType.UINT8, // opcode + OperandType.UINT8, // indirect bits + OperandType.UINT8, // member enum (immediate) + OperandType.UINT16, // addressOffset + OperandType.UINT16, // dstOffset + OperandType.UINT16, // existsOfsset ]; - constructor(private indirect: number, private addressOffset: number, private dstOffset: number) { + constructor( + private indirect: number, + private memberEnum: number, + private addressOffset: number, + private dstOffset: number, + private existsOffset: number, + ) { super(); } @@ -25,27 +38,38 @@ export class GetContractInstance extends Instruction { const memory = context.machineState.memory.track(this.type); context.machineState.consumeGas(this.gasCost()); - const operands = [this.addressOffset, this.dstOffset]; + if (!(this.memberEnum in ContractInstanceMember)) { + throw new InstructionExecutionError(`Invalid GETCONSTRACTINSTANCE member enum ${this.memberEnum}`); + } + + const operands = [this.addressOffset, this.dstOffset, this.existsOffset]; const addressing = Addressing.fromWire(this.indirect, operands.length); - const [addressOffset, dstOffset] = addressing.resolve(operands, memory); + const [addressOffset, dstOffset, existsOffset] = addressing.resolve(operands, memory); memory.checkTag(TypeTag.FIELD, addressOffset); const address = memory.get(addressOffset).toFr(); const instance = await context.persistableState.getContractInstance(address); + const exists = instance !== undefined; - const data = [ - new Fr(instance.exists), - instance.salt, - instance.deployer.toField(), - instance.contractClassId, - instance.initializationHash, - // This this okay ? - ...instance.publicKeys.toFields(), - ].map(f => new Field(f)); + let memberValue = new Field(0); + if (exists) { + switch (this.memberEnum as ContractInstanceMember) { + case ContractInstanceMember.DEPLOYER: + memberValue = new Field(instance.deployer.toField()); + break; + case ContractInstanceMember.CLASS_ID: + memberValue = new Field(instance.contractClassId.toField()); + break; + case ContractInstanceMember.INIT_HASH: + memberValue = new Field(instance.initializationHash); + break; + } + } - memory.setSlice(dstOffset, data); + memory.set(existsOffset, new Uint1(exists ? 1 : 0)); + memory.set(dstOffset, memberValue); - memory.assert({ reads: 1, writes: 17, addressing }); + memory.assert({ reads: 1, writes: 2, addressing }); context.machineState.incrementPc(); } } diff --git a/yarn-project/simulator/src/avm/opcodes/environment_getters.test.ts b/yarn-project/simulator/src/avm/opcodes/environment_getters.test.ts index 6f30a861577..8821e52cdf1 100644 --- a/yarn-project/simulator/src/avm/opcodes/environment_getters.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/environment_getters.test.ts @@ -83,4 +83,10 @@ describe('Environment getters', () => { expect(actual).toEqual(value); }); }); + + it(`GETENVVAR reverts for bad enum operand`, async () => { + const invalidEnum = 255; + const instruction = new GetEnvVar(/*indirect=*/ 0, invalidEnum, /*dstOffset=*/ 0); + await expect(instruction.execute(context)).rejects.toThrowError(`Invalid GETENVVAR var enum ${invalidEnum}`); + }); }); diff --git a/yarn-project/simulator/src/avm/opcodes/environment_getters.ts b/yarn-project/simulator/src/avm/opcodes/environment_getters.ts index 5bfb947d732..4f9205adff7 100644 --- a/yarn-project/simulator/src/avm/opcodes/environment_getters.ts +++ b/yarn-project/simulator/src/avm/opcodes/environment_getters.ts @@ -1,5 +1,6 @@ import type { AvmContext } from '../avm_context.js'; import { Field, Uint32, Uint64 } from '../avm_memory_types.js'; +import { InstructionExecutionError } from '../errors.js'; import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; import { Addressing } from './addressing_mode.js'; import { Instruction } from './instruction.js'; @@ -63,7 +64,7 @@ export class GetEnvVar extends Instruction { OperandType.UINT16, // dstOffset ]; - constructor(private indirect: number, private varEnum: EnvironmentVariable, private dstOffset: number) { + constructor(private indirect: number, private varEnum: number, private dstOffset: number) { super(); } @@ -71,11 +72,15 @@ export class GetEnvVar extends Instruction { const memory = context.machineState.memory.track(this.type); context.machineState.consumeGas(this.gasCost()); + if (!(this.varEnum in EnvironmentVariable)) { + throw new InstructionExecutionError(`Invalid GETENVVAR var enum ${this.varEnum}`); + } + const operands = [this.dstOffset]; const addressing = Addressing.fromWire(this.indirect, operands.length); const [dstOffset] = addressing.resolve(operands, memory); - memory.set(dstOffset, getValue(this.varEnum, context)); + memory.set(dstOffset, getValue(this.varEnum as EnvironmentVariable, context)); memory.assert({ writes: 1, addressing }); context.machineState.incrementPc(); diff --git a/yarn-project/simulator/src/public/dual_side_effect_trace.ts b/yarn-project/simulator/src/public/dual_side_effect_trace.ts index 7067f2f0899..b38e7845b7d 100644 --- a/yarn-project/simulator/src/public/dual_side_effect_trace.ts +++ b/yarn-project/simulator/src/public/dual_side_effect_trace.ts @@ -1,9 +1,9 @@ -import type { - CombinedConstantData, - ContractClassIdPreimage, - ContractInstanceWithAddress, - Gas, - VMCircuitPublicInputs, +import { + type CombinedConstantData, + type ContractClassIdPreimage, + type Gas, + type SerializableContractInstance, + type VMCircuitPublicInputs, } from '@aztec/circuits.js'; import { type Fr } from '@aztec/foundation/fields'; @@ -16,8 +16,6 @@ import { type PublicExecutionResult } from './execution.js'; import { type PublicSideEffectTrace } from './side_effect_trace.js'; import { type PublicSideEffectTraceInterface } from './side_effect_trace_interface.js'; -export type TracedContractInstance = { exists: boolean } & ContractInstanceWithAddress; - export class DualSideEffectTrace implements PublicSideEffectTraceInterface { constructor( public readonly innerCallTrace: PublicSideEffectTrace, @@ -33,15 +31,6 @@ export class DualSideEffectTrace implements PublicSideEffectTraceInterface { return this.innerCallTrace.getCounter(); } - public traceGetBytecode( - bytecode: Buffer, - contractInstance: TracedContractInstance, - contractClass: ContractClassIdPreimage, - ) { - this.innerCallTrace.traceGetBytecode(bytecode, contractInstance, contractClass); - this.enqueuedCallTrace.traceGetBytecode(bytecode, contractInstance, contractClass); - } - public tracePublicStorageRead(contractAddress: Fr, slot: Fr, value: Fr, exists: boolean, cached: boolean) { this.innerCallTrace.tracePublicStorageRead(contractAddress, slot, value, exists, cached); this.enqueuedCallTrace.tracePublicStorageRead(contractAddress, slot, value, exists, cached); @@ -88,9 +77,24 @@ export class DualSideEffectTrace implements PublicSideEffectTraceInterface { this.enqueuedCallTrace.traceUnencryptedLog(contractAddress, log); } - public traceGetContractInstance(instance: TracedContractInstance) { - this.innerCallTrace.traceGetContractInstance(instance); - this.enqueuedCallTrace.traceGetContractInstance(instance); + public traceGetContractInstance( + contractAddress: Fr, + exists: boolean, + instance: SerializableContractInstance | undefined, + ) { + this.innerCallTrace.traceGetContractInstance(contractAddress, exists, instance); + this.enqueuedCallTrace.traceGetContractInstance(contractAddress, exists, instance); + } + + public traceGetBytecode( + contractAddress: Fr, + exists: boolean, + bytecode: Buffer, + contractInstance: SerializableContractInstance | undefined, + contractClass: ContractClassIdPreimage | undefined, + ) { + this.innerCallTrace.traceGetBytecode(contractAddress, exists, bytecode, contractInstance, contractClass); + this.enqueuedCallTrace.traceGetBytecode(contractAddress, exists, bytecode, contractInstance, contractClass); } /** diff --git a/yarn-project/simulator/src/public/enqueued_call_side_effect_trace.test.ts b/yarn-project/simulator/src/public/enqueued_call_side_effect_trace.test.ts index 5bc2fc0f5ff..08d38f382f3 100644 --- a/yarn-project/simulator/src/public/enqueued_call_side_effect_trace.test.ts +++ b/yarn-project/simulator/src/public/enqueued_call_side_effect_trace.test.ts @@ -34,15 +34,9 @@ import { randomBytes, randomInt } from 'crypto'; import { AvmContractCallResult } from '../avm/avm_contract_call_result.js'; import { initExecutionEnvironment } from '../avm/fixtures/index.js'; -import { PublicEnqueuedCallSideEffectTrace, type TracedContractInstance } from './enqueued_call_side_effect_trace.js'; +import { PublicEnqueuedCallSideEffectTrace } from './enqueued_call_side_effect_trace.js'; import { SideEffectLimitReachedError } from './side_effect_errors.js'; -function randomTracedContractInstance(): TracedContractInstance { - const instance = SerializableContractInstance.random(); - const address = AztecAddress.random(); - return { exists: true, ...instance, address }; -} - describe('Enqueued-call Side Effect Trace', () => { const address = Fr.random(); const utxo = Fr.random(); @@ -52,7 +46,7 @@ describe('Enqueued-call Side Effect Trace', () => { const recipient = Fr.random(); const content = Fr.random(); const log = [Fr.random(), Fr.random(), Fr.random()]; - const contractInstance = SerializableContractInstance.default().withAddress(new Fr(42)); + const contractInstance = SerializableContractInstance.default(); const startGasLeft = Gas.fromFields([new Fr(randomInt(10000)), new Fr(randomInt(10000))]); const endGasLeft = Gas.fromFields([new Fr(randomInt(10000)), new Fr(randomInt(10000))]); @@ -234,18 +228,18 @@ describe('Enqueued-call Side Effect Trace', () => { }); it('Should trace get contract instance', () => { - const instance = randomTracedContractInstance(); + const instance = SerializableContractInstance.random(); const { version: _, ...instanceWithoutVersion } = instance; - trace.traceGetContractInstance(instance); + const exists = true; + trace.traceGetContractInstance(address, exists, instance); expect(trace.getCounter()).toBe(startCounterPlus1); //const circuitPublicInputs = toVMCircuitPublicInputs(trace); - // TODO(dbanks12): once this emits nullifier read, check here expect(trace.getAvmCircuitHints().contractInstances.items).toEqual([ { - // hint omits "version" and has "exists" as an Fr + address, + exists, ...instanceWithoutVersion, - exists: instance.exists, }, ]); }); @@ -348,11 +342,11 @@ describe('Enqueued-call Side Effect Trace', () => { for (let i = 0; i < MAX_NULLIFIER_READ_REQUESTS_PER_TX; i++) { trace.traceNullifierCheck(new Fr(i), new Fr(i), new Fr(i), true, true); } - expect(() => trace.traceGetContractInstance({ ...contractInstance, exists: true })).toThrow( + expect(() => trace.traceGetContractInstance(address, /*exists=*/ true, contractInstance)).toThrow( SideEffectLimitReachedError, ); // NOTE: also cannot do a existent check once non-existent checks have filled up - expect(() => trace.traceGetContractInstance({ ...contractInstance, exists: false })).toThrow( + expect(() => trace.traceGetContractInstance(address, /*exists=*/ false, contractInstance)).toThrow( SideEffectLimitReachedError, ); }); @@ -361,11 +355,11 @@ describe('Enqueued-call Side Effect Trace', () => { for (let i = 0; i < MAX_NULLIFIER_NON_EXISTENT_READ_REQUESTS_PER_TX; i++) { trace.traceNullifierCheck(new Fr(i), new Fr(i), new Fr(i), false, true); } - expect(() => trace.traceGetContractInstance({ ...contractInstance, exists: false })).toThrow( + expect(() => trace.traceGetContractInstance(address, /*exists=*/ false, contractInstance)).toThrow( SideEffectLimitReachedError, ); // NOTE: also cannot do a existent check once non-existent checks have filled up - expect(() => trace.traceGetContractInstance({ ...contractInstance, exists: true })).toThrow( + expect(() => trace.traceGetContractInstance(address, /*exists=*/ true, contractInstance)).toThrow( SideEffectLimitReachedError, ); }); @@ -417,10 +411,10 @@ describe('Enqueued-call Side Effect Trace', () => { expect(() => trace.traceUnencryptedLog(new Fr(42), [new Fr(42), new Fr(42)])).toThrow( SideEffectLimitReachedError, ); - expect(() => trace.traceGetContractInstance({ ...contractInstance, exists: false })).toThrow( + expect(() => trace.traceGetContractInstance(address, /*exists=*/ false, contractInstance)).toThrow( SideEffectLimitReachedError, ); - expect(() => trace.traceGetContractInstance({ ...contractInstance, exists: true })).toThrow( + expect(() => trace.traceGetContractInstance(address, /*exists=*/ true, contractInstance)).toThrow( SideEffectLimitReachedError, ); }); @@ -454,9 +448,9 @@ describe('Enqueued-call Side Effect Trace', () => { testCounter++; nestedTrace.traceUnencryptedLog(address, log); testCounter++; - nestedTrace.traceGetContractInstance({ ...contractInstance, exists: true }); + nestedTrace.traceGetContractInstance(address, /*exists=*/ true, contractInstance); testCounter++; - nestedTrace.traceGetContractInstance({ ...contractInstance, exists: false }); + nestedTrace.traceGetContractInstance(address, /*exists=*/ false, contractInstance); testCounter++; trace.traceNestedCall(nestedTrace, avmEnvironment, startGasLeft, endGasLeft, bytecode, callResults); diff --git a/yarn-project/simulator/src/public/enqueued_call_side_effect_trace.ts b/yarn-project/simulator/src/public/enqueued_call_side_effect_trace.ts index 84d7f429b85..a6f14282fa6 100644 --- a/yarn-project/simulator/src/public/enqueued_call_side_effect_trace.ts +++ b/yarn-project/simulator/src/public/enqueued_call_side_effect_trace.ts @@ -9,7 +9,6 @@ import { CallContext, type CombinedConstantData, type ContractClassIdPreimage, - type ContractInstanceWithAddress, ContractStorageRead, ContractStorageUpdateRequest, EthAddress, @@ -46,6 +45,7 @@ import { ScopedNoteHash, type ScopedNullifier, ScopedReadRequest, + SerializableContractInstance, TreeLeafReadRequest, VMCircuitPublicInputs, } from '@aztec/circuits.js'; @@ -60,8 +60,6 @@ import { type AvmExecutionEnvironment } from '../avm/avm_execution_environment.j import { SideEffectLimitReachedError } from './side_effect_errors.js'; import { type PublicSideEffectTraceInterface } from './side_effect_trace_interface.js'; -export type TracedContractInstance = { exists: boolean } & ContractInstanceWithAddress; - /** * A struct containing just the side effects as regular arrays * as opposed to "Tuple" arrays used by circuit public inputs. @@ -159,28 +157,6 @@ export class PublicEnqueuedCallSideEffectTrace implements PublicSideEffectTraceI this.sideEffectCounter++; } - // This tracing function gets called everytime we start simulation/execution. - // This happens both when starting a new top-level trace and the start of every nested trace - // We use this to collect the AvmContractBytecodeHints - public traceGetBytecode( - bytecode: Buffer, - contractInstance: TracedContractInstance, - contractClass: ContractClassIdPreimage, - ) { - const instance = new AvmContractInstanceHint( - contractInstance.address, - contractInstance.exists, - contractInstance.salt, - contractInstance.deployer, - contractInstance.contractClassId, - contractInstance.initializationHash, - contractInstance.publicKeys, - ); - this.avmCircuitHints.contractBytecodeHints.items.push( - new AvmContractBytecodeHints(bytecode, instance, contractClass), - ); - } - public tracePublicStorageRead(contractAddress: Fr, slot: Fr, value: Fr, _exists: boolean, _cached: boolean) { // NOTE: exists and cached are unused for now but may be used for optimizations or kernel hints later if ( @@ -331,14 +307,17 @@ export class PublicEnqueuedCallSideEffectTrace implements PublicSideEffectTraceI this.incrementSideEffectCounter(); } - public traceGetContractInstance(instance: TracedContractInstance) { + public traceGetContractInstance( + contractAddress: Fr, + exists: boolean, + instance: SerializableContractInstance = SerializableContractInstance.default(), + ) { this.enforceLimitOnNullifierChecks('(contract address nullifier from GETCONTRACTINSTANCE)'); - // TODO(dbanks12): should emit a nullifier read request this.avmCircuitHints.contractInstances.items.push( new AvmContractInstanceHint( - instance.address, - instance.exists, + contractAddress, + exists, instance.salt, instance.deployer, instance.contractClassId, @@ -350,6 +329,40 @@ export class PublicEnqueuedCallSideEffectTrace implements PublicSideEffectTraceI this.incrementSideEffectCounter(); } + // This tracing function gets called everytime we start simulation/execution. + // This happens both when starting a new top-level trace and the start of every nested trace + // We use this to collect the AvmContractBytecodeHints + public traceGetBytecode( + contractAddress: Fr, + exists: boolean, + bytecode: Buffer = Buffer.alloc(0), + contractInstance: SerializableContractInstance = SerializableContractInstance.default(), + contractClass: ContractClassIdPreimage = { + artifactHash: Fr.zero(), + privateFunctionsRoot: Fr.zero(), + publicBytecodeCommitment: Fr.zero(), + }, + ) { + const instance = new AvmContractInstanceHint( + contractAddress, + exists, + contractInstance.salt, + contractInstance.deployer, + contractInstance.contractClassId, + contractInstance.initializationHash, + contractInstance.publicKeys, + ); + // We need to deduplicate the contract instances based on addresses + this.avmCircuitHints.contractBytecodeHints.items.push( + new AvmContractBytecodeHints(bytecode, instance, contractClass), + ); + this.log.debug( + `Bytecode retrieval for contract execution traced: exists=${exists}, instance=${JSON.stringify( + contractInstance, + )}`, + ); + } + /** * Trace a nested call. * Accept some results from a finished nested call's trace into this one. diff --git a/yarn-project/simulator/src/public/side_effect_trace.test.ts b/yarn-project/simulator/src/public/side_effect_trace.test.ts index f24c04a9288..5eae41f49b4 100644 --- a/yarn-project/simulator/src/public/side_effect_trace.test.ts +++ b/yarn-project/simulator/src/public/side_effect_trace.test.ts @@ -23,13 +23,7 @@ import { randomBytes, randomInt } from 'crypto'; import { AvmContractCallResult } from '../avm/avm_contract_call_result.js'; import { initExecutionEnvironment } from '../avm/fixtures/index.js'; import { SideEffectLimitReachedError } from './side_effect_errors.js'; -import { PublicSideEffectTrace, type TracedContractInstance } from './side_effect_trace.js'; - -function randomTracedContractInstance(): TracedContractInstance { - const instance = SerializableContractInstance.random(); - const address = AztecAddress.random(); - return { exists: true, ...instance, address }; -} +import { PublicSideEffectTrace } from './side_effect_trace.js'; describe('Side Effect Trace', () => { const address = Fr.random(); @@ -40,7 +34,7 @@ describe('Side Effect Trace', () => { const recipient = Fr.random(); const content = Fr.random(); const log = [Fr.random(), Fr.random(), Fr.random()]; - const contractInstance = SerializableContractInstance.default().withAddress(new Fr(42)); + const contractInstance = SerializableContractInstance.default(); const startGasLeft = Gas.fromFields([new Fr(randomInt(10000)), new Fr(randomInt(10000))]); const endGasLeft = Gas.fromFields([new Fr(randomInt(10000)), new Fr(randomInt(10000))]); @@ -232,18 +226,19 @@ describe('Side Effect Trace', () => { }); it('Should trace get contract instance', () => { - const instance = randomTracedContractInstance(); + const instance = SerializableContractInstance.random(); const { version: _, ...instanceWithoutVersion } = instance; - trace.traceGetContractInstance(instance); + const exists = true; + trace.traceGetContractInstance(address, exists, instance); expect(trace.getCounter()).toBe(startCounterPlus1); const pxResult = toPxResult(trace); - // TODO(dbanks12): once this emits nullifier read, check here expect(pxResult.avmCircuitHints.contractInstances.items).toEqual([ { - // hint omits "version" and has "exists" as an Fr + // hint omits "version" + address, + exists, ...instanceWithoutVersion, - exists: instance.exists, }, ]); }); @@ -346,11 +341,11 @@ describe('Side Effect Trace', () => { for (let i = 0; i < MAX_NULLIFIER_READ_REQUESTS_PER_TX; i++) { trace.traceNullifierCheck(new Fr(i), new Fr(i), new Fr(i), true, true); } - expect(() => trace.traceGetContractInstance({ ...contractInstance, exists: true })).toThrow( + expect(() => trace.traceGetContractInstance(address, /*exists=*/ true, contractInstance)).toThrow( SideEffectLimitReachedError, ); // NOTE: also cannot do a existent check once non-existent checks have filled up - expect(() => trace.traceGetContractInstance({ ...contractInstance, exists: false })).toThrow( + expect(() => trace.traceGetContractInstance(address, /*exists=*/ false, contractInstance)).toThrow( SideEffectLimitReachedError, ); }); @@ -359,11 +354,11 @@ describe('Side Effect Trace', () => { for (let i = 0; i < MAX_NULLIFIER_NON_EXISTENT_READ_REQUESTS_PER_TX; i++) { trace.traceNullifierCheck(new Fr(i), new Fr(i), new Fr(i), false, true); } - expect(() => trace.traceGetContractInstance({ ...contractInstance, exists: false })).toThrow( + expect(() => trace.traceGetContractInstance(address, /*exists=*/ false, contractInstance)).toThrow( SideEffectLimitReachedError, ); // NOTE: also cannot do a existent check once non-existent checks have filled up - expect(() => trace.traceGetContractInstance({ ...contractInstance, exists: true })).toThrow( + expect(() => trace.traceGetContractInstance(address, /*exists=*/ true, contractInstance)).toThrow( SideEffectLimitReachedError, ); }); @@ -396,9 +391,9 @@ describe('Side Effect Trace', () => { testCounter++; nestedTrace.traceUnencryptedLog(address, log); testCounter++; - nestedTrace.traceGetContractInstance({ ...contractInstance, exists: true }); + nestedTrace.traceGetContractInstance(address, /*exists=*/ true, contractInstance); testCounter++; - nestedTrace.traceGetContractInstance({ ...contractInstance, exists: false }); + nestedTrace.traceGetContractInstance(address, /*exists=*/ false, contractInstance); testCounter++; trace.traceNestedCall(nestedTrace, avmEnvironment, startGasLeft, endGasLeft, bytecode, avmCallResults); diff --git a/yarn-project/simulator/src/public/side_effect_trace.ts b/yarn-project/simulator/src/public/side_effect_trace.ts index 4c49796e21f..9116e566094 100644 --- a/yarn-project/simulator/src/public/side_effect_trace.ts +++ b/yarn-project/simulator/src/public/side_effect_trace.ts @@ -29,6 +29,7 @@ import { Nullifier, type PublicInnerCallRequest, ReadRequest, + SerializableContractInstance, TreeLeafReadRequest, } from '@aztec/circuits.js'; import { Fr } from '@aztec/foundation/fields'; @@ -44,7 +45,7 @@ import { type PublicSideEffectTraceInterface } from './side_effect_trace_interfa export type TracedContractInstance = { exists: boolean } & ContractInstanceWithAddress; export class PublicSideEffectTrace implements PublicSideEffectTraceInterface { - public logger = createDebugLogger('aztec:public_side_effect_trace'); + public log = createDebugLogger('aztec:public_side_effect_trace'); /** The side effect counter increments with every call to the trace. */ private sideEffectCounter: number; // kept as number until finalized for efficiency @@ -92,32 +93,6 @@ export class PublicSideEffectTrace implements PublicSideEffectTraceInterface { this.sideEffectCounter++; } - // TODO(dbanks12): checks against tx-wide limit need access to parent trace's length - - // This tracing function gets called everytime we start simulation/execution. - // This happens both when starting a new top-level trace and the start of every nested trace - // We use this to collect the AvmContractBytecodeHints - public traceGetBytecode( - bytecode: Buffer, - contractInstance: TracedContractInstance, - contractClass: ContractClassIdPreimage, - ) { - const instance = new AvmContractInstanceHint( - contractInstance.address, - contractInstance.exists, - contractInstance.salt, - contractInstance.deployer, - contractInstance.contractClassId, - contractInstance.initializationHash, - contractInstance.publicKeys, - ); - // We need to deduplicate the contract instances based on addresses - this.avmCircuitHints.contractBytecodeHints.items.push( - new AvmContractBytecodeHints(bytecode, instance, contractClass), - ); - this.logger.debug(`New contract instance execution traced: ${contractInstance.address} added`); - } - public tracePublicStorageRead(contractAddress: Fr, slot: Fr, value: Fr, _exists: boolean, _cached: boolean) { // NOTE: exists and cached are unused for now but may be used for optimizations or kernel hints later if (this.contractStorageReads.length >= MAX_PUBLIC_DATA_READS_PER_TX) { @@ -129,7 +104,7 @@ export class PublicSideEffectTrace implements PublicSideEffectTraceInterface { this.avmCircuitHints.storageValues.items.push( new AvmKeyValueHint(/*key=*/ new Fr(this.sideEffectCounter), /*value=*/ value), ); - this.logger.debug(`SLOAD cnt: ${this.sideEffectCounter} val: ${value} slot: ${slot}`); + this.log.debug(`SLOAD cnt: ${this.sideEffectCounter} val: ${value} slot: ${slot}`); this.incrementSideEffectCounter(); } @@ -140,7 +115,7 @@ export class PublicSideEffectTrace implements PublicSideEffectTraceInterface { this.contractStorageUpdateRequests.push( new ContractStorageUpdateRequest(slot, value, this.sideEffectCounter, contractAddress), ); - this.logger.debug(`SSTORE cnt: ${this.sideEffectCounter} val: ${value} slot: ${slot}`); + this.log.debug(`SSTORE cnt: ${this.sideEffectCounter} val: ${value} slot: ${slot}`); this.incrementSideEffectCounter(); } @@ -162,7 +137,7 @@ export class PublicSideEffectTrace implements PublicSideEffectTraceInterface { throw new SideEffectLimitReachedError('note hash', MAX_NOTE_HASHES_PER_TX); } this.noteHashes.push(new NoteHash(noteHash, this.sideEffectCounter)); - this.logger.debug(`NEW_NOTE_HASH cnt: ${this.sideEffectCounter}`); + this.log.debug(`NEW_NOTE_HASH cnt: ${this.sideEffectCounter}`); this.incrementSideEffectCounter(); } @@ -187,7 +162,7 @@ export class PublicSideEffectTrace implements PublicSideEffectTraceInterface { this.avmCircuitHints.nullifierExists.items.push( new AvmKeyValueHint(/*key=*/ new Fr(this.sideEffectCounter), /*value=*/ new Fr(exists ? 1 : 0)), ); - this.logger.debug(`NULLIFIER_EXISTS cnt: ${this.sideEffectCounter}`); + this.log.debug(`NULLIFIER_EXISTS cnt: ${this.sideEffectCounter}`); this.incrementSideEffectCounter(); } @@ -197,7 +172,7 @@ export class PublicSideEffectTrace implements PublicSideEffectTraceInterface { throw new SideEffectLimitReachedError('nullifier', MAX_NULLIFIERS_PER_TX); } this.nullifiers.push(new Nullifier(nullifier, this.sideEffectCounter, /*noteHash=*/ Fr.ZERO)); - this.logger.debug(`NEW_NULLIFIER cnt: ${this.sideEffectCounter}`); + this.log.debug(`NEW_NULLIFIER cnt: ${this.sideEffectCounter}`); this.incrementSideEffectCounter(); } @@ -220,7 +195,7 @@ export class PublicSideEffectTrace implements PublicSideEffectTraceInterface { } const recipientAddress = EthAddress.fromField(recipient); this.newL2ToL1Messages.push(new L2ToL1Message(recipientAddress, content, this.sideEffectCounter)); - this.logger.debug(`NEW_L2_TO_L1_MSG cnt: ${this.sideEffectCounter}`); + this.log.debug(`NEW_L2_TO_L1_MSG cnt: ${this.sideEffectCounter}`); this.incrementSideEffectCounter(); } @@ -238,18 +213,21 @@ export class PublicSideEffectTrace implements PublicSideEffectTraceInterface { // This length is for charging DA and is checked on-chain - has to be length of log preimage + 4 bytes. // The .length call also has a +4 but that is unrelated this.unencryptedLogsHashes.push(new LogHash(basicLogHash, this.sideEffectCounter, new Fr(ulog.length + 4))); - this.logger.debug(`NEW_UNENCRYPTED_LOG cnt: ${this.sideEffectCounter}`); + this.log.debug(`NEW_UNENCRYPTED_LOG cnt: ${this.sideEffectCounter}`); this.incrementSideEffectCounter(); } - public traceGetContractInstance(instance: TracedContractInstance) { + public traceGetContractInstance( + contractAddress: Fr, + exists: boolean, + instance: SerializableContractInstance = SerializableContractInstance.default(), + ) { this.enforceLimitOnNullifierChecks('(contract address nullifier from GETCONTRACTINSTANCE)'); - // TODO(dbanks12): should emit a nullifier read request this.avmCircuitHints.contractInstances.items.push( new AvmContractInstanceHint( - instance.address, - instance.exists, + contractAddress, + exists, instance.salt, instance.deployer, instance.contractClassId, @@ -257,10 +235,44 @@ export class PublicSideEffectTrace implements PublicSideEffectTraceInterface { instance.publicKeys, ), ); - this.logger.debug(`CONTRACT_INSTANCE cnt: ${this.sideEffectCounter}`); + this.log.debug(`CONTRACT_INSTANCE cnt: ${this.sideEffectCounter}`); this.incrementSideEffectCounter(); } + // This tracing function gets called everytime we start simulation/execution. + // This happens both when starting a new top-level trace and the start of every nested trace + // We use this to collect the AvmContractBytecodeHints + public traceGetBytecode( + contractAddress: Fr, + exists: boolean, + bytecode: Buffer = Buffer.alloc(0), + contractInstance: SerializableContractInstance = SerializableContractInstance.default(), + contractClass: ContractClassIdPreimage = { + artifactHash: Fr.zero(), + privateFunctionsRoot: Fr.zero(), + publicBytecodeCommitment: Fr.zero(), + }, + ) { + const instance = new AvmContractInstanceHint( + contractAddress, + exists, + contractInstance.salt, + contractInstance.deployer, + contractInstance.contractClassId, + contractInstance.initializationHash, + contractInstance.publicKeys, + ); + // We need to deduplicate the contract instances based on addresses + this.avmCircuitHints.contractBytecodeHints.items.push( + new AvmContractBytecodeHints(bytecode, instance, contractClass), + ); + this.log.debug( + `Bytecode retrieval for contract execution traced: exists=${exists}, instance=${JSON.stringify( + contractInstance, + )}`, + ); + } + /** * Trace a nested call. * Accept some results from a finished nested call's trace into this one. diff --git a/yarn-project/simulator/src/public/side_effect_trace_interface.ts b/yarn-project/simulator/src/public/side_effect_trace_interface.ts index 7a5c44bd5fe..09e21b4f3a3 100644 --- a/yarn-project/simulator/src/public/side_effect_trace_interface.ts +++ b/yarn-project/simulator/src/public/side_effect_trace_interface.ts @@ -1,19 +1,13 @@ -import { type ContractClassIdPreimage, type Gas } from '@aztec/circuits.js'; +import { type ContractClassIdPreimage, type Gas, type SerializableContractInstance } from '@aztec/circuits.js'; import { type Fr } from '@aztec/foundation/fields'; import { type AvmContractCallResult } from '../avm/avm_contract_call_result.js'; import { type AvmExecutionEnvironment } from '../avm/avm_execution_environment.js'; -import { type TracedContractInstance } from './side_effect_trace.js'; export interface PublicSideEffectTraceInterface { fork(): PublicSideEffectTraceInterface; getCounter(): number; // all "trace*" functions can throw SideEffectLimitReachedError - traceGetBytecode( - bytecode: Buffer, - contractInstance: TracedContractInstance, - contractClass: ContractClassIdPreimage, - ): void; tracePublicStorageRead(contractAddress: Fr, slot: Fr, value: Fr, exists: boolean, cached: boolean): void; tracePublicStorageWrite(contractAddress: Fr, slot: Fr, value: Fr): void; traceNoteHashCheck(contractAddress: Fr, noteHash: Fr, leafIndex: Fr, exists: boolean): void; @@ -23,8 +17,14 @@ export interface PublicSideEffectTraceInterface { traceL1ToL2MessageCheck(contractAddress: Fr, msgHash: Fr, msgLeafIndex: Fr, exists: boolean): void; traceNewL2ToL1Message(contractAddress: Fr, recipient: Fr, content: Fr): void; traceUnencryptedLog(contractAddress: Fr, log: Fr[]): void; - // TODO(dbanks12): odd that getContractInstance is a one-off in that it accepts an entire object instead of components - traceGetContractInstance(instance: TracedContractInstance): void; + traceGetContractInstance(contractAddress: Fr, exists: boolean, instance?: SerializableContractInstance): void; + traceGetBytecode( + contractAddress: Fr, + exists: boolean, + bytecode?: Buffer, + contractInstance?: SerializableContractInstance, + contractClass?: ContractClassIdPreimage, + ): void; traceNestedCall( /** The trace of the nested call. */ nestedCallTrace: PublicSideEffectTraceInterface, diff --git a/yarn-project/txe/src/txe_service/txe_service.ts b/yarn-project/txe/src/txe_service/txe_service.ts index 3469c9ad6aa..f0f41fa232c 100644 --- a/yarn-project/txe/src/txe_service/txe_service.ts +++ b/yarn-project/txe/src/txe_service/txe_service.ts @@ -624,18 +624,30 @@ export class TXEService { return toForeignCallResult([]); } - async avmOpcodeGetContractInstance(address: ForeignCallSingle) { + async avmOpcodeGetContractInstanceDeployer(address: ForeignCallSingle) { const instance = await this.typedOracle.getContractInstance(fromSingle(address)); return toForeignCallResult([ - toArray([ - // AVM requires an extra boolean indicating the instance was found - new Fr(1), - instance.salt, - instance.deployer, - instance.contractClassId, - instance.initializationHash, - ...instance.publicKeys.toFields(), - ]), + toSingle(instance.deployer), + // AVM requires an extra boolean indicating the instance was found + toSingle(new Fr(1)), + ]); + } + + async avmOpcodeGetContractInstanceClassId(address: ForeignCallSingle) { + const instance = await this.typedOracle.getContractInstance(fromSingle(address)); + return toForeignCallResult([ + toSingle(instance.contractClassId), + // AVM requires an extra boolean indicating the instance was found + toSingle(new Fr(1)), + ]); + } + + async avmOpcodeGetContractInstanceInitializationHash(address: ForeignCallSingle) { + const instance = await this.typedOracle.getContractInstance(fromSingle(address)); + return toForeignCallResult([ + toSingle(instance.initializationHash), + // AVM requires an extra boolean indicating the instance was found + toSingle(new Fr(1)), ]); }