Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
ultra_circuit_checker.cpp
Go to the documentation of this file.
5#include <unordered_set>
6
7namespace bb {
8
9template <> auto UltraCircuitChecker::init_empty_values<UltraCircuitBuilder_<UltraExecutionTraceBlocks>>()
10{
12}
13
14template <> auto UltraCircuitChecker::init_empty_values<MegaCircuitBuilder_<bb::fr>>()
15{
16 return MegaFlavor::AllValues{};
17}
18
19template <>
22{
23 // Create a copy of the input circuit
25 if (!builder.circuit_finalized) { // avoid warnings about finalizing an already finalized circuit
27 }
28
29 return builder;
30}
31
32template <>
33MegaCircuitBuilder_<bb::fr> UltraCircuitChecker::prepare_circuit<MegaCircuitBuilder_<bb::fr>>(
34 const MegaCircuitBuilder_<bb::fr>& builder_in)
35{
36 // Create a copy of the input circuit
37 MegaCircuitBuilder_<bb::fr> builder{ builder_in };
38
39 // Deepcopy the opqueue to avoid modifying the original one
40 builder.op_queue = std::make_shared<ECCOpQueue>(*builder.op_queue);
41
42 if (!builder.circuit_finalized) { // avoid warnings about finalizing an already finalized circuit
43 builder.finalize_circuit();
44 }
45
46 return builder;
47}
48
49template <typename Builder> bool UltraCircuitChecker::check(const Builder& builder_in)
50{
51 if (builder_in.failed()) {
52 info("CircuitChecker: circuit contains invalid witnesses: ", builder_in.err());
53 }
54
56
57 // Construct a hash table for lookup table entries to efficiently determine if a lookup gate is valid
58 LookupHashTable lookup_hash_table;
59 for (const auto& table : builder.get_lookup_tables()) {
60 const FF table_index(table.table_index);
61 for (size_t i = 0; i < table.size(); ++i) {
62 lookup_hash_table.insert({ table.column_1[i], table.column_2[i], table.column_3[i], table_index });
63 }
64 }
65
66 // Instantiate structs used for checking tag and memory record correctness
67 TagCheckData tag_data;
68 MemoryCheckData memory_data{ builder };
69
70 bool result = true;
71 size_t block_idx = 0;
72 for (auto& block : builder.blocks.get()) {
73 result = result && check_block(builder, block, tag_data, memory_data, lookup_hash_table);
74 if (!result) {
75#ifndef FUZZING_DISABLE_WARNINGS
76 info("Failed at block idx = ", block_idx);
77#else
78 (void)block_idx;
79#endif
80 return false;
81 }
82 block_idx++;
83 }
84
85#ifdef ULTRA_FUZZ
86 result = result & relaxed_check_delta_range_relation(builder);
87 if (!result) {
88 return false;
89 }
90 result = result & relaxed_check_memory_relation(builder);
91 if (!result) {
92 return false;
93 }
94#endif
95#ifndef ULTRA_FUZZ
96 // Tag check is only expected to pass after entire execution trace (all blocks) have been processed
97 result = result && check_tag_data(tag_data);
98 if (!result) {
99 info("Failed tag check.");
100 return false;
101 }
102#endif
103
104 return result;
105};
106
107template <typename Builder>
109 auto& block,
110 TagCheckData& tag_data,
111 MemoryCheckData& memory_data,
112 LookupHashTable& lookup_hash_table)
113{
114 // Initialize empty AllValues of the correct Flavor based on Builder type; for input to Relation::accumulate
115 auto values = init_empty_values<Builder>();
116 Params params;
117 params.eta = memory_data.eta; // used in Memory relation for RAM/ROM consistency
118 params.eta_two = memory_data.eta_two;
119 params.eta_three = memory_data.eta_three;
120
121 auto report_fail = [&](const char* message, size_t row_idx) {
122#ifndef FUZZING_DISABLE_WARNINGS
123 info(message, row_idx);
124#else
125 (void)message;
126 (void)row_idx;
127#endif
128#ifdef CHECK_CIRCUIT_STACKTRACES
129 block.stack_traces.print(row_idx);
130#endif
131 return false;
132 };
133
134 // Perform checks on each gate defined in the builder
135 bool result = true;
136 for (size_t idx = 0; idx < block.size(); ++idx) {
137
138 populate_values(builder, block, values, tag_data, memory_data, idx);
139
140 result = result && check_relation<Arithmetic>(values, params);
141 if (!result) {
142 return report_fail("Failed Arithmetic relation at row idx = ", idx);
143 }
144 result = result && check_relation<Elliptic>(values, params);
145 if (!result) {
146 return report_fail("Failed Elliptic relation at row idx = ", idx);
147 }
148#ifndef ULTRA_FUZZ
149 result = result && check_relation<Memory>(values, params);
150 if (!result) {
151 return report_fail("Failed Memory relation at row idx = ", idx);
152 }
153 result = result && check_relation<NonNativeField>(values, params);
154 if (!result) {
155 return report_fail("Failed NonNativeField relation at row idx = ", idx);
156 }
157 result = result && check_relation<DeltaRangeConstraint>(values, params);
158 if (!result) {
159 return report_fail("Failed DeltaRangeConstraint relation at row idx = ", idx);
160 }
161#else
162 // Bigfield related nnf gates
163 if (values.q_nnf == 1) {
164 bool f0 = values.q_o == 1 && (values.q_4 == 1 || values.q_m == 1);
165 bool f1 = values.q_r == 1 && (values.q_o == 1 || values.q_4 == 1 || values.q_m == 1);
166 if (f0 && f1) {
167 result = result && check_relation<NonNativeField>(values, params);
168 if (!result) {
169 return report_fail("Failed NonNativeField relation at row idx = ", idx);
170 }
171 }
172 }
173#endif
174 result = result && check_lookup(values, lookup_hash_table);
175 if (!result) {
176 return report_fail("Failed Lookup check relation at row idx = ", idx);
177 }
178 if constexpr (!IsMegaBuilder<Builder>) {
179 // Mega covers all internal rounds via the compressed block; there is no
180 // q_poseidon2_internal selector in MegaFlavor.
181 result = result && check_relation<PoseidonInternal>(values, params);
182 if (!result) {
183 return report_fail("Failed PoseidonInternal relation at row idx = ", idx);
184 }
185 }
186 result = result && check_relation<PoseidonExternal>(values, params);
187 if (!result) {
188 return report_fail("Failed PoseidonExternal relation at row idx = ", idx);
189 }
190
191 if constexpr (IsMegaBuilder<Builder>) {
192 result = result && check_relation<PoseidonInitialExternal>(values, params);
193 if (!result) {
194 return report_fail("Failed PoseidonInitialExternal relation at row idx = ", idx);
195 }
196 result = result && check_relation<PoseidonQuadInternal>(values, params);
197 if (!result) {
198 return report_fail("Failed PoseidonQuadInternal relation at row idx = ", idx);
199 }
200 result = result && check_relation<PoseidonQuadInternalTerminal>(values, params);
201 if (!result) {
202 return report_fail("Failed PoseidonQuadInternalTerminal relation at row idx = ", idx);
203 }
204 result = result && check_relation<PoseidonTransitionEntry>(values, params);
205 if (!result) {
206 return report_fail("Failed PoseidonTransitionEntry relation at row idx = ", idx);
207 }
208 result = result && check_databus_read(values, builder);
209 if (!result) {
210 return report_fail("Failed databus read at row idx = ", idx);
211 }
212 // Note: EccOpQueueRelation is not checked here because it simply establishes that the ecc_op_wire
213 // polynomials contain copies of the conventional wire data in the ecc_op region (and are zero elsewhere) so
214 // there is nothing to check at the level of the builder.
215 }
216 if (!result) {
217 return report_fail("Failed at row idx = ", idx);
218 }
219 }
220
221 return result;
222};
223
224template <typename Relation> bool UltraCircuitChecker::check_relation(auto& values, auto& params)
225{
226 // Define zero initialized array to store the evaluation of each sub-relation
227 using SubrelationEvaluations = typename Relation::SumcheckArrayOfValuesOverSubrelations;
228 SubrelationEvaluations subrelation_evaluations;
229 for (auto& eval : subrelation_evaluations) {
230 eval = 0;
231 }
232
233 // Evaluate each subrelation in the relation
234 Relation::accumulate(subrelation_evaluations, values, params, /*scaling_factor=*/1);
235
236 // Ensure each subrelation evaluates to zero
237 for (auto& eval : subrelation_evaluations) {
238 if (eval != 0) {
239 return false;
240 }
241 }
242 return true;
243}
244
245bool UltraCircuitChecker::check_lookup(auto& values, auto& lookup_hash_table)
246{
247 // If this is a lookup gate, check the inputs are in the hash table containing all table entries
248 if (!values.q_lookup.is_zero()) {
249 return lookup_hash_table.contains({ values.w_l + values.q_r * values.w_l_shift,
250 values.w_r + values.q_m * values.w_r_shift,
251 values.w_o + values.q_c * values.w_o_shift,
252 values.q_o });
253 }
254 return true;
255};
256
257template <typename Builder> bool UltraCircuitChecker::check_databus_read(auto& values, Builder& builder)
258{
259 if (!values.q_busread.is_zero()) {
260 // Extract the {index, value} pair from the read gate inputs
261 auto raw_read_idx = static_cast<size_t>(uint256_t(values.w_r));
262 auto value = values.w_l;
263
264 // Map bus_idx → wire-linear selector on the values struct (mirrors BusData<i>::selector in the relation).
265 const std::array<const FF*, NUM_BUS_COLUMNS> bus_selectors{
266 &values.q_l, &values.q_r, &values.q_o, &values.q_4, &values.q_m
267 };
268
269 // Locate the bus column being read (exactly one selector should be active on a busread row) and look up the
270 // expected value from the builder's bus vector.
271 FF bus_value{};
272 bool read_matched = false;
273 for (size_t bus_idx = 0; bus_idx < NUM_BUS_COLUMNS; ++bus_idx) {
274 if (*bus_selectors[bus_idx] == 1) {
275 const auto& bus_vec = builder.get_bus_vector(bus_idx);
276 bus_value = builder.get_variable(bus_vec[raw_read_idx]);
277 read_matched = true;
278 }
279 }
280 BB_ASSERT(read_matched);
281 return (value == bus_value);
282 }
283 return true;
284};
285
287{
288 return tag_data.left_product == tag_data.right_product;
289};
290
291template <typename Builder>
293 Builder& builder, auto& block, auto& values, TagCheckData& tag_data, MemoryCheckData& memory_data, size_t idx)
294{
295 // Function to quickly update tag products and encountered variable set by index and value
296 auto update_tag_check_data = [&](const size_t variable_index, const FF& value) {
297 size_t real_index = builder.real_variable_index[variable_index];
298 // Check to ensure that we are not including a variable twice
299 if (tag_data.encountered_variables.contains(real_index)) {
300 return;
301 }
302 uint32_t tag_in = builder.real_variable_tags[real_index];
303 if (tag_in != DEFAULT_TAG) {
304 uint32_t tag_out = builder.tau().at(tag_in);
305 tag_data.left_product *= value + tag_data.gamma * FF(tag_in);
306 tag_data.right_product *= value + tag_data.gamma * FF(tag_out);
307 tag_data.encountered_variables.insert(real_index);
308 }
309 };
310
311 // A lambda function for computing a memory record term of the form w3 * eta_three + w2 * eta_two + w1 * eta
312 auto compute_memory_record_term =
313 [](const FF& w_1, const FF& w_2, const FF& w_3, const FF& eta, const FF& eta_two, FF& eta_three) {
314 return (w_3 * eta_three + w_2 * eta_two + w_1 * eta);
315 };
316
317 // Set wire values. Wire 4 is treated specially since it may contain memory records
318 values.w_l = builder.get_variable(block.w_l()[idx]);
319 values.w_r = builder.get_variable(block.w_r()[idx]);
320 values.w_o = builder.get_variable(block.w_o()[idx]);
321 // Note: memory_data contains indices into the block to which RAM/ROM gates were added so we need to check that
322 // we are indexing into the correct block before updating the w_4 value.
323 const bool is_ram_rom_block = (&block == &builder.blocks.memory);
324 if (is_ram_rom_block && memory_data.read_record_gates.contains(idx)) {
325 values.w_4 = compute_memory_record_term(
326 values.w_l, values.w_r, values.w_o, memory_data.eta, memory_data.eta_two, memory_data.eta_three);
327 } else if (is_ram_rom_block && memory_data.write_record_gates.contains(idx)) {
328 values.w_4 =
329 compute_memory_record_term(
330 values.w_l, values.w_r, values.w_o, memory_data.eta, memory_data.eta_two, memory_data.eta_three) +
331 FF::one();
332 } else {
333 values.w_4 = builder.get_variable(block.w_4()[idx]);
334 }
335
336 // Set shifted wire values. Again, wire 4 is treated specially. On final row, set shift values to zero
337 if (idx < block.size() - 1) {
338 values.w_l_shift = builder.get_variable(block.w_l()[idx + 1]);
339 values.w_r_shift = builder.get_variable(block.w_r()[idx + 1]);
340 values.w_o_shift = builder.get_variable(block.w_o()[idx + 1]);
341 if (is_ram_rom_block && memory_data.read_record_gates.contains(idx + 1)) {
342 values.w_4_shift = compute_memory_record_term(values.w_l_shift,
343 values.w_r_shift,
344 values.w_o_shift,
345 memory_data.eta,
346 memory_data.eta_two,
347 memory_data.eta_three);
348 } else if (is_ram_rom_block && memory_data.write_record_gates.contains(idx + 1)) {
349 values.w_4_shift = compute_memory_record_term(values.w_l_shift,
350 values.w_r_shift,
351 values.w_o_shift,
352 memory_data.eta,
353 memory_data.eta_two,
354 memory_data.eta_three) +
355 FF::one();
356 } else {
357 values.w_4_shift = builder.get_variable(block.w_4()[idx + 1]);
358 }
359 } else {
360 values.w_l_shift = 0;
361 values.w_r_shift = 0;
362 values.w_o_shift = 0;
363 values.w_4_shift = 0;
364 }
365
366 // Update tag check data
367 update_tag_check_data(block.w_l()[idx], values.w_l);
368 update_tag_check_data(block.w_r()[idx], values.w_r);
369 update_tag_check_data(block.w_o()[idx], values.w_o);
370 update_tag_check_data(block.w_4()[idx], values.w_4);
371
372 // Set selector values
373 values.q_m = block.q_m()[idx];
374 values.q_c = block.q_c()[idx];
375 values.q_l = block.q_1()[idx];
376 values.q_r = block.q_2()[idx];
377 values.q_o = block.q_3()[idx];
378 values.q_4 = block.q_4()[idx];
379 values.q_arith = read_gate_selector(block, GateKind::Arith, idx);
380 values.q_delta_range = read_gate_selector(block, GateKind::DeltaRange, idx);
381 values.q_elliptic = read_gate_selector(block, GateKind::Elliptic, idx);
382 values.q_memory = read_gate_selector(block, GateKind::Memory, idx);
383 values.q_nnf = read_gate_selector(block, GateKind::Nnf, idx);
384 values.q_lookup = read_gate_selector(block, GateKind::Lookup, idx);
385 values.q_poseidon2_external = read_gate_selector(block, GateKind::Poseidon2Ext, idx);
386 if constexpr (IsMegaBuilder<Builder>) {
387 values.q_5 = block.q_5()[idx];
388 values.q_poseidon2_external_initial = read_gate_selector(block, GateKind::Poseidon2ExtInitial, idx);
389 values.q_poseidon2_quad_internal = read_gate_selector(block, GateKind::Poseidon2QuadInt, idx);
390 values.q_poseidon2_quad_internal_terminal = read_gate_selector(block, GateKind::Poseidon2QuadIntTerminal, idx);
391 values.q_poseidon2_transition_entry = read_gate_selector(block, GateKind::Poseidon2TransitionEntry, idx);
392 values.q_busread = read_gate_selector(block, GateKind::BusRead, idx);
393 } else {
394 values.q_poseidon2_internal = read_gate_selector(block, GateKind::Poseidon2Int, idx);
395 }
396}
397
398#ifdef ULTRA_FUZZ
399
411template <typename Builder> bool UltraCircuitChecker::relaxed_check_delta_range_relation(Builder& builder)
412{
413 std::unordered_map<uint32_t, uint64_t> range_tags;
414 for (const auto& list : builder.range_lists) {
415 range_tags[list.second.range_tag] = list.first;
416 }
417
418 // Unprocessed blocks check
419 for (uint32_t i = 0; i < builder.real_variable_tags.size(); i++) {
420 uint32_t tag = builder.real_variable_tags[i];
421 if (tag != 0 && range_tags.contains(tag)) {
422 uint256_t range = static_cast<uint256_t>(range_tags[tag]);
423 uint256_t value = static_cast<uint256_t>(builder.get_variable(i));
424 if (value > range) {
425#ifndef FUZZING_DISABLE_WARNINGS
426 info("Failed range constraint on variable with index = ", i, ": ", value, " > ", range);
427#endif
428 return false;
429 }
430 }
431 }
432
433 // Processed blocks check
434 auto block = builder.blocks.delta_range;
435 for (size_t idx = 0; idx < block.size(); idx++) {
436 if (block.gate_selector_for(GateKind::DeltaRange)[idx] == 0) {
437 continue;
438 }
439 bb::fr w1 = builder.get_variable(block.w_l()[idx]);
440 bb::fr w2 = builder.get_variable(block.w_r()[idx]);
441 bb::fr w3 = builder.get_variable(block.w_o()[idx]);
442 bb::fr w4 = builder.get_variable(block.w_4()[idx]);
443 bb::fr w5 = idx == block.size() - 1 ? builder.get_variable(0) : builder.get_variable(block.w_l()[idx + 1]);
444
445 uint256_t delta = static_cast<uint256_t>(w2 - w1);
446 if (delta > 3) {
447#ifndef FUZZING_DISABLE_WARNINGS
448 info("Failed sort constraint relation at row idx = ", idx, " with delta1 = ", delta);
449 info(w1 - w2);
450#endif
451 return false;
452 }
453 delta = static_cast<uint256_t>(w3 - w2);
454 if (delta > 3) {
455#ifndef FUZZING_DISABLE_WARNINGS
456 info("Failed sort constraint relation at row idx = ", idx, " with delta2 = ", delta);
457#endif
458 return false;
459 }
460 delta = static_cast<uint256_t>(w4 - w3);
461 if (delta > 3) {
462#ifndef FUZZING_DISABLE_WARNINGS
463 info("Failed sort constraint at row idx = ", idx, " with delta3 = ", delta);
464#endif
465 return false;
466 }
467 delta = static_cast<uint256_t>(w5 - w4);
468 if (delta > 3) {
469#ifndef FUZZING_DISABLE_WARNINGS
470 info("Failed sort constraint at row idx = ", idx, " with delta4 = ", delta);
471#endif
472 return false;
473 }
474 }
475 return true;
476}
477
491template <typename Builder> bool UltraCircuitChecker::relaxed_check_memory_relation(Builder& builder)
492{
493 for (size_t i = 0; i < builder.rom_ram_logic.rom_arrays.size(); i++) {
494 auto rom_array = builder.rom_ram_logic.rom_arrays[i];
495
496 // check set and read ROM records
497 for (auto& rr : rom_array.records) {
498 uint32_t value_witness_1 = rr.value_column1_witness;
499 uint32_t value_witness_2 = rr.value_column2_witness;
500 uint32_t index = static_cast<uint32_t>(builder.get_variable(rr.index_witness));
501
502 uint32_t table_witness_1 = rom_array.state[index][0];
503 uint32_t table_witness_2 = rom_array.state[index][1];
504
505 if (builder.get_variable(value_witness_1) != builder.get_variable(table_witness_1)) {
506#ifndef FUZZING_DISABLE_WARNINGS
507 info("Failed SET/Read ROM[0] in table = ", i, " at idx = ", index);
508#endif
509 return false;
510 }
511 if (builder.get_variable(value_witness_2) != builder.get_variable(table_witness_2)) {
512#ifndef FUZZING_DISABLE_WARNINGS
513 info("Failed SET/Read ROM[1] in table = ", i, " at idx = ", index);
514#endif
515 return false;
516 }
517 }
518 }
519
520 for (size_t i = 0; i < builder.rom_ram_logic.ram_arrays.size(); i++) {
521 auto ram_array = builder.rom_ram_logic.ram_arrays[i];
522
523 std::vector<uint32_t> tmp_state(ram_array.state.size());
524
525 // Simulate the memory call trace
526 for (auto& rr : ram_array.records) {
527 uint32_t index = static_cast<uint32_t>(builder.get_variable(rr.index_witness));
528 uint32_t value_witness = rr.value_witness;
529 auto access_type = rr.access_type;
530
531 uint32_t table_witness = tmp_state[index];
532
533 switch (access_type) {
535 if (builder.get_variable(value_witness) != builder.get_variable(table_witness)) {
536#ifndef FUZZING_DISABLE_WARNINGS
537 info("Failed RAM read in table = ", i, " at idx = ", index);
538#endif
539 return false;
540 }
541 break;
543 tmp_state[index] = value_witness;
544 break;
545 default:
546 return false;
547 }
548 }
549
550 if (tmp_state != ram_array.state) {
551#ifndef FUZZING_DISABLE_WARNINGS
552 info("Failed RAM final state check at table = ", i);
553#endif
554 return false;
555 }
556 }
557 return true;
558}
559#endif
560
561// Template method instantiations for each check method
562template bool UltraCircuitChecker::check<UltraCircuitBuilder_<UltraExecutionTraceBlocks>>(
563 const UltraCircuitBuilder_<UltraExecutionTraceBlocks>& builder_in);
564template bool UltraCircuitChecker::check<MegaCircuitBuilder_<bb::fr>>(const MegaCircuitBuilder_<bb::fr>& builder_in);
565} // namespace bb
#define BB_ASSERT(expression,...)
Definition assert.hpp:70
A base class labelling all entities (for instance, all of the polynomials used by the prover during s...
ArrayOfValues< FF, RelationImpl::SUBRELATION_PARTIAL_LENGTHS > SumcheckArrayOfValuesOverSubrelations
std::unordered_set< Key, HashFunction > LookupHashTable
static bool check_databus_read(auto &values, Builder &builder)
Check that the {index, value} pair contained in a databus read gate reflects the actual value present...
static bool check_relation(auto &values, auto &params)
Check that a given relation is satisfied for the provided inputs corresponding to a single row.
static bool check_tag_data(const TagCheckData &tag_data)
Check whether the left and right running tag products are equal.
static bool check_lookup(auto &values, auto &lookup_hash_table)
Check whether the values in a lookup gate are contained within a corresponding hash table.
static void populate_values(Builder &builder, auto &block, auto &values, TagCheckData &tag_data, MemoryCheckData &memory_data, size_t idx)
Populate the values required to check the correctness of a single "row" of the circuit.
static bool check(const Builder &builder_in)
Check the correctness of a circuit witness.
static Builder prepare_circuit(const Builder &builder_in)
Copy the builder and finalize it before checking its validity.
static bool check_block(Builder &builder, auto &block, TagCheckData &tag_data, MemoryCheckData &memory_data, LookupHashTable &lookup_hash_table)
Checks that the provided witness satisfies all gates contained in a single execution trace block.
A field element for each entity of the flavor. These entities represent the prover polynomials evalua...
#define info(...)
Definition log.hpp:93
AluTraceBuilder builder
Definition alu.test.cpp:124
Entry point for Barretenberg command-line interface.
Definition api.hpp:5
constexpr size_t NUM_BUS_COLUMNS
The DataBus; facilitates storage of public circuit input/output.
Definition databus.hpp:72
FF read_gate_selector(const ExecutionTraceBlock< FF, NUM_WIRES > &block, GateKind kind, size_t idx)
Gate-selector value at (block, idx) for kind, returning zero if block does not own this kind....
@ Poseidon2QuadIntTerminal
@ Poseidon2TransitionEntry
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
Struct for managing memory record data for ensuring RAM/ROM correctness.
Struct for managing the running tag product data for ensuring tag correctness.
std::unordered_set< size_t > encountered_variables
static constexpr field one()