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}