Skip to main content

logicaffeine_base/
fec.rs

1//! Reed-Solomon forward error correction — the erasure codec that lets a lossy
2//! transport reconstruct dropped packets *without* a retransmit round-trip.
3//!
4//! Given `k` data shards we compute `m` parity shards; the `n = k + m` shards are
5//! **Maximum Distance Separable (MDS)**: *any* `k` of the `n` shards reconstruct the
6//! original data exactly. Losing up to `m` shards is fully recoverable.
7//!
8//! The construction is a *systematic* Reed-Solomon code over GF(2^8) (the [`crate::gf256`]
9//! field). Start from an `n×k` Vandermonde matrix `V` on `n` distinct field nodes
10//! `0,1,…,n-1`: every `k×k` submatrix of a Vandermonde on distinct nodes is invertible
11//! (its determinant is `∏_{a<b}(x_b − x_a) ≠ 0`), which is exactly the MDS property.
12//! Right-multiplying by the inverse of `V`'s top `k×k` block yields the generator
13//! `G = V · V_top⁻¹`, whose top `k` rows are the identity — so the first `k` encoded
14//! shards *are* the data (systematic), the rest are parity, and every `k`-row submatrix
15//! stays invertible (invertible · invertible). Encoding is `G` applied per byte-column;
16//! erasure recovery inverts the `k×k` submatrix of the surviving rows. All exact, no
17//! floating point.
18
19use crate::gf256;
20
21/// A systematic Reed-Solomon erasure code with `k` data shards and `m = n − k` parity
22/// shards. Cheap to clone (holds only the `n×k` generator matrix); construct once with
23/// [`ReedSolomon::new`] and reuse across many messages of the same shape.
24#[derive(Clone, Debug)]
25pub struct ReedSolomon {
26    k: usize,
27    n: usize,
28    /// The `n×k` systematic generator: rows `0..k` are the identity, rows `k..n` parity.
29    generator: Vec<Vec<u8>>,
30}
31
32impl ReedSolomon {
33    /// Build a code with `data_shards` data shards and `parity_shards` parity shards.
34    /// Returns `None` if the parameters are out of range: both counts must be at least
35    /// one and the total `n = data + parity` must fit the field's distinct nodes
36    /// (`n ≤ 256`). The construction is deterministic — same parameters, same code.
37    pub fn new(data_shards: usize, parity_shards: usize) -> Option<Self> {
38        let k = data_shards;
39        let n = k.checked_add(parity_shards)?;
40        if k == 0 || parity_shards == 0 || n > 256 {
41            return None;
42        }
43        // Vandermonde on nodes 0..n-1: row i, column j = node_i^j.
44        let vander: Vec<Vec<u8>> = (0..n)
45            .map(|i| {
46                let node = i as u8;
47                (0..k).map(|j| gf256::pow(node, j)).collect()
48            })
49            .collect();
50        // Systematic: divide out the top k×k block so rows 0..k become the identity.
51        let top: Vec<Vec<u8>> = vander[..k].to_vec();
52        let top_inv = gf256::invert(&top)?;
53        let generator = gf256::matmul(&vander, &top_inv);
54        Some(Self { k, n, generator })
55    }
56
57    /// The number of data shards `k`.
58    pub fn data_shards(&self) -> usize {
59        self.k
60    }
61
62    /// The number of parity shards `m = n − k`.
63    pub fn parity_shards(&self) -> usize {
64        self.n - self.k
65    }
66
67    /// The total number of shards `n = k + m`.
68    pub fn total_shards(&self) -> usize {
69        self.n
70    }
71
72    /// Encode `k` equal-length data shards into `n` shards. The first `k` outputs are the
73    /// data shards unchanged (the code is systematic); the remaining `m` are parity.
74    /// Returns `None` unless exactly `k` shards of equal length are supplied.
75    pub fn encode(&self, data: &[Vec<u8>]) -> Option<Vec<Vec<u8>>> {
76        if data.len() != self.k {
77            return None;
78        }
79        let len = data.first().map_or(0, |s| s.len());
80        if data.iter().any(|s| s.len() != len) {
81            return None;
82        }
83        Some(self.apply(&self.generator, data, len))
84    }
85
86    /// Reconstruct all `n` shards from a slice of `n` slots, each `Some(shard)` if the
87    /// shard survived or `None` if it was erased. Succeeds when at least `k` shards
88    /// survive (and share a length); returns `None` otherwise. The result is the full,
89    /// byte-exact set of `n` shards — recovered data in `0..k`, regenerated parity after.
90    pub fn reconstruct(&self, shards: &[Option<Vec<u8>>]) -> Option<Vec<Vec<u8>>> {
91        if shards.len() != self.n {
92            return None;
93        }
94        let present: Vec<usize> = (0..self.n).filter(|&i| shards[i].is_some()).collect();
95        if present.len() < self.k {
96            return None;
97        }
98        let len = shards[present[0]].as_ref().unwrap().len();
99        if present
100            .iter()
101            .any(|&i| shards[i].as_ref().unwrap().len() != len)
102        {
103            return None;
104        }
105        // Any k surviving rows suffice; the k×k submatrix they form is invertible (MDS).
106        let sel: Vec<usize> = present[..self.k].to_vec();
107        let sub: Vec<Vec<u8>> = sel.iter().map(|&i| self.generator[i].clone()).collect();
108        let sub_inv = gf256::invert(&sub)?;
109        let survived: Vec<Vec<u8>> = sel
110            .iter()
111            .map(|&i| shards[i].as_ref().unwrap().clone())
112            .collect();
113        // data = (G_sel)⁻¹ · survived, per byte-column.
114        let data = self.apply(&sub_inv, &survived, len);
115        // Re-encode to fill in every shard (data + regenerated parity), all byte-exact.
116        self.encode(&data)
117    }
118
119    /// Apply an `r×k` matrix to `k` shards of length `len`, producing `r` shards: output
120    /// row `i`, byte `b` = `Σ_j matrix[i][j] · shards[j][b]` over GF(2^8).
121    fn apply(&self, matrix: &[Vec<u8>], shards: &[Vec<u8>], len: usize) -> Vec<Vec<u8>> {
122        matrix
123            .iter()
124            .map(|row| {
125                // `out[b] = Σ_j row[j]·shards[j][b]` accumulated one whole shard at a time via the
126                // SIMD-friendly region multiply — the vectorizable RS hot path. Zero coefficients
127                // contribute nothing, so they are skipped (sparse rows get faster still).
128                let mut out = vec![0u8; len];
129                for (j, &coeff) in row.iter().enumerate() {
130                    if coeff != 0 {
131                        gf256::mul_add_region(coeff, &shards[j], &mut out);
132                    }
133                }
134                out
135            })
136            .collect()
137    }
138}
139
140#[cfg(test)]
141#[path = "../../logicaffeine_tests/unit/logicaffeine_base/fec/tests.rs"]
142mod tests;