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}