Skip to main content

commonware_cryptography/reed_solomon/engine/
tables.rs

1//! Lookup tables used by [`Engine`] implementations.
2//!
3//! All tables are global. With `std` each is initialized at most once. Without `std`, threads
4//! racing on first use may each initialize a table, but all of them read the one copy that is kept.
5//!
6//! # Tables
7//!
8//! | Table        | Size    | Used in encoding | Used in decoding | By engines         |
9//! | ------------ | ------- | ---------------- | ---------------- | ------------------ |
10//! | [`Exp`]      | 128 KiB | yes              | yes              | all                |
11//! | [`Log`]      | 128 KiB | yes              | yes              | all                |
12//! | [`LogWalsh`] | 128 KiB | -                | yes              | all                |
13//! | Short Walsh  | < 128 KiB | -              | yes              | all                |
14//! | [`Mul16`]    | 8 MiB   | yes              | yes              | [`Scalar`]         |
15//! | [`Mul128`]   | 8 MiB   | yes              | yes              | `Neon` `Avx2`      |
16//! | `MulGfni`    | 2 MiB   | yes              | yes              | `Avx512` |
17//! | [`Skew`]     | 128 KiB | yes              | yes              | all                |
18//!
19//! [`LogWalsh`] serves decoding domains of `GF_ORDER` positions. A smaller power-of-two
20//! domain of `n` positions uses an `n`-entry short Walsh kernel, built on first use for
21//! each `n`.
22//!
23//! [`Scalar`]: crate::reed_solomon::engine::Scalar
24//! [`Engine`]: crate::reed_solomon::engine
25
26use crate::reed_solomon::engine::{
27    CANTOR_BASIS, GF_BITS, GF_MODULUS, GF_ORDER, GF_POLYNOMIAL, GfElement, fwht, utils,
28};
29#[cfg(not(feature = "std"))]
30use alloc::boxed::Box;
31#[cfg(not(feature = "std"))]
32use alloc::vec;
33#[cfg(not(feature = "std"))]
34use alloc::vec::Vec;
35#[cfg(not(feature = "std"))]
36use once_cell::race::OnceBox;
37#[cfg(feature = "std")]
38use std::sync::{LazyLock, OnceLock};
39
40/// Used by [`Naive`] engine for multiplications
41/// and by all [`Engine`] implementations to initialize other tables.
42///
43/// Maps a logarithm to its field element. Entries `0` and `GF_MODULUS` both hold one.
44///
45/// [`Naive`]: crate::reed_solomon::engine::Naive
46/// [`Engine`]: crate::reed_solomon::engine
47pub type Exp = [GfElement; GF_ORDER];
48
49/// Used by [`Naive`] engine for multiplications
50/// and by all [`Engine`] implementations to initialize other tables.
51///
52/// Maps a field element to its logarithm in `0..GF_MODULUS`. Zero has no logarithm and maps
53/// to `GF_MODULUS`.
54///
55/// [`Naive`]: crate::reed_solomon::engine::Naive
56/// [`Engine`]: crate::reed_solomon::engine
57pub type Log = [GfElement; GF_ORDER];
58
59/// Used by `Neon` and `Avx2` engines for multiplications.
60///
61/// Indexed by multiplier logarithm.
62pub type Mul128 = [Multiply128lutT; GF_ORDER];
63
64/// GFNI affine matrices indexed by multiplier logarithm.
65///
66/// Entry `GF_MODULUS` duplicates the identity at entry zero.
67#[cfg(any(test, target_arch = "x86", target_arch = "x86_64"))]
68pub(crate) type MulGfni = [MultiplyGfni; GF_ORDER];
69
70/// GF2P8AFFINEQB matrices for multiplication by one field element.
71///
72/// Multiplication is a 16 x 16 binary linear map, split into four 8 x 8 maps between
73/// the input and output byte halves. In each matrix, byte `7 - i` holds the row for output
74/// bit `i`, and bit `j` of that row selects input bit `j`.
75#[cfg(any(test, target_arch = "x86", target_arch = "x86_64"))]
76#[derive(Clone, Debug)]
77pub(crate) struct MultiplyGfni {
78    /// Maps the low input byte to the low output byte.
79    pub(crate) low_from_low: u64,
80    /// Maps the high input byte to the low output byte.
81    pub(crate) low_from_high: u64,
82    /// Maps the low input byte to the high output byte.
83    pub(crate) high_from_low: u64,
84    /// Maps the high input byte to the high output byte.
85    pub(crate) high_from_high: u64,
86}
87
88/// Multiplication lookup bytes for the four nibbles of a field element.
89///
90/// Byte `x` of `lo[i].to_ne_bytes()` and `hi[i].to_ne_bytes()` holds the low and high byte
91/// of the product of `x << (4 * i)` and the multiplier.
92#[derive(Clone, Debug)]
93pub struct Multiply128lutT {
94    /// Low product bytes for each nibble position.
95    pub lo: [u128; 4],
96    /// High product bytes for each nibble position.
97    pub hi: [u128; 4],
98}
99
100/// Used by all [`Engine`] implementations in [`Engine::eval_poly`].
101///
102/// [`Engine`]: crate::reed_solomon::engine
103/// [`Engine::eval_poly`]: crate::reed_solomon::engine::Engine::eval_poly
104pub type LogWalsh = [GfElement; GF_ORDER];
105
106/// Used by [`Scalar`] engine for multiplications.
107///
108/// Entry `[log_m][i][x]` is the product of `x << (4 * i)` and the element with logarithm
109/// `log_m`.
110///
111/// [`Scalar`]: crate::reed_solomon::engine::Scalar
112pub type Mul16 = [[[GfElement; 16]; 4]; GF_ORDER];
113
114/// Used by all [`Engine`] implementations for FFT and IFFT.
115///
116/// Holds the logarithms of the butterfly coefficients. A `GF_MODULUS` entry encodes a zero
117/// coefficient.
118///
119/// [`Engine`]: crate::reed_solomon::engine
120pub type Skew = [GfElement; GF_MODULUS as usize];
121
122/// Struct holding the [`Exp`] and [`Log`] lookup tables.
123pub struct ExpLog {
124    /// Exponentiation table.
125    pub exp: Box<Exp>,
126    /// Logarithm table.
127    pub log: Box<Log>,
128}
129
130/// Lazily initialized exponentiation and logarithm tables.
131pub fn get_exp_log() -> &'static ExpLog {
132    #[cfg(feature = "std")]
133    {
134        static EXP_LOG: LazyLock<ExpLog> = LazyLock::new(initialize_exp_log);
135        &EXP_LOG
136    }
137    #[cfg(not(feature = "std"))]
138    {
139        static EXP_LOG: OnceBox<ExpLog> = OnceBox::new();
140        EXP_LOG.get_or_init(|| Box::new(initialize_exp_log()))
141    }
142}
143
144/// Lazily initialized logarithmic Walsh transform table.
145pub fn get_log_walsh() -> &'static LogWalsh {
146    #[cfg(feature = "std")]
147    {
148        static LOG_WALSH: LazyLock<Box<LogWalsh>> = LazyLock::new(initialize_log_walsh);
149        &LOG_WALSH
150    }
151    #[cfg(not(feature = "std"))]
152    {
153        static LOG_WALSH: OnceBox<LogWalsh> = OnceBox::new();
154        LOG_WALSH.get_or_init(initialize_log_walsh)
155    }
156}
157
158/// Lazily initialized logarithmic Walsh transform table over the first `n` positions.
159///
160/// Entries are scaled by `n^-1` modulo [`GF_MODULUS`] because the unnormalized transform
161/// applied twice multiplies by `n`. [`LogWalsh`] needs no scaling since `GF_ORDER` is 1
162/// modulo [`GF_MODULUS`].
163///
164/// # Panics
165///
166/// If `n` is not a power of two below `GF_ORDER`.
167pub(crate) fn get_short_log_walsh(n: usize) -> &'static [GfElement] {
168    assert!(n.is_power_of_two() && n < GF_ORDER);
169    let level = n.trailing_zeros() as usize;
170    #[cfg(feature = "std")]
171    {
172        static KERNELS: [OnceLock<Vec<GfElement>>; GF_BITS] = [const { OnceLock::new() }; GF_BITS];
173        KERNELS[level].get_or_init(|| initialize_short_log_walsh(n))
174    }
175    #[cfg(not(feature = "std"))]
176    {
177        static KERNELS: [OnceBox<Vec<GfElement>>; GF_BITS] = [const { OnceBox::new() }; GF_BITS];
178        KERNELS[level].get_or_init(|| Box::new(initialize_short_log_walsh(n)))
179    }
180}
181
182/// Lazily initialized multiplication table for the `Scalar` engine.
183pub fn get_mul16() -> &'static Mul16 {
184    #[cfg(feature = "std")]
185    {
186        static MUL16: LazyLock<Box<Mul16>> = LazyLock::new(initialize_mul16);
187        &MUL16
188    }
189    #[cfg(not(feature = "std"))]
190    {
191        static MUL16: OnceBox<Mul16> = OnceBox::new();
192        MUL16.get_or_init(initialize_mul16)
193    }
194}
195
196/// Lazily initialized multiplication table for the `Neon` and `Avx2` engines.
197pub fn get_mul128() -> &'static Mul128 {
198    #[cfg(feature = "std")]
199    {
200        static MUL128: LazyLock<Box<Mul128>> = LazyLock::new(initialize_mul128);
201        &MUL128
202    }
203    #[cfg(not(feature = "std"))]
204    {
205        static MUL128: OnceBox<Mul128> = OnceBox::new();
206        MUL128.get_or_init(initialize_mul128)
207    }
208}
209
210/// Lazily initialized GFNI affine multiplication table.
211#[cfg(any(test, target_arch = "x86", target_arch = "x86_64"))]
212pub(crate) fn get_mul_gfni() -> &'static MulGfni {
213    #[cfg(feature = "std")]
214    {
215        static MUL_GFNI: LazyLock<Box<MulGfni>> = LazyLock::new(initialize_mul_gfni);
216        &MUL_GFNI
217    }
218    #[cfg(not(feature = "std"))]
219    {
220        static MUL_GFNI: OnceBox<MulGfni> = OnceBox::new();
221        MUL_GFNI.get_or_init(initialize_mul_gfni)
222    }
223}
224
225/// Lazily initialized skew table used in FFT and IFFT operations.
226pub fn get_skew() -> &'static Skew {
227    #[cfg(feature = "std")]
228    {
229        static SKEW: LazyLock<Box<Skew>> = LazyLock::new(initialize_skew);
230        &SKEW
231    }
232    #[cfg(not(feature = "std"))]
233    {
234        static SKEW: OnceBox<Skew> = OnceBox::new();
235        SKEW.get_or_init(initialize_skew)
236    }
237}
238
239/// Multiply `x` by `exp[log_m]` using [`Exp`] and [`Log`] tables.
240#[inline(always)]
241pub fn mul(x: GfElement, log_m: GfElement, exp: &Exp, log: &Log) -> GfElement {
242    if x == 0 {
243        0
244    } else {
245        exp[utils::add_mod(log[x as usize], log_m) as usize]
246    }
247}
248
249/// Builds the [`Exp`] and [`Log`] tables for elements expressed in the [`CANTOR_BASIS`].
250fn initialize_exp_log() -> ExpLog {
251    let mut exp = Box::new([0; GF_ORDER]);
252    let mut log = Box::new([0; GF_ORDER]);
253
254    // Generate the LFSR table.
255    let mut state = 1;
256    for i in 0..GF_MODULUS {
257        exp[state] = i;
258        state <<= 1;
259        if state >= GF_ORDER {
260            state ^= GF_POLYNOMIAL;
261        }
262    }
263    exp[0] = GF_MODULUS;
264
265    // Convert to the Cantor basis.
266    log[0] = 0;
267    for (i, basis) in CANTOR_BASIS.iter().copied().enumerate().take(GF_BITS) {
268        let width = 1usize << i;
269        for j in 0..width {
270            log[j + width] = log[j] ^ basis;
271        }
272    }
273    for value in log.iter_mut() {
274        *value = exp[*value as usize];
275    }
276    for (i, value) in log.iter().copied().enumerate() {
277        exp[value as usize] = i as GfElement;
278    }
279
280    exp[GF_MODULUS as usize] = exp[0];
281
282    ExpLog { exp, log }
283}
284
285/// Builds [`LogWalsh`], the FWHT of [`Log`] with entry zero replaced by zero.
286fn initialize_log_walsh() -> Box<LogWalsh> {
287    let log = get_exp_log().log.as_slice();
288
289    let mut log_walsh: Box<LogWalsh> = Box::new([0; GF_ORDER]);
290
291    log_walsh.copy_from_slice(log);
292    log_walsh[0] = 0;
293    fwht::fwht(log_walsh.as_mut(), GF_ORDER);
294
295    log_walsh
296}
297
298/// Builds the kernel for [`get_short_log_walsh`], the FWHT of `log[..n]` with entry zero
299/// replaced by zero, scaled by `n^-1` modulo `GF_MODULUS`.
300///
301/// `n` must be a power of two no larger than `GF_ORDER`.
302fn initialize_short_log_walsh(n: usize) -> Vec<GfElement> {
303    let log = &get_exp_log().log;
304    let mut kernel = log[..n].to_vec();
305    kernel[0] = 0;
306    fwht::fwht(&mut kernel, n);
307
308    // Scale by n^-1 = GF_ORDER / n modulo GF_MODULUS. Multiplying by 2^j modulo 2^16 - 1
309    // rotates left by j bits, so the scale is a right rotation by log2(n).
310    for factor in &mut kernel {
311        *factor = factor.rotate_right(n.trailing_zeros());
312    }
313    kernel
314}
315
316/// Builds [`Mul16`] from [`Exp`] and [`Log`].
317fn initialize_mul16() -> Box<Mul16> {
318    let exp = &get_exp_log().exp;
319    let log = &get_exp_log().log;
320    let mut mul16 = vec![[[0; 16]; 4]; GF_ORDER];
321
322    for log_m in 0..=GF_MODULUS {
323        let lut = &mut mul16[log_m as usize];
324        let [row0, row1, row2, row3] = lut;
325        for (i, (((x0, x1), x2), x3)) in row0
326            .iter_mut()
327            .zip(row1.iter_mut())
328            .zip(row2.iter_mut())
329            .zip(row3.iter_mut())
330            .enumerate()
331        {
332            *x0 = mul(i as GfElement, log_m, exp, log);
333            *x1 = mul((i << 4) as GfElement, log_m, exp, log);
334            *x2 = mul((i << 8) as GfElement, log_m, exp, log);
335            *x3 = mul((i << 12) as GfElement, log_m, exp, log);
336        }
337    }
338
339    mul16.into_boxed_slice().try_into().unwrap()
340}
341
342/// Builds [`Mul128`] with the byte layout described on [`Multiply128lutT`].
343fn initialize_mul128() -> Box<Mul128> {
344    // Based on:
345    // https://github.com/catid/leopard/blob/22ddc7804998d31c8f1a2617ee720e063b1fa6cd/LeopardFF16.cpp#L375
346    let exp = &get_exp_log().exp;
347    let log = &get_exp_log().log;
348
349    let mut mul128 = vec![
350        Multiply128lutT {
351            lo: [0; 4],
352            hi: [0; 4],
353        };
354        GF_ORDER
355    ];
356
357    for log_m in 0..=GF_MODULUS {
358        for i in 0..=3 {
359            let mut prod_lo = [0u8; 16];
360            let mut prod_hi = [0u8; 16];
361            for x in 0..16 {
362                let prod = mul((x << (i * 4)) as GfElement, log_m, exp, log);
363                prod_lo[x] = prod as u8;
364                prod_hi[x] = (prod >> 8) as u8;
365            }
366            mul128[log_m as usize].lo[i] = u128::from_ne_bytes(prod_lo);
367            mul128[log_m as usize].hi[i] = u128::from_ne_bytes(prod_hi);
368        }
369    }
370
371    mul128.into_boxed_slice().try_into().unwrap()
372}
373
374/// Builds [`MulGfni`] by combining one matrix per coefficient bit along a Gray code.
375#[cfg(any(test, target_arch = "x86", target_arch = "x86_64"))]
376fn initialize_mul_gfni() -> Box<MulGfni> {
377    let exp = &get_exp_log().exp;
378    let log = &get_exp_log().log;
379    let mut table = vec![
380        MultiplyGfni {
381            low_from_low: 0,
382            low_from_high: 0,
383            high_from_low: 0,
384            high_from_high: 0,
385        };
386        GF_ORDER
387    ];
388
389    let mut basis_matrices = [[0u64; 4]; GF_BITS];
390    for (coefficient_bit, matrix) in basis_matrices.iter_mut().enumerate() {
391        let log_m = log[1 << coefficient_bit];
392        let mut rows = [[0u8; 8]; 4];
393        for input_bit in 0..16 {
394            let product = mul(1u16 << input_bit, log_m, exp, log);
395            for output_bit in 0..16 {
396                // Blocks follow the `MultiplyGfni` field order: 2 * output byte + input byte.
397                let block = (output_bit / 8) * 2 + input_bit / 8;
398                rows[block][output_bit % 8] |=
399                    (((product >> output_bit) & 1) as u8) << (input_bit % 8);
400            }
401        }
402
403        // GF2P8AFFINEQB reads row i from byte 7-i of each 64-bit matrix.
404        *matrix = rows.map(u64::from_be_bytes);
405    }
406
407    // Multiplication is linear in the coefficient. After each basis-matrix XOR, `current` is
408    // the multiplication matrix for `step ^ (step >> 1)` because consecutive binary-reflected
409    // Gray codes differ in bit `step.trailing_zeros()`.
410    let mut current = [0u64; 4];
411    for step in 1..GF_ORDER {
412        let toggled_bit = step.trailing_zeros() as usize;
413        for (block, basis) in current.iter_mut().zip(basis_matrices[toggled_bit]) {
414            *block ^= basis;
415        }
416
417        let coefficient = step ^ (step >> 1);
418        let [low_from_low, low_from_high, high_from_low, high_from_high] = current;
419        table[log[coefficient] as usize] = MultiplyGfni {
420            low_from_low,
421            low_from_high,
422            high_from_low,
423            high_from_high,
424        };
425    }
426
427    // Exponents are taken modulo `GF_MODULUS`, the multiplicative group order, so exponent
428    // `GF_MODULUS` intentionally duplicates the identity at exponent zero.
429    let identity = table[0].clone();
430    table[GF_MODULUS as usize] = identity;
431
432    table.into_boxed_slice().try_into().unwrap()
433}
434
435/// Builds [`Skew`], the logarithms of the FFT butterfly coefficients.
436///
437/// A zero coefficient is stored as `GF_MODULUS`, the logarithm [`Log`] assigns to zero.
438fn initialize_skew() -> Box<Skew> {
439    let exp = &get_exp_log().exp;
440    let log = &get_exp_log().log;
441
442    let mut skew = Box::new([0; GF_MODULUS as usize]);
443
444    let mut temp = [0; GF_BITS - 1];
445
446    for (i, value) in temp.iter_mut().enumerate() {
447        *value = 1 << (i + 1);
448    }
449
450    for m in 0..GF_BITS - 1 {
451        let step: usize = 1 << (m + 1);
452
453        skew[(1 << m) - 1] = 0;
454
455        for (i, temp_i) in temp.iter().copied().enumerate().skip(m) {
456            let s: usize = 1 << (i + 1);
457            let mut j = (1 << m) - 1;
458            while j < s {
459                skew[j + s] = skew[j] ^ temp_i;
460                j += step;
461            }
462        }
463
464        temp[m] = GF_MODULUS - log[mul(temp[m], log[(temp[m] ^ 1) as usize], exp, log) as usize];
465
466        for i in m + 1..GF_BITS - 1 {
467            let sum = utils::add_mod(log[(temp[i] ^ 1) as usize], temp[m]);
468            temp[i] = mul(temp[i], sum, exp, log);
469        }
470    }
471
472    for value in skew.iter_mut() {
473        *value = log[*value as usize];
474    }
475
476    skew
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482
483    #[test]
484    fn mul128_byte_layout() {
485        let scalar = get_mul16();
486        for (vector, scalar) in get_mul128().iter().zip(scalar.iter()) {
487            for ((lo, hi), products) in vector.lo.iter().zip(&vector.hi).zip(scalar) {
488                let lo = lo.to_ne_bytes();
489                let hi = hi.to_ne_bytes();
490                for (index, product) in products.iter().enumerate() {
491                    assert_eq!(lo[index], *product as u8);
492                    assert_eq!(hi[index], (product >> 8) as u8);
493                }
494            }
495        }
496    }
497
498    /// Reference GF2P8AFFINEQB on one byte, without the affine constant.
499    fn affine(matrix: u64, input: u8) -> u8 {
500        let matrix = matrix.to_le_bytes();
501        let mut output = 0;
502        for output_bit in 0..8 {
503            output |= ((matrix[7 - output_bit] & input).count_ones() as u8 & 1) << output_bit;
504        }
505        output
506    }
507
508    #[test]
509    fn mul_gfni_matrix_semantics() {
510        assert_eq!(
511            core::mem::size_of::<MultiplyGfni>(),
512            4 * core::mem::size_of::<u64>()
513        );
514        assert_eq!(core::mem::size_of::<MulGfni>(), 2 * 1024 * 1024);
515
516        let exp_log = get_exp_log();
517        for (log_m, matrices) in get_mul_gfni().iter().enumerate() {
518            for input_bit in 0..16 {
519                let input = 1u16 << input_bit;
520                let input_lo = input as u8;
521                let input_hi = (input >> 8) as u8;
522                let output_lo = affine(matrices.low_from_low, input_lo)
523                    ^ affine(matrices.low_from_high, input_hi);
524                let output_hi = affine(matrices.high_from_low, input_lo)
525                    ^ affine(matrices.high_from_high, input_hi);
526                let actual = output_lo as u16 | (output_hi as u16) << 8;
527                let expected = mul(input, log_m as GfElement, &exp_log.exp, &exp_log.log);
528                assert_eq!(actual, expected, "log_m={log_m} input_bit={input_bit}");
529            }
530        }
531
532        for log_m in [0, GF_MODULUS as usize] {
533            let identity = &get_mul_gfni()[log_m];
534            assert_eq!(identity.low_from_low, 0x0102_0408_1020_4080);
535            assert_eq!(identity.low_from_high, 0);
536            assert_eq!(identity.high_from_low, 0);
537            assert_eq!(identity.high_from_high, 0x0102_0408_1020_4080);
538        }
539    }
540}