Skip to content

Commit

Permalink
feat: Add Goblin Ultra Circuit builder (AztecProtocol/barretenberg#587)
Browse files Browse the repository at this point in the history
Co-authored-by: codygunton <codygunton@gmail.com>
  • Loading branch information
ledwards2225 and codygunton authored Jul 19, 2023
1 parent a8aee43 commit 427ab87
Show file tree
Hide file tree
Showing 6 changed files with 369 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ template <typename FF> struct poly_triple_ {

using poly_triple = poly_triple_<barretenberg::fr>;

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 <typename B> inline void read(B& buf, poly_triple& constraint)
{
using serialize::read;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#include "barretenberg/crypto/generators/generator_data.hpp"
#include "ultra_circuit_builder.hpp"
#include <gtest/gtest.h>

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<Point> points;
std::vector<Scalar> 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
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,154 @@ template <typename FF> uint32_t UltraCircuitBuilder_<FF>::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 <typename FF>
g1::affine_element UltraCircuitBuilder_<FF>::batch_mul(const std::vector<g1::affine_element>& points,
const std::vector<fr>& 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 <typename FF> void UltraCircuitBuilder_<FF>::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 <typename FF>
void UltraCircuitBuilder_<FF>::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 <typename FF> barretenberg::g1::affine_element UltraCircuitBuilder_<FF>::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 <typename FF>
void UltraCircuitBuilder_<FF>::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 <typename FF>
ecc_op_tuple UltraCircuitBuilder_<FF>::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 <typename FF> void UltraCircuitBuilder_<FF>::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 <typename FF> plookup::BasicTable& UltraCircuitBuilder_<FF>::get_table(const plookup::BasicTableId id)
{
for (plookup::BasicTable& table : lookup_tables) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -35,6 +36,12 @@ template <typename FF> class UltraCircuitBuilder_ : public CircuitBuilderBase<ar
static constexpr size_t NUM_RESERVED_GATES = 4;
// number of gates created per non-native field operation in process_non_native_field_multiplications
static constexpr size_t GATES_PER_NON_NATIVE_FIELD_MULTIPLICATION_ARITHMETIC = 7;

size_t num_ecc_op_gates = 0; // number of ecc op "gates" (rows); these are placed at the start of the circuit

// Stores record of ecc operations and performs corresponding native operations internally
ECCOpQueue op_queue;

struct non_native_field_witnesses {
// first 4 array elements = limbs
// 5th element = prime basis limb
Expand Down Expand Up @@ -532,6 +539,14 @@ template <typename FF> class UltraCircuitBuilder_ : public CircuitBuilderBase<ar
WireVector& w_o = std::get<2>(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<WireVector, arithmetization::Ultra<FF>::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;
Expand Down Expand Up @@ -688,6 +703,20 @@ template <typename FF> class UltraCircuitBuilder_ : public CircuitBuilderBase<ar

uint32_t put_constant_variable(const FF& variable);

/**
* ** Goblin Methods ** (methods for add ecc op queue gates)
**/
void queue_ecc_add_accum(const g1::affine_element& point);
void queue_ecc_mul_accum(const g1::affine_element& point, const fr& scalar);
g1::affine_element queue_ecc_eq();
g1::affine_element batch_mul(const std::vector<g1::affine_element>& points, const std::vector<fr>& 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; }

/**
Expand Down
Loading

0 comments on commit 427ab87

Please # to comment.