Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
eccvm_relation_corruption.test.cpp
Go to the documentation of this file.
1
13#include <gtest/gtest.h>
14
15using namespace bb;
16
17namespace {
18
19using Flavor = ECCVMFlavor;
20using FF = typename Flavor::FF;
21using G1 = bb::g1;
22using Fr = typename G1::Fr;
23using Polynomial = typename Flavor::Polynomial;
26
28
29void expand_shiftable_to_virtual_size(Polynomial& polynomial)
30{
31 polynomial = Polynomial(polynomial, polynomial.virtual_size() - polynomial.start_index());
32}
33
39std::vector<Polynomial*> get_msm_polynomials(ProverPolynomials& polys)
40{
41 return {
42 // From WireNonShiftedEntities (columns 21-44)
43 &polys.msm_size_of_msm,
44 &polys.msm_add2,
45 &polys.msm_add3,
46 &polys.msm_add4,
47 &polys.msm_x1,
48 &polys.msm_y1,
49 &polys.msm_x2,
50 &polys.msm_y2,
51 &polys.msm_x3,
52 &polys.msm_y3,
53 &polys.msm_x4,
54 &polys.msm_y4,
55 &polys.msm_collision_x1,
56 &polys.msm_collision_x2,
57 &polys.msm_collision_x3,
58 &polys.msm_collision_x4,
59 &polys.msm_lambda1,
60 &polys.msm_lambda2,
61 &polys.msm_lambda3,
62 &polys.msm_lambda4,
63 &polys.msm_slice1,
64 &polys.msm_slice2,
65 &polys.msm_slice3,
66 &polys.msm_slice4,
67 // From WireToBeShiftedWithoutAccumulatorsEntities (columns 68-77)
68 &polys.msm_transition,
69 &polys.msm_add,
70 &polys.msm_double,
71 &polys.msm_skew,
72 &polys.msm_accumulator_x,
73 &polys.msm_accumulator_y,
74 &polys.msm_count,
75 &polys.msm_round,
76 &polys.msm_add1,
77 &polys.msm_pc,
78 };
79}
80
84ProverPolynomials build_valid_eccvm_msm_state()
85{
86 auto generators = G1::derive_generators("test generators", 3);
87 auto a = generators[0];
88 auto b = generators[1];
91
92 auto op_queue = std::make_shared<ECCOpQueue>();
93 op_queue->mul_accumulate(a, x);
94 op_queue->mul_accumulate(b, y);
95 op_queue->eq_and_reset();
96 op_queue->merge();
97 add_hiding_op_for_test(op_queue);
98
99 ECCVMCircuitBuilder builder{ op_queue };
101}
102
107RelationParameters<FF> compute_full_relation_params(ProverPolynomials& polynomials)
108{
109 const FF beta = FF::random_element(&engine);
110 const FF gamma = FF::random_element(&engine);
111 const FF beta_sqr = beta.sqr();
112 const FF beta_cube = beta_sqr * beta;
113 auto eccvm_set_permutation_delta =
114 gamma * (gamma + beta_sqr) * (gamma + beta_sqr + beta_sqr) * (gamma + beta_sqr + beta_sqr + beta_sqr);
115 eccvm_set_permutation_delta = eccvm_set_permutation_delta.invert();
116
118 .eta = 0,
119 .beta = beta,
120 .gamma = gamma,
121 .public_input_delta = 0,
122 .beta_sqr = beta_sqr,
123 .beta_cube = beta_cube,
124 .eccvm_set_permutation_delta = eccvm_set_permutation_delta,
125 };
126
127 compute_logderivative_inverse<FF, ECCVMLookupRelation<FF>>(polynomials, params, Flavor::TRACE_OFFSET);
128 compute_grand_product<Flavor, ECCVMSetRelation<FF>>(polynomials, params);
129 polynomials.z_perm_shift = Polynomial(polynomials.z_perm.shifted());
130
131 return params;
132}
133
137size_t find_transcript_noop_row(const ProverPolynomials& polynomials)
138{
139 const size_t num_rows = polynomials.get_polynomial_size();
140 for (size_t i = Flavor::TRACE_OFFSET; i < num_rows - 1; i++) {
141 if (polynomials.transcript_add.get(i) == FF(0) && polynomials.transcript_mul.get(i) == FF(0) &&
142 polynomials.transcript_eq.get(i) == FF(0) && polynomials.transcript_reset_accumulator.get(i) == FF(0) &&
143 polynomials.lagrange_first.get(i) == FF(0) && polynomials.lagrange_last.get(i) == FF(0)) {
144 return i;
145 }
146 }
147 return 0;
148}
149
150} // anonymous namespace
151
152class ECCVMRelationCorruptionTests : public ::testing::Test {
153 protected:
155};
156
166TEST_F(ECCVMRelationCorruptionTests, MSMAccumulatorCorruptionAtTransitionRowIsHarmless)
167{
168 auto polynomials = build_valid_eccvm_msm_state();
169 RelationParameters<FF> params{};
170
172 polynomials, params, "ECCVMMSMRelation", Flavor::TRACE_OFFSET);
173 EXPECT_TRUE(baseline.empty()) << "Baseline MSM relation should pass";
174
175 // Confirm the first active MSM row is the transition row (offset by disabled head region)
176 constexpr size_t first_msm_row = Flavor::TRACE_OFFSET + 1;
177 ASSERT_EQ(polynomials.msm_add.get(first_msm_row), FF(1)) << "First MSM row should be an active MSM add row";
178 ASSERT_EQ(polynomials.msm_transition.get(first_msm_row), FF(1)) << "First MSM row should have msm_transition=1";
179
180 // Corrupt the accumulator at the transition row
181 polynomials.msm_accumulator_x.at(first_msm_row) = FF::random_element(&engine);
182 polynomials.msm_accumulator_y.at(first_msm_row) = FF::random_element(&engine);
183 polynomials.set_shifted();
184
186 polynomials, params, "ECCVMMSMRelation", Flavor::TRACE_OFFSET);
187 EXPECT_TRUE(failures.empty()) << "MSM relation should STILL PASS — acc is unused when msm_transition=1";
188}
189
200TEST_F(ECCVMRelationCorruptionTests, MSMAccumulatorCorruptionAtInteriorAndNoOpRows)
201{
202 RelationParameters<FF> params{};
203
204 // --- Part 1: corrupt the accumulator at an interior active MSM row (q_add=1, msm_transition=0) ---
205 {
206 auto polynomials = build_valid_eccvm_msm_state();
207
209 polynomials, params, "ECCVMMSMRelation", Flavor::TRACE_OFFSET);
210 EXPECT_TRUE(baseline.empty()) << "Baseline MSM relation should pass";
211
212 // Find an interior addition row: q_add=1, msm_transition=0
213 const size_t num_rows = polynomials.get_polynomial_size();
214 size_t active_row = 0;
215 for (size_t i = Flavor::TRACE_OFFSET; i < num_rows - 1; i++) {
216 if (polynomials.msm_add.get(i) == FF(1) && polynomials.msm_transition.get(i) == FF(0)) {
217 active_row = i;
218 break;
219 }
220 }
221 ASSERT_NE(active_row, 0) << "Should find an interior active MSM add row";
222
223 polynomials.msm_accumulator_x.at(active_row) = FF::random_element(&engine);
224 polynomials.msm_accumulator_y.at(active_row) = FF::random_element(&engine);
225 polynomials.set_shifted();
226
228 polynomials, params, "ECCVMMSMRelation", Flavor::TRACE_OFFSET);
229 EXPECT_FALSE(failures.empty()) << "MSM relation should fail after active-row accumulator corruption";
230 }
231
232 // --- Part 2: corrupt the accumulator at a trailing no-op row ---
233 {
234 auto polynomials = build_valid_eccvm_msm_state();
235
236 // Find the first no-op row (all MSM selectors zero, not lagrange_first)
237 const size_t num_rows = polynomials.get_polynomial_size();
238 size_t no_op_row = 0;
239 for (size_t i = Flavor::TRACE_OFFSET; i < num_rows - 1; i++) {
240 if (polynomials.msm_add.get(i) == FF(0) && polynomials.msm_double.get(i) == FF(0) &&
241 polynomials.msm_skew.get(i) == FF(0) && polynomials.msm_transition.get(i) == FF(0) &&
242 polynomials.lagrange_first.get(i) == FF(0)) {
243 no_op_row = i;
244 break;
245 }
246 }
247 ASSERT_NE(no_op_row, 0) << "Should find a no-op row in the MSM table";
248
249 expand_shiftable_to_virtual_size(polynomials.msm_accumulator_x);
250 expand_shiftable_to_virtual_size(polynomials.msm_accumulator_y);
251 polynomials.msm_accumulator_x.at(no_op_row) = FF::random_element(&engine);
252 polynomials.msm_accumulator_y.at(no_op_row) = FF::random_element(&engine);
253 polynomials.set_shifted();
254
256 polynomials, params, "ECCVMMSMRelation", Flavor::TRACE_OFFSET);
257 EXPECT_FALSE(failures.empty()) << "MSM relation should fail after no-op accumulator corruption";
258
259 // The failure should be in subrelations 45 or 46 (the no-op accumulator preservation constraints)
260 bool found_noop_subrelation_failure = failures.contains(45) || failures.contains(46);
261 EXPECT_TRUE(found_noop_subrelation_failure)
262 << "Failure should be detected by subrelations 45/46 (no-op accumulator preservation)";
263 }
264}
265
281TEST_F(ECCVMRelationCorruptionTests, MSMRelationFailsOnShiftedMSMTable)
282{
283 auto polynomials = build_valid_eccvm_msm_state();
284 RelationParameters<FF> params{};
285
286 // Baseline: MSM relation passes on clean data
288 polynomials, params, "ECCVMMSMRelation", Flavor::TRACE_OFFSET);
289 EXPECT_TRUE(baseline.empty()) << "Baseline MSM relation should pass";
290
291 auto msm_polys = get_msm_polynomials(polynomials);
292
293 // Shift every MSM column down by 1 within the active region
294 constexpr size_t ofs = Flavor::TRACE_OFFSET;
295 for (auto* poly : msm_polys) {
296 for (size_t k = poly->end_index() - 1; k >= ofs + 2; k--) {
297 poly->at(k) = (*poly)[k - 1];
298 }
299 poly->at(ofs + 1) = FF(0);
300 }
301
302 // Patch msm_size_of_msm at the injected row so the pc-continuity constraint is satisfied
303 polynomials.msm_size_of_msm.at(ofs + 1) = polynomials.msm_pc.get(ofs + 1) - polynomials.msm_pc.get(ofs + 2);
304
305 // Refresh shifted views
306 polynomials.set_shifted();
307
309 polynomials, params, "ECCVMMSMRelation", Flavor::TRACE_OFFSET);
310 EXPECT_FALSE(failures.empty()) << "MSM relation should fail after shifting MSM table by one row";
311
312 // Log all failing subrelations for visibility
313 for (const auto& [subrelation_idx, row_idx] : failures) {
314 info("Shifted MSM table: subrelation ", subrelation_idx, " first failed at row ", row_idx);
315 }
316
317 EXPECT_TRUE(failures.contains(45)) << "Subrelation 45 (no-op acc_x preservation) should fail";
318 EXPECT_TRUE(failures.contains(46)) << "Subrelation 46 (no-op acc_y preservation) should fail";
319
320 // Verify that all other ECCVM relations still pass after the shift.
321 // We compute random Fiat-Shamir challenges and derived polynomials (logderivative inverse, grand product)
322 // so we can also check ECCVMSetRelation and ECCVMLookupRelation.
323 auto full_params = compute_full_relation_params(polynomials);
324
325 // Relations that don't touch MSM columns should be completely unaffected.
327 polynomials, full_params, "ECCVMTranscriptRelation", Flavor::TRACE_OFFSET);
328 EXPECT_TRUE(transcript_failures.empty()) << "ECCVMTranscriptRelation should still pass";
329
331 polynomials, full_params, "ECCVMPointTableRelation", Flavor::TRACE_OFFSET);
332 EXPECT_TRUE(point_table_failures.empty()) << "ECCVMPointTableRelation should still pass";
333
335 polynomials, full_params, "ECCVMWnafRelation", Flavor::TRACE_OFFSET);
336 EXPECT_TRUE(wnaf_failures.empty()) << "ECCVMWnafRelation should still pass";
337
339 polynomials, full_params, "ECCVMBoolsRelation", Flavor::TRACE_OFFSET);
340 EXPECT_TRUE(bools_failures.empty()) << "ECCVMBoolsRelation should still pass";
341
342 // The Set relation enforces a multiset equality between MSM output tuples (pc, acc_x, acc_y, msm_size)
343 // and the transcript. Shifting the MSM columns corrupts these tuples, so the grand product (computed
344 // post-shift) reflects mismatched reads/writes and the relation correctly fails. It is possible that with more
345 // care, we could make this also pass.
347 polynomials, full_params, "ECCVMSetRelation", Flavor::TRACE_OFFSET);
348 EXPECT_FALSE(set_failures.empty()) << "ECCVMSetRelation should also fail (MSM output tuples are shifted)";
349
350 // The Lookup relation's logderivative inverse is computed post-shift, so it adapts to the
351 // shifted column values. The per-row subrelation passes, and the sum-over-trace (linearly
352 // dependent) subrelation also vanishes since the inverse was derived from the current data.
353 auto lookup_failures = RelationChecker<void>::check<ECCVMLookupRelation<FF>, /*has_linearly_dependent=*/true>(
354 polynomials, full_params, "ECCVMLookupRelation", Flavor::TRACE_OFFSET);
355 EXPECT_TRUE(lookup_failures.empty()) << "ECCVMLookupRelation should still pass (inverse computed post-shift)";
356}
357
366TEST_F(ECCVMRelationCorruptionTests, TranscriptNoOpRowRejectsAccumulatorNotEmpty)
367{
368 auto polynomials = build_valid_eccvm_msm_state();
369 RelationParameters<FF> params{};
370
372 polynomials, params, "ECCVMTranscriptRelation", Flavor::TRACE_OFFSET);
373 EXPECT_TRUE(baseline.empty()) << "Baseline transcript relation should pass";
374
375 size_t noop_row = find_transcript_noop_row(polynomials);
376 ASSERT_NE(noop_row, 0) << "Should find a transcript no-op row";
377
378 // The no-op constraint at row `noop_row` constrains is_accumulator_empty_shift,
379 // which reads from accumulator_not_empty at row `noop_row + 1`.
380 expand_shiftable_to_virtual_size(polynomials.transcript_accumulator_not_empty);
381 polynomials.transcript_accumulator_not_empty.at(noop_row + 1) = FF(1);
382 polynomials.set_shifted();
383
385 polynomials, params, "ECCVMTranscriptRelation", Flavor::TRACE_OFFSET);
386 EXPECT_FALSE(failures.empty()) << "Transcript relation should fail after corrupting accumulator_not_empty on "
387 "the row following a no-op";
389 << "ACCUMULATOR_EMPTY_UPDATE subrelation should catch the corruption";
390}
391
402TEST_F(ECCVMRelationCorruptionTests, SetRelationFailsOnZPermNonZeroAtFirstRow)
403{
404 auto polynomials = build_valid_eccvm_msm_state();
405 auto params = compute_full_relation_params(polynomials);
406
407 // Baseline: set relation passes (skip disabled head rows where masking values break relations)
409 polynomials, params, "ECCVMSetRelation", Flavor::TRACE_OFFSET);
410 EXPECT_TRUE(baseline.empty()) << "Baseline set relation should pass";
411
412 // Derive expected lagrange_first position from z_perm shiftable structure
413 ASSERT_TRUE(polynomials.z_perm.is_shiftable());
414 size_t structural_first_row = Flavor::TRACE_OFFSET;
415
416 // Independently scan lagrange_first for its non-zero entry
417 const auto& lagrange_first = polynomials.lagrange_first;
418 size_t scanned_first_row = 0;
419 bool found = false;
420 for (size_t i = lagrange_first.start_index(); i < lagrange_first.end_index(); ++i) {
421 if (lagrange_first[i] != FF(0)) {
422 scanned_first_row = i;
423 found = true;
424 break;
425 }
426 }
427 ASSERT_TRUE(found) << "lagrange_first has no non-zero entry";
428 ASSERT_EQ(structural_first_row, scanned_first_row)
429 << "lagrange_first position doesn't match z_perm shiftable structure";
430
431 const size_t first_row = scanned_first_row;
432
433 // Expand to full polynomials so we can write at index 0
434 polynomials.z_perm = polynomials.z_perm.full();
435 polynomials.z_perm_shift = polynomials.z_perm_shift.full();
436
437 ASSERT_EQ(polynomials.z_perm.get(first_row), FF(0));
438
439 // Tamper: set z_perm to non-zero where lagrange_first is active
440 polynomials.z_perm.at(first_row) = FF(1);
441
443 polynomials, params, "ECCVMSetRelation - After setting z_perm != 0 at lagrange_first", Flavor::TRACE_OFFSET);
444 EXPECT_FALSE(failures.empty()) << "Set relation should fail after z_perm init corruption";
445 EXPECT_TRUE(failures.contains(ECCVMSetRelationImpl<FF>::Z_PERM_INIT))
446 << "Sub-relation Z_PERM_INIT should catch the corruption";
447 EXPECT_EQ(failures.at(ECCVMSetRelationImpl<FF>::Z_PERM_INIT), first_row)
448 << "Failure should be at lagrange_first row";
449}
A container for the prover polynomials.
typename Curve::ScalarField FF
bb::Polynomial< FF > Polynomial
static constexpr size_t TRACE_OFFSET
ECCVMTranscriptRelationImpl evaluates the correctness of the ECCVM transcript columns.
Structured polynomial class that represents the coefficients 'a' of a_0 + a_1 x .....
size_t start_index() const
std::size_t virtual_size() const
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...
#define info(...)
Definition log.hpp:93
AluTraceBuilder builder
Definition alu.test.cpp:124
FF a
FF b
typename ECCVMFlavor::ProverPolynomials ProverPolynomials
numeric::RNG & engine
void add_hiding_op_for_test(const std::shared_ptr< ECCOpQueue > &op_queue)
Set a hiding op on the op_queue for testing.
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
group< fq, fr, Bn254G1Params > g1
Definition g1.hpp:34
TEST_F(IPATest, ChallengesAreZero)
Definition ipa.test.cpp:155
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
Curve::AffineElement G1
Container for parameters used by the grand product (permutation, lookup) Honk relations.
constexpr field invert() const noexcept
static field random_element(numeric::RNG *engine=nullptr) noexcept
BB_INLINE constexpr field sqr() const noexcept