Skip to main content

binius_ip_prover/sumcheck/
multilinear_eval.rs

1// Copyright 2026 Irreducible Inc.
2// Copyright 2026 The Binius Developers
3
4use std::iter;
5
6use binius_compute::Allocator;
7use binius_field::{Field, PackedField, WideMul};
8use binius_ip::sumcheck::RoundCoeffs;
9use binius_math::{FieldSlice, FieldVec};
10
11use super::{
12	mle_store::{ColId, EvaluationChunk, MleStore, RoundContext},
13	round_evals::RoundEvals,
14	round_evaluator::{MleCheckRoundEvaluator, SharedMleCheckProver},
15};
16
17/// MLE-check round evaluator for the multilinear evaluation of one store column.
18///
19/// The composition is the identity.
20/// Each round polynomial is therefore degree 1, with a single sampled evaluation.
21/// That evaluation is the inner product of the column's `X = 1` half with the round's eq chunk.
22/// The driving [`SharedMleCheckProver`] supplies that chunk.
23/// It folds the higher eq coordinates through its own reduction step.
24/// No full-width eq tensor is ever materialized, streamed, or folded.
25pub struct MultilinearEvalEvaluator {
26	col: ColId,
27}
28
29impl MultilinearEvalEvaluator {
30	/// Creates an evaluator over the store column `col`.
31	pub const fn new(col: ColId) -> Self {
32		Self { col }
33	}
34}
35
36impl<F, P> MleCheckRoundEvaluator<F, P> for MultilinearEvalEvaluator
37where
38	F: Field,
39	P: PackedField<Scalar = F>,
40{
41	fn degree(&self) -> usize {
42		// Identity composition: the round polynomial is degree 1.
43		// One sampled evaluation suffices.
44		1
45	}
46
47	fn accumulate(
48		&self,
49		chunk: &EvaluationChunk<'_, P>,
50		eq_ind: FieldSlice<'_, P>,
51		accum: &mut [<P as WideMul>::Output],
52	) {
53		// The column arrives split on the round's highest variable.
54		// Its high half is the specialization at `X = 1`.
55		let hi = chunk.col(self.col).hi.as_ref();
56		let eq_ind = eq_ind.as_ref();
57
58		// The two run in lockstep, so pairing them checks the length once per chunk.
59		// Indexing one by the other's position instead consults a bound on every element.
60		assert_eq!(hi.len(), eq_ind.len());
61
62		// R(1) = <M(.., X = 1), eq(.., z)> over this chunk.
63		// Only the eq multiply is widened.
64		// The wide accumulator is reduced once at the end of the chunk.
65		let mut y_1 = <P as WideMul>::Output::default();
66		for (&m_i, &eq_i) in iter::zip(hi, eq_ind) {
67			y_1 += P::wide_mul(m_i, eq_i);
68		}
69		RoundEvals([y_1]).add_to(accum);
70	}
71
72	fn interpolate(
73		&self,
74		ctx: &RoundContext<'_, P>,
75		accum: &[P],
76		claim: F,
77		alpha: F,
78	) -> RoundCoeffs<F> {
79		// The store has not folded this round yet.
80		// Its remaining-variable count is therefore this round's.
81		let n_vars_remaining = ctx.n_vars();
82		assert!(n_vars_remaining > 0);
83
84		// `accum` is already reduced by the prover's map pass.
85		// Sum its lanes, then interpolate.
86		// `claim` is this round's prime evaluation.
87		// `alpha` is the eq coordinate that ties it to the point.
88		RoundEvals::<P, 1>::from_slots(accum)
89			.sum_scalars(n_vars_remaining)
90			.interpolate_eq(claim, alpha)
91	}
92}
93
94/// Builds an MLE-check prover for the multilinear extension evaluation of a single multilinear.
95///
96/// The claim is `M(z) = s` for a multilinear `M` over the challenge field.
97/// This proves the equivalent MLE-check relation `s = sum_{v in B_n} M(v) * eq(v, z)`.
98/// Since `M` is multilinear, that relation holds if and only if `M(z) = s`.
99///
100/// The reduction runs on the split-eq [`SharedMleCheckProver`] with a degree-1 evaluator.
101/// Each round expands only a small low-coordinate prefix of the eq indicator.
102/// The higher coordinates are folded in through the prover's reduction step.
103/// The full `2^{n-1}` eq tensor is never materialized, streamed, or folded per round.
104///
105/// # Arguments
106///
107/// * `witness` - the multilinear whose extension is evaluated.
108/// * `eval_point` - the point of the evaluation claim.
109/// * `eval_claim` - the claimed value of the multilinear extension at that point.
110///
111/// # Panics
112///
113/// Panics if the witness length does not match the evaluation point length.
114pub fn multilinear_eval_prover<'alloc, A, F, P>(
115	alloc: &'alloc A,
116	witness: FieldVec<P, A>,
117	eval_point: &[F],
118	eval_claim: F,
119) -> SharedMleCheckProver<'alloc, A, F, P, MultilinearEvalEvaluator>
120where
121	A: Allocator,
122	F: Field,
123	P: PackedField<Scalar = F>,
124{
125	assert_eq!(
126		witness.log_len(),
127		eval_point.len(),
128		"witness must have number of variables equal to the evaluation point length"
129	);
130
131	// The store owns the witness as its single column.
132	let mut store = MleStore::new(eval_point.len(), alloc);
133	let col = store.push_owned(witness);
134	let evaluator = MultilinearEvalEvaluator::new(col);
135	SharedMleCheckProver::new(store, [(eval_claim, evaluator)], eval_point.to_vec())
136}
137
138#[cfg(test)]
139mod tests {
140	use binius_compute::GlobalAllocator;
141	use binius_field::{
142		FieldOps, Random,
143		arch::{OptimalB128, OptimalPackedB128},
144	};
145	use binius_ip::mlecheck;
146	use binius_math::{
147		multilinear::evaluate::evaluate,
148		test_utils::{random_field_buffer, random_scalars},
149	};
150	use binius_transcript::{ProverTranscript, fiat_shamir::HasherChallenger};
151	use rand::prelude::*;
152
153	use super::*;
154	use crate::sumcheck::{
155		common::MleCheckProver, prove_single_mlecheck,
156		quadratic_mle_evaluator::quadratic_mlecheck_prover,
157	};
158
159	type F = OptimalB128;
160	type P = OptimalPackedB128;
161	type StdChallenger = HasherChallenger<sha2::Sha256>;
162
163	// A quadratic MLE-check with the identity composition and a zero infinity composition is a
164	// degree-1 MLE-check over a single multilinear.
165	// That is exactly the single-multilinear evaluation reduction under test, with an always-zero
166	// degree-2 coefficient tacked on.
167	// Drive both in lockstep and compare round polynomials and final evaluations.
168	#[test]
169	fn test_conformance_with_quadratic_mlecheck() {
170		let mut rng = StdRng::seed_from_u64(0);
171		let n_vars = 8;
172		let alloc = GlobalAllocator;
173
174		let witness = random_field_buffer::<P>(&mut rng, n_vars);
175		let eval_point = random_scalars::<F>(&mut rng, n_vars);
176		let eval_claim = evaluate(&witness, &eval_point);
177
178		let mut eval_prover =
179			multilinear_eval_prover(&alloc, witness.clone(), &eval_point, eval_claim);
180		let mut quadratic_prover = quadratic_mlecheck_prover(
181			&alloc,
182			[witness],
183			|[a]: [P; 1]| a,
184			|[_a]: [P; 1]| P::zero(),
185			eval_point,
186			eval_claim,
187		);
188
189		for _ in 0..n_vars {
190			let eval_round = eval_prover.execute();
191			let mut quadratic_round = quadratic_prover.execute();
192			assert_eq!(eval_round.len(), 1);
193			assert_eq!(quadratic_round.len(), 1);
194
195			// The quadratic prover sizes its round polynomial for degree 2; the leading coefficient
196			// is zero because the composition is multilinear.
197			assert_eq!(quadratic_round[0].0.pop(), Some(F::ZERO));
198			assert_eq!(eval_round[0], quadratic_round[0]);
199
200			let challenge = F::random(&mut rng);
201			eval_prover.fold(challenge);
202			quadratic_prover.fold(challenge);
203		}
204
205		assert_eq!(eval_prover.finish(), quadratic_prover.finish());
206	}
207
208	// Full prove/verify roundtrip through the MLE-check protocol.
209	#[test]
210	fn test_prove_verify_roundtrip() {
211		let mut rng = StdRng::seed_from_u64(1);
212		let n_vars = 7;
213		let alloc = GlobalAllocator;
214
215		let witness = random_field_buffer::<P>(&mut rng, n_vars);
216		let eval_point = random_scalars::<F>(&mut rng, n_vars);
217		let eval_claim = evaluate(&witness, &eval_point);
218
219		let prover = multilinear_eval_prover(&alloc, witness.clone(), &eval_point, eval_claim);
220
221		let mut prover_transcript = ProverTranscript::new(StdChallenger::default());
222		let output = prove_single_mlecheck(prover, &mut prover_transcript);
223		prover_transcript
224			.message()
225			.write_slice(&output.multilinear_evals);
226
227		let mut verifier_transcript = prover_transcript.into_verifier();
228		let sumcheck_output =
229			mlecheck::verify::<F, _>(&eval_point, 1, eval_claim, &mut verifier_transcript).unwrap();
230		let multilinear_evals: Vec<F> = verifier_transcript.message().read_vec(1).unwrap();
231
232		assert_eq!(output.challenges, sumcheck_output.challenges);
233
234		// The reduced MLE-check evaluation is the witness multilinear at the challenge point.
235		assert_eq!(multilinear_evals[0], sumcheck_output.eval);
236
237		let mut reduced_point = sumcheck_output.challenges;
238		reduced_point.reverse();
239		assert_eq!(evaluate(&witness, &reduced_point), multilinear_evals[0]);
240	}
241}