Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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,111 @@
#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"
#include <deque>
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 as a std::deque 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 Subtable = std::vector<OpFormat>;
std::vector<Subtable> table;

public:
size_t size() const
{
size_t total = 0;
for (const auto& subtable : table) {
total += subtable.size();
}
return total;
}

auto& get() const { return table; }

void push(const OpFormat& op) { table.front().push_back(op); }

void create_new_subtable(size_t size_hint = 0)
{
std::vector<OpFormat> new_subtable;
new_subtable.reserve(size_hint);
table.insert(table.begin(), std::move(new_subtable));
}

// const version of operator[]
const OpFormat& operator[](size_t index) const
{
ASSERT(index < size());
// simple linear search to find the correct subtable
for (const auto& subtable : table) {
if (index < subtable.size()) {
return subtable[index]; // found the correct subtable
}
index -= subtable.size(); // move to the next subtable
}
return table.front().front(); // should never reach here
}
};

using RawEccOpsTable = EccOpsTable<eccvm::VMOperation<curve::BN254::Group>>;

/**
* @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 {
using Curve = curve::BN254;
using Fr = Curve::ScalarField;
using UltraOpsTable = EccOpsTable<UltraOp>;

UltraOpsTable table;
static constexpr size_t TABLE_WIDTH = 4;

public:
size_t size() const { return table.size(); }
void create_new_subtable(size_t size_hint = 0) { table.create_new_subtable(size_hint); }
void push(const UltraOp& op) { table.push(op); }

/**
* @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 (const auto& subtable : table.get()) {
for (const auto& op : subtable) {
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++;
}
}
}
};

} // namespace bb
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
#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
for (size_t i = 0; i < expected_num_ops; ++i) {
EXPECT_EQ(expected_raw_ops_table.raw_ops[i], raw_ops_table[i]);
}
}