Skip to content

Commit 2dfc7ea

Browse files
authored
feat: a minimal fixed offset append merge (#16517)
Implements a not very flexible but just what we need "fixed offset" append merge in the ops table logic. Allows for concatenating the ultra ops subtable at a fixed location as is needed for merge ZK.
1 parent 411cf5a commit 2dfc7ea

File tree

4 files changed

+280
-37
lines changed

4 files changed

+280
-37
lines changed

barretenberg/cpp/src/barretenberg/op_queue/ecc_op_queue.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@ class ECCOpQueue {
6363
ultra_ops_table.create_new_subtable();
6464
}
6565

66-
void merge(MergeSettings settings = MergeSettings::PREPEND)
66+
void merge(MergeSettings settings = MergeSettings::PREPEND, std::optional<size_t> ultra_fixed_offset = std::nullopt)
6767
{
6868
eccvm_ops_table.merge(settings);
69-
ultra_ops_table.merge(settings);
69+
ultra_ops_table.merge(settings, ultra_fixed_offset);
7070
}
7171

7272
// Construct polynomials corresponding to the columns of the full aggregate ultra ecc ops table

barretenberg/cpp/src/barretenberg/op_queue/ecc_op_queue.test.cpp

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ class ECCOpQueueTest {
2828
*
2929
*/
3030
static void check_table_column_polynomials(const std::shared_ptr<bb::ECCOpQueue>& op_queue,
31-
MergeSettings settings = MergeSettings::PREPEND)
31+
MergeSettings settings,
32+
std::optional<size_t> ultra_fixed_offset = std::nullopt)
3233
{
33-
op_queue->merge(settings);
3434
// Construct column polynomials corresponding to the full table (T), the previous table (T_prev), and the
3535
// current subtable (t_current)
3636
auto table_polynomials = op_queue->construct_ultra_ops_table_columns();
@@ -52,32 +52,32 @@ class ECCOpQueueTest {
5252
eval_challenge.pow(current_subtable_size); // x^k * T_prev(x)
5353
EXPECT_EQ(table_eval, subtable_eval + shifted_previous_table_eval);
5454
} else {
55-
// T(x) = T_prev(x) + x^k * t_current(x), where k is the size of the previous table
55+
// APPEND merge performs concatenation directly to end of previous table or at a specified fixed offset
5656
const size_t prev_table_size = op_queue->get_previous_ultra_ops_table_num_rows(); // k
57-
const Fr prev_table_eval = prev_table_poly.evaluate(eval_challenge); // T_prev(x)
57+
const size_t shift_magnitude = ultra_fixed_offset.value_or(prev_table_size);
58+
// T(x) = T_prev(x) + x^k * t_current(x), where k is the shift magnitude
59+
const Fr prev_table_eval = prev_table_poly.evaluate(eval_challenge); // T_prev(x)
5860
const Fr shifted_subtable_eval =
59-
subtable_poly.evaluate(eval_challenge) * eval_challenge.pow(prev_table_size); // x^k * t_current(x)
61+
subtable_poly.evaluate(eval_challenge) * eval_challenge.pow(shift_magnitude); // x^k * t_current(x)
6062
EXPECT_EQ(table_eval, shifted_subtable_eval + prev_table_eval);
6163
}
6264
}
6365
}
6466

