Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#pragma once

#include "barretenberg/ecc/curves/bn254/bn254.hpp"
#include "barretenberg/eccvm/eccvm_builder_types.hpp"
#include "barretenberg/stdlib_circuit_builders/op_queue/ecc_op_queue.hpp"
namespace bb {

/**
* @brief A table of ECC operations
* @details The table is constructed via concatenation of subtables of ECC operations. The table concatentation protocol
* (Merge protocol) requires that the concatenation be achieved via PRE-pending successive tables. To avoid the need for
* expensive memory reallocations associated with physically prepending, the subtables are stored in a linked list that
* can be traversed to reconstruct the columns of the aggregate tables as needed (e.g. in corresponding polynomials).
*
* @tparam OpFormat Format of the ECC operations stored in the table
*/
template <typename OpFormat> class EccOpsTable {
using Curve = curve::BN254;
using Fr = Curve::ScalarField;

protected:
// A segment of the table. (In practice, each subtable stores the operations of a single circuit)
struct Subtable {
std::vector<OpFormat> data;
std::unique_ptr<Subtable> prev; // the previously constructed subtable in the linked list

Subtable(size_t size_hint = 0) { data.reserve(size_hint); }

size_t size() const { return data.size(); }
};

std::unique_ptr<Subtable> current_subtable = nullptr; // the subtable to which new ops are appended

public:
size_t size() const
{
size_t total_size = 0;
for (auto* subtable = current_subtable.get(); subtable != nullptr; subtable = subtable->prev.get()) {
total_size += subtable->size();
}
return total_size;
}

// An iterator to allow range based iteration over the full table
class Iterator {
Subtable* subtable = nullptr;
size_t index = 0;
size_t subtable_size = 0;

public:
Iterator() = default;
Iterator(Subtable* sub, size_t idx)
: subtable(sub)
, index(idx)
, subtable_size(sub ? sub->size() : 0)
{}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: could get away without storing size

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yeah I did this to save on the size() call every time in a hot loop but maybe thats overkill. I would def prefer just calling size()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yeah so what's going to happen:

  • we are reading a pointer, offsetting it (basically free), then reading a field
    vs
  • we are reading a field
    the pointer we are dereferencing will be in cache, so it's a trivial difference

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We are also returning a rather big struct, which, fair if we are reading one field from it might be a hot loop, but if we are doing anything with a field the loop overhead is not that big. Comes down to how many times we will be iterating during a proving lifetime, I guess. Could be worth the cycles potentially


OpFormat operator*() { return subtable->data[index]; }

Iterator& operator++()
{
if (++index < subtable_size) { // return row within the current subtable
return *this;
}
if (subtable->prev) { // update to next subtable
subtable = subtable->prev.get();
subtable_size = subtable->size();
index = 0;
} else {
*this = Iterator(); // end of iteration
}
return *this;
}

bool operator!=(const Iterator& other) const { return subtable != other.subtable || index != other.index; }
};

Iterator begin() { return Iterator(current_subtable.get(), 0); }
Iterator end() { return {}; } // end marked by default iterator

void create_new_subtable(size_t size_hint = 0)
{
auto new_subtable = std::make_unique<Subtable>(size_hint); // new subtable
new_subtable->prev = std::move(current_subtable); // old one is the prev of the new one
current_subtable = std::move(new_subtable); // set the new one to current
}

// push a new operation to the current subtable
void push(const OpFormat& op) { current_subtable->data.push_back(op); }
};

/**
* @brief Stores a table of elliptic curve operations represented in the Ultra format
* @details An ECC operation OP involing point P(X,Y) and scalar z is represented in the Ultra format as a tuple of the
* form {OP, X_lo, X_hi, Y_lo, Y_hi, z1, z2}, where the coordinates are split into hi and lo limbs and z1, z2 are the
* endomorphism scalars associated with z. Because the Ultra/Mega arithmetization utilizes 4 wires, each op occupies two
* rows in a width-4 execution trace, arranged as follows:
*
* OP | X_lo | X_hi | Y_lo
* 0 | Y_hi | z1 | z2
*
* 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 : public EccOpsTable<UltraOp> {
static constexpr size_t TABLE_WIDTH = 4;
using Curve = curve::BN254;
using Fr = Curve::ScalarField;

public:
/**
* @brief Populate the provided array of columns with the width-4 representation of the table data
* @todo multithreaded this functionality
* @param target_columns
*/
void populate_column_data(std::array<std::span<Fr>, TABLE_WIDTH>& target_columns)
{
size_t i = 0;
for (auto* subtable = current_subtable.get(); subtable != nullptr; subtable = subtable->prev.get()) {
for (const auto& op : subtable->data) {
target_columns[0][i] = op.op;
target_columns[1][i] = op.x_lo;
target_columns[2][i] = op.x_hi;
target_columns[3][i] = op.y_lo;
i++;
target_columns[0][i] = 0; // only the first 'op' field is utilized
target_columns[1][i] = op.y_hi;
target_columns[2][i] = op.z_1;
target_columns[3][i] = op.z_2;
i++;
}
}
}
};

