Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
ecc_ops_table.test.cpp
Go to the documentation of this file.
5#include <gtest/gtest.h>
6
7#include <ranges>
8
9using namespace bb;
10
11class EccOpsTableTest : public ::testing::Test {
13 using Scalar = fr;
14
15 public:
16 template <typename Op> struct MockSubtableGenerator {
17 virtual ~MockSubtableGenerator() = default;
18 virtual Op generate_random_op() const = 0;
19 std::vector<std::vector<Op>> generate_subtables(size_t num_subtables, std::vector<size_t> ops_per_table)
20 {
21 BB_ASSERT_EQ(num_subtables, ops_per_table.size());
23 subtables.reserve(num_subtables);
24 for (size_t i = 0; i < num_subtables; ++i) {
25 std::vector<Op> subtable;
26 subtable.reserve(ops_per_table[i]);
27 for (size_t j = 0; j < ops_per_table[i]; ++j) {
28 subtable.push_back(generate_random_op());
29 }
30 subtables.push_back(std::move(subtable));
31 }
32
33 return subtables;
34 }
35 };
36
38 ~UltraOpTableGenerator() override = default;
39 UltraOp generate_random_op() const override
40 {
41 return UltraOp{ .op_code = EccOpCode{},
42 .x_lo = Scalar::random_element(),
43 .x_hi = Scalar::random_element(),
44 .y_lo = Scalar::random_element(),
45 .y_hi = Scalar::random_element(),
48 .return_is_infinity = false };
49 }
50 };
51
52 struct EccvmOpTableGenerator : public MockSubtableGenerator<ECCVMOperation> {
53 ~EccvmOpTableGenerator() override = default;
55 {
56 return ECCVMOperation{ .op_code = EccOpCode{ .mul = true },
57 .base_point = Curve::Group::affine_element::random_element(),
60 .mul_scalar_full = Scalar::random_element() };
61 }
62 };
63
64 // Mock ultra ops table that constructs a concatenated table from successively appended subtables.
67 void append(const UltraOp& op)
68 {
69 columns[0].push_back(op.op_code.value());
70 columns[1].push_back(op.x_lo);
71 columns[2].push_back(op.x_hi);
72 columns[3].push_back(op.y_lo);
73
74 columns[0].push_back(0);
75 columns[1].push_back(op.y_hi);
76 columns[2].push_back(op.z_1);
77 columns[3].push_back(op.z_2);
78 }
79
80 void append_zero_rows(size_t num_rows)
81 {
82 for (auto& column : columns) {
83 column.insert(column.end(), num_rows, Scalar::zero());
84 }
85 }
86
87 // Construct the ultra ops table from the given subtables, ordered as they should appear in the op queue.
88 MockUltraOpsTable(const auto& subtable_ops, bool last_subtable_has_preamble = false)
89 {
90 const size_t last_idx = subtable_ops.size() == 0 ? 0 : subtable_ops.size() - 1;
91 for (size_t i = 0; i < subtable_ops.size(); ++i) {
92 if (last_subtable_has_preamble && i == last_idx) {
94 }
95 for (const auto& op : subtable_ops[i]) {
96 append(op);
97 }
98 }
99 }
100
101 size_t size() const { return columns[0].size(); }
102 };
103
104 // Mock eccvm ops table that constructs a concatenated table from successively appended subtables.
106 std::vector<ECCVMOperation> eccvm_ops;
107
108 MockEccvmOpsTable(const auto& subtable_ops)
109 {
110 for (auto& ops : subtable_ops) {
111 for (const auto& op : ops) {
112 eccvm_ops.push_back(op);
113 }
114 }
115 }
116 };
117};
118
119// Ensure UltraOpsTable correctly constructs a concatenated table from successively appended subtables.
120TEST(EccOpsTableTest, UltraOpsTable)
121{
122 using Fr = fr;
123 using TableGenerator = EccOpsTableTest::UltraOpTableGenerator;
124
125 // Construct sets of ultra ops, each representing those added by a single circuit
126 const size_t NUM_SUBTABLES = 3;
127 std::vector<size_t> subtable_op_counts = { 4, 2, 7 };
128
129 TableGenerator table_generator;
130 auto subtables = table_generator.generate_subtables(NUM_SUBTABLES, subtable_op_counts);
131
132 // Construct the concatenated table internal to the op queue
133 UltraEccOpsTable ultra_ops_table;
134 for (const auto& subtable_ops : subtables) {
135 ultra_ops_table.create_new_subtable();
136 for (const auto& op : subtable_ops) {
137 ultra_ops_table.push(op);
138 }
139 ultra_ops_table.merge();
140 }
141
142 // Construct the mock ultra ops table which contains the subtables in append order.
143 EccOpsTableTest::MockUltraOpsTable expected_ultra_ops_table(subtables);
144
145 // Check that the ultra ops table internal to the op queue has the correct size
146 auto expected_num_ops = std::accumulate(subtable_op_counts.begin(), subtable_op_counts.end(), size_t(0));
147 EXPECT_EQ(ultra_ops_table.num_ops(), expected_num_ops);
148
149 // Construct polynomials corresponding to the columns of the ultra ops table
150 ultra_ops_table.construct_zk_columns();
151 std::array<Polynomial<Fr>, 4> ultra_ops_table_polynomials = ultra_ops_table.construct_table_columns();
152 std::array<Polynomial<Fr>, 4> no_zk_ultra_ops_table_polynomials =
153 ultra_ops_table.construct_table_columns(/*include_zk_ops=*/false);
154
155 // Check that the ultra ops table constructed by the op queue matches the expected table
156 for (auto [expected_column, poly, no_zk_poly] :
157 zip_view(expected_ultra_ops_table.columns, ultra_ops_table_polynomials, no_zk_ultra_ops_table_polynomials)) {
158 EXPECT_EQ(poly.size(), UltraEccOpsTable::ZK_ULTRA_OPS + expected_column.size());
159 EXPECT_EQ(no_zk_poly.size(), expected_column.size());
160 for (size_t row = 0; row < expected_column.size(); ++row) {
161 EXPECT_EQ(expected_column[row], poly.at(UltraEccOpsTable::ZK_ULTRA_OPS + row));
162 EXPECT_EQ(expected_column[row], no_zk_poly.at(row));
163 }
164 }
165}
166
167TEST(EccOpsTableTest, UltraOpsFixedLocationAppendNoGap)
168{
169 using Fr = fr;
170 using TableGenerator = EccOpsTableTest::UltraOpTableGenerator;
171
172 // Construct sets of ultra ops
173 const size_t NUM_SUBTABLES = 3;
174 std::vector<size_t> subtable_op_counts = { 4, 2, 7 };
175
176 TableGenerator table_generator;
177 auto subtables = table_generator.generate_subtables(NUM_SUBTABLES, subtable_op_counts);
178
179 // Construct the concatenated table with fixed-location append (no explicit offset)
180 UltraEccOpsTable ultra_ops_table;
181 for (size_t i = 0; i < NUM_SUBTABLES; ++i) {
182 ultra_ops_table.create_new_subtable();
183 for (const auto& op : subtables[i]) {
184 ultra_ops_table.push(op);
185 }
186
187 if (i == NUM_SUBTABLES - 1) {
188 // No-gap fixed-append: place the appended subtable immediately after the prior subtables.
189 const size_t no_gap_offset = subtable_op_counts[0] + subtable_op_counts[1];
190 ultra_ops_table.merge_with_fixed_append_offset(no_gap_offset);
191 } else {
192 ultra_ops_table.merge();
193 }
194 }
195
196 // Expected order: subtable[0], subtable[1], subtable[2] (no gap). The final APPEND carries
197 // APPEND_TRACE_OFFSET preamble rows.
198 std::vector<std::vector<UltraOp>> ordered_subtables = { subtables[0], subtables[1], subtables[2] };
199
200 // Construct the mock ultra ops table
201 EccOpsTableTest::MockUltraOpsTable expected_ultra_ops_table(ordered_subtables,
202 /*last_subtable_has_preamble=*/true);
203
204 // Check that the ultra ops table has the correct size
205 auto expected_num_ops = std::accumulate(subtable_op_counts.begin(), subtable_op_counts.end(), size_t(0));
206 EXPECT_EQ(ultra_ops_table.num_ops(), expected_num_ops);
207
208 // Construct polynomials corresponding to the columns of the ultra ops table
209 ultra_ops_table.construct_zk_columns();
210 std::array<Polynomial<Fr>, 4> ultra_ops_table_polynomials = ultra_ops_table.construct_table_columns();
211
212 // Check that the ultra ops table matches the expected table
213 for (auto [expected_column, poly] : zip_view(expected_ultra_ops_table.columns, ultra_ops_table_polynomials)) {
214 EXPECT_EQ(poly.size(), UltraEccOpsTable::ZK_ULTRA_OPS + expected_column.size());
215 for (size_t row = 0; row < expected_column.size(); ++row) {
216 EXPECT_EQ(expected_column[row], poly.at(UltraEccOpsTable::ZK_ULTRA_OPS + row));
217 }
218 }
219}
220
221TEST(EccOpsTableTest, UltraOpsFixedLocationAppendWithGap)
222{
223 using Fr = fr;
224 using TableGenerator = EccOpsTableTest::UltraOpTableGenerator;
225
226 const size_t ULTRA_ROWS_PER_OP = UltraEccOpsTable::NUM_ROWS_PER_OP;
227
228 // Construct sets of ultra ops
229 const size_t NUM_SUBTABLES = 3;
230 std::vector<size_t> subtable_op_counts = { 4, 2, 7 };
231
232 TableGenerator table_generator;
233 auto subtables = table_generator.generate_subtables(NUM_SUBTABLES, subtable_op_counts);
234
235 // Construct the concatenated table with fixed-location append at specific offset
236 UltraEccOpsTable ultra_ops_table;
237 // Define a fixed offset at which to append the table (must be greater than the total size of the prior tables).
238 const size_t fixed_offset = 20;
239 const size_t fixed_offset_num_rows = fixed_offset * ULTRA_ROWS_PER_OP;
240 const size_t prior_subtables_size = (subtable_op_counts[0] + subtable_op_counts[1]) * ULTRA_ROWS_PER_OP;
241 BB_ASSERT(fixed_offset_num_rows > prior_subtables_size);
242
243 // Construct the ultra ops table
244 for (size_t i = 0; i < NUM_SUBTABLES; ++i) {
245 ultra_ops_table.create_new_subtable();
246 for (const auto& op : subtables[i]) {
247 ultra_ops_table.push(op);
248 }
249
250 if (i == NUM_SUBTABLES - 1) {
251 ultra_ops_table.merge_with_fixed_append_offset(fixed_offset);
252 } else {
253 ultra_ops_table.merge();
254 }
255 }
256
257 // Check that the ultra ops table has the correct total size (gap is not present in raw ops table)
258 auto expected_num_ops = std::accumulate(subtable_op_counts.begin(), subtable_op_counts.end(), size_t(0));
259 EXPECT_EQ(ultra_ops_table.num_ops(), expected_num_ops);
260
261 // Check that the polynomials have the correct size (including gap and APPEND_TRACE_OFFSET preamble)
262 constexpr size_t LEADING_ZEROS = UltraEccOpsTable::APPEND_TRACE_OFFSET;
263 size_t expected_poly_size = fixed_offset_num_rows + LEADING_ZEROS + (subtable_op_counts[2] * ULTRA_ROWS_PER_OP);
264 EXPECT_EQ(ultra_ops_table.num_ultra_rows(), expected_poly_size);
265 ultra_ops_table.construct_zk_columns();
266 const size_t zk_prefix_rows = UltraEccOpsTable::ZK_ULTRA_OPS;
267
268 // Construct polynomials corresponding to the columns of the ultra ops table
269 std::array<Polynomial<Fr>, 4> ultra_ops_table_polynomials = ultra_ops_table.construct_table_columns();
270
271 // Verify each polynomial has the expected size
272 for (const auto& poly : ultra_ops_table_polynomials) {
273 EXPECT_EQ(poly.size(), zk_prefix_rows + expected_poly_size);
274 }
275
276 // Construct expected table with zeros in the gap
277 // Order: subtable[0], subtable[1], zeros, subtable[2]
278 std::vector<std::vector<UltraOp>> ordered_subtables = { subtables[0], subtables[1] };
279 EccOpsTableTest::MockUltraOpsTable expected_prior_table(ordered_subtables);
280
281 // Check prior subtables are at the beginning.
282 for (auto [ultra_op_poly, expected_poly] : zip_view(ultra_ops_table_polynomials, expected_prior_table.columns)) {
283 for (size_t row = 0; row < prior_subtables_size; ++row) {
284 EXPECT_EQ(ultra_op_poly.at(zk_prefix_rows + row), expected_poly[row]);
285 }
286 }
287
288 // Check gap from prior tables up to (fixed_offset + preamble) is filled with zeros.
289 for (auto ultra_op_poly : ultra_ops_table_polynomials) {
290 for (size_t row = prior_subtables_size; row < fixed_offset_num_rows + LEADING_ZEROS; ++row) {
291 EXPECT_EQ(ultra_op_poly.at(zk_prefix_rows + row), Fr::zero());
292 }
293 }
294
295 // Check appended subtable is placed right after the APPEND_TRACE_OFFSET preamble
296 std::vector<std::vector<UltraOp>> appended_subtables = { subtables[2] };
297 EccOpsTableTest::MockUltraOpsTable expected_appended_table(appended_subtables);
298 for (auto [ultra_op_poly, expected_poly] : zip_view(ultra_ops_table_polynomials, expected_appended_table.columns)) {
299 for (size_t row = 0; row < subtable_op_counts[2] * ULTRA_ROWS_PER_OP; row++) {
300 EXPECT_EQ(ultra_op_poly.at(zk_prefix_rows + fixed_offset_num_rows + LEADING_ZEROS + row),
301 expected_poly[row]);
302 }
303 }
304
305 // Mimic get_reconstructed by unifying all the ops from subtables into a single vector with the appropriate append
306 // offset
307 {
308 std::vector<UltraOp> expected_reconstructed;
309 expected_reconstructed.reserve(expected_num_ops + fixed_offset);
310
311 // Order: subtable[0], subtable[1], no-ops range (including APPEND_TRACE_OFFSET preamble), subtable[2]
312 for (const auto& op : subtables[0]) {
313 expected_reconstructed.push_back(op);
314 }
315 for (const auto& op : subtables[1]) {
316 expected_reconstructed.push_back(op);
317 }
318
319 // Add the range of noops up to (fixed_offset + preamble op slots).
320 constexpr size_t PREAMBLE_OP_SLOTS = LEADING_ZEROS / UltraEccOpsTable::NUM_ROWS_PER_OP;
321 UltraOp no_op = {};
322 size_t size_before = expected_reconstructed.size();
323 for (size_t i = size_before; i < fixed_offset + PREAMBLE_OP_SLOTS; i++) {
324 expected_reconstructed.push_back(no_op);
325 }
326
327 for (const auto& op : subtables[2]) {
328 expected_reconstructed.push_back(op);
329 }
330
331 const auto reconstructed = ultra_ops_table.get_no_zk_reconstructed_ultra_ops();
332 EXPECT_EQ(expected_reconstructed.size(), reconstructed.size());
333
334 // Compare to the op-queue's reconstruction (should include the gap as no-ops)
335 EXPECT_EQ(expected_reconstructed, reconstructed);
336 }
337}
338
339// Ensure EccvmOpsTable correctly constructs a concatenated table from successively appended subtables
341{
342
343 // Construct sets of eccvm ops, each representing those added by a single circuit
344 using TableGenerator = EccOpsTableTest::EccvmOpTableGenerator;
345
346 // Construct sets of ultra ops, each representing those added by a single circuit
347 const size_t NUM_SUBTABLES = 3;
348 std::vector<size_t> subtable_op_counts = { 4, 2, 7 };
349
350 TableGenerator table_generator;
351 auto subtables = table_generator.generate_subtables(NUM_SUBTABLES, subtable_op_counts);
352
353 // Construct the concatenated eccvm ops table
354 EccvmOpsTable eccvm_ops_table;
355 for (const auto& subtable_ops : subtables) {
356 eccvm_ops_table.create_new_subtable();
357 for (const auto& op : subtable_ops) {
358 eccvm_ops_table.push(op);
359 }
360 eccvm_ops_table.merge();
361 }
362
363 // Construct the mock eccvm ops table which contains the subtables in append order.
364 EccOpsTableTest::MockEccvmOpsTable expected_eccvm_ops_table(subtables);
365
366 // Check that the table has the correct size
367 auto expected_num_ops = std::accumulate(subtable_op_counts.begin(), subtable_op_counts.end(), size_t(0));
368 EXPECT_EQ(eccvm_ops_table.size(), expected_num_ops);
369
370 // Check that accessing the table values via operator[] matches the manually constructed mock table
371 for (size_t i = 0; i < expected_num_ops; ++i) {
372 EXPECT_EQ(expected_eccvm_ops_table.eccvm_ops[i], eccvm_ops_table[i]);
373 }
374
375 // Check that the copy-based reconstruction of the eccvm ops table matches the expected table
376 EXPECT_EQ(expected_eccvm_ops_table.eccvm_ops, eccvm_ops_table.get_reconstructed());
377}
378
379// Ensure EccvmOpsTable correctly constructs a concatenated table from successively appended
380// subtables
381TEST(EccOpsTableTest, EccvmOpsTableAppendOnly)
382{
383
384 // Construct sets of eccvm ops, each representing those added by a single circuit
385 using TableGenerator = EccOpsTableTest::EccvmOpTableGenerator;
386
387 // Construct sets of ops, each representing those added by a single circuit
388 const size_t NUM_SUBTABLES = 3;
389 std::vector<size_t> subtable_op_counts = { 4, 2, 7 };
390
391 TableGenerator table_generator;
392 auto subtables = table_generator.generate_subtables(NUM_SUBTABLES, subtable_op_counts);
393
394 // Construct the concatenated eccvm ops table
395 EccvmOpsTable eccvm_ops_table;
396 for (const auto& subtable_ops : subtables) {
397 eccvm_ops_table.create_new_subtable();
398 for (const auto& op : subtable_ops) {
399 eccvm_ops_table.push(op);
400 }
401 eccvm_ops_table.merge();
402 }
403
404 // Construct the mock eccvm ops table which contains the subtables in append order.
405 EccOpsTableTest::MockEccvmOpsTable expected_eccvm_ops_table(subtables);
406
407 // Check that the table has the correct size
408 auto expected_num_ops = std::accumulate(subtable_op_counts.begin(), subtable_op_counts.end(), size_t(0));
409 EXPECT_EQ(eccvm_ops_table.size(), expected_num_ops);
410
411 // Check that accessing the table values via operator[] matches the manually constructed mock table
412 for (size_t i = 0; i < expected_num_ops; ++i) {
413 EXPECT_EQ(expected_eccvm_ops_table.eccvm_ops[i], eccvm_ops_table[i]);
414 }
415
416 // Check that the copy-based reconstruction of the eccvm ops table matches the expected table
417 EXPECT_EQ(expected_eccvm_ops_table.eccvm_ops, eccvm_ops_table.get_reconstructed());
418}
#define BB_ASSERT(expression,...)
Definition assert.hpp:70
#define BB_ASSERT_EQ(actual, expected,...)
Definition assert.hpp:83
size_t size() const
void create_new_subtable(size_t size_hint=0)
std::vector< OpFormat > get_reconstructed() const
void push(const OpFormat &op)
Stores a table of elliptic curve operations represented in the Ultra format.
std::pair< ColumnPolynomials, ECCVMOperation > construct_zk_columns()
void push(const UltraOp &op)
size_t num_ultra_rows() const
static constexpr size_t APPEND_TRACE_OFFSET
static constexpr size_t NUM_ROWS_PER_OP
void create_new_subtable(size_t size_hint=0)
std::vector< UltraOp > get_no_zk_reconstructed_ultra_ops() const
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)
Entry point for Barretenberg command-line interface.
Definition api.hpp:5
field< Bn254FrParams > fr
Definition fr.hpp:155
TEST(BoomerangMegaCircuitBuilder, BasicCircuit)
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
ECCVMOperation generate_random_op() const override
MockEccvmOpsTable(const auto &subtable_ops)
std::vector< ECCVMOperation > eccvm_ops
std::vector< std::vector< Op > > generate_subtables(size_t num_subtables, std::vector< size_t > ops_per_table)
virtual Op generate_random_op() const =0
std::array< std::vector< Scalar >, 4 > columns
MockUltraOpsTable(const auto &subtable_ops, bool last_subtable_has_preamble=false)
Defines the opcodes for ECC operations used in both the Ultra and ECCVM formats. There are three opco...
uint32_t value() const
EccOpCode op_code
static field random_element(numeric::RNG *engine=nullptr) noexcept
static constexpr field zero()