Skip to main content

binius_iop/
logup_star.rs

1// Copyright 2026 The Binius Developers
2
3//! logUp* verification 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 never binds them to commitments, so those claims cannot be checked on their own.
7//! This layer receives one `Y` oracle per table over the IOP channel and returns the relations that
8//! open them.
9//!
10//! The receives precede the reduction, so its logUp challenges bind the received 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//! [`verify_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_field::{BinaryField1b, ExtensionField, Field};
21use binius_ip::logup_star as reduction;
22use binius_math::multilinear::eq::eq_ind;
23use itertools::izip;
24
25use crate::channel::{Error as ChannelError, IOPVerifierChannel, TransparentEvalFn};
26
27/// An error raised while verifying a committed logUp* reduction.
28#[derive(Debug, thiserror::Error)]
29pub enum Error {
30	/// The underlying logUp* reduction failed.
31	#[error("logUp* reduction error: {0}")]
32	Reduction(#[from] reduction::Error),
33	/// Receiving the pushforward oracle commitment failed.
34	#[error("IOP channel error: {0}")]
35	Channel(#[from] ChannelError),
36}
37
38/// The reduced claims of a committed logUp* verification.
39///
40/// The table and index claims are left for the caller to open against its own commitments.
41/// The pushforward claims are opened against the commitments received here, through the channel.
42pub struct LogupProof<Elem> {
43	/// The point the table claims are drawn from, spanning the widest table.
44	///
45	/// A table over `m` variables is claimed at its **first `m`** coordinates.
46	pub table_eval_point: Vec<Elem>,
47	/// The point the index claims are drawn from, spanning the deepest looker.
48	///
49	/// A looker over `n` variables is claimed at its **last `n`** coordinates.
50	pub index_eval_point: Vec<Elem>,
51	/// One entry per table, in the order the tables were given.
52	pub tables: Vec<reduction::LogupTableOutput<Elem>>,
53}
54
55/// Verify a logUp* reduction whose pushforwards are committed as oracles.
56///
57/// This wraps [`binius_ip::logup_star::verify_reduction`] with the pushforward commitments. The
58/// looker batching challenge is sampled first — the prover needs it to build the pushforwards —
59/// then one `Y` oracle is received per table, in table order, before the reduction, so the logUp
60/// challenges bind every commitment. The relations `<Y, eq_r> = Y(r)` at each table's reduced point
61/// are opened through the channel, which may defer the actual openings to `finish()`.
62///
63/// One oracle per table is the simple arrangement; the pushforwards could instead be concatenated
64/// into a single oracle, which is left for later.
65///
66/// # Arguments
67///
68/// * `tables` - One [`binius_ip::logup_star::TableLookup`] per table, by value. The lookers'
69///   evaluation points may differ in length, from each other and from the table.
70/// * `channel` - The IOP verifier channel carrying the `Y` commitments.
71///
72/// # Errors
73///
74/// Returns an error when a pushforward commitment is missing or the reduction identity fails.
75pub fn verify<'a, F, C>(
76	tables: impl IntoIterator<Item = reduction::TableLookup<'a, C::Elem>>,
77	channel: &mut C,
78) -> Result<LogupProof<C::Elem>, Error>
79where
80	F: Field + ExtensionField<BinaryField1b>,
81	C: IOPVerifierChannel<F>,
82	C::Elem: From<F> + 'a,
83{
84	// The tables are walked twice — once to receive their commitments, once to open them — so the
85	// iterator is materialized once here.
86	let tables = tables.into_iter().collect::<Vec<_>>();
87	// Only the variable counts are needed after the reduction consumes the tables.
88	let table_n_vars = tables.iter().map(|table| table.n_vars).collect::<Vec<_>>();
89
90	// Sample the looker batching challenge before the commitments: the prover needs gamma to build
91	// the pushforwards it commits.
92	let gamma = channel.sample();
93
94	// Receive the pushforward commitments next, so the reduction's logUp challenges bind them.
95	//
96	//     Y for a table over m variables has 2^m entries, so its message length is m.
97	//     Y is witness-dependent (it scatters the numerators by the secret indexes), so it may be
98	//     masked.
99	let oracles = table_n_vars
100		.iter()
101		.map(|&n_vars| channel.recv_oracle(n_vars, true))
102		.collect::<Result<Vec<_>, _>>()?;
103
104	// Run the bare reduction over the same channel, viewed as an IP channel.
105	let output = reduction::verify_reduction::<F, C>(&gamma, tables, channel)?;
106
107	// Open each pushforward relation through the channel; a deferring channel (e.g. BaseFold)
108	// batches them with every other queued relation in `finish()`.
109	//
110	//     <Y, eq_r> = Y(r) = that table's pushforward claim
111	//
112	// BaseFold reduces each inner product to a challenge point, where the transparent is eq(r, .).
113	// A table's own point is the first m coordinates of the shared reduced point.
114	for (oracle, &n_vars, table_output) in izip!(oracles, &table_n_vars, &output.tables) {
115		let point = output.table_eval_point[..n_vars].to_vec();
116		channel.verify_oracle_relation(
117			oracle,
118			Box::new(move |challenge: &[C::Elem]| eq_ind(&point, challenge)),
119			table_output.pushforward_claim.clone(),
120		)?;
121	}
122
123	Ok(LogupProof {
124		table_eval_point: output.table_eval_point,
125		index_eval_point: output.index_eval_point,
126		tables: output.tables,
127	})
128}
129
130/// One transparent table, the lookers that read it, and the closure evaluating its MLE.
131pub struct TransparentTableLookup<'a, Elem> {
132	/// The table's variable count and the claims of the lookers reading it.
133	pub lookup: reduction::TableLookup<'a, Elem>,
134	/// Evaluates the table's multilinear extension at a point of `lookup.n_vars` coordinates.
135	pub table_eval: TransparentEvalFn<Elem>,
136}
137
138/// The reduced claims of a committed logUp* verification 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 verify.
142pub struct LogupTransparentProof<Elem> {
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<Elem>,
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<Elem>>,
150}
151
152/// Verify a logUp* reduction over transparent tables, with the pushforwards committed as oracles.
153///
154/// This wraps [`binius_ip::logup_star::verify_reduction_transparent`] the way [`verify`] wraps the
155/// committed-table reduction. The reduction leaves two claims on each pushforward instead of one,
156/// 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 caller's transparent table
161/// ```
162///
163/// The two are queued in that order, so the prover must queue them the same way.
164/// A channel that batches an oracle's relations folds them into one opening.
165///
166/// # Arguments
167///
168/// * `tables` - One [`TransparentTableLookup`] per table, by value.
169/// * `channel` - The IOP verifier channel carrying the `Y` commitments.
170///
171/// # Errors
172///
173/// Returns an error when a pushforward commitment is missing or the reduction identity fails.
174pub fn verify_transparent<'a, F, C>(
175	tables: impl IntoIterator<Item = TransparentTableLookup<'a, C::Elem>>,
176	channel: &mut C,
177) -> Result<LogupTransparentProof<C::Elem>, Error>
178where
179	F: Field + ExtensionField<BinaryField1b>,
180	C: IOPVerifierChannel<F>,
181	C::Elem: From<F> + 'a,
182{
183	// The reduction consumes the lookups, so the transparent closures are split off up front.
184	let (lookups, table_evals): (Vec<_>, Vec<_>) = tables
185		.into_iter()
186		.map(|table| (table.lookup, table.table_eval))
187		.unzip();
188	let table_n_vars = lookups.iter().map(|table| table.n_vars).collect::<Vec<_>>();
189
190	// Sample gamma, then receive the commitments, exactly as [`verify`] does: the prover needs
191	// gamma to build the pushforwards, and the reduction's logUp challenges must bind them.
192	let gamma = channel.sample();
193	let oracles = table_n_vars
194		.iter()
195		.map(|&n_vars| channel.recv_oracle(n_vars, true))
196		.collect::<Result<Vec<_>, _>>()?;
197
198	let output = reduction::verify_reduction_transparent::<F, C>(&gamma, lookups, channel)?;
199
200	// Open both of a table's claims against its one pushforward commitment. The table side never
201	// reaches the caller: the product relation weighs Y directly against the transparent T.
202	for (oracle, table_eval, table) in izip!(oracles, table_evals, &output.tables) {
203		let point = table.pushforward_eval_point.clone();
204		channel.verify_oracle_relation(
205			oracle.clone(),
206			Box::new(move |challenge: &[C::Elem]| eq_ind(&point, challenge)),
207			table.pushforward_eval_claim.clone(),
208		)?;
209		channel.verify_oracle_relation(oracle, table_eval, table.product_claim.clone())?;
210	}
211
212	Ok(LogupTransparentProof {
213		index_eval_point: output.index_eval_point,
214		index_eval_claims: output
215			.tables
216			.into_iter()
217			.map(|table| table.index_eval_claims)
218			.collect(),
219	})
220}