diff --git a/cpp/src/barretenberg/proof_system/arithmetization/gate_data.hpp b/cpp/src/barretenberg/proof_system/arithmetization/gate_data.hpp index f47a033838..ba60a0dd0e 100644 --- a/cpp/src/barretenberg/proof_system/arithmetization/gate_data.hpp +++ b/cpp/src/barretenberg/proof_system/arithmetization/gate_data.hpp @@ -70,6 +70,16 @@ template struct poly_triple_ { using poly_triple = poly_triple_; +struct ecc_op_tuple { + uint32_t op; + uint32_t x_lo; + uint32_t x_hi; + uint32_t y_lo; + uint32_t y_hi; + uint32_t z_lo; + uint32_t z_hi; +}; + template inline void read(B& buf, poly_triple& constraint) { using serialize::read; diff --git a/cpp/src/barretenberg/proof_system/circuit_builder/goblin_ultra_circuit_builder.test.cpp b/cpp/src/barretenberg/proof_system/circuit_builder/goblin_ultra_circuit_builder.test.cpp new file mode 100644 index 0000000000..6e229f7ba6 --- /dev/null +++ b/cpp/src/barretenberg/proof_system/circuit_builder/goblin_ultra_circuit_builder.test.cpp @@ -0,0 +1,112 @@ +#include "barretenberg/crypto/generators/generator_data.hpp" +#include "ultra_circuit_builder.hpp" +#include + +using namespace barretenberg; + +namespace { +auto& engine = numeric::random::get_debug_engine(); +} +namespace proof_system { + +/** + * @brief Test the queueing of simple ecc ops via the Goblin builder + * @details There are two things to check here: 1) When ecc ops are queued by the builder, the corresponding native + * operations are performed correctly by the internal ecc op queue, and 2) The ecc op gate operands are correctly + * encoded in the op_wires, i.e. the operands can be reconstructed as expected. + * + */ +TEST(UltraCircuitBuilder, GoblinSimple) +{ + auto builder = UltraCircuitBuilder(); + + // Compute a simple point accumulation natively + auto P1 = g1::affine_element::random_element(); + auto P2 = g1::affine_element::random_element(); + auto z = fr::random_element(); + auto P_expected = P1 + P2 * z; + + // Add gates corresponding to the above operations + builder.queue_ecc_add_accum(P1); + builder.queue_ecc_mul_accum(P2, z); + + // Add equality op gates based on the internal accumulator + auto P_result = builder.queue_ecc_eq(); + + // Check that value returned from internal accumulator is correct + EXPECT_EQ(P_result, P_expected); + + // Check that the accumulator in the op queue has been reset to 0 + auto accumulator = builder.op_queue.get_accumulator(); + EXPECT_EQ(accumulator, g1::affine_point_at_infinity); + + // Check number of ecc op "gates"/rows = 3 ops * 2 rows per op = 6 + EXPECT_EQ(builder.num_ecc_op_gates, 6); + + // Check that the expected op codes have been correctly recorded in the 1st op wire + EXPECT_EQ(builder.ecc_op_wire_1[0], EccOpCode::ADD_ACCUM); + EXPECT_EQ(builder.ecc_op_wire_1[2], EccOpCode::MUL_ACCUM); + EXPECT_EQ(builder.ecc_op_wire_1[4], EccOpCode::EQUALITY); + + // Check that we can reconstruct the coordinates of P1 from the op_wires + auto chunk_size = plonk::NUM_LIMB_BITS_IN_FIELD_SIMULATION * 2; + auto P1_x_lo = uint256_t(builder.variables[builder.ecc_op_wire_2[0]]); + auto P1_x_hi = uint256_t(builder.variables[builder.ecc_op_wire_3[0]]); + auto P1_x = P1_x_lo + (P1_x_hi << chunk_size); + EXPECT_EQ(P1_x, uint256_t(P1.x)); + auto P1_y_lo = uint256_t(builder.variables[builder.ecc_op_wire_4[0]]); + auto P1_y_hi = uint256_t(builder.variables[builder.ecc_op_wire_2[1]]); + auto P1_y = P1_y_lo + (P1_y_hi << chunk_size); + EXPECT_EQ(P1_y, uint256_t(P1.y)); + + // Check that we can reconstruct the coordinates of P2 from the op_wires + auto P2_x_lo = uint256_t(builder.variables[builder.ecc_op_wire_2[2]]); + auto P2_x_hi = uint256_t(builder.variables[builder.ecc_op_wire_3[2]]); + auto P2_x = P2_x_lo + (P2_x_hi << chunk_size); + EXPECT_EQ(P2_x, uint256_t(P2.x)); + auto P2_y_lo = uint256_t(builder.variables[builder.ecc_op_wire_4[2]]); + auto P2_y_hi = uint256_t(builder.variables[builder.ecc_op_wire_2[3]]); + auto P2_y = P2_y_lo + (P2_y_hi << chunk_size); + EXPECT_EQ(P2_y, uint256_t(P2.y)); + + // Check that we can reconstruct the coordinates of P_result from the op_wires + auto P_expected_x_lo = uint256_t(builder.variables[builder.ecc_op_wire_2[4]]); + auto P_expected_x_hi = uint256_t(builder.variables[builder.ecc_op_wire_3[4]]); + auto P_expected_x = P_expected_x_lo + (P_expected_x_hi << chunk_size); + EXPECT_EQ(P_expected_x, uint256_t(P_expected.x)); + auto P_expected_y_lo = uint256_t(builder.variables[builder.ecc_op_wire_4[4]]); + auto P_expected_y_hi = uint256_t(builder.variables[builder.ecc_op_wire_2[5]]); + auto P_expected_y = P_expected_y_lo + (P_expected_y_hi << chunk_size); + EXPECT_EQ(P_expected_y, uint256_t(P_expected.y)); +} + +/** + * @brief Test correctness of native ecc batch mul performed behind the scenes when adding ecc op gates for a batch mul + * + */ +TEST(UltraCircuitBuilder, GoblinBatchMul) +{ + using Point = g1::affine_element; + using Scalar = fr; + + auto builder = UltraCircuitBuilder(); + const size_t num_muls = 3; + + // Compute some random points and scalars to batch multiply + std::vector points; + std::vector scalars; + auto batched_expected = Point::infinity(); + for (size_t i = 0; i < num_muls; ++i) { + points.emplace_back(Point::random_element()); + scalars.emplace_back(Scalar::random_element()); + batched_expected = batched_expected + points[i] * scalars[i]; + } + + // Populate the batch mul operands in the op wires and natively compute the result + auto batched_result = builder.batch_mul(points, scalars); + + // Extract current accumulator point from the op queue and check the result + EXPECT_EQ(batched_result, batched_expected); +} + +} // namespace proof_system \ No newline at end of file diff --git a/cpp/src/barretenberg/proof_system/circuit_builder/ultra_circuit_builder.cpp b/cpp/src/barretenberg/proof_system/circuit_builder/ultra_circuit_builder.cpp index b8c65a780d..0273773a2f 100644 --- a/cpp/src/barretenberg/proof_system/circuit_builder/ultra_circuit_builder.cpp +++ b/cpp/src/barretenberg/proof_system/circuit_builder/ultra_circuit_builder.cpp @@ -496,6 +496,154 @@ template uint32_t UltraCircuitBuilder_::put_constant_variable( } } +/** + * ** Goblin Methods ** + */ + +/** + * @brief Add gates corresponding to a batched mul + * + * @param points + * @param scalars + * @return g1::affine_element Result of batched mul + */ +template +g1::affine_element UltraCircuitBuilder_::batch_mul(const std::vector& points, + const std::vector& scalars) +{ + // TODO(luke): Do we necessarily want to check accum == 0? Other checks? + ASSERT(op_queue.get_accumulator().is_point_at_infinity()); + + size_t num_muls = points.size(); + for (size_t idx = 0; idx < num_muls; ++idx) { + queue_ecc_mul_accum(points[idx], scalars[idx]); + } + return op_queue.get_accumulator(); +} + +/** + * @brief Add gates for simple point addition without scalar and compute corresponding op natively + * + * @param point + */ +template void UltraCircuitBuilder_::queue_ecc_add_accum(const barretenberg::g1::affine_element& point) +{ + // Add raw op to queue + op_queue.add_accumulate(point); + + // Add ecc op gates + add_ecc_op_gates(EccOpCode::ADD_ACCUM, point); +} + +/** + * @brief Add gates for point mul and add and compute corresponding op natively + * + * @param point + * @param scalar + */ +template +void UltraCircuitBuilder_::queue_ecc_mul_accum(const barretenberg::g1::affine_element& point, + const barretenberg::fr& scalar) +{ + // Add raw op to op queue + op_queue.mul_accumulate(point, scalar); + + // Add ecc op gates + add_ecc_op_gates(EccOpCode::MUL_ACCUM, point, scalar); +} + +/** + * @brief Add point equality gates + * + * @return point to which equality has been asserted + */ +template barretenberg::g1::affine_element UltraCircuitBuilder_::queue_ecc_eq() +{ + // Add raw op to op queue + auto point = op_queue.eq(); + + // Add ecc op gates + add_ecc_op_gates(EccOpCode::EQUALITY, point); + + return point; +} + +/** + * @brief Add ecc op gates given an op code and its operands + * + * @param op Op code + * @param point + * @param scalar + */ +template +void UltraCircuitBuilder_::add_ecc_op_gates(uint32_t op, const g1::affine_element& point, const fr& scalar) +{ + auto op_tuple = make_ecc_op_tuple(op, point, scalar); + + record_ecc_op(op_tuple); +} + +/** + * @brief Decompose ecc operands into components, add corresponding variables, return ecc op tuple + * + * @param op + * @param point + * @param scalar + * @return ecc_op_tuple Tuple of indices into variables array used to construct pair of ecc op gates + */ +template +ecc_op_tuple UltraCircuitBuilder_::make_ecc_op_tuple(uint32_t op, const g1::affine_element& point, const fr& scalar) +{ + const size_t CHUNK_SIZE = 2 * DEFAULT_NON_NATIVE_FIELD_LIMB_BITS; + auto x_256 = uint256_t(point.x); + auto y_256 = uint256_t(point.y); + auto x_lo_idx = this->add_variable(x_256.slice(0, CHUNK_SIZE)); + auto x_hi_idx = this->add_variable(x_256.slice(CHUNK_SIZE, CHUNK_SIZE * 2)); + auto y_lo_idx = this->add_variable(y_256.slice(0, CHUNK_SIZE)); + auto y_hi_idx = this->add_variable(y_256.slice(CHUNK_SIZE, CHUNK_SIZE * 2)); + + // Split scalar into 128 bit endomorphism scalars + fr z_1 = 0; + fr z_2 = 0; + // TODO(luke): do this montgomery conversion here? + // auto converted = scalar.from_montgomery_form(); + // fr::split_into_endomorphism_scalars(converted, z_1, z_2); + // z_1 = z_1.to_montgomery_form(); + // z_2 = z_2.to_montgomery_form(); + fr::split_into_endomorphism_scalars(scalar, z_1, z_2); + auto z_1_idx = this->add_variable(z_1); + auto z_2_idx = this->add_variable(z_2); + + return { op, x_lo_idx, x_hi_idx, y_lo_idx, y_hi_idx, z_1_idx, z_2_idx }; +} + +/** + * @brief Add ecc operation to queue + * + * @param in Variables array indices corresponding to operation inputs + * @note We dont explicitly set values for the selectors here since their values are fully determined by + * num_ecc_op_gates. E.g. in the composer we can reconstruct q_ecc_op as the indicator on the first num_ecc_op_gates + * indices. All other selectors are simply 0 on this domain. + */ +template void UltraCircuitBuilder_::record_ecc_op(const ecc_op_tuple& in) +{ + ecc_op_wire_1.emplace_back(in.op); + ecc_op_wire_2.emplace_back(in.x_lo); + ecc_op_wire_3.emplace_back(in.x_hi); + ecc_op_wire_4.emplace_back(in.y_lo); + + ecc_op_wire_1.emplace_back(in.op); // TODO(luke): second op val is sort of a dummy. use "op" again? + ecc_op_wire_2.emplace_back(in.y_hi); + ecc_op_wire_3.emplace_back(in.z_lo); + ecc_op_wire_4.emplace_back(in.z_hi); + + num_ecc_op_gates += 2; +}; + +/** + * End of Goblin Methods + */ + template plookup::BasicTable& UltraCircuitBuilder_::get_table(const plookup::BasicTableId id) { for (plookup::BasicTable& table : lookup_tables) { diff --git a/cpp/src/barretenberg/proof_system/circuit_builder/ultra_circuit_builder.hpp b/cpp/src/barretenberg/proof_system/circuit_builder/ultra_circuit_builder.hpp index 5fd3e06da1..29220028f6 100644 --- a/cpp/src/barretenberg/proof_system/circuit_builder/ultra_circuit_builder.hpp +++ b/cpp/src/barretenberg/proof_system/circuit_builder/ultra_circuit_builder.hpp @@ -4,6 +4,7 @@ #include "barretenberg/plonk/proof_system/types/prover_settings.hpp" #include "barretenberg/polynomials/polynomial.hpp" #include "barretenberg/proof_system/arithmetization/arithmetization.hpp" +#include "barretenberg/proof_system/op_queue/ecc_op_queue.hpp" #include "barretenberg/proof_system/plookup_tables/plookup_tables.hpp" #include "barretenberg/proof_system/plookup_tables/types.hpp" #include "barretenberg/proof_system/types/merkle_hash_type.hpp" @@ -35,6 +36,12 @@ template class UltraCircuitBuilder_ : public CircuitBuilderBase class UltraCircuitBuilder_ : public CircuitBuilderBase(this->wires); WireVector& w_4 = std::get<3>(this->wires); + // Wires storing ecc op queue data; values are indices into the variables array + std::array::NUM_WIRES> ecc_op_wires; + + WireVector& ecc_op_wire_1 = std::get<0>(ecc_op_wires); + WireVector& ecc_op_wire_2 = std::get<1>(ecc_op_wires); + WireVector& ecc_op_wire_3 = std::get<2>(ecc_op_wires); + WireVector& ecc_op_wire_4 = std::get<3>(ecc_op_wires); + SelectorVector& q_m = this->selectors.q_m; SelectorVector& q_c = this->selectors.q_c; SelectorVector& q_1 = this->selectors.q_1; @@ -688,6 +703,20 @@ template class UltraCircuitBuilder_ : public CircuitBuilderBase& points, const std::vector& scalars); + + private: + void record_ecc_op(const ecc_op_tuple& in); + void add_ecc_op_gates(uint32_t op, const g1::affine_element& point, const fr& scalar = fr::zero()); + ecc_op_tuple make_ecc_op_tuple(uint32_t op, const g1::affine_element& point, const fr& scalar = fr::zero()); + + public: size_t get_num_constant_gates() const override { return 0; } /** diff --git a/cpp/src/barretenberg/proof_system/op_queue/ecc_op_queue.hpp b/cpp/src/barretenberg/proof_system/op_queue/ecc_op_queue.hpp index 4a810a37e8..a5c582e602 100644 --- a/cpp/src/barretenberg/proof_system/op_queue/ecc_op_queue.hpp +++ b/cpp/src/barretenberg/proof_system/op_queue/ecc_op_queue.hpp @@ -4,6 +4,8 @@ namespace proof_system { +enum EccOpCode { ADD_ACCUM, MUL_ACCUM, EQUALITY, NULL_OP }; + /** * @brief Raw description of an ECC operation used to produce equivalent descriptions over different curves. */ @@ -31,6 +33,10 @@ class ECCOpQueue { Point point_at_infinity = curve::BN254::Group::affine_point_at_infinity; using Fr = curve::BN254::ScalarField; using Fq = curve::BN254::BaseField; // Grumpkin's scalar field + + // The operations written to the queue are also performed natively; the result is stored in accumulator + Point accumulator = point_at_infinity; + public: std::vector raw_ops; std::vector> ultra_ops; @@ -52,8 +58,19 @@ class ECCOpQueue { return num_muls; } + Point get_accumulator() { return accumulator; } + + /** + * @brief Write point addition op to queue and natively perform addition + * + * @param to_add + */ void add_accumulate(const Point& to_add) { + // Update the accumulator natively + accumulator = accumulator + to_add; + + // Store the operation raw_ops.emplace_back(ECCOp{ .add = true, .mul = false, @@ -66,8 +83,17 @@ class ECCOpQueue { }); } + /** + * @brief Write multiply and add op to queue and natively perform operation + * + * @param to_add + */ void mul_accumulate(const Point& to_mul, const Fr& scalar) { + // Update the accumulator natively + accumulator = accumulator + to_mul * scalar; + + // Store the operation Fr scalar_1 = 0; Fr scalar_2 = 0; auto converted = scalar.from_montgomery_form(); @@ -85,8 +111,17 @@ class ECCOpQueue { .mul_scalar_full = scalar, }); } - void eq(const Point& expected) + + /** + * @brief Write equality op using internal accumulator point + * + * @return current internal accumulator point (prior to reset to 0) + */ + Point eq() { + auto expected = accumulator; + accumulator.self_set_infinity(); // TODO(luke): is this always desired? + raw_ops.emplace_back(ECCOp{ .add = false, .mul = false, @@ -97,8 +132,14 @@ class ECCOpQueue { .scalar_2 = 0, .mul_scalar_full = 0, }); + + return expected; } + /** + * @brief Write empty row to queue + * + */ void empty_row() { raw_ops.emplace_back(ECCOp{ @@ -114,4 +155,4 @@ class ECCOpQueue { } }; -} // namespace proof_system \ No newline at end of file +} // namespace proof_system diff --git a/cpp/src/barretenberg/proof_system/op_queue/ecc_op_queue.test.cpp b/cpp/src/barretenberg/proof_system/op_queue/ecc_op_queue.test.cpp index 7b1c160eb6..2b41923ecd 100644 --- a/cpp/src/barretenberg/proof_system/op_queue/ecc_op_queue.test.cpp +++ b/cpp/src/barretenberg/proof_system/op_queue/ecc_op_queue.test.cpp @@ -1,5 +1,5 @@ -#include #include "barretenberg/proof_system/op_queue/ecc_op_queue.hpp" +#include namespace proof_system::test_flavor { TEST(ECCOpQueueTest, Basic) @@ -11,4 +11,30 @@ TEST(ECCOpQueueTest, Basic) EXPECT_EQ(op_queue.raw_ops[1].add, false); } +TEST(ECCOpQueueTest, InternalAccumulatorCorrectness) +{ + using point = barretenberg::g1::affine_element; + using scalar = barretenberg::fr; + + // Compute a simple point accumulation natively + auto P1 = point::random_element(); + auto P2 = point::random_element(); + auto z = scalar::random_element(); + auto P_expected = P1 + P2 * z; + + // Add the same operations to the ECC op queue; the native computation is performed under the hood. + ECCOpQueue op_queue; + op_queue.add_accumulate(P1); + op_queue.mul_accumulate(P2, z); + + // The correct result should now be stored in the accumulator within the op queue + EXPECT_EQ(op_queue.get_accumulator(), P_expected); + + // Equivalently, we can check that the equality op returns the correct point + EXPECT_EQ(op_queue.eq(), P_expected); + + // Adding an equality op should reset the accumulator to zero (the point at infinity) + EXPECT_TRUE(op_queue.get_accumulator().is_point_at_infinity()); +} + } // namespace proof_system::test_flavor