Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
acir_dsl.fuzzer.cpp
Go to the documentation of this file.
1
20#include "acir_format.hpp"
28#include "serde/acir.hpp"
29#include <algorithm>
30#include <cstdint>
31#include <cstring>
32#include <map>
33#include <set>
34#include <vector>
35
36using namespace bb;
37using namespace acir_format;
38
39// LibFuzzer mutation function
40extern "C" size_t LLVMFuzzerMutate(uint8_t* Data, size_t Size, size_t MaxSize);
41
42namespace {
43
49bool solve_witnesses(std::vector<Acir::Expression>& expressions,
50 uint32_t num_witnesses,
51 std::map<uint32_t, fr>& witnesses)
52{
53 // Initial values already set from witness VM - just solve to satisfy constraints
54 (void)num_witnesses; // Reserved for future constraint validation
55
56 // STEP 1: Identify witnesses that appear ONLY in linear constraints (not in mul_terms)
57 // across ALL expressions
58 std::set<uint32_t> linear_only_witnesses;
59 std::map<uint32_t, bool> witness_has_nonlinear;
60
61 for (const auto& expr : expressions) {
62 // Mark all witnesses in mul_terms as non-linear
63 for (const auto& [coeff_bytes, w1, w2] : expr.mul_terms) {
64 witness_has_nonlinear[w1.value] = true;
65 witness_has_nonlinear[w2.value] = true;
66 }
67 }
68
69 // Collect witnesses that only appear in linear_combinations
70 for (const auto& expr : expressions) {
71 for (const auto& [coeff_bytes, w] : expr.linear_combinations) {
72 // Only consider if not already marked as non-linear
73 if (!witness_has_nonlinear.contains(w.value)) {
74 linear_only_witnesses.insert(w.value);
75 }
76 }
77 }
78
79 // STEP 2: Single pass through expressions
80 // Solve linear-only witnesses and adjust q_c as needed
81 for (auto& expr : expressions) {
82 // Evaluate current value
83 fr value = fr::serialize_from_buffer(&expr.q_c[0]);
84
85 for (const auto& [coeff_bytes, w1, w2] : expr.mul_terms) {
86 fr coeff = fr::serialize_from_buffer(&coeff_bytes[0]);
87 value += coeff * witnesses[w1.value] * witnesses[w2.value];
88 }
89
90 for (const auto& [coeff_bytes, w] : expr.linear_combinations) {
91 fr coeff = fr::serialize_from_buffer(&coeff_bytes[0]);
92 value += coeff * witnesses[w.value];
93 }
94
95 // If not satisfied, try to solve
96 if (value != fr::zero()) {
97 bool solved = false;
98
99 // TIER 1: Try to find a linear-only witness in this expression that we can solve for
100 // First pass: check which linear-only witnesses are in this expression and sum their coefficients
101 std::map<uint32_t, fr> linear_witness_coeffs;
102 for (const auto& [coeff_bytes, w] : expr.linear_combinations) {
103 if (linear_only_witnesses.contains(w.value)) {
104 fr coeff = fr::serialize_from_buffer(&coeff_bytes[0]);
105 linear_witness_coeffs[w.value] += coeff;
106 }
107 }
108
109 // Try to solve using any linear-only witness with non-zero total coefficient
110 for (const auto& [w_idx, total_coeff] : linear_witness_coeffs) {
111 if (total_coeff != fr::zero()) {
112 // Calculate value excluding this witness:
113 // value = q_c + mul_terms + (coeff_of_other_witnesses * other_witnesses)
114 fr value_without_witness = fr::serialize_from_buffer(&expr.q_c[0]);
115 for (const auto& [coeff_bytes, w1, w2] : expr.mul_terms) {
116 fr coeff = fr::serialize_from_buffer(&coeff_bytes[0]);
117 value_without_witness += coeff * witnesses[w1.value] * witnesses[w2.value];
118 }
119 for (const auto& [coeff_bytes, w] : expr.linear_combinations) {
120 if (w.value != w_idx) {
121 fr coeff = fr::serialize_from_buffer(&coeff_bytes[0]);
122 value_without_witness += coeff * witnesses[w.value];
123 }
124 }
125
126 // Solve: total_coeff * witness + value_without_witness = 0
127 // So: witness = -value_without_witness / total_coeff
128 witnesses[w_idx] = -value_without_witness / total_coeff;
129 solved = true;
130 break;
131 }
132 }
133
134 // TIER 2: If no linear-only witness found, adjust q_c to force equation to zero
135 if (!solved) {
136 // Set q_c = -value to make: q_c + value = 0
137 expr.q_c = (fr::serialize_from_buffer(&expr.q_c[0]) - value).to_buffer();
138 }
139 }
140
141 // Erase all witnesses in this expression's linear_combinations
142 // This prevents future expressions from modifying them and breaking this equation
143 // We have backups (original_witnesses) so this is safe
144 for (const auto& [coeff_bytes, w] : expr.linear_combinations) {
145 linear_only_witnesses.erase(w.value);
146 }
147 // Evaluate current value
148 }
149
150 return true;
151}
152
156bool is_trivial_expression(const Acir::Expression& expr)
157{
158 // Check constant term
159 fr q_c = fr::serialize_from_buffer(&expr.q_c[0]);
160 if (q_c != fr::zero()) {
161 return false;
162 }
163
164 // Check mul terms
165 for (const auto& [coeff_bytes, w1, w2] : expr.mul_terms) {
166 fr coeff = fr::serialize_from_buffer(&coeff_bytes[0]);
167 if (coeff != fr::zero()) {
168 return false;
169 }
170 }
171
172 // Check linear combinations
173 for (const auto& [coeff_bytes, w] : expr.linear_combinations) {
174 fr coeff = fr::serialize_from_buffer(&coeff_bytes[0]);
175 if (coeff != fr::zero()) {
176 return false;
177 }
178 }
179
180 return true; // All coefficients are zero - trivial constraint
181}
182
186void print_acir_format_gates(const AcirFormat& acir_format)
187{
188 std::cerr << "\n=== RESULTING GATES ===" << std::endl;
189
190 std::cerr << "\nQuad Constraints (" << acir_format.quad_constraints.size() << " total):" << std::endl;
191 for (size_t i = 0; i < acir_format.quad_constraints.size(); ++i) {
192 const auto& gate = acir_format.quad_constraints[i];
193 std::cerr << "\nQuad Gate " << i << ":" << std::endl;
194 std::cerr << " a=" << gate.a << ", b=" << gate.b << ", c=" << gate.c << ", d=" << gate.d << std::endl;
195 std::cerr << " mul_scaling=" << gate.mul_scaling << " (q_m)" << std::endl;
196 std::cerr << " a_scaling=" << gate.a_scaling << " (q_a)" << std::endl;
197 std::cerr << " b_scaling=" << gate.b_scaling << " (q_b)" << std::endl;
198 std::cerr << " c_scaling=" << gate.c_scaling << " (q_c)" << std::endl;
199 std::cerr << " d_scaling=" << gate.d_scaling << " (q_d)" << std::endl;
200 std::cerr << " const_scaling=" << gate.const_scaling << " (q_const)" << std::endl;
201
202 std::cerr << " Represents: " << gate.mul_scaling << "*w" << gate.a << "*w" << gate.b << " + " << gate.a_scaling
203 << "*w" << gate.a << " + " << gate.b_scaling << "*w" << gate.b << " + " << gate.c_scaling << "*w"
204 << gate.c << " + " << gate.d_scaling << "*w" << gate.d << " + " << gate.const_scaling << " = 0"
205 << std::endl;
206 }
207
208 std::cerr << "\nBig Quad Constraints (" << acir_format.big_quad_constraints.size() << " expressions):" << std::endl;
209 for (size_t expr_idx = 0; expr_idx < acir_format.big_quad_constraints.size(); ++expr_idx) {
210 const auto& gates = acir_format.big_quad_constraints[expr_idx];
211 std::cerr << "\nBig Expression " << expr_idx << " (" << gates.size() << " gates):" << std::endl;
212 for (size_t i = 0; i < gates.size(); ++i) {
213 const auto& gate = gates[i];
214 std::cerr << " Gate " << i << ": " << gate.mul_scaling << "*w" << gate.a << "*w" << gate.b << " + "
215 << gate.a_scaling << "*w" << gate.a << " + " << gate.b_scaling << "*w" << gate.b << " + "
216 << gate.c_scaling << "*w" << gate.c << " + " << gate.d_scaling << "*w" << gate.d << " + "
217 << gate.const_scaling << " = 0" << std::endl;
218 }
219 }
220
221 std::cerr << "=== END GATES ===" << std::endl << std::endl;
222}
223
227void print_expressions_and_witnesses(const std::vector<Acir::Expression>& expressions,
228 const std::map<uint32_t, fr>& witnesses)
229{
230 std::cerr << "\n=== EXPRESSION AND WITNESS DUMP ===" << std::endl;
231
232 // Print witnesses
233 std::cerr << "\nWitnesses (" << witnesses.size() << " total):" << std::endl;
234 for (const auto& [idx, value] : witnesses) {
235 std::cerr << " w" << idx << " = " << value << std::endl;
236 }
237
238 // Print expressions
239 std::cerr << "\nExpressions (" << expressions.size() << " total):" << std::endl;
240 for (size_t i = 0; i < expressions.size(); ++i) {
241 const auto& expr = expressions[i];
242 std::cerr << "\nExpression " << i << ":" << std::endl;
243
244 // Constant term
245 fr q_c = fr::serialize_from_buffer(&expr.q_c[0]);
246 std::cerr << " Constant: " << q_c << std::endl;
247
248 // Multiplication terms
249 if (!expr.mul_terms.empty()) {
250 std::cerr << " Mul terms:" << std::endl;
251 for (const auto& [coeff_bytes, w1, w2] : expr.mul_terms) {
252 fr coeff = fr::serialize_from_buffer(&coeff_bytes[0]);
253 std::cerr << " " << coeff << " * w" << w1.value << " * w" << w2.value << std::endl;
254 }
255 }
256
257 // Linear combinations
258 if (!expr.linear_combinations.empty()) {
259 std::cerr << " Linear terms:" << std::endl;
260 for (const auto& [coeff_bytes, w] : expr.linear_combinations) {
261 fr coeff = fr::serialize_from_buffer(&coeff_bytes[0]);
262 std::cerr << " " << coeff << " * w" << w.value << std::endl;
263 }
264 }
265
266 // Evaluate expression
267 fr value = q_c;
268 for (const auto& [coeff_bytes, w1, w2] : expr.mul_terms) {
269 fr coeff = fr::serialize_from_buffer(&coeff_bytes[0]);
270 auto it1 = witnesses.find(w1.value);
271 auto it2 = witnesses.find(w2.value);
272 if (it1 != witnesses.end() && it2 != witnesses.end()) {
273 value += coeff * it1->second * it2->second;
274 }
275 }
276 for (const auto& [coeff_bytes, w] : expr.linear_combinations) {
277 fr coeff = fr::serialize_from_buffer(&coeff_bytes[0]);
278 auto it = witnesses.find(w.value);
279 if (it != witnesses.end()) {
280 value += coeff * it->second;
281 }
282 }
283
284 std::cerr << " Evaluates to: " << value;
285 if (value == fr::zero()) {
286 std::cerr << " ✓ SATISFIED";
287 } else {
288 std::cerr << " ✗ NOT SATISFIED";
289 }
291 }
292
293 std::cerr << "=== END DUMP ===" << std::endl << std::endl;
294}
295
301bool validate_witnesses(const std::vector<Acir::Expression>& expressions,
302 const std::map<uint32_t, fr>& witnesses,
303 bool expect_satisfied = true)
304{
305 (void)expect_satisfied; // Unused - kept for API compatibility
306 size_t unsatisfied_count = 0;
307
308 for (size_t i = 0; i < expressions.size(); ++i) {
309 const auto& expr = expressions[i];
310
311 // Evaluate expression
313
314 for (const auto& [coeff_bytes, w1, w2] : expr.mul_terms) {
315 fr coeff = fr::serialize_from_buffer(&coeff_bytes[0]);
316 auto it1 = witnesses.find(w1.value);
317 auto it2 = witnesses.find(w2.value);
318 if (it1 != witnesses.end() && it2 != witnesses.end()) {
319 value += coeff * it1->second * it2->second;
320 }
321 }
322
323 for (const auto& [coeff_bytes, w] : expr.linear_combinations) {
324 fr coeff = fr::serialize_from_buffer(&coeff_bytes[0]);
325 auto it = witnesses.find(w.value);
326 if (it != witnesses.end()) {
327 value += coeff * it->second;
328 }
329 }
330
331 // Check if satisfied (should be zero for ASSERT_ZERO)
332 if (value != fr::zero()) {
333 unsatisfied_count++;
334 // Silent - don't print warnings during fuzzing
335 }
336 }
337
338 // Silent validation - only return true/false
339 return unsatisfied_count == 0;
340}
341
349bool test_acir_circuit(const uint8_t* data, size_t size)
350{
351 if (size < 31)
352 return false;
353
354 // SECURITY FUZZING: With 10% probability, disable sanitization to test raw data handling
355 // DISABLED BY DEFAULT - To enable, define ENABLE_UNSANITIZED_FUZZING at compile time
356 // This feature intentionally generates invalid data to test robustness against malformed input
357 bool disable_sanitization = false;
358#ifdef ENABLE_UNSANITIZED_FUZZING
359 if (size > 0) {
360 disable_sanitization = (data[0] % 10) == 0; // 10% probability
361 }
362#endif
363
364 // Parse header with scaling based on input size
365 // Small inputs (~64 bytes): 2-11 witnesses, 1-3 expressions
366 // Medium inputs (~500 bytes): 2-50 witnesses, 1-10 expressions
367 // Large inputs (~4KB): 2-100 witnesses, 1-20 expressions
368
369 // Calculate scale factor based on input size
370 // Scale grows logarithmically: log2(size / 64)
371 uint32_t scale_factor = 1;
372 if (size >= 128) {
373 scale_factor = 1;
374 size_t temp_size = size / 64;
375 while (temp_size > 1 && scale_factor < 10) {
376 temp_size /= 2;
377 scale_factor++;
378 }
379 }
380
381 uint32_t max_witnesses = std::min(10u * scale_factor, 100u);
382 uint32_t max_expressions = std::min(3u * scale_factor, 20u);
383 uint32_t max_vm_steps = std::min(10u * scale_factor, 50u);
384
385 uint32_t num_witnesses = (data[0] % max_witnesses) + 2; // 2 to max_witnesses+1
386 uint32_t num_expressions = (data[1] % max_expressions) + 1; // 1 to max_expressions
387 size_t coeff_vm_steps = (data[2] % max_vm_steps) + 3; // 3 to max_vm_steps+2
388 size_t witness_vm_steps = (data[3] % max_vm_steps) + 3; // 3 to max_vm_steps+2
389 uint8_t range_constraint_byte = data[4]; // Controls range constraint generation
390
391 const uint8_t* vm_data = data + 5;
392 size_t vm_data_size = size - 5;
393
394 // VM 1: Generate coefficients
395 FieldVM<fr> coeff_vm(false, coeff_vm_steps);
396 size_t coeff_consumed = coeff_vm.run(vm_data, vm_data_size, false);
397 const auto& coeff_state = coeff_vm.field_internal_state;
398
399 // VM 2: Generate initial witness values
400 const uint8_t* witness_vm_data = vm_data + coeff_consumed;
401 size_t witness_vm_data_size = (coeff_consumed < vm_data_size) ? (vm_data_size - coeff_consumed) : 0;
402
403 if (witness_vm_data_size < 10)
404 return false;
405
406 FieldVM<fr> witness_vm(false, witness_vm_steps);
407 size_t witness_consumed = witness_vm.run(witness_vm_data, witness_vm_data_size, false);
408 const auto& witness_state = witness_vm.field_internal_state;
409
410 // Move past both VM data sections
411 const uint8_t* ptr = witness_vm_data + witness_consumed;
412 size_t remaining = (witness_consumed < witness_vm_data_size) ? (witness_vm_data_size - witness_consumed) : 0;
413
414 if (remaining < 10)
415 return false;
416
417 // Build expressions using VM state
418 std::vector<Acir::Expression> expressions;
419 for (uint32_t e = 0; e < num_expressions && remaining > 2; ++e) {
420 Acir::Expression expr;
421
422 // Number of terms (scales with input size via scale_factor)
423 // Small inputs: 0-1 mul, 1-3 linear
424 // Large inputs: 0-5 mul, 1-10 linear
425 uint8_t max_mul_terms = static_cast<uint8_t>(std::min(1u + scale_factor / 2, 5u));
426 uint8_t max_lin_terms = static_cast<uint8_t>(std::min(3u + scale_factor, 10u));
427
428 uint8_t num_mul = (ptr[0] % std::max(max_mul_terms, static_cast<uint8_t>(1))); // Avoid modulo 0
429 uint8_t num_lin = 1 + (ptr[1] % std::max(max_lin_terms, static_cast<uint8_t>(1)));
430 ptr += 2;
431 remaining -= 2;
432
433 // Add mul terms using VM state for coefficients
434 for (uint8_t m = 0; m < num_mul && remaining >= 3; ++m) {
435 uint8_t coeff_reg = ptr[0] % INTERNAL_STATE_SIZE;
436 uint32_t w1_idx =
437 disable_sanitization ? *reinterpret_cast<const uint16_t*>(ptr + 1) : ptr[1] % num_witnesses;
438 uint32_t w2_idx =
439 disable_sanitization ? *reinterpret_cast<const uint16_t*>(ptr + 1) : ptr[2] % num_witnesses;
440 ptr += 3;
441 remaining -= 3;
442
443 std::vector<uint8_t> coeff(32);
444 coeff = coeff_state[coeff_reg].to_buffer();
445 Acir::Witness w1, w2;
446 w1.value = w1_idx;
447 w2.value = w2_idx;
448 expr.mul_terms.push_back(std::make_tuple(coeff, w1, w2));
449 }
450
451 // Add linear terms - sometimes duplicate witnesses
452 bool force_duplicate = (num_lin > 1) && (remaining > 0) && ((ptr[0] % 3) == 0);
453 uint32_t prev_witness = 0;
454
455 for (uint8_t l = 0; l < num_lin && remaining >= 2; ++l) {
456 uint8_t coeff_reg = ptr[0] % INTERNAL_STATE_SIZE;
457 uint32_t w_idx = disable_sanitization ? ptr[1] : ptr[1] % num_witnesses;
458 ptr += 2;
459 remaining -= 2;
460
461 // Force duplicate witness to test coefficient accumulation bug
462 if (force_duplicate && l > 0 && l < 3) {
463 w_idx = prev_witness;
464 }
465 prev_witness = w_idx;
466
467 std::vector<uint8_t> coeff(32);
468 coeff = coeff_state[coeff_reg].to_buffer();
470 w.value = w_idx;
471 expr.linear_combinations.push_back(std::make_tuple(coeff, w));
472 }
473
474 // Constant term from coefficient VM
475 if (remaining > 0) {
476 uint8_t const_reg = ptr[0] % INTERNAL_STATE_SIZE;
477 ptr++;
478 remaining--;
479 expr.q_c = coeff_state[const_reg].to_buffer();
480 } else {
481 expr.q_c = std::vector<uint8_t>(32, 0);
482 }
483
484 expressions.push_back(expr);
485 }
486
487 if (expressions.empty())
488 return false;
489
490 // Filter out trivial expressions (all zero coefficients)
491 std::vector<Acir::Expression> non_trivial_expressions;
492 for (const auto& expr : expressions) {
493 if (!is_trivial_expression(expr)) {
494 non_trivial_expressions.push_back(expr);
495 }
496 }
497
498 // Skip if no non-trivial constraints remain
499 if (non_trivial_expressions.empty()) {
500 return false;
501 }
502
503 // Use only non-trivial expressions
504 expressions = non_trivial_expressions;
505
506 // Initialize witnesses with VM-generated values (much better than random!)
507 std::map<uint32_t, fr> solved_witnesses;
508 for (uint32_t i = 0; i < num_witnesses; ++i) {
509 // Use witness VM state as initial values, cycling through if needed
510 solved_witnesses[i] = witness_state[i % INTERNAL_STATE_SIZE];
511 }
512
513 // Now solve to satisfy constraints, using VM values as starting point
514 solve_witnesses(expressions, num_witnesses, solved_witnesses);
515
516 // Validate that solver satisfied the constraints
517 // bool witnesses_valid = validate_witnesses(expressions, solved_witnesses, true);
518 // if (!witnesses_valid) {
519 // abort();
520 // // The solver couldn't satisfy all constraints - skip this test case
521 // return false;
522 // }
523
524 // Deterministic witness corruption for soundness testing
525 bool witnesses_corrupted = false;
526 std::vector<uint32_t> corrupted_witness_indices;
527 // Save original witnesses before corruption for meaningful-use checking
528 std::map<uint32_t, fr> original_witnesses = solved_witnesses;
529
530 if (size > 4 && (data[size - 1] % 5) == 0) {
531 witnesses_corrupted = true;
532 uint32_t num_to_corrupt = 1 + (data[size - 2] % 3);
533 bool actually_corrupted = false;
534 std::set<uint32_t> already_corrupted; // Track which witnesses we've already corrupted
535 for (uint32_t i = 0; i < num_to_corrupt && i < num_witnesses; ++i) {
536 size_t byte_idx = size - 3 - i;
537 if (byte_idx >= 4) { // Adjusted for 4-byte header
538 uint32_t witness_to_corrupt = disable_sanitization ? data[byte_idx] : data[byte_idx] % num_witnesses;
539
540 // Skip if we've already corrupted this witness
541 if (already_corrupted.count(witness_to_corrupt) > 0) {
542 continue;
543 }
544 already_corrupted.insert(witness_to_corrupt);
545
546 fr original_value = solved_witnesses[witness_to_corrupt];
547 // Use different part of coefficient VM state for corruption
548 // Add 1 to ensure we get a different value (especially important if original was 0)
549 size_t state_idx = (data[byte_idx] + INTERNAL_STATE_SIZE / 2) % INTERNAL_STATE_SIZE;
550 fr corruption_value = coeff_state[state_idx];
551 // If corruption value is same as original, add 1 to make it different
552 if (corruption_value == original_value) {
553 corruption_value += fr::one();
554 }
555
556 // Double-check that corruption actually changed the value
557 if (corruption_value == original_value) {
558 // Still the same after adding 1? Try subtracting 1 instead
559 corruption_value = original_value - fr::one();
560 }
561
562 // Only corrupt if we actually have a different value
563 if (corruption_value != original_value) {
564 solved_witnesses[witness_to_corrupt] = corruption_value;
565 corrupted_witness_indices.push_back(witness_to_corrupt);
566 actually_corrupted = true;
567 }
568 // else: Skip this witness - couldn't find a different value
569 }
570 }
571
572 // Only check if we actually corrupted something
573 if (actually_corrupted) {
574 // DYNAMIC CHECK: Directly evaluate expressions with corrupted witnesses
575 // This eliminates false positives from:
576 // - Algebraic cancellations (terms that cancel out)
577 // - Quadratics with multiple solutions
578 // - Under-constrained circuits
579 bool corrupted_witnesses_satisfy_constraints = validate_witnesses(expressions, solved_witnesses, false);
580
581 if (corrupted_witnesses_satisfy_constraints) {
582 // The corrupted witnesses STILL satisfy the expressions!
583 // This means the circuit is under-constrained (expected for random fuzzing)
584 // This is NOT a soundness bug - skip the CircuitChecker test
585 // IMPORTANT: Restore original witnesses since we're not testing soundness
586 solved_witnesses = original_witnesses;
587 witnesses_corrupted = false;
588 } else {
589 // Corrupted witnesses DON'T satisfy expressions - this is what we want to test!
590 // However, we need to check for assert_equal optimization cases.
591 // The assert_equal optimization (for patterns like w1 - w2 = 0) causes the
592 // circuit builder to unify w1 and w2 into a single variable. If a corrupted
593 // witness is ONLY used in such assert_equal patterns (and has no other
594 // constraints), then the unification makes the corruption ineffective.
595
596 // Detect assert_equal patterns: 2 linear terms, coefficients are negatives, no mul terms, no constant
597 std::set<uint32_t> assert_equal_only_witnesses;
598 std::set<uint32_t> constrained_in_other_expressions;
599
600 for (const auto& expr : expressions) {
601 bool is_assert_equal_pattern = expr.mul_terms.empty() && expr.linear_combinations.size() == 2 &&
603
604 if (is_assert_equal_pattern) {
607 if (coeff1 == -coeff2 && coeff1 != fr::zero()) {
608 // This is an assert_equal pattern (w1 - w2 = 0)
609 uint32_t w1 = std::get<1>(expr.linear_combinations[0]).value;
610 uint32_t w2 = std::get<1>(expr.linear_combinations[1]).value;
611 assert_equal_only_witnesses.insert(w1);
612 assert_equal_only_witnesses.insert(w2);
613 }
614 } else {
615 // Not an assert_equal pattern - track witnesses used here
616 for (const auto& [coeff_bytes, w1, w2] : expr.mul_terms) {
617 constrained_in_other_expressions.insert(w1.value);
618 constrained_in_other_expressions.insert(w2.value);
619 }
620 for (const auto& [coeff_bytes, w] : expr.linear_combinations) {
621 constrained_in_other_expressions.insert(w.value);
622 }
623 }
624 }
625
626 // Remove witnesses that appear in non-assert_equal expressions
627 for (uint32_t w : constrained_in_other_expressions) {
628 assert_equal_only_witnesses.erase(w);
629 }
630
631 // Check if ALL corrupted witnesses are only in assert_equal patterns
632 bool all_corrupted_are_assert_equal_only = true;
633 for (uint32_t w : corrupted_witness_indices) {
634 if (assert_equal_only_witnesses.count(w) == 0) {
635 all_corrupted_are_assert_equal_only = false;
636 break;
637 }
638 }
639
640 if (all_corrupted_are_assert_equal_only && !assert_equal_only_witnesses.empty()) {
641 // All corrupted witnesses are only used in assert_equal patterns
642 // The circuit builder will unify these, making corruption ineffective
643 // Skip the soundness check for this case
644 // IMPORTANT: Restore original witnesses since we're not testing soundness
645 solved_witnesses = original_witnesses;
646 witnesses_corrupted = false;
647 }
648 }
649 // else: Corrupted witnesses DON'T satisfy expressions AND are properly constrained
650 // This is the expected case for soundness testing - proceed with CircuitChecker
651 } else {
652 // Didn't actually corrupt anything, skip soundness check
653 witnesses_corrupted = false;
654 }
655 }
656
657 // ========== RANGE CONSTRAINT GENERATION ==========
658 // Generate range constraints for witnesses
659 // Uses range_constraint_byte to control which witnesses get constraints and what bit widths
660 std::vector<std::pair<uint32_t, uint32_t>> range_constraints; // (witness_idx, num_bits)
661 std::map<uint32_t, uint32_t> minimal_range; // Track tightest constraint per witness (for test setup)
662 bool should_violate_range = false;
663 uint32_t violated_witness_idx = 0;
664 uint32_t violated_range_bits = 0;
665
666 // Helper to check if a witness value satisfies a range constraint
667 auto satisfies_range = [](const fr& value, uint32_t num_bits) -> bool {
668 if (num_bits >= 254) {
669 // For 254+ bits, any field element is within range (BN254 field is ~254 bits)
670 return true;
671 }
672 // Convert field element to uint256_t
673 uint256_t value_int = value;
674
675 // Special case: 0-bit range means only value 0 is valid
676 if (num_bits == 0) {
677 return value_int == 0;
678 }
679
680 // Check if MSB position < num_bits (i.e., value < 2^num_bits)
681 size_t msb = value_int.get_msb();
682 return msb < num_bits;
683 };
684
685 // Decide if we should generate any range constraints (50% chance)
686 if ((range_constraint_byte & 0x80) != 0 && num_witnesses > 1) {
687 // Number of range constraints to add (1-4)
688 uint32_t num_range_constraints = ((range_constraint_byte >> 5) & 0x3) + 1;
689 num_range_constraints = std::min(num_range_constraints, num_witnesses - 1);
690
691 // Add range constraints for random witnesses
692 for (uint32_t i = 0; i < num_range_constraints; ++i) {
693 uint32_t witness_idx =
694 disable_sanitization ? range_constraint_byte + i : (range_constraint_byte + i) % num_witnesses;
695
696 // Determine bit width based on fuzzer input
697 // Mix of common bit widths and edge cases (capped at 254 bits for BN254 field)
698 uint8_t bit_selector = (range_constraint_byte + i * 37) & 0x1F;
699 uint32_t num_bits = 0;
700
701 if (bit_selector < 8) {
702 num_bits = 8; // Common: u8
703 } else if (bit_selector < 14) {
704 num_bits = 16; // Common: u16
705 } else if (bit_selector < 18) {
706 num_bits = 32; // Common: u32
707 } else if (bit_selector < 21) {
708 num_bits = 64; // Common: u64
709 } else if (bit_selector < 24) {
710 num_bits = 128; // Common: u128
711 } else if (bit_selector < 28) {
712 num_bits = 254; // Max valid for BN254 (all field elements satisfy this)
713 } else if (bit_selector < 30) {
714 num_bits = 1; // Edge case: boolean
715 } else {
716 num_bits = 0; // Edge case: must be zero
717 }
718
719 // Track minimal range for each witness
720 auto it = minimal_range.find(witness_idx);
721 if (it == minimal_range.end() || num_bits < it->second) {
722 minimal_range[witness_idx] = num_bits;
723 }
724 }
725
726 // Check if any existing witnesses violate their MINIMAL range constraints
727 // This tests that witnesses satisfy the tightest constraint
728 // and creates a single range constraint as the minimal one
729 bool has_accidental_violation = false;
730 for (const auto& [witness_idx, min_bits] : minimal_range) {
731 range_constraints.push_back({ witness_idx, min_bits });
732
733 auto it = solved_witnesses.find(witness_idx);
734 if (it != solved_witnesses.end()) {
735 if (!satisfies_range(it->second, min_bits)) {
736 has_accidental_violation = true;
737 break;
738 }
739 }
740 }
741
742 // Decide if we should intentionally violate a range constraint (25% chance)
743 // But only if we don't already have an accidental violation
744 if (!has_accidental_violation && (range_constraint_byte & 0x10) != 0 && !minimal_range.empty()) {
745 // Pick a witness with range constraints to violate
746 // Use the MINIMAL range for that witness
747 size_t violate_idx = range_constraint_byte % minimal_range.size();
748 auto it = minimal_range.begin();
749 std::advance(it, violate_idx);
750 uint32_t candidate_witness = it->first;
751 uint32_t candidate_bits = it->second;
752
753 // Only attempt violation for range constraints < 254 bits
754 // For >= 254 bits, every field element is in range, so violation is impossible
755 if (candidate_bits < 254) {
756 should_violate_range = true;
757 violated_witness_idx = candidate_witness;
758 violated_range_bits = candidate_bits;
759
760 // Set witness to exceed the MINIMAL range: 2^num_bits (just outside the range)
761 fr violation_value = fr(1);
762 for (uint32_t b = 0; b < violated_range_bits; ++b) {
763 violation_value = violation_value + violation_value;
764 }
765 solved_witnesses[violated_witness_idx] = violation_value;
766 }
767 // For num_bits >= 254, skip violation testing since all field elements are in range
768 } else if (has_accidental_violation) {
769 // We have an accidental range violation from the witness generation
770 // Treat this as an intentional soundness test
771 // Find which witness is violating its MINIMAL range (and only test if < 254 bits)
772 for (const auto& [witness_idx, min_bits] : minimal_range) {
773 auto it = solved_witnesses.find(witness_idx);
774 if (it != solved_witnesses.end() && min_bits < 254) {
775 if (!satisfies_range(it->second, min_bits)) {
776 should_violate_range = true;
777 violated_witness_idx = witness_idx;
778 violated_range_bits = min_bits;
779 break;
780 }
781 }
782 }
783 }
784 }
785
786 try {
787 // Create ACIR Circuit
788 Acir::Circuit circuit;
789 circuit.function_name = "main";
790 circuit.current_witness_index = num_witnesses - 1;
791 circuit.private_parameters = {};
792 circuit.public_parameters.value = {};
793 circuit.return_values.value = {};
794 circuit.assert_messages = {};
795
796 // Add AssertZero opcodes
797 for (const auto& expr : expressions) {
798 Acir::Opcode::AssertZero assert_zero;
799 assert_zero.value = expr;
800
801 Acir::Opcode opcode;
802 opcode.value = assert_zero;
803 circuit.opcodes.push_back(opcode);
804 }
805
806 // Add Range constraint opcodes
807 for (const auto& [witness_idx, num_bits] : range_constraints) {
808 // Create FunctionInput for the witness
809 Acir::FunctionInput::Witness witness_input;
810 witness_input.value = Acir::Witness{ witness_idx };
811
813 input.value = witness_input;
814
815 // Create RANGE BlackBoxFuncCall
817 range_op.input = input;
818 range_op.num_bits = num_bits;
819
820 // Wrap in BlackBoxFuncCall
822 bb_call.value = range_op;
823
824 // Wrap in Opcode
826 bb_opcode.value = bb_call;
827
828 Acir::Opcode opcode;
829 opcode.value = bb_opcode;
830 circuit.opcodes.push_back(opcode);
831 }
832
833 // *** Go through acir_to_constraint_buf pipeline directly ***
834 // This exercises the core conversion logic without serialization issues
836
837 // ========== BUILD CIRCUIT FROM ACIR FORMAT ==========
838
839 // Create witness vector
840 WitnessVector witness_vec;
841 witness_vec.reserve(num_witnesses);
842 for (uint32_t i = 0; i < num_witnesses; ++i) {
843 auto it = solved_witnesses.find(i);
844 if (it != solved_witnesses.end()) {
845 witness_vec.push_back(it->second);
846 } else {
847 witness_vec.push_back(fr::zero());
848 }
849 }
850
851 // Build circuit using the proper constructor that initializes witnesses
852 // NOTE: Must use the witness-aware constructor, not default constructor!
853 // The default constructor leaves witnesses uninitialized, causing false negatives.
855 /*size_hint*/ 0, witness_vec, acir_format.public_inputs, /*is_write_vk_mode=*/false
856 };
857
859
860 // Check if the builder is in a failed state (e.g., from assert_equal with unequal values)
861 if (builder.failed()) {
862#ifndef FUZZING_DISABLE_WARNINGS
863 info("Circuit builder is in a failed state: ", builder.err());
864#endif
865 return false;
866 }
867
868 bool circuit_valid = CircuitChecker::check(builder);
869
870 // SOUNDNESS CHECK: Corrupted witnesses or range violations should fail
871 if (witnesses_corrupted || should_violate_range) {
872 if (circuit_valid) {
873 std::cerr << "\n=== CRITICAL SOUNDNESS BUG ===" << std::endl;
874 if (witnesses_corrupted) {
875 std::cerr << "Corrupted witnesses passed CircuitChecker verification!" << std::endl;
876 }
877 if (should_violate_range) {
878 std::cerr << "Range constraint violation passed CircuitChecker verification!" << std::endl;
879 std::cerr << "Violated witness: w" << violated_witness_idx << " (range: " << violated_range_bits
880 << " bits)" << std::endl;
881 std::cerr << "Witness value: " << solved_witnesses[violated_witness_idx] << std::endl;
882 }
883 std::cerr << "Num witnesses: " << num_witnesses << ", Num expressions: " << expressions.size()
884 << std::endl;
885 print_expressions_and_witnesses(expressions, solved_witnesses);
886 print_acir_format_gates(acir_format);
887 abort();
888 }
889 return false;
890 }
891
892 // COMPLETENESS CHECK
893 if (!circuit_valid) {
894 std::cerr << "\n=== COMPLETENESS BUG ===" << std::endl;
895 std::cerr << "Valid witnesses failed CircuitChecker verification!" << std::endl;
896 std::cerr << "Num witnesses: " << num_witnesses << ", Num expressions: " << expressions.size() << std::endl;
897 print_expressions_and_witnesses(expressions, solved_witnesses);
898 print_acir_format_gates(acir_format);
899 abort();
900 }
901
902 return circuit_valid;
903
904 } catch (const std::exception& e) {
905 // Silent - exceptions are expected for edge cases and corrupted witnesses
906 // If there's a real bug, it will show up in the coverage/crash reports
907 (void)e; // Avoid unused variable warning
908 return false;
909 } catch (...) {
910 // Silent - just skip this test case
911 return false;
912 }
913}
914
915} // anonymous namespace
916
920extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
921{
922 if (size < 50) {
923 return 0;
924 }
925
926 test_acir_circuit(data, size);
927
928 return 0;
929}
930
934extern "C" size_t LLVMFuzzerCustomMutator(uint8_t* data, size_t size, size_t max_size, unsigned int seed)
935{
936 if (size < 10 || max_size < 50) {
937 return LLVMFuzzerMutate(data, size, max_size);
938 }
939
940 std::srand(seed);
941 int strategy = static_cast<int>(static_cast<unsigned>(std::rand()) % 100u);
942
943 if (strategy < 30) {
944 // Mutate VM instructions (scales with input size)
945 if (size > 10) {
946 size_t vm_section_start = 4;
947 size_t vm_section_end = size / 2; // First half is VM data
948 if (vm_section_end > vm_section_start) {
949 size_t pos = vm_section_start + (static_cast<unsigned>(std::rand()) %
950 static_cast<unsigned>(vm_section_end - vm_section_start));
951 data[pos] = static_cast<uint8_t>(static_cast<unsigned>(std::rand()) % 256u);
952 }
953 }
954 } else if (strategy < 55) {
955 // Mutate expression structure (second half of input)
956 if (size > 20) {
957 size_t expr_section_start = size / 2;
958 size_t expr_section_end = size - 10;
959 if (expr_section_end > expr_section_start) {
960 size_t pos = expr_section_start + (static_cast<unsigned>(std::rand()) %
961 static_cast<unsigned>(expr_section_end - expr_section_start));
962 data[pos] = static_cast<uint8_t>(static_cast<unsigned>(std::rand()) % 256u);
963 }
964 }
965 } else if (strategy < 70) {
966 // Mutate header (controls scaling)
967 if (size > 3) {
968 data[static_cast<unsigned>(std::rand()) % 4u] =
969 static_cast<uint8_t>(static_cast<unsigned>(std::rand()) % 256u);
970 }
971 } else if (strategy < 80) {
972 // Grow input size (add more data for larger test cases)
973 if (size < max_size && max_size - size >= 32) {
974 size_t grow_by = std::min(static_cast<size_t>(32), max_size - size);
975 // Shift existing data and insert new bytes
976 for (size_t i = 0; i < grow_by; ++i) {
977 data[size + i] = static_cast<uint8_t>(static_cast<unsigned>(std::rand()) % 256u);
978 }
979 return size + grow_by;
980 }
981 } else if (strategy < 85) {
982 // Shrink input size (remove redundant data)
983 if (size > 100) {
984 size_t shrink_by = std::min(static_cast<size_t>(32), size - 50);
985 return size - shrink_by;
986 }
987 } else {
988 // Use LibFuzzer's built-in mutation
989 return LLVMFuzzerMutate(data, size, max_size);
990 }
991
992 return size;
993}
size_t LLVMFuzzerMutate(uint8_t *Data, size_t Size, size_t MaxSize)
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
LibFuzzer entry point.
size_t LLVMFuzzerCustomMutator(uint8_t *data, size_t size, size_t max_size, unsigned int seed)
Custom mutator for structure-aware mutations with size scaling.
static bool check(const Builder &circuit)
Check the witness satisifies the circuit.
constexpr uint64_t get_msb() const
void info(Args... args)
Definition log.hpp:75
AluTraceBuilder builder
Definition alu.test.cpp:124
const std::vector< MemoryValue > data
FF b
Field arithmetic fuzzer for testing cryptographic field operations.
AcirFormat circuit_serde_to_acir_format(Acir::Circuit const &circuit)
Convert an Acir::Circuit into an AcirFormat by processing all the opcodes.
std::vector< bb::fr > WitnessVector
void build_constraints(Builder &builder, AcirFormat &constraints, const ProgramMetadata &metadata)
Entry point for Barretenberg command-line interface.
Definition api.hpp:5
const size_t INTERNAL_STATE_SIZE
Constant defining the number of elements in the VM's internal state.
field< Bn254FrParams > fr
Definition fr.hpp:174
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
std::vector< uint8_t > to_buffer(T const &value)
Acir::FunctionInput input
Definition acir.hpp:3175
std::variant< AES128Encrypt, AND, XOR, RANGE, Blake2s, Blake3, EcdsaSecp256k1, EcdsaSecp256r1, MultiScalarMul, EmbeddedCurveAdd, Keccakf1600, RecursiveAggregation, Poseidon2Permutation, Sha256Compression > value
Definition acir.hpp:3608
Acir::PublicInputs return_values
Definition acir.hpp:5014
std::vector< Acir::Opcode > opcodes
Definition acir.hpp:5011
uint32_t current_witness_index
Definition acir.hpp:5010
std::vector< Acir::Witness > private_parameters
Definition acir.hpp:5012
Acir::PublicInputs public_parameters
Definition acir.hpp:5013
std::string function_name
Definition acir.hpp:5009
std::vector< std::tuple< Acir::OpcodeLocation, Acir::AssertionPayload > > assert_messages
Definition acir.hpp:5015
std::vector< std::tuple< std::vector< uint8_t >, Acir::Witness > > linear_combinations
Definition acir.hpp:4006
std::vector< uint8_t > q_c
Definition acir.hpp:4007
std::vector< std::tuple< std::vector< uint8_t >, Acir::Witness, Acir::Witness > > mul_terms
Definition acir.hpp:4005
std::variant< Constant, Witness > value
Definition acir.hpp:2968
Acir::Expression value
Definition acir.hpp:4365
Acir::BlackBoxFuncCall value
Definition acir.hpp:4385
std::variant< AssertZero, BlackBoxFuncCall, MemoryOp, MemoryInit, BrilligCall, Call > value
Definition acir.hpp:4552
std::vector< Acir::Witness > value
Definition acir.hpp:4989
uint32_t value
Definition acir.hpp:2907
Virtual machine for field arithmetic operations.
static constexpr field one()
static field serialize_from_buffer(const uint8_t *buffer)
BB_INLINE std::vector< uint8_t > to_buffer() const
static constexpr field zero()