Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
multilinear_batching.test.cpp
Go to the documentation of this file.
1// === AUDIT STATUS ===
2// internal: { status: not started, auditors: [], commit: }
3// external_1: { status: not started, auditors: [], commit: }
4// external_2: { status: not started, auditors: [], commit: }
5// =====================
6
10
19
20#include <vector>
21
22using namespace bb;
23
24namespace {
25
26using Curve = curve::BN254;
28using Commitment = Curve::AffineElement;
29using ProverClaim = MultilinearBatchingProverClaim;
30using VerifierClaim = MultilinearBatchingVerifierClaim<Curve>;
31using NativeTranscriptType = NativeTranscript;
32
33// Base log-size of the slot polynomials used in the tests. To exercise the protocol's support for mixed-size inputs,
34// claim i is built on 2^(LOG_N_BASE + i) points (so three claims use LOG_N 5, 6, 7). The protocol pads every
35// polynomial up to 2^VIRTUAL_LOG_N virtual variables, so the actual sizes only need to be small.
36constexpr size_t LOG_N_BASE = 5;
37constexpr size_t VIRTUAL_LOG_N = MultilinearBatchingFlavor::VIRTUAL_LOG_N;
38// Per round univariate length (the relation is degree 2, so 3 evaluations per round).
39constexpr size_t UNIVARIATE_LENGTH = MultilinearBatchingFlavor::BATCHED_RELATION_PARTIAL_LENGTH;
40// In production the batching transcript is shared and already holds the group's instance sumchecks, so the first
41// batching challenge is never drawn from an empty transcript. The standalone tests reproduce that by sending one seed
42// field element first; it occupies index 0 of every exported proof.
43constexpr size_t SEED_FRS = 1;
44// Offset, in field elements, of the "Sumcheck:evaluations" block within an exported batching proof: it follows the
45// seed element and the VIRTUAL_LOG_N round univariates.
46constexpr size_t EVALS_OFFSET = SEED_FRS + (VIRTUAL_LOG_N * UNIVARIATE_LENGTH);
47
51enum class FaultMode : uint8_t {
52 NONE,
53 // The next six faults corrupt a verifier-held input claim so the sumcheck target / eq-consistency check is wrong.
54 // Each of the three batched components (non-shifted eval, shifted eval, eq polynomial via the claim's challenge) is
55 // targeted on both the first claim (batched with coefficient γ^0 = 1) and the second claim (coefficient γ^1 = γ).
56 FALSE_NONSHIFTED_EVAL_FIRST, // wrong non-shifted evaluation on claim 0
57 FALSE_NONSHIFTED_EVAL_SECOND, // wrong non-shifted evaluation on claim 1
58 FALSE_SHIFTED_EVAL_FIRST, // wrong shifted evaluation on claim 0
59 FALSE_SHIFTED_EVAL_SECOND, // wrong shifted evaluation on claim 1
60 FALSE_EQ_FIRST, // wrong challenge on claim 0 -> eq polynomial mismatch
61 FALSE_EQ_SECOND, // wrong challenge on claim 1 -> eq polynomial mismatch
62 // The next six faults corrupt one prover-sent claimed evaluation in the proof. The non-shifted/shifted variants
63 // break the sumcheck final relation check; the eq variants break eq-consistency. Each of the three components is
64 // tampered on both the first claim (idx 0) and the second claim (idx 1).
65 TAMPER_NONSHIFTED_EVAL_FIRST, // corrupt claimed non-shifted evaluation of claim 0
66 TAMPER_NONSHIFTED_EVAL_SECOND, // corrupt claimed non-shifted evaluation of claim 1
67 TAMPER_SHIFTED_EVAL_FIRST, // corrupt claimed shifted evaluation of claim 0
68 TAMPER_SHIFTED_EVAL_SECOND, // corrupt claimed shifted evaluation of claim 1
69 TAMPER_EQ_EVAL_FIRST, // corrupt claimed eq evaluation of claim 0
70 TAMPER_EQ_EVAL_SECOND, // corrupt claimed eq evaluation of claim 1
71 WRONG_NONSHIFTED_COMMITMENT, // verifier holds a wrong input commitment -> output claim is not bound to it
72 WRONG_SHIFTED_COMMITMENT, // verifier holds a wrong input commitment -> output claim is not bound to it
73 BREAK_EVAL_BINDING, // polynomial evaluations that keep the γ-weighted relation but break the ρ-merge binding
74};
75
81FF mle_padded(const Polynomial<FF>& poly, const std::vector<FF>& r, size_t log_n, bool shift = false)
82{
83 std::vector<FF> head(r.begin(), r.begin() + static_cast<std::ptrdiff_t>(log_n));
84 FF value = poly.evaluate_mle(head, shift);
85 for (size_t j = log_n; j < r.size(); ++j) {
86 value *= (FF(1) - r[j]);
87 }
88 return value;
89}
90
94struct ClaimSet {
95 std::vector<ProverClaim> prover_claims;
96 std::vector<VerifierClaim> verifier_claims;
97 // Copies of the polynomials used to recompute the honest output claim.
98 std::vector<Polynomial<FF>> non_shifted_polynomials;
99 std::vector<Polynomial<FF>> shifted_polynomials; // pre-shift form (start index 1)
100 std::vector<size_t> log_ns; // per-claim log-size, needed to re-pad the MLE evaluations
101};
102
106ClaimSet build_honest_claims(size_t num_claims)
107{
108 // Claim i lives on 2^(LOG_N_BASE + i) points, so the largest claim sizes the commitment key (it can commit any
109 // smaller polynomial using a prefix of the same SRS).
110 const size_t max_dyadic_size = 1UL << (LOG_N_BASE + num_claims - 1);
111 CommitmentKey<Curve> commitment_key(max_dyadic_size);
112
113 ClaimSet set;
114 for (size_t i = 0; i < num_claims; ++i) {
115 const size_t log_n = LOG_N_BASE + i;
116 const size_t dyadic_size = 1UL << log_n;
117
118 // Independent random evaluation point per claim, of full sumcheck length.
119 std::vector<FF> challenge(VIRTUAL_LOG_N);
120 for (auto& c : challenge) {
121 c = FF::random_element();
122 }
123
124 Polynomial<FF> non_shifted = Polynomial<FF>::random(dyadic_size);
125 Polynomial<FF> shifted = Polynomial<FF>::random(dyadic_size - 1, dyadic_size, /*start_index=*/1);
126
127 const FF non_shifted_eval = mle_padded(non_shifted, challenge, log_n);
128 const FF shifted_eval = mle_padded(shifted, challenge, log_n, /*shift=*/true);
129 const Commitment non_shifted_commitment = commitment_key.commit(non_shifted);
130 const Commitment shifted_commitment = commitment_key.commit(shifted);
131
132 set.non_shifted_polynomials.push_back(non_shifted);
133 set.shifted_polynomials.push_back(shifted);
134 set.log_ns.push_back(log_n);
135
136 set.prover_claims.push_back(ProverClaim{ .challenge = challenge,
137 .non_shifted_evaluation = non_shifted_eval,
138 .shifted_evaluation = shifted_eval,
139 .non_shifted_polynomial = std::move(non_shifted),
140 .shifted_polynomial = std::move(shifted),
141 .non_shifted_commitment = non_shifted_commitment,
142 .shifted_commitment = shifted_commitment,
143 .dyadic_size = dyadic_size });
144 set.verifier_claims.push_back(VerifierClaim{ .challenge = challenge,
145 .non_shifted_evaluation = non_shifted_eval,
146 .shifted_evaluation = shifted_eval,
147 .non_shifted_commitment = non_shifted_commitment,
148 .shifted_commitment = shifted_commitment });
149 }
150 return set;
151}
152
159struct DerivedChallenges {
160 FF gamma;
161 std::vector<FF> r;
162 FF rho;
163};
164
165DerivedChallenges replay_challenges(const HonkProof& proof, size_t num_claims)
166{
167 auto transcript = std::make_shared<NativeTranscriptType>();
168 transcript->load_proof(proof);
169 [[maybe_unused]] FF seed = transcript->template receive_from_prover<FF>("init");
170
171 DerivedChallenges out;
172 out.gamma = transcript->template get_challenge<FF>("claim_batching_challenge");
173 [[maybe_unused]] FF alpha = transcript->template get_challenge<FF>("Sumcheck:alpha");
174 for (size_t round = 0; round < VIRTUAL_LOG_N; ++round) {
175 for (size_t e = 0; e < UNIVARIATE_LENGTH; ++e) {
176 [[maybe_unused]] FF _ = transcript->template receive_from_prover<FF>("u");
177 }
178 out.r.push_back(transcript->template get_challenge<FF>("Sumcheck:u_" + std::to_string(round)));
179 }
180 for (size_t e = 0; e < 3 * num_claims; ++e) {
181 [[maybe_unused]] FF _ = transcript->template receive_from_prover<FF>("e");
182 }
183 out.rho = transcript->template get_challenge<FF>("claim_merge_challenge");
184 return out;
185}
186
192bool output_claim_is_bound(const ClaimSet& set, const HonkProof& proof, const VerifierClaim& new_claim)
193{
194 auto powers = [](const FF& base, size_t count) {
195 std::vector<FF> result(count);
196 result[0] = FF(1);
197 for (size_t i = 1; i < count; ++i) {
198 result[i] = result[i - 1] * base;
199 }
200 return result;
201 };
202
203 const size_t num_claims = set.verifier_claims.size();
204 const DerivedChallenges challenges = replay_challenges(proof, num_claims);
205 const std::vector<FF> rho_powers = powers(challenges.rho, num_claims);
206
207 FF expected_non_shifted_eval(0);
208 FF expected_shifted_eval(0);
209 std::vector<Commitment> non_shifted_commitments;
210 std::vector<Commitment> shifted_commitments;
211 for (size_t i = 0; i < num_claims; ++i) {
212 expected_non_shifted_eval +=
213 rho_powers[i] * mle_padded(set.non_shifted_polynomials[i], challenges.r, set.log_ns[i]);
214 expected_shifted_eval +=
215 rho_powers[i] * mle_padded(set.shifted_polynomials[i], challenges.r, set.log_ns[i], /*shift=*/true);
216 non_shifted_commitments.push_back(set.verifier_claims[i].non_shifted_commitment);
217 shifted_commitments.push_back(set.verifier_claims[i].shifted_commitment);
218 }
219 std::vector<FF> scalars = rho_powers;
220 const Commitment expected_non_shifted_commitment = Curve::Element::batch_mul(non_shifted_commitments, scalars);
221 const Commitment expected_shifted_commitment = Curve::Element::batch_mul(shifted_commitments, scalars);
222
223 return new_claim.challenge == challenges.r && new_claim.non_shifted_evaluation == expected_non_shifted_eval &&
224 new_claim.shifted_evaluation == expected_shifted_eval &&
225 new_claim.non_shifted_commitment == expected_non_shifted_commitment &&
226 new_claim.shifted_commitment == expected_shifted_commitment;
227}
228
233struct FaultyProof {
234 ClaimSet set;
235 std::vector<VerifierClaim> verifier_claims; // the verifier-held claims (some faults corrupt these)
236 HonkProof proof;
237};
238
239FaultyProof build_faulty_proof(size_t num_claims, FaultMode fault)
240{
241 ClaimSet set = build_honest_claims(num_claims);
242
243 // The verifier-held input claims; some faults corrupt these without touching the proof.
244 std::vector<VerifierClaim> verifier_claims = set.verifier_claims;
245 if (fault == FaultMode::FALSE_NONSHIFTED_EVAL_FIRST) {
246 verifier_claims[0].non_shifted_evaluation += FF(1);
247 } else if (fault == FaultMode::FALSE_NONSHIFTED_EVAL_SECOND) {
248 verifier_claims[1].non_shifted_evaluation += FF(1);
249 } else if (fault == FaultMode::FALSE_SHIFTED_EVAL_FIRST) {
250 verifier_claims[0].shifted_evaluation += FF(1);
251 } else if (fault == FaultMode::FALSE_SHIFTED_EVAL_SECOND) {
252 verifier_claims[1].shifted_evaluation += FF(1);
253 } else if (fault == FaultMode::FALSE_EQ_FIRST) {
254 verifier_claims[0].challenge[0] += FF(1);
255 } else if (fault == FaultMode::FALSE_EQ_SECOND) {
256 verifier_claims[1].challenge[0] += FF(1);
257 } else if (fault == FaultMode::WRONG_NONSHIFTED_COMMITMENT) {
258 verifier_claims[0].non_shifted_commitment = verifier_claims[0].non_shifted_commitment + Commitment::one();
259 } else if (fault == FaultMode::WRONG_SHIFTED_COMMITMENT) {
260 verifier_claims[0].shifted_commitment = verifier_claims[0].shifted_commitment + Commitment::one();
261 }
262
263 auto prover_transcript = std::make_shared<NativeTranscriptType>();
264 // Seed the transcript so the first batching challenge is not drawn from an empty hash buffer (see SEED_FRS).
265 prover_transcript->send_to_verifier("init", FF::random_element());
266 MultilinearBatchingProver prover(std::move(set.prover_claims), prover_transcript);
267 HonkProof proof = prover.construct_proof();
268
269 // Proof-side faults are applied after the honest proof is built; this maintains consistency of FS because the
270 // prover doesn't send anything to the verifier after the claimed evaluations. The claimed-evaluation block is laid
271 // out as [non_shifted(0..n-1)][shifted(0..n-1)][eq(0..n-1)] (see MultilinearBatchingFlavor::AllEntities).
272 if (fault == FaultMode::TAMPER_NONSHIFTED_EVAL_FIRST) {
273 proof[EVALS_OFFSET + 0] += FF(1);
274 } else if (fault == FaultMode::TAMPER_NONSHIFTED_EVAL_SECOND) {
275 proof[EVALS_OFFSET + 1] += FF(1);
276 } else if (fault == FaultMode::TAMPER_SHIFTED_EVAL_FIRST) {
277 proof[EVALS_OFFSET + num_claims + 0] += FF(1);
278 } else if (fault == FaultMode::TAMPER_SHIFTED_EVAL_SECOND) {
279 proof[EVALS_OFFSET + num_claims + 1] += FF(1);
280 } else if (fault == FaultMode::TAMPER_EQ_EVAL_FIRST) {
281 proof[EVALS_OFFSET + (2 * num_claims) + 0] += FF(1);
282 } else if (fault == FaultMode::TAMPER_EQ_EVAL_SECOND) {
283 proof[EVALS_OFFSET + (2 * num_claims) + 1] += FF(1);
284 } else if (fault == FaultMode::BREAK_EVAL_BINDING) {
285 // Perturb the first three non-shifted claimed evals by a δ that lies in the joint kernel of BOTH the sumcheck
286 // final relation form (a·δ = 0, with a_i = γ^i·eq_i(r)) AND the γ-weighted merge (b·δ = 0, with b_i = γ^i),
287 // Then:
288 // - the sumcheck and eq-consistency checks still pass (a·δ = 0), so `verified` stays true;
289 // - a hypothetical γ-weighted output merge would still be correct (b·δ = 0)
290 // - the fresh merge challenge ρ is drawn only after δ is committed, so Σ ρ^i δ_i ≠ 0 and the bound
291 // output claim is wrong. δ = a × b can be taken to be the cross product of the two vector a, b.
292 const DerivedChallenges challenges = replay_challenges(proof, num_claims);
293 std::array<FF, 3> eq;
294 std::array<FF, 3> b; // b_i = γ^i
295 b[0] = FF(1);
296 for (size_t i = 0; i < 3; ++i) {
297 eq[i] = VerifierEqPolynomial<FF>::eval(set.verifier_claims[i].challenge, challenges.r);
298 if (i > 0) {
299 b[i] = b[i - 1] * challenges.gamma;
300 }
301 }
302 const std::array<FF, 3> a{ b[0] * eq[0], b[1] * eq[1], b[2] * eq[2] };
303 const std::array<FF, 3> delta{ a[1] * b[2] - a[2] * b[1],
304 a[2] * b[0] - a[0] * b[2],
305 a[0] * b[1] - a[1] * b[0] };
306 for (size_t i = 0; i < 3; ++i) {
307 proof[EVALS_OFFSET + i] += delta[i];
308 }
309 }
310
311 return { std::move(set), std::move(verifier_claims), std::move(proof) };
312}
313
314template <bool Recursive, size_t Claims> struct Config {
315 static constexpr bool IsRecursive = Recursive;
316 static constexpr size_t NumClaims = Claims;
317};
318
319template <typename Params> class MultilinearBatchingTests : public ::testing::Test {
320 public:
321 static constexpr bool IsRecursive = Params::IsRecursive;
322 static constexpr size_t NumClaims = Params::NumClaims;
323
324 static void SetUpTestSuite() { bb::srs::init_file_crs_factory(bb::srs::bb_crs_path()); }
325
326 struct RunResult {
327 bool verified; // sumcheck + eq-consistency accepted (in-circuit value checks, for the recursive path)
328 bool circuit_ok; // recursive verifier circuit is satisfiable (always true for the native path)
329 bool claim_bound; // the resulting accumulator opens to the honest batched polynomial
330 bool accepted() const { return verified && circuit_ok && claim_bound; }
331 };
332
333 static RunResult run(FaultMode fault)
334 {
335 FaultyProof faulty = build_faulty_proof(NumClaims, fault);
336
337 bool verified = false;
338 bool circuit_ok = true;
339 VerifierClaim new_claim;
340
341 if constexpr (!IsRecursive) {
342 auto transcript = std::make_shared<NativeTranscriptType>();
343 transcript->load_proof(faulty.proof);
344 [[maybe_unused]] FF seed = transcript->template receive_from_prover<FF>("init");
345 MultilinearBatchingNativeVerifier verifier(transcript);
346 std::tie(verified, new_claim) = verifier.verify_proof(faulty.verifier_claims);
347 } else {
348 using RecursiveVerifier = MultilinearBatchingRecursiveVerifier;
349 using RecursiveCurve = typename RecursiveVerifier::Curve;
351 using RecursiveFF = typename RecursiveCurve::ScalarField;
352
355 typename RecursiveVerifier::Proof stdlib_proof(builder, faulty.proof);
356 transcript->load_proof(stdlib_proof);
357 [[maybe_unused]] RecursiveFF seed = transcript->template receive_from_prover<RecursiveFF>("init");
358
359 std::vector<RecursiveClaim> recursive_claims;
360 recursive_claims.reserve(faulty.verifier_claims.size());
361 for (const auto& claim : faulty.verifier_claims) {
362 RecursiveClaim recursive_claim =
363 RecursiveClaim::template stdlib_from_native<RecursiveCurve>(&builder, claim);
364 // The claims stand in for values the kernel would receive already constrained; clear the free-witness
365 // tags so the verifier's origin-tag mechanism does not flag them when they mix with transcript values.
366 for (auto& challenge_element : recursive_claim.challenge) {
367 challenge_element.unset_free_witness_tag();
368 }
369 recursive_claim.non_shifted_evaluation.unset_free_witness_tag();
370 recursive_claim.shifted_evaluation.unset_free_witness_tag();
371 recursive_claim.non_shifted_commitment.unset_free_witness_tag();
372 recursive_claim.shifted_commitment.unset_free_witness_tag();
373 recursive_claims.push_back(std::move(recursive_claim));
374 }
375
376 RecursiveVerifier verifier(transcript);
377 auto [verified_in_circuit, recursive_new_claim] = verifier.verify_proof(recursive_claims);
378 verified = verified_in_circuit;
380 new_claim = recursive_new_claim.template get_value<VerifierClaim>();
381 }
382
383 const bool claim_bound = output_claim_is_bound(faulty.set, faulty.proof, new_claim);
384 return { verified, circuit_ok, claim_bound };
385 }
386};
387
388// Cover every supported batch width, 2 .. CHONK_MAX_CLAIMS_PER_KERNEL, in both the native and recursive verifier.
389// the static_assert flags when bumping CHONK_MAX_CLAIMS_PER_KERNEL leaves a width uncovered
390static_assert(CHONK_MAX_CLAIMS_PER_KERNEL == 7,
391 "Update TestConfigs to cover every width in 2 .. CHONK_MAX_CLAIMS_PER_KERNEL.");
392using TestConfigs = ::testing::Types<Config<false, 2>,
393 Config<false, 3>,
394 Config<false, 4>,
395 Config<false, 5>,
396 Config<false, 6>,
397 Config<false, 7>,
398 Config<true, 2>,
399 Config<true, 3>,
400 Config<true, 4>,
401 Config<true, 5>,
402 Config<true, 6>,
403 Config<true, 7>>;
404
405TYPED_TEST_SUITE(MultilinearBatchingTests, TestConfigs);
406
407// Completeness: honest claims verify and the output claim is bound to the honest batched polynomial.
408TYPED_TEST(MultilinearBatchingTests, ValidProofPasses)
409{
410 auto result = TestFixture::run(FaultMode::NONE);
411 EXPECT_TRUE(result.verified);
412 EXPECT_TRUE(result.circuit_ok);
413 EXPECT_TRUE(result.claim_bound);
414}
415
416// A wrong non-shifted input claim makes the sumcheck target inconsistent with the polynomials. Targeted on both the
417// first claim (coefficient γ^0 = 1) and the second claim (coefficient γ^1 = γ).
418TYPED_TEST(MultilinearBatchingTests, FalseNonShiftedClaimFirstFails)
419{
421 EXPECT_FALSE(TestFixture::run(FaultMode::FALSE_NONSHIFTED_EVAL_FIRST).accepted());
422}
423
424TYPED_TEST(MultilinearBatchingTests, FalseNonShiftedClaimSecondFails)
425{
427 EXPECT_FALSE(TestFixture::run(FaultMode::FALSE_NONSHIFTED_EVAL_SECOND).accepted());
428}
429
430// A wrong shifted input claim makes the sumcheck target inconsistent with the polynomials. Targeted on both the first
431// claim (coefficient 1) and the second claim (coefficient γ).
432TYPED_TEST(MultilinearBatchingTests, FalseShiftedClaimFirstFails)
433{
435 EXPECT_FALSE(TestFixture::run(FaultMode::FALSE_SHIFTED_EVAL_FIRST).accepted());
436}
437
438TYPED_TEST(MultilinearBatchingTests, FalseShiftedClaimSecondFails)
439{
441 EXPECT_FALSE(TestFixture::run(FaultMode::FALSE_SHIFTED_EVAL_SECOND).accepted());
442}
443
444// A wrong claim challenge makes the eq polynomial evaluated by the verifier inconsistent with the one the prover
445// committed to, so eq-consistency fails. Targeted on both the first claim (coefficient 1) and the second claim (γ).
446TYPED_TEST(MultilinearBatchingTests, FalseEqFirstFails)
447{
449 EXPECT_FALSE(TestFixture::run(FaultMode::FALSE_EQ_FIRST).accepted());
450}
451
452TYPED_TEST(MultilinearBatchingTests, FalseEqSecondFails)
453{
455 EXPECT_FALSE(TestFixture::run(FaultMode::FALSE_EQ_SECOND).accepted());
456}
457
458// Corrupting a claimed non-shifted evaluation breaks the sumcheck final relation check. Tampered on both the first
459// claim (idx 0) and the second claim (idx 1).
460TYPED_TEST(MultilinearBatchingTests, TamperedNonShiftedEvalFirstFails)
461{
463 EXPECT_FALSE(TestFixture::run(FaultMode::TAMPER_NONSHIFTED_EVAL_FIRST).accepted());
464}
465
466TYPED_TEST(MultilinearBatchingTests, TamperedNonShiftedEvalSecondFails)
467{
469 EXPECT_FALSE(TestFixture::run(FaultMode::TAMPER_NONSHIFTED_EVAL_SECOND).accepted());
470}
471
472// Corrupting a claimed shifted evaluation breaks the sumcheck final relation check. Tampered on both the first claim
473// (idx 0) and the second claim (idx 1).
474TYPED_TEST(MultilinearBatchingTests, TamperedShiftedEvalFirstFails)
475{
477 EXPECT_FALSE(TestFixture::run(FaultMode::TAMPER_SHIFTED_EVAL_FIRST).accepted());
478}
479
480TYPED_TEST(MultilinearBatchingTests, TamperedShiftedEvalSecondFails)
481{
483 EXPECT_FALSE(TestFixture::run(FaultMode::TAMPER_SHIFTED_EVAL_SECOND).accepted());
484}
485
486// Corrupting a claimed eq evaluation breaks the eq-consistency check. Tampered on both the first claim (idx 0) and the
487// second claim (idx 1).
488TYPED_TEST(MultilinearBatchingTests, TamperedEqEvalFirstFails)
489{
491 EXPECT_FALSE(TestFixture::run(FaultMode::TAMPER_EQ_EVAL_FIRST).accepted());
492}
493
494TYPED_TEST(MultilinearBatchingTests, TamperedEqEvalSecondFails)
495{
497 EXPECT_FALSE(TestFixture::run(FaultMode::TAMPER_EQ_EVAL_SECOND).accepted());
498}
499
500// A wrong non shifted commitment slips past the sumcheck (which never reads commitments), but the output claim is no
501// longer bound to it — the downstream PCS opening would reject it.
502TYPED_TEST(MultilinearBatchingTests, WrongNonShiftedCommitment)
503{
504 auto result = TestFixture::run(FaultMode::WRONG_NONSHIFTED_COMMITMENT);
505 EXPECT_TRUE(result.verified);
506 EXPECT_TRUE(result.circuit_ok);
507 EXPECT_FALSE(result.claim_bound);
508}
509
510// A wrong shifted commitment slips past the sumcheck (which never reads commitments), but the output claim is no longer
511// bound to it — the downstream PCS opening would reject it.
512TYPED_TEST(MultilinearBatchingTests, WrongShiftedCommitment)
513{
514 auto result = TestFixture::run(FaultMode::WRONG_SHIFTED_COMMITMENT);
515 EXPECT_TRUE(result.verified);
516 EXPECT_TRUE(result.circuit_ok);
517 EXPECT_FALSE(result.claim_bound);
518}
519
520// The core soundness property of the fresh-ρ merge: polynomials evaluations crafted to satisfy the γ-weighted sumcheck
521// (and eq consistency) still fail to bind, because the merge challenge ρ is drawn only after they are committed.
522TYPED_TEST(MultilinearBatchingTests, BrokenEvalBindingIsCaughtByMerge)
523{
524 if (TestFixture::NumClaims < 3) {
525 GTEST_SKIP() << "The eval-binding attack needs >= 3 claims (2 constraints, 3 unknowns) to also satisfy the "
526 "pre-fix γ-weighted merge; with 2 claims no such non-trivial perturbation exists.";
527 }
528 auto result = TestFixture::run(FaultMode::BREAK_EVAL_BINDING);
529 EXPECT_TRUE(result.verified);
530 EXPECT_TRUE(result.circuit_ok);
531 EXPECT_FALSE(result.claim_bound);
532}
533
538bool verify_with_mismatched_claim_count(size_t prover_num_claims, size_t verifier_num_claims)
539{
540 const HonkProof proof = build_faulty_proof(prover_num_claims, FaultMode::NONE).proof;
541 const std::vector<VerifierClaim> claims = build_honest_claims(verifier_num_claims).verifier_claims;
542
543 auto transcript = std::make_shared<NativeTranscriptType>();
544 transcript->load_proof(proof);
545 [[maybe_unused]] FF seed = transcript->template receive_from_prover<FF>("init");
546 MultilinearBatchingNativeVerifier verifier(transcript);
547 return std::get<0>(verifier.verify_proof(claims));
548}
549
550class MultilinearBatchingClaimCountTests : public ::testing::Test {
551 protected:
552 static void SetUpTestSuite() { bb::srs::init_file_crs_factory(bb::srs::bb_crs_path()); }
553};
554
555// The verifier supplies one more claim than the prover proved. The wider verifier tries to read 3·(N+1) claimed
556// evaluations from a proof that only carries 3·N, so the transcript runs out of bounds and the verifier throws.
557TEST_F(MultilinearBatchingClaimCountTests, MoreClaimsThanProvedThrows)
558{
560 EXPECT_ANY_THROW(verify_with_mismatched_claim_count(/*prover_num_claims=*/2, /*verifier_num_claims=*/3));
561}
562
563// The verifier supplies one fewer claim than the prover proved. This produces a transcript mismatch and therefore the
564// verifier rejects
565TEST_F(MultilinearBatchingClaimCountTests, FewerClaimsThanProvedThrows)
566{
567 EXPECT_FALSE(verify_with_mismatched_claim_count(/*prover_num_claims=*/3, /*verifier_num_claims=*/2));
568}
569
570} // namespace
#define BB_DISABLE_ASSERTS()
Definition assert.hpp:33
CommitmentKey object over a pairing group 𝔾₁.
static constexpr size_t BATCHED_RELATION_PARTIAL_LENGTH
Public entrypoint for multilinear batching.
Public entrypoint for multilinear batching verification.
static Polynomial random(size_t size, size_t start_index=0)
Fr evaluate_mle(std::span< const Fr > evaluation_points, bool shift=false) const
evaluate multi-linear extension p(X_0,…,X_{n-1}) = \sum_i a_i*L_i(X_0,…,X_{n-1}) at u = (u_0,...
static bool check(const Builder &circuit)
Check the witness satisifies the circuit.
typename Group::affine_element AffineElement
Definition grumpkin.hpp:64
AluTraceBuilder builder
Definition alu.test.cpp:124
FF a
FF b
std::filesystem::path bb_crs_path()
void init_file_crs_factory(const std::filesystem::path &path)
testing::Types< RecursiveVerifierTestParams< MegaRecursiveFlavor_< MegaCircuitBuilder >, DefaultIO< MegaCircuitBuilder > >, RecursiveVerifierTestParams< MegaRecursiveFlavor_< UltraCircuitBuilder >, DefaultIO< UltraCircuitBuilder > >, RecursiveVerifierTestParams< UltraRecursiveFlavor_< UltraCircuitBuilder >, DefaultIO< UltraCircuitBuilder > >, RecursiveVerifierTestParams< UltraRecursiveFlavor_< UltraCircuitBuilder >, RollupIO >, RecursiveVerifierTestParams< UltraRecursiveFlavor_< MegaCircuitBuilder >, DefaultIO< MegaCircuitBuilder > >, RecursiveVerifierTestParams< UltraZKRecursiveFlavor_< UltraCircuitBuilder >, DefaultIO< UltraCircuitBuilder > >, RecursiveVerifierTestParams< UltraZKRecursiveFlavor_< MegaCircuitBuilder >, DefaultIO< MegaCircuitBuilder > >, RecursiveVerifierTestParams< MegaZKRecursiveFlavor_< MegaCircuitBuilder >, DefaultIO< MegaCircuitBuilder > >, RecursiveVerifierTestParams< MegaZKRecursiveFlavor_< UltraCircuitBuilder >, DefaultIO< UltraCircuitBuilder > > > TestConfigs
Entry point for Barretenberg command-line interface.
Definition api.hpp:5
std::vector< fr > HonkProof
Definition proof.hpp:15
TEST_F(IPATest, ChallengesAreZero)
Definition ipa.test.cpp:160
TYPED_TEST_SUITE(CommitmentKeyTest, Curves)
MultilinearBatchingVerifier< true > MultilinearBatchingRecursiveVerifier
TYPED_TEST(CommitmentKeyTest, CommitToZeroPoly)
BaseTranscript< FrCodec, bb::crypto::Poseidon2< bb::crypto::Poseidon2Bn254ScalarFieldParams > > NativeTranscript
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
std::string to_string(bb::avm2::ValueTag tag)
bb::VectorAffineElementPushSpan< BaseParams > out
Prover's claim for multilinear batching - contains polynomials and their evaluation claims.
Verifier's claim for multilinear batching - contains commitments and evaluation claims.
static FF eval(std::span< const FF > r_in, std::span< const FF > u)
static field random_element(numeric::RNG *engine=nullptr) noexcept
VectorField result