Skip to main content

binius_iop_prover/
logup_star.rs

1// Copyright 2026 The Binius Developers
2
3//! logUp* proving with the pushforwards committed as oracles.
4//!
5//! The bare reduction returns a claimed evaluation of each table's pushforward `Y = I_* eq_r`.
6//! It commits nothing, so those claims cannot be checked on their own.
7//! This layer commits one `Y` per table over the IOP channel and returns the relations that open
8//! them.
9//!
10//! The commits precede the reduction, so its logUp challenges bind the committed oracles.
11//! The tables `T` and indexes `I` stay the caller's oracles, so their claims are returned
12//! unchanged. The pushforwards are the only oracles this protocol introduces, per
13//! [Soukhanov25, Section 3].
14//!
15//! [`prove_transparent`] is the variant for tables the verifier evaluates itself.
16//! It opens each `Y` against both `eq_z` and the table, and so needs no pushforward sumcheck.
17//!
18//! [Soukhanov25]: <https://eprint.iacr.org/2025/946>
19
20use binius_compute::Allocator;
21use binius_field::{BinaryField, Divisible, PackedField};
22use binius_ip_prover::logup_star::{self as reduction, LogupTableOutput, witness};
23pub use binius_ip_prover::logup_star::{Looker, TableLookup};
24use binius_math::{FieldBuffer, multilinear::eq::eq_ind_partial_eval_in};
25use itertools::izip;
26
27use crate::channel::IOPProverChannel;
28
29/// The reduced claims of a committed logUp* proof.
30///
31/// The table and index claims are left for the caller to open against its own commitments.
32/// The pushforward claims are opened against the commitments sent here, through the channel.
33pub struct LogupProof<F> {
34	/// The point the table claims are drawn from, spanning the widest table.
35	///
36	/// A table over `m` variables is claimed at its **first `m`** coordinates.
37	pub table_eval_point: Vec<F>,
38	/// The point the index claims are drawn from, spanning the deepest looker.
39	///
40	/// A looker over `n` variables is claimed at its **last `n`** coordinates.
41	pub index_eval_point: Vec<F>,
42	/// One entry per table, in the order the tables were given.
43	pub tables: Vec<LogupTableOutput<F>>,
44}
45
46/// Prove a logUp* reduction whose pushforwards are committed as oracles.
47///
48/// This wraps [`binius_ip_prover::logup_star::prove_reduction`] with the pushforward commitments.
49/// It builds each table's pushforward `Y` once, commits it, then runs the reduction over those same
50/// buffers. Committing before the reduction binds every `Y` into the logUp challenges.
51///
52/// The relations `<Y, eq_r> = Y(r)` at each table's reduced point are opened through the channel,
53/// which may defer the actual openings to `finish()`.
54///
55/// One oracle per table is the simple arrangement; the pushforwards could instead be concatenated
56/// into a single oracle, which is left for later.
57///
58/// # Arguments
59///
60/// * `tables` - One [`binius_ip_prover::logup_star::TableLookup`] per table, by value: its
61///   multilinear and the lookers that read it.
62/// * `channel` - The IOP prover channel, whose next `tables.len()` oracles have message lengths
63///   `2^m` in table order.
64/// * `alloc` - The allocator the witnesses are drawn from.
65///
66/// # Preconditions
67///
68/// - `tables` is non-empty, each has at least one variable, and each has at least one looker.
69/// - Every index entry is less than the size of the table its looker reads.
70#[tracing::instrument(skip_all, level = "debug", name = "logup* (committed)")]
71pub fn prove<'a, F, P, Channel, A>(
72	tables: impl IntoIterator<Item = reduction::TableLookup<'a, P>>,
73	channel: &mut Channel,
74	alloc: &A,
75) -> LogupProof<F>
76where
77	F: BinaryField<Underlier: Divisible<u64>>,
78	P: PackedField<Scalar = F> + 'a,
79	Channel: IOPProverChannel<P, A>,
80	A: Allocator,
81{
82	// The tables are walked several times — to build the witnesses, to run the reduction, and to
83	// open the commitments — so the iterator is materialized once here.
84	let tables = tables.into_iter().collect::<Vec<_>>();
85
86	// Sample the looker batching challenge before the commitments: the prover needs gamma to build
87	// the pushforwards it commits.
88	//
89	//     gamma^i * eq_{r_i} = a table's scaled numerators
90	//     Y = the scatter of that table's numerators
91	let gamma = channel.sample();
92	let (numerators, pushforwards) = witness::combined_lookers::<A, F, P>(alloc, gamma, &tables);
93
94	// Commit every Y before the reduction, so the logUp challenges bind the commitments.
95	let oracles = tracing::debug_span!("Commit pushforwards").in_scope(|| {
96		pushforwards
97			.iter()
98			.map(|pushforward| channel.send_oracle(pushforward.as_view()))
99			.collect::<Vec<_>>()
100	});
101
102	// Run the reduction over the committed pushforwards and the numerators, viewing the channel as
103	// IP.
104	let pushforward_slices = pushforwards
105		.iter()
106		.map(FieldBuffer::as_view)
107		.collect::<Vec<_>>();
108	let output =
109		reduction::prove_reduction(alloc, gamma, &tables, numerators, &pushforward_slices, channel);
110
111	// Open each pushforward relation through the channel; a deferring channel (e.g. BaseFold)
112	// batches them with every other queued relation in `finish()`.
113	//
114	//     <Y, eq_r> = Y(r) = that table's pushforward claim
115	//
116	// A table's own point is the first m coordinates of the shared reduced point.
117	let _open_guard = tracing::debug_span!("Open pushforward relations").entered();
118	for (oracle, pushforward, table, table_output) in
119		izip!(oracles, pushforwards, &tables, &output.tables)
120	{
121		let m = table.table.log_len();
122		let transparent = eq_ind_partial_eval_in::<A, P>(alloc, &output.table_eval_point[..m]);
123		channel.prove_oracle_relation(
124			oracle.clone(),
125			transparent.into(),
126			table_output.pushforward_claim,
127		);
128		channel.finalize_oracle(oracle, pushforward);
129	}
130
131	LogupProof {
132		table_eval_point: output.table_eval_point,
133		index_eval_point: output.index_eval_point,
134		tables: output.tables,
135	}
136}
137
138/// The reduced claims of a committed logUp* proof over transparent tables.
139///
140/// Nothing is left on the tables or the pushforwards: both of a table's claims are opened here,
141/// against the one `Y` commitment. Only the index claims are the caller's to prove.
142pub struct LogupTransparentProof<F> {
143	/// The point the index claims are drawn from, spanning the deepest looker.
144	///
145	/// A looker over `n` variables is claimed at its **last `n`** coordinates.
146	pub index_eval_point: Vec<F>,
147	/// One entry per table, in the order the tables were given, holding that table's lookers'
148	/// index claims in its own looker order.
149	pub index_eval_claims: Vec<Vec<F>>,
150}
151
152/// Prove a logUp* reduction over transparent tables, with the pushforwards committed as oracles.
153///
154/// This wraps [`binius_ip_prover::logup_star::prove_reduction_transparent`] the way [`prove`] wraps
155/// the committed-table reduction. The reduction leaves two claims on each pushforward instead of
156/// one, and both are opened here through the channel:
157///
158/// ```text
159///     <Y, eq_z> = Y(z)      the fractional-addition leaf claim
160///     <Y, T>    = e         the product claim, against the transparent table itself
161/// ```
162///
163/// The two are queued in that order, matching
164/// [`binius_iop::logup_star::verify_transparent`](binius_iop::logup_star::verify_transparent).
165///
166/// # Arguments
167///
168/// The arguments of [`prove`]. The tables are transparent, so no oracle is committed for them;
169/// their multilinears are still needed, both by the reduction and by the product relation.
170///
171/// # Preconditions
172///
173/// The preconditions of [`prove`].
174#[tracing::instrument(skip_all, level = "debug", name = "logup* transparent (committed)")]
175pub fn prove_transparent<'a, F, P, Channel, A>(
176	tables: impl IntoIterator<Item = reduction::TableLookup<'a, P>>,
177	channel: &mut Channel,
178	alloc: &A,
179) -> LogupTransparentProof<F>
180where
181	F: BinaryField<Underlier: Divisible<u64>>,
182	P: PackedField<Scalar = F> + 'a,
183	Channel: IOPProverChannel<P, A>,
184	A: Allocator,
185{
186	let tables = tables.into_iter().collect::<Vec<_>>();
187
188	// Sample gamma, build the witnesses, then commit every Y before the reduction, exactly as
189	// [`prove`] does.
190	let gamma = channel.sample();
191	let (numerators, pushforwards) = witness::combined_lookers::<A, F, P>(alloc, gamma, &tables);
192	let oracles = tracing::debug_span!("Commit pushforwards").in_scope(|| {
193		pushforwards
194			.iter()
195			.map(|pushforward| channel.send_oracle(pushforward.as_view()))
196			.collect::<Vec<_>>()
197	});
198
199	let pushforward_slices = pushforwards
200		.iter()
201		.map(FieldBuffer::as_view)
202		.collect::<Vec<_>>();
203	let output = reduction::prove_reduction_transparent(
204		alloc,
205		gamma,
206		&tables,
207		numerators,
208		&pushforward_slices,
209		channel,
210	);
211
212	// Open both of a table's claims against its one pushforward commitment. The product relation
213	// weighs Y against a copy of the table drawn from the caller's allocator, since the channel
214	// owns its transparents until the opening runs.
215	let _open_guard = tracing::debug_span!("Open pushforward relations").entered();
216	for (oracle, pushforward, table, open) in izip!(oracles, pushforwards, &tables, &output.tables)
217	{
218		let leaf_eq = eq_ind_partial_eval_in::<A, P>(alloc, &open.pushforward_eval_point);
219		channel.prove_oracle_relation(oracle.clone(), leaf_eq.into(), open.pushforward_eval_claim);
220		channel.prove_oracle_relation(
221			oracle.clone(),
222			FieldBuffer::from_view_in(alloc, table.table).into(),
223			open.product_claim,
224		);
225		channel.finalize_oracle(oracle, pushforward);
226	}
227
228	LogupTransparentProof {
229		index_eval_point: output.index_eval_point,
230		index_eval_claims: output
231			.tables
232			.into_iter()
233			.map(|table| table.index_eval_claims)
234			.collect(),
235	}
236}
237
238#[cfg(test)]
239mod tests {
240	use std::iter;
241
242	use binius_compute::GlobalAllocator;
243	use binius_field::{
244		BinaryField1b, ExtensionField, Field, PackedGhash1x128b,
245		arch::{OptimalB128, OptimalPackedB128},
246	};
247	use binius_hash::{StdDigest, StdHashSuite};
248	use binius_iop::{
249		basefold::compiler::BaseFoldVerifierCompiler,
250		channel::{OracleSpec, naive::NaiveVerifierChannel},
251		fri::MinProofSizeStrategy,
252		logup_star::{self as verify_logup, TransparentTableLookup},
253		merkle_tree::BinaryMerkleTreeScheme,
254	};
255	use binius_ip::logup_star::LookerClaim;
256	use binius_math::{
257		FieldBuffer,
258		multilinear::{
259			eq::eq_ind_partial_eval_scalars,
260			evaluate::{evaluate, evaluate_inplace_scalars},
261		},
262		ntt::{NeighborsLastSingleThread, domain_context::GaoMateerOnTheFly},
263		test_utils::{random_field_buffer, random_scalars},
264	};
265	use binius_transcript::{ProverTranscript, fiat_shamir::HasherChallenger};
266	use rand::prelude::*;
267
268	use super::*;
269	use crate::{basefold::compiler::BaseFoldProverCompiler, channel::naive::NaiveProverChannel};
270
271	type F = OptimalB128;
272	type P = OptimalPackedB128;
273	type StdChallenger = HasherChallenger<sha2::Sha256>;
274	/// The commitment field is the GHASH 128-bit field; BaseFold uses a single-lane packing of it.
275	type BP = PackedGhash1x128b;
276	type Chal = HasherChallenger<StdDigest>;
277
278	// Embed a table position j into the field through the GF(2)-linear basis, as the protocol does.
279	//
280	//     iota(j) = sum_{t : bit t of j is set} basis(t)
281	fn iota<E: Field + ExtensionField<BinaryField1b>>(j: usize, m: usize) -> E {
282		(0..m)
283			.filter(|t| (j >> t) & 1 == 1)
284			.map(E::basis)
285			.fold(E::ZERO, |acc, b| acc + b)
286	}
287
288	/// One looker of a test instance: its column and its honest claim against its table.
289	struct TestLooker<E> {
290		index: Vec<usize>,
291		eval_point: Vec<E>,
292		eval_claim: E,
293	}
294
295	/// One table of a test instance: its values and the lookers that read it.
296	struct TestTable<E, Q: PackedField> {
297		values: FieldBuffer<Q>,
298		lookers: Vec<TestLooker<E>>,
299	}
300
301	// Draw a looker over `n` variables reading `table_values`, with its honest claim.
302	fn random_looker<E, Q>(
303		rng: &mut StdRng,
304		n: usize,
305		table_values: &FieldBuffer<Q>,
306	) -> TestLooker<E>
307	where
308		E: Field,
309		Q: PackedField<Scalar = E>,
310	{
311		let m = table_values.log_len();
312		let index = (0..(1usize << n))
313			.map(|_| rng.random_range(0..(1usize << m)))
314			.collect::<Vec<_>>();
315		let eval_point = random_scalars::<E>(&mut *rng, n);
316
317		// The looked-up evaluation: e = (I^* T)(r) = sum_i eq_r(i) * T[index[i]].
318		let eq_r = eq_ind_partial_eval_scalars(&eval_point);
319		let eval_claim = index
320			.iter()
321			.zip(&eq_r)
322			.map(|(&j, &eq)| eq * table_values.get(j))
323			.fold(E::ZERO, |acc, t| acc + t);
324
325		TestLooker {
326			index,
327			eval_point,
328			eval_claim,
329		}
330	}
331
332	// Build the instance named by `spec`: one entry per table, giving its variable count and the
333	// variable counts of the lookers that read it.
334	fn random_instance<E, Q>(spec: &[(usize, Vec<usize>)], seed: u64) -> Vec<TestTable<E, Q>>
335	where
336		E: Field,
337		Q: PackedField<Scalar = E>,
338	{
339		let mut rng = StdRng::seed_from_u64(seed);
340		spec.iter()
341			.map(|(m, looker_n_vars)| {
342				let values = random_field_buffer::<Q>(&mut rng, *m);
343				let lookers = looker_n_vars
344					.iter()
345					.map(|&n| random_looker::<E, Q>(&mut rng, n, &values))
346					.collect::<Vec<_>>();
347				TestTable { values, lookers }
348			})
349			.collect()
350	}
351
352	// Assert both sides agree and that every reduced claim is the honest evaluation. The
353	// pushforward claims are bound by the channel's own opening, so they need no assertion here.
354	fn check_proofs<Q>(
355		prover_proof: &LogupProof<F>,
356		verifier_proof: &verify_logup::LogupProof<F>,
357		tables: &[TestTable<F, Q>],
358		shape: &str,
359	) where
360		Q: PackedField<Scalar = F>,
361	{
362		assert_eq!(
363			prover_proof.table_eval_point, verifier_proof.table_eval_point,
364			"table point ({shape})"
365		);
366		assert_eq!(
367			prover_proof.index_eval_point, verifier_proof.index_eval_point,
368			"index point ({shape})"
369		);
370		assert_eq!(prover_proof.tables, verifier_proof.tables, "per-table claims ({shape})");
371
372		// A table's claim is at the first m coordinates of the shared table point.
373		let table_point = &prover_proof.table_eval_point;
374		for (table_index, table) in tables.iter().enumerate() {
375			let m = table.values.log_len();
376			assert_eq!(
377				prover_proof.tables[table_index].eval_claim,
378				evaluate(&table.values, &table_point[..m]),
379				"table claim wrong for table {table_index} ({shape})"
380			);
381		}
382
383		let claims_by_table = prover_proof
384			.tables
385			.iter()
386			.map(|table| table.index_eval_claims.clone())
387			.collect::<Vec<_>>();
388		check_index_claims(&prover_proof.index_eval_point, &claims_by_table, tables, shape);
389	}
390
391	// Check every looker's index claim, given the claims grouped by table.
392	//
393	// A looker's claim is at its own suffix of the shared index point.
394	fn check_index_claims<Q>(
395		index_point: &[F],
396		claims_by_table: &[Vec<F>],
397		tables: &[TestTable<F, Q>],
398		shape: &str,
399	) where
400		Q: PackedField<Scalar = F>,
401	{
402		for (table_index, (table, claims)) in iter::zip(tables, claims_by_table).enumerate() {
403			let m = table.values.log_len();
404			assert_eq!(claims.len(), table.lookers.len(), "claim count ({shape})");
405			for (looker, claim) in iter::zip(&table.lookers, claims) {
406				let embedded = looker
407					.index
408					.iter()
409					.map(|&j| iota::<F>(j, m))
410					.collect::<Vec<_>>();
411				let embedded = FieldBuffer::<P>::from_values(&embedded);
412				let own_point = &index_point[index_point.len() - looker.eval_point.len()..];
413				assert_eq!(
414					*claim,
415					evaluate(&embedded, own_point),
416					"index claim wrong for table {table_index}, n={} ({shape})",
417					looker.eval_point.len()
418				);
419			}
420		}
421	}
422
423	// Build the prover-side witnesses for an instance.
424	fn prover_tables<'a, Q>(tables: &'a [TestTable<F, Q>]) -> Vec<TableLookup<'a, Q>>
425	where
426		Q: PackedField<Scalar = F>,
427	{
428		tables
429			.iter()
430			.map(|table| TableLookup {
431				table: table.values.as_view(),
432				lookers: table
433					.lookers
434					.iter()
435					.map(|looker| Looker {
436						index: &looker.index,
437						eval_point: &looker.eval_point,
438						eval_claim: looker.eval_claim,
439					})
440					.collect(),
441			})
442			.collect()
443	}
444
445	// Build the verifier-side claims for an instance.
446	fn verifier_tables<'a, Q>(
447		tables: &'a [TestTable<F, Q>],
448	) -> Vec<binius_ip::logup_star::TableLookup<'a, F>>
449	where
450		Q: PackedField<Scalar = F>,
451	{
452		tables
453			.iter()
454			.map(|table| binius_ip::logup_star::TableLookup {
455				n_vars: table.values.log_len(),
456				lookers: table
457					.lookers
458					.iter()
459					.map(|looker| LookerClaim {
460						eval_point: &looker.eval_point,
461						eval_claim: looker.eval_claim,
462					})
463					.collect(),
464			})
465			.collect()
466	}
467
468	// Build the verifier-side claims for an instance whose tables are transparent.
469	//
470	// A test table is a random buffer, so its "succinct" MLE is just the evaluation of its values.
471	fn transparent_verifier_tables<'a, Q>(
472		tables: &'a [TestTable<F, Q>],
473	) -> Vec<TransparentTableLookup<'a, F>>
474	where
475		Q: PackedField<Scalar = F>,
476	{
477		iter::zip(verifier_tables(tables), tables)
478			.map(|(lookup, table)| {
479				let values = table.values.iter_scalars().collect::<Vec<_>>();
480				TransparentTableLookup {
481					lookup,
482					table_eval: Box::new(move |point: &[F]| {
483						evaluate_inplace_scalars(values.clone(), point)
484					}),
485				}
486			})
487			.collect()
488	}
489
490	/// Round-trip a whole instance over the naive channel and check every reduced claim.
491	fn check_prove_verify(spec: &[(usize, Vec<usize>)], seed: u64) {
492		let tables = random_instance::<F, P>(spec, seed);
493		let shape = format!("{spec:?}");
494
495		// One oracle per table: pushforward Y, of message length 2^m, in table order.
496		let specs = tables
497			.iter()
498			.map(|table| OracleSpec::new(table.values.log_len()))
499			.collect::<Vec<_>>();
500
501		// Prove: commit every Y, run the reduction, then open them as the caller would.
502		let mut prover_transcript = ProverTranscript::new(StdChallenger::default());
503		let mut prover_channel =
504			NaiveProverChannel::<F, _>::new(&mut prover_transcript, specs.clone());
505		let prover_proof =
506			prove::<F, P, _, _>(prover_tables(&tables), &mut prover_channel, &GlobalAllocator);
507		prover_channel.finish();
508
509		// Verify: receive every Y, run the reduction, then open the pushforward relations.
510		let mut verifier_transcript = prover_transcript.into_verifier();
511		let mut verifier_channel =
512			NaiveVerifierChannel::<F, _>::new(&mut verifier_transcript, &specs);
513		let verifier_proof = verify_logup::verify(verifier_tables(&tables), &mut verifier_channel)
514			.expect("verification succeeds");
515		verifier_channel.finish();
516
517		check_proofs(&prover_proof, &verifier_proof, &tables, &shape);
518	}
519
520	/// Round-trip a transparent-table instance over the naive channel.
521	///
522	/// The channel checks both relations on every pushforward, so a passing run already proves the
523	/// two claims the reduction left open. Only the index claims need checking here.
524	fn check_prove_verify_transparent(spec: &[(usize, Vec<usize>)], seed: u64) {
525		let tables = random_instance::<F, P>(spec, seed);
526		let shape = format!("{spec:?}");
527
528		// One oracle per table still: the pushforward Y. A transparent table commits nothing.
529		let specs = tables
530			.iter()
531			.map(|table| OracleSpec::new(table.values.log_len()))
532			.collect::<Vec<_>>();
533
534		let mut prover_transcript = ProverTranscript::new(StdChallenger::default());
535		let mut prover_channel =
536			NaiveProverChannel::<F, _>::new(&mut prover_transcript, specs.clone());
537		let prover_proof = prove_transparent::<F, P, _, _>(
538			prover_tables(&tables),
539			&mut prover_channel,
540			&GlobalAllocator,
541		);
542		prover_channel.finish();
543
544		let mut verifier_transcript = prover_transcript.into_verifier();
545		let mut verifier_channel =
546			NaiveVerifierChannel::<F, _>::new(&mut verifier_transcript, &specs);
547		let verifier_proof = verify_logup::verify_transparent(
548			transparent_verifier_tables(&tables),
549			&mut verifier_channel,
550		)
551		.expect("verification succeeds");
552		verifier_channel.finish();
553
554		assert_eq!(
555			prover_proof.index_eval_point, verifier_proof.index_eval_point,
556			"index point ({shape})"
557		);
558		assert_eq!(
559			prover_proof.index_eval_claims, verifier_proof.index_eval_claims,
560			"index claims ({shape})"
561		);
562		check_index_claims(
563			&prover_proof.index_eval_point,
564			&prover_proof.index_eval_claims,
565			&tables,
566			&shape,
567		);
568	}
569
570	#[test]
571	fn test_prove_verify_round_trip() {
572		// A spread of shapes: m << n (the target regime), m == n, and a wide table.
573		for (n, m) in [(6, 2), (5, 3), (4, 4), (3, 5), (7, 1)] {
574			check_prove_verify(&[(m, vec![n])], 0);
575		}
576	}
577
578	#[test]
579	fn test_multi_looker_committed_round_trip() {
580		// Several lookers sharing one committed pushforward.
581		check_prove_verify(&[(3, vec![5, 5])], 13);
582	}
583
584	#[test]
585	fn test_prove_verify_single_table_variable() {
586		// m = 1 exercises the batched final layer with an empty layer-1 point.
587		check_prove_verify(&[(1, vec![4])], 1);
588	}
589
590	#[test]
591	fn test_multi_table_committed_round_trip() {
592		// One oracle per table, received in table order. The shapes mix table sizes, looker
593		// lengths, and how many lookers each table has, and put the deepest instance on either
594		// side.
595		for spec in [
596			vec![(3usize, vec![5usize, 3usize]), (2, vec![2, 6])],
597			vec![(4, vec![1]), (2, vec![3]), (5, vec![2])],
598			vec![(2, vec![4, 4, 2]), (3, vec![5])],
599			vec![(1, vec![0]), (4, vec![3])],
600		] {
601			check_prove_verify(&spec, 23);
602		}
603	}
604
605	#[test]
606	fn test_prove_verify_transparent_round_trip() {
607		// The transparent variant shares every step but the last, so one spread covers it: m << n,
608		// m == n, a wide table, a one-variable table, several lookers on one pushforward, and
609		// several tables with the deepest instance on either side.
610		for spec in [
611			vec![(2usize, vec![6usize])],
612			vec![(4, vec![4])],
613			vec![(5, vec![3])],
614			vec![(1, vec![4])],
615			vec![(3, vec![5, 5])],
616			vec![(3, vec![5, 3]), (2, vec![2, 6])],
617			vec![(4, vec![1]), (2, vec![3]), (5, vec![2])],
618		] {
619			check_prove_verify_transparent(&spec, 31);
620		}
621	}
622
623	#[test]
624	fn test_verifier_rejects_wrong_eval_claim() {
625		let mut tables = random_instance::<F, P>(&[(3, vec![5])], 3);
626		let specs = vec![OracleSpec::new(3)];
627
628		// Prove a false statement by perturbing the looked-up evaluation.
629		tables[0].lookers[0].eval_claim += F::ONE;
630
631		let mut prover_transcript = ProverTranscript::new(StdChallenger::default());
632		let mut prover_channel =
633			NaiveProverChannel::<F, _>::new(&mut prover_transcript, specs.clone());
634		let _prover_proof =
635			prove::<F, P, _, _>(prover_tables(&tables), &mut prover_channel, &GlobalAllocator);
636		prover_channel.finish();
637
638		// The reduction's product check must surface the inconsistency as a verification failure.
639		let mut verifier_transcript = prover_transcript.into_verifier();
640		let mut verifier_channel =
641			NaiveVerifierChannel::<F, _>::new(&mut verifier_transcript, &specs);
642		let result = verify_logup::verify(verifier_tables(&tables), &mut verifier_channel);
643		assert!(result.is_err(), "verifier must reject a wrong eval claim");
644	}
645
646	// Run a transparent-table instance over the real BaseFold channel.
647	//
648	// One witness-dependent (ZK) oracle per table — the pushforward Y, with 2^m entries — each
649	// carrying two relations that the channel folds together before its single FRI opening. The
650	// verifier's `finish` is folded into the result, so a caller can tell a rejected opening from
651	// an accepted one.
652	fn run_basefold_transparent(
653		tables: &[TestTable<F, BP>],
654	) -> Result<
655		(LogupTransparentProof<F>, verify_logup::LogupTransparentProof<F>),
656		verify_logup::Error,
657	> {
658		const LOG_INV_RATE: usize = 1;
659		const SECURITY_BITS: usize = 32;
660		let n_test_queries = SECURITY_BITS.div_ceil(LOG_INV_RATE);
661		let oracle_specs = tables
662			.iter()
663			.map(|table| OracleSpec::new_zk(table.values.log_len()))
664			.collect::<Vec<_>>();
665
666		let verifier_compiler = BaseFoldVerifierCompiler::new(
667			&BinaryMerkleTreeScheme::<F, StdHashSuite>::new(),
668			oracle_specs,
669			LOG_INV_RATE,
670			n_test_queries,
671			&MinProofSizeStrategy,
672		);
673
674		// Prove: commit the pushforwards with real FRI, run the reduction, open both relations.
675		let domain_context = GaoMateerOnTheFly::generate(verifier_compiler.max_log_domain_size());
676		let ntt = NeighborsLastSingleThread::new(domain_context);
677		let prover_compiler =
678			BaseFoldProverCompiler::<BP, _>::from_verifier_compiler(&verifier_compiler, ntt);
679
680		let mut prover_transcript = ProverTranscript::new(Chal::default());
681		let mut prover_channel = prover_compiler
682			.create_channel_from_transcript::<StdHashSuite, Chal, _, _>(
683				&mut prover_transcript,
684				StdRng::seed_from_u64(8),
685				GlobalAllocator,
686			);
687
688		let alloc = GlobalAllocator;
689		let prover_proof =
690			prove_transparent::<F, BP, _, _>(prover_tables(tables), &mut prover_channel, &alloc);
691		prover_channel.finish();
692
693		// Verify: receive the pushforwards, run the reduction, open both relations for real.
694		let mut verifier_transcript = prover_transcript.into_verifier();
695		let mut verifier_channel = verifier_compiler
696			.create_channel_from_transcript::<StdHashSuite, Chal, _>(&mut verifier_transcript);
697		let verifier_proof = verify_logup::verify_transparent(
698			transparent_verifier_tables(tables),
699			&mut verifier_channel,
700		)?;
701		verifier_channel.finish()?;
702
703		Ok((prover_proof, verifier_proof))
704	}
705
706	#[test]
707	fn test_basefold_round_trip() {
708		// Two tables of different sizes, so the real FRI path carries one masked oracle per table
709		// and each is opened at its own prefix of the reduced point.
710		let spec = [(2usize, vec![6usize, 2usize]), (4, vec![3])];
711		let tables = random_instance::<F, BP>(&spec, 7);
712
713		const LOG_INV_RATE: usize = 1;
714		const SECURITY_BITS: usize = 32;
715		let n_test_queries = SECURITY_BITS.div_ceil(LOG_INV_RATE);
716		let oracle_specs = tables
717			.iter()
718			.map(|table| OracleSpec::new_zk(table.values.log_len()))
719			.collect::<Vec<_>>();
720
721		let verifier_compiler = BaseFoldVerifierCompiler::new(
722			&BinaryMerkleTreeScheme::<F, StdHashSuite>::new(),
723			oracle_specs,
724			LOG_INV_RATE,
725			n_test_queries,
726			&MinProofSizeStrategy,
727		);
728
729		// Prove: commit the pushforwards with real FRI, run the reduction, open them.
730		let domain_context = GaoMateerOnTheFly::generate(verifier_compiler.max_log_domain_size());
731		let ntt = NeighborsLastSingleThread::new(domain_context);
732		let prover_compiler =
733			BaseFoldProverCompiler::<BP, _>::from_verifier_compiler(&verifier_compiler, ntt);
734
735		let mut prover_transcript = ProverTranscript::new(Chal::default());
736		let prover_channel_rng = StdRng::seed_from_u64(8);
737		let mut prover_channel = prover_compiler
738			.create_channel_from_transcript::<StdHashSuite, Chal, _, _>(
739				&mut prover_transcript,
740				prover_channel_rng,
741				GlobalAllocator,
742			);
743
744		let alloc = GlobalAllocator;
745		let prover_proof =
746			prove::<F, BP, _, _>(prover_tables(&tables), &mut prover_channel, &alloc);
747		prover_channel.finish();
748
749		// Verify: receive the pushforwards, run the reduction, open them through the real FRI
750		// check.
751		let mut verifier_transcript = prover_transcript.into_verifier();
752		let mut verifier_channel = verifier_compiler
753			.create_channel_from_transcript::<StdHashSuite, Chal, _>(&mut verifier_transcript);
754		let verifier_proof = verify_logup::verify(verifier_tables(&tables), &mut verifier_channel)
755			.expect("verification succeeds");
756		verifier_channel
757			.finish()
758			.expect("the batched FRI openings verify");
759
760		// The FRI openings already bound every Y to its claim.
761		// Cross-check the table and index claims against honest values.
762		check_proofs(&prover_proof, &verifier_proof, &tables, "basefold");
763	}
764
765	#[test]
766	fn test_basefold_transparent_round_trip() {
767		// Two tables of different sizes, so each pushforward oracle carries its own pair of
768		// relations into the one folded FRI opening.
769		let tables = random_instance::<F, BP>(&[(2usize, vec![6usize, 2usize]), (4, vec![3])], 7);
770
771		let (prover_proof, verifier_proof) =
772			run_basefold_transparent(&tables).expect("the batched FRI openings verify");
773
774		assert_eq!(prover_proof.index_eval_point, verifier_proof.index_eval_point, "index point");
775		assert_eq!(
776			prover_proof.index_eval_claims, verifier_proof.index_eval_claims,
777			"index claims"
778		);
779		check_index_claims(
780			&prover_proof.index_eval_point,
781			&prover_proof.index_eval_claims,
782			&tables,
783			"basefold transparent",
784		);
785	}
786
787	#[test]
788	fn test_basefold_transparent_rejects_wrong_eval_claim() {
789		// The transparent reduction hands the product claim back unchecked, so a perturbed
790		// looked-up evaluation survives it. Opening <Y, T> = e is what must reject.
791		let mut tables = random_instance::<F, BP>(&[(3usize, vec![5usize])], 3);
792		tables[0].lookers[0].eval_claim += F::ONE;
793
794		assert!(
795			run_basefold_transparent(&tables).is_err(),
796			"the opening must reject a wrong eval claim"
797		);
798	}
799}