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}