Skip to main content

subetha_cxc/
tower.rs

1//! Cross-block (segment) outer code: the second FEC rung.
2//!
3//! The inner code ([`crate::fec`]) protects shards *within* a block. The
4//! tower adds an outer code *across* blocks: a segment of `d` data blocks
5//! ships with `r_outer` parity blocks, each a Cauchy Reed-Solomon
6//! combination of the `d` data blocks. A burst that erases an *entire*
7//! block - every shard, which the inner per-block code cannot recover and
8//! which a long-enough burst defeats even with interleaving - is
9//! reconstructed from the surviving blocks in its segment, with no
10//! retransmit round-trip.
11//!
12//! Each block is treated as one outer symbol: its information is the `k`
13//! inner data shards concatenated (the inner parity shards are
14//! re-derivable), so an outer symbol is `k * shard_len` bytes. The outer
15//! code recovers up to `r_outer` fully-lost blocks as long as at least
16//! `d` of the `d + r_outer` blocks in the segment survived.
17//!
18//! ARQ remains the correctness floor; the tower is a latency optimization
19//! that recovers whole-block losses before ARQ has to.
20
21use crate::fec::{FecError, RsCode};
22
23/// A systematic Cauchy-RS outer code over whole blocks: `d` data blocks
24/// plus `r_outer` parity blocks per segment.
25#[derive(Debug, Clone)]
26pub struct SegmentCode {
27    code: RsCode,
28    d: usize,
29    r_outer: usize,
30}
31
32impl SegmentCode {
33    /// Build a `(d, r_outer)` segment code. `d >= 1`, `r_outer >= 1`,
34    /// `d + r_outer <= 256`.
35    pub fn new(d: usize, r_outer: usize) -> Result<Self, FecError> {
36        Ok(Self {
37            code: RsCode::new(d, r_outer)?,
38            d,
39            r_outer,
40        })
41    }
42
43    /// Data blocks per segment.
44    pub fn d(&self) -> usize {
45        self.d
46    }
47
48    /// Outer parity blocks per segment.
49    pub fn r_outer(&self) -> usize {
50        self.r_outer
51    }
52
53    /// Compute the `r_outer` outer-parity block payloads from the `d`
54    /// data-block payloads. Every block payload (data and parity) must be
55    /// the same length (`k * shard_len`).
56    pub fn encode(
57        &self,
58        data_blocks: &[&[u8]],
59        parity_blocks: &mut [&mut [u8]],
60    ) -> Result<(), FecError> {
61        self.code.encode(data_blocks, parity_blocks)
62    }
63
64    /// Recover missing data-block payloads in place. `blocks` has
65    /// `d + r_outer` entries (data blocks first, then outer parity);
66    /// `Some` = present (inner-recovered), `None` = whole block lost. On
67    /// success every data slot `0..d` is `Some`. Returns `TooFewShards`
68    /// if fewer than `d` blocks survived (ARQ must cover the rest).
69    pub fn decode(&self, blocks: &mut [Option<Vec<u8>>]) -> Result<(), FecError> {
70        self.code.decode(blocks)
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    /// Build `d` data blocks of `block_len` bytes with recognizable
79    /// content, compute the outer parity, then verify that dropping every
80    /// pattern of up to `r_outer` WHOLE blocks recovers the data exactly.
81    fn exhaustive_whole_block_recovery(d: usize, r_outer: usize, block_len: usize) {
82        let seg = SegmentCode::new(d, r_outer).expect("segment code");
83        let data: Vec<Vec<u8>> = (0..d)
84            .map(|i| {
85                (0..block_len)
86                    .map(|b| ((i * 251 + b * 13 + 5) & 0xFF) as u8)
87                    .collect()
88            })
89            .collect();
90        let mut parity: Vec<Vec<u8>> = vec![vec![0u8; block_len]; r_outer];
91        {
92            let dref: Vec<&[u8]> = data.iter().map(|v| v.as_slice()).collect();
93            let mut pref: Vec<&mut [u8]> = parity.iter_mut().map(|v| v.as_mut_slice()).collect();
94            seg.encode(&dref, &mut pref).expect("encode");
95        }
96        let n = d + r_outer;
97        let all: Vec<Vec<u8>> = data.iter().chain(parity.iter()).cloned().collect();
98        for lost in 1..=r_outer {
99            for mask in 0u32..(1 << n) {
100                if mask.count_ones() as usize != lost {
101                    continue;
102                }
103                let mut blocks: Vec<Option<Vec<u8>>> = all
104                    .iter()
105                    .enumerate()
106                    .map(|(i, b)| if mask & (1 << i) != 0 { None } else { Some(b.clone()) })
107                    .collect();
108                seg.decode(&mut blocks).expect("decode");
109                for i in 0..d {
110                    assert_eq!(
111                        blocks[i].as_ref().unwrap(),
112                        &data[i],
113                        "d={d} r_outer={r_outer} mask={mask:b}: block {i} mismatch"
114                    );
115                }
116            }
117        }
118    }
119
120    #[test]
121    fn recover_whole_blocks_d4_r2() {
122        exhaustive_whole_block_recovery(4, 2, 80);
123    }
124
125    #[test]
126    fn recover_whole_blocks_d8_r2() {
127        exhaustive_whole_block_recovery(8, 2, 40);
128    }
129
130    #[test]
131    fn recover_whole_blocks_d6_r3() {
132        exhaustive_whole_block_recovery(6, 3, 32);
133    }
134
135    #[test]
136    fn too_many_lost_blocks_is_reported() {
137        let seg = SegmentCode::new(4, 2).expect("code");
138        let len = 32;
139        let data: Vec<Vec<u8>> = (0..4).map(|_| vec![7u8; len]).collect();
140        let mut parity: Vec<Vec<u8>> = vec![vec![0u8; len]; 2];
141        {
142            let dref: Vec<&[u8]> = data.iter().map(|v| v.as_slice()).collect();
143            let mut pref: Vec<&mut [u8]> = parity.iter_mut().map(|v| v.as_mut_slice()).collect();
144            seg.encode(&dref, &mut pref).expect("encode");
145        }
146        // Lose 3 of 6 whole blocks (more than r_outer=2): ARQ territory.
147        let mut blocks: Vec<Option<Vec<u8>>> =
148            vec![None, None, None, Some(data[3].clone()), Some(parity[0].clone()), Some(parity[1].clone())];
149        assert_eq!(seg.decode(&mut blocks), Err(FecError::TooFewShards));
150    }
151}