Skip to main content

binius_iop/fri/
fold.rs

1// Copyright 2024-2025 Irreducible Inc.
2// Copyright 2026 The Binius Developers
3
4use binius_field::{BinaryField, ExtensionField};
5use binius_math::{line::extrapolate_line, ntt::AdditiveNTT};
6
7use super::{FRIParams, error::Error};
8use crate::merkle_channel::MerkleIPVerifierChannel;
9
10/// Calculate fold of `values` at `index` with `r` random coefficient.
11///
12/// See [DP24], Def. 3.6.
13///
14/// [DP24]: <https://eprint.iacr.org/2024/504>
15#[inline]
16fn fold_pair<F, FS, NTT>(ntt: &NTT, round: usize, index: usize, values: (F, F), r: F) -> F
17where
18	F: BinaryField + ExtensionField<FS>,
19	FS: BinaryField,
20	NTT: AdditiveNTT<Field = FS>,
21{
22	// Perform inverse additive NTT butterfly
23	let t = ntt.twiddle(round - 1, index);
24	let (mut u, mut v) = values;
25	v += u;
26	u += v * t;
27	extrapolate_line(u, v, r)
28}
29
30/// Calculate FRI fold of `values` at a `chunk_index` with random folding challenges.
31///
32/// Folds a coset of a Reed–Solomon codeword into a single value using the FRI folding algorithm.
33/// The coset has size $2^n$, where $n$ is the number of challenges.
34///
35/// See [DP24], Def. 3.6 and Lemma 3.9 for more details.
36///
37/// NB: This method is on a hot path and does not perform any allocations or
38/// precondition checks.
39///
40/// ## Arguments
41///
42/// * `math` - the NTT instance, used to look up the twiddle values.
43/// * `log_len` - the binary logarithm of the code length.
44/// * `chunk_index` - the index of the chunk, of size $2^n$, in the full codeword.
45/// * `values` - mutable slice of values to fold, modified in place.
46/// * `challenges` - the sequence of folding challenges, with length $n$.
47///
48/// ## Pre-conditions
49///
50/// - `challenges.len() <= log_len`.
51/// - `log_len <= math.log_domain_size()`, so that the NTT domain is large enough.
52/// - `values.len() == 1 << challenges.len()`.
53///
54/// [DP24]: <https://eprint.iacr.org/2024/504>
55#[inline]
56pub fn fold_chunk<F, FS, NTT>(
57	ntt: &NTT,
58	mut log_len: usize,
59	chunk_index: usize,
60	values: &mut [F],
61	challenges: &[F],
62) -> F
63where
64	F: BinaryField + ExtensionField<FS>,
65	FS: BinaryField,
66	NTT: AdditiveNTT<Field = FS>,
67{
68	let mut log_size = challenges.len();
69
70	// Preconditions
71	debug_assert!(log_size <= log_len);
72	debug_assert!(log_len <= ntt.log_domain_size());
73	debug_assert_eq!(values.len(), 1 << log_size);
74
75	// FRI-fold the values in place.
76	for &challenge in challenges {
77		// Fold the (2i) and (2i+1)th cells of the scratch buffer in-place into the i-th cell
78		for index_offset in 0..1 << (log_size - 1) {
79			let pair = (values[index_offset << 1], values[(index_offset << 1) | 1]);
80			values[index_offset] = fold_pair(
81				ntt,
82				log_len,
83				(chunk_index << (log_size - 1)) | index_offset,
84				pair,
85				challenge,
86			);
87		}
88
89		log_len -= 1;
90		log_size -= 1;
91	}
92
93	values[0]
94}
95
96/// One commitment the prover is expected to send during the fold phase.
97struct ExpectedCommitment {
98	/// The fold round this commitment arrives in.
99	round: usize,
100	/// Number of field elements packed into a single leaf of the Merkle tree.
101	leaf_size: usize,
102	/// Base-2 logarithm of the number of leaves in the Merkle tree.
103	depth: usize,
104}
105
106/// Receives the prover's fold-phase commitments in the order the protocol sends them.
107///
108/// The fold phase spends one round per variable of the committed message.
109/// Most rounds only absorb a folding challenge and carry no message from the prover.
110///
111/// A round that completes a fold shrinks the codeword.
112/// The prover commits to that new codeword, and this type reads it off the channel.
113pub struct FRIFoldVerifier<'a, F, C>
114where
115	F: BinaryField,
116{
117	/// Every commitment the fold phase expects, ordered by the round it arrives in.
118	commit_schedule: Vec<ExpectedCommitment>,
119	/// Commitments read off the channel so far, in arrival order.
120	round_commitments: Vec<C>,
121	/// The round about to be processed, counted from zero.
122	curr_round: usize,
123	/// Number of rounds the fold phase runs.
124	n_rounds: usize,
125	_phantom: std::marker::PhantomData<&'a F>,
126}
127
128impl<'a, F, C> FRIFoldVerifier<'a, F, C>
129where
130	F: BinaryField,
131	C: Clone,
132{
133	/// Derives the schedule of expected commitments from the protocol parameters.
134	pub fn new(params: &'a FRIParams<F>) -> Self {
135		// One commitment per codeword the fold phase sends: batched, one per fold, then terminal.
136		let expected_oracles = params.n_oracles();
137
138		// A fold absorbs one challenge per round, then commits to the codeword it produced.
139		// So commitments land on the running total of the fold widths, never in between.
140		//
141		// Concrete run: batch width 4, folds of arity 4 then 3, over a 2^18-symbol codeword.
142		//
143		//     rounds 0..3   batch fold        commit at 4    leaf 2^4   2^14 leaves
144		//     rounds 4..7   fold of arity 4   commit at 8    leaf 2^3   2^11 leaves
145		//     rounds 8..10  fold of arity 3   commit at 11   leaf 2^9   2^2  leaves
146		//
147		// A leaf holds the whole coset that the *next* fold reads.
148		// So each tree is shorter than its own codeword by that fold's arity.
149		let mut commit_schedule = Vec::with_capacity(expected_oracles);
150
151		// The opening fold absorbs the batching challenges.
152		// So nothing is committed until it ends.
153		let mut round = params.log_batch_size();
154
155		// Ahead of any fold, the tree holds one leaf per codeword symbol.
156		let mut depth = params.index_bits();
157
158		for &arity in params.fold_arities() {
159			// This fold reads cosets of its own arity.
160			// So packing one per leaf costs the tree that many levels.
161			depth -= arity;
162			commit_schedule.push(ExpectedCommitment {
163				round,
164				leaf_size: 1 << arity,
165				depth,
166			});
167
168			// The next commitment waits for this fold to absorb every one of its challenges.
169			round += arity;
170		}
171
172		// The terminal codeword is sent in full rather than folded again.
173		// So its leaf spans a whole message, not a coset, leaving one leaf per rate position.
174		commit_schedule.push(ExpectedCommitment {
175			round,
176			leaf_size: 1 << params.n_final_challenges(),
177			depth: params.rs_code().log_inv_rate(),
178		});
179
180		Self {
181			commit_schedule,
182			round_commitments: Vec::with_capacity(expected_oracles),
183			curr_round: 0,
184			// The opening round precedes the first challenge.
185			// So the phase runs one round longer than it has challenges.
186			n_rounds: params.n_fold_rounds() + 1,
187			_phantom: std::marker::PhantomData,
188		}
189	}
190
191	/// The next commitment still awaited, or nothing once every one of them has arrived.
192	fn next_commitment(&self) -> Option<&ExpectedCommitment> {
193		// Commitments arrive in schedule order.
194		// So the number received so far is the read cursor.
195		self.commit_schedule.get(self.round_commitments.len())
196	}
197
198	/// Advances one fold round, reading a commitment off the channel if this round carries one.
199	///
200	/// # Returns
201	///
202	/// The commitment received, or nothing when this round carries none.
203	///
204	/// # Panics
205	///
206	/// Panics once the fold phase has already run all of its rounds.
207	pub fn process_round<Channel>(&mut self, channel: &mut Channel) -> Result<Option<C>, Error>
208	where
209		Channel: MerkleIPVerifierChannel<F, Commitment = C>,
210	{
211		// The fold phase has a length fixed by the parameters.
212		// Stepping past it would read a message the prover never sent.
213		assert!(
214			self.curr_round < self.n_rounds(),
215			"precondition: process_round must not be called more than n_rounds() times"
216		);
217
218		// Only the round named by the next scheduled commitment carries one.
219		let commitment = match self.next_commitment() {
220			Some(&ExpectedCommitment {
221				round,
222				leaf_size,
223				depth,
224			}) if round == self.curr_round => {
225				// Demanding the scheduled shape pins the prover to the prescribed layout.
226				// A mis-shaped tree is rejected here rather than at query time.
227				let commitment = channel.recv_merkle_commitment(leaf_size, depth)?;
228				self.round_commitments.push(commitment.clone());
229				Some(commitment)
230			}
231			_ => None,
232		};
233
234		self.curr_round += 1;
235		Ok(commitment)
236	}
237
238	/// Reports whether every fold round has been processed.
239	pub const fn is_complete(&self) -> bool {
240		self.curr_round == self.n_rounds()
241	}
242
243	/// Consumes the verifier and yields the commitments it collected, in arrival order.
244	///
245	/// # Panics
246	///
247	/// Panics unless every fold round has been processed.
248	/// Stopping early would drop commitments the query phase needs to open.
249	pub fn finalize(self) -> Vec<C> {
250		// Every scheduled commitment lands on some round, so an unfinished phase is a short list.
251		assert!(
252			self.is_complete(),
253			"precondition: all fold rounds must be processed before finalize"
254		);
255
256		self.round_commitments
257	}
258
259	/// Number of rounds the fold phase runs.
260	pub const fn n_rounds(&self) -> usize {
261		self.n_rounds
262	}
263}