Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
ecc_ops_table.hpp
Go to the documentation of this file.
1// === AUDIT STATUS ===
2// internal: { status: Complete, auditors: [Raju], commit: }
3// external_1: { status: not started, auditors: [], commit: }
4// external_2: { status: not started, auditors: [], commit: }
5// =====================
6
7#pragma once
8
15#include <vector>
16namespace bb {
17
18// Constants determining the structure of the zk columns. These must match the structure expected by Translator.
19static constexpr size_t ECC_NUM_RANDOM_OPS_START = 3;
20static constexpr size_t ECC_NUM_NO_OPS_START = 1;
21static constexpr size_t ECC_NUM_HIDING_OPS_START = 1;
22
37struct EccOpCode {
39 bool add = false;
40 bool mul = false;
41 bool eq = false;
42 bool reset = false;
43 bool operator==(const EccOpCode& other) const = default;
44
45 bool is_random_op = false;
48
49 // encodes add*8 + mul*4 + eq*2 + reset*1
50 [[nodiscard]] uint32_t value() const
51 {
52 if (is_random_op) {
53 throw_or_abort("EccOpCode::value() should not be called on a random op");
54 }
55 auto res = static_cast<uint32_t>(add);
56 res += res;
57 res += static_cast<uint32_t>(mul);
58 res += res;
59 res += static_cast<uint32_t>(eq);
60 res += res;
61 res += static_cast<uint32_t>(reset);
62 return res;
63 }
64};
65
66struct UltraOp {
77
78 bool operator==(const UltraOp& other) const = default;
79
86 {
88 return { Fq(0), Fq(0) };
89 }
90 auto x = Fq((uint256_t(x_hi) << 2 * stdlib::NUM_LIMB_BITS_IN_FIELD_SIMULATION) + uint256_t(x_lo));
91 auto y = Fq((uint256_t(y_hi) << 2 * stdlib::NUM_LIMB_BITS_IN_FIELD_SIMULATION) + uint256_t(y_lo));
92
93 return { x, y };
94 }
95};
96
99 using AffineElement = Curve::Group::affine_element;
106 bool operator==(const ECCVMOperation& other) const = default;
107};
108
116template <typename OpFormat> class EccOpsTable {
117 using Subtable = std::vector<OpFormat>;
118 std::vector<Subtable> table;
119 Subtable current_subtable; // used to store the current subtable before it is added to the table
120 public:
121 size_t size() const
122 {
124 "Current subtable should be merged before computing the size of the full table of ecc ops.");
125 size_t total = 0;
126 for (const auto& subtable : table) {
127 total += subtable.size();
128 }
129
130 return total;
131 }
132
133 size_t num_subtables() const { return table.size(); }
134 size_t get_current_subtable_size() const { return current_subtable.size(); }
135
136 auto& get() const { return table; }
137
138 void push(const OpFormat& op) { current_subtable.push_back(op); }
139
141 {
142 BB_ASSERT(current_subtable.empty(), "Cannot create a new subtable until the current subtable has been merged.");
144 }
145
146 // const operator[]. (there is no non-const version.)
147 const OpFormat& operator[](size_t index) const
148 {
150 "Current subtable should be merged before attempting to index into the full table.");
152 // simple linear search to find the correct subtable
153 for (const auto& subtable : table) {
154 if (index < subtable.size()) {
155 return subtable[index]; // found the correct subtable
156 }
157 index -= subtable.size(); // move to the next subtable
158 }
159 BB_ASSERT(
160 false,
161 "Unreachable: something has gone wrong with the subtable sizes, which do not add up to the table size.");
162 // Unreachable
163 return table.front().front();
164 }
165
166 // highly inefficient copy-based reconstruction of the table for use in ECCVM/Translator. Used once at the end of an
167 // IVC.
168 std::vector<OpFormat> get_reconstructed() const
169 {
171 "current subtable should be merged before reconstructing the full table of operations.");
172
173 std::vector<OpFormat> reconstructed_table;
174 reconstructed_table.reserve(size());
175 for (const auto& subtable : table) {
176 for (const auto& op : subtable) {
177 reconstructed_table.push_back(op);
178 }
179 }
180 return reconstructed_table;
181 }
182
183 void merge()
184 {
185 table.push_back(std::move(current_subtable));
186 current_subtable.clear(); // clear the current subtable after merging
187 BB_ASSERT(current_subtable.empty(), "current subtable should be empty after merging. Check the merge logic.");
188 }
189};
190
196
211 public:
212 static constexpr size_t TABLE_WIDTH = 4; // dictated by the number of wires in the Ultra arithmetization
213 static constexpr size_t NUM_ROWS_PER_OP = 2; // A single ECC op is split across two width-4 rows
214 static constexpr size_t ZK_ULTRA_OPS =
215 (ECC_NUM_RANDOM_OPS_START + ECC_NUM_NO_OPS_START + ECC_NUM_HIDING_OPS_START) * NUM_ROWS_PER_OP;
216
217 // Leading-zero preamble on the APPEND subtable. Matches the appender flavor's TRACE_OFFSET, i.e. the
218 // number of leading zeros carried by its ecc_op_wire polynomial commitments. Sourced from
219 // NUM_DISABLED_ROWS_IN_SUMCHECK, which is == MegaZKFlavor::TRACE_OFFSET.
220 // Must be a multiple of NUM_ROWS_PER_OP so ops land on even row boundaries.
221 static constexpr size_t APPEND_TRACE_OFFSET = NUM_DISABLED_ROWS_IN_SUMCHECK;
222 static_assert(APPEND_TRACE_OFFSET % NUM_ROWS_PER_OP == 0);
223
233 const curve::BN254::BaseField& Py)
234 {
236 using Point = curve::BN254::AffineElement;
237
238 EccOpCode op_code{ .eq = true, .reset = true };
239 Point base_point;
240 base_point.x = Px;
241 base_point.y = Py;
242
243 constexpr size_t CHUNK_SIZE = 2 * stdlib::NUM_LIMB_BITS_IN_FIELD_SIMULATION;
244 uint256_t x_256(Px);
245 uint256_t y_256(Py);
246 UltraOp ultra_op{
247 .op_code = op_code,
248 .x_lo = Fr(x_256.slice(0, CHUNK_SIZE)),
249 .x_hi = Fr(x_256.slice(CHUNK_SIZE, CHUNK_SIZE * 2)),
250 .y_lo = Fr(y_256.slice(0, CHUNK_SIZE)),
251 .y_hi = Fr(y_256.slice(CHUNK_SIZE, CHUNK_SIZE * 2)),
252 .z_1 = Fr(0),
253 .z_2 = Fr(0),
254 .return_is_infinity = false,
255 };
256 ECCVMOperation eccvm_op{ .op_code = op_code, .base_point = base_point };
257 return { ultra_op, eccvm_op };
258 }
259
260 private:
265
267 std::vector<UltraOp> zk_ops; // ops used to mask real ops in Chonk
268
269 // Set by merge_with_fixed_append_offset to record the row offset (in NUM_ROWS_PER_OP units) at which the
270 // most recent subtable should be placed when constructing the full table polynomials. Setting this value
271 // also ensures that subsequent reconstructions/polynomial constructions include the APPEND_TRACE_OFFSET
272 // leading-zero preamble for the appended subtable, so the resulting commitments line up with the
273 // appender flavor's ecc_op_wire commitments. See chonk/README.md "Constant Merged Table Size for ZK".
275
276 public:
277 // Returns the number of ECC operations in the table
278 size_t num_ops() const { return table.size(); }
279
280 // Returns the number of rows in the Ultra execution trace (each op occupies NUM_ROWS_PER_OP rows).
281 // NOTE: this count covers the merged subtables only and EXCLUDES the ZK prefix (zk_ops, size ZK_ULTRA_OPS).
282 // Callers that need the full polynomial size (e.g. for sizing a commitment key) must add ZK_ULTRA_OPS.
283 size_t num_ultra_rows() const
284 {
286 return table.size() * NUM_ROWS_PER_OP;
287 }
288 BB_ASSERT(!table.get().empty(), "Fixed-append set but no subtables present");
289 // Last subtable starts at fixed_append_offset (in op units), preceded by APPEND_TRACE_OFFSET zero rows.
290 const size_t last_subtable_rows = table.get().back().size() * NUM_ROWS_PER_OP;
291 return (fixed_append_offset.value() * NUM_ROWS_PER_OP) + APPEND_TRACE_OFFSET + last_subtable_rows;
292 }
294 {
297 0UL,
298 "Current subtable should be merged before computing the size of table of operations up to the tail.");
299 BB_ASSERT_GT(table.num_subtables(), 1UL, "Cannot compute tail table size without at least two tables.");
300 size_t size = 0;
301 for (size_t subtable_idx = 0; subtable_idx < table.num_subtables() - 1; ++subtable_idx) {
302 size += table.get()[subtable_idx].size() * NUM_ROWS_PER_OP;
303 }
304 return size;
305 }
306 void create_new_subtable(size_t size_hint = 0) { table.create_new_subtable(size_hint); }
307 void push(const UltraOp& op) { table.push(op); }
308 bool has_fixed_append_offset() const { return fixed_append_offset.has_value(); }
309 bool has_zk_ops() const { return !zk_ops.empty(); }
310 void merge()
311 {
312 BB_ASSERT(!has_fixed_append_offset(), "Cannot perform regular merge after fixed-location append");
313 table.merge();
314 }
316 {
317 BB_ASSERT(!has_fixed_append_offset(), "Can only perform fixed-location append once");
318
319 size_t prior_subtables_size = 0;
320 for (const auto& subtable : table.get()) {
321 prior_subtables_size += subtable.size();
322 }
323 BB_ASSERT_LTE(prior_subtables_size,
324 offset,
325 "Merged table size exceeds fixed append offset. This means that there are too many ops before "
326 "the last subtable. The last subtable doesn't fit at the end of the op queue.");
327
329 table.merge();
330 }
331
333
334 std::vector<UltraOp> get_no_zk_reconstructed_ultra_ops() const
335 {
336 return get_reconstructed(/*include_zk_ops=*/false);
337 }
338
339 std::vector<UltraOp> get_zk_reconstructed_ultra_ops() const { return get_reconstructed(/*include_zk_ops=*/true); }
340
341 private:
342 // Reconstruct the full table of ultra ops in contiguous memory. When include_zk_ops is set, the result includes
343 // the ZK prefix at the front. Under fixed-location append, the result then has gap no-ops up to the fixed offset,
344 // the APPEND_TRACE_OFFSET zero preamble, then the most recently merged subtable.
345 std::vector<UltraOp> get_reconstructed(const bool include_zk_ops) const
346 {
348 0UL,
349 "current subtable should be merged before reconstructing the full table of operations.");
350 BB_ASSERT(!include_zk_ops || has_zk_ops(), "ZK ops must be constructed before reconstructing the Ultra table.");
351
352 std::vector<UltraOp> reconstructed_table;
353 reconstructed_table.reserve(1 << CONST_OP_QUEUE_LOG_SIZE);
354
355 if (include_zk_ops) {
356 reconstructed_table.insert(reconstructed_table.end(), zk_ops.begin(), zk_ops.end());
357 }
358
360 for (const auto& subtable : table.get()) {
361 reconstructed_table.insert(reconstructed_table.end(), subtable.begin(), subtable.end());
362 }
363 return reconstructed_table;
364 }
365
366 // Previously-merged subtables (everything except the most recent)
367 for (size_t idx = 0; idx + 1 < table.num_subtables(); ++idx) {
368 const auto& subtable = table.get()[idx];
369 reconstructed_table.insert(reconstructed_table.end(), subtable.begin(), subtable.end());
370 }
371
372 // Pad with no-ops up to fixed offset + APPEND_TRACE_OFFSET preamble
373 constexpr size_t preamble_op_slots = APPEND_TRACE_OFFSET / NUM_ROWS_PER_OP;
374 const size_t zk_offset_ops = include_zk_ops ? zk_ops.size() : 0;
375 const size_t target_op_count = fixed_append_offset.value() + zk_offset_ops + preamble_op_slots;
377 reconstructed_table.size(), target_op_count, "Current table size is larger than fixed append offset.");
378 reconstructed_table.insert(
379 reconstructed_table.end(), target_op_count - reconstructed_table.size(), UltraOp{ /* no-op */ });
380
381 // Final subtable
382 const auto& final_subtable = table.get().back();
383 reconstructed_table.insert(reconstructed_table.end(), final_subtable.begin(), final_subtable.end());
384 return reconstructed_table;
385 }
386
387 public:
389 {
390 BB_ASSERT(!has_zk_ops(), "ZK ops should only be constructed once.");
391
392 // Construct the table of ops
393 for (size_t idx = 0; idx < ECC_NUM_NO_OPS_START; idx++) {
394 zk_ops.push_back(UltraOp{ /* no_op */ });
395 }
396
397 // Each random op contributes 8 fresh Fr values to the column polynomials, masking commitments and
398 // evaluations of the columns in the merge protocol and Translator.
399 for (size_t idx = 0; idx < ECC_NUM_RANDOM_OPS_START; idx++) {
400 zk_ops.push_back(UltraOp{ .op_code = EccOpCode{ .is_random_op = true,
401 .random_value_1 = Fr::random_element(),
402 .random_value_2 = Fr::random_element() },
403 .x_lo = Fr::random_element(),
404 .x_hi = Fr::random_element(),
405 .y_lo = Fr::random_element(),
406 .y_hi = Fr::random_element(),
407 .z_1 = Fr::random_element(),
408 .z_2 = Fr::random_element(),
409 .return_is_infinity = false });
410 }
411
413 auto [hiding_ultra_op, hiding_eccvm_op] = make_hiding_op_pair(Fq::random_element(), Fq::random_element());
414 zk_ops.push_back(hiding_ultra_op);
415
416 const size_t poly_size = (zk_ops.size() * NUM_ROWS_PER_OP);
417 BB_ASSERT_EQ(poly_size, ZK_ULTRA_OPS);
418
419 // Construct the column polynomials
420 ColumnPolynomials column_polynomials;
421 for (auto& poly : column_polynomials) {
422 poly = Polynomial<Fr>(poly_size);
423 }
424
425 size_t i = 0;
426 for (const auto& op : zk_ops) {
427 write_op_to_polynomials(column_polynomials, op, i);
428 i += NUM_ROWS_PER_OP;
429 }
430
431 return { column_polynomials, hiding_eccvm_op };
432 }
433
434 // Construct column polynomials for all subtables
436 {
437 std::vector<ColumnPolynomials> subtable_columns;
438
439 for (size_t idx = 0; idx < table.num_subtables(); idx++) {
440 const auto& subtable = table.get()[idx];
441 const size_t poly_size = (subtable.size() * NUM_ROWS_PER_OP);
442 ColumnPolynomials columns = construct_columns_in_range(poly_size, idx, idx + 1);
443 subtable_columns.push_back(std::move(columns));
444 }
445
446 return subtable_columns;
447 }
448
449 // Construct column polynomials for the full ultra ecc ops table
450 ColumnPolynomials construct_table_columns(const bool include_zk_ops = true) const
451 {
452 BB_ASSERT(!include_zk_ops || has_zk_ops(),
453 "ZK ops must be constructed before constructing the full Ultra table with ZK ops.");
455 num_ultra_rows(), 0, table.num_subtables(), include_zk_ops, fixed_append_offset);
456 }
457
458 // Construct column polynomials for the aggregate table up to and including the tail subtable.
460 {
461 BB_ASSERT(has_zk_ops(), "ZK ops should have been constructed before constructing the table up to tail");
463 1UL,
464 "There should be at least two subtables (including the tail) to construct the table up to tail");
465 BB_ASSERT_GT(table.num_subtables(), 0UL, "Cannot construct table up to tail without a current subtable");
466
468 ultra_table_size_up_to_tail(), 0, table.num_subtables() - 1, /*include_zk_ops=*/true);
469 }
470
471 // Construct the columns of the most recently merged subtable.
472 // Under fixed-location append, the returned polynomials carry APPEND_TRACE_OFFSET leading zero rows so their
473 // commitments match the appender's ecc_op_wire commitments.
475 {
476 BB_ASSERT(table.num_subtables() > 0, "Cannot construct current subtable columns with no merged subtables");
477 const size_t leading_zeros = has_fixed_append_offset() ? APPEND_TRACE_OFFSET : 0;
478 const auto& subtable = table.get().back();
479 const size_t poly_size = leading_zeros + (subtable.size() * NUM_ROWS_PER_OP);
480
481 ColumnPolynomials column_polynomials;
482 if (poly_size == 0) {
483 return column_polynomials;
484 }
485 for (auto& poly : column_polynomials) {
486 poly = Polynomial<Fr>(poly_size);
487 }
488
489 size_t row = leading_zeros;
490 for (const auto& op : subtable) {
491 write_op_to_polynomials(column_polynomials, op, row);
492 row += NUM_ROWS_PER_OP;
493 }
494 return column_polynomials;
495 }
496
497 private:
505 static void write_op_to_polynomials(ColumnPolynomials& column_polynomials, const UltraOp& op, const size_t row_idx)
506 {
507 column_polynomials[0].at(row_idx) = !op.op_code.is_random_op ? op.op_code.value() : op.op_code.random_value_1;
508 column_polynomials[1].at(row_idx) = op.x_lo;
509 column_polynomials[2].at(row_idx) = op.x_hi;
510 column_polynomials[3].at(row_idx) = op.y_lo;
511 column_polynomials[0].at(row_idx + 1) = !op.op_code.is_random_op ? 0 : op.op_code.random_value_2;
512 column_polynomials[1].at(row_idx + 1) = op.y_hi;
513 column_polynomials[2].at(row_idx + 1) = op.z_1;
514 column_polynomials[3].at(row_idx + 1) = op.z_2;
515 }
516
530 const size_t poly_size,
531 const size_t subtable_start_idx,
532 const size_t subtable_end_idx,
533 const bool include_zk_ops = false,
534 const std::optional<size_t> fixed_append_offset_for_last = std::nullopt) const
535 {
536 const size_t final_poly_size = poly_size + (include_zk_ops ? ZK_ULTRA_OPS : 0);
537
538 ColumnPolynomials column_polynomials;
539 if (final_poly_size == 0) {
540 return column_polynomials;
541 }
542 for (auto& poly : column_polynomials) {
543 poly = Polynomial<Fr>(final_poly_size);
544 }
545
546 size_t row = 0;
547
548 if (include_zk_ops) {
549 BB_ASSERT(has_zk_ops(), "ZK ops should have been constructed before including them in the columns");
550 for (const auto& op : zk_ops) {
551 write_op_to_polynomials(column_polynomials, op, row);
552 row += NUM_ROWS_PER_OP;
553 }
554 }
555
556 // Lay out subtables sequentially. If a fixed-append target is set, exclude the last-in-range subtable
557 // from the sequential pass; it is placed at the fixed offset below.
558 const size_t sequential_end =
559 fixed_append_offset_for_last.has_value() ? subtable_end_idx - 1 : subtable_end_idx;
560 for (size_t idx = subtable_start_idx; idx < sequential_end; ++idx) {
561 for (const auto& op : table.get()[idx]) {
562 write_op_to_polynomials(column_polynomials, op, row);
563 row += NUM_ROWS_PER_OP;
564 }
565 }
566
567 if (fixed_append_offset_for_last.has_value()) {
568 const size_t zk_prefix_rows = include_zk_ops ? ZK_ULTRA_OPS : 0;
569 size_t append_row =
570 zk_prefix_rows + (fixed_append_offset_for_last.value() * NUM_ROWS_PER_OP) + APPEND_TRACE_OFFSET;
571 for (const auto& op : table.get()[subtable_end_idx - 1]) {
572 write_op_to_polynomials(column_polynomials, op, append_row);
573 append_row += NUM_ROWS_PER_OP;
574 }
575 }
576
577 return column_polynomials;
578 }
579};
580
581} // namespace bb
#define BB_ASSERT(expression,...)
Definition assert.hpp:70
#define BB_ASSERT_GT(left, right,...)
Definition assert.hpp:113
#define BB_ASSERT_EQ(actual, expected,...)
Definition assert.hpp:83
#define BB_ASSERT_LTE(left, right,...)
Definition assert.hpp:158
#define BB_ASSERT_LT(left, right,...)
Definition assert.hpp:143
A table of ECC operations.
std::vector< OpFormat > Subtable
std::vector< Subtable > table
size_t size() const
size_t get_current_subtable_size() const
void create_new_subtable(size_t size_hint=0)
auto & get() const
Subtable current_subtable
size_t num_subtables() const
std::vector< OpFormat > get_reconstructed() const
const OpFormat & operator[](size_t index) const
void push(const OpFormat &op)
Structured polynomial class that represents the coefficients 'a' of a_0 + a_1 x .....
Stores a table of elliptic curve operations represented in the Ultra format.
std::pair< ColumnPolynomials, ECCVMOperation > construct_zk_columns()
Curve::ScalarField Fr
size_t get_current_subtable_size() const
std::vector< UltraOp > zk_ops
std::optional< size_t > fixed_append_offset
size_t ultra_table_size_up_to_tail() const
std::vector< ColumnPolynomials > construct_subtable_columns() const
void push(const UltraOp &op)
size_t num_ultra_rows() const
ColumnPolynomials construct_current_ultra_ops_subtable_columns() const
static constexpr size_t APPEND_TRACE_OFFSET
ColumnPolynomials construct_table_columns_up_to_tail() const
std::array< Polynomial< Fr >, TABLE_WIDTH > ColumnPolynomials
std::vector< UltraOp > get_reconstructed(const bool include_zk_ops) const
static constexpr size_t NUM_ROWS_PER_OP
ColumnPolynomials construct_columns_in_range(const size_t poly_size, const size_t subtable_start_idx, const size_t subtable_end_idx, const bool include_zk_ops=false, const std::optional< size_t > fixed_append_offset_for_last=std::nullopt) const
Construct column polynomials covering subtables [start, end), optionally with a ZK prefix and an opti...
void create_new_subtable(size_t size_hint=0)
static std::pair< UltraOp, ECCVMOperation > make_hiding_op_pair(const curve::BN254::BaseField &Px, const curve::BN254::BaseField &Py)
Build a hiding op as paired Ultra and ECCVM operations from raw Fq coordinates.
std::vector< UltraOp > get_no_zk_reconstructed_ultra_ops() const
bool has_fixed_append_offset() const
static void write_op_to_polynomials(ColumnPolynomials &column_polynomials, const UltraOp &op, const size_t row_idx)
Write a single UltraOp to the column polynomials at the given position.
static constexpr size_t TABLE_WIDTH
ColumnPolynomials construct_table_columns(const bool include_zk_ops=true) const
static constexpr size_t ZK_ULTRA_OPS
void merge_with_fixed_append_offset(size_t offset)
std::vector< UltraOp > get_zk_reconstructed_ultra_ops() const
bb::fq BaseField
Definition bn254.hpp:19
typename Group::affine_element AffineElement
Definition bn254.hpp:22
bb::fr ScalarField
Definition bn254.hpp:18
constexpr uint256_t slice(uint64_t start, uint64_t end) const
ssize_t offset
Definition engine.cpp:62
Entry point for Barretenberg command-line interface.
Definition api.hpp:5
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
AffineElement base_point
Curve::Group::affine_element AffineElement
bool operator==(const ECCVMOperation &other) const =default
Defines the opcodes for ECC operations used in both the Ultra and ECCVM formats. There are three opco...
bool operator==(const EccOpCode &other) const =default
uint32_t value() const
curve::BN254::ScalarField Fr
bool return_is_infinity
EccOpCode op_code
bool operator==(const UltraOp &other) const =default
std::array< Fq, 2 > get_base_point_standard_form() const
Get the point in standard form i.e. as two coordinates x and y in the base field or as a point at inf...
curve::BN254::BaseField Fq
static field random_element(numeric::RNG *engine=nullptr) noexcept
void throw_or_abort(std::string const &err)