6567
/**
66-
* @brief Check that the opcode values are consistent between the first column polynomial and the eccvm
67-
ops table
68+
* @brief Check that the opcode values are consistent between the ultra ops table and the eccvm ops table
6869
*
6970
* @param op_queue
7071
*/
7172
static void check_opcode_consistency_with_eccvm(const std::shared_ptr<bb::ECCOpQueue>& op_queue)
7273
{
73-
auto ultra_opcode_values = op_queue->construct_ultra_ops_table_columns()[0];
74+
auto ultra_table = op_queue->get_ultra_ops();
7475
auto eccvm_table = op_queue->get_eccvm_ops();
75-
// Every second value in the opcode column polynomial should be 0
76-
EXPECT_EQ(eccvm_table.size() * 2, ultra_opcode_values.size());
7776

78-
for (size_t i = 0; i < eccvm_table.size(); ++i) {
79-
EXPECT_EQ(ultra_opcode_values[2 * i], eccvm_table[i].op_code.value());
80-
EXPECT_EQ(ultra_opcode_values[2 * i + 1], Fr(0));
77+
EXPECT_EQ(eccvm_table.size(), ultra_table.size());
78+
79+
for (auto [ultra_op, eccvm_op] : zip_view(ultra_table, eccvm_table)) {
80+
EXPECT_EQ(ultra_op.op_code.value(), eccvm_op.op_code.value());
8181
}
8282
};
8383
};
@@ -131,7 +131,9 @@ TEST(ECCOpQueueTest, ColumnPolynomialConstructionPrependOnly)
131131
const size_t NUM_SUBTABLES = 5;
132132
for (size_t i = 0; i < NUM_SUBTABLES; ++i) {
133133
ECCOpQueueTest::populate_an_arbitrary_subtable_of_ops(op_queue);
134-
ECCOpQueueTest::check_table_column_polynomials(op_queue);
134+
MergeSettings settings = MergeSettings::PREPEND;
135+
op_queue->merge(settings);
136+
ECCOpQueueTest::check_table_column_polynomials(op_queue, settings);
135137
}
136138

137139
ECCOpQueueTest::check_opcode_consistency_with_eccvm(op_queue);
@@ -147,12 +149,41 @@ TEST(ECCOpQueueTest, ColumnPolynomialConstructionPrependThenAppend)
147149
const size_t NUM_SUBTABLES = 2;
148150
for (size_t i = 0; i < NUM_SUBTABLES; ++i) {
149151
ECCOpQueueTest::populate_an_arbitrary_subtable_of_ops(op_queue);
150-
ECCOpQueueTest::check_table_column_polynomials(op_queue);
152+
MergeSettings settings = MergeSettings::PREPEND;
153+
op_queue->merge(settings);
154+
ECCOpQueueTest::check_table_column_polynomials(op_queue, settings);
151155
}
152156

153157
// Do a single append operation
154158
ECCOpQueueTest::populate_an_arbitrary_subtable_of_ops(op_queue);
155-
ECCOpQueueTest::check_table_column_polynomials(op_queue, MergeSettings::APPEND);
159+
MergeSettings settings = MergeSettings::APPEND;
160+
op_queue->merge(settings);
161+
ECCOpQueueTest::check_table_column_polynomials(op_queue, settings);
162+
163+
ECCOpQueueTest::check_opcode_consistency_with_eccvm(op_queue);
164+
}
165+
166+
TEST(ECCOpQueueTest, ColumnPolynomialConstructionPrependThenAppendAtFixedOffset)
167+
{
168+
169+
// Instantiate an EccOpQueue and populate it with several subtables of ECC ops
170+
auto op_queue = std::make_shared<bb::ECCOpQueue>();
171+
172+
// Check that the table polynomials have the correct form after each subtable concatenation
173+
const size_t NUM_SUBTABLES = 2;
174+
for (size_t i = 0; i < NUM_SUBTABLES; ++i) {
175+
ECCOpQueueTest::populate_an_arbitrary_subtable_of_ops(op_queue);
176+
MergeSettings settings = MergeSettings::PREPEND;
177+
op_queue->merge(settings);
178+
ECCOpQueueTest::check_table_column_polynomials(op_queue, settings);
179+
}
180+
181+
// Do a single append operation at a fixed offset (sufficiently large as to not overlap with the existing table)
182+
const size_t ultra_fixed_offset = op_queue->get_ultra_ops_table_num_rows() + 20;
183+
ECCOpQueueTest::populate_an_arbitrary_subtable_of_ops(op_queue);
184+
MergeSettings settings = MergeSettings::APPEND;
185+
op_queue->merge(settings, ultra_fixed_offset);
186+
ECCOpQueueTest::check_table_column_polynomials(op_queue, settings, ultra_fixed_offset);
156187

157188
ECCOpQueueTest::check_opcode_consistency_with_eccvm(op_queue);
158189
}

barretenberg/cpp/src/barretenberg/op_queue/ecc_ops_table.hpp

