binius_iop/channel/mod.rs
1// Copyright 2026 The Binius Developers
2
3//! Channel abstraction for interactive oracle protocol (IOP) verifiers.
4
5pub mod grinding;
6pub mod merge;
7pub mod naive;
8pub mod oracle_setup;
9pub mod size_tracking;
10
11use std::iter;
12
13use binius_field::Field;
14use binius_ip::channel::IPVerifierChannel;
15use binius_utils::checked_arithmetics::log2_ceil_usize;
16
17use crate::basefold;
18
19/// Error type for IOP verifier channel operations.
20#[derive(Debug, thiserror::Error)]
21pub enum Error {
22 #[error("proof is empty")]
23 ProofEmpty,
24 #[error("BaseFold verification failed: {0}")]
25 BaseFold(#[from] basefold::Error),
26 #[error("IP channel error: {0}")]
27 IPChannel(#[from] binius_ip::channel::Error),
28 #[error("sumcheck error: {0}")]
29 Sumcheck(#[from] binius_ip::sumcheck::Error),
30 #[error("Merkle channel error: {0}")]
31 Merkle(#[from] crate::merkle_channel::Error),
32}
33
34/// Specification for an oracle to be committed in the IOP.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct OracleSpec {
37 /// Log2 of the message length (number of field elements).
38 pub log_msg_len: usize,
39 /// Whether the oracle is committed with zero-knowledge (hiding) masking.
40 ///
41 /// ZK oracles interleave the message with a fresh mask and are folded by a shared masking
42 /// challenge γ in the batched BaseFold opening; non-ZK oracles are committed without a mask.
43 pub is_zk: bool,
44}
45
46impl OracleSpec {
47 /// A non-ZK (unmasked) oracle of the given message length.
48 pub const fn new(log_msg_len: usize) -> Self {
49 Self {
50 log_msg_len,
51 is_zk: false,
52 }
53 }
54
55 /// A ZK (masked, hiding) oracle of the given message length.
56 pub const fn new_zk(log_msg_len: usize) -> Self {
57 Self {
58 log_msg_len,
59 is_zk: true,
60 }
61 }
62}
63
64/// The length of the one oracle a round's oracles are committed as.
65///
66/// The oracles lie end to end, so this is the smallest power of two covering their total.
67fn merged_log_msg_len(log_msg_lens: impl IntoIterator<Item = usize>) -> usize {
68 let total_len: usize = log_msg_lens.into_iter().map(|n| 1usize << n).sum();
69 log2_ceil_usize(total_len)
70}
71
72/// Every oracle an IOP commits, grouped into the rounds they are committed in.
73///
74/// A round is the run of oracles sent between two challenge samples.
75///
76/// A challenge can only be derived once the commitments before it are absorbed.
77///
78/// So a round closes the moment its challenge is drawn, and takes no further members.
79///
80/// ```text
81/// recv, recv, sample, recv, sample, recv, recv, recv
82/// \__________/ \__/ \______________/
83/// round 0 round 1 round 2
84/// ```
85///
86/// A flat spec list cannot say where those boundaries fall.
87///
88/// A caller that commits a whole round as one oracle needs them.
89#[derive(Debug, Default, Clone, PartialEq, Eq)]
90pub struct OracleSchedule {
91 /// Every oracle, in arrival order, across all rounds.
92 specs: Vec<OracleSpec>,
93
94 /// The exclusive end of each closed round, as an index into `specs`.
95 ///
96 /// Round `r` spans `specs[ends[r - 1]..ends[r]]`, and round `0` starts at `0`.
97 ///
98 /// Anything past the last entry is in the round still open.
99 ends: Vec<usize>,
100}
101
102impl OracleSchedule {
103 /// Creates an empty schedule.
104 pub const fn new() -> Self {
105 Self {
106 specs: Vec::new(),
107 ends: Vec::new(),
108 }
109 }
110
111 /// Appends one oracle to the round currently open.
112 pub fn push(&mut self, spec: OracleSpec) {
113 self.specs.push(spec);
114 }
115
116 /// Closes the round currently open, so later oracles start a new one.
117 ///
118 /// Does nothing when no oracle has arrived since the last close.
119 ///
120 /// Calling it wherever a challenge could be drawn is therefore always safe.
121 pub fn end_round(&mut self) {
122 let open_start = self.ends.last().copied().unwrap_or(0);
123 if self.specs.len() > open_start {
124 self.ends.push(self.specs.len());
125 }
126 }
127
128 /// Every oracle in the schedule, in arrival order, with round boundaries dropped.
129 pub fn specs(&self) -> &[OracleSpec] {
130 &self.specs
131 }
132
133 /// Consumes the schedule and returns every oracle, with round boundaries dropped.
134 pub fn into_specs(self) -> Vec<OracleSpec> {
135 self.specs
136 }
137
138 /// The number of closed rounds.
139 pub const fn n_rounds(&self) -> usize {
140 self.ends.len()
141 }
142
143 /// The oracles of each closed round, in commit order.
144 pub fn rounds(&self) -> impl Iterator<Item = &[OracleSpec]> {
145 let starts = iter::once(0).chain(self.ends.iter().copied());
146 iter::zip(starts, self.ends.iter().copied()).map(|(start, end)| &self.specs[start..end])
147 }
148
149 /// One spec per round: the oracle that round's oracles are committed as.
150 ///
151 /// This is the coarser list an underlying channel is configured with.
152 ///
153 /// A round is masked as a whole, so its oracle is zero-knowledge if any member is.
154 pub fn merged_specs(&self) -> Vec<OracleSpec> {
155 self.rounds()
156 .map(|round| OracleSpec {
157 log_msg_len: merged_log_msg_len(round.iter().map(|spec| spec.log_msg_len)),
158 is_zk: round.iter().any(|spec| spec.is_zk),
159 })
160 .collect()
161 }
162}
163
164/// A boxed closure that evaluates a transparent MLE at a given point.
165///
166/// The closure receives the challenge point sampled during the opening and returns the evaluation
167/// of the transparent polynomial's MLE there. It is `'static` and owns every value it reads,
168/// sharing large data via `Rc`/`Arc`, so a channel that defers the opening can store it and
169/// evaluate it later.
170pub type TransparentEvalFn<Elem> = Box<dyn Fn(&[Elem]) -> Elem + 'static>;
171
172/// Channel for IOP verifiers that extends the IP verifier channel with oracle operations.
173///
174/// In an IOP, the verifier can:
175/// 1. Receive field elements from the prover via `recv_*` methods (inherited)
176/// 2. Sample random challenges via `sample` (inherited)
177/// 3. Receive oracle commitments from the prover
178/// 4. Query oracles at specific positions and verify opening proofs
179///
180/// # Contract
181///
182/// The caller must call `recv_oracle()` exactly `remaining_oracle_specs().len()` times before
183/// calling `verify_oracle_relation()`. The oracles must be received in order and match their
184/// specifications.
185pub trait IOPVerifierChannel<F: Field>: IPVerifierChannel<F, Elem: 'static> {
186 type Oracle: Clone;
187
188 /// Returns the specifications for the remaining oracles to be received.
189 ///
190 /// This slice shrinks as oracles are received via `recv_oracle()`.
191 fn remaining_oracle_specs(&self) -> &[OracleSpec];
192
193 /// Receives an oracle commitment from the prover.
194 ///
195 /// The caller describes the oracle being received: `log_msg_len` is the log2 of the message
196 /// length, and `is_witness_dependent` is whether the oracle's contents depend on the witness.
197 /// These let a channel record the oracle's [`OracleSpec`] rather than requiring the specs to be
198 /// supplied up front. The resulting oracle is zero-knowledge iff the channel is configured for
199 /// ZK *and* the oracle is witness-dependent — a non-witness-dependent oracle (e.g. a
200 /// pre-indexed commitment to the wiring matrix for succinctness, a planned feature) is never
201 /// masked.
202 fn recv_oracle(
203 &mut self,
204 log_msg_len: usize,
205 is_witness_dependent: bool,
206 ) -> Result<Self::Oracle, Error>;
207
208 /// Queues one oracle linear relation to be opened.
209 ///
210 /// Implementations may either verify the relation immediately, or queue it and defer the
211 /// actual opening (masking + sumcheck + FRI) to `finish()`. Either way, the relation asserts
212 /// that `<oracle_poly, transparent> = claim`. An oracle may carry any number of relations.
213 ///
214 /// # Preconditions
215 ///
216 /// * `oracle` must be a valid handle returned by `recv_oracle()`.
217 fn verify_oracle_relation(
218 &mut self,
219 oracle: Self::Oracle,
220 transparent: TransparentEvalFn<Self::Elem>,
221 claim: Self::Elem,
222 ) -> Result<(), Error>;
223}