Skip to main content

binius_ip/
channel.rs

1// Copyright 2026 The Binius Developers
2
3//! Channel abstraction for public-coin interactive protocol verifiers.
4//!
5//! In a public-coin interactive protocol, the verifier's messages consist entirely of random
6//! challenges, while the prover sends deterministic messages based on the protocol state.
7//! This module provides the [`IPVerifierChannel`] trait that models the verifier's view of such
8//! an interaction.
9//!
10//! The trait abstracts over:
11//! - Receiving prover messages (field elements)
12//! - Sampling random challenges (which, in the Fiat-Shamir transform, are derived deterministically
13//!   from the transcript)
14//!
15//! This abstraction allows protocol implementations to be generic over the underlying
16//! communication mechanism, whether it's an actual interactive channel or a non-interactive
17//! transcript using the Fiat-Shamir heuristic.
18//!
19//! [`WordIPVerifierChannel`] extends it for protocols that also carry 64-bit words, such as a
20//! constraint system's public inputs or a code proximity test's query indices.
21
22use std::{iter::repeat_with, ops::Shr};
23
24use binius_core::word::Word;
25use binius_field::{BinaryField, Field, field::FieldOps};
26use binius_transcript::{
27	VerifierTranscript,
28	fiat_shamir::{CanSample, CanSampleBits, Challenger},
29};
30
31/// Channel for receiving prover messages and sampling challenges in a public-coin interactive
32/// protocol.
33///
34/// In a public-coin protocol, the verifier only sends random challenges (no secret information),
35/// so the verifier's role is to:
36/// 1. Receive field elements from the prover via `recv_*` methods
37/// 2. Sample random challenges via `sample`
38///
39/// When used with a Fiat-Shamir transcript, the challenges are derived deterministically from
40/// the transcript state, making the protocol non-interactive.
41pub trait IPVerifierChannel<F: Field> {
42	/// The element type returned by receive and sample methods.
43	type Elem: FieldOps<Scalar = F>;
44
45	/// Receives a single field element from the prover.
46	fn recv_one(&mut self) -> Result<Self::Elem, Error>;
47
48	/// Receives `n` field elements from the prover.
49	fn recv_many(&mut self, n: usize) -> Result<Vec<Self::Elem>, Error> {
50		repeat_with(|| self.recv_one()).take(n).collect()
51	}
52
53	/// Receives a fixed-size array of field elements from the prover.
54	fn recv_array<const N: usize>(&mut self) -> Result<[Self::Elem; N], Error> {
55		array_util::try_from_fn(|_| self.recv_one())
56	}
57
58	/// Receives a value the verifier could compute for itself, taken as advice.
59	///
60	/// The prover states it and the verifier checks it — by recomputing it, or by any argument
61	/// that establishes the same thing — which is worth doing when the check is cheaper than the
62	/// computation, or when several such claims are better checked at once.
63	///
64	/// The caller MUST check what it receives here. Nothing else does.
65	///
66	/// A claim is a function of public-channel-derived values alone, so it carries nothing about
67	/// the witness. Channels that mask the prover's messages must therefore leave this one in the
68	/// clear, and channels that carry elements as wires allocate it as a public wire rather than a
69	/// masked private one. The default is the plain read, for a channel that draws no such
70	/// distinction.
71	fn recv_public_claim(&mut self) -> Result<Self::Elem, Error> {
72		self.recv_one()
73	}
74
75	/// Samples a random challenge.
76	///
77	/// In a Fiat-Shamir transcript, this derives the challenge deterministically from
78	/// the current transcript state.
79	fn sample(&mut self) -> Self::Elem;
80
81	/// Samples `n` random challenges.
82	fn sample_many(&mut self, n: usize) -> Vec<Self::Elem> {
83		repeat_with(|| self.sample()).take(n).collect()
84	}
85
86	/// Samples a fixed-size array of random challenges.
87	fn sample_array<const N: usize>(&mut self) -> [Self::Elem; N] {
88		std::array::from_fn(|_| self.sample())
89	}
90
91	/// Observes a single field element, feeding it into the Fiat-Shamir state.
92	///
93	/// Returns the element converted to `Self::Elem`.
94	fn observe_one(&mut self, val: F) -> Self::Elem;
95
96	/// Observes multiple field elements, feeding them into the Fiat-Shamir state.
97	///
98	/// Returns the elements converted to `Vec<Self::Elem>`.
99	fn observe_many(&mut self, vals: &[F]) -> Vec<Self::Elem> {
100		vals.iter().map(|&val| self.observe_one(val)).collect()
101	}
102
103	/// Asserts that a value is zero.
104	///
105	/// Returns [`Error::InvalidAssert`] if the value is not zero.
106	fn assert_zero(&mut self, val: Self::Elem) -> Result<(), Error>;
107}
108
109/// A verifier channel whose protocol carries 64-bit words alongside field elements.
110///
111/// Some values a verifier handles are words rather than field elements: the public inputs of a
112/// Binius64 constraint system, and the query indices a code proximity test samples. A channel that
113/// symbolically executes a verifier to build a circuit carries those as wires, so protocol code
114/// cannot name a concrete word type and goes through [`Self::Word`] instead.
115///
116/// [`Self::subset_sum`] and [`Self::select`] are how protocol code reaches the bits of a word. A
117/// channel over concrete values reads them directly; a circuit-building one emits a sub-circuit.
118pub trait WordIPVerifierChannel<F: Field>: IPVerifierChannel<F> {
119	/// The word type this channel carries.
120	///
121	/// A channel over concrete values uses [`Word`]. A channel that builds a circuit uses a wire
122	/// type that folds the operations below over a builder.
123	///
124	/// [`From<Word>`](From) lifts a word the protocol description fixes, such as a constraint
125	/// system constant, and [`Shr`] is the index arithmetic a code proximity test performs between
126	/// fold rounds. Both are plain operations on the type rather than channel methods, so protocol
127	/// code writes `word.into()` and `word >> n` whatever the channel is. The shift amount is a
128	/// `u32` to match [`Word`]'s own [`Shl`](std::ops::Shl) and [`Shr`] impls.
129	type Word: Clone + From<Word> + Shr<u32, Output = Self::Word>;
130
131	/// Feeds words into the Fiat-Shamir state, each as eight little-endian bytes, and returns them
132	/// as this channel's word type.
133	///
134	/// The words go in concrete, because the statement is fixed data the verifier is handed rather
135	/// than something the protocol derives. They come back as [`Self::Word`], which is where a
136	/// channel that carries words as wires introduces them: it allocates the wires here, and the
137	/// protocol sees the statement symbolically from this point on. A channel over concrete values
138	/// hands the same words straight back.
139	fn observe_words(&mut self, words: &[Word]) -> Vec<Self::Word>;
140
141	/// Returns the sum of the `elems` selected by the low bits of `word`, low bit first.
142	///
143	/// This is the inner product of `elems` with the bit decomposition of `word`. Bits of `word`
144	/// at or above `elems.len()` do not contribute.
145	///
146	/// ## Preconditions
147	///
148	/// * `elems.len()` must be at most 64.
149	fn subset_sum(&mut self, elems: &[Self::Elem], word: &Self::Word) -> Self::Elem;
150
151	/// Returns the element of `elems` at the index in the low bits of `word`.
152	///
153	/// Bits of `word` at or above `log2(elems.len())` are ignored.
154	///
155	/// ## Preconditions
156	///
157	/// * `elems` must be non-empty and its length must be a power of two.
158	fn select(&mut self, elems: &[Self::Elem], word: &Self::Word) -> Self::Elem;
159
160	/// Samples a uniform word of the given bit width.
161	///
162	/// The result is masked to `bits` bits. Protocols rely on that bound, so an implementation
163	/// must enforce it rather than assume the sampled value already fits.
164	fn sample_bits(&mut self, bits: usize) -> Self::Word;
165
166	/// Packs words into field elements, as many words to an element as one holds.
167	///
168	/// Word `i` occupies bits `[Word::BITS * i, Word::BITS * (i + 1))` of element
169	/// `i / words_per_elem`, so the packed form reads the same way the committed trace does: the
170	/// low bit-index coordinates address the bit within a word, the next the word within its
171	/// element. A word count that does not fill the last element leaves its high words zero.
172	///
173	/// This is a channel method rather than a free function because the words may be wires: a
174	/// channel over concrete values computes the elements, and a circuit-building one emits the
175	/// gates that assemble them.
176	fn pack_words(&mut self, words: &[Self::Word]) -> Vec<Self::Elem>;
177}
178
179/// The number of elements [`WordIPVerifierChannel::pack_words`] returns for `n_words` words.
180///
181/// A channel that packs the words into wires rather than computing them needs the count on its
182/// own, so the layout stays in one place.
183pub const fn n_packed_elems<F: BinaryField>(n_words: usize) -> usize {
184	n_words.div_ceil(F::N_BITS / Word::BITS)
185}
186
187/// [`WordIPVerifierChannel::pack_words`] over concrete words, for channels carrying [`Word`].
188///
189/// A set bit contributes the basis element at its position in the packed layout, which is what
190/// makes this agree with reading the element's bits back out.
191pub fn pack_words_concrete<F, E>(words: &[Word]) -> Vec<E>
192where
193	F: BinaryField,
194	E: FieldOps<Scalar = F> + From<F>,
195{
196	let words_per_elem = F::N_BITS / Word::BITS;
197	words
198		.chunks(words_per_elem)
199		.map(|chunk| {
200			let packed = chunk
201				.iter()
202				.enumerate()
203				.flat_map(|(i, word)| {
204					(0..Word::BITS)
205						.filter(|&bit| word.extract_bit(bit))
206						.map(move |bit| F::basis(i * Word::BITS + bit))
207				})
208				.sum::<F>();
209			E::from(packed)
210		})
211		.collect()
212}
213
214/// [`WordIPVerifierChannel::subset_sum`] over a concrete word, for channels carrying [`Word`].
215///
216/// ## Preconditions
217///
218/// * `elems.len()` must be at most 64.
219pub fn subset_sum_word<E: FieldOps>(elems: &[E], word: Word) -> E {
220	assert!(elems.len() <= Word::BITS); // precondition
221
222	elems
223		.iter()
224		.enumerate()
225		.filter(|&(bit, _)| word.extract_bit(bit))
226		.map(|(_, elem)| elem.clone())
227		.sum()
228}
229
230/// [`WordIPVerifierChannel::select`] over a concrete word, for channels carrying [`Word`].
231///
232/// ## Preconditions
233///
234/// * `elems` must be non-empty and its length must be a power of two.
235pub fn select_word<E: FieldOps>(elems: &[E], word: Word) -> E {
236	assert!(!elems.is_empty() && elems.len().is_power_of_two()); // precondition
237
238	elems[word.as_u64() as usize & (elems.len() - 1)].clone()
239}
240
241impl<F, Challenger_> IPVerifierChannel<F> for VerifierTranscript<Challenger_>
242where
243	F: Field,
244	Challenger_: Challenger,
245{
246	type Elem = F;
247
248	fn recv_one(&mut self) -> Result<F, Error> {
249		self.message().read_scalar().map_err(|_| Error::ProofEmpty)
250	}
251
252	fn recv_many(&mut self, n: usize) -> Result<Vec<F>, Error> {
253		self.message()
254			.read_scalar_slice(n)
255			.map_err(|_| Error::ProofEmpty)
256	}
257
258	fn recv_array<const N: usize>(&mut self) -> Result<[F; N], Error> {
259		self.message().read().map_err(|_| Error::ProofEmpty)
260	}
261
262	fn sample(&mut self) -> F {
263		CanSample::sample(self)
264	}
265
266	fn observe_one(&mut self, val: F) -> F {
267		self.observe().write_scalar(val);
268		val
269	}
270
271	fn observe_many(&mut self, vals: &[F]) -> Vec<F> {
272		self.observe().write_scalar_slice(vals);
273		vals.to_vec()
274	}
275
276	fn assert_zero(&mut self, val: F) -> Result<(), Error> {
277		if val == F::ZERO {
278			Ok(())
279		} else {
280			Err(Error::InvalidAssert)
281		}
282	}
283}
284
285impl<F, Challenger_> WordIPVerifierChannel<F> for VerifierTranscript<Challenger_>
286where
287	F: BinaryField,
288	Challenger_: Challenger,
289{
290	type Word = Word;
291
292	fn observe_words(&mut self, words: &[Word]) -> Vec<Word> {
293		self.observe().write_slice(words);
294		words.to_vec()
295	}
296
297	fn subset_sum(&mut self, elems: &[F], word: &Word) -> F {
298		subset_sum_word(elems, *word)
299	}
300
301	fn select(&mut self, elems: &[F], word: &Word) -> F {
302		select_word(elems, *word)
303	}
304
305	fn sample_bits(&mut self, bits: usize) -> Word {
306		Word::from_u64(CanSampleBits::<u32>::sample_bits(self, bits) as u64)
307	}
308
309	fn pack_words(&mut self, words: &[Word]) -> Vec<F> {
310		pack_words_concrete::<F, F>(words)
311	}
312}
313
314#[derive(Debug, thiserror::Error)]
315pub enum Error {
316	#[error("proof is empty")]
317	ProofEmpty,
318	#[error("invalid assertion: value is not zero")]
319	InvalidAssert,
320}