/**
* @brief The table of ECC ops used by the ECCVM circuit builder
* @note just an alias for a specialized EccOpsTable
*/
using RawEccOpsTable = EccOpsTable<eccvm::VMOperation<curve::BN254::Group>>;

} // namespace bb
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
#include "barretenberg/stdlib_circuit_builders/op_queue/ecc_ops_table.hpp"
#include "barretenberg/common/zip_view.hpp"
#include "barretenberg/polynomials/polynomial.hpp"
#include "barretenberg/stdlib_circuit_builders/op_queue/ecc_op_queue.hpp"
#include <gtest/gtest.h>

#include <ranges>

using namespace bb;

class EccOpsTableTest : public ::testing::Test {
using Curve = curve::BN254;
using Scalar = fr;
using ECCVMOperation = eccvm::VMOperation<Curve::Group>;

public:
// Mock ultra ops table that constructs a concatenated table from successively prepended subtables
struct MockUltraOpsTable {
std::array<std::vector<Scalar>, 4> columns;
void append(const UltraOp& op)
{
columns[0].push_back(op.op);
columns[1].push_back(op.x_lo);
columns[2].push_back(op.x_hi);
columns[3].push_back(op.y_lo);

columns[0].push_back(0);
columns[1].push_back(op.y_hi);
columns[2].push_back(op.z_1);
columns[3].push_back(op.z_2);
}

// Construct the= ultra ops table such that the subtables appear in reverse order, as if prepended
MockUltraOpsTable(const auto& subtable_ops)
{
for (auto& ops : std::ranges::reverse_view(subtable_ops)) {
for (const auto& op : ops) {
append(op);
}
}
}

size_t size() const { return columns[0].size(); }
};

// Mock raw ops table that constructs a concatenated table from successively prepended subtables
struct MockRawOpsTable {
std::vector<ECCVMOperation> raw_ops;
void append(const ECCVMOperation& op) { raw_ops.push_back(op); }

MockRawOpsTable(const auto& subtable_ops)
{
for (auto& ops : std::ranges::reverse_view(subtable_ops)) {
for (const auto& op : ops) {
append(op);
}
}
}
};

static UltraOp random_ultra_op()
{
UltraOp op;
op.op_code = NULL_OP;
op.op = Scalar::random_element();
op.x_lo = Scalar::random_element();
op.x_hi = Scalar::random_element();
op.y_lo = Scalar::random_element();
op.y_hi = Scalar::random_element();
op.z_1 = Scalar::random_element();
op.z_2 = Scalar::random_element();
op.return_is_infinity = false;
return op;
}

static ECCVMOperation random_raw_op()
{
return ECCVMOperation{ .mul = true,
.base_point = curve::BN254::Group::affine_element::random_element(),
.z1 = uint256_t(Scalar::random_element()),
.z2 = uint256_t(Scalar::random_element()),
.mul_scalar_full = Scalar::random_element() };
}
};

