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;