Skip to main content

binius_iop/channel/
oracle_setup.rs

1// Copyright 2026 The Binius Developers
2
3//! An [`IOPVerifierChannel`] dry run that records the [`OracleSpec`] sequence an IOP uses.
4
5use std::{
6	iter::{Product, Sum},
7	marker::PhantomData,
8	ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign},
9};
10
11use binius_core::word::Word;
12use binius_field::{
13	BinaryField, ExtensionField, Field, FieldOps,
14	arithmetic_traits::{InvertOrZero, Square},
15};
16use binius_ip::channel::{IPVerifierChannel, WordIPVerifierChannel};
17
18use crate::channel::{Error, IOPVerifierChannel, OracleSchedule, OracleSpec, TransparentEvalFn};
19
20/// A dummy field element for [`OracleSetupChannel`], generic over the field `F` it stands in for.
21///
22/// The setup channel performs no real verification, so the field values flowing through it are
23/// never inspected. `DummyElem<F>` is a zero-sized stand-in whose arithmetic is all no-ops; the
24/// `PhantomData<F>` lets it satisfy `FieldOps<Scalar = F>` without doing (pointless) real field
25/// arithmetic during the structural dry run.
26#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
27pub struct DummyElem<F>(PhantomData<F>);
28
29macro_rules! dummy_binop {
30	($trait:ident, $method:ident) => {
31		impl<F> $trait for DummyElem<F> {
32			type Output = Self;
33			fn $method(self, _rhs: Self) -> Self {
34				self
35			}
36		}
37		impl<F> $trait<&DummyElem<F>> for DummyElem<F> {
38			type Output = Self;
39			fn $method(self, _rhs: &Self) -> Self {
40				self
41			}
42		}
43	};
44}
45dummy_binop!(Add, add);
46dummy_binop!(Sub, sub);
47dummy_binop!(Mul, mul);
48
49macro_rules! dummy_assign {
50	($trait:ident, $method:ident) => {
51		impl<F> $trait for DummyElem<F> {
52			fn $method(&mut self, _rhs: Self) {}
53		}
54		impl<F> $trait<&DummyElem<F>> for DummyElem<F> {
55			fn $method(&mut self, _rhs: &Self) {}
56		}
57	};
58}
59dummy_assign!(AddAssign, add_assign);
60dummy_assign!(SubAssign, sub_assign);
61dummy_assign!(MulAssign, mul_assign);
62
63impl<F> Neg for DummyElem<F> {
64	type Output = Self;
65	fn neg(self) -> Self {
66		self
67	}
68}
69
70impl<F> Sum for DummyElem<F> {
71	fn sum<I: Iterator<Item = Self>>(_iter: I) -> Self {
72		Self(PhantomData)
73	}
74}
75impl<'a, F> Sum<&'a DummyElem<F>> for DummyElem<F> {
76	fn sum<I: Iterator<Item = &'a Self>>(_iter: I) -> Self {
77		Self(PhantomData)
78	}
79}
80impl<F> Product for DummyElem<F> {
81	fn product<I: Iterator<Item = Self>>(_iter: I) -> Self {
82		Self(PhantomData)
83	}
84}
85impl<'a, F> Product<&'a DummyElem<F>> for DummyElem<F> {
86	fn product<I: Iterator<Item = &'a Self>>(_iter: I) -> Self {
87		Self(PhantomData)
88	}
89}
90
91impl<F> Square for DummyElem<F> {
92	fn square(self) -> Self {
93		self
94	}
95}
96impl<F> InvertOrZero for DummyElem<F> {
97	fn invert_or_zero(self) -> Self {
98		self
99	}
100}
101
102impl<F> From<F> for DummyElem<F> {
103	fn from(_value: F) -> Self {
104		Self(PhantomData)
105	}
106}
107
108impl<F: Field> FieldOps for DummyElem<F> {
109	type Scalar = F;
110
111	fn zero() -> Self {
112		Self(PhantomData)
113	}
114
115	fn one() -> Self {
116		Self(PhantomData)
117	}
118
119	fn square_transpose<FSub: Field>(_elems: &mut [Self])
120	where
121		F: ExtensionField<FSub>,
122	{
123	}
124}
125
126/// An [`IOPVerifierChannel`] that records the [`OracleSpec`] of each received oracle.
127///
128/// This performs no verification.
129///
130/// Every receive returns a dummy value, and sampling, observing, and asserting are no-ops.
131///
132/// Drive an IOP verifier with it, then read back what it recorded.
133///
134/// The channel carries one protocol-level zero-knowledge flag.
135///
136/// An oracle is zero-knowledge only if that flag is set and the oracle is witness-dependent.
137///
138/// # What is recorded
139///
140/// Two views of the same dry run, differing only in whether round boundaries survive.
141///
142/// - The flat sequence of oracles, in arrival order.
143/// - That same sequence, grouped into the rounds the oracles are committed in.
144///
145/// A round closes wherever a challenge could be drawn.
146///
147/// That is where a decorator committing a round as one oracle stops taking members.
148#[derive(Debug, Default, Clone)]
149pub struct OracleSetupChannel {
150	is_zk: bool,
151	schedule: OracleSchedule,
152}
153
154impl OracleSetupChannel {
155	/// Creates a new setup channel with the given protocol-level zero-knowledge flag.
156	pub const fn new(is_zk: bool) -> Self {
157		Self {
158			is_zk,
159			schedule: OracleSchedule::new(),
160		}
161	}
162
163	/// Returns the oracle specs recorded so far.
164	pub fn oracle_specs(&self) -> &[OracleSpec] {
165		self.schedule.specs()
166	}
167
168	/// Consumes the channel and returns the recorded oracle specs, in the order received.
169	pub fn into_oracle_specs(self) -> Vec<OracleSpec> {
170		self.schedule.into_specs()
171	}
172
173	/// Consumes the channel and returns the recorded oracles, grouped by commit round.
174	///
175	/// The round left open at the end of the dry run is closed here.
176	///
177	/// So every recorded oracle belongs to some round.
178	pub fn into_oracle_schedule(mut self) -> OracleSchedule {
179		self.schedule.end_round();
180		self.schedule
181	}
182}
183
184impl<F: Field> IPVerifierChannel<F> for OracleSetupChannel {
185	type Elem = DummyElem<F>;
186
187	fn recv_one(&mut self) -> Result<DummyElem<F>, binius_ip::channel::Error> {
188		Ok(DummyElem(PhantomData))
189	}
190
191	fn recv_many(&mut self, n: usize) -> Result<Vec<DummyElem<F>>, binius_ip::channel::Error> {
192		Ok(vec![DummyElem(PhantomData); n])
193	}
194
195	fn recv_array<const N: usize>(
196		&mut self,
197	) -> Result<[DummyElem<F>; N], binius_ip::channel::Error> {
198		Ok([DummyElem(PhantomData); N])
199	}
200
201	fn sample(&mut self) -> DummyElem<F> {
202		// A challenge follows the commitments it is derived from, so it ends their round.
203		self.schedule.end_round();
204		DummyElem(PhantomData)
205	}
206
207	fn observe_one(&mut self, _val: F) -> DummyElem<F> {
208		DummyElem(PhantomData)
209	}
210
211	fn observe_many(&mut self, vals: &[F]) -> Vec<DummyElem<F>> {
212		vec![DummyElem(PhantomData); vals.len()]
213	}
214
215	fn assert_zero(&mut self, _val: DummyElem<F>) -> Result<(), binius_ip::channel::Error> {
216		Ok(())
217	}
218}
219
220impl<F: BinaryField> WordIPVerifierChannel<F> for OracleSetupChannel {
221	type Word = Word;
222
223	// The dry run records oracle shapes only, so nothing reaches a Fiat-Shamir state.
224	fn observe_words(&mut self, words: &[Word]) -> Vec<Word> {
225		words.to_vec()
226	}
227
228	fn subset_sum(&mut self, _elems: &[DummyElem<F>], _word: &Word) -> DummyElem<F> {
229		DummyElem(PhantomData)
230	}
231
232	fn select(&mut self, _elems: &[DummyElem<F>], _word: &Word) -> DummyElem<F> {
233		DummyElem(PhantomData)
234	}
235
236	// The recorded oracle shapes do not depend on which leaves a protocol would query.
237	fn sample_bits(&mut self, _bits: usize) -> Word {
238		// A sampled word is a challenge like any other, so it ends the open round too.
239		self.schedule.end_round();
240		Word::ZERO
241	}
242
243	// Only the element count matters here, and it follows from the word count alone.
244	fn pack_words(&mut self, words: &[Word]) -> Vec<DummyElem<F>> {
245		let words_per_elem = F::N_BITS / Word::BITS;
246		vec![DummyElem(PhantomData); words.len().div_ceil(words_per_elem)]
247	}
248}
249
250impl<F: Field> IOPVerifierChannel<F> for OracleSetupChannel {
251	type Oracle = ();
252
253	fn remaining_oracle_specs(&self) -> &[OracleSpec] {
254		// A setup channel has no pre-supplied specs; it records them as they are received.
255		&[]
256	}
257
258	fn recv_oracle(
259		&mut self,
260		log_msg_len: usize,
261		is_witness_dependent: bool,
262	) -> Result<Self::Oracle, Error> {
263		// A non-witness-dependent oracle is never masked, whatever the protocol-level flag says.
264		self.schedule.push(OracleSpec {
265			log_msg_len,
266			is_zk: self.is_zk && is_witness_dependent,
267		});
268		Ok(())
269	}
270
271	fn verify_oracle_relation(
272		&mut self,
273		_oracle: Self::Oracle,
274		_transparent: TransparentEvalFn<Self::Elem>,
275		_claim: Self::Elem,
276	) -> Result<(), Error> {
277		// Opening an oracle needs every commitment in place, so nothing may still be queued.
278		self.schedule.end_round();
279		Ok(())
280	}
281}
282
283#[cfg(test)]
284mod tests {
285	use binius_field::Ghash128b;
286
287	use super::*;
288
289	type F = Ghash128b;
290
291	/// Draws a challenge, which is what closes a round.
292	fn sample(channel: &mut OracleSetupChannel) {
293		IPVerifierChannel::<F>::sample(channel);
294	}
295
296	/// Receives one witness-carrying oracle of the given length.
297	fn recv(channel: &mut OracleSetupChannel, log_msg_len: usize) {
298		IOPVerifierChannel::<F>::recv_oracle(channel, log_msg_len, true)
299			.expect("the setup channel never fails");
300	}
301
302	#[test]
303	fn a_challenge_ends_the_open_round() {
304		let mut channel = OracleSetupChannel::new(false);
305
306		// Two oracles, a challenge, then one more.
307		//
308		//     recv 3, recv 3, sample, recv 4, sample
309		//     \____________/          \____/
310		//        round 0              round 1
311		recv(&mut channel, 3);
312		recv(&mut channel, 3);
313		sample(&mut channel);
314		recv(&mut channel, 4);
315		sample(&mut channel);
316
317		let schedule = channel.into_oracle_schedule();
318		assert_eq!(schedule.n_rounds(), 2);
319		assert_eq!(
320			schedule.rounds().collect::<Vec<_>>(),
321			vec![
322				[OracleSpec::new(3), OracleSpec::new(3)].as_slice(),
323				[OracleSpec::new(4)].as_slice(),
324			]
325		);
326	}
327
328	#[test]
329	fn the_final_round_closes_without_a_challenge() {
330		let mut channel = OracleSetupChannel::new(false);
331
332		// The run ends with an oracle, not a challenge.
333		// That trailing oracle is still a round of its own.
334		recv(&mut channel, 2);
335		sample(&mut channel);
336		recv(&mut channel, 5);
337
338		let schedule = channel.into_oracle_schedule();
339		assert_eq!(schedule.n_rounds(), 2);
340		assert_eq!(schedule.rounds().last(), Some([OracleSpec::new(5)].as_slice()));
341	}
342
343	#[test]
344	fn a_sampled_word_ends_the_open_round_too() {
345		let mut channel = OracleSetupChannel::new(false);
346
347		// Query indices are drawn as words rather than as field elements.
348		// That is still a challenge, so it still closes the round before it.
349		recv(&mut channel, 3);
350		WordIPVerifierChannel::<F>::sample_bits(&mut channel, 4);
351		recv(&mut channel, 2);
352
353		assert_eq!(channel.into_oracle_schedule().n_rounds(), 2);
354	}
355
356	#[test]
357	fn back_to_back_challenges_make_no_empty_round() {
358		let mut channel = OracleSetupChannel::new(false);
359
360		// A protocol may draw several challenges in a row with nothing committed between them.
361		// Only the first of those closes anything.
362		recv(&mut channel, 3);
363		sample(&mut channel);
364		sample(&mut channel);
365		sample(&mut channel);
366
367		assert_eq!(channel.into_oracle_schedule().n_rounds(), 1);
368	}
369
370	#[test]
371	fn a_run_with_no_oracles_has_no_rounds() {
372		let mut channel = OracleSetupChannel::new(false);
373
374		// A challenge alone commits nothing, so it closes nothing.
375		sample(&mut channel);
376
377		let schedule = channel.into_oracle_schedule();
378		assert_eq!(schedule.n_rounds(), 0);
379		assert!(schedule.specs().is_empty());
380	}
381
382	#[test]
383	fn dropping_the_boundaries_leaves_the_flat_sequence() {
384		let mut channel = OracleSetupChannel::new(false);
385
386		// Grouping is the only thing the schedule adds.
387		// The oracles themselves, and their order, must survive it untouched.
388		recv(&mut channel, 3);
389		sample(&mut channel);
390		recv(&mut channel, 1);
391		recv(&mut channel, 4);
392
393		let flat = channel.clone().into_oracle_specs();
394		assert_eq!(channel.into_oracle_schedule().into_specs(), flat);
395	}
396
397	#[test]
398	fn a_round_is_masked_if_any_of_its_oracles_is() {
399		// Protocol-level zero-knowledge is on, but not every oracle carries witness data.
400		//
401		//     round 0: [structural 2^3, witness 2^3] -> 2^4, masked because one member is
402		//     round 1: [structural 2^2]              -> 2^2, nothing to hide
403		let mut channel = OracleSetupChannel::new(true);
404		IOPVerifierChannel::<F>::recv_oracle(&mut channel, 3, false).unwrap();
405		IOPVerifierChannel::<F>::recv_oracle(&mut channel, 3, true).unwrap();
406		sample(&mut channel);
407		IOPVerifierChannel::<F>::recv_oracle(&mut channel, 2, false).unwrap();
408
409		let merged = channel.into_oracle_schedule().merged_specs();
410		assert_eq!(merged, vec![OracleSpec::new_zk(4), OracleSpec::new(2)]);
411	}
412}