Skip to main content

binius_math/
reed_solomon.rs

1// Copyright 2023-2025 Irreducible Inc.
2// Copyright 2026 The Binius Developers
3
4//! [Reed–Solomon] codes over binary fields.
5//!
6//! See [`ReedSolomonCode`] for details.
7
8use std::marker::PhantomData;
9
10use binius_compute::Allocator;
11use binius_field::{BinaryField, PackedField};
12use getset::CopyGetters;
13
14use super::{FieldBuffer, FieldSlice, binary_subspace::BinarySubspace, ntt::AdditiveNTT};
15use crate::{
16	bit_reverse::bit_reverse_packed,
17	ntt::{DomainContext, domain_context::GaoMateerOnTheFly},
18};
19
20/// [Reed–Solomon] codes over binary fields.
21///
22/// The Reed–Solomon code admits an efficient encoding algorithm over binary fields due to [LCH14].
23/// The additive NTT encoding algorithm encodes messages interpreted as the coefficients of a
24/// polynomial in a non-standard, novel polynomial basis and the codewords are the polynomial
25/// evaluations over a linear subspace of the field. See the [binius-math] crate for more details.
26///
27/// [Reed–Solomon]: <https://en.wikipedia.org/wiki/Reed%E2%80%93Solomon_error_correction>
28/// [LCH14]: <https://arxiv.org/abs/1404.3458>
29#[derive(Debug, Clone, CopyGetters)]
30pub struct ReedSolomonCode<F> {
31	log_dimension: usize,
32	#[get_copy = "pub"]
33	log_inv_rate: usize,
34	_marker: PhantomData<F>,
35}
36
37impl<F: BinaryField> ReedSolomonCode<F> {
38	/// A code of the given dimension and rate, evaluated over the Gao-Mateer basis.
39	///
40	/// The evaluation domain is not a parameter: it is the Gao-Mateer basis of `log_dimension +
41	/// log_inv_rate`, the same one [`GaoMateerOnTheFly`] and [`GaoMateerPreExpanded`] generate. A
42	/// verifier can therefore rebuild the domain from the code's shape alone, without being told
43	/// which basis the prover encoded over.
44	///
45	/// [`GaoMateerOnTheFly`]: crate::ntt::domain_context::GaoMateerOnTheFly
46	/// [`GaoMateerPreExpanded`]: crate::ntt::domain_context::GaoMateerPreExpanded
47	pub const fn new(log_dimension: usize, log_inv_rate: usize) -> Self {
48		Self {
49			log_dimension,
50			log_inv_rate,
51			_marker: PhantomData,
52		}
53	}
54
55	/// The evaluation domain: the Gao-Mateer basis of [`Self::log_len`] dimensions.
56	///
57	/// Derived on demand rather than stored, so there is no way for it to disagree with the
58	/// domain a prover or verifier generates from the same dimension.
59	pub fn subspace(&self) -> BinarySubspace<F> {
60		GaoMateerOnTheFly::<F>::generate(self.log_len()).subspace(self.log_len())
61	}
62
63	/// The dimension.
64	pub const fn dim(&self) -> usize {
65		1 << self.dim_bits()
66	}
67
68	pub const fn log_dim(&self) -> usize {
69		self.log_dimension
70	}
71
72	pub const fn log_len(&self) -> usize {
73		self.log_dimension + self.log_inv_rate
74	}
75
76	/// The block length.
77	#[allow(clippy::len_without_is_empty)]
78	pub const fn len(&self) -> usize {
79		1 << (self.log_dimension + self.log_inv_rate)
80	}
81
82	/// The base-2 log of the dimension.
83	const fn dim_bits(&self) -> usize {
84		self.log_dimension
85	}
86
87	/// The reciprocal of the rate, ie. `self.len() / self.dim()`.
88	pub const fn inv_rate(&self) -> usize {
89		1 << self.log_inv_rate
90	}
91
92	/// Encodes a message with an interleaved Reed–Solomon code.
93	///
94	/// This function interprets the message as a batch of independent vectors and applies an
95	/// interleaved Reed–Solomon.
96	///
97	/// ## Preconditions
98	///
99	/// * `data.log_len()` must equal `log_dim() + log_batch_size`.
100	/// * The NTT subspace must match the code's subspace.
101	///
102	/// ## Postconditions
103	///
104	/// * All elements in the output buffer are initialized with the encoded codeword.
105	pub fn encode_batch<P, NTT, A>(
106		&self,
107		ntt: &NTT,
108		data: FieldSlice<'_, P>,
109		log_batch_size: usize,
110		alloc: &A,
111	) -> FieldBuffer<P, A::Vec<P>>
112	where
113		P: PackedField<Scalar = F>,
114		NTT: AdditiveNTT<Field = F> + Sync,
115		A: Allocator,
116	{
117		assert_eq!(
118			ntt.subspace(self.log_len()),
119			self.subspace(),
120			"precondition: NTT subspace must match code subspace"
121		);
122		assert_eq!(
123			data.log_len(),
124			self.log_dim() + log_batch_size,
125			"precondition: data.log_len() must equal log_dim() + log_batch_size"
126		);
127
128		let _scope = tracing::trace_span!(
129			"Reed-Solomon encode",
130			log_len = self.log_len(),
131			log_batch_size = log_batch_size,
132			symbol_bits = F::N_BITS,
133		)
134		.entered();
135
136		// The forward transform below skips its first `log_inv_rate` layers.
137		// Each skipped layer would butterfly a coefficient with a zero pad:
138		//
139		//     u += v * twiddle; v += u;   with v = 0   =>   (c, 0) -> (c, c)
140		//
141		// That is one doubling per layer, so repeating the message does the skipped work.
142		let log_output_len = self.log_dim() + log_batch_size + self.log_inv_rate;
143		let mut output = FieldBuffer::from_view_with_capacity_in(alloc, data, log_output_len);
144
145		// Permute the message once, then repeat it, so every copy inherits the permutation.
146		bit_reverse_packed(output.as_mut_view());
147		output.repeat_extend(log_output_len);
148
149		ntt.forward_transform(output.as_mut_view(), self.log_inv_rate, log_batch_size);
150		output
151	}
152}
153
154#[cfg(test)]
155mod tests {
156	use binius_compute::GlobalAllocator;
157	use binius_field::{BinaryField, PackedField, PackedGhash1x128b, PackedGhash4x128b};
158	use rand::{SeedableRng, rngs::StdRng};
159
160	use super::*;
161	use crate::{
162		FieldBuffer,
163		bit_reverse::reverse_bits,
164		ntt::{NeighborsLastReference, domain_context::GaoMateerPreExpanded},
165		test_utils::random_field_buffer,
166	};
167
168	fn test_encode_batch_helper<P: PackedField>(
169		log_dim: usize,
170		log_inv_rate: usize,
171		log_batch_size: usize,
172	) where
173		P::Scalar: BinaryField,
174	{
175		let mut rng = StdRng::seed_from_u64(0);
176
177		let rs_code = ReedSolomonCode::<P::Scalar>::new(log_dim, log_inv_rate);
178
179		// The code's domain is the Gao-Mateer basis of its length, so the NTT generates the same.
180		let domain_context = GaoMateerPreExpanded::<P::Scalar>::generate(rs_code.log_len());
181		let ntt = NeighborsLastReference {
182			domain_context: &domain_context,
183		};
184
185		// Generate random message buffer
186		let message = random_field_buffer::<P>(&mut rng, log_dim + log_batch_size);
187
188		// Test the new encode_batch interface
189		let encoded_buffer =
190			rs_code.encode_batch(&ntt, message.as_view(), log_batch_size, &GlobalAllocator);
191
192		// Method 2: Reference implementation - apply NTT with zero-padded coefficients to the
193		// bit-reversal permuted message.
194		let mut reference_buffer = FieldBuffer::zeros(rs_code.log_len() + log_batch_size);
195		for (i, val) in message.iter_scalars().enumerate() {
196			let bits = (rs_code.log_dim() + log_batch_size) as u32;
197			reference_buffer.set(reverse_bits(i, bits), val);
198		}
199
200		// Perform large NTT with zero-padded coefficients.
201		ntt.forward_transform(reference_buffer.as_mut_view(), 0, log_batch_size);
202
203		// Compare results
204		assert_eq!(
205			encoded_buffer.as_ref(),
206			reference_buffer.as_ref(),
207			"encode_batch_inplace result differs from reference NTT implementation"
208		);
209	}
210
211	#[test]
212	fn test_encode_batch_above_packing_width() {
213		// Test with PackedGhash1x128b
214		test_encode_batch_helper::<PackedGhash1x128b>(4, 2, 0);
215		test_encode_batch_helper::<PackedGhash1x128b>(6, 2, 1);
216		test_encode_batch_helper::<PackedGhash1x128b>(8, 3, 2);
217
218		// Test with PackedGhash4x128b
219		test_encode_batch_helper::<PackedGhash4x128b>(4, 2, 0);
220		test_encode_batch_helper::<PackedGhash4x128b>(6, 2, 1);
221		test_encode_batch_helper::<PackedGhash4x128b>(8, 3, 2);
222	}
223
224	#[test]
225	fn test_encode_batch_below_packing_width() {
226		// Test where message length is less than the packing width and codeword length is greater.
227		test_encode_batch_helper::<PackedGhash4x128b>(1, 2, 0);
228	}
229
230	/// Pins the codeword-duplication identity that underlies Lifted FRI (oracle padding).
231	///
232	/// Lifting a message `π` of dimension `m` to a larger dimension `M = m + η` zero-pads it on
233	/// the most-significant hypercube coordinates (`ZeroPadMSB_η`). The novel-basis / bit-reversed
234	/// encoding turns this into a *duplication* of the codeword: encoding the lifted message over
235	/// the dimension-`M` code yields each entry of the dimension-`m` codeword repeated `2^η` times.
236	/// This test asserts the contiguous form `Enc_M(ZeroPadMSB_η(π))[j] == Enc_m(π)[j >> η]`, which
237	/// is the index translation Lifted FRI's prover and verifier rely on.
238	fn test_lift_duplicate_identity_helper<P: PackedField>(
239		log_dim_small: usize,
240		log_dim_large: usize,
241		log_inv_rate: usize,
242	) where
243		P::Scalar: BinaryField,
244	{
245		assert!(log_dim_small <= log_dim_large);
246		let eta = log_dim_large - log_dim_small;
247
248		let mut rng = StdRng::seed_from_u64(0);
249
250		// One shared NTT covers the larger code. Both codes evaluate over the Gao-Mateer basis, and
251		// the smaller one's is a prefix of the larger one's, which is what the shared twiddles
252		// expect -- a property the codes now have by construction rather than by wiring.
253		let domain_context =
254			GaoMateerPreExpanded::<P::Scalar>::generate(log_dim_large + log_inv_rate);
255		let ntt = NeighborsLastReference {
256			domain_context: &domain_context,
257		};
258
259		let rs_small = ReedSolomonCode::new(log_dim_small, log_inv_rate);
260		let rs_large = ReedSolomonCode::new(log_dim_large, log_inv_rate);
261
262		// Random message for the small code.
263		let msg_small = random_field_buffer::<P>(&mut rng, log_dim_small);
264
265		// ZeroPadMSB lift: the small message occupies the low `2^log_dim_small` hypercube values,
266		// the high coordinates are zero.
267		let mut msg_large = FieldBuffer::<P>::zeros(log_dim_large);
268		for (i, val) in msg_small.iter_scalars().enumerate() {
269			msg_large.set(i, val);
270		}
271
272		let enc_small = rs_small.encode_batch(&ntt, msg_small.as_view(), 0, &GlobalAllocator);
273		let enc_large = rs_large.encode_batch(&ntt, msg_large.as_view(), 0, &GlobalAllocator);
274
275		let small_scalars = enc_small.iter_scalars().collect::<Vec<_>>();
276		let large_scalars = enc_large.iter_scalars().collect::<Vec<_>>();
277		assert_eq!(small_scalars.len(), 1 << (log_dim_small + log_inv_rate));
278		assert_eq!(large_scalars.len(), 1 << (log_dim_large + log_inv_rate));
279
280		for (j, &large) in large_scalars.iter().enumerate() {
281			assert_eq!(
282				large,
283				small_scalars[j >> eta],
284				"lift identity failed at index {j} (eta = {eta})"
285			);
286		}
287	}
288
289	#[test]
290	fn test_lift_duplicate_identity() {
291		// eta = 0 degrades to plain equality.
292		test_lift_duplicate_identity_helper::<PackedGhash1x128b>(6, 6, 2);
293		// Non-trivial lifts of varying sizes.
294		test_lift_duplicate_identity_helper::<PackedGhash1x128b>(4, 6, 2);
295		test_lift_duplicate_identity_helper::<PackedGhash1x128b>(2, 8, 1);
296		test_lift_duplicate_identity_helper::<PackedGhash1x128b>(0, 4, 3);
297		// Same lifts with a wider packing width.
298		test_lift_duplicate_identity_helper::<PackedGhash4x128b>(4, 8, 2);
299	}
300}