diff --git a/avm-transpiler/src/opcodes.rs b/avm-transpiler/src/opcodes.rs index 172531271374..e4d642dd8298 100644 --- a/avm-transpiler/src/opcodes.rs +++ b/avm-transpiler/src/opcodes.rs @@ -72,6 +72,7 @@ pub enum AvmOpcode { POSEIDON2, SHA256, // temp - may be removed, but alot of contracts rely on it PEDERSEN, // temp - may be removed, but alot of contracts rely on it + ECADD, // Conversions TORADIXLE, } @@ -163,6 +164,7 @@ impl AvmOpcode { AvmOpcode::POSEIDON2 => "POSEIDON2", AvmOpcode::SHA256 => "SHA256 ", AvmOpcode::PEDERSEN => "PEDERSEN", + AvmOpcode::ECADD => "ECADD", // Conversions AvmOpcode::TORADIXLE => "TORADIXLE", } diff --git a/avm-transpiler/src/transpile.rs b/avm-transpiler/src/transpile.rs index 099dfcbd06fb..67d0f8043a6e 100644 --- a/avm-transpiler/src/transpile.rs +++ b/avm-transpiler/src/transpile.rs @@ -831,6 +831,30 @@ fn handle_black_box_function(avm_instrs: &mut Vec, operation: &B ], }); } + // This will be changed to utilise relative memory offsets + BlackBoxOp::EmbeddedCurveAdd { + input1_x: p1_x_offset, + input1_y: p1_y_offset, + input1_infinite: p1_infinite_offset, + input2_x: p2_x_offset, + input2_y: p2_y_offset, + input2_infinite: p2_infinite_offset, + result, + } => avm_instrs.push(AvmInstruction { + opcode: AvmOpcode::ECADD, + // The result (SIXTH operand) is indirect. + indirect: Some(0b1000000), + operands: vec![ + AvmOperand::U32 { value: p1_x_offset.0 as u32 }, + AvmOperand::U32 { value: p1_y_offset.0 as u32 }, + AvmOperand::U32 { value: p1_infinite_offset.0 as u32 }, + AvmOperand::U32 { value: p2_x_offset.0 as u32 }, + AvmOperand::U32 { value: p2_y_offset.0 as u32 }, + AvmOperand::U32 { value: p2_infinite_offset.0 as u32 }, + AvmOperand::U32 { value: result.pointer.0 as u32 }, + ], + ..Default::default() + }), _ => panic!("Transpiler doesn't know how to process {:?}", operation), } } 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 358eba7e3c87..6b10ab40afc1 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_deserialization.cpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_deserialization.cpp @@ -148,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 } }, // dst_offset // Gadget - Conversion { OpCode::TORADIXLE, { OperandType::INDIRECT, OperandType::UINT32, OperandType::UINT32, OperandType::UINT32, OperandType::UINT32 } }, 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 27dc5548b79a..46f037e42ea9 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_execution.cpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_execution.cpp @@ -664,6 +664,16 @@ 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_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)), 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 cc8e53aeb366..9934164ad238 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 a45bde206d73..fb9e8b7e576d 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_trace.cpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_trace.cpp @@ -3520,6 +3520,113 @@ 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_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) +{ + 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); + } + + mem_trace_builder.write_into_memory( + call_ptr, clk, IntermRegister::IA, direct_output_offset, result.x, AvmMemoryTag::U0, AvmMemoryTag::FF); + mem_trace_builder.write_into_memory( + call_ptr, clk, IntermRegister::IB, direct_output_offset + 1, result.y, AvmMemoryTag::U0, AvmMemoryTag::FF); + main_trace.push_back(Row{ + .avm_main_clk = clk, + .avm_main_ia = result.x, + .avm_main_ib = result.y, + .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_idx_b = FF(direct_output_offset + 1), + .avm_main_mem_op_a = FF(1), + .avm_main_mem_op_b = FF(1), + .avm_main_pc = FF(pc), + .avm_main_rwa = FF(1), + .avm_main_rwb = FF(1), + .avm_main_w_in_tag = FF(static_cast(AvmMemoryTag::FF)), + }); + 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 70d45adb3d55..b0e99b791d69 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" @@ -192,6 +193,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_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. @@ -217,6 +227,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..fd3fc8955d13 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/vm/avm_trace/gadgets/avm_ecc.cpp @@ -0,0 +1,33 @@ +#include "barretenberg/vm/avm_trace/gadgets/avm_ecc.hpp" +#include "barretenberg/vm/avm_trace/avm_common.hpp" + +namespace bb::avm_trace { +using element = grumpkin::g1::affine_element; + +AvmEccTraceBuilder::AvmEccTraceBuilder() +{ + ecc_trace.reserve(AVM_TRACE_SIZE); +} + +std::vector AvmEccTraceBuilder::finalize() +{ + return std::move(ecc_trace); +} + +void AvmEccTraceBuilder::reset() +{ + ecc_trace.clear(); +} + +element AvmEccTraceBuilder::embedded_curve_add(element lhs, element rhs, uint32_t clk) +{ + 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..6450a33db399 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/vm/avm_trace/gadgets/avm_ecc.hpp @@ -0,0 +1,29 @@ +#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 b3a227b9dc51..69d47e3f7d4f 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,68 @@ 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 calldata = { a.x, a.y, b.x, b.y }; + auto trace = Execution::gen_trace(instructions, returndata, calldata, public_inputs_vec); + + 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 bd54379030ff..5b92424f0aaf 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 @@ -24,6 +24,7 @@ contract AvmTest { global big_field_136_bits: Field = 0x991234567890abcdef1234567890abcdef; // Libs + use dep::std::embedded_curve_ops::EmbeddedCurvePoint; use dep::aztec::protocol_types::constants::CONTRACT_INSTANCE_LENGTH; use dep::aztec::prelude::{Map, Deserialize}; use dep::aztec::state_vars::PublicMutable; @@ -134,6 +135,15 @@ contract AvmTest { a % 2 } + #[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 + } + /************************************************************************ * Misc ************************************************************************/ diff --git a/yarn-project/foundation/src/fields/fields.ts b/yarn-project/foundation/src/fields/fields.ts index c01abe041c63..a0233d91a995 100644 --- a/yarn-project/foundation/src/fields/fields.ts +++ b/yarn-project/foundation/src/fields/fields.ts @@ -248,6 +248,14 @@ export class Fr extends BaseField { return new Fr((this.toBigInt() + rhs.toBigInt()) % Fr.MODULUS); } + square() { + return new Fr((this.toBigInt() * this.toBigInt()) % Fr.MODULUS); + } + + negate() { + return new Fr(Fr.MODULUS - this.toBigInt()); + } + sub(rhs: Fr) { const result = this.toBigInt() - rhs.toBigInt(); return new Fr(result < 0 ? result + Fr.MODULUS : result); diff --git a/yarn-project/foundation/src/fields/point.ts b/yarn-project/foundation/src/fields/point.ts index 9fd8e8fdf03e..b152bffcc6a2 100644 --- a/yarn-project/foundation/src/fields/point.ts +++ b/yarn-project/foundation/src/fields/point.ts @@ -23,7 +23,9 @@ export class Point { * The point's y coordinate */ public readonly y: Fr, - ) {} + ) { + // TODO: Do we want to check if the point is on the curve here? + } /** * Generate a random Point instance. @@ -44,7 +46,7 @@ export class Point { */ static fromBuffer(buffer: Buffer | BufferReader) { const reader = BufferReader.asReader(buffer); - return new this(Fr.fromBuffer(reader.readBytes(32)), Fr.fromBuffer(reader.readBytes(32))); + return new this(Fr.fromBuffer(reader), Fr.fromBuffer(reader)); } /** @@ -134,6 +136,18 @@ export class Point { hash() { return poseidon2Hash(this.toFields()); } + + isOnGrumpkin() { + if (this.isZero()) { + return true; + } + + // p.y * p.y == p.x * p.x * p.x - 17 + const A = new Fr(17); + const lhs = this.y.square(); + const rhs = this.x.square().mul(this.x).sub(A); + return lhs.equals(rhs); + } } /** diff --git a/yarn-project/simulator/src/avm/avm_gas.ts b/yarn-project/simulator/src/avm/avm_gas.ts index b7616012f40a..7802d4177d1e 100644 --- a/yarn-project/simulator/src/avm/avm_gas.ts +++ b/yarn-project/simulator/src/avm/avm_gas.ts @@ -122,6 +122,7 @@ const BaseGasCosts: Record = { [Opcode.POSEIDON2]: DefaultBaseGasCost, [Opcode.SHA256]: DefaultBaseGasCost, [Opcode.PEDERSEN]: DefaultBaseGasCost, + [Opcode.ECADD]: DefaultBaseGasCost, // Conversions [Opcode.TORADIXLE]: DefaultBaseGasCost, }; diff --git a/yarn-project/simulator/src/avm/avm_simulator.test.ts b/yarn-project/simulator/src/avm/avm_simulator.test.ts index f043c4279bcd..f1e8c98fb904 100644 --- a/yarn-project/simulator/src/avm/avm_simulator.test.ts +++ b/yarn-project/simulator/src/avm/avm_simulator.test.ts @@ -1,9 +1,10 @@ import { UnencryptedL2Log } from '@aztec/circuit-types'; +import { Grumpkin } from '@aztec/circuits.js/barretenberg'; import { computeVarArgsHash } from '@aztec/circuits.js/hash'; import { EventSelector, FunctionSelector } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { keccak256, pedersenHash, poseidon2Hash, sha256 } from '@aztec/foundation/crypto'; -import { Fr } from '@aztec/foundation/fields'; +import { Fq, Fr } from '@aztec/foundation/fields'; import { type Fieldable } from '@aztec/foundation/serialize'; import { jest } from '@jest/globals'; @@ -95,6 +96,18 @@ describe('AVM simulator: transpiled Noir contracts', () => { expect(await isAvmBytecode(bytecode)); }); + it('elliptic curve operations', async () => { + const context = initContext(); + + const bytecode = getAvmTestContractBytecode('elliptic_curve_add_and_double'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); + + expect(results.reverted).toBe(false); + const grumpkin = new Grumpkin(); + const g3 = grumpkin.mul(grumpkin.generator(), new Fq(3)); + expect(results.output).toEqual([g3.x, g3.y, Fr.ZERO]); + }); + describe('U128 addition and overflows', () => { it('U128 addition', async () => { const calldata: Fr[] = [ diff --git a/yarn-project/simulator/src/avm/opcodes/ec_add.test.ts b/yarn-project/simulator/src/avm/opcodes/ec_add.test.ts new file mode 100644 index 000000000000..2c665378238e --- /dev/null +++ b/yarn-project/simulator/src/avm/opcodes/ec_add.test.ts @@ -0,0 +1,111 @@ +import { Fr, Point } from '@aztec/circuits.js'; +import { Grumpkin } from '@aztec/circuits.js/barretenberg'; + +import { beforeEach } from '@jest/globals'; + +import { type AvmContext } from '../avm_context.js'; +import { Field, Uint32 } from '../avm_memory_types.js'; +import { initContext } from '../fixtures/index.js'; +import { EcAdd } from './ec_add.js'; + +describe('EC Instructions', () => { + let context: AvmContext; + const grumpkin: Grumpkin = new Grumpkin(); + + beforeEach(() => { + context = initContext(); + }); + + 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, + /*p2X=*/ 0x12345672, + /*p2Y=*/ 0x12345673, + /*p2IsInfinite=*/ 1, + /*dstOffset=*/ 0x12345674, + ); + + 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 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)); + + await new EcAdd( + /*indirect=*/ 0, + /*p1X=*/ 0, + /*p1Y=*/ 1, + /*p1IsInfinite=*/ 2, + /*p2X=*/ 3, + /*p2Y=*/ 4, + /*p2IsInfinite=*/ 5, + /*dstOffset=*/ 6, + ).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); + }); + + 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); + + 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( + /*indirect=*/ 0, + /*p1X=*/ 0, + /*p1Y=*/ 1, + /*p1IsInfinite=*/ 2, + /*p2X=*/ 3, + /*p2Y=*/ 4, + /*p2IsInfinite=*/ 5, + /*dstOffset=*/ 6, + ).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); + }); + }); +}); diff --git a/yarn-project/simulator/src/avm/opcodes/ec_add.ts b/yarn-project/simulator/src/avm/opcodes/ec_add.ts new file mode 100644 index 000000000000..044ca5e31a44 --- /dev/null +++ b/yarn-project/simulator/src/avm/opcodes/ec_add.ts @@ -0,0 +1,92 @@ +import { Grumpkin } from '@aztec/circuits.js/barretenberg'; +import { Point } from '@aztec/foundation/fields'; + +import { type AvmContext } from '../avm_context.js'; +import { Field } 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; + + // 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 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: 6, writes: 3, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); + + 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(p1XOffset); + const p1Y = memory.get(p1YOffset); + const p1IsInfinite = memory.get(p1IsInfiniteOffset).toNumber() === 1; + const p1 = new Point(p1X.toFr(), p1Y.toFr()); + if (!p1.isOnGrumpkin()) { + throw new Error(`Point1 is not on the curve`); + } + + const p2X = memory.get(p2XOffset); + const p2Y = memory.get(p2YOffset); + // unused. Point doesn't store this information + const p2IsInfinite = memory.get(p2IsInfiniteOffset).toNumber() === 1; + const p2 = new Point(p2X.toFr(), p2Y.toFr()); + if (!p2.isOnGrumpkin()) { + throw new Error(`Point1 is not on the curve`); + } + + const grumpkin = new Grumpkin(); + let dest = grumpkin.add(p1, p2); + // Temporary, + if (p1IsInfinite) { + dest = p2; + } else if (p2IsInfinite) { + dest = p1; + } + 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)); + + memory.assert(memoryOperations); + context.machineState.incrementPc(); + } +} diff --git a/yarn-project/simulator/src/avm/serialization/bytecode_serialization.ts b/yarn-project/simulator/src/avm/serialization/bytecode_serialization.ts index f7906fffc265..0cf22ba0a5c4 100644 --- a/yarn-project/simulator/src/avm/serialization/bytecode_serialization.ts +++ b/yarn-project/simulator/src/avm/serialization/bytecode_serialization.ts @@ -1,4 +1,5 @@ import { DAGasLeft, L2GasLeft } from '../opcodes/context_getters.js'; +import { EcAdd } from '../opcodes/ec_add.js'; import { Keccak, Pedersen, Poseidon2, Sha256 } from '../opcodes/hashing.js'; import type { Instruction } from '../opcodes/index.js'; import { @@ -137,6 +138,7 @@ const INSTRUCTION_SET = () => [DebugLog.opcode, DebugLog], // Gadgets + [EcAdd.opcode, EcAdd], [Keccak.opcode, Keccak], [Poseidon2.opcode, Poseidon2], [Sha256.opcode, Sha256], diff --git a/yarn-project/simulator/src/avm/serialization/instruction_serialization.ts b/yarn-project/simulator/src/avm/serialization/instruction_serialization.ts index dca3368b89b8..d8ccbd918409 100644 --- a/yarn-project/simulator/src/avm/serialization/instruction_serialization.ts +++ b/yarn-project/simulator/src/avm/serialization/instruction_serialization.ts @@ -76,6 +76,7 @@ export enum Opcode { POSEIDON2, SHA256, // temp - may be removed, but alot of contracts rely on it PEDERSEN, // temp - may be removed, but alot of contracts rely on it + ECADD, // Conversion TORADIXLE, } diff --git a/yarn-project/tsconfig.json b/yarn-project/tsconfig.json index 96d001f5ded6..52b02c625b21 100644 --- a/yarn-project/tsconfig.json +++ b/yarn-project/tsconfig.json @@ -49,5 +49,6 @@ { "path": "entrypoints/tsconfig.json" }, { "path": "cli/tsconfig.json" } ], - "files": ["./@types/jest/index.d.ts"] + "files": ["./@types/jest/index.d.ts"], + "exclude": ["node_modules"] }