binius_core/protocols/evalcheck/
verify.rs

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