binius_core/protocols/sumcheck/
verify.rs

1// Copyright 2024-2025 Irreducible Inc.
2
3use binius_field::{Field, TowerField};
4use binius_math::{evaluate_univariate, CompositionPoly, EvaluationOrder};
5use binius_utils::{bail, sorting::is_sorted_ascending};
6use itertools::izip;
7use tracing::instrument;
8
9use super::{
10	common::{batch_weighted_value, BatchSumcheckOutput, RoundProof, SumcheckClaim},
11	error::{Error, VerificationError},
12	RoundCoeffs,
13};
14use crate::{
15	fiat_shamir::{CanSample, Challenger},
16	transcript::VerifierTranscript,
17};
18
19/// Verify a batched sumcheck protocol execution.
20///
21/// The sumcheck protocol over can be batched over multiple instances by taking random linear
22/// combinations over the claimed sums and polynomials. When the sumcheck instances are not all
23/// over polynomials with the same number of variables, we can still batch them together, sharing
24/// later round challenges. Importantly, the verifier samples mixing challenges "just-in-time".
25/// That is, the verifier samples mixing challenges for new sumcheck claims over n variables only
26/// after the last sumcheck round message has been sent by the prover.
27///
28/// For each sumcheck claim, we sample one random mixing coefficient. The multiple composite claims
29/// within each claim over a group of multilinears are mixed using the powers of the mixing
30/// coefficient.
31pub fn batch_verify<F, Composition, Challenger_>(
32	evaluation_order: EvaluationOrder,
33	claims: &[SumcheckClaim<F, Composition>],
34	transcript: &mut VerifierTranscript<Challenger_>,
35) -> Result<BatchSumcheckOutput<F>, Error>
36where
37	F: TowerField,
38	Composition: CompositionPoly<F>,
39	Challenger_: Challenger,
40{
41	let start = BatchVerifyStart {
42		batch_coeffs: Vec::new(),
43		sum: F::ZERO,
44		max_degree: 0,
45		skip_rounds: 0,
46	};
47
48	batch_verify_with_start(evaluation_order, start, claims, transcript)
49}
50
51/// A struct describing the starting state of batched sumcheck verify invocation.
52#[derive(Debug)]
53pub struct BatchVerifyStart<F: Field> {
54	/// Batching coefficients for the already batched claims.
55	pub batch_coeffs: Vec<F>,
56	/// Batched sum claims.
57	pub sum: F,
58	/// Maximum individual degree of the already batched claims
59	pub max_degree: usize,
60	/// Number of multilinear rounds to skip during verification.
61	pub skip_rounds: usize,
62}
63
64/// Verify a batched sumcheck protocol execution, but after some rounds have been processed.
65#[instrument(skip_all, level = "debug")]
66pub fn batch_verify_with_start<F, Composition, Challenger_>(
67	evaluation_order: EvaluationOrder,
68	start: BatchVerifyStart<F>,
69	claims: &[SumcheckClaim<F, Composition>],
70	transcript: &mut VerifierTranscript<Challenger_>,
71) -> Result<BatchSumcheckOutput<F>, Error>
72where
73	F: TowerField,
74	Composition: CompositionPoly<F>,
75	Challenger_: Challenger,
76{
77	let BatchVerifyStart {
78		mut batch_coeffs,
79		mut sum,
80		mut max_degree,
81		skip_rounds,
82	} = start;
83
84	// Check that the claims are in descending order by n_vars
85	if !is_sorted_ascending(claims.iter().map(|claim| claim.n_vars()).rev()) {
86		bail!(Error::ClaimsOutOfOrder);
87	}
88
89	if batch_coeffs.len() > claims.len() {
90		bail!(Error::TooManyPrebatchedCoeffs);
91	}
92
93	let n_rounds = claims.iter().map(|claim| claim.n_vars()).max().unwrap_or(0);
94
95	if skip_rounds > n_rounds {
96		return Err(VerificationError::IncorrectSkippedRoundsCount.into());
97	}
98
99	// active_index is an index into the claims slice. Claims before the active index have already
100	// been batched into the instance and claims after the index have not.
101	let mut active_index = batch_coeffs.len();
102	let mut challenges = Vec::with_capacity(n_rounds - skip_rounds);
103	for round_no in skip_rounds..n_rounds {
104		let n_vars = n_rounds - round_no;
105
106		while let Some(claim) = claims.get(active_index) {
107			if claim.n_vars() != n_vars {
108				break;
109			}
110
111			let next_batch_coeff = transcript.sample();
112			batch_coeffs.push(next_batch_coeff);
113
114			// Batch the next claimed sum into the batched sum.
115			sum += batch_weighted_value(
116				next_batch_coeff,
117				claim
118					.composite_sums()
119					.iter()
120					.map(|inner_claim| inner_claim.sum),
121			);
122			max_degree = max_degree.max(claim.max_individual_degree());
123			active_index += 1;
124		}
125
126		let coeffs = transcript.message().read_scalar_slice(max_degree)?;
127		let round_proof = RoundProof(RoundCoeffs(coeffs));
128
129		let challenge = transcript.sample();
130		challenges.push(challenge);
131
132		sum = interpolate_round_proof(round_proof, sum, challenge);
133	}
134
135	// Batch in any claims for 0-variate (ie. constant) polynomials.
136	while let Some(claim) = claims.get(active_index) {
137		debug_assert_eq!(claim.n_vars(), 0);
138
139		let next_batch_coeff = transcript.sample();
140		batch_coeffs.push(next_batch_coeff);
141
142		// Batch the next claimed sum into the batched sum.
143		sum += batch_weighted_value(
144			next_batch_coeff,
145			claim
146				.composite_sums()
147				.iter()
148				.map(|inner_claim| inner_claim.sum),
149		);
150		active_index += 1;
151	}
152
153	let mut multilinear_evals = Vec::with_capacity(claims.len());
154	let mut reader = transcript.message();
155	for claim in claims {
156		let evals = reader.read_scalar_slice::<F>(claim.n_multilinears())?;
157		multilinear_evals.push(evals);
158	}
159
160	let expected_sum = compute_expected_batch_composite_evaluation_multi_claim(
161		batch_coeffs,
162		claims,
163		&multilinear_evals,
164	)?;
165
166	if sum != expected_sum {
167		return Err(VerificationError::IncorrectBatchEvaluation.into());
168	}
169
170	if EvaluationOrder::HighToLow == evaluation_order {
171		challenges.reverse();
172	}
173
174	Ok(BatchSumcheckOutput {
175		challenges,
176		multilinear_evals,
177	})
178}
179
180pub fn compute_expected_batch_composite_evaluation_single_claim<F: Field, Composition>(
181	batch_coeff: F,
182	claim: &SumcheckClaim<F, Composition>,
183	multilinear_evals: &[F],
184) -> Result<F, Error>
185where
186	Composition: CompositionPoly<F>,
187{
188	let composite_evals = claim
189		.composite_sums()
190		.iter()
191		.map(|sum_claim| sum_claim.composition.evaluate(multilinear_evals))
192		.collect::<Result<Vec<_>, _>>()?;
193	Ok(batch_weighted_value(batch_coeff, composite_evals.into_iter()))
194}
195
196fn compute_expected_batch_composite_evaluation_multi_claim<F: Field, Composition>(
197	batch_coeffs: Vec<F>,
198	claims: &[SumcheckClaim<F, Composition>],
199	multilinear_evals: &[Vec<F>],
200) -> Result<F, Error>
201where
202	Composition: CompositionPoly<F>,
203{
204	izip!(batch_coeffs, claims, multilinear_evals.iter())
205		.map(|(batch_coeff, claim, multilinear_evals)| {
206			compute_expected_batch_composite_evaluation_single_claim(
207				batch_coeff,
208				claim,
209				multilinear_evals,
210			)
211		})
212		.try_fold(F::ZERO, |sum, term| Ok(sum + term?))
213}
214
215pub fn interpolate_round_proof<F: Field>(round_proof: RoundProof<F>, sum: F, challenge: F) -> F {
216	let coeffs = round_proof.recover(sum);
217	evaluate_univariate(&coeffs.0, challenge)
218}