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
22#include "acir_format.hpp"
30#include "serde/acir.hpp"
31#include <algorithm>
32#include <cstdint>
33#include <cstring>
34#include <map>
35#include <set>
36#include <vector>
37
38using namespace bb;
39using namespace acir_format;
40
41// LibFuzzer mutation function
42extern "C" size_t LLVMFuzzerMutate(uint8_t* Data, size_t Size, size_t MaxSize);
43
44namespace {
45
51bool solve_witnesses(std::vector<Acir::Expression>& expressions,
52 uint32_t num_witnesses,
53 std::map<uint32_t, fr>& witnesses)
54{
55 // Initial values already set from witness VM - just solve to satisfy constraints
56 (void)num_witnesses; // Reserved for future constraint validation
57
58 // STEP 1: Identify witnesses that appear ONLY in linear constraints (not in mul_terms)
59 // across ALL expressions
60 std::set<uint32_t> linear_only_witnesses;
61 std::map<uint32_t, bool> witness_has_nonlinear;
62
63 for (const auto& expr : expressions) {
64 // Mark all witnesses in mul_terms as non-linear
65 for (const auto& [coeff_bytes, w1, w2] : expr.mul_terms) {
66 witness_has_nonlinear[w1.value] = true;
67 witness_has_nonlinear[w2.value] = true;
68 }
69 }
70
71 // Collect witnesses that only appear in linear_combinations
72 for (const auto& expr : expressions) {
73 for (const auto& [coeff_bytes, w] : expr.linear_combinations) {
74 // Only consider if not already marked as non-linear
75 if (!witness_has_nonlinear.contains(w.value)) {
76 linear_only_witnesses.insert(w.value);
77 }
78 }
79 }
80
81 // STEP 2: Single pass through expressions
82 // Solve linear-only witnesses and adjust q_c as needed
83 for (auto& expr : expressions) {
84 // Evaluate current value
85 fr value = fr::serialize_from_buffer(&expr.q_c[0]);
86
87 for (const auto& [coeff_bytes, w1, w2] : expr.mul_terms) {
88 fr coeff = fr::serialize_from_buffer(&coeff_bytes[0]);
89 value += coeff * witnesses[w1.value] * witnesses[w2.value];
90 }
91
92 for (const auto& [coeff_bytes, w] : expr.linear_combinations) {
93 fr coeff = fr::serialize_from_buffer(&coeff_bytes[0]);
94 value += coeff * witnesses[w.value];
95 }
96
97 // If not satisfied, try to solve
98 if (value != fr::zero()) {
99 bool solved = false;
100
101 // TIER 1: Try to find a linear-only witness in this expression that we can solve for
102 // First pass: check which linear-only witnesses are in this expression and sum their coefficients
103 std::map<uint32_t, fr> linear_witness_coeffs;
104 for (const auto& [coeff_bytes, w] : expr.linear_combinations) {
105 if (linear_only_witnesses.contains(w.value)) {
106 fr coeff = fr::serialize_from_buffer(&coeff_bytes[0]);
107 linear_witness_coeffs[w.value] += coeff;
108 }
109 }
110
111 // Try to solve using any linear-only witness with non-zero total coefficient
112 for (const auto& [w_idx, total_coeff] : linear_witness_coeffs) {
113 if (total_coeff != fr::zero()) {
114 // Calculate value excluding this witness:
115 // value = q_c + mul_terms + (coeff_of_other_witnesses * other_witnesses)
116 fr value_without_witness = fr::serialize_from_buffer(&expr.q_c[0]);
117 for (const auto& [coeff_bytes, w1, w2] : expr.mul_terms) {
118 fr coeff = fr::serialize_from_buffer(&coeff_bytes[0]);
119 value_without_witness += coeff * witnesses[w1.value] * witnesses[w2.value];
120 }
121 for (const auto& [coeff_bytes, w] : expr.linear_combinations) {
122 if (w.value != w_idx) {
123 fr coeff = fr::serialize_from_buffer(&coeff_bytes[0]);
124 value_without_witness += coeff * witnesses[w.value];
125 }
126 }
127
128 // Solve: total_coeff * witness + value_without_witness = 0
129 // So: witness = -value_without_witness / total_coeff
130 witnesses[w_idx] = -value_without_witness / total_coeff;
131 solved = true;
132 break;
133 }
134 }
135
136 // TIER 2: If no linear-only witness found, adjust q_c to force equation to zero
137 if (!solved) {
138 // Set q_c = -value to make: q_c + value = 0
139 expr.q_c = (fr::serialize_from_buffer(&expr.q_c[0]) - value).to_buffer();
140 }
141 }
142
143 // Erase all witnesses in this expression's linear_combinations
144 // This prevents future expressions from modifying them and breaking this equation
145 // We have backups (original_witnesses) so this is safe
146 for (const auto& [coeff_bytes, w] : expr.linear_combinations) {
147 linear_only_witnesses.erase(w.value);
148 }
149 // Evaluate current value
150 }
151
152 return true;
153}
154
158bool is_trivial_expression(const Acir::Expression& expr)
159{
160 // Check constant term
161 fr q_c = fr::serialize_from_buffer(&expr.q_c[0]);
162 if (q_c != fr::zero()) {
163 return false;
164 }
165
166 // Check mul terms
167 for (const auto& [coeff_bytes, w1, w2] : expr.mul_terms) {
168 fr coeff = fr::serialize_from_buffer(&coeff_bytes[0]);
169 if (coeff != fr::zero()) {
170 return false;
171 }
172 }
173
174 // Check linear combinations
175 for (const auto& [coeff_bytes, w] : expr.linear_combinations) {
176 fr coeff = fr::serialize_from_buffer(&coeff_bytes[0]);
177 if (coeff != fr::zero()) {
178 return false;
179 }
180 }
181
182 return true; // All coefficients are zero - trivial constraint
183}
184
188void print_acir_format_gates(const AcirFormat& acir_format)
189{
190 std::cerr << "\n=== RESULTING GATES ===" << std::endl;
191
192 std::cerr << "\nQuad Constraints (" << acir_format.quad_constraints.size() << " total):" << std::endl;
193 for (size_t i = 0; i < acir_format.quad_constraints.size(); ++i) {
194 const auto& gate = acir_format.quad_constraints[i];
195 std::cerr << "\nQuad Gate " << i << ":" << std::endl;
196 std::cerr << " a=" << gate.a << ", b=" << gate.b << ", c=" << gate.c << ", d=" << gate.d << std::endl;
197 std::cerr << " mul_scaling=" << gate.mul_scaling << " (q_m)" << std::endl;
198 std::cerr << " a_scaling=" << gate.a_scaling << " (q_a)" << std::endl;
199 std::cerr << " b_scaling=" << gate.b_scaling << " (q_b)" << std::endl;
200 std::cerr << " c_scaling=" << gate.c_scaling << " (q_c)" << std::endl;
201 std::cerr << " d_scaling=" << gate.d_scaling << " (q_d)" << std::endl;
202 std::cerr << " const_scaling=" << gate.const_scaling << " (q_const)" << std::endl;
203
204 std::cerr << " Represents: " << gate.mul_scaling << "*w" << gate.a << "*w" << gate.b << " + " << gate.a_scaling
205 << "*w" << gate.a << " + " << gate.b_scaling << "*w" << gate.b << " + " << gate.c_scaling << "*w"
206 << gate.c << " + " << gate.d_scaling << "*w" << gate.d << " + " << gate.const_scaling << " = 0"
207 << std::endl;
208 }
209
210 std::cerr << "\nBig Quad Constraints (" << acir_format.big_quad_constraints.size() << " expressions):" << std::endl;
211 for (size_t expr_idx = 0; expr_idx < acir_format.big_quad_constraints.size(); ++expr_idx) {
212 const auto& gates = acir_format.big_quad_constraints[expr_idx];
213 std::cerr << "\nBig Expression " << expr_idx << " (" << gates.size() << " gates):" << std::endl;
214 for (size_t i = 0; i < gates.size(); ++i) {
215 const auto& gate = gates[i];
216 std::cerr << " Gate " << i << ": " << gate.mul_scaling << "*w" << gate.a << "*w" << gate.b << " + "
217 << gate.a_scaling << "*w" << gate.a << " + " << gate.b_scaling << "*w" << gate.b << " + "
218 << gate.c_scaling << "*w" << gate.c << " + " << gate.d_scaling << "*w" << gate.d << " + "
219 << gate.const_scaling << " = 0" << std::endl;
220 }
221 }
222
223 std::cerr << "=== END GATES ===" << std::endl << std::endl;
224}
225
229void print_expressions_and_witnesses(const std::vector<Acir::Expression>& expressions,
230 const std::map<uint32_t, fr>& witnesses)
231{
232 std::cerr << "\n=== EXPRESSION AND WITNESS DUMP ===" << std::endl;
233
234 // Print witnesses
235 std::cerr << "\nWitnesses (" << witnesses.size() << " total):" << std::endl;
236 for (const auto& [idx, value] : witnesses) {
237 std::cerr << " w" << idx << " = " << value << std::endl;
238 }
239
240 // Print expressions
241 std::cerr << "\nExpressions (" << expressions.size() << " total):" << std::endl;
242 for (size_t i = 0; i < expressions.size(); ++i) {
243 const auto& expr = expressions[i];
244 std::cerr << "\nExpression " << i << ":" << std::endl;
245
246 // Constant term
247 fr q_c = fr::serialize_from_buffer(&expr.q_c[0]);
248 std::cerr << " Constant: " << q_c << std::endl;
249
250 // Multiplication terms
251 if (!expr.mul_terms.empty()) {
252 std::cerr << " Mul terms:" << std::endl;
253 for (const auto& [coeff_bytes, w1, w2] : expr.mul_terms) {
254 fr coeff = fr::serialize_from_buffer(&coeff_bytes[0]);
255 std::cerr << " " << coeff << " * w" << w1.value << " * w" << w2.value << std::endl;
256 }
257 }
258
259 // Linear combinations
260 if (!expr.linear_combinations.empty()) {
261 std::cerr << " Linear terms:" << std::endl;
262 for (const auto& [coeff_bytes, w] : expr.linear_combinations) {
263 fr coeff = fr::serialize_from_buffer(&coeff_bytes[0]);
264 std::cerr << " " << coeff << " * w" << w.value << std::endl;
265 }
266 }
267
268 // Evaluate expression
269 fr value = q_c;
270 for (const auto& [coeff_bytes, w1, w2] : expr.mul_terms) {
271 fr coeff = fr::serialize_from_buffer(&coeff_bytes[0]);
272 auto it1 = witnesses.find(w1.value);
273 auto it2 = witnesses.find(w2.value);
274 if (it1 != witnesses.end() && it2 != witnesses.end()) {
275 value += coeff * it1->second * it2->second;
276 }
277 }
278 for (const auto& [coeff_bytes, w] : expr.linear_combinations) {
279 fr coeff = fr::serialize_from_buffer(&coeff_bytes[0]);
280 auto it = witnesses.find(w.value);
281 if (it != witnesses.end()) {
282 value += coeff * it->second;
283 }
284 }
285
286 std::cerr << " Evaluates to: " << value;
287 if (value == fr::zero()) {
288 std::cerr << " ✓ SATISFIED";
289 } else {
290 std::cerr << " ✗ NOT SATISFIED";
291 }
293 }
294
295 std::cerr << "=== END DUMP ===" << std::endl << std::endl;
296}
297
303bool validate_witnesses(const std::vector<Acir::Expression>& expressions,
304 const std::map<uint32_t, fr>& witnesses,
305 bool expect_satisfied = true)
306{
307 (void)expect_satisfied; // Unused - kept for API compatibility
308 size_t unsatisfied_count = 0;
309
310 for (size_t i = 0; i < expressions.size(); ++i) {
311 const auto& expr = expressions[i];
312
313 // Evaluate expression
315
316 for (const auto& [coeff_bytes, w1, w2] : expr.mul_terms) {
317 fr coeff = fr::serialize_from_buffer(&coeff_bytes[0]);
318 auto it1 = witnesses.find(w1.value);
319 auto it2 = witnesses.find(w2.value);
320 if (it1 != witnesses.end() && it2 != witnesses.end()) {
321 value += coeff * it1->second * it2->second;
322 }
323 }
324
325 for (const auto& [coeff_bytes, w] : expr.linear_combinations) {
326 fr coeff = fr::serialize_from_buffer(&coeff_bytes[0]);
327 auto it = witnesses.find(w.value);
328 if (it != witnesses.end()) {
329 value += coeff * it->second;
330 }
331 }
332
333 // Check if satisfied (should be zero for ASSERT_ZERO)
334 if (value != fr::zero()) {
335 unsatisfied_count++;
336 // Silent - don't print warnings during fuzzing
337 }
338 }
339
340 // Silent validation - only return true/false
341 return unsatisfied_count == 0;
342}
343
347struct LogicConstraintInfo {
348 // Input can be either a witness index or a constant value
349 bool lhs_is_constant;
350 bool rhs_is_constant;
351 uint32_t lhs_witness; // Only valid if lhs_is_constant == false
352 uint32_t rhs_witness; // Only valid if rhs_is_constant == false
353 fr lhs_constant; // Only valid if lhs_is_constant == true
354 fr rhs_constant; // Only valid if rhs_is_constant == true
355 uint32_t output_witness;
356 uint32_t num_bits;
357 bool is_xor;
358};
359
363fr compute_and(const fr& lhs, const fr& rhs, uint32_t num_bits)
364{
365 uint256_t lhs_int = static_cast<uint256_t>(lhs);
366 uint256_t rhs_int = static_cast<uint256_t>(rhs);
367 uint256_t mask = (uint256_t(1) << num_bits) - 1;
368 uint256_t result = (lhs_int & rhs_int) & mask;
369 return fr(result);
370}
371
375fr compute_xor(const fr& lhs, const fr& rhs, uint32_t num_bits)
376{
377 uint256_t lhs_int = static_cast<uint256_t>(lhs);
378 uint256_t rhs_int = static_cast<uint256_t>(rhs);
379 uint256_t mask = (uint256_t(1) << num_bits) - 1;
380 uint256_t result = ((lhs_int ^ rhs_int) & mask);
381 return fr(result);
382}
383
387Acir::FunctionInput make_witness_input(uint32_t witness_idx)
388{
389 Acir::FunctionInput::Witness witness_input;
390 witness_input.value = Acir::Witness{ witness_idx };
391
393 input.value = witness_input;
394 return input;
395}
396
400Acir::FunctionInput make_constant_input(const fr& value)
401{
402 Acir::FunctionInput::Constant constant_input;
403 // Convert field element to bytes (big-endian)
404 constant_input.value.resize(32);
405 auto value_bytes = value.to_buffer();
406 std::copy(value_bytes.begin(), value_bytes.end(), constant_input.value.begin());
407
409 input.value = constant_input;
410 return input;
411}
412
420bool test_acir_circuit(const uint8_t* data, size_t size)
421{
422 // Minimum 32 bytes required:
423 // - 6 bytes header (num_witnesses, num_expressions, coeff_vm_steps, witness_vm_steps,
424 // range_constraint_byte, logic_constraint_byte)
425 // - 26+ bytes for VM data to execute meaningful operations on both VMs
426 if (size < 32)
427 return false;
428
429 // SECURITY FUZZING: With 10% probability, disable sanitization to test raw data handling
430 // DISABLED BY DEFAULT - To enable, define ENABLE_UNSANITIZED_FUZZING at compile time
431 // This feature intentionally generates invalid data to test robustness against malformed input
432 bool disable_sanitization = false;
433#ifdef ENABLE_UNSANITIZED_FUZZING
434 if (size > 0) {
435 disable_sanitization = (data[0] % 10) == 0; // 10% probability
436 }
437#endif
438
439 // Parse header with scaling based on input size
440 // Small inputs (~64 bytes): 2-11 witnesses, 1-3 expressions
441 // Medium inputs (~500 bytes): 2-50 witnesses, 1-10 expressions
442 // Large inputs (~4KB): 2-100 witnesses, 1-20 expressions
443
444 // Calculate scale factor based on input size
445 // Scale grows logarithmically: log2(size / 64)
446 uint32_t scale_factor = 1;
447 if (size >= 128) {
448 scale_factor = 1;
449 size_t temp_size = size / 64;
450 while (temp_size > 1 && scale_factor < 10) {
451 temp_size /= 2;
452 scale_factor++;
453 }
454 }
455
456 uint32_t max_witnesses = std::min(10u * scale_factor, 100u);
457 uint32_t max_expressions = std::min(3u * scale_factor, 20u);
458 uint32_t max_vm_steps = std::min(10u * scale_factor, 50u);
459
460 uint32_t num_witnesses = (data[0] % max_witnesses) + 2; // 2 to max_witnesses+1
461 uint32_t num_expressions = (data[1] % max_expressions) + 1; // 1 to max_expressions
462 size_t coeff_vm_steps = (data[2] % max_vm_steps) + 3; // 3 to max_vm_steps+2
463 size_t witness_vm_steps = (data[3] % max_vm_steps) + 3; // 3 to max_vm_steps+2
464 uint8_t range_constraint_byte = data[4]; // Controls range constraint generation
465 uint8_t logic_constraint_byte = data[5]; // Controls logic constraint generation
466
467 const uint8_t* vm_data = data + 6;
468 size_t vm_data_size = size - 6;
469
470 // VM 1: Generate coefficients
471 FieldVM<fr> coeff_vm(false, coeff_vm_steps);
472 size_t coeff_consumed = coeff_vm.run(vm_data, vm_data_size, false);
473 const auto& coeff_state = coeff_vm.field_internal_state;
474
475 // VM 2: Generate initial witness values
476 const uint8_t* witness_vm_data = vm_data + coeff_consumed;
477 size_t witness_vm_data_size = (coeff_consumed < vm_data_size) ? (vm_data_size - coeff_consumed) : 0;
478
479 if (witness_vm_data_size < 10)
480 return false;
481
482 FieldVM<fr> witness_vm(false, witness_vm_steps);
483 size_t witness_consumed = witness_vm.run(witness_vm_data, witness_vm_data_size, false);
484 const auto& witness_state = witness_vm.field_internal_state;
485
486 // Move past both VM data sections
487 const uint8_t* ptr = witness_vm_data + witness_consumed;
488 size_t remaining = (witness_consumed < witness_vm_data_size) ? (witness_vm_data_size - witness_consumed) : 0;
489
490 if (remaining < 10)
491 return false;
492
493 // Build expressions using VM state
494 std::vector<Acir::Expression> expressions;
495 for (uint32_t e = 0; e < num_expressions && remaining > 2; ++e) {
496 Acir::Expression expr;
497
498 // Number of terms (scales with input size via scale_factor)
499 // Small inputs: 0-1 mul, 1-3 linear
500 // Large inputs: 0-5 mul, 1-10 linear
501 uint8_t max_mul_terms = static_cast<uint8_t>(std::min(1u + scale_factor / 2, 5u));
502 uint8_t max_lin_terms = static_cast<uint8_t>(std::min(3u + scale_factor, 10u));
503
504 uint8_t num_mul = (ptr[0] % std::max(max_mul_terms, static_cast<uint8_t>(1))); // Avoid modulo 0
505 uint8_t num_lin = 1 + (ptr[1] % std::max(max_lin_terms, static_cast<uint8_t>(1)));
506 ptr += 2;
507 remaining -= 2;
508
509 // Add mul terms using VM state for coefficients
510 for (uint8_t m = 0; m < num_mul && remaining >= 3; ++m) {
511 uint8_t coeff_reg = ptr[0] % INTERNAL_STATE_SIZE;
512 uint32_t w1_idx =
513 disable_sanitization ? *reinterpret_cast<const uint16_t*>(ptr + 1) : ptr[1] % num_witnesses;
514 uint32_t w2_idx =
515 disable_sanitization ? *reinterpret_cast<const uint16_t*>(ptr + 1) : ptr[2] % num_witnesses;
516 ptr += 3;
517 remaining -= 3;
518
519 std::vector<uint8_t> coeff(32);
520 coeff = coeff_state[coeff_reg].to_buffer();
521 Acir::Witness w1, w2;
522 w1.value = w1_idx;
523 w2.value = w2_idx;
524 expr.mul_terms.push_back(std::make_tuple(coeff, w1, w2));
525 }
526
527 // Add linear terms - sometimes duplicate witnesses
528 bool force_duplicate = (num_lin > 1) && (remaining > 0) && ((ptr[0] % 3) == 0);
529 uint32_t prev_witness = 0;
530
531 for (uint8_t l = 0; l < num_lin && remaining >= 2; ++l) {
532 uint8_t coeff_reg = ptr[0] % INTERNAL_STATE_SIZE;
533 uint32_t w_idx = disable_sanitization ? ptr[1] : ptr[1] % num_witnesses;
534 ptr += 2;
535 remaining -= 2;
536
537 // Force duplicate witness to test coefficient accumulation bug
538 if (force_duplicate && l > 0 && l < 3) {
539 w_idx = prev_witness;
540 }
541 prev_witness = w_idx;
542
543 std::vector<uint8_t> coeff(32);
544 coeff = coeff_state[coeff_reg].to_buffer();
546 w.value = w_idx;
547 expr.linear_combinations.push_back(std::make_tuple(coeff, w));
548 }
549
550 // Constant term from coefficient VM
551 if (remaining > 0) {
552 uint8_t const_reg = ptr[0] % INTERNAL_STATE_SIZE;
553 ptr++;
554 remaining--;
555 expr.q_c = coeff_state[const_reg].to_buffer();
556 } else {
557 expr.q_c = std::vector<uint8_t>(32, 0);
558 }
559
560 expressions.push_back(expr);
561 }
562
563 if (expressions.empty())
564 return false;
565
566 // Filter out trivial expressions (all zero coefficients)
567 std::vector<Acir::Expression> non_trivial_expressions;
568 for (const auto& expr : expressions) {
569 if (!is_trivial_expression(expr)) {
570 non_trivial_expressions.push_back(expr);
571 }
572 }
573
574 // Skip if no non-trivial constraints remain
575 if (non_trivial_expressions.empty()) {
576 return false;
577 }
578
579 // Use only non-trivial expressions
580 expressions = non_trivial_expressions;
581
582 // Initialize witnesses with VM-generated values (much better than random!)
583 std::map<uint32_t, fr> solved_witnesses;
584 for (uint32_t i = 0; i < num_witnesses; ++i) {
585 // Use witness VM state as initial values, cycling through if needed
586 solved_witnesses[i] = witness_state[i % INTERNAL_STATE_SIZE];
587 }
588
589 // Now solve to satisfy constraints, using VM values as starting point
590 solve_witnesses(expressions, num_witnesses, solved_witnesses);
591
592 // Validate that solver satisfied the constraints
593 // bool witnesses_valid = validate_witnesses(expressions, solved_witnesses, true);
594 // if (!witnesses_valid) {
595 // abort();
596 // // The solver couldn't satisfy all constraints - skip this test case
597 // return false;
598 // }
599
600 // Deterministic witness corruption for soundness testing
601 bool witnesses_corrupted = false;
602 std::vector<uint32_t> corrupted_witness_indices;
603 // Save original witnesses before corruption for meaningful-use checking
604 std::map<uint32_t, fr> original_witnesses = solved_witnesses;
605
606 if (size > 4 && (data[size - 1] % 5) == 0) {
607 witnesses_corrupted = true;
608 uint32_t num_to_corrupt = 1 + (data[size - 2] % 3);
609 bool actually_corrupted = false;
610 std::set<uint32_t> already_corrupted; // Track which witnesses we've already corrupted
611 for (uint32_t i = 0; i < num_to_corrupt && i < num_witnesses; ++i) {
612 size_t byte_idx = size - 3 - i;
613 if (byte_idx >= 6) { // Adjusted for 6-byte header
614 uint32_t witness_to_corrupt = disable_sanitization ? data[byte_idx] : data[byte_idx] % num_witnesses;
615
616 // Skip if we've already corrupted this witness
617 if (already_corrupted.count(witness_to_corrupt) > 0) {
618 continue;
619 }
620 already_corrupted.insert(witness_to_corrupt);
621
622 fr original_value = solved_witnesses[witness_to_corrupt];
623
624 // Read corruption parameters from input - gives fuzzer control over replacement value
625 // Use bytes further back in the input for corruption mode and value
626 size_t mode_idx = (byte_idx > 1) ? byte_idx - 1 : 0;
627 size_t value_idx = (byte_idx > 2) ? byte_idx - 2 : 0;
628 uint8_t corruption_mode = (mode_idx >= 6) ? (data[mode_idx] & 0x07) : 0;
629 uint8_t corruption_seed = (value_idx >= 6) ? data[value_idx] : 1;
630
631 fr corruption_value;
632 switch (corruption_mode) {
633 case 0:
634 // Use seed as direct replacement (small value)
635 corruption_value = fr(corruption_seed);
636 break;
637 case 1:
638 // Add seed to original
639 corruption_value = original_value + fr(corruption_seed);
640 break;
641 case 2:
642 // Subtract seed from original
643 corruption_value = original_value - fr(corruption_seed);
644 break;
645 case 3:
646 // XOR with seed (flip bits)
647 corruption_value = fr(static_cast<uint256_t>(original_value) ^ uint256_t(corruption_seed));
648 break;
649 case 4:
650 // Large value: seed << 128
651 corruption_value = fr(uint256_t(corruption_seed) << 128);
652 break;
653 case 5:
654 // Negate original
655 corruption_value = -original_value;
656 break;
657 case 6:
658 // Use VM state value (previous behavior) for variety
659 corruption_value = coeff_state[(data[byte_idx] + INTERNAL_STATE_SIZE / 2) % INTERNAL_STATE_SIZE];
660 break;
661 default:
662 // Scaled seed value
663 corruption_value = fr(uint256_t(corruption_seed) << 64);
664 break;
665 }
666
667 // Ensure corruption actually changed the value
668 if (corruption_value == original_value) {
669 corruption_value = original_value + fr::one();
670 }
671 if (corruption_value == original_value) {
672 corruption_value = original_value - fr::one();
673 }
674
675 // Only corrupt if we actually have a different value
676 if (corruption_value != original_value) {
677 solved_witnesses[witness_to_corrupt] = corruption_value;
678 corrupted_witness_indices.push_back(witness_to_corrupt);
679 actually_corrupted = true;
680 }
681 }
682 }
683
684 // Only check if we actually corrupted something
685 if (actually_corrupted) {
686 // DYNAMIC CHECK: Directly evaluate expressions with corrupted witnesses
687 // This eliminates false positives from:
688 // - Algebraic cancellations (terms that cancel out)
689 // - Quadratics with multiple solutions
690 // - Under-constrained circuits
691 bool corrupted_witnesses_satisfy_constraints = validate_witnesses(expressions, solved_witnesses, false);
692
693 if (corrupted_witnesses_satisfy_constraints) {
694 // The corrupted witnesses STILL satisfy the expressions!
695 // This means the circuit is under-constrained (expected for random fuzzing)
696 // This is NOT a soundness bug - skip the CircuitChecker test
697 // IMPORTANT: Restore original witnesses since we're not testing soundness
698 solved_witnesses = original_witnesses;
699 witnesses_corrupted = false;
700 } else {
701 // Corrupted witnesses DON'T satisfy expressions - this is what we want to test!
702 // However, we need to check for assert_equal optimization cases.
703 // The assert_equal optimization (for patterns like w1 - w2 = 0) causes the
704 // circuit builder to unify w1 and w2 into a single variable. If a corrupted
705 // witness is ONLY used in such assert_equal patterns (and has no other
706 // constraints), then the unification makes the corruption ineffective.
707
708 // Detect assert_equal patterns: 2 linear terms, coefficients are negatives, no mul terms, no constant
709 std::set<uint32_t> assert_equal_only_witnesses;
710 std::set<uint32_t> constrained_in_other_expressions;
711
712 for (const auto& expr : expressions) {
713 bool is_assert_equal_pattern = expr.mul_terms.empty() && expr.linear_combinations.size() == 2 &&
715
716 if (is_assert_equal_pattern) {
719 if (coeff1 == -coeff2 && coeff1 != fr::zero()) {
720 // This is an assert_equal pattern (w1 - w2 = 0)
721 uint32_t w1 = std::get<1>(expr.linear_combinations[0]).value;
722 uint32_t w2 = std::get<1>(expr.linear_combinations[1]).value;
723 assert_equal_only_witnesses.insert(w1);
724 assert_equal_only_witnesses.insert(w2);
725 }
726 } else {
727 // Not an assert_equal pattern - track witnesses used here
728 for (const auto& [coeff_bytes, w1, w2] : expr.mul_terms) {
729 constrained_in_other_expressions.insert(w1.value);
730 constrained_in_other_expressions.insert(w2.value);
731 }
732 for (const auto& [coeff_bytes, w] : expr.linear_combinations) {
733 constrained_in_other_expressions.insert(w.value);
734 }
735 }
736 }
737
738 // Remove witnesses that appear in non-assert_equal expressions
739 for (uint32_t w : constrained_in_other_expressions) {
740 assert_equal_only_witnesses.erase(w);
741 }
742
743 // Check if ALL corrupted witnesses are only in assert_equal patterns
744 bool all_corrupted_are_assert_equal_only = true;
745 for (uint32_t w : corrupted_witness_indices) {
746 if (assert_equal_only_witnesses.count(w) == 0) {
747 all_corrupted_are_assert_equal_only = false;
748 break;
749 }
750 }
751
752 if (all_corrupted_are_assert_equal_only && !assert_equal_only_witnesses.empty()) {
753 // All corrupted witnesses are only used in assert_equal patterns
754 // The circuit builder will unify these, making corruption ineffective
755 // Skip the soundness check for this case
756 // IMPORTANT: Restore original witnesses since we're not testing soundness
757 solved_witnesses = original_witnesses;
758 witnesses_corrupted = false;
759 }
760 }
761 // else: Corrupted witnesses DON'T satisfy expressions AND are properly constrained
762 // This is the expected case for soundness testing - proceed with CircuitChecker
763 } else {
764 // Didn't actually corrupt anything, skip soundness check
765 witnesses_corrupted = false;
766 }
767 }
768
769 // ========== RANGE CONSTRAINT GENERATION ==========
770 // Generate range constraints for witnesses
771 // Uses range_constraint_byte to control which witnesses get constraints and what bit widths
772 std::vector<std::pair<uint32_t, uint32_t>> range_constraints; // (witness_idx, num_bits)
773 std::map<uint32_t, uint32_t> minimal_range; // Track tightest constraint per witness (for test setup)
774 bool should_violate_range = false;
775 uint32_t violated_witness_idx = 0;
776 uint32_t violated_range_bits = 0;
777
778 // Helper to check if a witness value satisfies a range constraint
779 auto satisfies_range = [](const fr& value, uint32_t num_bits) -> bool {
780 if (num_bits >= 254) {
781 // For 254+ bits, any field element is within range (BN254 field is ~254 bits)
782 return true;
783 }
784 // Convert field element to uint256_t
785 uint256_t value_int = value;
786
787 // Special case: 0-bit range means only value 0 is valid
788 if (num_bits == 0) {
789 return value_int == 0;
790 }
791
792 // Check if MSB position < num_bits (i.e., value < 2^num_bits)
793 size_t msb = value_int.get_msb();
794 return msb < num_bits;
795 };
796
797 // Decide if we should generate any range constraints (50% chance)
798 if ((range_constraint_byte & 0x80) != 0 && num_witnesses > 1) {
799 // Number of range constraints to add (1-4)
800 uint32_t num_range_constraints = ((range_constraint_byte >> 5) & 0x3) + 1;
801 num_range_constraints = std::min(num_range_constraints, num_witnesses - 1);
802
803 // Add range constraints for random witnesses
804 // Each constraint reads 2 bytes from input: witness_byte, bit_selector_byte
805 for (uint32_t i = 0; i < num_range_constraints; ++i) {
806 // Read parameters directly from input data - gives fuzzer byte-level control
807 size_t param_offset = i * 2;
808
809 // Bounds check - use default values if not enough data
810 uint8_t witness_byte = (param_offset < vm_data_size) ? vm_data[param_offset] : 0;
811 uint8_t bit_selector_byte = (param_offset + 1 < vm_data_size) ? vm_data[param_offset + 1] : 0;
812
813 uint32_t witness_idx = disable_sanitization ? witness_byte : witness_byte % num_witnesses;
814 uint8_t bit_selector = bit_selector_byte & 0x1F; // Mask to 5 bits [0,31]
815
816 // Map bit_selector [0,31] to num_bits with weighted distribution:
817 // 0-7 -> 8 bits (u8) 25%
818 // 8-13 -> 16 bits (u16) 19%
819 // 14-17 -> 32 bits (u32) 12%
820 // 18-20 -> 64 bits (u64) 9%
821 // 21-23 -> 128 bits (u128) 9%
822 // 24-27 -> 254 bits (max) 12%
823 // 28-29 -> 1 bit (bool) 6%
824 // 30-31 -> 0 bits (zero) 6%
825 uint32_t num_bits = 0;
826 if (bit_selector < 8) {
827 num_bits = 8;
828 } else if (bit_selector < 14) {
829 num_bits = 16;
830 } else if (bit_selector < 18) {
831 num_bits = 32;
832 } else if (bit_selector < 21) {
833 num_bits = 64;
834 } else if (bit_selector < 24) {
835 num_bits = 128;
836 } else if (bit_selector < 28) {
837 num_bits = 254;
838 } else if (bit_selector < 30) {
839 num_bits = 1;
840 } else {
841 num_bits = 0;
842 }
843
844 // Track minimal range for each witness
845 auto it = minimal_range.find(witness_idx);
846 if (it == minimal_range.end() || num_bits < it->second) {
847 minimal_range[witness_idx] = num_bits;
848 }
849 }
850
851 // Check if any existing witnesses violate their MINIMAL range constraints
852 // This tests that witnesses satisfy the tightest constraint
853 // and creates a single range constraint as the minimal one
854 bool has_accidental_violation = false;
855 for (const auto& [witness_idx, min_bits] : minimal_range) {
856 range_constraints.push_back({ witness_idx, min_bits });
857
858 auto it = solved_witnesses.find(witness_idx);
859 if (it != solved_witnesses.end()) {
860 if (!satisfies_range(it->second, min_bits)) {
861 has_accidental_violation = true;
862 break;
863 }
864 }
865 }
866
867 // Decide if we should intentionally violate a range constraint (25% chance)
868 // But only if we don't already have an accidental violation
869 if (!has_accidental_violation && (range_constraint_byte & 0x10) != 0 && !minimal_range.empty()) {
870 // Pick a witness with range constraints to violate
871 // Use the MINIMAL range for that witness
872 size_t violate_idx = range_constraint_byte % minimal_range.size();
873 auto it = minimal_range.begin();
874 std::advance(it, violate_idx);
875 uint32_t candidate_witness = it->first;
876 uint32_t candidate_bits = it->second;
877
878 // Only attempt violation for range constraints < 254 bits
879 // For >= 254 bits, every field element is in range, so violation is impossible
880 if (candidate_bits < 254) {
881 should_violate_range = true;
882 violated_witness_idx = candidate_witness;
883 violated_range_bits = candidate_bits;
884
885 // Set witness to exceed the MINIMAL range: 2^num_bits (just outside the range)
886 fr violation_value = fr(1);
887 for (uint32_t b = 0; b < violated_range_bits; ++b) {
888 violation_value = violation_value + violation_value;
889 }
890 solved_witnesses[violated_witness_idx] = violation_value;
891 }
892 // For num_bits >= 254, skip violation testing since all field elements are in range
893 } else if (has_accidental_violation) {
894 // We have an accidental range violation from the witness generation
895 // Treat this as an intentional soundness test
896 // Find which witness is violating its MINIMAL range (and only test if < 254 bits)
897 for (const auto& [witness_idx, min_bits] : minimal_range) {
898 auto it = solved_witnesses.find(witness_idx);
899 if (it != solved_witnesses.end() && min_bits < 254) {
900 if (!satisfies_range(it->second, min_bits)) {
901 should_violate_range = true;
902 violated_witness_idx = witness_idx;
903 violated_range_bits = min_bits;
904 break;
905 }
906 }
907 }
908 }
909 }
910
911 // ========== LOGIC CONSTRAINT GENERATION (AND/XOR) ==========
912 // Generate AND/XOR constraints for pairs of witnesses
913 // Uses logic_constraint_byte to control generation
914 std::vector<LogicConstraintInfo> logic_constraints;
915 bool should_violate_logic = false;
916 uint32_t violated_logic_idx = 0;
917
918 // Decide if we should generate any logic constraints (50% chance)
919 if ((logic_constraint_byte & 0x80) != 0 && num_witnesses >= 3) {
920 // Number of logic constraints to add (1-3)
921 uint32_t num_logic_constraints = ((logic_constraint_byte >> 5) & 0x3) + 1;
922 num_logic_constraints = std::min(num_logic_constraints, (num_witnesses - 1) / 3 + 1);
923
924 // We need to allocate new witnesses for outputs
925 uint32_t next_witness = num_witnesses;
926
927 for (uint32_t i = 0; i < num_logic_constraints; ++i) {
928 // Read parameters directly from input data - gives fuzzer byte-level control
929 // Each constraint uses 3 bytes: control_byte, lhs_byte, rhs_byte
930 // Read from vm_data section (after header) to allow direct mutation
931 size_t param_offset = i * 3;
932
933 // Bounds check - use default values if not enough data
934 uint8_t control_byte = (param_offset < vm_data_size) ? vm_data[param_offset] : 0;
935 uint8_t lhs_byte = (param_offset + 1 < vm_data_size) ? vm_data[param_offset + 1] : 0;
936 uint8_t rhs_byte = (param_offset + 2 < vm_data_size) ? vm_data[param_offset + 2] : 0;
937
938 // Determine bit width: bits [0:1] select from {8, 16, 32, 64}
939 uint8_t bit_selector = control_byte & 0x3;
940 uint32_t num_bits = 8;
941 switch (bit_selector) {
942 case 0:
943 num_bits = 8;
944 break;
945 case 1:
946 num_bits = 16;
947 break;
948 case 2:
949 num_bits = 32;
950 break;
951 default:
952 num_bits = 64;
953 break;
954 }
955
956 uint256_t max_val_mask = (uint256_t(1) << num_bits) - 1;
957
958 // Determine operation type and input modes from control byte
959 // bit 2: is_xor (0=AND, 1=XOR)
960 // bit 3: lhs_is_const
961 // bit 4: rhs_is_const
962 bool is_xor = (control_byte & 0x04) != 0;
963 bool lhs_is_const = (control_byte & 0x08) != 0;
964 bool rhs_is_const = (control_byte & 0x10) != 0;
965
966 // Compute witness indices from input bytes
967 uint32_t lhs_idx = lhs_byte % num_witnesses;
968 uint32_t rhs_idx = rhs_byte % num_witnesses;
969
970 // Generate constant values from input bytes (masked to num_bits)
971 // Use lhs_byte/rhs_byte as seed for constant value
972 fr lhs_const = fr::zero();
973 fr rhs_const = fr::zero();
974 if (lhs_is_const) {
975 // Expand byte to field by using it as a multiplier with witness_state
976 uint256_t const_int = (static_cast<uint256_t>(lhs_byte) *
977 static_cast<uint256_t>(witness_state[i % INTERNAL_STATE_SIZE])) &
978 max_val_mask;
979 lhs_const = fr(const_int);
980 }
981 if (rhs_is_const) {
982 uint256_t const_int = (static_cast<uint256_t>(rhs_byte) *
983 static_cast<uint256_t>(witness_state[(i + 1) % INTERNAL_STATE_SIZE])) &
984 max_val_mask;
985 rhs_const = fr(const_int);
986 }
987
988 // Allocate output witness
989 uint32_t out_idx = next_witness++;
990
991 logic_constraints.push_back(LogicConstraintInfo{ .lhs_is_constant = lhs_is_const,
992 .rhs_is_constant = rhs_is_const,
993 .lhs_witness = lhs_idx,
994 .rhs_witness = rhs_idx,
995 .lhs_constant = lhs_const,
996 .rhs_constant = rhs_const,
997 .output_witness = out_idx,
998 .num_bits = num_bits,
999 .is_xor = is_xor });
1000 }
1001
1002 // Update num_witnesses to account for new output witnesses
1003 num_witnesses = next_witness;
1004
1005 // Generate correct output witnesses for logic constraints
1006 // First, ensure input witnesses fit within num_bits range
1007 uint256_t max_val_mask;
1008 for (auto& lc : logic_constraints) {
1009 max_val_mask = (uint256_t(1) << lc.num_bits) - 1;
1010
1011 // Get lhs value (from witness or constant)
1012 fr lhs_val;
1013 if (lc.lhs_is_constant) {
1014 lhs_val = lc.lhs_constant; // Already masked during generation
1015 } else {
1016 lhs_val = solved_witnesses[lc.lhs_witness];
1017 uint256_t lhs_int = static_cast<uint256_t>(lhs_val) & max_val_mask;
1018 lhs_val = fr(lhs_int);
1019 // Update witness to be within range
1020 solved_witnesses[lc.lhs_witness] = lhs_val;
1021 }
1022
1023 // Get rhs value (from witness or constant)
1024 fr rhs_val;
1025 if (lc.rhs_is_constant) {
1026 rhs_val = lc.rhs_constant; // Already masked during generation
1027 } else {
1028 rhs_val = solved_witnesses[lc.rhs_witness];
1029 uint256_t rhs_int = static_cast<uint256_t>(rhs_val) & max_val_mask;
1030 rhs_val = fr(rhs_int);
1031 // Update witness to be within range
1032 solved_witnesses[lc.rhs_witness] = rhs_val;
1033 }
1034
1035 // Compute correct output
1036 fr output_val;
1037 if (lc.is_xor) {
1038 output_val = compute_xor(lhs_val, rhs_val, lc.num_bits);
1039 } else {
1040 output_val = compute_and(lhs_val, rhs_val, lc.num_bits);
1041 }
1042 solved_witnesses[lc.output_witness] = output_val;
1043 }
1044
1045 // Decide if we should intentionally violate a logic constraint (20% chance)
1046 if ((logic_constraint_byte & 0x08) != 0 && !logic_constraints.empty()) {
1047 should_violate_logic = true;
1048 violated_logic_idx = logic_constraint_byte % static_cast<uint32_t>(logic_constraints.size());
1049
1050 const auto& violated_lc = logic_constraints[violated_logic_idx];
1051 fr correct_output = solved_witnesses[violated_lc.output_witness];
1052
1053 // Multiple violation strategies controlled by fuzzer input
1054 // Read violation mode from vm_data to give fuzzer control
1055 size_t violation_offset = num_logic_constraints * 3; // After logic constraint params
1056 uint8_t violation_mode =
1057 (violation_offset < vm_data_size) ? (vm_data[violation_offset] & 0x07) : 0; // 3 bits = 8 modes
1058 uint8_t violation_value =
1059 (violation_offset + 1 < vm_data_size) ? vm_data[violation_offset + 1] : 1; // Replacement seed
1060
1061 fr corrupted_output;
1062 switch (violation_mode) {
1063 case 0:
1064 // Add violation_value (default: +1, but fuzzer can control)
1065 corrupted_output = correct_output + fr(violation_value);
1066 break;
1067 case 1:
1068 // Subtract violation_value
1069 corrupted_output = correct_output - fr(violation_value);
1070 break;
1071 case 2:
1072 // XOR with violation_value (flip some bits)
1073 corrupted_output = fr(static_cast<uint256_t>(correct_output) ^ uint256_t(violation_value));
1074 break;
1075 case 3:
1076 // Replace with violation_value directly
1077 corrupted_output = fr(violation_value);
1078 break;
1079 case 4:
1080 // Replace with scaled violation_value (larger corruption)
1081 corrupted_output = fr(uint256_t(violation_value) << 8);
1082 break;
1083 case 5:
1084 // Negate
1085 corrupted_output = -correct_output;
1086 if (corrupted_output == correct_output) {
1087 corrupted_output = fr::one(); // If negation is same (0), use 1
1088 }
1089 break;
1090 case 6:
1091 // Make input witness exceed num_bits (test range check in logic gate)
1092 // Only if lhs is a witness
1093 if (!violated_lc.lhs_is_constant) {
1094 // Set lhs to 2^num_bits + violation_value (just over the limit)
1095 uint256_t over_limit = (uint256_t(1) << violated_lc.num_bits) + violation_value;
1096 solved_witnesses[violated_lc.lhs_witness] = fr(over_limit);
1097 }
1098 // Also corrupt output to ensure constraint fails
1099 corrupted_output = correct_output + fr::one();
1100 break;
1101 case 7:
1102 // Make input witness exceed num_bits on rhs side
1103 if (!violated_lc.rhs_is_constant) {
1104 uint256_t over_limit = (uint256_t(1) << violated_lc.num_bits) + violation_value;
1105 solved_witnesses[violated_lc.rhs_witness] = fr(over_limit);
1106 }
1107 corrupted_output = correct_output + fr::one();
1108 break;
1109 }
1110
1111 // Ensure corruption actually changed the value
1112 if (corrupted_output == correct_output) {
1113 corrupted_output = correct_output + fr::one();
1114 }
1115 solved_witnesses[violated_lc.output_witness] = corrupted_output;
1116 }
1117 }
1118
1119 try {
1120 // Create ACIR Circuit
1121 // Note: num_witnesses may have been updated by logic constraint generation
1122 Acir::Circuit circuit;
1123 circuit.function_name = "main";
1124 circuit.current_witness_index = num_witnesses - 1;
1125 circuit.private_parameters = {};
1126 circuit.public_parameters.value = {};
1127 circuit.return_values.value = {};
1128 circuit.assert_messages = {};
1129
1130 // Add AssertZero opcodes
1131 for (const auto& expr : expressions) {
1132 Acir::Opcode::AssertZero assert_zero;
1133 assert_zero.value = expr;
1134
1135 Acir::Opcode opcode;
1136 opcode.value = assert_zero;
1137 circuit.opcodes.push_back(opcode);
1138 }
1139
1140 // Add Range constraint opcodes
1141 for (const auto& [witness_idx, num_bits] : range_constraints) {
1142 // Create FunctionInput for the witness
1143 Acir::FunctionInput::Witness witness_input;
1144 witness_input.value = Acir::Witness{ witness_idx };
1145
1146 Acir::FunctionInput input;
1147 input.value = witness_input;
1148
1149 // Create RANGE BlackBoxFuncCall
1151 range_op.input = input;
1152 range_op.num_bits = num_bits;
1153
1154 // Wrap in BlackBoxFuncCall
1155 Acir::BlackBoxFuncCall bb_call;
1156 bb_call.value = range_op;
1157
1158 // Wrap in Opcode
1160 bb_opcode.value = bb_call;
1161
1162 Acir::Opcode opcode;
1163 opcode.value = bb_opcode;
1164 circuit.opcodes.push_back(opcode);
1165 }
1166
1167 // Add Logic constraint opcodes (AND/XOR)
1168 for (const auto& lc : logic_constraints) {
1169 // Create lhs input (witness or constant)
1170 Acir::FunctionInput lhs_input =
1171 lc.lhs_is_constant ? make_constant_input(lc.lhs_constant) : make_witness_input(lc.lhs_witness);
1172
1173 // Create rhs input (witness or constant)
1174 Acir::FunctionInput rhs_input =
1175 lc.rhs_is_constant ? make_constant_input(lc.rhs_constant) : make_witness_input(lc.rhs_witness);
1176
1177 if (lc.is_xor) {
1178 // Create XOR BlackBoxFuncCall
1180 xor_op.lhs = lhs_input;
1181 xor_op.rhs = rhs_input;
1182 xor_op.num_bits = lc.num_bits;
1183 xor_op.output = Acir::Witness{ lc.output_witness };
1184
1185 Acir::BlackBoxFuncCall bb_call;
1186 bb_call.value = xor_op;
1187
1189 bb_opcode.value = bb_call;
1190
1191 Acir::Opcode opcode;
1192 opcode.value = bb_opcode;
1193 circuit.opcodes.push_back(opcode);
1194 } else {
1195 // Create AND BlackBoxFuncCall
1197 and_op.lhs = lhs_input;
1198 and_op.rhs = rhs_input;
1199 and_op.num_bits = lc.num_bits;
1200 and_op.output = Acir::Witness{ lc.output_witness };
1201
1202 Acir::BlackBoxFuncCall bb_call;
1203 bb_call.value = and_op;
1204
1206 bb_opcode.value = bb_call;
1207
1208 Acir::Opcode opcode;
1209 opcode.value = bb_opcode;
1210 circuit.opcodes.push_back(opcode);
1211 }
1212 }
1213
1214 // *** Go through acir_to_constraint_buf pipeline directly ***
1215 // This exercises the core conversion logic without serialization issues
1217
1218 // ========== BUILD CIRCUIT FROM ACIR FORMAT ==========
1219
1220 // Create witness vector
1221 WitnessVector witness_vec;
1222 witness_vec.reserve(num_witnesses);
1223 for (uint32_t i = 0; i < num_witnesses; ++i) {
1224 auto it = solved_witnesses.find(i);
1225 if (it != solved_witnesses.end()) {
1226 witness_vec.push_back(it->second);
1227 } else {
1228 witness_vec.push_back(fr::zero());
1229 }
1230 }
1231
1232 // Build circuit using the proper constructor that initializes witnesses
1233 // NOTE: Must use the witness-aware constructor, not default constructor!
1234 // The default constructor leaves witnesses uninitialized, causing false negatives.
1235 UltraCircuitBuilder builder{ witness_vec, acir_format.public_inputs, /*is_write_vk_mode=*/false };
1236
1238
1239 // Check if the builder is in a failed state (e.g., from assert_equal with unequal values)
1240 if (builder.failed()) {
1241#ifndef FUZZING_DISABLE_WARNINGS
1242 info("Circuit builder is in a failed state: ", builder.err());
1243#endif
1244 return false;
1245 }
1246
1247 bool circuit_valid = CircuitChecker::check(builder);
1248
1249 // Re-verify that range violations still exist after all witness modifications
1250 // (Logic constraint generation may have masked witnesses, accidentally "fixing" the violation)
1251 if (should_violate_range) {
1252 fr actual_value = solved_witnesses[violated_witness_idx];
1253 if (satisfies_range(actual_value, violated_range_bits)) {
1254 // The violation was accidentally fixed by subsequent witness modifications
1255 should_violate_range = false;
1256 }
1257 }
1258
1259 // Re-verify that witness corruption still breaks the expressions
1260 // Logic constraint generation may have modified witnesses (masking for bit widths),
1261 // potentially undoing the corruption or making it ineffective
1262 if (witnesses_corrupted) {
1263 // First check: verify corrupted witnesses still have different values from originals
1264 bool corruption_still_effective = false;
1265 for (uint32_t corrupted_w : corrupted_witness_indices) {
1266 if (solved_witnesses[corrupted_w] != original_witnesses[corrupted_w]) {
1267 corruption_still_effective = true;
1268 break;
1269 }
1270 }
1271
1272 if (!corruption_still_effective) {
1273 // All corrupted values were reset by subsequent operations (e.g., logic masking)
1274 witnesses_corrupted = false;
1275 } else {
1276 // Second check: verify corruption actually breaks the expressions
1277 bool expressions_still_broken = !validate_witnesses(expressions, solved_witnesses, false);
1278 if (!expressions_still_broken) {
1279 // Expressions are still satisfied despite corruption - under-constrained circuit
1280 witnesses_corrupted = false;
1281 }
1282 }
1283 }
1284
1285 // SOUNDNESS CHECK: Corrupted witnesses, range violations, or logic violations should fail
1286 if (witnesses_corrupted || should_violate_range || should_violate_logic) {
1287 if (circuit_valid) {
1288 std::cerr << "\n=== CRITICAL SOUNDNESS BUG ===" << std::endl;
1289 if (witnesses_corrupted) {
1290 std::cerr << "Corrupted witnesses passed CircuitChecker verification!" << std::endl;
1291 }
1292 if (should_violate_range) {
1293 std::cerr << "Range constraint violation passed CircuitChecker verification!" << std::endl;
1294 std::cerr << "Violated witness: w" << violated_witness_idx << " (range: " << violated_range_bits
1295 << " bits)" << std::endl;
1296 std::cerr << "Witness value: " << solved_witnesses[violated_witness_idx] << std::endl;
1297 }
1298 if (should_violate_logic) {
1299 std::cerr << "Logic constraint violation passed CircuitChecker verification!" << std::endl;
1300 const auto& violated_lc = logic_constraints[violated_logic_idx];
1301 std::cerr << "Violated logic op: " << (violated_lc.is_xor ? "XOR" : "AND") << std::endl;
1302 std::cerr << "LHS witness w" << violated_lc.lhs_witness << " = "
1303 << solved_witnesses[violated_lc.lhs_witness] << std::endl;
1304 std::cerr << "RHS witness w" << violated_lc.rhs_witness << " = "
1305 << solved_witnesses[violated_lc.rhs_witness] << std::endl;
1306 std::cerr << "Output witness w" << violated_lc.output_witness << " = "
1307 << solved_witnesses[violated_lc.output_witness] << std::endl;
1308 std::cerr << "Num bits: " << violated_lc.num_bits << std::endl;
1309 }
1310 std::cerr << "Num witnesses: " << num_witnesses << ", Num expressions: " << expressions.size()
1311 << std::endl;
1312 print_expressions_and_witnesses(expressions, solved_witnesses);
1313 print_acir_format_gates(acir_format);
1314 abort();
1315 }
1316 return false;
1317 }
1318
1319 // COMPLETENESS CHECK
1320 if (!circuit_valid) {
1321 std::cerr << "\n=== COMPLETENESS BUG ===" << std::endl;
1322 std::cerr << "Valid witnesses failed CircuitChecker verification!" << std::endl;
1323 std::cerr << "Num witnesses: " << num_witnesses << ", Num expressions: " << expressions.size() << std::endl;
1324 print_expressions_and_witnesses(expressions, solved_witnesses);
1325 print_acir_format_gates(acir_format);
1326 abort();
1327 }
1328
1329 return circuit_valid;
1330
1331 } catch (const std::exception& e) {
1332 // Silent - exceptions are expected for edge cases and corrupted witnesses
1333 // If there's a real bug, it will show up in the coverage/crash reports
1334 (void)e; // Avoid unused variable warning
1335 return false;
1336 } catch (...) {
1337 // Silent - just skip this test case
1338 return false;
1339 }
1340}
1341
1342} // anonymous namespace
1343
1347extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
1348{
1349 if (size < 50) {
1350 return 0;
1351 }
1352
1353 test_acir_circuit(data, size);
1354
1355 return 0;
1356}
1357
1361extern "C" size_t LLVMFuzzerCustomMutator(uint8_t* data, size_t size, size_t max_size, unsigned int seed)
1362{
1363 if (size < 10 || max_size < 50) {
1364 return LLVMFuzzerMutate(data, size, max_size);
1365 }
1366
1367 std::srand(seed);
1368 int strategy = static_cast<int>(static_cast<unsigned>(std::rand()) % 100u);
1369
1370 if (strategy < 30) {
1371 // Mutate VM instructions (scales with input size)
1372 if (size > 10) {
1373 size_t vm_section_start = 6; // Header is now 6 bytes
1374 size_t vm_section_end = size / 2; // First half is VM data
1375 if (vm_section_end > vm_section_start) {
1376 size_t pos = vm_section_start + (static_cast<unsigned>(std::rand()) %
1377 static_cast<unsigned>(vm_section_end - vm_section_start));
1378 data[pos] = static_cast<uint8_t>(static_cast<unsigned>(std::rand()) % 256u);
1379 }
1380 }
1381 } else if (strategy < 55) {
1382 // Mutate expression structure (second half of input)
1383 if (size > 20) {
1384 size_t expr_section_start = size / 2;
1385 size_t expr_section_end = size - 10;
1386 if (expr_section_end > expr_section_start) {
1387 size_t pos = expr_section_start + (static_cast<unsigned>(std::rand()) %
1388 static_cast<unsigned>(expr_section_end - expr_section_start));
1389 data[pos] = static_cast<uint8_t>(static_cast<unsigned>(std::rand()) % 256u);
1390 }
1391 }
1392 } else if (strategy < 70) {
1393 // Mutate header (controls scaling and constraint generation)
1394 if (size > 5) {
1395 data[static_cast<unsigned>(std::rand()) % 6u] =
1396 static_cast<uint8_t>(static_cast<unsigned>(std::rand()) % 256u);
1397 }
1398 } else if (strategy < 80) {
1399 // Grow input size (add more data for larger test cases)
1400 if (size < max_size && max_size - size >= 32) {
1401 size_t grow_by = std::min(static_cast<size_t>(32), max_size - size);
1402 // Shift existing data and insert new bytes
1403 for (size_t i = 0; i < grow_by; ++i) {
1404 data[size + i] = static_cast<uint8_t>(static_cast<unsigned>(std::rand()) % 256u);
1405 }
1406 return size + grow_by;
1407 }
1408 } else if (strategy < 85) {
1409 // Shrink input size (remove redundant data)
1410 if (size > 100) {
1411 size_t shrink_by = std::min(static_cast<size_t>(32), size - 50);
1412 return size - shrink_by;
1413 }
1414 } else {
1415 // Use LibFuzzer's built-in mutation
1416 return LLVMFuzzerMutate(data, size, max_size);
1417 }
1418
1419 return size;
1420}
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:89
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)
Add to the builder the constraints contained in an AcirFormat instance.
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 lhs
Definition acir.hpp:3089
Acir::FunctionInput rhs
Definition acir.hpp:3090
Acir::FunctionInput input
Definition acir.hpp:3169
Acir::FunctionInput rhs
Definition acir.hpp:3130
Acir::FunctionInput lhs
Definition acir.hpp:3129
std::variant< AES128Encrypt, AND, XOR, RANGE, Blake2s, Blake3, EcdsaSecp256k1, EcdsaSecp256r1, MultiScalarMul, EmbeddedCurveAdd, Keccakf1600, RecursiveAggregation, Poseidon2Permutation, Sha256Compression > value
Definition acir.hpp:3602
Acir::PublicInputs return_values
Definition acir.hpp:5008
std::vector< Acir::Opcode > opcodes
Definition acir.hpp:5005
uint32_t current_witness_index
Definition acir.hpp:5004
std::vector< Acir::Witness > private_parameters
Definition acir.hpp:5006
Acir::PublicInputs public_parameters
Definition acir.hpp:5007
std::string function_name
Definition acir.hpp:5003
std::vector< std::tuple< Acir::OpcodeLocation, Acir::AssertionPayload > > assert_messages
Definition acir.hpp:5009
std::vector< std::tuple< std::vector< uint8_t >, Acir::Witness > > linear_combinations
Definition acir.hpp:4000
std::vector< uint8_t > q_c
Definition acir.hpp:4001
std::vector< std::tuple< std::vector< uint8_t >, Acir::Witness, Acir::Witness > > mul_terms
Definition acir.hpp:3999
std::vector< uint8_t > value
Definition acir.hpp:2923
std::variant< Constant, Witness > value
Definition acir.hpp:2962
Acir::Expression value
Definition acir.hpp:4359
Acir::BlackBoxFuncCall value
Definition acir.hpp:4379
std::variant< AssertZero, BlackBoxFuncCall, MemoryOp, MemoryInit, BrilligCall, Call > value
Definition acir.hpp:4546
std::vector< Acir::Witness > value
Definition acir.hpp:4983
uint32_t value
Definition acir.hpp:2901
Barretenberg's representation of ACIR constraints.
Metadata required to create a circuit.
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()