Skip to main content

commonware_cryptography/reed_solomon/
engine.rs

1//! Low-level building blocks for Reed-Solomon encoding/decoding.
2//!
3//! See [basic usage] for encoding and decoding with automatic engine selection.
4//!
5//! This module is relevant if you want to
6//! - use [`rate`] module and need an [`Engine`] to use with it.
7//! - create your own [`Engine`].
8//! - understand/benchmark/test at low level.
9//!
10//! # Engines
11//!
12//! An [`Engine`] is an implementation of basic low-level algorithms
13//! needed for Reed-Solomon encoding/decoding.
14//!
15//! - [`Naive`]
16//!     - Simple reference implementation.
17//! - [`Scalar`]
18//!     - Portable engine without SIMD that works on all CPUs.
19//! - `Avx2`
20//!     - Optimized engine that takes advantage of the x86(-64) AVX2 SIMD instructions.
21//! - `Avx512`
22//!     - Optimized engine that takes advantage of the x86(-64) AVX-512F and GFNI instructions.
23//! - `Neon`
24//!     - Optimized engine that takes advantage of the `AArch64` Neon SIMD instructions.
25//! - [`DefaultEngine`]
26//!     - Default engine which is used when no specific engine is given.
27//!     - Automatically selects best engine at runtime.
28//!
29//! [basic usage]: crate::reed_solomon#basic-usage
30//! [`Encoder`]: crate::reed_solomon::Encoder
31//! [`Decoder`]: crate::reed_solomon::Decoder
32//! [`rate`]: crate::reed_solomon::rate
33
34/// Runtime CPU feature detection used to select and guard SIMD engines.
35///
36/// Each function returns whether the current CPU supports the named engine's features.
37mod cpu_features {
38    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
39    cpufeatures::new!(has_avx512, "avx512f", "gfni");
40    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
41    cpufeatures::new!(has_avx2, "avx2");
42    #[cfg(target_arch = "aarch64")]
43    cpufeatures::new!(has_neon, "neon");
44
45    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
46    pub(super) use self::{has_avx2::get as avx2, has_avx512::get as avx512};
47    #[cfg(target_arch = "aarch64")]
48    pub(super) use has_neon::get as neon;
49}
50
51#[cfg(target_arch = "aarch64")]
52pub use self::engine_neon::Neon;
53pub(crate) use self::shards::Shards;
54#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
55pub use self::{engine_avx2::Avx2, engine_avx512::Avx512};
56pub use self::{
57    engine_default::DefaultEngine, engine_naive::Naive, engine_scalar::Scalar, shards::ShardsRefMut,
58};
59pub(crate) use utils::{fft_skew_end, formal_derivative, ifft_skew_end, xor_within};
60
61mod engine_default;
62mod engine_naive;
63mod engine_scalar;
64
65#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
66mod engine_avx2;
67#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
68mod engine_avx512;
69
70#[cfg(target_arch = "aarch64")]
71mod engine_neon;
72
73mod fwht;
74mod shards;
75
76pub mod tables;
77pub mod utils;
78
79/// Size of Galois field element [`GfElement`] in bits.
80pub const GF_BITS: usize = 16;
81
82/// Galois field order, i.e. number of elements.
83pub const GF_ORDER: usize = 65536;
84
85/// `GF_ORDER - 1`
86pub const GF_MODULUS: GfElement = 65535;
87
88/// Galois field polynomial.
89pub const GF_POLYNOMIAL: usize = 0x1002D;
90
91/// Byte width of a shard chunk.
92///
93/// [`Engine`] methods process shard buffers as arrays of this size.
94/// Input shards may span multiple chunks. Any partial final chunk is padded
95/// during processing and returned at the original shard length.
96pub const SHARD_CHUNK_BYTES: usize = 64;
97
98/// Cantor basis used by the additive FFT over GF(2^16).
99pub const CANTOR_BASIS: [GfElement; GF_BITS] = [
100    0x0001, 0xACCA, 0x3C0E, 0x163E, 0xC582, 0xED2E, 0x914C, 0x4012, 0x6C98, 0x10D8, 0x6A72, 0xB900,
101    0xFDB8, 0xFB34, 0xFF38, 0x991E,
102];
103
104/// Galois field element expressed in the [`CANTOR_BASIS`].
105pub type GfElement = u16;
106
107/// Trait for compute-intensive low-level algorithms needed
108/// for Reed-Solomon encoding/decoding.
109///
110/// This is the trait you would implement to provide SIMD support
111/// for a CPU architecture not already provided.
112///
113/// [`Naive`] engine is provided for those who want to
114/// study the source code to understand [`Engine`].
115pub trait Engine {
116    /// In-place decimation-in-time FFT (fast Fourier transform).
117    ///
118    /// Transforms `data[pos..pos + size]`, producing the requested output prefix in
119    /// `data[pos..pos + truncated_size]`. The remaining shards in the transform block have
120    /// unspecified values. Shards outside the block are unchanged.
121    ///
122    /// A transform of size one is the identity and ignores `skew_delta`.
123    ///
124    /// # Panics
125    ///
126    /// If `size` is not a power of two in `1..=GF_ORDER`, `truncated_size > size`, or the
127    /// transform block is outside `data`. For `size > 1`, also panics if
128    /// `skew_delta > GF_ORDER - size`.
129    fn fft(
130        &self,
131        data: &mut ShardsRefMut<'_>,
132        pos: usize,
133        size: usize,
134        truncated_size: usize,
135        skew_delta: usize,
136    );
137
138    /// In-place decimation-in-time IFFT (inverse fast Fourier transform).
139    ///
140    /// Transforms `data[pos..pos + size]`. Input shards in
141    /// `data[pos + truncated_size..pos + size]` must be zero. The full transform block then
142    /// contains the inverse result. Shards outside the block are unchanged.
143    ///
144    /// A transform of size one is the identity and ignores `skew_delta`.
145    ///
146    /// # Panics
147    ///
148    /// If the size, range, or skew requirements of [`Engine::fft`] are not met.
149    fn ifft(
150        &self,
151        data: &mut ShardsRefMut<'_>,
152        pos: usize,
153        size: usize,
154        truncated_size: usize,
155        skew_delta: usize,
156    );
157
158    /// Multiply every field element in `x` by the field element with logarithm `log_m`.
159    ///
160    /// Each chunk stores 32 low bytes followed by their 32 high bytes.
161    /// Exponents `0` and [`GF_MODULUS`] both represent the multiplicative identity.
162    fn mul(&self, x: &mut [[u8; SHARD_CHUNK_BYTES]], log_m: GfElement);
163
164    /// Replace `erasures` with its XOR convolution against the field logarithm table.
165    ///
166    /// On return, `erasures[i]` is congruent modulo [`GF_MODULUS`] to the sum over `j != i`
167    /// of `erasures[j] * log(i ^ j)`. For a 0/1 erasure indicator, this is the logarithm of
168    /// the product of `i ^ j` over erased `j != i`.
169    ///
170    /// Entries at and after `truncated_size` must be zero.
171    ///
172    /// # Panics
173    ///
174    /// If `truncated_size > GF_ORDER`.
175    fn eval_poly(erasures: &mut [GfElement; GF_ORDER], truncated_size: usize)
176    where
177        Self: Sized,
178    {
179        utils::eval_poly(erasures, truncated_size);
180    }
181}
182
183/// Assert the [`Engine::fft`] and [`Engine::ifft`] preconditions.
184#[inline]
185fn validate_transform(
186    data: &ShardsRefMut<'_>,
187    pos: usize,
188    size: usize,
189    truncated_size: usize,
190    skew_delta: usize,
191) {
192    assert!(size.is_power_of_two() && size <= GF_ORDER);
193    assert!(truncated_size <= size);
194    assert!(pos <= data.len() && size <= data.len() - pos);
195
196    // Butterflies read skew entries up to `size + skew_delta - 2`, and `Skew` has
197    // `GF_ORDER - 1` entries. A size-one transform has no butterflies.
198    assert!(size == 1 || skew_delta <= GF_ORDER - size);
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    use std::panic::{AssertUnwindSafe, catch_unwind};
205
206    /// Every engine the host supports.
207    fn engines() -> Vec<Box<dyn Engine>> {
208        let mut engines: Vec<Box<dyn Engine>> =
209            vec![Box::new(Scalar::new()), Box::new(Naive::new())];
210        #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
211        {
212            if cpu_features::avx512() {
213                engines.push(Box::new(Avx512::new()));
214            }
215            if cpu_features::avx2() {
216                engines.push(Box::new(Avx2::new()));
217            }
218        }
219        #[cfg(target_arch = "aarch64")]
220        if cpu_features::neon() {
221            engines.push(Box::new(Neon::new()));
222        }
223        engines
224    }
225
226    /// Asserts that every engine panics on an FFT and an IFFT with the given arguments
227    /// over `shard_count` zero-length shards.
228    fn invalid_transform(
229        shard_count: usize,
230        pos: usize,
231        size: usize,
232        truncated_size: usize,
233        skew_delta: usize,
234    ) {
235        for engine in engines() {
236            for inverse in [false, true] {
237                let mut shards = ShardsRefMut::new(shard_count, 0, &mut []);
238                let result = catch_unwind(AssertUnwindSafe(|| {
239                    if inverse {
240                        engine.ifft(&mut shards, pos, size, truncated_size, skew_delta);
241                    } else {
242                        engine.fft(&mut shards, pos, size, truncated_size, skew_delta);
243                    }
244                }));
245                assert!(
246                    result.is_err(),
247                    "invalid transform accepted (inverse={inverse})"
248                );
249            }
250        }
251    }
252
253    #[test]
254    fn transform_zero_size() {
255        invalid_transform(4, 0, 0, 0, 0);
256    }
257
258    #[test]
259    fn transform_non_power_of_two_size() {
260        invalid_transform(4, 0, 3, 0, 0);
261    }
262
263    #[test]
264    fn transform_size_exceeds_field() {
265        invalid_transform(GF_ORDER * 2, 0, GF_ORDER * 2, 0, 0);
266    }
267
268    #[test]
269    fn transform_range_out_of_bounds() {
270        for pos in [3, 4, usize::MAX] {
271            invalid_transform(4, pos, 2, 0, 0);
272        }
273    }
274
275    #[test]
276    fn transform_truncation_out_of_bounds() {
277        invalid_transform(4, 0, 2, 3, 0);
278    }
279
280    #[test]
281    fn transform_skew_out_of_bounds() {
282        for skew_delta in [GF_ORDER - 1, usize::MAX] {
283            invalid_transform(4, 0, 2, 0, skew_delta);
284        }
285    }
286
287    #[test]
288    fn transform_empty_shards_and_identity() {
289        for engine in engines() {
290            let mut empty = ShardsRefMut::new(GF_ORDER + 3, 0, &mut []);
291            engine.fft(&mut empty, 3, GF_ORDER, 0, 0);
292            engine.ifft(&mut empty, 3, GF_ORDER, 0, 0);
293
294            for truncated_size in [0, 1] {
295                let mut data = [[17; SHARD_CHUNK_BYTES]; 3];
296                let mut shards = ShardsRefMut::new(3, 1, &mut data);
297                engine.fft(&mut shards, 1, 1, truncated_size, usize::MAX);
298                engine.ifft(&mut shards, 1, 1, 1, usize::MAX);
299                assert_eq!(data, [[17; SHARD_CHUNK_BYTES]; 3]);
300            }
301        }
302    }
303
304    #[test]
305    fn eval_poly_feature_guard() {
306        let mut input = Box::new([0; GF_ORDER]);
307        input[..4].copy_from_slice(&[1, 0, 1, 1]);
308        let mut expected = input.clone();
309        Scalar::eval_poly(&mut expected, 4);
310
311        type Evaluate = fn(&mut [GfElement; GF_ORDER], usize);
312        let evaluators: &[(Evaluate, bool)] = &[
313            (DefaultEngine::eval_poly, true),
314            #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
315            (Avx2::eval_poly, cpu_features::avx2()),
316            #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
317            (Avx512::eval_poly, cpu_features::avx512()),
318            #[cfg(target_arch = "aarch64")]
319            (Neon::eval_poly, cpu_features::neon()),
320        ];
321
322        for (evaluate, supported) in evaluators {
323            let mut actual = input.clone();
324            if *supported {
325                evaluate(&mut actual, 4);
326                assert_eq!(actual, expected);
327            } else {
328                assert!(catch_unwind(AssertUnwindSafe(|| evaluate(&mut actual, 4))).is_err());
329                assert_eq!(actual, input);
330            }
331        }
332    }
333
334    #[test]
335    #[should_panic]
336    fn eval_poly_truncation_out_of_bounds() {
337        utils::eval_poly(&mut [0; GF_ORDER], GF_ORDER + 1);
338    }
339}