diff --git a/barretenberg/cpp/src/barretenberg/stdlib_circuit_builders/op_queue/ecc_ops_table.hpp b/barretenberg/cpp/src/barretenberg/stdlib_circuit_builders/op_queue/ecc_ops_table.hpp new file mode 100644 index 000000000000..890e117a478a --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/stdlib_circuit_builders/op_queue/ecc_ops_table.hpp @@ -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 +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 class EccOpsTable { + using Subtable = std::vector; + std::vector 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 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>; + +/** + * @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; + + 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, 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 diff --git a/barretenberg/cpp/src/barretenberg/stdlib_circuit_builders/op_queue/ecc_ops_table.test.cpp b/barretenberg/cpp/src/barretenberg/stdlib_circuit_builders/op_queue/ecc_ops_table.test.cpp new file mode 100644 index 000000000000..b8ccfe4c77bf --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/stdlib_circuit_builders/op_queue/ecc_ops_table.test.cpp @@ -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 + +#include + +using namespace bb; + +class EccOpsTableTest : public ::testing::Test { + using Curve = curve::BN254; + using Scalar = fr; + using ECCVMOperation = eccvm::VMOperation; + + public: + // Mock ultra ops table that constructs a concatenated table from successively prepended subtables + struct MockUltraOpsTable { + std::array, 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 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, NUM_SUBTABLES> subtable_ultra_ops; + std::array 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, 4> ultra_ops_table_polynomials; + std::array, 4> column_spans; + for (auto [column_span, column] : zip_view(column_spans, ultra_ops_table_polynomials)) { + column = Polynomial(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; + + // Construct sets of raw ops, each representing those added by a single circuit + const size_t NUM_SUBTABLES = 3; + std::array, NUM_SUBTABLES> subtable_raw_ops; + std::array 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]); + } +}