Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
relation_failure.test.cpp
Go to the documentation of this file.
1
52#include <gtest/gtest.h>
53
54using namespace bb;
55
56namespace {
57
59using FF = typename Flavor::FF;
61
65struct ValidTranslatorState {
68};
69
77ValidTranslatorState build_valid_translator_state()
78{
79 const size_t full_circuit_size = Flavor::MINI_CIRCUIT_SIZE * Flavor::CONCATENATION_GROUP_SIZE;
81
84 ProverPolynomials& pp = key.proving_key->polynomials;
85
86 // Fill group wire polynomials with random 14-bit values in circuit region, random FF in masking rows
87 for (const auto& group : pp.get_groups_to_be_concatenated()) {
88 for (auto& poly : group) {
89 if (poly.is_empty()) {
90 continue;
91 }
92 for (size_t i = poly.start_index(); i < poly.end_index() - NUM_DISABLED_ROWS_IN_SUMCHECK; i++) {
93 poly.at(i) = engine.get_random_uint16() & ((1 << Flavor::MICRO_LIMB_BITS) - 1);
94 }
95 for (size_t i = poly.end_index() - NUM_DISABLED_ROWS_IN_SUMCHECK; i < poly.end_index(); i++) {
96 poly.at(i) = FF::random_element();
97 }
98 }
99 }
100
101 // Reallocate lagrange polynomials to full circuit size and compute them
102 pp.lagrange_first = typename Flavor::Polynomial(full_circuit_size);
103 pp.lagrange_last = typename Flavor::Polynomial(full_circuit_size);
104 pp.lagrange_real_last = typename Flavor::Polynomial(full_circuit_size);
105 pp.lagrange_masking = typename Flavor::Polynomial(full_circuit_size);
106
107 key.compute_lagrange_polynomials();
108 key.compute_extra_range_constraint_numerator();
109 key.compute_concatenated_polynomials();
110 key.compute_translator_range_constraint_ordered_polynomials();
111
112 // Compute grand product
114 compute_grand_product<Flavor, TranslatorPermutationRelation<FF>>(pp, params);
115
116 return { std::move(key), params };
117}
118
127ValidTranslatorState build_valid_accumulator_transfer_state()
128{
129 using BF = typename Flavor::BF;
130 using GroupElement = typename Flavor::GroupElement;
131
133
134 auto op_queue = std::make_shared<ECCOpQueue>();
135 op_queue->construct_zk_columns();
136
137 // Add mixed ops, merge, more mixed ops, random end ops, final merge
138 for (size_t i = 0; i < 50; i++) {
139 op_queue->add_accumulate(GroupElement::random_element(&engine));
140 op_queue->mul_accumulate(GroupElement::random_element(&engine), FF::random_element(&engine));
141 }
142 op_queue->eq_and_reset();
143 op_queue->merge();
144 for (size_t i = 0; i < 50; i++) {
145 op_queue->add_accumulate(GroupElement::random_element(&engine));
146 op_queue->mul_accumulate(GroupElement::random_element(&engine), FF::random_element(&engine));
147 }
148 op_queue->eq_and_reset();
149 for (size_t i = 0; i < Flavor::CircuitBuilder::NUM_RANDOM_OPS_END; i++) {
150 op_queue->random_op_ultra_only();
151 }
152 op_queue->merge_fixed_append(op_queue->get_append_offset_for_prover());
153
154 const auto batching_challenge_v = BF::random_element(&engine);
155 const auto evaluation_input_x = BF::random_element(&engine);
156
157 auto circuit_builder = Flavor::CircuitBuilder(batching_challenge_v, evaluation_input_x, op_queue);
158 TranslatorProvingKey key(circuit_builder);
159
160 // Read accumulated_result from the witness (same as the prover does)
161 auto& pp = key.proving_key->polynomials;
163 params.accumulated_result = { pp.accumulators_binary_limbs_0[Flavor::RESULT_ROW],
164 pp.accumulators_binary_limbs_1[Flavor::RESULT_ROW],
165 pp.accumulators_binary_limbs_2[Flavor::RESULT_ROW],
166 pp.accumulators_binary_limbs_3[Flavor::RESULT_ROW] };
167
168 // Populate evaluation_input_x and batching_challenge_v limbs + native values
169 // (needed by TranslatorNonNativeFieldRelation; harmless for other relations)
170 static constexpr size_t NUM_LIMB_BITS = Flavor::CircuitBuilder::NUM_LIMB_BITS;
171 auto uint_input_x = uint256_t(evaluation_input_x);
172 params.evaluation_input_x = { uint_input_x.slice(0, NUM_LIMB_BITS),
173 uint_input_x.slice(NUM_LIMB_BITS, NUM_LIMB_BITS * 2),
174 uint_input_x.slice(NUM_LIMB_BITS * 2, NUM_LIMB_BITS * 3),
175 uint_input_x.slice(NUM_LIMB_BITS * 3, NUM_LIMB_BITS * 4),
176 uint_input_x };
177 auto v_power = BF::one();
178 for (size_t i = 0; i < 4; i++) {
179 v_power *= batching_challenge_v;
180 auto uint_v_power = uint256_t(v_power);
181 params.batching_challenge_v.at(i) = { uint_v_power.slice(0, NUM_LIMB_BITS),
182 uint_v_power.slice(NUM_LIMB_BITS, NUM_LIMB_BITS * 2),
183 uint_v_power.slice(NUM_LIMB_BITS * 2, NUM_LIMB_BITS * 3),
184 uint_v_power.slice(NUM_LIMB_BITS * 3, NUM_LIMB_BITS * 4),
185 uint_v_power };
186 }
187
188 return { std::move(key), params };
189}
190
191} // anonymous namespace
192
193class TranslatorRelationFailureTests : public ::testing::Test {
194 protected:
196};
197
202TEST_F(TranslatorRelationFailureTests, PermutationFailsOnConcatenatedCorruption)
203{
204 auto [key, params] = build_valid_translator_state();
205 auto& pp = key.proving_key->polynomials;
206
207 // Baseline: permutation relation passes
208 auto baseline =
209 RelationChecker<Flavor>::check<TranslatorPermutationRelation<FF>>(pp, params, "TranslatorPermutationRelation");
210 EXPECT_TRUE(baseline.empty()) << "Baseline permutation should pass";
211
212 // Corrupt a non-masking position in block 1 of concatenated_range_constraints_0
213 // Block 1 starts at MINI_CIRCUIT_SIZE, position 1 within it is non-masking (start_index=1)
214 const size_t corrupt_pos = Flavor::MINI_CIRCUIT_SIZE + 1;
215 pp.concatenated_range_constraints_0.at(corrupt_pos) = FF::random_element();
216
217 // Re-compute grand product with corrupted data
218 compute_grand_product<Flavor, TranslatorPermutationRelation<FF>>(pp, params);
219
220 auto failures =
221 RelationChecker<Flavor>::check<TranslatorPermutationRelation<FF>>(pp, params, "TranslatorPermutationRelation");
222 EXPECT_FALSE(failures.empty()) << "Permutation should fail after concatenated corruption";
223}
224
229TEST_F(TranslatorRelationFailureTests, DeltaRangeFailsOnMaxValueCorruption)
230{
231 auto [key, params] = build_valid_translator_state();
232 auto& pp = key.proving_key->polynomials;
233
234 const size_t full_circuit_size = Flavor::MINI_CIRCUIT_SIZE * Flavor::CONCATENATION_GROUP_SIZE;
235
236 // Baseline: delta range passes
238 pp, params, "TranslatorDeltaRangeConstraintRelation");
239 EXPECT_TRUE(baseline.empty()) << "Baseline delta range should pass";
240
241 // The real_last position must hold exactly 2^14 - 1. Corrupt it to something else.
242 const size_t real_last_pos = full_circuit_size - Flavor::MAX_RANDOM_VALUES_PER_ORDERED - 1;
243 pp.ordered_range_constraints_0.at(real_last_pos) = FF(42);
244
246 pp, params, "TranslatorDeltaRangeConstraintRelation");
247 EXPECT_FALSE(failures.empty()) << "Delta range should fail when real_last position != 2^14 - 1";
248}
249
253TEST_F(TranslatorRelationFailureTests, PermutationFailsOnZPermCorruption)
254{
255 auto [key, params] = build_valid_translator_state();
256 auto& pp = key.proving_key->polynomials;
257
258 // Baseline: permutation relation passes
259 auto baseline =
260 RelationChecker<Flavor>::check<TranslatorPermutationRelation<FF>>(pp, params, "TranslatorPermutationRelation");
261 EXPECT_TRUE(baseline.empty()) << "Baseline permutation should pass";
262
263 // Corrupt z_perm at a position in the middle of the circuit
264 const size_t corrupt_pos = (Flavor::MINI_CIRCUIT_SIZE * 2) + 500;
265 pp.z_perm.at(corrupt_pos) = FF::random_element();
266 // Must also update the shifted view
267 pp.set_shifted();
268
269 auto failures =
270 RelationChecker<Flavor>::check<TranslatorPermutationRelation<FF>>(pp, params, "TranslatorPermutationRelation");
271 EXPECT_FALSE(failures.empty()) << "Permutation should fail after z_perm corruption";
272}
273
284TEST_F(TranslatorRelationFailureTests, PermutationFailsOnZPermNonZeroAtFirstRow)
285{
286 auto [key, params] = build_valid_translator_state();
287 auto& pp = key.proving_key->polynomials;
288
289 // Baseline: permutation relation passes
290 auto baseline =
291 RelationChecker<Flavor>::check<TranslatorPermutationRelation<FF>>(pp, params, "TranslatorPermutationRelation");
292 EXPECT_TRUE(baseline.empty()) << "Baseline permutation should pass";
293
294 // Derive expected lagrange_first position from z_perm shiftable structure
295 ASSERT_TRUE(pp.z_perm.is_shiftable());
296 size_t structural_first_row = pp.z_perm.start_index() - 1;
297
298 // Independently scan lagrange_first for its non-zero entry
299 const auto& lagrange_first = pp.lagrange_first;
300 size_t scanned_first_row = 0;
301 bool found = false;
302 for (size_t i = lagrange_first.start_index(); i < lagrange_first.end_index(); ++i) {
303 if (lagrange_first[i] != FF(0)) {
304 scanned_first_row = i;
305 found = true;
306 break;
307 }
308 }
309 ASSERT_TRUE(found) << "lagrange_first has no non-zero entry";
310 ASSERT_EQ(structural_first_row, scanned_first_row)
311 << "lagrange_first position doesn't match z_perm shiftable structure";
312
313 const size_t first_row = scanned_first_row;
314
315 // Expand to full polynomials so we can write at the zero row
316 pp.z_perm = pp.z_perm.full();
317 pp.z_perm_shift = pp.z_perm_shift.full();
318
319 ASSERT_EQ(pp.z_perm[first_row], FF(0));
320
321 // Tamper: set z_perm to non-zero where lagrange_first is active
322 pp.z_perm.at(first_row) = FF(1);
323
325 pp, params, "TranslatorPermutationRelation - After setting z_perm != 0 at lagrange_first");
326 EXPECT_FALSE(failures.empty()) << "Permutation should fail after z_perm init corruption";
327 // Sub-relation 2 (lagrange_first * z_perm = 0) should catch this
328 EXPECT_TRUE(failures.contains(2)) << "Sub-relation 2 (z_perm init) should catch the corruption";
329 EXPECT_EQ(failures.at(2), static_cast<uint32_t>(first_row)) << "Failure should be at lagrange_first row";
330}
331
340TEST_F(TranslatorRelationFailureTests, ShiftableFirstCoeffZeroFailsOnOrderedNonZero)
341{
342 auto [key, params] = build_valid_translator_state();
343 auto& pp = key.proving_key->polynomials;
344
345 // Baseline: the relation passes on valid data
347 pp, params, "TranslatorShiftableFirstCoeffZeroRelation");
348 EXPECT_TRUE(baseline.empty()) << "Baseline shiftable-first-coeff-zero should pass";
349
350 // Derive the lagrange_first position from the ordered poly's shiftable structure and cross-check it.
351 ASSERT_TRUE(pp.ordered_range_constraints_0.is_shiftable());
352 const size_t first_row = pp.ordered_range_constraints_0.start_index() - 1;
353 ASSERT_NE(pp.lagrange_first[first_row], FF(0)) << "lagrange_first should be active at the ordered poly's zero row";
354
355 // Expand to a full polynomial so we can write at the zero row. The shift drops index 0, so the unshifted/shift
356 // views stay consistent after we only touch the zero row.
357 pp.ordered_range_constraints_0 = pp.ordered_range_constraints_0.full();
358 pp.ordered_range_constraints_0_shift = pp.ordered_range_constraints_0_shift.full();
359 ASSERT_EQ(pp.ordered_range_constraints_0[first_row], FF(0));
360
361 // Tamper: place an out-of-range micro-limb (-3 = p-3) at the sorted-chain start.
362 pp.ordered_range_constraints_0.at(first_row) = -FF(3);
363
365 pp, params, "TranslatorShiftableFirstCoeffZeroRelation - After setting ordered_range_constraints_0[0] != 0");
366 EXPECT_FALSE(failures.empty()) << "Relation should fail after nonzero first sorted value";
367 // Sub-relation 0 (lagrange_first * ordered_range_constraints_0 = 0) should catch this
368 EXPECT_TRUE(failures.contains(0)) << "Sub-relation 0 (ordered_0 first-coeff anchor) should catch the corruption";
369 EXPECT_EQ(failures.at(0), static_cast<uint32_t>(first_row)) << "Failure should be at lagrange_first row";
370}
371
380TEST_F(TranslatorRelationFailureTests, DeltaRangeFailsOnOrderedMaskingBoundary)
381{
382 auto [key, params] = build_valid_translator_state();
383 auto& pp = key.proving_key->polynomials;
384
385 const size_t full_circuit_size = Flavor::MINI_CIRCUIT_SIZE * Flavor::CONCATENATION_GROUP_SIZE;
386
387 // Baseline: delta range passes
389 pp, params, "TranslatorDeltaRangeConstraintRelation");
390 EXPECT_TRUE(baseline.empty()) << "Baseline delta range should pass";
391
392 // Position circuit_size - MAX_RANDOM - 2 is the last row with delta enforcement active.
393 // The next row (real_last) holds 2^14 - 1. Setting this to 0 creates delta = 16383.
394 const size_t boundary_pos = full_circuit_size - Flavor::MAX_RANDOM_VALUES_PER_ORDERED - 2;
395 pp.ordered_range_constraints_0.at(boundary_pos) = FF(0);
396
398 pp, params, "TranslatorDeltaRangeConstraintRelation");
399 EXPECT_FALSE(failures.empty()) << "Delta range should fail at the masking boundary";
400}
401
406TEST_F(TranslatorRelationFailureTests, DeltaRangeFailsOnNegativeDelta)
407{
408 auto [key, params] = build_valid_translator_state();
409 auto& pp = key.proving_key->polynomials;
410
411 // Baseline: delta range passes
413 pp, params, "TranslatorDeltaRangeConstraintRelation");
414 EXPECT_TRUE(baseline.empty()) << "Baseline delta range should pass";
415
416 // Set ordered[pos] to be larger than ordered[pos+1], creating a negative delta at row pos.
417 // D = ordered[pos+1] - ordered[pos] becomes a huge field element (negative in the integers),
418 // which is not in {0,1,2,3}.
419 const size_t pos = 5000;
420 pp.ordered_range_constraints_0.at(pos) = pp.ordered_range_constraints_0[pos + 1] + FF(1);
421
423 pp, params, "TranslatorDeltaRangeConstraintRelation");
424 EXPECT_FALSE(failures.empty()) << "Delta range should fail on descending (negative delta) pair";
425}
426
431TEST_F(TranslatorRelationFailureTests, DeltaRangeFailsOnDeltaFour)
432{
433 auto [key, params] = build_valid_translator_state();
434 auto& pp = key.proving_key->polynomials;
435
436 // Baseline: delta range passes
438 pp, params, "TranslatorDeltaRangeConstraintRelation");
439 EXPECT_TRUE(baseline.empty()) << "Baseline delta range should pass";
440
441 // Pick a mid-range position and set it so the delta to the next row is exactly 4
442 const size_t pos = 3000;
443 FF next_val = pp.ordered_range_constraints_0[pos + 1];
444 pp.ordered_range_constraints_0.at(pos) = next_val - FF(4);
445
447 pp, params, "TranslatorDeltaRangeConstraintRelation");
448 EXPECT_FALSE(failures.empty()) << "Delta range should fail when delta is exactly 4";
449}
450
455TEST_F(TranslatorRelationFailureTests, DeltaRangeFailsOnFirstSortedValueTooLarge)
456{
457 auto [key, params] = build_valid_translator_state();
458 auto& pp = key.proving_key->polynomials;
459
460 // Baseline: delta range passes
462 pp, params, "TranslatorDeltaRangeConstraintRelation");
463 EXPECT_TRUE(baseline.empty()) << "Baseline delta range should pass";
464
465 // Position 0 is virtual zero (always 0). Position 1 is the first sorted value.
466 // Setting it to 100 creates delta = 100 from row 0, which violates D ∈ {0,1,2,3}.
467 pp.ordered_range_constraints_0.at(1) = FF(100);
468
470 pp, params, "TranslatorDeltaRangeConstraintRelation");
471 EXPECT_FALSE(failures.empty()) << "Delta range should fail when first sorted value > 3";
472}
473
478TEST_F(TranslatorRelationFailureTests, DeltaRangeFailsOnFifthOrderedPolyCorruption)
479{
480 auto [key, params] = build_valid_translator_state();
481 auto& pp = key.proving_key->polynomials;
482
483 // Baseline: delta range passes
485 pp, params, "TranslatorDeltaRangeConstraintRelation");
486 EXPECT_TRUE(baseline.empty()) << "Baseline delta range should pass";
487
488 // Corrupt ordered_range_constraints_4 at a mid position
489 const size_t pos = 2000;
490 pp.ordered_range_constraints_4.at(pos) = pp.ordered_range_constraints_4[pos - 1] + FF(100);
491
493 pp, params, "TranslatorDeltaRangeConstraintRelation");
494 EXPECT_FALSE(failures.empty()) << "Delta range should fail on 5th ordered poly corruption";
495}
496
501TEST_F(TranslatorRelationFailureTests, PermutationFailsOnOrderedCorruption)
502{
503 auto [key, params] = build_valid_translator_state();
504 auto& pp = key.proving_key->polynomials;
505
506 // Baseline: permutation relation passes
507 auto baseline =
508 RelationChecker<Flavor>::check<TranslatorPermutationRelation<FF>>(pp, params, "TranslatorPermutationRelation");
509 EXPECT_TRUE(baseline.empty()) << "Baseline permutation should pass";
510
511 // Corrupt a non-masking position in ordered_range_constraints_0 without recomputing z_perm
512 const size_t corrupt_pos = 500;
513 pp.ordered_range_constraints_0.at(corrupt_pos) = FF::random_element();
514
515 auto failures =
516 RelationChecker<Flavor>::check<TranslatorPermutationRelation<FF>>(pp, params, "TranslatorPermutationRelation");
517 EXPECT_FALSE(failures.empty()) << "Permutation should fail after ordered poly corruption";
518}
519
528TEST_F(TranslatorRelationFailureTests, InRangeValueInMaskingFlowsToOrderedTail)
529{
530 const size_t full_circuit_size = Flavor::MINI_CIRCUIT_SIZE * Flavor::CONCATENATION_GROUP_SIZE;
532
535 ProverPolynomials& pp = key.proving_key->polynomials;
536
537 // Fill wire polynomials with random 14-bit values (circuit) and random FF (masking)
538 for (const auto& group : pp.get_groups_to_be_concatenated()) {
539 for (auto& poly : group) {
540 if (poly.is_empty()) {
541 continue;
542 }
543 for (size_t i = poly.start_index(); i < poly.end_index() - NUM_DISABLED_ROWS_IN_SUMCHECK; i++) {
544 poly.at(i) = engine.get_random_uint16() & ((1 << Flavor::MICRO_LIMB_BITS) - 1);
545 }
546 for (size_t i = poly.end_index() - NUM_DISABLED_ROWS_IN_SUMCHECK; i < poly.end_index(); i++) {
547 poly.at(i) = FF::random_element();
548 }
549 }
550 }
551
552 // Place a known small in-range value at the first masking position of group[0][0]
553 // (p_x_low_limbs_range_constraint_0, block 0)
554 const FF sentinel(42);
555 auto groups = pp.get_groups_to_be_concatenated();
556 auto& target_wire = groups[0][0];
557 const size_t wire_masking_start = target_wire.end_index() - NUM_DISABLED_ROWS_IN_SUMCHECK;
558 target_wire.at(wire_masking_start) = sentinel;
559
560 // Reallocate lagrange polynomials
561 pp.lagrange_first = typename Flavor::Polynomial(full_circuit_size);
562 pp.lagrange_last = typename Flavor::Polynomial(full_circuit_size);
563 pp.lagrange_real_last = typename Flavor::Polynomial(full_circuit_size);
564 pp.lagrange_masking = typename Flavor::Polynomial(full_circuit_size);
565
566 key.compute_lagrange_polynomials();
567 key.compute_extra_range_constraint_numerator();
568 key.compute_concatenated_polynomials();
569
570 // After concatenation: group[0][0] maps to block 0 of concatenated_range_constraints_0
571 // Masking position in concatenated poly = 0 * MINI + wire_masking_start
572 const size_t concat_masking_pos = wire_masking_start; // block 0, so offset is 0
573 EXPECT_EQ(pp.concatenated_range_constraints_0[concat_masking_pos], sentinel)
574 << "Sentinel should appear at the correct concatenated position";
575
576 key.compute_translator_range_constraint_ordered_polynomials();
577
578 // The sentinel should now be in the contiguous masking tail of one of the ordered polys.
579 // split_concatenated_random_coefficients_to_ordered extracts from concat[0..3] in order:
580 // concat 0, block 0, rows [MINI-4, MINI) → first 4 values in random_values[]
581 // Our sentinel is random_values[0] (first extracted value from concat 0, block 0, first masking row).
582 //
583 // Distribution: 256 total values across 5 ordered polys.
584 // ordered[0] gets 52 values (256/5=51, remainder 1 → first poly gets +1).
585 // random_values[0] → ordered[0] at position circuit_size - 52.
586 bool found = false;
587 for (const auto& ord_poly : pp.get_ordered_range_constraints()) {
588 for (size_t pos = full_circuit_size - Flavor::MAX_RANDOM_VALUES_PER_ORDERED; pos < full_circuit_size; pos++) {
589 if (ord_poly[pos] == sentinel) {
590 found = true;
591 break;
592 }
593 }
594 if (found) {
595 break;
596 }
597 }
598 EXPECT_TRUE(found) << "Sentinel value 42 should appear in the ordered poly masking tail";
599
600 // Verify all relations still pass with an in-range value in the masking position
602 compute_grand_product<Flavor, TranslatorPermutationRelation<FF>>(pp, params);
603
604 auto perm_failures =
605 RelationChecker<Flavor>::check<TranslatorPermutationRelation<FF>>(pp, params, "TranslatorPermutationRelation");
606 EXPECT_TRUE(perm_failures.empty()) << "Permutation should pass with in-range masking value";
607
609 pp, params, "TranslatorDeltaRangeConstraintRelation");
610 EXPECT_TRUE(delta_failures.empty()) << "Delta range should pass with in-range masking value";
611}
612
617TEST_F(TranslatorRelationFailureTests, PermutationFailsOnExtraNumeratorCorruption)
618{
619 auto [key, params] = build_valid_translator_state();
620 auto& pp = key.proving_key->polynomials;
621
622 // Baseline: permutation relation passes
623 auto baseline =
624 RelationChecker<Flavor>::check<TranslatorPermutationRelation<FF>>(pp, params, "TranslatorPermutationRelation");
625 EXPECT_TRUE(baseline.empty()) << "Baseline permutation should pass";
626
627 // Corrupt a value in the extra range constraint numerator without recomputing z_perm
628 const size_t corrupt_pos = 5;
629 pp.ordered_extra_range_constraints_numerator.at(corrupt_pos) = FF::random_element();
630
631 auto failures =
632 RelationChecker<Flavor>::check<TranslatorPermutationRelation<FF>>(pp, params, "TranslatorPermutationRelation");
633 EXPECT_FALSE(failures.empty()) << "Permutation should fail after extra numerator corruption";
634}
635
636// ======================== Accumulator Transfer Relation ========================
637
642TEST_F(TranslatorRelationFailureTests, AccumulatorTransferFailsOnOddRowCorruption)
643{
644 auto [key, params] = build_valid_accumulator_transfer_state();
645 auto& pp = key.proving_key->polynomials;
646
647 // Baseline: accumulator transfer passes with real Horner-scheme accumulator values
649 pp, params, "TranslatorAccumulatorTransferRelation");
650 EXPECT_TRUE(baseline.empty()) << "Baseline accumulator transfer should pass";
651
652 // Corrupt accumulators_binary_limbs_0 at an interior odd row.
653 // Transfer checks acc[101] == acc[102]. Corrupting acc[101] breaks this.
654 pp.accumulators_binary_limbs_0.at(101) = FF::random_element();
655
657 pp, params, "TranslatorAccumulatorTransferRelation");
658 EXPECT_FALSE(failures.empty()) << "Accumulator transfer should fail after odd row corruption";
659}
660
665TEST_F(TranslatorRelationFailureTests, AccumulatorTransferFailsOnZeroInitCorruption)
666{
667 auto [key, params] = build_valid_accumulator_transfer_state();
668 auto& pp = key.proving_key->polynomials;
669
671 pp, params, "TranslatorAccumulatorTransferRelation");
672 EXPECT_TRUE(baseline.empty()) << "Baseline accumulator transfer should pass";
673
674 // Corrupt: set non-zero accumulator at the last minicircuit row (zero-init position 8187)
675 const size_t last_in_minicircuit = Flavor::MINI_CIRCUIT_SIZE - Flavor::NUM_MASKED_ROWS_END - 1;
676 pp.accumulators_binary_limbs_0.at(last_in_minicircuit) = FF(1);
677
679 pp, params, "TranslatorAccumulatorTransferRelation");
680 EXPECT_FALSE(failures.empty()) << "Accumulator transfer should fail when zero-init position is non-zero";
681}
682
687TEST_F(TranslatorRelationFailureTests, AccumulatorTransferFailsOnResultMismatch)
688{
689 auto [key, params] = build_valid_accumulator_transfer_state();
690 auto& pp = key.proving_key->polynomials;
691
693 pp, params, "TranslatorAccumulatorTransferRelation");
694 EXPECT_TRUE(baseline.empty()) << "Baseline accumulator transfer should pass";
695
696 // Perturb accumulated_result so it no longer matches the witness at RESULT_ROW
697 params.accumulated_result[0] += FF(1);
698
700 pp, params, "TranslatorAccumulatorTransferRelation");
701 EXPECT_FALSE(failures.empty()) << "Accumulator transfer should fail on result mismatch";
702}
703
714TEST_F(TranslatorRelationFailureTests, AccumulatorTransferPassesWithMaskingRegionValues)
715{
716 auto [key, params] = build_valid_accumulator_transfer_state();
717 auto& pp = key.proving_key->polynomials;
718
719 // Place non-zero values at start masking positions [RANDOMNESS_START, RESULT_ROW) = [2, 8)
720 for (size_t i = Flavor::RANDOMNESS_START; i < Flavor::RESULT_ROW; i++) {
721 pp.accumulators_binary_limbs_0.at(i) = FF::random_element();
722 pp.accumulators_binary_limbs_1.at(i) = FF::random_element();
723 pp.accumulators_binary_limbs_2.at(i) = FF::random_element();
724 pp.accumulators_binary_limbs_3.at(i) = FF::random_element();
725 }
726
727 // Place non-zero values at end masking positions [MINI - NUM_MASKED, MINI) = [8188, 8192)
728 const size_t end_mask_start = Flavor::MINI_CIRCUIT_SIZE - Flavor::NUM_MASKED_ROWS_END;
729 for (size_t i = end_mask_start; i < Flavor::MINI_CIRCUIT_SIZE; i++) {
730 pp.accumulators_binary_limbs_0.at(i) = FF::random_element();
731 pp.accumulators_binary_limbs_1.at(i) = FF::random_element();
732 pp.accumulators_binary_limbs_2.at(i) = FF::random_element();
733 pp.accumulators_binary_limbs_3.at(i) = FF::random_element();
734 }
735
736 // Relation should still pass — masking regions are excluded by selectors
738 pp, params, "TranslatorAccumulatorTransferRelation");
739 EXPECT_TRUE(failures.empty()) << "Accumulator transfer should pass even with arbitrary masking region values";
740}
741
746TEST_F(TranslatorRelationFailureTests, AccumulatorTransferFailsAtFirstTransferRow)
747{
748 auto [key, params] = build_valid_accumulator_transfer_state();
749 auto& pp = key.proving_key->polynomials;
750
752 pp, params, "TranslatorAccumulatorTransferRelation");
753 EXPECT_TRUE(baseline.empty()) << "Baseline accumulator transfer should pass";
754
755 // Row 9 is the first odd row where lagrange_odd_in_minicircuit = 1.
756 // Transfer checks acc[9] == acc[10]. Corrupting acc[9] breaks this.
757 const size_t first_transfer_row = Flavor::RESULT_ROW + 1;
758 pp.accumulators_binary_limbs_0.at(first_transfer_row) = FF::random_element();
759
761 pp, params, "TranslatorAccumulatorTransferRelation");
762 EXPECT_FALSE(failures.empty()) << "Accumulator transfer should fail at first transfer row";
763}
764
770TEST_F(TranslatorRelationFailureTests, AccumulatorTransferFailsAtLastTransferRow)
771{
772 auto [key, params] = build_valid_accumulator_transfer_state();
773 auto& pp = key.proving_key->polynomials;
774
776 pp, params, "TranslatorAccumulatorTransferRelation");
777 EXPECT_TRUE(baseline.empty()) << "Baseline accumulator transfer should pass";
778
779 // Row MINI - NUM_MASKED - 3 = 8185 is the last odd row before 8187 where transfer is enforced
780 const size_t last_transfer_row = Flavor::MINI_CIRCUIT_SIZE - Flavor::NUM_MASKED_ROWS_END - 3;
781 pp.accumulators_binary_limbs_0.at(last_transfer_row) = FF::random_element();
782
784 pp, params, "TranslatorAccumulatorTransferRelation");
785 EXPECT_FALSE(failures.empty()) << "Accumulator transfer should fail at last transfer row";
786}
787
793TEST_F(TranslatorRelationFailureTests, PermutationFailsOnConcatenatedBlockBoundaryCorruption)
794{
795 auto [key, params] = build_valid_translator_state();
796 auto& pp = key.proving_key->polynomials;
797
798 // Baseline: permutation relation passes
799 auto baseline =
800 RelationChecker<Flavor>::check<TranslatorPermutationRelation<FF>>(pp, params, "TranslatorPermutationRelation");
801 EXPECT_TRUE(baseline.empty()) << "Baseline permutation should pass";
802
803 // Position 0 of block 5 in concatenated_range_constraints_0.
804 // This is at index 5 * MINI_CIRCUIT_SIZE, which should be zero (below start_index of that block's wire).
805 const size_t block_boundary_pos = 5 * Flavor::MINI_CIRCUIT_SIZE;
806 EXPECT_EQ(pp.concatenated_range_constraints_0[block_boundary_pos], FF(0))
807 << "Block boundary should initially be zero";
808
809 pp.concatenated_range_constraints_0.at(block_boundary_pos) = FF(999);
810
811 // Re-compute grand product with corrupted data
812 compute_grand_product<Flavor, TranslatorPermutationRelation<FF>>(pp, params);
813
814 auto failures =
815 RelationChecker<Flavor>::check<TranslatorPermutationRelation<FF>>(pp, params, "TranslatorPermutationRelation");
816 EXPECT_FALSE(failures.empty()) << "Permutation should fail after block boundary corruption";
817}
818
819// ======================== Non-Native Field Relation: Accumulator Alias ========================
820
832TEST_F(TranslatorRelationFailureTests, NonNativeFieldRejectsAccumulatorAlias)
833{
834 using BF = typename Flavor::BF;
835 static constexpr size_t NUM_LIMB_BITS = Flavor::CircuitBuilder::NUM_LIMB_BITS;
836
837 auto [key, params] = build_valid_accumulator_transfer_state();
838 auto& pp = key.proving_key->polynomials;
839
840 // Baseline: all three NonNativeField subrelations pass
842 pp, params, "TranslatorNonNativeFieldRelation");
843 EXPECT_TRUE(baseline.empty()) << "Baseline non-native field should pass";
844
845 constexpr size_t ROW = Flavor::RESULT_ROW; // 8
846
847 // --- Read old accumulator and quotient at RESULT_ROW ---
848 // Accumulator limbs are at the even row (current acc = row ROW, previous acc = row ROW+1 via shift)
849 auto read_limbs = [](const auto& l0, const auto& l1, const auto& l2, const auto& l3, size_t row) {
850 return uint256_t(l0[row]) | (uint256_t(l1[row]) << NUM_LIMB_BITS) |
851 (uint256_t(l2[row]) << (2 * NUM_LIMB_BITS)) | (uint256_t(l3[row]) << (3 * NUM_LIMB_BITS));
852 };
853
854 uint256_t old_acc = read_limbs(pp.accumulators_binary_limbs_0,
855 pp.accumulators_binary_limbs_1,
856 pp.accumulators_binary_limbs_2,
857 pp.accumulators_binary_limbs_3,
858 ROW);
859
860 // Quotient: limbs 0,1 from quotient_low at rows ROW, ROW+1; limbs 2,3 from quotient_high at rows ROW, ROW+1
861 uint256_t old_quot = uint256_t(pp.quotient_low_binary_limbs[ROW]) |
862 (uint256_t(pp.quotient_low_binary_limbs[ROW + 1]) << NUM_LIMB_BITS) |
863 (uint256_t(pp.quotient_high_binary_limbs[ROW]) << (2 * NUM_LIMB_BITS)) |
864 (uint256_t(pp.quotient_high_binary_limbs[ROW + 1]) << (3 * NUM_LIMB_BITS));
865
866 // --- Apply the alias mutation: acc += p, quotient -= 1 ---
867 const uint256_t p_mod = BF::modulus;
868 uint256_t new_acc = old_acc + p_mod;
869 uint256_t new_quot = old_quot - uint256_t(1);
870
871 auto split = [](const uint256_t& val) -> std::array<FF, 4> {
872 return { FF(val.slice(0, NUM_LIMB_BITS)),
873 FF(val.slice(NUM_LIMB_BITS, 2 * NUM_LIMB_BITS)),
874 FF(val.slice(2 * NUM_LIMB_BITS, 3 * NUM_LIMB_BITS)),
875 FF(val.slice(3 * NUM_LIMB_BITS, 4 * NUM_LIMB_BITS)) };
876 };
877
878 auto new_acc_limbs = split(new_acc);
879 auto new_quot_limbs = split(new_quot);
880
881 // Write mutated accumulator limbs
882 pp.accumulators_binary_limbs_0.at(ROW) = new_acc_limbs[0];
883 pp.accumulators_binary_limbs_1.at(ROW) = new_acc_limbs[1];
884 pp.accumulators_binary_limbs_2.at(ROW) = new_acc_limbs[2];
885 pp.accumulators_binary_limbs_3.at(ROW) = new_acc_limbs[3];
886
887 // Write mutated quotient limbs
888 pp.quotient_low_binary_limbs.at(ROW) = new_quot_limbs[0];
889 pp.quotient_low_binary_limbs.at(ROW + 1) = new_quot_limbs[1];
890 pp.quotient_high_binary_limbs.at(ROW) = new_quot_limbs[2];
891 pp.quotient_high_binary_limbs.at(ROW + 1) = new_quot_limbs[3];
892
893 // Deliberately do NOT update relation_wide_limbs (carry witnesses) — the stale carries
894 // should cause the higher mod-2^136 check to fail.
895
897 pp, params, "TranslatorNonNativeFieldRelation");
898
899 // The higher carry check (subrelation 1) must fail at RESULT_ROW.
900 EXPECT_TRUE(failures.contains(1)) << "Subrelation 1 (higher carry check) should reject the alias";
901 EXPECT_EQ(failures.at(1), static_cast<uint32_t>(ROW)) << "Failure should be at RESULT_ROW";
902
903 // The native-field check (subrelation 2) should still pass — the mod-r projection is preserved.
904 EXPECT_FALSE(failures.contains(2)) << "Subrelation 2 (native check) should pass under the alias mutation";
905}
906
907// ======================== Opcode Constraint Relation: op on odd rows ========================
908
919TEST_F(TranslatorRelationFailureTests, OpcodeConstraintFailsOnGenuineOpcodeAtOddRow)
920{
921 auto [key, params] = build_valid_accumulator_transfer_state();
922 auto& pp = key.proving_key->polynomials;
923
925 pp, params, "TranslatorOpcodeConstraintRelation");
926 EXPECT_TRUE(baseline.empty()) << "Baseline opcode constraint should pass";
927
928 // Row 9 is the first odd row in the genuine-op processing range (lagrange_odd_in_minicircuit = 1, op = 0).
929 const size_t odd_row = Flavor::RESULT_ROW + 1;
930 ASSERT_EQ(pp.op[odd_row], FF(0));
931
932 // Place a genuine opcode (3 = eq + reset) on the odd row.
933 pp.op.at(odd_row) = FF(3);
934
936 pp, params, "TranslatorOpcodeConstraintRelation");
937 EXPECT_FALSE(failures.empty()) << "Opcode constraint should fail with a genuine opcode on an odd row";
938 EXPECT_TRUE(failures.contains(0)) << "Subrelation 0 (opcode validity) should catch the odd-row opcode";
939 EXPECT_EQ(failures.at(0), static_cast<uint32_t>(odd_row)) << "Failure should be at the odd row";
940}
A container for the prover polynomials.
typename Curve::ScalarField FF
ECCVMCircuitBuilder CircuitBuilder
typename Curve::BaseField BF
bb::Polynomial< FF > Polynomial
typename G1::element GroupElement
A debugging utility for checking whether a set of polynomials satisfies the relations for a given Fla...
A wrapper for Relations to expose methods used by the Sumcheck prover or verifier to add the contribu...
group class. Represents an elliptic curve group element. Group is parametrised by Fq and Fr
Definition group.hpp:38
virtual uint16_t get_random_uint16()=0
typename ECCVMFlavor::ProverPolynomials ProverPolynomials
numeric::RNG & engine
RNG & get_debug_randomness(bool reset, std::uint_fast64_t seed)
Definition engine.cpp:245
std::filesystem::path bb_crs_path()
void init_file_crs_factory(const std::filesystem::path &path)
Entry point for Barretenberg command-line interface.
Definition api.hpp:5
TEST_F(IPATest, ChallengesAreZero)
Definition ipa.test.cpp:160
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
Container for parameters used by the grand product (permutation, lookup) Honk relations.
std::array< std::array< T, NUM_BINARY_LIMBS_IN_GOBLIN_TRANSLATOR+NUM_NATIVE_LIMBS_IN_GOBLIN_TRANSLATOR >, NUM_CHALLENGE_POWERS_IN_GOBLIN_TRANSLATOR > batching_challenge_v
std::array< T, NUM_BINARY_LIMBS_IN_GOBLIN_TRANSLATOR > accumulated_result
std::array< T, NUM_BINARY_LIMBS_IN_GOBLIN_TRANSLATOR+NUM_NATIVE_LIMBS_IN_GOBLIN_TRANSLATOR > evaluation_input_x
static field random_element(numeric::RNG *engine=nullptr) noexcept