Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions barretenberg/cpp/src/barretenberg/op_queue/ecc_op_queue.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
#include "barretenberg/op_queue/ecc_ops_table.hpp"
#include "barretenberg/op_queue/eccvm_row_tracker.hpp"
#include "barretenberg/polynomials/polynomial.hpp"
#include "barretenberg/stdlib/primitives/bigfield/constants.hpp"
namespace bb {

/**
Expand All @@ -28,15 +27,15 @@ class ECCOpQueue {
// The operations written to the queue are also performed natively; the result is stored in accumulator
Point accumulator = point_at_infinity;

static constexpr size_t DEFAULT_NON_NATIVE_FIELD_LIMB_BITS = stdlib::NUM_LIMB_BITS_IN_FIELD_SIMULATION;

EccvmOpsTable eccvm_ops_table; // table of ops in the ECCVM format
UltraEccOpsTable ultra_ops_table; // table of ops in the Ultra-arithmetization format

// Storage for the reconstructed eccvm ops table in contiguous memory. (Intended to be constructed once and for all
// prior to ECCVM construction to avoid repeated prepending of subtables in physical memory).
std::vector<ECCVMOperation> eccvm_ops_reconstructed;

std::vector<UltraOp> ultra_ops_reconstructed; // Storage for the reconstructed ultra ops table in contiguous memory

// Tracks number of muls and size of eccvm in real time as the op queue is updated
EccvmRowTracker eccvm_row_tracker;

Expand Down Expand Up @@ -71,6 +70,7 @@ class ECCOpQueue {

// Reconstruct the full table of eccvm ops in contiguous memory from the independent subtables
void construct_full_eccvm_ops_table() { eccvm_ops_reconstructed = eccvm_ops_table.get_reconstructed(); }
void construct_full_ultra_ops_table() { ultra_ops_reconstructed = ultra_ops_table.get_reconstructed(); }

size_t get_ultra_ops_table_num_rows() const { return ultra_ops_table.ultra_table_size(); }
size_t get_current_ultra_ops_subtable_num_rows() const { return ultra_ops_table.current_ultra_subtable_size(); }
Expand All @@ -84,6 +84,14 @@ class ECCOpQueue {
return eccvm_ops_reconstructed;
}

std::vector<UltraOp>& get_ultra_ops()
{
if (ultra_ops_reconstructed.empty()) {
construct_full_ultra_ops_table();
}
return ultra_ops_reconstructed;
}

/**
* @brief Get the number of rows in the 'msm' column section, for all msms in the circuit
*/
Expand Down Expand Up @@ -239,7 +247,7 @@ class ECCOpQueue {
ultra_op.op_code = op_code;

// Decompose point coordinates (Fq) into hi-lo chunks (Fr)
const size_t CHUNK_SIZE = 2 * DEFAULT_NON_NATIVE_FIELD_LIMB_BITS;
const size_t CHUNK_SIZE = 2 * stdlib::NUM_LIMB_BITS_IN_FIELD_SIMULATION;
uint256_t x_256(point.x);
uint256_t y_256(point.y);
ultra_op.return_is_infinity = point.is_point_at_infinity();
Expand Down
51 changes: 20 additions & 31 deletions barretenberg/cpp/src/barretenberg/op_queue/ecc_ops_table.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "barretenberg/ecc/curves/bn254/bn254.hpp"
#include "barretenberg/eccvm/eccvm_builder_types.hpp"
#include "barretenberg/polynomials/polynomial.hpp"
#include "barretenberg/stdlib/primitives/bigfield/constants.hpp"
#include <deque>
namespace bb {

Expand Down Expand Up @@ -43,6 +44,18 @@ struct UltraOp {
Fr z_1;
Fr z_2;
bool return_is_infinity;

std::array<uint256_t, 2> get_base_point_standard_form() const
{
uint256_t x = (uint256_t(x_hi) << 2 * stdlib::NUM_LIMB_BITS_IN_FIELD_SIMULATION) + uint256_t(x_lo);
uint256_t y = (uint256_t(y_hi) << 2 * stdlib::NUM_LIMB_BITS_IN_FIELD_SIMULATION) + uint256_t(y_lo);

if (return_is_infinity) {
x = 0;
y = 0;
}
return { x, y };
}
};

template <typename CycleGroup> struct VMOperation {
Expand All @@ -52,24 +65,6 @@ template <typename CycleGroup> struct VMOperation {
uint256_t z2 = 0;
typename CycleGroup::subgroup_field mul_scalar_full = 0;
bool operator==(const VMOperation<CycleGroup>& other) const = default;

/**
* @brief Get the point in standard form i.e. as two coordinates x and y in the base field or as a point at
* infinity whose coordinates are set to (0,0).
*
* @details These are represented as uint265_t to make chunking easier, the function being used in translator
* where each coordinate is chunked to efficiently be represented in the scalar field.
*/
std::array<uint256_t, 2> get_base_point_standard_form() const
{
uint256_t x(base_point.x);
uint256_t y(base_point.y);
if (base_point.is_point_at_infinity()) {
x = 0;
y = 0;
}
return { x, y };
}
};
using ECCVMOperation = VMOperation<curve::BN254::Group>;

Expand Down Expand Up @@ -141,7 +136,7 @@ template <typename OpFormat> class EccOpsTable {
}
};

/***
/**
* @brief A VM operation is represented as one row with 6 columns in the ECCVM version of the Op Queue.
* | OP | X | Y | z_1 | z_2 | mul_scalar_full |
*/
Expand All @@ -160,34 +155,28 @@ using EccvmOpsTable = EccOpsTable<ECCVMOperation>;
* The table data is stored in the UltraOp tuple format but is converted to four columns of Fr scalars for use in the
* polynomials in the proving system.
*/
class UltraEccOpsTable {
class UltraEccOpsTable : public EccOpsTable<UltraOp> {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems more natural for this to be a derived class of the generic EccOpsTable class and to me it looks like we are simplifying the code but curious to hear opinions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah so the canonical question in situations like this is which one of these is more logically correct: (1) UltraEccOpsTable has an EccOpsTable, or (2) UltraEccOpsTable is an EccOpsTable. If (1) is true then we should use composition (as it is now). If (2), we should use inheritance. In this case it's pretty clear to me that (2) is not true, but it may be the case that the implementation of (1) is not as clean as it should be. (E.g. in principle UltraEccOpsTable is a class for managing the width-4 representation of the table whileEccOpsTable contains the more generic UltraOp table - the former is not a subclass of the latter). Inheritance can make things opaque so I think it should be avoided unless its very clear that (2) applies. This is something @ludamad has convinced me of (and maybe even steered me away from in the initial implementation of this class). In any case, the syntactic advantage we appear to get from employing inheritance seems minimal - maybe another sign that its not appropriate!

Anyway, there is definitely room for improvement. But I would encourage you to think about how to make the composition model cleaner rather than resorting to inheritance here.

@maramihali maramihali Apr 17, 2025

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum - I see your point. In my head the UltraEccOpsTable was just adding extra functionality to the generic EccOpsTable so the inheritance seemed natural, but as I'm writing this I'm thinking this is not functionality of the EccOpsTable instantiated on UltraOps but rather functionality using the EccOpsTable<UltraOp> class so I think I agree with you and perhaps (one of) the culprit here is the name UltraEccOpsTable, will think how to rework this more

public:
static constexpr size_t TABLE_WIDTH = 4; // dictated by the number of wires in the Ultra arithmetization
static constexpr size_t NUM_ROWS_PER_OP = 2; // A single ECC op is split across two width-4 rows

private:
using Curve = curve::BN254;
using Fr = Curve::ScalarField;
using UltraOpsTable = EccOpsTable<UltraOp>;
using TableView = std::array<std::span<Fr>, TABLE_WIDTH>;
using ColumnPolynomials = std::array<Polynomial<Fr>, TABLE_WIDTH>;

UltraOpsTable table;

public:
size_t size() const { return table.size(); }
size_t ultra_table_size() const { return table.size() * NUM_ROWS_PER_OP; }
size_t current_ultra_subtable_size() const { return table.get()[0].size() * NUM_ROWS_PER_OP; }
size_t ultra_table_size() const { return size() * NUM_ROWS_PER_OP; }
size_t current_ultra_subtable_size() const { return get()[0].size() * NUM_ROWS_PER_OP; }
size_t previous_ultra_table_size() const { return (ultra_table_size() - current_ultra_subtable_size()); }
void create_new_subtable(size_t size_hint = 0) { table.create_new_subtable(size_hint); }
void push(const UltraOp& op) { table.push(op); }

// Construct the columns of the full ultra ecc ops table
ColumnPolynomials construct_table_columns() const
{
const size_t poly_size = ultra_table_size();
const size_t subtable_start_idx = 0; // include all subtables
const size_t subtable_end_idx = table.num_subtables();
const size_t subtable_end_idx = num_subtables();

return construct_column_polynomials_from_subtables(poly_size, subtable_start_idx, subtable_end_idx);
}
Expand All @@ -197,7 +186,7 @@ class UltraEccOpsTable {
{
const size_t poly_size = previous_ultra_table_size();
const size_t subtable_start_idx = 1; // exclude the 0th subtable
const size_t subtable_end_idx = table.num_subtables();
const size_t subtable_end_idx = num_subtables();

return construct_column_polynomials_from_subtables(poly_size, subtable_start_idx, subtable_end_idx);
}
Expand Down Expand Up @@ -230,7 +219,7 @@ class UltraEccOpsTable {

size_t i = 0;
for (size_t subtable_idx = subtable_start_idx; subtable_idx < subtable_end_idx; ++subtable_idx) {
const auto& subtable = table.get()[subtable_idx];
const auto& subtable = get()[subtable_idx];
for (const auto& op : subtable) {
column_polynomials[0].at(i) = op.op_code.value();
column_polynomials[1].at(i) = op.x_lo;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -544,89 +544,52 @@ void TranslatorCircuitBuilder::create_accumulation_gate(const AccumulationInput
bb::constexpr_for<0, TOTAL_COUNT, 1>([&]<size_t i>() { ASSERT(std::get<i>(wires).size() == num_gates); });
}

/**
* @brief Given an ECCVM operation, previous accumulator and necessary challenges, compute witnesses for one
* accumulation
*
* @tparam Fq
* @return TranslatorCircuitBuilder::AccumulationInput
*/

TranslatorCircuitBuilder::AccumulationInput TranslatorCircuitBuilder::compute_witness_values_for_one_ecc_op(
const ECCVMOperation& ecc_op,
const Fq previous_accumulator,
const Fq batching_challenge_v,
const Fq evaluation_input_x)
{
// Get the Opcode value
Fr op(ecc_op.op_code.value());
Fr p_x_lo(0);
Fr p_x_hi(0);
Fr p_y_lo(0);
Fr p_y_hi(0);

// Split P.x and P.y into their representations in bn254 transcript
// if we have a point at infinity, set x/y to zero
// in the biggroup_goblin class we use `assert_equal` statements to validate
// the original in-circuit coordinate values are also zero
const auto [x_256, y_256] = ecc_op.get_base_point_standard_form();

p_x_lo = Fr(uint256_t(x_256).slice(0, 2 * NUM_LIMB_BITS));
p_x_hi = Fr(uint256_t(x_256).slice(2 * NUM_LIMB_BITS, 4 * NUM_LIMB_BITS));
p_y_lo = Fr(uint256_t(y_256).slice(0, 2 * NUM_LIMB_BITS));
p_y_hi = Fr(uint256_t(y_256).slice(2 * NUM_LIMB_BITS, 4 * NUM_LIMB_BITS));

// Generate the full witness values
return generate_witness_values(op,
p_x_lo,
p_x_hi,
p_y_lo,
p_y_hi,
Fr(ecc_op.z1),
Fr(ecc_op.z2),
previous_accumulator,
batching_challenge_v,
evaluation_input_x);
}

// TODO(https://github.com/AztecProtocol/barretenberg/issues/1266): Evaluate whether this method can reuse existing data
// in the op queue for improved efficiency
void TranslatorCircuitBuilder::feed_ecc_op_queue_into_circuit(const std::shared_ptr<ECCOpQueue> ecc_op_queue)
{
using Fq = bb::fq;
const auto& eccvm_ops = ecc_op_queue->get_eccvm_ops();
const auto& ultra_ops = ecc_op_queue->get_ultra_ops();
std::vector<Fq> accumulator_trace;
Fq current_accumulator(0);
if (eccvm_ops.empty()) {
if (ultra_ops.empty()) {
return;
}
// Rename for ease of use
auto x = evaluation_input_x;
auto v = batching_challenge_v;

// We need to precompute the accumulators at each step, because in the actual circuit we compute the values starting
// from the later indices. We need to know the previous accumulator to create the gate
for (size_t i = 0; i < eccvm_ops.size(); i++) {
const auto& ecc_op = eccvm_ops[eccvm_ops.size() - 1 - i];
current_accumulator *= x;
const auto [x_256, y_256] = ecc_op.get_base_point_standard_form();
for (size_t i = 0; i < ultra_ops.size(); i++) {
const auto& ultra_op = ultra_ops[ultra_ops.size() - 1 - i];
current_accumulator *= evaluation_input_x;
const auto [x_256, y_256] = ultra_op.get_base_point_standard_form();
current_accumulator +=
(Fq(ecc_op.op_code.value()) + v * (x_256 + v * (y_256 + v * (ecc_op.z1 + v * ecc_op.z2))));
Fq(ultra_op.op_code.value()) +
batching_challenge_v *
(x_256 + batching_challenge_v *
(y_256 + batching_challenge_v *
(uint256_t(ultra_op.z_1) + batching_challenge_v * uint256_t(ultra_op.z_2))));
accumulator_trace.push_back(current_accumulator);
}

// We don't care about the last value since we'll recompute it during witness generation anyway
accumulator_trace.pop_back();

for (const auto& eccvm_op : eccvm_ops) {
for (const auto& ultra_op : ultra_ops) {
Fq previous_accumulator = 0;
// Pop the last value from accumulator trace and use it as previous accumulator
if (!accumulator_trace.empty()) {
previous_accumulator = accumulator_trace.back();
accumulator_trace.pop_back();
}
// Compute witness values
auto one_accumulation_step = compute_witness_values_for_one_ecc_op(eccvm_op, previous_accumulator, v, x);
AccumulationInput one_accumulation_step = generate_witness_values(ultra_op.op_code.value(),
ultra_op.x_lo,
ultra_op.x_hi,
ultra_op.y_lo,
ultra_op.y_hi,
ultra_op.z_1,
ultra_op.z_2,
previous_accumulator,
batching_challenge_v,
evaluation_input_x);

// And put them into the wires
create_accumulation_gate(one_accumulation_step);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -469,10 +469,6 @@ class TranslatorCircuitBuilder : public CircuitBuilderBase<bb::fr> {
const Fq previous_accumulator,
const Fq batching_challenge_v,
const Fq evaluation_input_x);
static AccumulationInput compute_witness_values_for_one_ecc_op(const ECCVMOperation& ecc_op,
const Fq previous_accumulator,
const Fq batching_challenge_v,
const Fq evaluation_input_x);
};

} // namespace bb
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,16 @@ TEST(TranslatorCircuitBuilder, SeveralOperationCorrectness)
// Get an inverse
Fq x_inv = x.invert();
// Compute the batched evaluation of polynomials (multiplying by inverse to go from lower to higher)
const auto& eccvm_ops = op_queue->get_eccvm_ops();
for (const auto& ecc_op : eccvm_ops) {
const auto& ultra_ops = op_queue->get_ultra_ops();
for (const auto& ecc_op : ultra_ops) {
op_accumulator = op_accumulator * x_inv + ecc_op.op_code.value();
const auto [x_u256, y_u256] = ecc_op.get_base_point_standard_form();
p_x_accumulator = p_x_accumulator * x_inv + x_u256;
p_y_accumulator = p_y_accumulator * x_inv + y_u256;
z_1_accumulator = z_1_accumulator * x_inv + ecc_op.z1;
z_2_accumulator = z_2_accumulator * x_inv + ecc_op.z2;
z_1_accumulator = z_1_accumulator * x_inv + uint256_t(ecc_op.z_1);
z_2_accumulator = z_2_accumulator * x_inv + uint256_t(ecc_op.z_2);
}
Fq x_pow = x.pow(eccvm_ops.size() - 1);
Fq x_pow = x.pow(ultra_ops.size() - 1);

// Multiply by an appropriate power of x to get rid of the inverses
Fq result = ((((z_2_accumulator * batching_challenge + z_1_accumulator) * batching_challenge + p_y_accumulator) *
Expand Down