binius_core/protocols/evalcheck/
verify.rs

1// Copyright 2024-2025 Irreducible Inc.
2
3use std::{iter, mem};
4
5use binius_field::{Field, TowerField, util::inner_product_unchecked};
6use getset::{Getters, MutGetters};
7use itertools::chain;
8use tracing::instrument;
9
10use super::{
11	EvalPoint, deserialize_evalcheck_proof,
12	error::{Error, VerificationError},
13	evalcheck::{EvalcheckHint, EvalcheckMultilinearClaim},
14	subclaims::{
15		add_bivariate_sumcheck_to_constraints, add_composite_sumcheck_to_constraints,
16		packed_sumcheck_meta, shifted_sumcheck_meta,
17	},
18};
19use crate::{
20	fiat_shamir::Challenger,
21	oracle::{
22		ConstraintSetBuilder, Error as OracleError, MultilinearOracleSet, MultilinearPolyVariant,
23		OracleId, SizedConstraintSet,
24	},
25	polynomial::MultivariatePoly,
26	transcript::VerifierTranscript,
27	transparent::select_row::SelectRow,
28};
29
30/// A mutable verifier state.
31///
32/// Can be persisted across [`EvalcheckVerifier::verify`] invocations. Accumulates
33/// `new_sumchecks` bivariate sumcheck constraints, as well as holds mutable references to
34/// the trace (to which new oracles & multilinears may be added during verification)
35#[derive(Getters, MutGetters)]
36pub struct EvalcheckVerifier<'a, F>
37where
38	F: TowerField,
39{
40	/// Mutable reference to the oracle set which is modified to create new claims arising from
41	/// sumchecks
42	pub(crate) oracles: &'a mut MultilinearOracleSet<F>,
43
44	/// The committed evaluation claims in this round
45	#[getset(get = "pub", get_mut = "pub")]
46	committed_eval_claims: Vec<EvalcheckMultilinearClaim<F>>,
47
48	/// The new sumcheck constraints in this round
49	new_sumcheck_constraints: Vec<ConstraintSetBuilder<F>>,
50
51	// The new mle sumcheck constraints arising in this round
52	new_mlechecks_constraints: Vec<(EvalPoint<F>, ConstraintSetBuilder<F>)>,
53
54	/// The list of claims that have been verified in this round
55	round_claims: Vec<EvalcheckMultilinearClaim<F>>,
56}
57
58impl<'a, F: TowerField> EvalcheckVerifier<'a, F> {
59	/// Create a new verifier state from a mutable reference to the oracle set
60	/// (it needs to be mutable because `new_sumcheck` reduction may add new
61	/// oracles & multilinears)
62	pub const fn new(oracles: &'a mut MultilinearOracleSet<F>) -> Self {
63		Self {
64			oracles,
65			committed_eval_claims: Vec::new(),
66			new_sumcheck_constraints: Vec::new(),
67			new_mlechecks_constraints: Vec::new(),
68			round_claims: Vec::new(),
69		}
70	}
71
72	/// A helper method to move out sumcheck constraints
73	pub fn take_new_sumcheck_constraints(
74		&mut self,
75	) -> Result<Vec<SizedConstraintSet<F>>, OracleError> {
76		self.new_sumcheck_constraints
77			.iter_mut()
78			.map(|builder| mem::take(builder).build_one(self.oracles))
79			.filter(|constraint| !matches!(constraint, Err(OracleError::EmptyConstraintSet)))
80			.collect()
81	}
82
83	/// A helper method to move out mlechecks constraints
84	pub fn take_new_mlechecks_constraints(
85		&mut self,
86	) -> Result<ConstraintSetsEqIndPoints<F>, OracleError> {
87		let new_mlechecks_constraints = std::mem::take(&mut self.new_mlechecks_constraints);
88
89		let mut eq_ind_challenges = Vec::with_capacity(new_mlechecks_constraints.len());
90		let mut constraint_sets = Vec::with_capacity(new_mlechecks_constraints.len());
91
92		for (ep, builder) in new_mlechecks_constraints {
93			eq_ind_challenges.push(ep.to_vec());
94			constraint_sets.push(builder.build_one(self.oracles)?)
95		}
96		Ok(ConstraintSetsEqIndPoints {
97			eq_ind_challenges,
98			constraint_sets,
99		})
100	}
101
102	/// Verify an evalcheck claim.
103	///
104	/// For each claim, we verify the proof by recursively verifying the subclaims in a DFS manner
105	/// deduplicating previously verified claims
106	/// See [`EvalcheckProver::prove`](`super::prove::EvalcheckProver::prove`) docs for more
107	/// details.
108	#[instrument(skip_all, name = "EvalcheckVerifierState::verify", level = "debug")]
109	pub fn verify<Challenger_: Challenger>(
110		&mut self,
111		evalcheck_claims: impl IntoIterator<Item = EvalcheckMultilinearClaim<F>>,
112		transcript: &mut VerifierTranscript<Challenger_>,
113	) -> Result<(), Error> {
114		self.round_claims.clear();
115		for claim in evalcheck_claims {
116			self.verify_multilinear(claim, transcript)?;
117		}
118		Ok(())
119	}
120
121	fn verify_multilinear<Challenger_: Challenger>(
122		&mut self,
123		evalcheck_claim: EvalcheckMultilinearClaim<F>,
124		transcript: &mut VerifierTranscript<Challenger_>,
125	) -> Result<(), Error> {
126		let evalcheck_proof = deserialize_evalcheck_proof(&mut transcript.message())?;
127
128		// If the proof is a duplicate claim, we need to check if the claim is already in the round
129		// claims, which have been verified.
130		if let EvalcheckHint::DuplicateClaim(index) = evalcheck_proof {
131			if let Some(expected_claim) = self.round_claims.get(index as usize) {
132				if *expected_claim == evalcheck_claim {
133					return Ok(());
134				}
135			}
136			return Err(VerificationError::DuplicateClaimMismatch.into());
137		}
138
139		self.verify_multilinear_skip_duplicate_check(evalcheck_claim, transcript)
140	}
141
142	fn verify_multilinear_skip_duplicate_check<Challenger_: Challenger>(
143		&mut self,
144		evalcheck_claim: EvalcheckMultilinearClaim<F>,
145		transcript: &mut VerifierTranscript<Challenger_>,
146	) -> Result<(), Error> {
147		self.round_claims.push(evalcheck_claim.clone());
148
149		let EvalcheckMultilinearClaim {
150			id,
151			eval_point,
152			eval,
153		} = evalcheck_claim;
154
155		let multilinear = &self.oracles[id];
156		let multilinear_label = multilinear.label();
157		match multilinear.variant {
158			MultilinearPolyVariant::Transparent(ref inner) => {
159				let actual_eval = inner.poly().evaluate(&eval_point)?;
160				if actual_eval != eval {
161					return Err(VerificationError::IncorrectEvaluation(multilinear_label).into());
162				}
163			}
164			MultilinearPolyVariant::Structured(ref inner) => {
165				// Here we need to extend the eval_point to the input domain of the arith circuit
166				// by assigning zeroes to the variables.
167				let eval_point: &[F] = &eval_point;
168				let n_pad_zeros = inner.n_vars() - eval_point.len();
169				let query = eval_point
170					.iter()
171					.copied()
172					.chain(iter::repeat_n(F::ZERO, n_pad_zeros))
173					.collect::<Vec<_>>();
174				let actual_eval = inner.evaluate(&query)?;
175				if actual_eval != eval {
176					return Err(VerificationError::IncorrectEvaluation(multilinear_label).into());
177				}
178			}
179
180			MultilinearPolyVariant::Committed => {
181				self.committed_eval_claims.push(EvalcheckMultilinearClaim {
182					id,
183					eval_point,
184					eval,
185				});
186			}
187
188			MultilinearPolyVariant::Repeating { id, log_count } => {
189				let n_vars = eval_point.len() - log_count;
190				self.verify_multilinear(
191					EvalcheckMultilinearClaim {
192						id,
193						eval_point: eval_point.slice(0..n_vars),
194						eval,
195					},
196					transcript,
197				)?;
198			}
199
200			MultilinearPolyVariant::Projected(ref projected) => {
201				let new_eval_point = {
202					let (lo, hi) = eval_point.split_at(projected.start_index());
203					chain!(lo, projected.values(), hi)
204						.copied()
205						.collect::<Vec<_>>()
206				};
207
208				self.verify_multilinear(
209					EvalcheckMultilinearClaim {
210						id: projected.id(),
211						eval_point: new_eval_point.into(),
212						eval,
213					},
214					transcript,
215				)?;
216			}
217
218			MultilinearPolyVariant::Shifted(ref shifted) => {
219				let shifted = shifted.clone();
220				let meta = shifted_sumcheck_meta(self.oracles, &shifted, &eval_point)?;
221				add_bivariate_sumcheck_to_constraints(
222					&meta,
223					&mut self.new_sumcheck_constraints,
224					shifted.block_size(),
225					eval,
226				)
227			}
228
229			MultilinearPolyVariant::Packed(ref packed) => {
230				let packed = packed.clone();
231				let meta = packed_sumcheck_meta(self.oracles, &packed, &eval_point)?;
232				add_bivariate_sumcheck_to_constraints(
233					&meta,
234					&mut self.new_sumcheck_constraints,
235					packed.log_degree(),
236					eval,
237				)
238			}
239
240			MultilinearPolyVariant::LinearCombination(ref linear_combination) => {
241				let linear_combination = linear_combination.clone();
242				let evals = linear_combination
243					.polys()
244					.map(|sub_oracle_id| {
245						self.verify_multilinear_subclaim(
246							sub_oracle_id,
247							eval_point.clone(),
248							transcript,
249						)
250					})
251					.collect::<Result<Vec<_>, _>>()?;
252
253				// Verify the evaluation of the linear combination over the claimed evaluations
254				let actual_eval = linear_combination.offset()
255					+ inner_product_unchecked::<F, F>(evals, linear_combination.coefficients());
256
257				if actual_eval != eval {
258					return Err(VerificationError::IncorrectEvaluation(multilinear_label).into());
259				}
260			}
261			MultilinearPolyVariant::ZeroPadded(ref padded) => {
262				let subclaim_eval_point = chain!(
263					&eval_point[..padded.start_index()],
264					&eval_point[padded.start_index() + padded.n_pad_vars()..],
265				)
266				.copied()
267				.collect::<Vec<_>>();
268
269				let zs =
270					&eval_point[padded.start_index()..padded.start_index() + padded.n_pad_vars()];
271				let select_row = SelectRow::new(zs.len(), padded.nonzero_index())?;
272				let select_row_term = select_row
273					.evaluate(zs)
274					.expect("select_row is constructor with zs.len() variables");
275
276				let inner_eval = match select_row_term.invert() {
277					Some(invert) => eval * invert,
278					None if eval.is_zero() => return Ok(()),
279					None => {
280						return Err(
281							VerificationError::IncorrectEvaluation(multilinear_label).into()
282						);
283					}
284				};
285
286				self.verify_multilinear(
287					EvalcheckMultilinearClaim {
288						id: padded.id(),
289						eval_point: subclaim_eval_point.into(),
290						eval: inner_eval,
291					},
292					transcript,
293				)?;
294			}
295			MultilinearPolyVariant::Composite(ref composite) => {
296				let position = transcript.message().read::<u32>()? as usize;
297
298				if let Some((constraints_eval_point, _)) =
299					self.new_mlechecks_constraints.get(position)
300				{
301					if *constraints_eval_point != eval_point {
302						return Err(VerificationError::MLECheckConstraintSetPositionMismatch.into());
303					}
304				}
305
306				add_composite_sumcheck_to_constraints(
307					position,
308					&eval_point,
309					&mut self.new_mlechecks_constraints,
310					composite,
311					eval,
312				);
313			}
314		}
315
316		Ok(())
317	}
318
319	fn verify_multilinear_subclaim<Challenger_: Challenger>(
320		&mut self,
321		oracle_id: OracleId,
322		eval_point: EvalPoint<F>,
323		transcript: &mut VerifierTranscript<Challenger_>,
324	) -> Result<F, Error> {
325		// If the subproof is a duplicate claim, we need to check that the claim is already in the
326		// round claims and return the evaluation. Otherwise, we verify the subclaim recursively.
327		let subproof = deserialize_evalcheck_proof(&mut transcript.message())?;
328		match subproof {
329			EvalcheckHint::DuplicateClaim(index) => {
330				let index = index as usize;
331				if self.round_claims[index].id != oracle_id
332					|| self.round_claims[index].eval_point != eval_point
333				{
334					return Err(VerificationError::DuplicateClaimMismatch.into());
335				}
336
337				Ok(self.round_claims[index].eval)
338			}
339			EvalcheckHint::NewClaim => {
340				let eval = transcript.message().read_scalar()?;
341				let subclaim = EvalcheckMultilinearClaim {
342					id: oracle_id,
343					eval_point,
344					eval,
345				};
346				self.verify_multilinear_skip_duplicate_check(subclaim, transcript)?;
347				Ok(eval)
348			}
349		}
350	}
351}
352
353pub struct ConstraintSetsEqIndPoints<F: Field> {
354	pub eq_ind_challenges: Vec<Vec<F>>,
355	pub constraint_sets: Vec<SizedConstraintSet<F>>,
356}