Skip to main content

binius_iop_prover/basefold/
opening.rs

1// Copyright 2025 Irreducible Inc.
2// Copyright 2026 The Binius Developers
3
4//! The core BaseFold opening protocol on the prover side.
5
6use std::ops::Deref;
7
8use binius_compute::Allocator;
9use binius_field::{BinaryField, PackedField};
10use binius_ip::mlecheck;
11use binius_ip_prover::sumcheck::{
12	common::MleCheckProver, multilinear_eval::multilinear_eval_prover,
13};
14use binius_math::{FieldVec, ntt::AdditiveNTT};
15
16use crate::{fri::FRIFoldProver, merkle_channel::MerkleIPProverChannel};
17
18/// Proves a *combined* multilinear evaluation claim `𝛑(eval_point) = eval_claim` by interleaving a
19/// single multilinear-evaluation MLE-check with a single combined FRI over the
20/// piecewise-concatenated oracle of the Batched ZK BaseFold construction (whitepaper §7.2 /
21/// §sec:batched-basefold Step 2).
22///
23/// A prior batched sumcheck reduced the `k` masked opening claims to per-oracle point-evaluation
24/// claims `π_i'(ρ_i) = α_i` at a shared point `r ∈ K^𝐧` (`𝐧 = max_i n_i`). The caller has collapsed
25/// the oracle-index variables up front at sampled batching challenges `r'` into a single combined
26/// multilinear `𝛑(X) = Σ_i e[i]·π_i^↑(X)`, `e` the indicator expanded at `r'` (passed as
27/// `witness`), with target `s' = 𝛑(r)`. Here we run the degree-1 MLE-check on `𝛑` against `r`,
28/// interleaved with the FRI codeword built (via [`FRIFoldProver::new_batch`]) from the `k`
29/// committed interleaved `[π_i ‖ ω_i]` codewords.
30///
31/// ## Arguments
32///
33/// * `witness` - the combined oracle multilinear `𝛑` with `log_len = 𝐧`
34/// * `eval_point` - the point `r` with `len = 𝐧`, in low-to-high variable order
35/// * `eval_claim` - the combined target `s' = 𝛑(r)`
36/// * `batch_challenge` - the masking challenge `γ`; folds each interleaved `[π_i ‖ ω_i]` codeword
37///   down to the codeword of `π_i'` in the FRI inner (unbatch) round
38/// * `outer_challenges` - the batching challenges `r'` (`len = log_n_oracles`); combine the `k`
39///   lifted codewords in the FRI outer (oracle-combine) rounds
40/// * `fri_folder` - the combined FRI fold prover, with `n_rounds == 𝐧 + 1 + log_n_oracles`
41/// * `channel` - the Merkle channel carrying all prover interaction: round coefficients,
42///   challenges, commitments, and query openings
43///
44/// The final FRI value equals the final MLE-check value.
45/// The verifier asserts that equality when it runs the matching opening.
46#[allow(clippy::too_many_arguments)]
47pub fn prove_mlecheck_basefold<A, F, P, NTT, Channel, Data>(
48	witness: FieldVec<P, A>,
49	eval_point: &[F],
50	eval_claim: F,
51	batch_challenge: Option<F>,
52	outer_challenges: &[F],
53	mut fri_folder: FRIFoldProver<'_, F, P, NTT, Channel::Commitment, Data>,
54	channel: &mut Channel,
55	alloc: &A,
56) where
57	A: Allocator,
58	F: BinaryField,
59	P: PackedField<Scalar = F>,
60	NTT: AdditiveNTT<Field = F> + Sync,
61	Channel: MerkleIPProverChannel<F>,
62	Data: Deref<Target = [P]>,
63{
64	let _scope = tracing::debug_span!("Basefold MLE-check ZK (batched)").entered();
65
66	let n_vars = witness.log_len();
67	assert_eq!(eval_point.len(), n_vars);
68	// The FRI folder has `n_inner` inner (unbatch) rounds — one for the shared mask challenge γ
69	// when any ZK oracle is present, none otherwise — `log_n_oracles` outer (oracle-combine)
70	// rounds, and `𝐧` standard fold rounds.
71	let n_inner = usize::from(batch_challenge.is_some());
72	assert_eq!(n_vars + n_inner + outer_challenges.len(), fri_folder.n_rounds());
73
74	// Inner (unbatch) round: fold every interleaved (π_i ‖ ω_i) ZK codeword at the masking
75	// challenge.
76	if let Some(gamma) = batch_challenge {
77		fri_folder.receive_challenge(gamma);
78	}
79	// Outer rounds: combine the k lifted codewords at the batching challenges r'. These carry no
80	// sumcheck round-polynomial; the folder applies them lazily at the first commit round. The
81	// FirstFold consumes its accumulated challenges as `[early ++ outer ++ later]`, so feeding the
82	// outer challenges here (right after γ, before any standard round) lands them in the outer
83	// window; the leading standard rounds then supply each non-ZK oracle's later batch fold.
84	for &outer_challenge in outer_challenges {
85		fri_folder.receive_challenge(outer_challenge);
86	}
87
88	let mut sumcheck = multilinear_eval_prover(alloc, witness, eval_point, eval_claim);
89	for _ in 0..n_vars {
90		let mut round_coeffs_vec = sumcheck.execute();
91		let round_coeffs = round_coeffs_vec
92			.pop()
93			.expect("the multilinear-evaluation prover proves exactly one claim");
94
95		// Send the round coefficients first: on a commitment round, `execute_fold_round` commits
96		// the folded codeword and writes its root, which must land after the coefficients.
97		channel.send_many(mlecheck::RoundProof::truncate(round_coeffs).coeffs());
98		fri_folder.execute_fold_round(channel);
99
100		let challenge = channel.sample();
101		sumcheck.fold(challenge);
102		fri_folder.receive_challenge(challenge);
103	}
104
105	fri_folder.execute_fold_round(channel);
106	fri_folder.finish_proof(channel);
107}
108
109#[cfg(test)]
110mod test {
111	use anyhow::Result;
112	use binius_compute::GlobalAllocator;
113	use binius_field::{BinaryField, PackedField, PackedGhash1x128b};
114	use binius_hash::{StdDigest, StdHashSuite};
115	use binius_iop::{
116		basefold as verifier_basefold,
117		channel::OracleSpec,
118		merkle_channel::{MerkleIPVerifierChannel, VerifierMerkleTranscriptChannel},
119	};
120	use binius_ip::channel::IPVerifierChannel;
121	use binius_ip_prover::channel::IPProverChannel;
122	use binius_math::{
123		FieldBuffer,
124		inner_product::inner_product_buffers,
125		line::extrapolate_line,
126		multilinear::eq::eq_ind_partial_eval,
127		ntt::{NeighborsLastSingleThread, domain_context::GaoMateerOnTheFly},
128		test_utils::{random_field_buffer, random_scalars},
129	};
130	use binius_transcript::{ProverTranscript, fiat_shamir::HasherChallenger};
131	use binius_utils::rayon::prelude::*;
132	use rand::{SeedableRng, rngs::StdRng};
133
134	use super::prove_mlecheck_basefold;
135	use crate::{
136		fri::{self, FRIFoldProver, MaskedCodeword},
137		merkle_channel::{MerkleIPProverChannel, ProverMerkleTranscriptChannel},
138		merkle_tree::prover::BinaryMerkleTreeProver,
139	};
140
141	type StdChallenger = HasherChallenger<StdDigest>;
142
143	pub const LOG_INV_RATE: usize = 1;
144
145	/// Drives [`prove_mlecheck_basefold`] against
146	/// [`binius_iop::basefold::verify_mlecheck_basefold`] for a single oracle (`k = 1`, no
147	/// outer rounds): commits the interleaved (π ‖ ω) codeword, samples the masking challenge γ,
148	/// forms π' = (1-γ)π + γω, and proves/verifies the point-evaluation claim π'(ρ) via the
149	/// combined FRI path. If `tamper`, the claim is corrupted and verification must fail. (The
150	/// multi-oracle path is exercised end-to-end by the channel tests.)
151	fn run_mlecheck_basefold_zk_prove_and_verify<F, P>(
152		witness: &FieldBuffer<P>,
153		evaluation_point: &[F],
154		tamper: bool,
155	) -> Result<()>
156	where
157		F: BinaryField,
158		P: PackedField<Scalar = F>,
159	{
160		let n_vars = evaluation_point.len();
161		assert_eq!(witness.log_len(), n_vars);
162
163		let merkle_prover = BinaryMerkleTreeProver::<F, StdHashSuite>::new();
164
165		let domain_context = GaoMateerOnTheFly::generate(n_vars + 1 + LOG_INV_RATE);
166		let ntt = NeighborsLastSingleThread::new(domain_context);
167
168		// For a single oracle the combined opening params (`optimal_for_batch`) also satisfy
169		// `encode_masked`'s preconditions (`log_batch_size() == 1`, `rs_code().log_dim() ==
170		// n_vars`).
171		let (fri_params, _) = binius_iop::fri::FRIParams::optimal_for_batch(
172			merkle_prover.scheme(),
173			&[OracleSpec::new_zk(n_vars)],
174			LOG_INV_RATE,
175			32,
176		);
177
178		// Encode the interleaved (witness ‖ mask), generating the mask internally, and commit the
179		// codeword over the Merkle channel.
180		let mut commit_rng = StdRng::seed_from_u64(7);
181		let MaskedCodeword { codeword, mask } = fri::encode_masked(
182			&fri_params,
183			0,
184			&ntt,
185			witness.as_view(),
186			&mut commit_rng,
187			&GlobalAllocator,
188		);
189
190		let mut prover_transcript = ProverTranscript::new(StdChallenger::default());
191		let mut prover_channel =
192			ProverMerkleTranscriptChannel::<_, StdChallenger, _, StdHashSuite>::with_merkle_prover(
193				&mut prover_transcript,
194				merkle_prover,
195			);
196		let codeword_commitment = prover_channel.send_merkle_commitment(codeword.as_view(), 2);
197
198		// Sample the masking challenge γ and form π' = (1-γ)·witness + γ·mask.
199		let batch_challenge: F = IPProverChannel::sample(&mut prover_channel);
200		let mut witness_prime = witness.clone();
201		let gamma_broadcast = P::broadcast(batch_challenge);
202		(witness_prime.as_mut(), mask.as_ref())
203			.into_par_iter()
204			.for_each(|(w, &m)| {
205				*w = extrapolate_line(*w, m, gamma_broadcast);
206			});
207
208		let eval_point_eq = eq_ind_partial_eval::<P>(evaluation_point);
209		let mut eval_claim = inner_product_buffers(&witness_prime, &eval_point_eq);
210		if tamper {
211			eval_claim += F::ONE;
212		}
213
214		let fri_folder =
215			FRIFoldProver::new_batch(&fri_params, &ntt, vec![(codeword, codeword_commitment)]);
216		prove_mlecheck_basefold(
217			witness_prime,
218			evaluation_point,
219			eval_claim,
220			Some(batch_challenge),
221			&[],
222			fri_folder,
223			&mut prover_channel,
224			&GlobalAllocator,
225		);
226		// Hand the transcript back, releasing the channel's borrow of it.
227		prover_channel.into_transcript();
228
229		let mut verifier_transcript = prover_transcript.into_verifier();
230		let mut verifier_channel =
231			VerifierMerkleTranscriptChannel::<_, StdChallenger, _, StdHashSuite>::new(
232				&mut verifier_transcript,
233			);
234		// The committed codeword has one interleaved (π ‖ ω) coset of 2 scalars per leaf.
235		let retrieved_commitment =
236			verifier_channel.recv_merkle_commitment(2, n_vars + LOG_INV_RATE)?;
237		let batch_challenge_v: F = IPVerifierChannel::sample(&mut verifier_channel);
238
239		// The verifier asserts FRI/MLE-check consistency internally.
240		// A tampered proof therefore surfaces here as an error.
241		verifier_basefold::verify_mlecheck_basefold(
242			&fri_params,
243			&[retrieved_commitment],
244			eval_claim,
245			evaluation_point,
246			Some(batch_challenge_v),
247			&[],
248			&mut verifier_channel,
249		)?;
250
251		Ok(())
252	}
253
254	#[test]
255	fn test_mlecheck_basefold_zk_valid_proof() {
256		type P = PackedGhash1x128b;
257
258		let n_vars = 8;
259		let mut rng = StdRng::seed_from_u64(0);
260		let witness = random_field_buffer::<P>(&mut rng, n_vars);
261		let evaluation_point = random_scalars(&mut rng, n_vars);
262
263		run_mlecheck_basefold_zk_prove_and_verify::<_, P>(&witness, &evaluation_point, false)
264			.unwrap();
265	}
266
267	#[test]
268	fn test_mlecheck_basefold_zk_invalid_proof() {
269		type P = PackedGhash1x128b;
270
271		let n_vars = 8;
272		let mut rng = StdRng::seed_from_u64(0);
273		let witness = random_field_buffer::<P>(&mut rng, n_vars);
274		let evaluation_point = random_scalars(&mut rng, n_vars);
275
276		let result =
277			run_mlecheck_basefold_zk_prove_and_verify::<_, P>(&witness, &evaluation_point, true);
278		assert!(result.is_err());
279	}
280}