// Ensure UltraOpsTable correctly constructs a concatenated table from successively prepended subtables
TEST(EccOpsTableTest, UltraOpsTable)
{
using Fr = fr;

constexpr size_t NUM_ROWS_PER_OP = 2; // Each ECC op is represented by two rows in the ultra ops table

// Construct sets of ultra ops, each representing those added by a single circuit
const size_t NUM_SUBTABLES = 3;
std::array<std::vector<UltraOp>, NUM_SUBTABLES> subtable_ultra_ops;
std::array<size_t, NUM_SUBTABLES> subtable_op_counts = { 4, 2, 7 };
for (auto [subtable_ops, op_count] : zip_view(subtable_ultra_ops, subtable_op_counts)) {
for (size_t i = 0; i < op_count; ++i) {
subtable_ops.push_back(EccOpsTableTest::random_ultra_op());
}
}

// Construct the mock ultra ops table which contains the subtables ordered in reverse (as if prepended)
EccOpsTableTest::MockUltraOpsTable expected_ultra_ops_table(subtable_ultra_ops);

// Construct the concatenated table internal to the op queue
UltraEccOpsTable ultra_ops_table;
for (const auto& subtable_ops : subtable_ultra_ops) {
ultra_ops_table.create_new_subtable();
for (const auto& op : subtable_ops) {
ultra_ops_table.push(op);
}
}

// Check that the ultra ops table internal to the op queue has the correct size
auto expected_num_ops = std::accumulate(subtable_op_counts.begin(), subtable_op_counts.end(), size_t(0));
EXPECT_EQ(ultra_ops_table.size(), expected_num_ops);

// Construct polynomials corresponding to the columns of the ultra ops table
const size_t expected_table_size = expected_num_ops * NUM_ROWS_PER_OP;
std::array<Polynomial<Fr>, 4> ultra_ops_table_polynomials;
std::array<std::span<fr>, 4> column_spans;
for (auto [column_span, column] : zip_view(column_spans, ultra_ops_table_polynomials)) {
column = Polynomial<Fr>(expected_table_size);
column_span = column.coeffs();
}
ultra_ops_table.populate_column_data(column_spans);

// Check that the ultra ops table constructed by the op queue matches the expected table
for (auto [expected_column, poly] : zip_view(expected_ultra_ops_table.columns, ultra_ops_table_polynomials)) {
for (auto [expected_value, value] : zip_view(expected_column, poly.coeffs())) {
EXPECT_EQ(expected_value, value);
}
}
}

// Ensure RawOpsTable correctly constructs a concatenated table from successively prepended subtables
TEST(EccOpsTableTest, RawOpsTable)
{
using ECCVMOperation = bb::eccvm::VMOperation<curve::BN254::Group>;

// Construct sets of raw ops, each representing those added by a single circuit
const size_t NUM_SUBTABLES = 3;
std::array<std::vector<ECCVMOperation>, NUM_SUBTABLES> subtable_raw_ops;
std::array<size_t, NUM_SUBTABLES> subtable_op_counts = { 4, 2, 7 };
for (auto [subtable_ops, op_count] : zip_view(subtable_raw_ops, subtable_op_counts)) {
for (size_t i = 0; i < op_count; ++i) {
subtable_ops.push_back(EccOpsTableTest::random_raw_op());
}
}

// Construct the mock raw ops table which contains the subtables ordered in reverse (as if prepended)
EccOpsTableTest::MockRawOpsTable expected_raw_ops_table(subtable_raw_ops);

// Construct the concatenated raw ops table
RawEccOpsTable raw_ops_table;
for (const auto& subtable_ops : subtable_raw_ops) {
raw_ops_table.create_new_subtable();
for (const auto& op : subtable_ops) {
raw_ops_table.push(op);
}
}

// Check that the table has the correct size
auto expected_num_ops = std::accumulate(subtable_op_counts.begin(), subtable_op_counts.end(), size_t(0));
EXPECT_EQ(raw_ops_table.size(), expected_num_ops);

// Check that the table matches the manually constructed mock table
size_t idx = 0;
for (const auto& op : raw_ops_table) {
EXPECT_EQ(expected_raw_ops_table.raw_ops[idx++], op);
}
}