Skip to main content

binius_ip_prover/sumcheck/
mle_to_sumcheck.rs

1// Copyright 2025 Irreducible Inc.
2// Copyright 2026 The Binius Developers
3
4use binius_field::{Field, PackedField, WideMul};
5use binius_ip::sumcheck::RoundCoeffs;
6use binius_math::multilinear::eq::eq_one_var;
7
8use crate::sumcheck::{
9	common::{MleCheckProver, SumcheckProver},
10	mle_store::{EqId, EvaluationChunk, RoundContext},
11	round_evaluator::{MleCheckRoundEvaluator, SumcheckRoundEvaluator},
12};
13
14/// Adaptor that exposes a `SumcheckProver` interface for an internal `MleCheckProver`.
15///
16/// This struct implements the technique from [Gruen24] to convert an MLE-check protocol
17/// into a standard sumcheck protocol. The key insight is that the MLE-check claim
18/// $\sum_{v \in \{0,1\}^n} F(v) \cdot \text{eq}(v, z) = s$ can be rewritten as a sumcheck
19/// claim by multiplying in the equality polynomial term-by-term during the protocol execution.
20///
21/// In each round, the adaptor multiplies the round polynomials from the inner MLE-check
22/// prover by a linear polynomial term $(X - \alpha)$ where $\alpha$ is the corresponding
23/// coordinate of the evaluation point. This effectively transforms the MLE-check round
24/// polynomial into a sumcheck round polynomial that includes the equality check.
25///
26/// The `eq_prefix_eval` field accumulates the product of all previously factored equality
27/// terms, ensuring the round polynomials maintain the correct scaling throughout the protocol.
28///
29/// [Gruen24]: <https://eprint.iacr.org/2024/108>
30#[derive(Debug, Clone)]
31pub struct MleToSumCheckDecorator<F: Field, InnerProver> {
32	mlecheck_prover: InnerProver,
33	eq_prefix_eval: F,
34}
35
36impl<F: Field, InnerProver: MleCheckProver<F>> MleToSumCheckDecorator<F, InnerProver> {
37	pub const fn new(mlecheck_prover: InnerProver) -> Self {
38		Self {
39			mlecheck_prover,
40			eq_prefix_eval: F::ONE,
41		}
42	}
43}
44
45impl<F: Field, InnerProver: MleCheckProver<F>> SumcheckProver<F>
46	for MleToSumCheckDecorator<F, InnerProver>
47{
48	fn n_vars(&self) -> usize {
49		self.mlecheck_prover.n_vars()
50	}
51
52	fn execute(&mut self) -> Vec<RoundCoeffs<F>> {
53		let round_coeffs_multi = self.mlecheck_prover.execute();
54
55		// Multiply the round polynomials from the inner MLE-check prover by (X - α).
56		let alpha = self.mlecheck_prover.eval_point()[self.n_vars() - 1];
57		round_coeffs_multi
58			.into_iter()
59			.map(|round_coeffs| round_coeffs.mul_by_eq(alpha) * self.eq_prefix_eval)
60			.collect()
61	}
62
63	fn fold(&mut self, challenge: F) {
64		assert_ne!(self.n_vars(), 0, "fold called out of order; expected finish");
65
66		let alpha = self.mlecheck_prover.eval_point()[self.n_vars() - 1];
67		self.eq_prefix_eval *= eq_one_var(challenge, alpha);
68
69		self.mlecheck_prover.fold(challenge);
70	}
71
72	fn finish(self) -> Vec<F> {
73		self.mlecheck_prover.finish()
74	}
75}
76
77/// Adaptor that turns an [`MleCheckRoundEvaluator`] into a [`SumcheckRoundEvaluator`].
78///
79/// This is the evaluator-level mirror of [`MleToSumCheckDecorator`], applying the same [Gruen24]
80/// technique: each round, the inner evaluator's prime round polynomials are multiplied by the
81/// linear equality term in the bound coordinate and by the accumulated equality prefix. It lets
82/// eq-weighted and plain claims live in one evaluator group behind a single
83/// [`SharedSumcheckProver`].
84///
85/// Unlike a bare [`MleCheckRoundEvaluator`], whose driving [`SharedMleCheckProver`] owns the eq
86/// tracker, this wrapper runs under a [`SharedSumcheckProver`] that knows nothing of eq indicators.
87/// So the wrapper itself holds the [`EqId`] of the shared evaluation point's eq tracker, and
88/// supplies the round's eq chunk to [`MleCheckRoundEvaluator::accumulate`] and its alpha and
89/// equality prefix to [`MleCheckRoundEvaluator::interpolate`] — all read from that tracker, which
90/// the store folds in lockstep, so the wrapper keeps no copy of the point and no equality
91/// bookkeeping of its own.
92///
93/// [`SharedSumcheckProver`]: super::round_evaluator::SharedSumcheckProver
94/// [`SharedMleCheckProver`]: super::round_evaluator::SharedMleCheckProver
95/// [Gruen24]: <https://eprint.iacr.org/2024/108>
96pub struct MleToSumCheckEvaluator<Inner> {
97	inner: Inner,
98	// The shared eq tracker for the inner MLE-check evaluation point; the store maintains its
99	// current alpha and equality prefix.
100	eq_tracker: EqId,
101}
102
103impl<Inner> MleToSumCheckEvaluator<Inner> {
104	/// Wraps an MLE-check evaluator whose claims share the point of eq tracker `eq_tracker`.
105	pub const fn new(inner: Inner, eq_tracker: EqId) -> Self {
106		Self { inner, eq_tracker }
107	}
108}
109
110impl<F, P, Inner> SumcheckRoundEvaluator<F, P> for MleToSumCheckEvaluator<Inner>
111where
112	F: Field,
113	P: PackedField<Scalar = F>,
114	Inner: MleCheckRoundEvaluator<F, P>,
115{
116	fn degree(&self) -> usize {
117		// The eq factor multiplies the emitted round polynomial, not the accumulator: the wide
118		// slots still hold the inner evaluator's prime-polynomial evaluations.
119		self.inner.degree()
120	}
121
122	fn accumulate(&self, chunk: &EvaluationChunk<'_, P>, accum: &mut [<P as WideMul>::Output]) {
123		self.inner
124			.accumulate(chunk, chunk.eq(self.eq_tracker).as_view(), accum);
125	}
126
127	fn interpolate(&self, ctx: &RoundContext<'_, P>, accum: &[P], claim: F) -> RoundCoeffs<F> {
128		// `claim` is the sumcheck round claim: the inner MLE-check claim `m` scaled by the
129		// accumulated equality prefix. Recover `m` for the inner evaluator by dividing the prefix
130		// back out. (`invert_or_zero` mirrors the interpolation routines; the prefix is a product
131		// of non-degenerate equality factors for honest challenges.)
132		//
133		// alpha and the equality prefix are read from the shared eq tracker (the store has not yet
134		// folded this round, so the tracker is at the current round's alpha and prefix).
135		let eq_prefix = ctx.eq_prefix(self.eq_tracker);
136		let alpha = ctx.eq_alpha(self.eq_tracker);
137		let inner_claim = claim * eq_prefix.invert_or_zero();
138		// The inner evaluator interpolates its prime polynomial from the same reduced slots.
139		let round_coeffs = self.inner.interpolate(ctx, accum, inner_claim, alpha);
140
141		// Multiply the inner MLE-check round polynomial by (X - α) and the equality prefix.
142		round_coeffs.mul_by_eq(alpha) * eq_prefix
143	}
144}