binius_core/protocols/sumcheck/prove/
eq_ind.rs

1// Copyright 2025 Irreducible Inc.
2
3use std::{cmp::Reverse, marker::PhantomData, ops::Range};
4
5use binius_field::{util::eq, ExtensionField, Field, PackedExtension, PackedField, TowerField};
6use binius_hal::{
7	make_portable_backend, ComputationBackend, Error as HalError, SumcheckEvaluator,
8	SumcheckMultilinear,
9};
10use binius_math::{
11	CompositionPoly, EvaluationDomainFactory, EvaluationOrder, InterpolationDomain,
12	MLEDirectAdapter, MultilinearPoly, RowsBatchRef,
13};
14use binius_maybe_rayon::prelude::*;
15use binius_utils::bail;
16use getset::Getters;
17use itertools::izip;
18use stackalloc::stackalloc_with_default;
19use tracing::instrument;
20
21use crate::{
22	polynomial::{ArithCircuitPoly, Error as PolynomialError, MultivariatePoly},
23	protocols::sumcheck::{
24		common::{
25			equal_n_vars_check, get_nontrivial_evaluation_points,
26			interpolation_domains_for_composition_degrees, RoundCoeffs,
27		},
28		prove::{common::fold_partial_eq_ind, ProverState, SumcheckInterpolator, SumcheckProver},
29		CompositeSumClaim, Error,
30	},
31	transparent::{eq_ind::EqIndPartialEval, step_up::StepUp},
32};
33
34pub fn validate_witness<F, P, M, Composition>(
35	n_vars: usize,
36	multilinears: &[SumcheckMultilinear<P, M>],
37	eq_ind_challenges: &[F],
38	eq_ind_sum_claims: impl IntoIterator<Item = CompositeSumClaim<F, Composition>>,
39) -> Result<(), Error>
40where
41	F: Field,
42	P: PackedField<Scalar = F>,
43	M: MultilinearPoly<P> + Send + Sync,
44	Composition: CompositionPoly<P>,
45{
46	if eq_ind_challenges.len() != n_vars {
47		bail!(Error::IncorrectEqIndChallengesLength);
48	}
49
50	for multilinear in multilinears {
51		match *multilinear {
52			SumcheckMultilinear::Transparent {
53				ref multilinear,
54				const_suffix: (suffix_eval, suffix_len),
55				..
56			} => {
57				if multilinear.n_vars() != n_vars {
58					bail!(Error::NumberOfVariablesMismatch);
59				}
60
61				let first_const = (1usize << n_vars)
62					.checked_sub(suffix_len)
63					.ok_or(Error::IncorrectConstSuffixes)?;
64
65				for i in first_const..1 << n_vars {
66					if multilinear.evaluate_on_hypercube(i)? != suffix_eval {
67						bail!(Error::IncorrectConstSuffixes);
68					}
69				}
70			}
71
72			SumcheckMultilinear::Folded {
73				large_field_folded_evals: ref evals,
74				..
75			} => {
76				if evals.len() > 1 << n_vars.saturating_sub(P::LOG_WIDTH) {
77					bail!(Error::IncorrectConstSuffixes)
78				}
79			}
80		}
81	}
82
83	let backend = make_portable_backend();
84	let eq_ind =
85		EqIndPartialEval::new(eq_ind_challenges).multilinear_extension::<P, _>(&backend)?;
86
87	for (i, claim) in eq_ind_sum_claims.into_iter().enumerate() {
88		let CompositeSumClaim {
89			composition,
90			sum: expected_sum,
91		} = claim;
92		let sum = (0..(1 << n_vars))
93			.into_par_iter()
94			.try_fold(
95				|| (vec![P::zero(); multilinears.len()], F::ZERO),
96				|(mut multilinear_evals, mut running_sum), j| -> Result<_, Error> {
97					for (eval, multilinear) in izip!(&mut multilinear_evals, multilinears) {
98						*eval = P::broadcast(match multilinear {
99							SumcheckMultilinear::Transparent { multilinear, .. } => {
100								multilinear.evaluate_on_hypercube(j)?
101							}
102							SumcheckMultilinear::Folded {
103								large_field_folded_evals,
104								suffix_eval,
105							} => binius_field::packed::get_packed_slice_checked(
106								large_field_folded_evals,
107								j,
108							)
109							.unwrap_or(*suffix_eval),
110						});
111					}
112
113					running_sum += eq_ind.evaluate_on_hypercube(j)?
114						* composition.evaluate(&multilinear_evals)?.get(0);
115					Ok((multilinear_evals, running_sum))
116				},
117			)
118			.map(|fold_state| -> Result<_, Error> { Ok(fold_state?.1) })
119			.try_reduce(|| F::ZERO, |a, b| Ok(a + b))?;
120
121		if sum != expected_sum {
122			bail!(Error::SumcheckNaiveValidationFailure {
123				composition_index: i,
124			});
125		}
126	}
127	Ok(())
128}
129
130/// An "eq-ind" sumcheck prover.
131///
132/// The main difference of this prover from the `RegularSumcheckProver` is that
133/// it computes round evaluations of a much simpler "prime" polynomial
134/// multiplied by an already substituted portion of the equality indicator. This
135/// "prime" polynomial has the same degree as the underlying composition,
136/// reducing the number of would-be evaluation points by one, and avoids
137/// interpolating the tensor expansion of the equality indicator.  Round
138/// evaluations for the "full" assumed composition are computed in
139/// monomial form, out of hot loop.  See [Gruen24] Section 3.2 for details.
140///
141/// The rationale behind builder interface is the need to specify the pre-expanded
142/// equality indicator and potentially known evaluations at one in first round.
143///
144/// [Gruen24]: <https://eprint.iacr.org/2024/108>
145pub struct EqIndSumcheckProverBuilder<'a, P, M, Backend>
146where
147	P: PackedField,
148	M: MultilinearPoly<P>,
149	Backend: ComputationBackend,
150{
151	n_vars: usize,
152	eq_ind_partial_evals: Option<Backend::Vec<P>>,
153	first_round_eval_1s: Option<Vec<P::Scalar>>,
154	multilinears: Vec<SumcheckMultilinear<P, M>>,
155	backend: &'a Backend,
156}
157
158impl<'a, F, P, Backend> EqIndSumcheckProverBuilder<'a, P, MLEDirectAdapter<P, Vec<P>>, Backend>
159where
160	F: TowerField,
161	P: PackedField<Scalar = F>,
162	Backend: ComputationBackend,
163{
164	pub fn without_switchover(
165		n_vars: usize,
166		multilinears: Vec<Vec<P>>,
167		backend: &'a Backend,
168	) -> Self {
169		let multilinears = multilinears
170			.into_iter()
171			.map(SumcheckMultilinear::folded)
172			.collect();
173
174		Self {
175			n_vars,
176			eq_ind_partial_evals: None,
177			first_round_eval_1s: None,
178			multilinears,
179			backend,
180		}
181	}
182}
183
184impl<'a, F, P, M, Backend> EqIndSumcheckProverBuilder<'a, P, M, Backend>
185where
186	F: TowerField,
187	P: PackedField<Scalar = F>,
188	M: MultilinearPoly<P> + Send + Sync,
189	Backend: ComputationBackend,
190{
191	pub fn with_switchover(
192		multilinears: Vec<M>,
193		switchover_fn: impl Fn(usize) -> usize,
194		backend: &'a Backend,
195	) -> Result<Self, Error> {
196		let n_vars = equal_n_vars_check(&multilinears)?;
197		let multilinears = multilinears
198			.into_iter()
199			.map(|multilinear| SumcheckMultilinear::transparent(multilinear, &switchover_fn))
200			.collect();
201
202		Ok(Self {
203			n_vars,
204			eq_ind_partial_evals: None,
205			first_round_eval_1s: None,
206			multilinears,
207			backend,
208		})
209	}
210
211	/// Specify an existing tensor expansion for `eq_ind_challenges` in [`Self::build`]. Avoids duplicate work.
212	pub fn with_eq_ind_partial_evals(mut self, eq_ind_partial_evals: Backend::Vec<P>) -> Self {
213		self.eq_ind_partial_evals = Some(eq_ind_partial_evals);
214		self
215	}
216
217	/// Specify the value of round polynomial at 1 in the first round if it is available beforehand.
218	///
219	/// Prime example of this is GPA (grand product argument), where the value of the previous GKR layer
220	/// may be used as an advice to compute the round polynomial at 1 directly with less effort compared
221	/// to direct composite evaluation.
222	pub fn with_first_round_eval_1s(mut self, first_round_eval_1s: &[F]) -> Self {
223		self.first_round_eval_1s = Some(first_round_eval_1s.to_vec());
224		self
225	}
226
227	/// Specify the const suffixes for multilinears.
228	///
229	/// The provided array specifies the const suffixes at the end of each multilinear.
230	/// Prover is able to reduce multilinear storage and compute using this information.
231	pub fn with_const_suffixes(mut self, const_suffixes: &[(F, usize)]) -> Result<Self, Error> {
232		if const_suffixes.len() != self.multilinears.len() {
233			bail!(Error::IncorrectConstSuffixes);
234		}
235
236		for (multilinear, &const_suffix) in izip!(&mut self.multilinears, const_suffixes) {
237			let (_, suffix_len) = const_suffix;
238
239			if suffix_len > 1 << self.n_vars {
240				bail!(Error::IncorrectConstSuffixes);
241			}
242
243			multilinear.update_const_suffix(self.n_vars, const_suffix);
244		}
245
246		Ok(self)
247	}
248
249	#[instrument(skip_all, level = "debug", name = "EqIndSumcheckProverBuilder::build")]
250	pub fn build<FDomain, Composition>(
251		self,
252		evaluation_order: EvaluationOrder,
253		eq_ind_challenges: &[F],
254		composite_claims: impl IntoIterator<Item = CompositeSumClaim<F, Composition>>,
255		domain_factory: impl EvaluationDomainFactory<FDomain>,
256	) -> Result<EqIndSumcheckProver<'a, FDomain, P, Composition, M, Backend>, Error>
257	where
258		F: ExtensionField<FDomain>,
259		P: PackedExtension<FDomain>,
260		FDomain: Field,
261		Composition: CompositionPoly<P>,
262	{
263		let Self {
264			n_vars,
265			backend,
266			multilinears,
267			..
268		} = self;
269		let composite_claims = composite_claims.into_iter().collect::<Vec<_>>();
270
271		#[cfg(feature = "debug_validate_sumcheck")]
272		{
273			let composite_claims = composite_claims
274				.iter()
275				.map(|composite_claim| CompositeSumClaim {
276					composition: &composite_claim.composition,
277					sum: composite_claim.sum,
278				})
279				.collect::<Vec<_>>();
280			validate_witness(n_vars, &multilinears, eq_ind_challenges, composite_claims.clone())?;
281		}
282
283		if eq_ind_challenges.len() != n_vars {
284			bail!(Error::IncorrectEqIndChallengesLength);
285		}
286
287		// Only one value of the expanded equality indicator is used per each
288		// 1-variable subcube, thus it should be twice smaller.
289		let eq_ind_partial_evals = if let Some(eq_ind_partial_evals) = self.eq_ind_partial_evals {
290			if eq_ind_partial_evals.len() != 1 << n_vars.saturating_sub(P::LOG_WIDTH + 1) {
291				bail!(Error::IncorrectEqIndPartialEvalsSize);
292			}
293
294			eq_ind_partial_evals
295		} else {
296			eq_ind_expand(evaluation_order, eq_ind_challenges, backend)?
297		};
298
299		if let Some(ref first_round_eval_1s) = self.first_round_eval_1s {
300			if first_round_eval_1s.len() != composite_claims.len() {
301				bail!(Error::IncorrectFirstRoundEvalOnesLength);
302			}
303		}
304
305		for claim in &composite_claims {
306			if claim.composition.n_vars() != multilinears.len() {
307				bail!(Error::InvalidComposition {
308					expected: multilinears.len(),
309					actual: claim.composition.n_vars(),
310				});
311			}
312		}
313
314		let (compositions, claimed_sums) = determine_const_eval_suffixes(
315			composite_claims,
316			multilinears
317				.iter()
318				.map(|multilinear| multilinear.const_suffix(n_vars)),
319		);
320
321		let domains = interpolation_domains_for_composition_degrees(
322			domain_factory,
323			compositions
324				.iter()
325				.map(|(composition, _)| composition.degree()),
326		)?;
327
328		let nontrivial_evaluation_points = get_nontrivial_evaluation_points(&domains)?;
329
330		let state = ProverState::new(
331			evaluation_order,
332			n_vars,
333			multilinears,
334			claimed_sums,
335			nontrivial_evaluation_points,
336			backend,
337		)?;
338
339		let eq_ind_prefix_eval = F::ONE;
340		let eq_ind_challenges = eq_ind_challenges.to_vec();
341		let first_round_eval_1s = self.first_round_eval_1s;
342
343		Ok(EqIndSumcheckProver {
344			n_vars,
345			state,
346			eq_ind_prefix_eval,
347			eq_ind_partial_evals,
348			eq_ind_challenges,
349			compositions,
350			domains,
351			first_round_eval_1s,
352			backend: PhantomData,
353		})
354	}
355}
356
357#[derive(Default, PartialEq, Eq, Debug)]
358pub struct ConstEvalSuffix<F: Field> {
359	pub suffix: usize,
360	pub value: F,
361	pub value_at_inf: F,
362}
363
364impl<F: Field> ConstEvalSuffix<F> {
365	fn update(&mut self, evaluation_order: EvaluationOrder, n_vars: usize) {
366		let eval_prefix = (1 << n_vars) - self.suffix;
367		let updated_eval_prefix = match evaluation_order {
368			EvaluationOrder::LowToHigh => eval_prefix.div_ceil(2),
369			EvaluationOrder::HighToLow => eval_prefix.min(1 << (n_vars - 1)),
370		};
371		self.suffix = (1 << (n_vars - 1)) - updated_eval_prefix;
372	}
373}
374
375#[derive(Debug, Getters)]
376pub struct EqIndSumcheckProver<'a, FDomain, P, Composition, M, Backend>
377where
378	FDomain: Field,
379	P: PackedField,
380	M: MultilinearPoly<P> + Send + Sync,
381	Backend: ComputationBackend,
382{
383	n_vars: usize,
384	state: ProverState<'a, FDomain, P, M, Backend>,
385	eq_ind_prefix_eval: P::Scalar,
386	eq_ind_partial_evals: Backend::Vec<P>,
387	eq_ind_challenges: Vec<P::Scalar>,
388	#[getset(get = "pub")]
389	compositions: Vec<(Composition, ConstEvalSuffix<P::Scalar>)>,
390	domains: Vec<InterpolationDomain<FDomain>>,
391	first_round_eval_1s: Option<Vec<P::Scalar>>,
392	backend: PhantomData<Backend>,
393}
394
395impl<F, FDomain, P, Composition, M, Backend>
396	EqIndSumcheckProver<'_, FDomain, P, Composition, M, Backend>
397where
398	F: TowerField + ExtensionField<FDomain>,
399	FDomain: Field,
400	P: PackedExtension<FDomain, Scalar = F>,
401	Composition: CompositionPoly<P>,
402	M: MultilinearPoly<P> + Send + Sync,
403	Backend: ComputationBackend,
404{
405	fn round(&self) -> usize {
406		self.n_vars - self.n_rounds_remaining()
407	}
408
409	fn n_rounds_remaining(&self) -> usize {
410		self.state.n_vars()
411	}
412
413	fn eq_ind_round_challenge(&self) -> F {
414		match self.state.evaluation_order() {
415			EvaluationOrder::LowToHigh => self.eq_ind_challenges[self.round()],
416			EvaluationOrder::HighToLow => {
417				self.eq_ind_challenges[self.eq_ind_challenges.len() - 1 - self.round()]
418			}
419		}
420	}
421
422	fn update_eq_ind_prefix_eval(&mut self, challenge: F) {
423		// Update the running eq ind evaluation.
424		self.eq_ind_prefix_eval *= eq(self.eq_ind_round_challenge(), challenge);
425	}
426}
427
428pub fn eq_ind_expand<P, Backend>(
429	evaluation_order: EvaluationOrder,
430	eq_ind_challenges: &[P::Scalar],
431	backend: &Backend,
432) -> Result<Backend::Vec<P>, HalError>
433where
434	P: PackedField,
435	Backend: ComputationBackend,
436{
437	let n_vars = eq_ind_challenges.len();
438	backend.tensor_product_full_query(match evaluation_order {
439		EvaluationOrder::LowToHigh => &eq_ind_challenges[n_vars.min(1)..],
440		EvaluationOrder::HighToLow => &eq_ind_challenges[..n_vars.saturating_sub(1)],
441	})
442}
443
444type CompositionsAndSums<F, Composition> = (Vec<(Composition, ConstEvalSuffix<F>)>, Vec<F>);
445
446// Automatically determine trace suffix which evaluates to constant polynomials during sumcheck.
447//
448// Algorithm outline:
449//  * sort multilinears by non-increasing const suffix length
450//  * processing multilinears in this order, symbolically substitute suffix eval for the current variable and optimize
451//  * if the remaning expressions at finite points and Karatsuba infinity are constant, assume this suffix
452fn determine_const_eval_suffixes<F, P, Composition>(
453	composite_claims: Vec<CompositeSumClaim<F, Composition>>,
454	const_suffixes: impl IntoIterator<Item = (F, usize)>,
455) -> CompositionsAndSums<F, Composition>
456where
457	F: Field,
458	P: PackedField<Scalar = F>,
459	Composition: CompositionPoly<P>,
460{
461	let mut const_suffixes = const_suffixes.into_iter().enumerate().collect::<Vec<_>>();
462
463	const_suffixes.sort_by_key(|&(_var, (_suffix_eval, suffix_len))| Reverse(suffix_len));
464
465	composite_claims
466		.into_iter()
467		.map(|claim| {
468			let CompositeSumClaim { composition, sum } = claim;
469			assert_eq!(const_suffixes.len(), composition.n_vars());
470
471			let mut const_eval_suffix = Default::default();
472
473			let mut expr = composition.expression();
474			let mut expr_at_inf = composition.expression().leading_term();
475
476			for &(var_index, (suffix_eval, suffix)) in &const_suffixes {
477				expr = expr.const_subst(var_index, suffix_eval).optimize();
478				// NB: infinity point has a different interpolation result; in characteristic 2, it's always zero.
479				expr_at_inf = expr_at_inf
480					.const_subst(var_index, suffix_eval + suffix_eval)
481					.optimize();
482
483				if let Some((value, value_at_inf)) =
484					expr.get_constant().zip(expr_at_inf.get_constant())
485				{
486					const_eval_suffix = ConstEvalSuffix {
487						suffix,
488						value,
489						value_at_inf,
490					};
491					break;
492				}
493			}
494
495			((composition, const_eval_suffix), sum)
496		})
497		.unzip::<_, _, Vec<_>, Vec<_>>()
498}
499
500impl<F, FDomain, P, Composition, M, Backend> SumcheckProver<F>
501	for EqIndSumcheckProver<'_, FDomain, P, Composition, M, Backend>
502where
503	F: TowerField + ExtensionField<FDomain>,
504	FDomain: Field,
505	P: PackedExtension<FDomain, Scalar = F>,
506	Composition: CompositionPoly<P>,
507	M: MultilinearPoly<P> + Send + Sync,
508	Backend: ComputationBackend,
509{
510	fn n_vars(&self) -> usize {
511		self.n_vars
512	}
513
514	fn evaluation_order(&self) -> EvaluationOrder {
515		self.state.evaluation_order()
516	}
517
518	fn execute(&mut self, batch_coeff: F) -> Result<RoundCoeffs<F>, Error> {
519		let round = self.round();
520		let n_rounds_remaining = self.n_rounds_remaining();
521
522		let alpha = self.eq_ind_round_challenge();
523		let eq_ind_partial_evals = &self.eq_ind_partial_evals;
524
525		let first_round_eval_1s = self.first_round_eval_1s.take();
526		let have_first_round_eval_1s = first_round_eval_1s.is_some();
527
528		let eq_ind_challenges = match self.state.evaluation_order() {
529			EvaluationOrder::LowToHigh => &self.eq_ind_challenges[self.n_vars.min(round + 1)..],
530			EvaluationOrder::HighToLow => {
531				&self.eq_ind_challenges[..self.n_vars.saturating_sub(round + 1)]
532			}
533		};
534
535		let evaluators = self
536			.compositions
537			.iter_mut()
538			.map(|(composition, const_eval_suffix)| {
539				let composition_at_infinity =
540					ArithCircuitPoly::new(composition.expression().leading_term());
541
542				const_eval_suffix.update(self.state.evaluation_order(), n_rounds_remaining);
543
544				Evaluator {
545					n_rounds_remaining,
546					composition,
547					composition_at_infinity,
548					have_first_round_eval_1s,
549					eq_ind_challenges,
550					eq_ind_partial_evals,
551					const_eval_suffix,
552				}
553			})
554			.collect::<Vec<_>>();
555
556		let interpolators = self
557			.domains
558			.iter()
559			.enumerate()
560			.map(|(index, interpolation_domain)| Interpolator {
561				interpolation_domain,
562				alpha,
563				first_round_eval_1: first_round_eval_1s
564					.as_ref()
565					.map(|first_round_eval_1s| first_round_eval_1s[index]),
566			})
567			.collect::<Vec<_>>();
568
569		let round_evals = self.state.calculate_round_evals(&evaluators)?;
570
571		let prime_coeffs = self.state.calculate_round_coeffs_from_evals(
572			&interpolators,
573			batch_coeff,
574			round_evals,
575		)?;
576
577		// Convert v' polynomial into v polynomial
578
579		// eq(X, α) = (1 − α) + (2 α − 1) X
580		// NB: In binary fields, this expression can be simplified to 1 + α + challenge.
581		let (prime_coeffs_scaled_by_constant_term, mut prime_coeffs_scaled_by_linear_term) =
582			if F::CHARACTERISTIC == 2 {
583				(prime_coeffs.clone() * (F::ONE + alpha), prime_coeffs)
584			} else {
585				(prime_coeffs.clone() * (F::ONE - alpha), prime_coeffs * (alpha.double() - F::ONE))
586			};
587
588		prime_coeffs_scaled_by_linear_term.0.insert(0, F::ZERO); // Multiply prime polynomial by X
589
590		let coeffs = (prime_coeffs_scaled_by_constant_term + &prime_coeffs_scaled_by_linear_term)
591			* self.eq_ind_prefix_eval;
592
593		Ok(coeffs)
594	}
595
596	#[instrument(skip_all, name = "EqIndSumcheckProver::fold", level = "debug")]
597	fn fold(&mut self, challenge: F) -> Result<(), Error> {
598		self.update_eq_ind_prefix_eval(challenge);
599
600		let evaluation_order = self.state.evaluation_order();
601		let n_rounds_remaining = self.n_rounds_remaining();
602
603		let Self {
604			state,
605			eq_ind_partial_evals,
606			..
607		} = self;
608
609		binius_maybe_rayon::join(
610			|| state.fold(challenge),
611			|| {
612				fold_partial_eq_ind::<P, Backend>(
613					evaluation_order,
614					n_rounds_remaining - 1,
615					eq_ind_partial_evals,
616				);
617			},
618		)
619		.0?;
620		Ok(())
621	}
622
623	fn finish(self: Box<Self>) -> Result<Vec<F>, Error> {
624		let mut evals = self.state.finish()?;
625		evals.push(self.eq_ind_prefix_eval);
626		Ok(evals)
627	}
628}
629
630struct Evaluator<'a, P, Composition>
631where
632	P: PackedField,
633{
634	n_rounds_remaining: usize,
635	composition: &'a Composition,
636	composition_at_infinity: ArithCircuitPoly<P::Scalar>,
637	have_first_round_eval_1s: bool,
638	eq_ind_challenges: &'a [P::Scalar],
639	eq_ind_partial_evals: &'a [P],
640	const_eval_suffix: &'a ConstEvalSuffix<P::Scalar>,
641}
642
643impl<P, Composition> SumcheckEvaluator<P, Composition> for Evaluator<'_, P, Composition>
644where
645	P: PackedField<Scalar: TowerField>,
646	Composition: CompositionPoly<P>,
647{
648	fn eval_point_indices(&self) -> Range<usize> {
649		// Do not evaluate r(1) in first round when its value is known
650		let start_index = if self.have_first_round_eval_1s { 2 } else { 1 };
651		start_index..self.composition.degree() + 1
652	}
653
654	fn process_subcube_at_eval_point(
655		&self,
656		subcube_vars: usize,
657		subcube_index: usize,
658		is_infinity_point: bool,
659		batch_query: &RowsBatchRef<P>,
660	) -> P {
661		let row_len = batch_query.row_len();
662
663		stackalloc_with_default(row_len, |evals| {
664			if is_infinity_point {
665				self.composition_at_infinity
666					.batch_evaluate(batch_query, evals)
667					.expect("correct by query construction invariant");
668			} else {
669				self.composition
670					.batch_evaluate(batch_query, evals)
671					.expect("correct by query construction invariant");
672			};
673
674			let subcube_start = subcube_index << subcube_vars.saturating_sub(P::LOG_WIDTH);
675			for (i, eval) in evals.iter_mut().enumerate() {
676				// REVIEW: investigate whether its possible to access a subcube smaller than
677				//         the packing width and unaligned on the packed field binary; in that
678				//         case spread multiplication may be needed.
679				*eval *= self.eq_ind_partial_evals[subcube_start + i];
680			}
681			evals.iter().copied().sum::<P>()
682		})
683	}
684
685	fn process_constant_eval_suffix(
686		&self,
687		const_eval_suffix: usize,
688		is_infinity_point: bool,
689	) -> P::Scalar {
690		let eval_prefix = (1 << self.n_rounds_remaining) - const_eval_suffix;
691		let eq_ind_suffix_sum = StepUp::new(self.eq_ind_challenges.len(), eval_prefix)
692			.expect("eval_prefix does not exceed the equality indicator size")
693			.evaluate(self.eq_ind_challenges)
694			.expect("StepUp is initialized with eq_ind_challenges.len()");
695
696		eq_ind_suffix_sum
697			* if is_infinity_point {
698				self.const_eval_suffix.value_at_inf
699			} else {
700				self.const_eval_suffix.value
701			}
702	}
703
704	fn composition(&self) -> &Composition {
705		self.composition
706	}
707
708	fn eq_ind_partial_eval(&self) -> Option<&[P]> {
709		Some(self.eq_ind_partial_evals)
710	}
711
712	fn const_eval_suffix(&self) -> usize {
713		self.const_eval_suffix.suffix
714	}
715}
716
717struct Interpolator<'a, F, FDomain>
718where
719	F: Field,
720	FDomain: Field,
721{
722	interpolation_domain: &'a InterpolationDomain<FDomain>,
723	alpha: F,
724	first_round_eval_1: Option<F>,
725}
726
727impl<F, FDomain> SumcheckInterpolator<F> for Interpolator<'_, F, FDomain>
728where
729	F: ExtensionField<FDomain>,
730	FDomain: Field,
731{
732	#[instrument(
733		skip_all,
734		name = "eq_ind::Interpolator::round_evals_to_coeffs",
735		level = "debug"
736	)]
737	fn round_evals_to_coeffs(
738		&self,
739		last_round_sum: F,
740		mut round_evals: Vec<F>,
741	) -> Result<Vec<F>, PolynomialError> {
742		if let Some(first_round_eval_1) = self.first_round_eval_1 {
743			round_evals.insert(0, first_round_eval_1);
744		}
745
746		let one_evaluation = round_evals[0];
747		let zero_evaluation_numerator = last_round_sum - one_evaluation * self.alpha;
748		let zero_evaluation_denominator_inv = (F::ONE - self.alpha).invert_or_zero();
749		let zero_evaluation = zero_evaluation_numerator * zero_evaluation_denominator_inv;
750		round_evals.insert(0, zero_evaluation);
751
752		if round_evals.len() > 3 {
753			// SumcheckRoundCalculator orders interpolation points as 0, 1, "infinity", then subspace points.
754			// InterpolationDomain expects "infinity" at the last position, thus reordering is needed.
755			// Putting "special" evaluation points at the beginning of domain allows benefitting from
756			// faster/skipped interpolation even in case of mixed degree compositions .
757			let infinity_round_eval = round_evals.remove(2);
758			round_evals.push(infinity_round_eval);
759		}
760
761		Ok(self.interpolation_domain.interpolate(&round_evals)?)
762	}
763}