Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
rom_ram_logic.cpp
Go to the documentation of this file.
1// === AUDIT STATUS ===
2// internal: { status: Complete, auditors: [Raju], commit: 05a381f8b31ae4648e480f1369e911b148216e8b}
3// external_1: { status: Complete, auditors: [Sherlock], commit: e6694849223 }
4// external_2: { status: not started, auditors: [], commit: }
5// =====================
6
7#include "rom_ram_logic.hpp"
10#include <execution>
11
12namespace bb {
13
14template <typename ExecutionTrace> size_t RomRamLogic_<ExecutionTrace>::create_ROM_array(const size_t array_size)
15{
16 RomTranscript new_transcript;
17 for (size_t i = 0; i < array_size; ++i) {
18 new_transcript.state.emplace_back(
19 std::array<uint32_t, 2>{ UNINITIALIZED_MEMORY_RECORD, UNINITIALIZED_MEMORY_RECORD });
20 }
21 rom_arrays.emplace_back(new_transcript);
22 return rom_arrays.size() - 1;
23}
34template <typename ExecutionTrace>
36 const size_t rom_id,
37 const size_t index_value,
38 const uint32_t value_witness)
39{
40 BB_ASSERT_GT(rom_arrays.size(), rom_id);
41 RomTranscript& rom_array = rom_arrays[rom_id];
42 // First single-value op commits this array to the LogUp scheme. If pair ops have already been issued on
43 // this array, the mismatch is a programmer error (the two schemes coexist across arrays, not within one).
44 BB_ASSERT(rom_array.records.empty() || rom_array.use_logup,
45 "ROM array: single-value op issued on an array already using pair gates");
46 rom_array.use_logup = true;
47 const uint32_t index_witness =
48 (index_value == 0) ? builder->zero_idx() : builder->put_constant_variable((uint64_t)index_value);
49 BB_ASSERT_GT(rom_array.state.size(), index_value);
50 // Each index must be initialized exactly once: the LogUp argument relies on at most one table row per
51 // (array, index) key and does not enforce this in-circuit (see memory_relation.hpp).
52 BB_ASSERT_EQ(rom_array.state[index_value][0], UNINITIALIZED_MEMORY_RECORD);
53
54 RomRecord new_record{
55 .index_witness = index_witness,
56 .value_column1_witness = value_witness,
57 .value_column2_witness = builder->zero_idx(), // multiplicity placeholder; replaced at finalize
58 .index = static_cast<uint32_t>(index_value),
60 .record_witness = 0,
61 .gate_index = 0,
62 };
63 rom_array.state[index_value][0] = value_witness;
64 rom_array.state[index_value][1] = builder->zero_idx();
65 create_ROM_logup_gate(builder, new_record, rom_id, /*is_read=*/false);
66 rom_array.records.emplace_back(new_record);
67}
71template <typename ExecutionTrace>
73 const size_t rom_id,
74 const size_t index_value,
75 const std::array<uint32_t, 2>& value_witnesses)
76{
77 BB_ASSERT_GT(rom_arrays.size(), rom_id);
78 RomTranscript& rom_array = rom_arrays[rom_id];
79 BB_ASSERT(!rom_array.use_logup, "ROM array: pair op issued on an array already using LogUp single-value gates");
80 const uint32_t index_witness = builder->put_constant_variable((uint64_t)index_value);
81 BB_ASSERT_GT(rom_array.state.size(), index_value);
82 BB_ASSERT_EQ(rom_array.state[index_value][0], UNINITIALIZED_MEMORY_RECORD);
83 RomRecord new_record{
84 .index_witness = index_witness,
85 .value_column1_witness = value_witnesses[0],
86 .value_column2_witness = value_witnesses[1],
87 .index = static_cast<uint32_t>(index_value),
89 .record_witness = 0,
90 .gate_index = 0,
91 };
92 rom_array.state[index_value][0] = value_witnesses[0];
93 rom_array.state[index_value][1] = value_witnesses[1];
94 // `create_ROM_gate` fills in the `gate_index` of the `RamRecord`.
95 create_ROM_gate(builder, new_record);
96 rom_array.records.emplace_back(new_record);
97}
98
99template <typename ExecutionTrace>
101 const size_t rom_id,
102 const uint32_t index_witness)
103{
104 BB_ASSERT_GT(rom_arrays.size(), rom_id);
105 RomTranscript& rom_array = rom_arrays[rom_id];
106 BB_ASSERT(rom_array.use_logup,
107 "ROM array: single-value read issued before any set_ROM_element; or pair-ops already started");
108 const uint32_t index = static_cast<uint32_t>(uint256_t(builder->get_variable(index_witness)));
109 BB_ASSERT_GT(rom_array.state.size(), index);
110 BB_ASSERT(rom_array.state[index][0] != UNINITIALIZED_MEMORY_RECORD);
111 const auto value = builder->get_variable(rom_array.state[index][0]);
112 const uint32_t value_witness = builder->add_variable(value);
113 RomRecord new_record{
114 .index_witness = index_witness,
115 .value_column1_witness = value_witness,
116 .value_column2_witness = builder->zero_idx(),
117 .index = index,
118 .access_type = RomRecord::AccessType::READ,
119 .record_witness = 0,
120 .gate_index = 0,
121 };
122 create_ROM_logup_gate(builder, new_record, rom_id, /*is_read=*/true);
123 rom_array.records.emplace_back(new_record);
124
125 return value_witness;
126}
127
128template <typename ExecutionTrace>
130 const size_t rom_id,
131 const uint32_t index_witness)
132{
133 std::array<uint32_t, 2> value_witnesses;
134
135 const uint32_t index = static_cast<uint32_t>(uint256_t(builder->get_variable(index_witness)));
136 BB_ASSERT_GT(rom_arrays.size(), rom_id);
137 RomTranscript& rom_array = rom_arrays[rom_id];
138 BB_ASSERT(!rom_array.use_logup, "ROM array: pair read issued on an array already using LogUp single-value gates");
139 BB_ASSERT_GT(rom_array.state.size(), index);
140 BB_ASSERT(rom_array.state[index][0] != UNINITIALIZED_MEMORY_RECORD);
141 BB_ASSERT(rom_array.state[index][1] != UNINITIALIZED_MEMORY_RECORD);
142 const auto value1 = builder->get_variable(rom_array.state[index][0]);
143 const auto value2 = builder->get_variable(rom_array.state[index][1]);
144 value_witnesses[0] = builder->add_variable(value1);
145 value_witnesses[1] = builder->add_variable(value2);
146 RomRecord new_record{
147 .index_witness = index_witness,
148 .value_column1_witness = value_witnesses[0],
149 .value_column2_witness = value_witnesses[1],
150 .index = index,
151 .access_type = RomRecord::AccessType::READ,
152 .record_witness = 0,
153 .gate_index = 0,
154 };
155 create_ROM_gate(builder, new_record);
156 rom_array.records.emplace_back(new_record);
157
158 return value_witnesses;
159}
160
161// There is one important difference between `create_ROM_gate` and `create_sorted_ROM_gate`: we apply a different memory
162// selectors. We also only call `update_used_witnesses` for `record_witness` in the latter, but this is just for
163// Boomerang value detection.
164
165template <typename ExecutionTrace>
167{
168 // Record wire value can't yet be computed; it will be filled in later.
169 record.record_witness = builder->add_variable(FF(0));
170 {
171 auto row = builder->memory_selectors_row(CircuitBuilder::MEMORY_SELECTORS::ROM_READ);
172 row.wires = {
174 };
175 builder->blocks.memory.append_gate(row);
176 }
177 // Note: record the index into the memory block that contains the RAM/ROM gates
178 record.gate_index = builder->blocks.memory.size() - 1;
179 builder->increment_num_gates();
180}
181
182template <typename ExecutionTrace>
184{
185 record.record_witness = builder->add_variable(FF(0));
186 // record_witness is intentionally used only in a single gate
187 builder->update_used_witnesses(record.record_witness);
188 {
189 auto row = builder->memory_selectors_row(CircuitBuilder::MEMORY_SELECTORS::ROM_CONSISTENCY_CHECK);
190 row.wires = {
192 };
193 builder->blocks.memory.append_gate(row);
194 }
195 // Note: record the index into the memory block that contains the RAM/ROM gates
196 record.gate_index = builder->blocks.memory.size() - 1;
197 builder->increment_num_gates();
198}
199
200// ---- ROM LogUp gate creators ----
201//
202// ROM-LogUp wire layout (table-entry and read-access gates alike):
203// w_l = index, w_r = value, w_o = multiplicity (table) or zero (read), w_4 = inverse helper.
204// `record_witness` (w_4) is a placeholder filled by the prover in oink once `eta`/`rom_logup_gamma` are known.
205// `value_column2_witness` (w_o) is `zero_idx()` at creation time; on table rows `process_ROM_logup_array`
206// repoints the wire at a witness holding the read count `m_i` during finalization.
207// The q_c selector carries the ROM array id, which the memory relation folds into the LogUp fingerprint so
208// that reads are bound to the array they were issued against (the sum is global across the trace).
209// The gate index is appended to `builder->rom_logup_records` so the prover knows which rows need inverse-fill.
210
211template <typename ExecutionTrace>
213 RomRecord& record,
214 const size_t rom_id,
215 const bool is_read)
216{
217 // record_witness (w_4) holds the inverse helper, filled by the prover once challenges are known.
218 record.record_witness = builder->add_variable(FF(0));
219 builder->update_used_witnesses(record.record_witness);
220 {
221 auto row = builder->memory_selectors_row(is_read ? CircuitBuilder::MEMORY_SELECTORS::ROM_LOGUP_READ
222 : CircuitBuilder::MEMORY_SELECTORS::ROM_LOGUP_TABLE);
223 row.wires = {
225 };
226 // Tag the row with the array id via q_c. This is a precomputed selector, so the array
227 // separation is committed in the VK.
228 row.q_c = FF(static_cast<uint64_t>(rom_id));
229 builder->blocks.memory.append_gate(row);
230 }
231 record.gate_index = builder->blocks.memory.size() - 1;
232 builder->rom_logup_records.push_back(static_cast<uint32_t>(record.gate_index));
233 builder->increment_num_gates();
234}
235
236template <typename ExecutionTrace>
238{
239 auto& rom_array = rom_arrays[rom_id];
240 // when we process a given ROM array, we apply a "multiset equality check" between the records of the gates and then
241 // the records of the sorted gates. at the time of witness generation, the prover certainly knows the permutation;
242 // however, incarnating this with copy constraints would make the circuit (i.e., the VK) _witness dependent_.
243 const auto read_tag = builder->get_new_tag(); // current_tag + 1;
244 const auto sorted_list_tag = builder->get_new_tag(); // current_tag + 2;
245 builder->set_tau_transposition(read_tag, sorted_list_tag);
246
247 // Make sure that every cell has been initialized
248 for (size_t i = 0; i < rom_array.state.size(); ++i) {
249 BB_ASSERT_NEQ(rom_array.state[i][0], UNINITIALIZED_MEMORY_RECORD);
250 BB_ASSERT_NEQ(rom_array.state[i][1], UNINITIALIZED_MEMORY_RECORD);
251 }
252
253#ifdef NO_PAR_ALGOS
254 std::sort(rom_array.records.begin(), rom_array.records.end());
255#else
256 std::sort(std::execution::par_unseq, rom_array.records.begin(), rom_array.records.end());
257#endif
258
259 // The index-delta sub-relation has roots {0,-1} in the field, so a sorted chain starting at
260 // p-1 satisfies the delta=1 check when transitioning to 0. We symbolically pin the chain at
261 // both ends:
262 // - first sorted record's index_witness = zero_idx() (start of chain)
263 // - last sorted record's index_witness = put_constant_variable(N-1) (end of chain)
264 // Combined with the index-delta sub-relation, every sorted index is symbolically bounded in
265 // [0, state.size() - 1] without relying on the multiset / Schwartz-Zippel argument.
266 //
267 // We use put_constant_variable rather than emitting a separate big_add_gate so the cost is
268 // amortized: put_constant_variable caches by value, and the wire's value is bound via the
269 // permutation argument's copy constraint to the cached fix_witness gate. In the typical
270 // case where the constant N-1 is already used elsewhere in the circuit (as a bound, count,
271 // index, etc.), no new gate is emitted at all.
272 for (size_t i = 0; i < rom_array.records.size(); ++i) {
273 const RomRecord& record = rom_array.records[i];
274 const auto index = record.index;
275 const auto value1 = builder->get_variable(record.value_column1_witness);
276 const auto value2 = builder->get_variable(record.value_column2_witness);
277 uint32_t index_witness;
278 if (i == 0) {
279 // Start-of-chain pin: bound to the circuit's constant zero.
280 index_witness = builder->zero_idx();
281 } else if (i == rom_array.records.size() - 1) {
282 // End-of-chain pin: bound to the circuit constant state.size() - 1 via copy constraint.
283 index_witness = builder->put_constant_variable(static_cast<uint64_t>(rom_array.state.size()) - 1);
284 } else {
285 index_witness = builder->add_variable(FF((uint64_t)index));
286 builder->update_used_witnesses(index_witness);
287 }
288 const auto value1_witness = builder->add_variable(value1);
289 const auto value2_witness = builder->add_variable(value2);
290 // (the real values in) `sorted_record` will be identical to (those in) `record`, except with a different
291 // `gate_index` field, which will be filled out by `create_sorted_ROM_Gate`.
292 RomRecord sorted_record{
293 .index_witness = index_witness,
294 .value_column1_witness = value1_witness,
295 .value_column2_witness = value2_witness,
296 .index = index,
297 .access_type = record.access_type,
298 .record_witness = 0,
299 .gate_index = 0,
300 };
301 // the position of the sorted ROM gate in the execution trace depends on the witness data.
302 create_sorted_ROM_gate(builder, sorted_record);
303
304 builder->assign_tag(record.record_witness, read_tag);
305 builder->assign_tag(sorted_record.record_witness, sorted_list_tag);
306
307 // For ROM/RAM gates, the 'record' wire value (wire column 4) is a linear combination of the first 3 wire
308 // values. However, the record value uses the random challenge 'eta', generated after the first 3 wires are
309 // committed to. i.e., we can't compute the record witness here because we don't know what `eta` is! Take the
310 // gate indices of the two rom gates (original read gate + sorted gate) and store in `memory_records`. Once we
311 // generate the `eta` challenge, we'll use `memory_records` to figure out which gates need a record wire value
312 // to be computed.
313 //
314 // `record` (w4) = w3 * eta^3 + w2 * eta^2 + w1 * eta + read_write_flag (0 for reads, 1 for writes)
315 // Separate containers used to store gate indices of reads and writes. Need to differentiate because of
316 // `read_write_flag` (N.B. all ROM accesses are considered reads. Writes are for RAM operations)
317 builder->memory_read_records.push_back(static_cast<uint32_t>(sorted_record.gate_index));
318 builder->memory_read_records.push_back(static_cast<uint32_t>(record.gate_index));
319 }
320 // One of the checks we run on the sorted list is to validate the difference between the index field across two
321 // adjacent gates is either 0 or 1. To make this work with the last gate, we add a dummy gate at the end of the
322 // sorted list, where we set the first wire to equal `m + 1`, where `m` is the maximum allowed index in the sorted
323 // list. Moreover, as `m + 1` is a circuit constant, this ensures that the checks correctly constrain the sorted ROM
324 // gate chunks.
325 //
326 // N.B. We deliberately set max_index = state.size() (not state.size() - 1). The ROM consistency sub-relation
327 // adjacent_values_match_if_adjacent_indices_match = index_delta_is_zero * record_delta
328 // is gated on the *current* row's q_memory * q_1 * q_2, so it is active for the last sorted record. When the
329 // delta from last.index (= state.size() - 1, pinned above) to dummy.index (= state.size()) is -1,
330 // index_delta_is_zero evaluates to 0 and the values-match constraint is vacuous, as desired. If we instead set
331 // max_index = state.size() - 1, the delta would be 0 and the relation would force last.w4 == dummy.w4 = 0, which
332 // fails for any non-trivial ROM read.
333 FF max_index_value((uint64_t)rom_array.state.size());
334 uint32_t max_index = builder->add_variable(max_index_value);
335
336 builder->create_unconstrained_gate(
337 builder->blocks.memory, max_index, builder->zero_idx(), builder->zero_idx(), builder->zero_idx());
338 builder->create_big_add_gate(
339 {
340 max_index,
341 builder->zero_idx(),
342 builder->zero_idx(),
343 builder->zero_idx(),
344 1,
345 0,
346 0,
347 0,
348 -max_index_value,
349 },
350 false);
351}
352
353template <typename ExecutionTrace>
355{
356 auto& rom_array = rom_arrays[rom_id];
357 BB_ASSERT(rom_array.use_logup);
358
359 // Every cell must have been initialized. An uninitialized cell is a programmer error and would either
360 // leak an arbitrary witness or break the LogUp sum.
361 for (size_t i = 0; i < rom_array.state.size(); ++i) {
362 BB_ASSERT_NEQ(rom_array.state[i][0], UNINITIALIZED_MEMORY_RECORD);
363 }
364
365 // Derive each index's table-row gate and read count from the records.
366 std::vector<size_t> table_row_gate_indices(rom_array.state.size(), 0);
367 std::vector<uint64_t> read_counts(rom_array.state.size(), 0);
368 for (const auto& record : rom_array.records) {
369 if (record.access_type == RomRecord::AccessType::TABLE_ENTRY) {
370 table_row_gate_indices[record.index] = record.gate_index;
371 } else {
372 read_counts[record.index]++;
373 }
374 }
375
376 // Point each table row's w_o wire at a witness holding that row's read count. The wire vectors of the
377 // memory block are still mutable here (finalization has not yet handed them to trace construction), so
378 // this is the same mechanism as any finalize-time gate that adds witnesses with known values.
379 for (size_t i = 0; i < rom_array.state.size(); ++i) {
380 const uint32_t count_witness = builder->add_variable(FF(read_counts[i]));
381 builder->update_used_witnesses(count_witness);
382 builder->blocks.memory.w_o()[table_row_gate_indices[i]] = count_witness;
383 }
384}
385
387{
388 for (size_t i = 0; i < rom_arrays.size(); ++i) {
389 if (rom_arrays[i].use_logup) {
390 process_ROM_logup_array(builder, i);
391 } else {
392 process_ROM_array(builder, i);
393 }
394 }
395}
396
397template <typename ExecutionTrace> size_t RomRamLogic_<ExecutionTrace>::create_RAM_array(const size_t array_size)
398{
399 RamTranscript new_transcript;
400 for (size_t i = 0; i < array_size; ++i) {
401 new_transcript.state.emplace_back(UNINITIALIZED_MEMORY_RECORD);
402 }
403 ram_arrays.emplace_back(new_transcript);
404 return ram_arrays.size() - 1;
405}
421template <typename ExecutionTrace>
423 const size_t ram_id,
424 const size_t index_value,
425 const uint32_t value_witness)
426{
427 BB_ASSERT_GT(ram_arrays.size(), ram_id);
428 RamTranscript& ram_array = ram_arrays[ram_id];
429 const uint32_t index_witness =
430 (index_value == 0) ? builder->zero_idx() : builder->put_constant_variable((uint64_t)index_value);
431 BB_ASSERT_GT(ram_array.state.size(), index_value);
432 BB_ASSERT_EQ(ram_array.state[index_value], UNINITIALIZED_MEMORY_RECORD);
433 RamRecord new_record{ .index_witness = index_witness,
434 .timestamp_witness = builder->put_constant_variable((uint64_t)ram_array.access_count),
435 .value_witness = value_witness,
436 .index = static_cast<uint32_t>(index_value),
437 .timestamp = ram_array.access_count,
438 .access_type = RamRecord::AccessType::WRITE,
439 .record_witness = 0,
440 .gate_index = 0 };
441 ram_array.state[index_value] = value_witness;
442 BB_ASSERT_LT(ram_array.access_count, UINT32_MAX, "RAM access count overflow");
443 ram_array.access_count++;
444 // mutates the gate_index
445 create_RAM_gate(builder, new_record);
446 ram_array.records.emplace_back(new_record);
447}
448
449template <typename ExecutionTrace>
451 const size_t ram_id,
452 const uint32_t index_witness)
453{
454 BB_ASSERT_GT(ram_arrays.size(), ram_id);
455 RamTranscript& ram_array = ram_arrays[ram_id];
456 const uint32_t index = static_cast<uint32_t>(uint256_t(builder->get_variable(index_witness)));
457 BB_ASSERT_GT(ram_array.state.size(), index);
458 BB_ASSERT(ram_array.state[index] != UNINITIALIZED_MEMORY_RECORD);
459 const auto value = builder->get_variable(ram_array.state[index]);
460 const uint32_t value_witness = builder->add_variable(value);
461
462 RamRecord new_record{ .index_witness = index_witness,
463 .timestamp_witness = builder->put_constant_variable((uint64_t)ram_array.access_count),
464 .value_witness = value_witness,
465 .index = index,
466 .timestamp = ram_array.access_count,
467 .access_type = RamRecord::AccessType::READ,
468 .record_witness = 0,
469 .gate_index = 0 };
470
471 // mutates `gate_index`
472 create_RAM_gate(builder, new_record);
473 ram_array.records.emplace_back(new_record);
474
475 // increment ram array's access count
476 BB_ASSERT_LT(ram_array.access_count, UINT32_MAX, "RAM access count overflow");
477 ram_array.access_count++;
478
479 // return witness index of the value in the array
480 return value_witness;
481}
494template <typename ExecutionTrace>
496 const size_t ram_id,
497 const uint32_t index_witness,
498 const uint32_t value_witness)
499{
500 BB_ASSERT_GT(ram_arrays.size(), ram_id);
501 RamTranscript& ram_array = ram_arrays[ram_id];
502 const uint32_t index = static_cast<uint32_t>(uint256_t(builder->get_variable(index_witness)));
503 BB_ASSERT_GT(ram_array.state.size(), index);
504 BB_ASSERT(ram_array.state[index] != UNINITIALIZED_MEMORY_RECORD);
505
506 RamRecord new_record{ .index_witness = index_witness,
507 .timestamp_witness = builder->put_constant_variable((uint64_t)ram_array.access_count),
508 .value_witness = value_witness,
509 .index = index,
510 .timestamp = ram_array.access_count,
511 .access_type = RamRecord::AccessType::WRITE,
512 .record_witness = 0,
513 .gate_index = 0 };
514 // mutates `gate_index`
515 create_RAM_gate(builder, new_record);
516 ram_array.records.emplace_back(new_record);
517
518 // increment ram array's access count
519 BB_ASSERT_LT(ram_array.access_count, UINT32_MAX, "RAM access count overflow");
520 ram_array.access_count++;
521
522 // update Composer's current state of RAM array
523 ram_array.state[index] = value_witness;
524}
525
526template <typename ExecutionTrace>
528{
529 // Record wire value can't yet be computed (uses randomness generated during proof construction).
530 // However it needs a distinct witness index,
531 // we will be applying copy constraints + set membership constraints.
532 // Later on during proof construction we will compute the record wire value + assign it
533 record.record_witness = builder->add_variable(FF(0));
534 {
535 auto row = builder->memory_selectors_row(record.access_type == RamRecord::AccessType::READ
536 ? CircuitBuilder::MEMORY_SELECTORS::RAM_READ
537 : CircuitBuilder::MEMORY_SELECTORS::RAM_WRITE);
538 row.wires = { record.index_witness, record.timestamp_witness, record.value_witness, record.record_witness };
539 builder->blocks.memory.append_gate(row);
540 }
541
542 // Note: record the index into the block that contains the RAM/ROM gates
543 record.gate_index = builder->blocks.memory.size() - 1;
544 builder->increment_num_gates();
545}
546
547template <typename ExecutionTrace>
549{
550 record.record_witness = builder->add_variable(FF(0));
551 {
552 auto row = builder->memory_selectors_row(CircuitBuilder::MEMORY_SELECTORS::RAM_CONSISTENCY_CHECK);
553 row.wires = { record.index_witness, record.timestamp_witness, record.value_witness, record.record_witness };
554 builder->blocks.memory.append_gate(row);
555 }
556 // Note: record the index into the memory block that contains the RAM/ROM gates
557 record.gate_index = builder->blocks.memory.size() - 1;
558 builder->increment_num_gates();
559}
560
561template <typename ExecutionTrace>
563 RamRecord& record,
564 const size_t ram_array_size)
565{
566 record.record_witness = builder->add_variable(FF(0));
567 // Note: record the index into the block that contains the RAM/ROM gates
568 record.gate_index = builder->blocks.memory.size(); // no -1 since we _haven't_ added the gate yet
569
570 // Create a final gate with all selectors zero (hence unconstrained). In particular, the `MEMORY_SELECTORS` are not
571 // on. Wire values are accessed by the previous RAM gate via shifted wires.
572 builder->create_unconstrained_gate(builder->blocks.memory,
573 record.index_witness,
574 record.timestamp_witness,
575 record.value_witness,
576 record.record_witness);
577
578 // Create an add gate ensuring the final index is consistent with the size of the RAM array
579 builder->create_big_add_gate({
580 record.index_witness,
581 builder->zero_idx(),
582 builder->zero_idx(),
583 builder->zero_idx(),
584 1,
585 0,
586 0,
587 0,
588 -FF(static_cast<uint64_t>(ram_array_size) - 1),
589 });
590}
591
592// Gate cost of RAM interactions:
593// Currently, a circuit consisting predominantly of RAM interactions with 1 RAM array costs ~3.25 gates per
594// interaction:
595// 1. The memory gate itself (create_RAM_gate)
596// 2. Fixing the witness for the timestamp (put_constant_variable -> fix_witness, 1 gate per unique timestamp)
597// 3. Sorted memory gate (create_sorted_RAM_gate)
598// 4. 0.25 for the range constraint on the timestamp delta in the sorted memory gate
599//
600// Potential optimization: If we delay the creation of memory gates until circuit finalisation, we can eliminate step 2.
601// We would add the relation `timestamp_omega - timestamp - 1 == 0` into the RAM memory gate and fix the first timestamp
602// to 0 or 1. This would reduce the cost to 2.25 gates per interaction + 1 fix_witness + 1 waste gate after the memory
603// gates.
604
605template <typename ExecutionTrace>
607{
608 RamTranscript& ram_array = ram_arrays[ram_id];
609 const auto access_tag = builder->get_new_tag(); // current_tag + 1;
610 const auto sorted_list_tag = builder->get_new_tag(); // current_tag + 2;
611 // when we process a given RAM array, we apply a "multiset equality check" between the records of the gates and then
612 // the records of the sorted gates. at the time of witness generation, the prover certainly knows the permutation;
613 // however, incarnating this with copy constraints would make the circuit (i.e., the VK) _witness dependent_.
614 builder->set_tau_transposition(access_tag, sorted_list_tag);
615
616 // NOTE: we simply assert that all cells have been initialized. The circuit should initialize all RAM elements to
617 // prevent witness-dependent constraints. For example, if a RAM record is uninitialized but the index of that record
618 // is a function of witness data (e.g. public/private inputs), different public inputs will produce different
619 // circuit constraints, and in particular VKs will not be independent of witness generation.
620 for (size_t i = 0; i < ram_array.state.size(); ++i) {
621 BB_ASSERT_NEQ(ram_array.state[i], UNINITIALIZED_MEMORY_RECORD);
622 }
623
624#ifdef NO_PAR_ALGOS
625 std::sort(ram_array.records.begin(), ram_array.records.end());
626#else
627 std::sort(std::execution::par_unseq, ram_array.records.begin(), ram_array.records.end());
628#endif
629
630 std::vector<RamRecord> sorted_ram_records;
631
632 // Iterate over all but final RAM record. This is because one of the checks for the "interior" RAM gates is that the
633 // next gate is also a RAM gate. We therfore apply a simplified check for the last gate.
634 //
635 // The index-delta sub-relation has roots {0,-1} in the field, so a sorted chain starting at
636 // p-1 satisfies the delta=1 check when transitioning to 0. Pin the first sorted gate's
637 // index_witness to zero_idx() so that a malicious prover cannot start the chain at p-1.
638 for (size_t i = 0; i < ram_array.records.size(); ++i) {
639 const RamRecord& record = ram_array.records[i];
640
641 const auto index = record.index;
642 const auto value = builder->get_variable(record.value_witness);
643 // For the first sorted record (i==0, index must be 0 after sorting), bind to zero_idx()
644 // rather than a fresh add_variable so the witness is copy-constrained to the constant zero.
645 const uint32_t index_witness =
646 (i == 0) ? builder->zero_idx() : builder->add_variable(FF(static_cast<uint64_t>(index)));
647 const auto timestamp_witess = builder->add_variable(FF(record.timestamp));
648 const auto value_witness = builder->add_variable(value);
649 // (the values in) `sorted_record` will be identical to (the values in) `record`, except with a different
650 // `gate_index` field, which will be fixed by `create_sorted_RAM_Gate` (resp. `created_final_sorted_RAM_Gate`).
651 RamRecord sorted_record{
652 .index_witness = index_witness,
653 .timestamp_witness = timestamp_witess,
654 .value_witness = value_witness,
655 .index = index,
656 .timestamp = record.timestamp,
657 .access_type = record.access_type,
658 .record_witness = 0,
659 .gate_index = 0,
660 };
661
662 // create a list of sorted ram records
663 sorted_ram_records.emplace_back(sorted_record);
664
665 // We don't apply the RAM consistency check gate to the final record,
666 // as this gate expects a RAM record to be present at the next gate
667 if (i < ram_array.records.size() - 1) {
668 create_sorted_RAM_gate(builder, sorted_record);
669 } else {
670 // For the final record in the sorted list, we do not apply the full consistency check gate.
671 // Only need to check the index value = RAM array size - 1.
672 create_final_sorted_RAM_gate(builder, sorted_record, ram_array.state.size());
673 }
674
675 // Assign record/sorted records to tags that we will perform set equivalence checks on
676 builder->assign_tag(record.record_witness, access_tag);
677 builder->assign_tag(sorted_record.record_witness, sorted_list_tag);
678
679 // For ROM/RAM gates, the 'record' wire value (wire column 4) is a linear combination of the first 3 wire
680 // values. However, the record value uses the random challenge 'eta', generated after the first 3 wires are
681 // committed to. i.e. we can't compute the record witness here because we don't know what `eta` is!
682 //
683 // Take the gate indices of the two rom gates (original read gate + sorted gate) and store in `memory_records`.
684 // Once we generate the `eta` challenge, we'll use `memory_records` to figure out which gates need a record wire
685 // value to be computed.
686
687 switch (record.access_type) {
689 builder->memory_read_records.push_back(static_cast<uint32_t>(sorted_record.gate_index));
690 builder->memory_read_records.push_back(static_cast<uint32_t>(record.gate_index));
691 break;
692 }
694 builder->memory_write_records.push_back(static_cast<uint32_t>(sorted_record.gate_index));
695 builder->memory_write_records.push_back(static_cast<uint32_t>(record.gate_index));
696 break;
697 }
698 default: {
699 throw_or_abort("Unexpected record.access_type."); // shouldn't get here!
700 }
701 }
702 }
703
704 // Step 2: Create gates that validate correctness of RAM timestamps
705
706 std::vector<uint32_t> timestamp_deltas;
707 // Guard against empty sorted_ram_records (e.g., RAM array of size 0)
708 if (sorted_ram_records.size() <= 1) {
709 return;
710 }
711 for (size_t i = 0; i < sorted_ram_records.size() - 1; ++i) {
712 const auto& current = sorted_ram_records[i];
713 const auto& next = sorted_ram_records[i + 1];
714
715 const bool share_index = current.index == next.index;
716
717 FF timestamp_delta = 0;
718 if (share_index) {
719 BB_ASSERT_GT(next.timestamp, current.timestamp);
720 timestamp_delta = FF(next.timestamp - current.timestamp);
721 }
722
723 uint32_t timestamp_delta_witness = builder->add_variable(timestamp_delta);
724 // note that the `index_witness` and `timestamp_witness` are taken from `current`. This means that there are
725 // copy constraints, which will mean that once we constrain the sorted gates to be in lexicographic order,
726 // these gates will _automatically_ be in lexicographic order.
727 {
728 auto row = builder->memory_selectors_row(CircuitBuilder::MEMORY_SELECTORS::RAM_TIMESTAMP_CHECK);
729 row.wires = {
730 current.index_witness, current.timestamp_witness, timestamp_delta_witness, builder->zero_idx()
731 };
732 builder->blocks.memory.append_gate(row);
733 }
734
735 builder->increment_num_gates();
736
737 // store timestamp offsets for later. Need to apply range checks to them, but calling
738 // `create_new_range_constraint` can add gates, which could ruin the structure of our sorted timestamp list.
739 timestamp_deltas.push_back(timestamp_delta_witness);
740 }
741
742 // add the index/timestamp values of the last sorted record in an empty add gate.
743 // (the previous gate will access the wires on this gate and requires them to be those of the last record)
744 const auto& last = sorted_ram_records[ram_array.records.size() - 1];
745 builder->create_unconstrained_gate(
746 builder->blocks.memory, last.index_witness, last.timestamp_witness, builder->zero_idx(), builder->zero_idx());
747
748 // Step 3: validate that the successive difference of timestamp_witnesses for the same index are
749 // monotonically increasing. We do this as follows:
750 // - we enforce that each timestamp_delta is <= maximum timestamp.
751 // - in the relation, we enforce that if two consecutive gates have the same index, then the timestamp_delta is
752 // equal to the difference between the timestamp_witness of the current gate and the timestamp_witness of the
753 // next gate
754 // The combination of the above checks implies
755 // timestamp_witness_i - timestamp_witness_{i+1} = timestamp_delta < max_timestamp
756 // To conclude that timestamp_witness_i - timestamp_witness_{i+1} > 0 we leverage the way our circuits are
757 // constructed: the timestamp_witness in each RAM record is set equal to a fixed witness holding the value
758 // ram_array.access_count. Hence, sorted gates = unsorted gates contain each timestamp value between 0 and
759 // ram_array.access_count - 1 exactly once. This means
760 // | timestamp_witness_i - timestamp_witness_{i+1} | < 2 * ram_array.access_count << (1 << 256)
761 // and therefore if (timestamp_witness_i - timestamp_witness_{i+1}) < max_timestamp < (1 << 32) then there is no
762 // wraparound because
763 // (1 << 256) - timestamp_witness_i + timestamp_witness_{i+1} > (1 << 256) - 2 * max_timestamp
764 // Hence, timestamp_witness_i - timestamp_witness_{i+1} >= 0, and as all the timestamp witnesses are different by
765 // construction, we get timestamp_witness_i - timestamp_witness_{i+1} > 0.
766 const uint32_t max_timestamp = ram_array.access_count - 1;
767 for (auto& w : timestamp_deltas) {
768 builder->create_small_range_constraint(w, max_timestamp);
769 }
770}
771
773{
774 for (size_t i = 0; i < ram_arrays.size(); ++i) {
775 process_RAM_array(builder, i);
776 }
777}
778
779// Template instantiations
782
783} // namespace bb
#define BB_ASSERT(expression,...)
Definition assert.hpp:70
#define BB_ASSERT_GT(left, right,...)
Definition assert.hpp:113
#define BB_ASSERT_NEQ(actual, expected,...)
Definition assert.hpp:98
#define BB_ASSERT_EQ(actual, expected,...)
Definition assert.hpp:83
#define BB_ASSERT_LT(left, right,...)
Definition assert.hpp:143
bb::field< bb::Bn254FrParams > FF
Definition field.cpp:24
ROM/RAM logic handler for UltraCircuitBuilder.
size_t create_ROM_array(const size_t array_size)
Create a new read-only memory region.
uint32_t read_ROM_array(CircuitBuilder *builder, const size_t rom_id, const uint32_t index_witness)
Read a single element from ROM.
void process_ROM_array(CircuitBuilder *builder, const size_t rom_id)
Compute additional gates required to validate ROM reads. Called when generating the proving key.
void process_ROM_logup_array(CircuitBuilder *builder, const size_t rom_id)
Finalize a LogUp-style ROM array.
void create_sorted_RAM_gate(CircuitBuilder *builder, RamRecord &record)
Gate that performs consistency checks to validate that a claimed RAM read/write value is correct.
void process_ROM_arrays(CircuitBuilder *builder)
Process all of the ROM arrays.
std::array< uint32_t, 2 > read_ROM_array_pair(CircuitBuilder *builder, const size_t rom_id, const uint32_t index_witness)
Read a pair of elements from ROM.
void set_ROM_element(CircuitBuilder *builder, const size_t rom_id, const size_t index_value, const uint32_t value_witness)
Initialize a rom cell to equal value_witness
void create_final_sorted_RAM_gate(CircuitBuilder *builder, RamRecord &record, const size_t ram_array_size)
Performs consistency checks to validate that a claimed RAM read/write value is correct....
void create_ROM_logup_gate(CircuitBuilder *builder, RomRecord &record, const size_t rom_id, const bool is_read)
Emit a ROM-LogUp gate (table entry or read access) at circuit construction time.
void process_RAM_arrays(CircuitBuilder *builder)
void init_RAM_element(CircuitBuilder *builder, const size_t ram_id, const size_t index_value, const uint32_t value_witness)
Initialize a RAM cell to equal value_witness
void create_sorted_ROM_gate(CircuitBuilder *builder, RomRecord &record)
Gate that performs consistency checks to validate that a claimed ROM read value is correct.
void write_RAM_array(CircuitBuilder *builder, const size_t ram_id, const uint32_t index_witness, const uint32_t value_witness)
Write a cell in a RAM array.
void set_ROM_element_pair(CircuitBuilder *builder, const size_t rom_id, const size_t index_value, const std::array< uint32_t, 2 > &value_witnesses)
Initialize a ROM array element with a pair of witness values.
void create_ROM_gate(CircuitBuilder *builder, RomRecord &record)
Gate that'reads' from a ROM table, i.e., the table index is a witness not precomputed.
uint32_t read_RAM_array(CircuitBuilder *builder, const size_t ram_id, const uint32_t index_witness)
typename ExecutionTrace::FF FF
void create_RAM_gate(CircuitBuilder *builder, RamRecord &record)
Gate that performs a read/write operation into a RAM table, i.e. table index is a witness not precomp...
void process_RAM_array(CircuitBuilder *builder, const size_t ram_id)
Compute additional gates required to validate RAM read/writes. Called when generating the proving key...
size_t create_RAM_array(const size_t array_size)
Create a new updatable memory region.
AluTraceBuilder builder
Definition alu.test.cpp:124
Entry point for Barretenberg command-line interface.
Definition api.hpp:5
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
A RAM memory record that can be ordered, first by index, then by timestamp.
uint32_t index_witness
uint32_t value_witness
AccessType access_type
uint32_t record_witness
uint32_t timestamp_witness
RamTranscript contains the RamRecords for a particular RAM table (recording READ and WRITE operations...
std::vector< RamRecord > records
std::vector< uint32_t > state
A ROM memory record that can be ordered, where the ordering is given by the index (a....
uint32_t value_column1_witness
uint32_t index_witness
uint32_t record_witness
uint32_t value_column2_witness
AccessType access_type
RomTranscript contains the RomRecords for a particular ROM table as well as the vector whose ith entr...
std::vector< std::array< uint32_t, 2 > > state
std::vector< RomRecord > records
void throw_or_abort(std::string const &err)