From e8a754e29ff4dd334d9c69657cfe8e81d8cd6a8b Mon Sep 17 00:00:00 2001 From: IlyasRidhuan Date: Tue, 4 Jun 2024 09:29:38 +0000 Subject: [PATCH] feat(avm): ecc --- avm-transpiler/src/transpile.rs | 6 +- .../vm/avm_trace/avm_deserialization.cpp | 14 + .../vm/avm_trace/avm_execution.cpp | 15 + .../vm/avm_trace/avm_gas_trace.hpp | 3 +- .../barretenberg/vm/avm_trace/avm_opcode.hpp | 1 + .../barretenberg/vm/avm_trace/avm_trace.cpp | 112 +++ .../barretenberg/vm/avm_trace/avm_trace.hpp | 12 + .../vm/avm_trace/gadgets/avm_ecc.cpp | 35 + .../vm/avm_trace/gadgets/avm_ecc.hpp | 30 + .../vm/tests/avm_execution.test.cpp | 71 ++ .../contracts/avm_test_contract/src/main.nr | 5 +- .../bb-prover/src/avm_proving.test.ts | 2 +- .../simulator/src/avm/avm_memory_types.ts | 844 +++++++++--------- .../simulator/src/avm/opcodes/ec_add.test.ts | 126 +-- .../simulator/src/avm/opcodes/ec_add.ts | 142 +-- 15 files changed, 861 insertions(+), 557 deletions(-) create mode 100644 barretenberg/cpp/src/barretenberg/vm/avm_trace/gadgets/avm_ecc.cpp create mode 100644 barretenberg/cpp/src/barretenberg/vm/avm_trace/gadgets/avm_ecc.hpp diff --git a/avm-transpiler/src/transpile.rs b/avm-transpiler/src/transpile.rs index 5945bd366b42..14886fc8818b 100644 --- a/avm-transpiler/src/transpile.rs +++ b/avm-transpiler/src/transpile.rs @@ -844,14 +844,14 @@ fn handle_black_box_function(avm_instrs: &mut Vec, operation: &B result, } => avm_instrs.push(AvmInstruction { opcode: AvmOpcode::ECADD, - indirect: Some(ALL_DIRECT), + indirect: Some(0b1000000), operands: vec![ AvmOperand::U32 { value: input1_x.0 as u32 }, AvmOperand::U32 { value: input1_y.0 as u32 }, - AvmOperand::U8 { value: input1_infinite.0 as u8 }, + AvmOperand::U32 { value: input1_infinite.0 as u32 }, AvmOperand::U32 { value: input2_x.0 as u32 }, AvmOperand::U32 { value: input2_y.0 as u32 }, - AvmOperand::U8 { value: input2_infinite.0 as u8 }, + AvmOperand::U32 { value: input2_infinite.0 as u32 }, AvmOperand::U32 { value: result.pointer.0 as u32 }, ], ..Default::default() diff --git a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_deserialization.cpp b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_deserialization.cpp index 1a6554fb8ac9..e4531ec5cced 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_deserialization.cpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_deserialization.cpp @@ -133,6 +133,7 @@ const std::unordered_map> OPCODE_WIRE_FORMAT = // DELEGATECALL, -- not in simulator { OpCode::RETURN, { OperandType::INDIRECT, OperandType::UINT32, OperandType::UINT32 } }, // REVERT, + { OpCode::REVERT, { OperandType::INDIRECT, OperandType::UINT32, OperandType::UINT32 } }, // Misc { OpCode::DEBUGLOG, { OperandType::INDIRECT, OperandType::UINT32, OperandType::UINT32, OperandType::UINT32, OperandType::UINT32 } }, @@ -147,6 +148,16 @@ const std::unordered_map> OPCODE_WIRE_FORMAT = { OpCode::SHA256, { OperandType::INDIRECT, OperandType::UINT32, OperandType::UINT32, OperandType::UINT32 } }, { OpCode::PEDERSEN, { OperandType::INDIRECT, OperandType::UINT32, OperandType::UINT32, OperandType::UINT32, OperandType::UINT32 } }, + // TEMP ECADD without relative memory + { OpCode::ECADD, + { OperandType::INDIRECT, + OperandType::UINT32, // lhs.x + OperandType::UINT32, // lhs.y + OperandType::UINT32, // lhs.is_infinite + OperandType::UINT32, // rhs.x + OperandType::UINT32, // rhs.y + OperandType::UINT32, // rhs.is_infinite + OperandType::UINT32 } }, // res_offset // Gadget - Conversion { OpCode::TORADIXLE, { OperandType::INDIRECT, OperandType::UINT32, OperandType::UINT32, OperandType::UINT32, OperandType::UINT32 } }, @@ -257,6 +268,9 @@ std::vector Deserialization::parse(std::vector const& byte case OperandType::TAG: { uint8_t tag_u8 = bytecode.at(pos); if (tag_u8 == static_cast(AvmMemoryTag::U0) || tag_u8 > MAX_MEM_TAG) { + info("tag_u8: ", static_cast(tag_u8)); + info("pos: ", pos); + info("opcode: ", static_cast(opcode)); throw_or_abort("Instruction tag is invalid at position " + std::to_string(pos) + " value: " + std::to_string(tag_u8) + " for opcode: " + to_hex(opcode)); } diff --git a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_execution.cpp b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_execution.cpp index 12a4d1b611ab..9ece56050ab3 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_execution.cpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_execution.cpp @@ -635,6 +635,21 @@ std::vector Execution::gen_trace(std::vector const& instructio std::get(inst.operands.at(3)), std::get(inst.operands.at(4))); break; + case OpCode::ECADD: + trace_builder.op_embedded_ec_add(std::get(inst.operands.at(0)), + std::get(inst.operands.at(1)), + std::get(inst.operands.at(2)), + std::get(inst.operands.at(3)), + std::get(inst.operands.at(4)), + std::get(inst.operands.at(5)), + std::get(inst.operands.at(6)), + std::get(inst.operands.at(7))); + break; + case OpCode::REVERT: + trace_builder.op_revert(std::get(inst.operands.at(0)), + std::get(inst.operands.at(1)), + std::get(inst.operands.at(2))); + break; default: throw_or_abort("Don't know how to execute opcode " + to_hex(inst.op_code) + " at pc " + std::to_string(pc) + "."); diff --git a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_gas_trace.hpp b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_gas_trace.hpp index 7d182e9935f1..cd544ec1d742 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_gas_trace.hpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_gas_trace.hpp @@ -98,6 +98,7 @@ static const inline std::unordered_map GAS_COST_TABLE = { { OpCode::POSEIDON2, temp_default_gas_entry }, { OpCode::SHA256, temp_default_gas_entry }, { OpCode::PEDERSEN, temp_default_gas_entry }, + { OpCode::ECADD, temp_default_gas_entry }, // Conversions { OpCode::TORADIXLE, temp_default_gas_entry }, @@ -146,4 +147,4 @@ class AvmGasTraceBuilder { uint32_t remaining_da_gas = 0; }; -} // namespace bb::avm_trace \ No newline at end of file +} // namespace bb::avm_trace diff --git a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_opcode.hpp b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_opcode.hpp index 87e0f03fe7b2..83de32e65682 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_opcode.hpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_opcode.hpp @@ -104,6 +104,7 @@ enum class OpCode : uint8_t { POSEIDON2, SHA256, PEDERSEN, + ECADD, // Conversions TORADIXLE, // Future Gadgets -- pending changes in noir diff --git a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_trace.cpp b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_trace.cpp index fe78c6bf03ab..2c0d211def62 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_trace.cpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_trace.cpp @@ -1843,6 +1843,10 @@ void AvmTraceBuilder::calldata_copy( pc++; } +std::vector AvmTraceBuilder::op_revert(uint8_t indirect, uint32_t ret_offset, uint32_t ret_size) +{ + return return_op(indirect, ret_offset, ret_size); +} /** * @brief RETURN opcode with direct and indirect memory access, i.e., * direct: return(M[ret_offset:ret_offset+ret_size]) @@ -3289,6 +3293,114 @@ void AvmTraceBuilder::op_pedersen_hash(uint8_t indirect, write_slice_to_memory( call_ptr, clk, output_offset, AvmMemoryTag::FF, AvmMemoryTag::FF, FF(internal_return_ptr), { output }); } + +void AvmTraceBuilder::op_embedded_ec_add(uint8_t indirect, + uint32_t lhs_x_offset, + uint32_t lhs_y_offset, + uint32_t lhs_is_inf_offset, + uint32_t rhs_x_offset, + uint32_t rhs_y_offset, + uint32_t rhs_is_inf_offset, + uint32_t output_offset) +{ + // output_offset could be indirect + auto clk = static_cast(main_trace.size()) + 1; + // Load lhs point + auto lhs_x_read = mem_trace_builder.read_and_load_from_memory( + call_ptr, clk, IntermRegister::IA, lhs_x_offset, AvmMemoryTag::FF, AvmMemoryTag::U0); + auto lhs_y_read = mem_trace_builder.read_and_load_from_memory( + call_ptr, clk, IntermRegister::IB, lhs_y_offset, AvmMemoryTag::FF, AvmMemoryTag::U0); + // Load rhs point + auto rhs_x_read = mem_trace_builder.read_and_load_from_memory( + call_ptr, clk, IntermRegister::IC, rhs_x_offset, AvmMemoryTag::FF, AvmMemoryTag::U0); + auto rhs_y_read = mem_trace_builder.read_and_load_from_memory( + call_ptr, clk, IntermRegister::ID, rhs_y_offset, AvmMemoryTag::FF, AvmMemoryTag::U0); + + // Save this clk time to line up with the gadget op. + auto ecc_clk = clk; + main_trace.push_back(Row{ + .avm_main_clk = clk, + .avm_main_ia = lhs_x_read.val, + .avm_main_ib = lhs_y_read.val, + .avm_main_ic = rhs_x_read.val, + .avm_main_id = rhs_y_read.val, + .avm_main_internal_return_ptr = FF(internal_return_ptr), + .avm_main_mem_idx_a = FF(lhs_x_offset), + .avm_main_mem_idx_b = FF(lhs_y_offset), + .avm_main_mem_idx_c = FF(rhs_x_offset), + .avm_main_mem_idx_d = FF(rhs_y_offset), + .avm_main_mem_op_a = FF(1), + .avm_main_mem_op_b = FF(1), + .avm_main_mem_op_c = FF(1), + .avm_main_mem_op_d = FF(1), + .avm_main_pc = FF(pc++), + .avm_main_r_in_tag = FF(static_cast(AvmMemoryTag::FF)), + }); + clk++; + // Load the infinite bools separately since they have a different memory tag + auto lhs_is_inf_read = mem_trace_builder.read_and_load_from_memory( + call_ptr, clk, IntermRegister::IA, lhs_is_inf_offset, AvmMemoryTag::U8, AvmMemoryTag::U0); + auto rhs_is_inf_read = mem_trace_builder.read_and_load_from_memory( + call_ptr, clk, IntermRegister::IB, rhs_is_inf_offset, AvmMemoryTag::U8, AvmMemoryTag::U0); + + main_trace.push_back(Row{ + .avm_main_clk = clk, + .avm_main_ia = lhs_is_inf_read.val, + .avm_main_ib = rhs_is_inf_read.val, + .avm_main_internal_return_ptr = FF(internal_return_ptr), + .avm_main_mem_idx_a = FF(lhs_is_inf_offset), + .avm_main_mem_idx_b = FF(rhs_is_inf_offset), + .avm_main_mem_op_a = FF(1), + .avm_main_mem_op_b = FF(1), + .avm_main_pc = FF(pc), + .avm_main_r_in_tag = FF(static_cast(AvmMemoryTag::U8)), + }); + clk++; + grumpkin::g1::affine_element lhs = uint8_t(lhs_is_inf_read.val) == 1 + ? grumpkin::g1::affine_element::infinity() + : grumpkin::g1::affine_element{ lhs_x_read.val, lhs_y_read.val }; + grumpkin::g1::affine_element rhs = uint8_t(rhs_is_inf_read.val) == 1 + ? grumpkin::g1::affine_element::infinity() + : grumpkin::g1::affine_element{ rhs_x_read.val, rhs_y_read.val }; + auto result = ecc_trace_builder.embedded_curve_add(lhs, rhs, ecc_clk); + // Write across two lines since we have different mem_tags + uint32_t direct_output_offset = output_offset; + bool indirect_flag_output = is_operand_indirect(indirect, 6); + if (indirect_flag_output) { + auto read_ind_output = + mem_trace_builder.indirect_read_and_load_from_memory(call_ptr, clk, IndirectRegister::IND_A, output_offset); + direct_output_offset = uint32_t(read_ind_output.val); + } + auto read_output = mem_trace_builder.read_and_load_from_memory( + call_ptr, clk, IntermRegister::IA, direct_output_offset, AvmMemoryTag::U32, AvmMemoryTag::U0); + main_trace.push_back(Row{ + .avm_main_clk = clk, + .avm_main_ia = read_output.val, + .avm_main_ind_a = indirect_flag_output ? FF(output_offset) : FF(0), + .avm_main_ind_op_a = FF(static_cast(indirect_flag_output)), + .avm_main_internal_return_ptr = FF(internal_return_ptr), + .avm_main_mem_idx_a = FF(direct_output_offset), + .avm_main_mem_op_a = FF(1), + .avm_main_pc = FF(pc), + .avm_main_r_in_tag = FF(static_cast(AvmMemoryTag::U32)), + }); + clk++; + write_slice_to_memory(call_ptr, + clk, + direct_output_offset, + AvmMemoryTag::FF, + AvmMemoryTag::FF, + FF(internal_return_ptr), + { result.x, result.y }); + clk++; + write_slice_to_memory(call_ptr, + clk, + direct_output_offset + 2, + AvmMemoryTag::U8, + AvmMemoryTag::U8, + FF(internal_return_ptr), + { result.is_point_at_infinity() }); +} // Finalise Lookup Counts // // For log derivative lookups, we require a column that contains the number of times each lookup is consumed diff --git a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_trace.hpp b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_trace.hpp index 92233be8358d..0f82e6c125a8 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_trace.hpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_trace.hpp @@ -11,6 +11,7 @@ #include "barretenberg/vm/avm_trace/avm_opcode.hpp" #include "barretenberg/vm/avm_trace/constants.hpp" #include "barretenberg/vm/avm_trace/gadgets/avm_conversion_trace.hpp" +#include "barretenberg/vm/avm_trace/gadgets/avm_ecc.hpp" #include "barretenberg/vm/avm_trace/gadgets/avm_keccak.hpp" #include "barretenberg/vm/avm_trace/gadgets/avm_pedersen.hpp" #include "barretenberg/vm/avm_trace/gadgets/avm_poseidon2.hpp" @@ -153,6 +154,7 @@ class AvmTraceBuilder { uint32_t dst_offset, std::vector const& call_data_mem); + std::vector op_revert(uint8_t indirect, uint32_t ret_offset, uint32_t ret_size); // RETURN opcode with direct and indirect memory access, i.e., // direct: return(M[ret_offset:ret_offset+ret_size]) // indirect: return(M[M[ret_offset]:M[ret_offset]+ret_size]) @@ -190,6 +192,15 @@ class AvmTraceBuilder { uint32_t output_offset, uint32_t input_offset, uint32_t input_size_offset); + // Embedded EC Add - the offsets are temporary + void op_embedded_ec_add(uint8_t indirect, + uint32_t lhs_x_offset, + uint32_t lhs_y_offset, + uint32_t lhs_is_inf_offset, + uint32_t rhs_x_offset, + uint32_t rhs_y_offset, + uint32_t rhs_is_inf_offset, + uint32_t output_offset); private: // Used for the standard indirect address resolution of three operands opcode. @@ -215,6 +226,7 @@ class AvmTraceBuilder { AvmPoseidon2TraceBuilder poseidon2_trace_builder; AvmKeccakTraceBuilder keccak_trace_builder; AvmPedersenTraceBuilder pedersen_trace_builder; + AvmEccTraceBuilder ecc_trace_builder; /** * @brief Create a kernel lookup opcode object diff --git a/barretenberg/cpp/src/barretenberg/vm/avm_trace/gadgets/avm_ecc.cpp b/barretenberg/cpp/src/barretenberg/vm/avm_trace/gadgets/avm_ecc.cpp new file mode 100644 index 000000000000..eda2444d61c7 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/vm/avm_trace/gadgets/avm_ecc.cpp @@ -0,0 +1,35 @@ + +#include "barretenberg/vm/avm_trace/gadgets/avm_ecc.hpp" +#include "barretenberg/vm/avm_trace/avm_common.hpp" + +namespace bb::avm_trace { + +AvmEccTraceBuilder::AvmEccTraceBuilder() +{ + ecc_trace.reserve(AVM_TRACE_SIZE); +} + +std::vector AvmEccTraceBuilder::finalize() +{ + return std::move(ecc_trace); +} + +void AvmEccTraceBuilder::reset() +{ + ecc_trace.clear(); +} + +grumpkin::g1::affine_element AvmEccTraceBuilder::embedded_curve_add(grumpkin::g1::affine_element lhs, + grumpkin::g1::affine_element rhs, + uint32_t clk) +{ + grumpkin::g1::affine_element result = lhs + rhs; + std::tuple p1 = { lhs.x, lhs.y, lhs.is_point_at_infinity() }; + std::tuple p2 = { rhs.x, rhs.y, rhs.is_point_at_infinity() }; + std::tuple result_tuple = { result.x, result.y, result.is_point_at_infinity() }; + ecc_trace.push_back({ clk, p1, p2, result_tuple }); + + return result; +} + +} // namespace bb::avm_trace diff --git a/barretenberg/cpp/src/barretenberg/vm/avm_trace/gadgets/avm_ecc.hpp b/barretenberg/cpp/src/barretenberg/vm/avm_trace/gadgets/avm_ecc.hpp new file mode 100644 index 000000000000..dafde997cfe3 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/vm/avm_trace/gadgets/avm_ecc.hpp @@ -0,0 +1,30 @@ + +#pragma once + +#include "barretenberg/ecc/curves/grumpkin/grumpkin.hpp" +#include "barretenberg/ecc/groups/affine_element.hpp" +#include "barretenberg/vm/avm_trace/avm_common.hpp" + +namespace bb::avm_trace { +class AvmEccTraceBuilder { + public: + struct EccTraceEntry { + uint32_t clk = 0; + std::tuple p1; // x, y, is_infinity + std::tuple p2; + std::tuple result; + }; + + AvmEccTraceBuilder(); + void reset(); + // Finalize the trace + std::vector finalize(); + grumpkin::g1::affine_element embedded_curve_add(grumpkin::g1::affine_element lhs, + grumpkin::g1::affine_element rhs, + uint32_t clk); + + private: + std::vector ecc_trace; +}; + +} // namespace bb::avm_trace diff --git a/barretenberg/cpp/src/barretenberg/vm/tests/avm_execution.test.cpp b/barretenberg/cpp/src/barretenberg/vm/tests/avm_execution.test.cpp index 56ab35364a23..6bf398232e5b 100644 --- a/barretenberg/cpp/src/barretenberg/vm/tests/avm_execution.test.cpp +++ b/barretenberg/cpp/src/barretenberg/vm/tests/avm_execution.test.cpp @@ -1331,6 +1331,77 @@ TEST_F(AvmExecutionTests, pedersenHashOpCode) validate_trace(std::move(trace)); } +// +// Positive test with EmbeddedCurveAdd +TEST_F(AvmExecutionTests, embeddedCurveAddOpCode) +{ + + // TODO: Look for hardcoded test vectors since bb is missing them + grumpkin::g1::affine_element a = grumpkin::g1::affine_element::random_element(); + auto a_is_inf = a.is_point_at_infinity(); + grumpkin::g1::affine_element b = grumpkin::g1::affine_element::random_element(); + auto b_is_inf = b.is_point_at_infinity(); + grumpkin::g1::affine_element res = a + b; + auto expected_output = std::vector{ res.x, res.y, res.is_point_at_infinity() }; + std::string bytecode_hex = to_hex(OpCode::CALLDATACOPY) + // Calldatacopy + "00" // Indirect flag + "00000000" // cd_offset + "00000002" // copy_size + "00000000" // dst_offset + + to_hex(OpCode::SET) + // opcode SET for direct src_length + "00" // Indirect flag + "01" // U8 + + to_hex(a_is_inf) + // + "00000002" // dst_offset + + to_hex(OpCode::CALLDATACOPY) + // calldatacopy + "00" // Indirect flag + "00000002" // cd_offset + "00000002" // copy_size + "00000003" // dst_offset + + to_hex(OpCode::SET) + // opcode SET for direct src_length + "00" // Indirect flag + "01" // U32 + + to_hex(b_is_inf) + // value 2 + "00000005" // dst_offset + + to_hex(OpCode::SET) + // opcode SET for direct src_length + "00" // Indirect flag + "03" // U32 + "00000007" // value + "00000006" // dst_offset + + to_hex(OpCode::ECADD) + // opcode ECADD + "40" // Indirect flag ( sixth operand indirect) + "00000000" // hash_index offset (direct) + "00000001" // dest offset (direct) + "00000002" // input offset (indirect) + "00000003" // length offset (direct) + "00000004" // length offset (direct) + "00000005" // length offset (direct) + "00000006" // length offset (direct) + + to_hex(OpCode::RETURN) + // opcode RETURN + "00" // Indirect flag + "00000007" // ret offset 3 + "00000003"; // ret size 1 + + auto bytecode = hex_to_bytes(bytecode_hex); + auto instructions = Deserialization::parse(bytecode); + + // Assign a vector that we will mutate internally in gen_trace to store the return values; + std::vector returndata = std::vector(); + std::vector calldata = { a.x, a.y, b.x, b.y }; + auto trace = Execution::gen_trace(instructions, returndata, calldata, public_inputs_vec); + + // Find the first row enabling the pedersen selector + // auto row = std::ranges::find_if(trace.begin(), trace.end(), [](Row r) { return r.avm_main_sel_op_pedersen == 1; + // }); EXPECT_EQ(row->avm_main_ind_a, 4); // Register A is indirect EXPECT_EQ(row->avm_main_mem_idx_a, 0); // + // Indirect(4) -> 1 EXPECT_EQ(row->avm_main_ia, 1); // The first input + // // The second row loads the U32 values + // std::advance(row, 1); + // EXPECT_EQ(row->avm_main_ia, 2); // Input length is 2 + // EXPECT_EQ(row->avm_main_ib, 5); // Hash offset is 5 + EXPECT_EQ(returndata, expected_output); + + validate_trace(std::move(trace)); +} // Positive test for Kernel Input opcodes TEST_F(AvmExecutionTests, kernelInputOpcodes) 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 0e131e612e37..526bab60530e 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 @@ -137,10 +137,7 @@ contract AvmTest { #[aztec(public)] fn elliptic_curve_add_and_double() -> EmbeddedCurvePoint { let g = EmbeddedCurvePoint { x: 1, y: 17631683881184975370165255887551781615748388533673675138860, is_infinite: false }; - - let doubled = g + g; - let added = g + doubled; - added + g + g } /************************************************************************ diff --git a/yarn-project/bb-prover/src/avm_proving.test.ts b/yarn-project/bb-prover/src/avm_proving.test.ts index 40ebc18d6e94..e142ce4bbe7a 100644 --- a/yarn-project/bb-prover/src/avm_proving.test.ts +++ b/yarn-project/bb-prover/src/avm_proving.test.ts @@ -118,7 +118,7 @@ describe('AVM WitGen, proof generation and verification', () => { TIMEOUT, ); - // TODO: requires revert + // TODO: requires waiting for memory to change to u32 // it("Should prove to radix", // async () => { // await proveAndVerifyAvmTestContract('to_radix_le', [new Fr(10)]); diff --git a/yarn-project/simulator/src/avm/avm_memory_types.ts b/yarn-project/simulator/src/avm/avm_memory_types.ts index 9509b0297bb9..212715c41cf8 100644 --- a/yarn-project/simulator/src/avm/avm_memory_types.ts +++ b/yarn-project/simulator/src/avm/avm_memory_types.ts @@ -10,46 +10,46 @@ import { Addressing, AddressingMode } from './opcodes/addressing_mode.js'; /** MemoryValue gathers the common operations for all memory types. */ export abstract class MemoryValue { - public abstract add(rhs: MemoryValue): MemoryValue; - public abstract sub(rhs: MemoryValue): MemoryValue; - public abstract mul(rhs: MemoryValue): MemoryValue; - public abstract div(rhs: MemoryValue): MemoryValue; + public abstract add(rhs: MemoryValue): MemoryValue; + public abstract sub(rhs: MemoryValue): MemoryValue; + public abstract mul(rhs: MemoryValue): MemoryValue; + public abstract div(rhs: MemoryValue): MemoryValue; - public abstract equals(rhs: MemoryValue): boolean; - public abstract lt(rhs: MemoryValue): boolean; + public abstract equals(rhs: MemoryValue): boolean; + public abstract lt(rhs: MemoryValue): boolean; - // We need this to be able to build an instance of the subclasses. - public abstract build(n: bigint): MemoryValue; + // We need this to be able to build an instance of the subclasses. + public abstract build(n: bigint): MemoryValue; - // Use sparingly. - public abstract toBigInt(): bigint; + // Use sparingly. + public abstract toBigInt(): bigint; - // To Buffer - public abstract toBuffer(): Buffer; + // To Buffer + public abstract toBuffer(): Buffer; - // To field - public toFr(): Fr { - return new Fr(this.toBigInt()); - } + // To field + public toFr(): Fr { + return new Fr(this.toBigInt()); + } - // To number. Throws if exceeds max safe int. - public toNumber(): number { - return this.toFr().toNumber(); - } + // To number. Throws if exceeds max safe int. + public toNumber(): number { + return this.toFr().toNumber(); + } - public toString(): string { - return `${this.constructor.name}(0x${this.toBigInt().toString(16)})`; - } + public toString(): string { + return `${this.constructor.name}(0x${this.toBigInt().toString(16)})`; + } } /** IntegralValue gathers the common operations for all integral memory types. */ export abstract class IntegralValue extends MemoryValue { - public abstract shl(rhs: IntegralValue): IntegralValue; - public abstract shr(rhs: IntegralValue): IntegralValue; - public abstract and(rhs: IntegralValue): IntegralValue; - public abstract or(rhs: IntegralValue): IntegralValue; - public abstract xor(rhs: IntegralValue): IntegralValue; - public abstract not(): IntegralValue; + public abstract shl(rhs: IntegralValue): IntegralValue; + public abstract shr(rhs: IntegralValue): IntegralValue; + public abstract and(rhs: IntegralValue): IntegralValue; + public abstract or(rhs: IntegralValue): IntegralValue; + public abstract xor(rhs: IntegralValue): IntegralValue; + public abstract not(): IntegralValue; } /** @@ -57,442 +57,442 @@ export abstract class IntegralValue extends MemoryValue { * In TypeScript terms, it's a class mixin. **/ function UnsignedIntegerClassFactory(bits: number) { - return class NewUintClass extends IntegralValue { - static readonly mod: bigint = 1n << BigInt(bits); - static readonly bitmask: bigint = this.mod - 1n; - public readonly n: bigint; // Cannot be private due to TS limitations. + return class NewUintClass extends IntegralValue { + static readonly mod: bigint = 1n << BigInt(bits); + static readonly bitmask: bigint = this.mod - 1n; + public readonly n: bigint; // Cannot be private due to TS limitations. + + public constructor(n: bigint | number) { + super(); + this.n = BigInt(n); + assert(n < NewUintClass.mod, `Value ${n} is too large for ${this.constructor.name}.`); + } + + public build(n: bigint): NewUintClass { + return new this.constructor.prototype.constructor(n); + } + + public add(rhs: NewUintClass): NewUintClass { + return this.build((this.n + rhs.n) & NewUintClass.bitmask); + } + + public sub(rhs: NewUintClass): NewUintClass { + const res: bigint = this.n - rhs.n; + return this.build(res >= 0 ? res : res + NewUintClass.mod); + } + + public mul(rhs: NewUintClass): NewUintClass { + return this.build((this.n * rhs.n) & NewUintClass.bitmask); + } + + public div(rhs: NewUintClass): NewUintClass { + return this.build(this.n / rhs.n); + } + + // No sign extension. + public shr(rhs: NewUintClass): NewUintClass { + // Note that this.n is > 0 by class invariant. + return this.build(this.n >> rhs.n); + } + + public shl(rhs: NewUintClass): NewUintClass { + return this.build((this.n << rhs.n) & NewUintClass.bitmask); + } + + public and(rhs: NewUintClass): NewUintClass { + return this.build(this.n & rhs.n); + } + + public or(rhs: NewUintClass): NewUintClass { + return this.build(this.n | rhs.n); + } + + public xor(rhs: NewUintClass): NewUintClass { + return this.build(this.n ^ rhs.n); + } + + public not(): NewUintClass { + return this.build(~this.n & NewUintClass.bitmask); + } + + public equals(rhs: NewUintClass): boolean { + return this.n === rhs.n; + } + + public lt(rhs: NewUintClass): boolean { + return this.n < rhs.n; + } + + public toBigInt(): bigint { + return this.n; + } + + public toBuffer(): Buffer { + return toBufferBE(this.n, bits / 8); + } + }; +} + +// Now we can create the classes for each unsigned integer type. +// We extend instead of just assigning so that the class has the right name. +// Otherwise they are all called "NewUintClass". +export class Uint8 extends UnsignedIntegerClassFactory(8) { } +export class Uint16 extends UnsignedIntegerClassFactory(16) { } +export class Uint32 extends UnsignedIntegerClassFactory(32) { } +export class Uint64 extends UnsignedIntegerClassFactory(64) { } +export class Uint128 extends UnsignedIntegerClassFactory(128) { } + +export class Field extends MemoryValue { + public static readonly MODULUS: bigint = Fr.MODULUS; + private readonly rep: Fr; - public constructor(n: bigint | number) { - super(); - this.n = BigInt(n); - assert(n < NewUintClass.mod, `Value ${n} is too large for ${this.constructor.name}.`); + constructor(v: number | bigint | Fr | Buffer) { + super(); + this.rep = new Fr(v); } - public build(n: bigint): NewUintClass { - return new this.constructor.prototype.constructor(n); + public build(n: bigint): Field { + return new Field(n); } - public add(rhs: NewUintClass): NewUintClass { - return this.build((this.n + rhs.n) & NewUintClass.bitmask); + public add(rhs: Field): Field { + return new Field(this.rep.add(rhs.rep)); } - public sub(rhs: NewUintClass): NewUintClass { - const res: bigint = this.n - rhs.n; - return this.build(res >= 0 ? res : res + NewUintClass.mod); + public sub(rhs: Field): Field { + return new Field(this.rep.sub(rhs.rep)); } - public mul(rhs: NewUintClass): NewUintClass { - return this.build((this.n * rhs.n) & NewUintClass.bitmask); + public mul(rhs: Field): Field { + return new Field(this.rep.mul(rhs.rep)); } - public div(rhs: NewUintClass): NewUintClass { - return this.build(this.n / rhs.n); + // Euclidean division. + public div(rhs: Field): Field { + return new Field(this.rep.ediv(rhs.rep)); } - // No sign extension. - public shr(rhs: NewUintClass): NewUintClass { - // Note that this.n is > 0 by class invariant. - return this.build(this.n >> rhs.n); + // Field division. + public fdiv(rhs: Field): Field { + return new Field(this.rep.div(rhs.rep)); } - public shl(rhs: NewUintClass): NewUintClass { - return this.build((this.n << rhs.n) & NewUintClass.bitmask); + public equals(rhs: Field): boolean { + return this.rep.equals(rhs.rep); } - public and(rhs: NewUintClass): NewUintClass { - return this.build(this.n & rhs.n); + public lt(rhs: Field): boolean { + return this.rep.lt(rhs.rep); } - public or(rhs: NewUintClass): NewUintClass { - return this.build(this.n | rhs.n); + public toBigInt(): bigint { + return this.rep.toBigInt(); + } + + public toBuffer(): Buffer { + return this.rep.toBuffer(); + } +} + +export enum TypeTag { + UNINITIALIZED, + UINT8, + UINT16, + UINT32, + UINT64, + UINT128, + FIELD, + INVALID, +} + +// Lazy interface definition for tagged memory +export type TaggedMemoryInterface = FunctionsOf; + +// TODO: Consider automatic conversion when getting undefined values. +export class TaggedMemory implements TaggedMemoryInterface { + static readonly log: DebugLogger = createDebugLogger('aztec:avm_simulator:memory'); + + // Whether to track and validate memory accesses for each instruction. + static readonly TRACK_MEMORY_ACCESSES = process.env.NODE_ENV === 'test'; + + // 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[]; + + constructor() { + // We do not initialize memory size here because otherwise tests blow up when diffing. + this._mem = []; } - public xor(rhs: NewUintClass): NewUintClass { - return this.build(this.n ^ rhs.n); + /** Returns a MeteredTaggedMemory instance to track the number of reads and writes if TRACK_MEMORY_ACCESSES is set. */ + public track(type: string = 'instruction'): TaggedMemoryInterface { + return TaggedMemory.TRACK_MEMORY_ACCESSES ? new MeteredTaggedMemory(this, type) : this; } - public not(): NewUintClass { - return this.build(~this.n & NewUintClass.bitmask); + public get(offset: number): MemoryValue { + assert(offset < TaggedMemory.MAX_MEMORY_SIZE); + const value = this.getAs(offset); + return value; } - public equals(rhs: NewUintClass): boolean { - return this.n === rhs.n; + public getAs(offset: number): T { + assert(offset < TaggedMemory.MAX_MEMORY_SIZE); + const word = this._mem[offset]; + TaggedMemory.log.debug(`get(${offset}) = ${word}`); + if (word === undefined) { + TaggedMemory.log.debug(`WARNING: Memory at offset ${offset} is undefined!`); + } + return word as T; } - public lt(rhs: NewUintClass): boolean { - return this.n < rhs.n; + public getSlice(offset: number, size: number): MemoryValue[] { + assert(offset < TaggedMemory.MAX_MEMORY_SIZE); + assert(offset + size < TaggedMemory.MAX_MEMORY_SIZE); + const value = this._mem.slice(offset, offset + size); + TaggedMemory.log.debug(`getSlice(${offset}, ${size}) = ${value}`); + assert(!value.some(e => e === undefined), 'Memory slice contains undefined values.'); + assert(value.length === size, `Expected slice of size ${size}, got ${value.length}.`); + return value; } - public toBigInt(): bigint { - return this.n; + public getSliceAs(offset: number, size: number): T[] { + assert(offset < TaggedMemory.MAX_MEMORY_SIZE); + assert(offset + size < TaggedMemory.MAX_MEMORY_SIZE); + return this.getSlice(offset, size) as T[]; } - public toBuffer(): Buffer { - return toBufferBE(this.n, bits / 8); + public getSliceTags(offset: number, size: number): TypeTag[] { + assert(offset < TaggedMemory.MAX_MEMORY_SIZE); + assert(offset + size < TaggedMemory.MAX_MEMORY_SIZE); + return this._mem.slice(offset, offset + size).map(TaggedMemory.getTag); } - }; -} -// Now we can create the classes for each unsigned integer type. -// We extend instead of just assigning so that the class has the right name. -// Otherwise they are all called "NewUintClass". -export class Uint8 extends UnsignedIntegerClassFactory(8) {} -export class Uint16 extends UnsignedIntegerClassFactory(16) {} -export class Uint32 extends UnsignedIntegerClassFactory(32) {} -export class Uint64 extends UnsignedIntegerClassFactory(64) {} -export class Uint128 extends UnsignedIntegerClassFactory(128) {} + public set(offset: number, v: MemoryValue) { + assert(offset < TaggedMemory.MAX_MEMORY_SIZE); + this._mem[offset] = v; + TaggedMemory.log.debug(`set(${offset}, ${v})`); + } -export class Field extends MemoryValue { - public static readonly MODULUS: bigint = Fr.MODULUS; - private readonly rep: Fr; - - constructor(v: number | bigint | Fr | Buffer) { - super(); - this.rep = new Fr(v); - } - - public build(n: bigint): Field { - return new Field(n); - } - - public add(rhs: Field): Field { - return new Field(this.rep.add(rhs.rep)); - } - - public sub(rhs: Field): Field { - return new Field(this.rep.sub(rhs.rep)); - } - - public mul(rhs: Field): Field { - return new Field(this.rep.mul(rhs.rep)); - } - - // Euclidean division. - public div(rhs: Field): Field { - return new Field(this.rep.ediv(rhs.rep)); - } - - // Field division. - public fdiv(rhs: Field): Field { - return new Field(this.rep.div(rhs.rep)); - } - - public equals(rhs: Field): boolean { - return this.rep.equals(rhs.rep); - } - - public lt(rhs: Field): boolean { - return this.rep.lt(rhs.rep); - } - - public toBigInt(): bigint { - return this.rep.toBigInt(); - } - - public toBuffer(): Buffer { - return this.rep.toBuffer(); - } -} + public setSlice(offset: number, vs: MemoryValue[]) { + assert(offset < TaggedMemory.MAX_MEMORY_SIZE); + assert(offset + vs.length < TaggedMemory.MAX_MEMORY_SIZE); + // We may need to extend the memory size, otherwise splice doesn't insert. + if (offset + vs.length > this._mem.length) { + this._mem.length = offset + vs.length; + } + this._mem.splice(offset, vs.length, ...vs); + TaggedMemory.log.debug(`setSlice(${offset}, ${vs})`); + } -export enum TypeTag { - UNINITIALIZED, - UINT8, - UINT16, - UINT32, - UINT64, - UINT128, - FIELD, - INVALID, -} + public getTag(offset: number): TypeTag { + return TaggedMemory.getTag(this._mem[offset]); + } -// Lazy interface definition for tagged memory -export type TaggedMemoryInterface = FunctionsOf; + /** + * Check that the memory at the given offset matches the specified tag. + */ + public checkTag(tag: TypeTag, offset: number) { + if (this.getTag(offset) !== tag) { + throw TagCheckError.forOffset(offset, TypeTag[this.getTag(offset)], TypeTag[tag]); + } + } -// TODO: Consider automatic conversion when getting undefined values. -export class TaggedMemory implements TaggedMemoryInterface { - static readonly log: DebugLogger = createDebugLogger('aztec:avm_simulator:memory'); - - // Whether to track and validate memory accesses for each instruction. - static readonly TRACK_MEMORY_ACCESSES = process.env.NODE_ENV === 'test'; - - // 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[]; - - constructor() { - // We do not initialize memory size here because otherwise tests blow up when diffing. - this._mem = []; - } - - /** Returns a MeteredTaggedMemory instance to track the number of reads and writes if TRACK_MEMORY_ACCESSES is set. */ - public track(type: string = 'instruction'): TaggedMemoryInterface { - return TaggedMemory.TRACK_MEMORY_ACCESSES ? new MeteredTaggedMemory(this, type) : this; - } - - public get(offset: number): MemoryValue { - assert(offset < TaggedMemory.MAX_MEMORY_SIZE); - const value = this.getAs(offset); - return value; - } - - public getAs(offset: number): T { - assert(offset < TaggedMemory.MAX_MEMORY_SIZE); - const word = this._mem[offset]; - TaggedMemory.log.debug(`get(${offset}) = ${word}`); - if (word === undefined) { - TaggedMemory.log.debug(`WARNING: Memory at offset ${offset} is undefined!`); - } - return word as T; - } - - public getSlice(offset: number, size: number): MemoryValue[] { - assert(offset < TaggedMemory.MAX_MEMORY_SIZE); - assert(offset + size < TaggedMemory.MAX_MEMORY_SIZE); - const value = this._mem.slice(offset, offset + size); - TaggedMemory.log.debug(`getSlice(${offset}, ${size}) = ${value}`); - assert(!value.some(e => e === undefined), 'Memory slice contains undefined values.'); - assert(value.length === size, `Expected slice of size ${size}, got ${value.length}.`); - return value; - } - - public getSliceAs(offset: number, size: number): T[] { - assert(offset < TaggedMemory.MAX_MEMORY_SIZE); - assert(offset + size < TaggedMemory.MAX_MEMORY_SIZE); - return this.getSlice(offset, size) as T[]; - } - - public getSliceTags(offset: number, size: number): TypeTag[] { - assert(offset < TaggedMemory.MAX_MEMORY_SIZE); - assert(offset + size < TaggedMemory.MAX_MEMORY_SIZE); - return this._mem.slice(offset, offset + size).map(TaggedMemory.getTag); - } - - public set(offset: number, v: MemoryValue) { - assert(offset < TaggedMemory.MAX_MEMORY_SIZE); - this._mem[offset] = v; - TaggedMemory.log.debug(`set(${offset}, ${v})`); - } - - public setSlice(offset: number, vs: MemoryValue[]) { - assert(offset < TaggedMemory.MAX_MEMORY_SIZE); - assert(offset + vs.length < TaggedMemory.MAX_MEMORY_SIZE); - // We may need to extend the memory size, otherwise splice doesn't insert. - if (offset + vs.length > this._mem.length) { - this._mem.length = offset + vs.length; - } - this._mem.splice(offset, vs.length, ...vs); - TaggedMemory.log.debug(`setSlice(${offset}, ${vs})`); - } - - public getTag(offset: number): TypeTag { - return TaggedMemory.getTag(this._mem[offset]); - } - - /** - * Check that the memory at the given offset matches the specified tag. - */ - public checkTag(tag: TypeTag, offset: number) { - if (this.getTag(offset) !== tag) { - throw TagCheckError.forOffset(offset, TypeTag[this.getTag(offset)], TypeTag[tag]); - } - } - - public checkIsValidMemoryOffsetTag(offset: number) { - if (this.getTag(offset) > TypeTag.UINT64) { - throw TagCheckError.forOffset(offset, TypeTag[this.getTag(offset)], 'UINT64'); - } - } - - public static checkIsIntegralTag(tag: TypeTag) { - if (![TypeTag.UINT8, TypeTag.UINT16, TypeTag.UINT32, TypeTag.UINT64, TypeTag.UINT128].includes(tag)) { - throw TagCheckError.forTag(TypeTag[tag], 'integral'); - } - } - - /** - * Check tags for memory at all of the specified offsets. - */ - public checkTags(tag: TypeTag, ...offsets: number[]) { - for (const offset of offsets) { - this.checkTag(tag, offset); - } - } - - /** - * Check tags for all memory in the specified range. - */ - public checkTagsRange(tag: TypeTag, startOffset: number, size: number) { - for (let offset = startOffset; offset < startOffset + size; offset++) { - this.checkTag(tag, offset); - } - } - - // TODO: this might be slow, but I don't want to have the types know of their tags. - // It might be possible to have a map. - public static getTag(v: MemoryValue | undefined): TypeTag { - let tag = TypeTag.INVALID; - - if (v === undefined) { - tag = TypeTag.UNINITIALIZED; - } else if (v instanceof Field) { - tag = TypeTag.FIELD; - } else if (v instanceof Uint8) { - tag = TypeTag.UINT8; - } else if (v instanceof Uint16) { - tag = TypeTag.UINT16; - } else if (v instanceof Uint32) { - tag = TypeTag.UINT32; - } else if (v instanceof Uint64) { - tag = TypeTag.UINT64; - } else if (v instanceof Uint128) { - tag = TypeTag.UINT128; - } - - return tag; - } - - // Truncates the value to fit the type. - public static integralFromTag(v: bigint | number, tag: TypeTag): IntegralValue { - v = BigInt(v); - switch (tag) { - case TypeTag.UINT8: - return new Uint8(v & ((1n << 8n) - 1n)); - case TypeTag.UINT16: - return new Uint16(v & ((1n << 16n) - 1n)); - case TypeTag.UINT32: - return new Uint32(v & ((1n << 32n) - 1n)); - case TypeTag.UINT64: - return new Uint64(v & ((1n << 64n) - 1n)); - case TypeTag.UINT128: - return new Uint128(v & ((1n << 128n) - 1n)); - default: - 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.`); - } - } - - /** No-op. Implemented here for compatibility with the MeteredTaggedMemory. */ - public assert(_operations: Partial) {} + public checkIsValidMemoryOffsetTag(offset: number) { + if (this.getTag(offset) > TypeTag.UINT64) { + throw TagCheckError.forOffset(offset, TypeTag[this.getTag(offset)], 'UINT64'); + } + } + + public static checkIsIntegralTag(tag: TypeTag) { + if (![TypeTag.UINT8, TypeTag.UINT16, TypeTag.UINT32, TypeTag.UINT64, TypeTag.UINT128].includes(tag)) { + throw TagCheckError.forTag(TypeTag[tag], 'integral'); + } + } + + /** + * Check tags for memory at all of the specified offsets. + */ + public checkTags(tag: TypeTag, ...offsets: number[]) { + for (const offset of offsets) { + this.checkTag(tag, offset); + } + } + + /** + * Check tags for all memory in the specified range. + */ + public checkTagsRange(tag: TypeTag, startOffset: number, size: number) { + for (let offset = startOffset; offset < startOffset + size; offset++) { + this.checkTag(tag, offset); + } + } + + // TODO: this might be slow, but I don't want to have the types know of their tags. + // It might be possible to have a map. + public static getTag(v: MemoryValue | undefined): TypeTag { + let tag = TypeTag.INVALID; + + if (v === undefined) { + tag = TypeTag.UNINITIALIZED; + } else if (v instanceof Field) { + tag = TypeTag.FIELD; + } else if (v instanceof Uint8) { + tag = TypeTag.UINT8; + } else if (v instanceof Uint16) { + tag = TypeTag.UINT16; + } else if (v instanceof Uint32) { + tag = TypeTag.UINT32; + } else if (v instanceof Uint64) { + tag = TypeTag.UINT64; + } else if (v instanceof Uint128) { + tag = TypeTag.UINT128; + } + + return tag; + } + + // Truncates the value to fit the type. + public static integralFromTag(v: bigint | number, tag: TypeTag): IntegralValue { + v = BigInt(v); + switch (tag) { + case TypeTag.UINT8: + return new Uint8(v & ((1n << 8n) - 1n)); + case TypeTag.UINT16: + return new Uint16(v & ((1n << 16n) - 1n)); + case TypeTag.UINT32: + return new Uint32(v & ((1n << 32n) - 1n)); + case TypeTag.UINT64: + return new Uint64(v & ((1n << 64n) - 1n)); + case TypeTag.UINT128: + return new Uint128(v & ((1n << 128n) - 1n)); + default: + 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.`); + } + } + + /** No-op. Implemented here for compatibility with the MeteredTaggedMemory. */ + public assert(_operations: Partial) { } } /** Tagged memory wrapper with metering for each memory read and write operation. */ export class MeteredTaggedMemory implements TaggedMemoryInterface { - private reads: number = 0; - private writes: number = 0; - - constructor(private wrapped: TaggedMemory, private type: string = 'instruction') {} - - /** Returns the number of reads and writes tracked so far and resets them to zero. */ - public reset(): MemoryOperations { - const stats = { reads: this.reads, writes: this.writes }; - this.reads = 0; - this.writes = 0; - return stats; - } - - /** - * Asserts that the exact number of memory operations have been performed. - * Indirect represents the flags for indirect accesses: each bit set to one counts as an extra read. - */ - public assert(operations: Partial) { - const { reads: expectedReads, writes: expectedWrites, indirect } = { reads: 0, writes: 0, ...operations }; - - const totalExpectedReads = expectedReads + Addressing.fromWire(indirect ?? 0).count(AddressingMode.INDIRECT); - const { reads: actualReads, writes: actualWrites } = this.reset(); - if (actualReads !== totalExpectedReads) { - throw new InstructionExecutionError( - `Incorrect number of memory reads for ${this.type}: expected ${totalExpectedReads} but executed ${actualReads}`, - ); - } - if (actualWrites !== expectedWrites) { - throw new InstructionExecutionError( - `Incorrect number of memory writes for ${this.type}: expected ${expectedWrites} but executed ${actualWrites}`, - ); - } - } - - public track(type: string = 'instruction'): MeteredTaggedMemory { - return new MeteredTaggedMemory(this.wrapped, type); - } - - public get(offset: number): MemoryValue { - this.reads++; - return this.wrapped.get(offset); - } - - public getSliceAs(offset: number, size: number): T[] { - this.reads += size; - return this.wrapped.getSliceAs(offset, size); - } - - public getAs(offset: number): T { - this.reads++; - return this.wrapped.getAs(offset); - } - - public getSlice(offset: number, size: number): MemoryValue[] { - this.reads += size; - return this.wrapped.getSlice(offset, size); - } - - public set(offset: number, v: MemoryValue): void { - this.writes++; - this.wrapped.set(offset, v); - } - - public setSlice(offset: number, vs: MemoryValue[]): void { - this.writes += vs.length; - this.wrapped.setSlice(offset, vs); - } - - public getSliceTags(offset: number, size: number): TypeTag[] { - return this.wrapped.getSliceTags(offset, size); - } - - public getTag(offset: number): TypeTag { - return this.wrapped.getTag(offset); - } - - public checkTag(tag: TypeTag, offset: number): void { - this.wrapped.checkTag(tag, offset); - } - - public checkIsValidMemoryOffsetTag(offset: number): void { - this.wrapped.checkIsValidMemoryOffsetTag(offset); - } - - public checkTags(tag: TypeTag, ...offsets: number[]): void { - this.wrapped.checkTags(tag, ...offsets); - } - - public checkTagsRange(tag: TypeTag, startOffset: number, size: number): void { - this.wrapped.checkTagsRange(tag, startOffset, size); - } + private reads: number = 0; + private writes: number = 0; + + constructor(private wrapped: TaggedMemory, private type: string = 'instruction') { } + + /** Returns the number of reads and writes tracked so far and resets them to zero. */ + public reset(): MemoryOperations { + const stats = { reads: this.reads, writes: this.writes }; + this.reads = 0; + this.writes = 0; + return stats; + } + + /** + * Asserts that the exact number of memory operations have been performed. + * Indirect represents the flags for indirect accesses: each bit set to one counts as an extra read. + */ + public assert(operations: Partial) { + const { reads: expectedReads, writes: expectedWrites, indirect } = { reads: 0, writes: 0, ...operations }; + + const totalExpectedReads = expectedReads + Addressing.fromWire(indirect ?? 0).count(AddressingMode.INDIRECT); + const { reads: actualReads, writes: actualWrites } = this.reset(); + if (actualReads !== totalExpectedReads) { + throw new InstructionExecutionError( + `Incorrect number of memory reads for ${this.type}: expected ${totalExpectedReads} but executed ${actualReads}`, + ); + } + if (actualWrites !== expectedWrites) { + throw new InstructionExecutionError( + `Incorrect number of memory writes for ${this.type}: expected ${expectedWrites} but executed ${actualWrites}`, + ); + } + } + + public track(type: string = 'instruction'): MeteredTaggedMemory { + return new MeteredTaggedMemory(this.wrapped, type); + } + + public get(offset: number): MemoryValue { + this.reads++; + return this.wrapped.get(offset); + } + + public getSliceAs(offset: number, size: number): T[] { + this.reads += size; + return this.wrapped.getSliceAs(offset, size); + } + + public getAs(offset: number): T { + this.reads++; + return this.wrapped.getAs(offset); + } + + public getSlice(offset: number, size: number): MemoryValue[] { + this.reads += size; + return this.wrapped.getSlice(offset, size); + } + + public set(offset: number, v: MemoryValue): void { + this.writes++; + this.wrapped.set(offset, v); + } + + public setSlice(offset: number, vs: MemoryValue[]): void { + this.writes += vs.length; + this.wrapped.setSlice(offset, vs); + } + + public getSliceTags(offset: number, size: number): TypeTag[] { + return this.wrapped.getSliceTags(offset, size); + } + + public getTag(offset: number): TypeTag { + return this.wrapped.getTag(offset); + } + + public checkTag(tag: TypeTag, offset: number): void { + this.wrapped.checkTag(tag, offset); + } + + public checkIsValidMemoryOffsetTag(offset: number): void { + this.wrapped.checkIsValidMemoryOffsetTag(offset); + } + + public checkTags(tag: TypeTag, ...offsets: number[]): void { + this.wrapped.checkTags(tag, ...offsets); + } + + public checkTagsRange(tag: TypeTag, startOffset: number, size: number): void { + this.wrapped.checkTagsRange(tag, startOffset, size); + } } /** Tracks number of memory reads and writes. */ export type MemoryOperations = { - /** How many total reads are performed. Slice reads are count as one per element. */ - reads: number; - /** How many total writes are performed. Slice writes are count as one per element. */ - writes: number; + /** How many total reads are performed. Slice reads are count as one per element. */ + reads: number; + /** How many total writes are performed. Slice writes are count as one per element. */ + writes: number; }; diff --git a/yarn-project/simulator/src/avm/opcodes/ec_add.test.ts b/yarn-project/simulator/src/avm/opcodes/ec_add.test.ts index 894f47b6dfe3..7aa58e0fedcd 100644 --- a/yarn-project/simulator/src/avm/opcodes/ec_add.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/ec_add.test.ts @@ -9,28 +9,28 @@ import { initContext } from '../fixtures/index.js'; import { EcAdd } from './ec_add.js'; describe('EC Instructions', () => { - let context: AvmContext; - const grumpkin: Grumpkin = new Grumpkin(); + let context: AvmContext; + const grumpkin: Grumpkin = new Grumpkin(); - beforeEach(() => { - context = initContext(); - }); + beforeEach(() => { + context = initContext(); + }); - describe('EcAdd', () => { - it('Should (de)serialize correctly', () => { - const buf = Buffer.from([ - EcAdd.opcode, // opcode - 0x01, // indirect - ...Buffer.from('12345670', 'hex'), // p1x - ...Buffer.from('12345671', 'hex'), // p1y - ...Buffer.from('00', 'hex'), // p1IsInfinite - ...Buffer.from('12345672', 'hex'), // p2x - ...Buffer.from('12345673', 'hex'), // p2y - ...Buffer.from('01', 'hex'), // p2IsInfinite - ...Buffer.from('12345674', 'hex'), // dstOffset - ]); - const inst = new EcAdd( - /*indirect=*/ 0x01, + describe('EcAdd', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + EcAdd.opcode, // opcode + 0x20, // indirect + ...Buffer.from('12345670', 'hex'), // p1x + ...Buffer.from('12345671', 'hex'), // p1y + ...Buffer.from('00000000', 'hex'), // p1IsInfinite + ...Buffer.from('12345672', 'hex'), // p2x + ...Buffer.from('12345673', 'hex'), // p2y + ...Buffer.from('00000001', 'hex'), // p2IsInfinite + ...Buffer.from('12345674', 'hex'), // dstOffset + ]); + const inst = new EcAdd( + /*indirect=*/ 0x20, /*p1X=*/ 0x12345670, /*p1Y=*/ 0x12345671, /*p1IsInfinite=*/ 0, @@ -38,26 +38,26 @@ describe('EC Instructions', () => { /*p2Y=*/ 0x12345673, /*p2IsInfinite=*/ 1, /*dstOffset=*/ 0x12345674, - ); + ); - expect(EcAdd.deserialize(buf)).toEqual(inst); - expect(inst.serialize()).toEqual(buf); - }); + expect(EcAdd.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); - it(`Should double correctly`, async () => { - const x = new Field(grumpkin.generator().x); - const y = new Field(grumpkin.generator().y); - const zero = new Uint8(0); + it(`Should double correctly`, async () => { + const x = new Field(grumpkin.generator().x); + const y = new Field(grumpkin.generator().y); + const zero = new Uint32(0); - context.machineState.memory.set(0, x); - context.machineState.memory.set(1, y); - context.machineState.memory.set(2, zero); - context.machineState.memory.set(3, x); - context.machineState.memory.set(4, y); - context.machineState.memory.set(5, zero); - context.machineState.memory.set(6, new Uint32(6)); + context.machineState.memory.set(0, x); + context.machineState.memory.set(1, y); + context.machineState.memory.set(2, zero); + context.machineState.memory.set(3, x); + context.machineState.memory.set(4, y); + context.machineState.memory.set(5, zero); + // context.machineState.memory.set(6, new Uint32(6)); - await new EcAdd( + await new EcAdd( /*indirect=*/ 0, /*p1X=*/ 0, /*p1Y=*/ 1, @@ -66,32 +66,32 @@ describe('EC Instructions', () => { /*p2Y=*/ 4, /*p2IsInfinite=*/ 5, /*dstOffset=*/ 6, - ).execute(context); + ).execute(context); - const actual = new Point(context.machineState.memory.get(6).toFr(), context.machineState.memory.get(7).toFr()); - const expected = grumpkin.add(grumpkin.generator(), grumpkin.generator()); - expect(actual).toEqual(expected); - expect(context.machineState.memory.get(8).toFr().equals(Fr.ZERO)).toBe(true); - }); + const actual = new Point(context.machineState.memory.get(6).toFr(), context.machineState.memory.get(7).toFr()); + const expected = grumpkin.add(grumpkin.generator(), grumpkin.generator()); + expect(actual).toEqual(expected); + expect(context.machineState.memory.get(8).toFr().equals(Fr.ZERO)).toBe(true); + }); - it('Should add correctly', async () => { - const G2 = grumpkin.add(grumpkin.generator(), grumpkin.generator()); - const zero = new Uint8(0); + it('Should add correctly', async () => { + const G2 = grumpkin.add(grumpkin.generator(), grumpkin.generator()); + const zero = new Uint32(0); - const x1 = new Field(grumpkin.generator().x); - const y1 = new Field(grumpkin.generator().y); - const x2 = new Field(G2.x); - const y2 = new Field(G2.y); + const x1 = new Field(grumpkin.generator().x); + const y1 = new Field(grumpkin.generator().y); + const x2 = new Field(G2.x); + const y2 = new Field(G2.y); - context.machineState.memory.set(0, x1); - context.machineState.memory.set(1, y1); - context.machineState.memory.set(2, zero); - context.machineState.memory.set(3, x2); - context.machineState.memory.set(4, y2); - context.machineState.memory.set(5, zero); - context.machineState.memory.set(6, new Uint32(6)); + context.machineState.memory.set(0, x1); + context.machineState.memory.set(1, y1); + context.machineState.memory.set(2, zero); + context.machineState.memory.set(3, x2); + context.machineState.memory.set(4, y2); + context.machineState.memory.set(5, zero); + context.machineState.memory.set(6, new Uint32(6)); - await new EcAdd( + await new EcAdd( /*indirect=*/ 0, /*p1X=*/ 0, /*p1Y=*/ 1, @@ -100,12 +100,12 @@ describe('EC Instructions', () => { /*p2Y=*/ 4, /*p2IsInfinite=*/ 5, /*dstOffset=*/ 6, - ).execute(context); + ).execute(context); - const actual = new Point(context.machineState.memory.get(6).toFr(), context.machineState.memory.get(7).toFr()); - const G3 = grumpkin.add(grumpkin.generator(), G2); - expect(actual).toEqual(G3); - expect(context.machineState.memory.get(8).toFr().equals(Fr.ZERO)).toBe(true); + const actual = new Point(context.machineState.memory.get(6).toFr(), context.machineState.memory.get(7).toFr()); + const G3 = grumpkin.add(grumpkin.generator(), G2); + expect(actual).toEqual(G3); + expect(context.machineState.memory.get(8).toFr().equals(Fr.ZERO)).toBe(true); + }); }); - }); }); diff --git a/yarn-project/simulator/src/avm/opcodes/ec_add.ts b/yarn-project/simulator/src/avm/opcodes/ec_add.ts index 2acbda553c19..c37da3685863 100644 --- a/yarn-project/simulator/src/avm/opcodes/ec_add.ts +++ b/yarn-project/simulator/src/avm/opcodes/ec_add.ts @@ -5,80 +5,96 @@ import { type AvmContext } from '../avm_context.js'; import { getBaseGasCost, getGasCostForTypeTag, getMemoryGasCost, sumGas } from '../avm_gas.js'; import { Field, MemoryOperations, TypeTag } from '../avm_memory_types.js'; import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; +import { Addressing } from './addressing_mode.js'; import { Instruction } from './instruction.js'; export class EcAdd extends Instruction { - static type: string = 'ECADD'; - static readonly opcode = Opcode.ECADD; + static type: string = 'ECADD'; + static readonly opcode = Opcode.ECADD; - // Informs (de)serialization. See Instruction.deserialize. - static readonly wireFormat: OperandType[] = [ - OperandType.UINT8, // reserved - OperandType.UINT8, // indirect - OperandType.UINT32, // p1X - OperandType.UINT32, // p1Y - OperandType.UINT8, // p1IsInfinite - OperandType.UINT32, // p2X - OperandType.UINT32, // p2Y - OperandType.UINT8, // p2IsInfinite - OperandType.UINT32, // dst - ]; + // Informs (de)serialization. See Instruction.deserialize. + static readonly wireFormat: OperandType[] = [ + OperandType.UINT8, // reserved + OperandType.UINT8, // indirect + OperandType.UINT32, // p1X + OperandType.UINT32, // p1Y + OperandType.UINT32, // p1IsInfinite + OperandType.UINT32, // p2X + OperandType.UINT32, // p2Y + OperandType.UINT32, // p2IsInfinite + OperandType.UINT32, // dst + ]; - constructor( - private indirect: number, - private p1X: number, - private p1Y: number, - private p1IsInfinite: number, - private p2X: number, - private p2Y: number, - private p2IsInfinite: number, - private dstOffset: number, - ) { - super(); - } + constructor( + private indirect: number, + private p1XOffset: number, + private p1YOffset: number, + private p1IsInfiniteOffset: number, + private p2XOffset: number, + private p2YOffset: number, + private p2IsInfiniteOffset: number, + private dstOffset: number, + ) { + super(); + } - public async execute(context: AvmContext): Promise { - const memoryOperations = { reads: 5, writes: 3, indirect: this.indirect }; - const memory = context.machineState.memory.track(this.type); - context.machineState.consumeGas(this.gasCost(memoryOperations)); + public async execute(context: AvmContext): Promise { + const memoryOperations = { reads: 4, writes: 3, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); - memory.checkTags(TypeTag.FIELD, this.p1X, this.p1Y, this.p2X, this.p2Y); - memory.checkTags(TypeTag.UINT8, this.p1IsInfinite, this.p2IsInfinite); + const [p1XOffset, p1YOffset, p1IsInfiniteOffset, p2XOffset, p2YOffset, p2IsInfiniteOffset, dstOffset] = + Addressing.fromWire(this.indirect).resolve( + [ + this.p1XOffset, + this.p1YOffset, + this.p1IsInfiniteOffset, + this.p2XOffset, + this.p2YOffset, + this.p2IsInfiniteOffset, + this.dstOffset, + ], + memory, + ); - const p1X = memory.get(this.p1X); - const p1Y = memory.get(this.p1Y); - // unused. Point doesn't store this information - // const p1IsInfinite = memory.get(this.p1IsInfinite); - const p1 = new Point(p1X.toFr(), p1Y.toFr()); - if (!p1.isOnGrumpkin()) { - throw new Error(`Point1 is not on the curve`); - } + // memory.checkTags(TypeTag.FIELD, p1XOffset, p1YOffset, p2XOffset, p2YOffset); + // memory.checkTags(TypeTag.UINT8, p1IsInfiniteOffset, p2IsInfiniteOffset); - const p2X = memory.get(this.p2X); - const p2Y = memory.get(this.p2Y); - // unused. Point doesn't store this information - // const p2IsInfinite = memory.get(this.p2IsInfinite); - const p2 = new Point(p2X.toFr(), p2Y.toFr()); - if (!p2.isOnGrumpkin()) { - throw new Error(`Point1 is not on the curve`); - } + const p1X = memory.get(p1XOffset); + const p1Y = memory.get(p1YOffset); + // const p1IsInfinite = memory.get(p1IsInfiniteOffset); + // unused. Point doesn't store this information + // const p1IsInfinite = memory.get(this.p1IsInfinite); + const p1 = new Point(p1X.toFr(), p1Y.toFr()); + if (!p1.isOnGrumpkin()) { + throw new Error(`Point1 is not on the curve`); + } - // const dest = p1.add(p2); - const grumpkin = new Grumpkin(); - const dest = grumpkin.add(p1, p2); - const dstOffsetResolved = Number(memory.get(this.dstOffset).toBigInt()); + const p2X = memory.get(p2XOffset); + const p2Y = memory.get(p2YOffset); + // unused. Point doesn't store this information + // const p2IsInfinite = memory.get(this.p2IsInfinite); + const p2 = new Point(p2X.toFr(), p2Y.toFr()); + if (!p2.isOnGrumpkin()) { + throw new Error(`Point1 is not on the curve`); + } - memory.set(dstOffsetResolved, new Field(dest.x)); - memory.set(dstOffsetResolved + 1, new Field(dest.y)); - memory.set(dstOffsetResolved + 2, new Field(dest.equals(Point.ZERO) ? 1 : 0)); + // const dest = p1.add(p2); + const grumpkin = new Grumpkin(); + const dest = grumpkin.add(p1, p2); - memory.assert(memoryOperations); - context.machineState.incrementPc(); - } + memory.set(dstOffset, new Field(dest.x)); + memory.set(dstOffset + 1, new Field(dest.y)); + // Check representation of infinity for grumpkin + memory.set(dstOffset + 2, new Field(dest.equals(Point.ZERO) ? 1 : 0)); - protected override gasCost(memoryOps: Partial) { - const baseGasCost = getGasCostForTypeTag(TypeTag.FIELD, getBaseGasCost(this.opcode)); - const memoryGasCost = getMemoryGasCost(memoryOps); - return sumGas(baseGasCost, memoryGasCost); - } + memory.assert(memoryOperations); + context.machineState.incrementPc(); + } + + protected override gasCost(memoryOps: Partial) { + const baseGasCost = getGasCostForTypeTag(TypeTag.FIELD, getBaseGasCost(this.opcode)); + const memoryGasCost = getMemoryGasCost(memoryOps); + return sumGas(baseGasCost, memoryGasCost); + } }