Skip to main content

logicaffeine_compile/concurrency/
fec.rs

1//! Reed-Solomon erasure coding over GF(2^8) — the wire codec's `redundant` knob.
2//!
3//! Split a payload into `k` data shards plus `n − k` parity shards; a receiver
4//! reconstructs the EXACT payload from ANY `k` of the `n` shards. This is the
5//! reconstructable axis no general wire format ships: drop, duplicate, or reorder up to
6//! `n − k` shards on a lossy link (UDP / multicast / BLE / LoRa) and the message still
7//! arrives, with no retransmit and no coordination.
8//!
9//! The code is **systematic** (the first `k` shards ARE the data chunks, so lossless
10//! delivery costs no decode) and **MDS** (any `k` shards suffice). The encoding matrix
11//! is a Vandermonde matrix made systematic by multiplying through the inverse of its own
12//! top `k×k` block: every `k`-row subset stays invertible, which is exactly the
13//! any-`k`-of-`n` guarantee.
14
15use std::sync::OnceLock;
16
17/// GF(2^8) log/exp tables under the primitive polynomial x^8 + x^4 + x^3 + x^2 + 1
18/// (0x11d). `exp` is doubled to 512 entries so a `log[a] + log[b]` index never wraps.
19struct Gf {
20    exp: [u8; 512],
21    log: [u8; 256],
22}
23
24fn gf() -> &'static Gf {
25    static GF: OnceLock<Gf> = OnceLock::new();
26    GF.get_or_init(|| {
27        let mut exp = [0u8; 512];
28        let mut log = [0u8; 256];
29        let mut x: u16 = 1;
30        for i in 0..255 {
31            exp[i] = x as u8;
32            log[x as usize] = i as u8;
33            x <<= 1;
34            if x & 0x100 != 0 {
35                x ^= 0x11d;
36            }
37        }
38        for i in 255..512 {
39            exp[i] = exp[i - 255];
40        }
41        Gf { exp, log }
42    })
43}
44
45fn mul(a: u8, b: u8) -> u8 {
46    if a == 0 || b == 0 {
47        return 0;
48    }
49    let g = gf();
50    g.exp[g.log[a as usize] as usize + g.log[b as usize] as usize]
51}
52
53/// GF division (`b` must be non-zero — every caller divides by a pivot).
54fn div(a: u8, b: u8) -> u8 {
55    if a == 0 {
56        return 0;
57    }
58    let g = gf();
59    g.exp[g.log[a as usize] as usize + 255 - g.log[b as usize] as usize]
60}
61
62/// `base^exp` in GF(2^8). `base^0 == 1` for every base (including 0).
63fn pow(base: u8, exp: usize) -> u8 {
64    let mut r = 1u8;
65    for _ in 0..exp {
66        r = mul(r, base);
67    }
68    r
69}
70
71/// Invert a `k×k` matrix over GF(2^8) by Gauss-Jordan elimination; `None` if singular.
72fn invert(m: &[Vec<u8>]) -> Option<Vec<Vec<u8>>> {
73    let k = m.len();
74    // Augment with the identity: [m | I].
75    let mut a: Vec<Vec<u8>> = m
76        .iter()
77        .enumerate()
78        .map(|(i, row)| {
79            let mut r = row.clone();
80            r.extend((0..k).map(|j| if i == j { 1 } else { 0 }));
81            r
82        })
83        .collect();
84    for col in 0..k {
85        let mut piv = col;
86        while piv < k && a[piv][col] == 0 {
87            piv += 1;
88        }
89        if piv == k {
90            return None; // singular
91        }
92        a.swap(col, piv);
93        let inv_p = div(1, a[col][col]);
94        for j in 0..2 * k {
95            a[col][j] = mul(a[col][j], inv_p);
96        }
97        for row in 0..k {
98            if row == col {
99                continue;
100            }
101            let f = a[row][col];
102            if f != 0 {
103                for j in 0..2 * k {
104                    a[row][j] ^= mul(f, a[col][j]);
105                }
106            }
107        }
108    }
109    Some(a.iter().map(|r| r[k..2 * k].to_vec()).collect())
110}
111
112/// Multiply an `n×k` matrix by a `k×c` matrix over GF(2^8).
113fn matmul(a: &[Vec<u8>], b: &[Vec<u8>]) -> Vec<Vec<u8>> {
114    let inner = b.len();
115    let cols = b[0].len();
116    a.iter()
117        .map(|row| {
118            (0..cols)
119                .map(|j| {
120                    let mut acc = 0u8;
121                    for t in 0..inner {
122                        acc ^= mul(row[t], b[t][j]);
123                    }
124                    acc
125                })
126                .collect()
127        })
128        .collect()
129}
130
131/// The systematic `n×k` encoding matrix: a Vandermonde (`V[i][j] = i^j`, distinct nodes
132/// `0..n` ⇒ any `k` rows invertible) multiplied through the inverse of its top `k×k`
133/// block, so the first `k` rows become the identity (data passes through unchanged) while
134/// the MDS property is preserved.
135fn encode_matrix(n: usize, k: usize) -> Option<Vec<Vec<u8>>> {
136    let vander: Vec<Vec<u8>> = (0..n)
137        .map(|i| (0..k).map(|j| pow(i as u8, j)).collect())
138        .collect();
139    let top = vander[0..k].to_vec();
140    let inv = invert(&top)?;
141    Some(matmul(&vander, &inv))
142}
143
144/// Encode `data` into `n` shards (`k` data + `n − k` parity). Returns the original byte
145/// length (for un-padding on decode) and the shards. `None` for a degenerate `(k, n)`
146/// (`k == 0`, `n < k`, or `n > 256` — GF(2^8) has only 256 distinct nodes).
147pub fn encode(data: &[u8], k: usize, n: usize) -> Option<(usize, Vec<Vec<u8>>)> {
148    if k == 0 || n < k || n > 256 {
149        return None;
150    }
151    let shard_len = data.len().div_ceil(k).max(1);
152    let mut padded = data.to_vec();
153    padded.resize(k * shard_len, 0);
154    let e = encode_matrix(n, k)?;
155    let shards = (0..n)
156        .map(|i| {
157            (0..shard_len)
158                .map(|b| {
159                    let mut acc = 0u8;
160                    for j in 0..k {
161                        acc ^= mul(e[i][j], padded[j * shard_len + b]);
162                    }
163                    acc
164                })
165                .collect()
166        })
167        .collect();
168    Some((data.len(), shards))
169}
170
171/// Reconstruct the payload from ANY `k` (index, shard) pairs out of the `n`. `None` if
172/// fewer than `k` distinct valid shards are present (then the message is unrecoverable).
173pub fn decode(orig_len: usize, k: usize, n: usize, have: &[(usize, Vec<u8>)]) -> Option<Vec<u8>> {
174    if k == 0 || n < k || n > 256 || have.is_empty() {
175        return None;
176    }
177    let shard_len = have[0].1.len();
178    let e = encode_matrix(n, k)?;
179    let mut idxs: Vec<usize> = Vec::with_capacity(k);
180    let mut rows: Vec<Vec<u8>> = Vec::with_capacity(k);
181    let mut vals: Vec<&Vec<u8>> = Vec::with_capacity(k);
182    for (idx, shard) in have {
183        if *idx >= n || shard.len() != shard_len || idxs.contains(idx) {
184            continue;
185        }
186        idxs.push(*idx);
187        rows.push(e[*idx].clone());
188        vals.push(shard);
189        if idxs.len() == k {
190            break;
191        }
192    }
193    if idxs.len() < k {
194        return None;
195    }
196    let minv = invert(&rows)?;
197    let mut data = vec![0u8; k * shard_len];
198    for b in 0..shard_len {
199        for j in 0..k {
200            let mut acc = 0u8;
201            for r in 0..k {
202                acc ^= mul(minv[j][r], vals[r][b]);
203            }
204            data[j * shard_len + b] = acc;
205        }
206    }
207    if orig_len > data.len() {
208        return None;
209    }
210    data.truncate(orig_len);
211    Some(data)
212}
213
214// ---- The `redundant` framing layer: self-describing FEC shards ----------------------
215//
216// Each shard is independently transmittable (its own packet on a lossy link). A fixed
217// header carries everything a receiver needs to group shards of the same message and
218// reconstruct it — no external state, no ordering assumption.
219
220const FEC_MAGIC: u8 = 0xFE;
221// magic(1) + msg_id(8) + k(2) + n(2) + orig_len(4) + index(2)
222const FEC_HEADER_LEN: usize = 1 + 8 + 2 + 2 + 4 + 2;
223
224fn frame_shard(msg_id: u64, k: usize, n: usize, orig_len: usize, index: usize, shard: &[u8]) -> Vec<u8> {
225    let mut out = Vec::with_capacity(FEC_HEADER_LEN + shard.len());
226    out.push(FEC_MAGIC);
227    out.extend_from_slice(&msg_id.to_le_bytes());
228    out.extend_from_slice(&(k as u16).to_le_bytes());
229    out.extend_from_slice(&(n as u16).to_le_bytes());
230    out.extend_from_slice(&(orig_len as u32).to_le_bytes());
231    out.extend_from_slice(&(index as u16).to_le_bytes());
232    out.extend_from_slice(shard);
233    out
234}
235
236struct ShardHeader {
237    msg_id: u64,
238    k: usize,
239    n: usize,
240    orig_len: usize,
241    index: usize,
242}
243
244fn parse_shard(bytes: &[u8]) -> Option<(ShardHeader, &[u8])> {
245    if bytes.len() < FEC_HEADER_LEN || bytes[0] != FEC_MAGIC {
246        return None;
247    }
248    let h = ShardHeader {
249        msg_id: u64::from_le_bytes(bytes[1..9].try_into().ok()?),
250        k: u16::from_le_bytes(bytes[9..11].try_into().ok()?) as usize,
251        n: u16::from_le_bytes(bytes[11..13].try_into().ok()?) as usize,
252        orig_len: u32::from_le_bytes(bytes[13..17].try_into().ok()?) as usize,
253        index: u16::from_le_bytes(bytes[17..19].try_into().ok()?) as usize,
254    };
255    Some((h, &bytes[FEC_HEADER_LEN..]))
256}
257
258/// Peek a framed shard's identity without copying its payload: `(msg_id, k, n)`. `None`
259/// if `bytes` is not a FEC shard (wrong magic / too short). Lets a receiver group shards
260/// by message and know how many it needs (`k`) before reconstructing.
261pub fn shard_header(bytes: &[u8]) -> Option<(u64, usize, usize)> {
262    parse_shard(bytes).map(|(h, _)| (h.msg_id, h.k, h.n))
263}
264
265/// Split a message into `n` self-describing FEC shards (`k` data + `n − k` parity),
266/// each independently transmittable. A receiver reconstructs the exact message from
267/// ANY `k` via [`reconstruct_redundant`]. `msg_id` groups a message's shards on the wire.
268pub fn frame_redundant(msg_id: u64, payload: &[u8], k: usize, n: usize) -> Option<Vec<Vec<u8>>> {
269    let (orig_len, shards) = encode(payload, k, n)?;
270    Some(
271        shards
272            .iter()
273            .enumerate()
274            .map(|(i, s)| frame_shard(msg_id, k, n, orig_len, i, s))
275            .collect(),
276    )
277}
278
279/// Reconstruct a message from a bag of received FEC shards. Groups by message id and
280/// reconstructs the first message that has at least `k` distinct shards present; returns
281/// its `(msg_id, payload)`. Shards from other messages, malformed shards, header-
282/// inconsistent shards, and duplicate indices are ignored. `None` if no message has
283/// reached its `k`-shard threshold.
284pub fn reconstruct_redundant(received: &[Vec<u8>]) -> Option<(u64, Vec<u8>)> {
285    use std::collections::HashMap;
286    // msg_id -> (k, n, orig_len, [(index, shard_bytes)])
287    let mut groups: HashMap<u64, (usize, usize, usize, Vec<(usize, Vec<u8>)>)> = HashMap::new();
288    for bytes in received {
289        let Some((h, shard)) = parse_shard(bytes) else { continue };
290        let entry = groups.entry(h.msg_id).or_insert((h.k, h.n, h.orig_len, Vec::new()));
291        // Accept only header-consistent shards with a not-yet-seen index.
292        if entry.0 == h.k
293            && entry.1 == h.n
294            && entry.2 == h.orig_len
295            && !entry.3.iter().any(|(i, _)| *i == h.index)
296        {
297            entry.3.push((h.index, shard.to_vec()));
298        }
299    }
300    for (msg_id, (k, n, orig_len, shards)) in groups {
301        if shards.len() >= k {
302            if let Some(data) = decode(orig_len, k, n, &shards) {
303                return Some((msg_id, data));
304            }
305        }
306    }
307    None
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313
314    struct R(u64);
315    impl R {
316        fn next(&mut self) -> u64 {
317            self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
318            let mut z = self.0;
319            z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
320            z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
321            z ^ (z >> 31)
322        }
323    }
324
325    #[test]
326    fn gf_mul_div_are_inverse_across_the_field() {
327        for a in 1u8..=255 {
328            for b in 1u8..=255 {
329                assert_eq!(div(mul(a, b), b), a, "div(mul(a,b),b)=a for a={a} b={b}");
330            }
331        }
332    }
333
334    #[test]
335    fn systematic_first_k_shards_are_the_data() {
336        let data: Vec<u8> = (0..40u8).collect();
337        let (k, n) = (5usize, 8usize);
338        let (_, shards) = encode(&data, k, n).unwrap();
339        let shard_len = 8;
340        for j in 0..k {
341            assert_eq!(
342                &shards[j][..],
343                &data[j * shard_len..(j + 1) * shard_len],
344                "data shard {j} must pass through unchanged (systematic)"
345            );
346        }
347    }
348
349    #[test]
350    fn reconstructs_from_any_k_of_n_sliding_window() {
351        let mut rng = R(0x1234_5678);
352        for &(k, n) in &[(4usize, 6usize), (6, 10), (3, 5), (8, 12), (1, 3), (10, 16), (2, 9)] {
353            let len = (rng.next() % 600) as usize + 1;
354            let data: Vec<u8> = (0..len).map(|_| rng.next() as u8).collect();
355            let (orig, shards) = encode(&data, k, n).unwrap();
356            // Every window of (n-k) consecutive (mod n) lost shards must still reconstruct.
357            for drop_start in 0..n {
358                let dropped: Vec<usize> = (0..(n - k)).map(|d| (drop_start + d) % n).collect();
359                let have: Vec<(usize, Vec<u8>)> = (0..n)
360                    .filter(|i| !dropped.contains(i))
361                    .map(|i| (i, shards[i].clone()))
362                    .collect();
363                let got = decode(orig, k, n, &have).expect("reconstruct from k survivors");
364                assert_eq!(got, data, "k={k} n={n} dropped={dropped:?}");
365            }
366        }
367    }
368
369    #[test]
370    fn reconstructs_from_random_k_subsets() {
371        let mut rng = R(0xC0FF_EE00);
372        for &(k, n) in &[(5usize, 9usize), (7, 11), (4, 13)] {
373            let len = (rng.next() % 400) as usize + 1;
374            let data: Vec<u8> = (0..len).map(|_| rng.next() as u8).collect();
375            let (orig, shards) = encode(&data, k, n).unwrap();
376            for _ in 0..40 {
377                // Pick a random subset of exactly k surviving indices.
378                let mut all: Vec<usize> = (0..n).collect();
379                // Fisher-Yates the first k slots.
380                for s in 0..k {
381                    let pick = s + (rng.next() as usize) % (n - s);
382                    all.swap(s, pick);
383                }
384                let have: Vec<(usize, Vec<u8>)> =
385                    all[0..k].iter().map(|&i| (i, shards[i].clone())).collect();
386                let got = decode(orig, k, n, &have).expect("reconstruct from random k-subset");
387                assert_eq!(got, data, "k={k} n={n} survivors={:?}", &all[0..k]);
388            }
389        }
390    }
391
392    #[test]
393    fn fewer_than_k_shards_is_unrecoverable() {
394        let data: Vec<u8> = (0..30u8).collect();
395        let (orig, shards) = encode(&data, 5, 8).unwrap();
396        let have: Vec<(usize, Vec<u8>)> = (0..4).map(|i| (i, shards[i].clone())).collect();
397        assert!(decode(orig, 5, 8, &have).is_none(), "k-1 shards must not reconstruct");
398    }
399
400    #[test]
401    fn reordered_and_duplicated_shards_still_reconstruct() {
402        let data: Vec<u8> = (0..100u8).collect();
403        let (k, n) = (6usize, 10usize);
404        let (orig, shards) = encode(&data, k, n).unwrap();
405        // Hand the decoder shards out of order, with duplicates, missing a few.
406        let mut have: Vec<(usize, Vec<u8>)> = vec![
407            (9, shards[9].clone()),
408            (2, shards[2].clone()),
409            (2, shards[2].clone()), // duplicate
410            (7, shards[7].clone()),
411            (0, shards[0].clone()),
412            (5, shards[5].clone()),
413            (5, shards[5].clone()), // duplicate
414            (3, shards[3].clone()),
415        ];
416        have.reverse();
417        let got = decode(orig, k, n, &have).expect("reorder + dup tolerant");
418        assert_eq!(got, data);
419    }
420
421    #[test]
422    fn degenerate_parameters_are_rejected() {
423        assert!(encode(b"x", 0, 3).is_none());
424        assert!(encode(b"x", 4, 2).is_none()); // n < k
425        assert!(encode(b"x", 1, 257).is_none()); // n > 256
426    }
427
428    // ---- G6 phase-2: the self-describing `redundant` framing layer -----------------
429
430    #[test]
431    fn framed_redundant_reconstructs_from_any_k_after_loss() {
432        let payload: Vec<u8> = (0..250u8).cycle().take(777).collect();
433        let (k, n) = (5usize, 8usize);
434        let shards = frame_redundant(0xABCD, &payload, k, n).unwrap();
435        assert_eq!(shards.len(), n, "one framed shard per n");
436        // Deliver only k of them (drop 3), out of order.
437        let delivered: Vec<Vec<u8>> = vec![
438            shards[7].clone(),
439            shards[1].clone(),
440            shards[4].clone(),
441            shards[0].clone(),
442            shards[6].clone(),
443        ];
444        let (id, got) = reconstruct_redundant(&delivered).expect("reconstruct from k framed shards");
445        assert_eq!(id, 0xABCD);
446        assert_eq!(got, payload);
447    }
448
449    #[test]
450    fn framed_redundant_below_k_does_not_reconstruct() {
451        let payload = b"the quick brown fox".to_vec();
452        let shards = frame_redundant(7, &payload, 4, 7).unwrap();
453        let delivered: Vec<Vec<u8>> = shards[0..3].to_vec(); // only 3 < 4
454        assert!(reconstruct_redundant(&delivered).is_none());
455    }
456
457    #[test]
458    fn framed_redundant_ignores_other_messages_and_garbage() {
459        let a = frame_redundant(1, b"message A payload here", 3, 5).unwrap();
460        let b = frame_redundant(2, b"message B totally different", 3, 5).unwrap();
461        // 2 shards of A (not enough), all of B, plus garbage. Only B reconstructs.
462        let mut bag = vec![a[0].clone(), a[1].clone(), vec![0u8, 1, 2, 3], Vec::new()];
463        bag.extend(b.iter().cloned());
464        let (id, got) = reconstruct_redundant(&bag).unwrap();
465        assert_eq!(id, 2);
466        assert_eq!(got, b"message B totally different");
467    }
468
469    #[test]
470    fn framed_redundant_tolerates_duplicate_shards() {
471        let payload: Vec<u8> = (0..200u8).collect();
472        let shards = frame_redundant(99, &payload, 6, 10).unwrap();
473        // 5 distinct (with duplicates) is still < k=6 — duplicates must not count.
474        let mut bag = vec![
475            shards[0].clone(),
476            shards[0].clone(),
477            shards[1].clone(),
478            shards[1].clone(),
479            shards[2].clone(),
480            shards[3].clone(),
481            shards[4].clone(),
482        ];
483        assert!(reconstruct_redundant(&bag).is_none(), "5 distinct (with dups) < k=6");
484        bag.push(shards[8].clone()); // the 6th distinct shard
485        let (_, got) = reconstruct_redundant(&bag).expect("6 distinct shards reconstruct");
486        assert_eq!(got, payload);
487    }
488}