Lines changed: 96 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -230,17 +230,44 @@ class UltraEccOpsTable {
230230
size_t current_subtable_idx = 0; // index of the current subtable being constructed
231231
UltraOpsTable table;
232232

233+
// For fixed-location append functionality (ultra ops only)
234+
std::optional<size_t> fixed_append_offset;
235+
bool has_fixed_append = false;
236+
233237
public:
234238
size_t size() const { return table.size(); }
235-
size_t ultra_table_size() const { return table.size() * NUM_ROWS_PER_OP; }
239+
size_t ultra_table_size() const
240+
{
241+
size_t base_size = table.size() * NUM_ROWS_PER_OP;
242+
if (has_fixed_append && fixed_append_offset.has_value()) {
243+
// Include zeros gap and final subtable at fixed location
244+
size_t last_subtable_size = 0;
245+
if (!table.get().empty()) {
246+
// The last subtable in deque is the fixed-location one
247+
last_subtable_size = table.get().back().size() * NUM_ROWS_PER_OP;
248+
}
249+
return std::max(base_size, fixed_append_offset.value() + last_subtable_size);
250+
}
251+
return base_size;
252+
}
236253
size_t current_ultra_subtable_size() const { return table.get()[current_subtable_idx].size() * NUM_ROWS_PER_OP; }
237254
size_t previous_ultra_table_size() const { return (ultra_table_size() - current_ultra_subtable_size()); }
238255
void create_new_subtable(size_t size_hint = 0) { table.create_new_subtable(size_hint); }
239256
void push(const UltraOp& op) { table.push(op); }
240-
void merge(MergeSettings settings = MergeSettings::PREPEND)
257+
void merge(MergeSettings settings = MergeSettings::PREPEND, std::optional<size_t> offset = std::nullopt)
241258
{
242-
table.merge(settings);
243-
current_subtable_idx = settings == MergeSettings::PREPEND ? 0 : table.num_subtables() - 1;
259+
if (settings == MergeSettings::APPEND) {
260+
// All appends are treated as fixed-location for ultra ops
261+
ASSERT(!has_fixed_append, "Can only perform fixed-location append once");
262+
// Set fixed location at which to append ultra ops. If nullopt --> append right after prepended tables
263+
fixed_append_offset = offset;
264+
has_fixed_append = true;
265+
table.merge(settings);
266+
current_subtable_idx = table.num_subtables() - 1;
267+
} else { // MergeSettings::PREPEND
268+
table.merge(settings);
269+
current_subtable_idx = 0;
270+
}
244271
}
245272

246273
std::vector<UltraOp> get_reconstructed() const { return table.get_reconstructed(); }
@@ -249,25 +276,30 @@ class UltraEccOpsTable {
249276
ColumnPolynomials construct_table_columns() const
250277
{
251278
const size_t poly_size = ultra_table_size();
279+
280+
if (has_fixed_append) {
281+
// Handle fixed-location append: prepended tables first, then appended table at fixed offset
282+
return construct_column_polynomials_with_fixed_append(poly_size);
283+
}
284+
285+
// Normal case: all subtables in order
252286
const size_t subtable_start_idx = 0; // include all subtables
253287
const size_t subtable_end_idx = table.num_subtables();
254-
255288
return construct_column_polynomials_from_subtables(poly_size, subtable_start_idx, subtable_end_idx);
256289
}
257290

258291
// Construct the columns of the previous full ultra ecc ops table
259292
ColumnPolynomials construct_previous_table_columns() const
260293
{
261-
262294
const size_t poly_size = previous_ultra_table_size();
263295
const size_t subtable_start_idx = current_subtable_idx == 0 ? 1 : 0;
264296
const size_t subtable_end_idx = current_subtable_idx == 0 ? table.num_subtables() : table.num_subtables() - 1;
265297

266298
return construct_column_polynomials_from_subtables(poly_size, subtable_start_idx, subtable_end_idx);
267299
}
268300

269-
// Construct the columns of the current ultra ecc ops subtable which is either the first or the last one depening on
270-
// whether it has been prepended or appended
301+
// Construct the columns of the current ultra ecc ops subtable which is either the first or the last one
302+
// depening on whether it has been prepended or appended
271303
ColumnPolynomials construct_current_ultra_ops_subtable_columns() const
272304
{
273305
const size_t poly_size = current_ultra_subtable_size();
@@ -278,6 +310,60 @@ class UltraEccOpsTable {
278310
}
279311

280312
private:
313+
/**
314+
* @brief Write a single UltraOp to the column polynomials at the given position
315+
* @details Each op is written across 2 rows (NUM_ROWS_PER_OP)
316+
* @param column_polynomials The column polynomials to write to
317+
* @param op The operation to write
318+
* @param row_idx The starting row index (will write to row_idx and row_idx+1)
319+
*/
320+
static void write_op_to_polynomials(ColumnPolynomials& column_polynomials, const UltraOp& op, const size_t row_idx)
321+
{
322+
column_polynomials[0].at(row_idx) = !op.op_code.is_random_op ? op.op_code.value() : op.op_code.random_value_1;
323+
column_polynomials[1].at(row_idx) = op.x_lo;
324+
column_polynomials[2].at(row_idx) = op.x_hi;
325+
column_polynomials[3].at(row_idx) = op.y_lo;
326+
column_polynomials[0].at(row_idx + 1) = !op.op_code.is_random_op ? 0 : op.op_code.random_value_2;
327+
column_polynomials[1].at(row_idx + 1) = op.y_hi;
328+
column_polynomials[2].at(row_idx + 1) = op.z_1;
329+
column_polynomials[3].at(row_idx + 1) = op.z_2;
330+
}
331+
332+
/**
333+
* @brief Construct polynomials with fixed-location append
334+
* @details Process prepended subtables first, then place the appended subtable at the fixed offset
335+
*/
336+
ColumnPolynomials construct_column_polynomials_with_fixed_append(const size_t poly_size) const
337+
{
338+
ColumnPolynomials column_polynomials;
339+
for (auto& poly : column_polynomials) {
340+
poly = Polynomial<Fr>(poly_size); // Initialized to zeros
341+
}
342+
343+
// Process all prepended subtables (all except last)
344+
size_t i = 0;
345+
for (size_t subtable_idx = 0; subtable_idx < table.num_subtables() - 1; ++subtable_idx) {
346+
const auto& subtable = table.get()[subtable_idx];
347+
for (const auto& op : subtable) {
348+
write_op_to_polynomials(column_polynomials, op, i);
349+
i += NUM_ROWS_PER_OP;
350+
}
351+
}
352+
353+
// Place the appended subtable at the fixed offset
354+
size_t append_position = fixed_append_offset.value_or(i);
355+
const auto& appended_subtable = table.get()[table.num_subtables() - 1];
356+
357+
size_t j = append_position;
358+
for (const auto& op : appended_subtable) {
359+
write_op_to_polynomials(column_polynomials, op, j);
360+
j += NUM_ROWS_PER_OP;
361+
}
362+
363+
// Any gap between prepended tables and appended table remains zeros (from initialization)
364+
return column_polynomials;
365+
}
366+
281367
/**
282368
* @brief Construct polynomials corresponding to the columns of the reconstructed ultra ops table for the given
283369
* range of subtables
@@ -297,17 +383,8 @@ class UltraEccOpsTable {
297383
for (size_t subtable_idx = subtable_start_idx; subtable_idx < subtable_end_idx; ++subtable_idx) {
298384
const auto& subtable = table.get()[subtable_idx];
299385
for (const auto& op : subtable) {
300-
column_polynomials[0].at(i) = !op.op_code.is_random_op ? op.op_code.value() : op.op_code.random_value_1;
301-
column_polynomials[1].at(i) = op.x_lo;
302-
column_polynomials[2].at(i) = op.x_hi;
303-
column_polynomials[3].at(i) = op.y_lo;
304-
i++;
305-
column_polynomials[0].at(i) = !op.op_code.is_random_op ? 0 : op.op_code.random_value_2;
306-
// only the first 'op' field is utilized
307-
column_polynomials[1].at(i) = op.y_hi;
308-
column_polynomials[2].at(i) = op.z_1;
309-
column_polynomials[3].at(i) = op.z_2;
310-
i++;
386+
write_op_to_polynomials(column_polynomials, op, i);
387+
i += NUM_ROWS_PER_OP;
311388
}
312389
}
313390
return column_polynomials;

0 commit comments

Comments
 (0)