subetha_cxc/reliable_udp.rs
1//! Sens-O-Matic protocol: a reliable-UDP transport, FEC-primary,
2//! ARQ-fallback.
3//!
4//! The coding and wire format for Sens-O-Matic, the sighted,
5//! forward-correcting reliable-UDP transport. The socket layer that
6//! drives it lives in [`crate::udp_bridge`].
7//!
8//! This is the encryption-free reliable datagram layer that gives a
9//! trusted-network bridge ordered, lossless delivery over `UdpSocket`
10//! without TLS. Reliability comes from two mechanisms, in priority
11//! order:
12//!
13//! 1. **FEC (primary).** Source items are grouped into blocks of `k`
14//! shards and shipped with `r` Cauchy Reed-Solomon parity shards
15//! ([`crate::fec`]). Up to `r` losses per block are reconstructed by
16//! the receiver with **no retransmit round-trip**.
17//! 2. **ARQ (fallback).** When a block loses MORE than `r` shards - the
18//! rare burst FEC cannot cover - the receiver NAKs the missing shard
19//! indices and the sender retransmits exactly those.
20//!
21//! The parity rate `r` is **automatic**: the receiver reports its
22//! measured loss fraction on every feedback packet and the sender raises
23//! or lowers `r` for subsequent blocks so FEC carries the common case
24//! (small `r` on a clean LAN, larger `r` on lossy Wi-Fi) and ARQ stays a
25//! fallback.
26//!
27//! The protocol is transport-agnostic: [`Encoder`] turns items into
28//! datagrams and [`Decoder`] turns datagrams back into ordered items,
29//! both over byte slices. A real socket or a deterministic lossy channel
30//! plugs in identically, which is what lets the FEC/ARQ behavior be
31//! proven without a network.
32
33use std::collections::BTreeMap;
34use std::sync::atomic::{AtomicU32, Ordering};
35
36use crate::fec::RsCode;
37use crate::loss_class_sensor::LossClassSensor;
38use crate::temporal_sensor::TemporalSensor;
39use crate::tower::SegmentCode;
40
41/// Packet type tag (first wire byte). Data datagrams use this tag; the
42/// control plane (ACK / NAK / loss / timing / ring / path / link / ...) rides
43/// the framed `PKT_CONTROL` container in [`crate::control_frame`].
44const PKT_DATA: u8 = 1;
45
46/// Fixed data-packet header length: `type(1) block_id(4) shard_index(1)
47/// k(1) r(1) flags(1) epoch(4)`.
48pub const DATA_HEADER: usize = 13;
49
50/// Offset of the session epoch within the data header.
51const EPOCH_OFFSET: usize = 9;
52
53/// The session epoch a data datagram carries, or `None` if `buf` is not a data
54/// datagram.
55pub fn datagram_epoch(buf: &[u8]) -> Option<u32> {
56 if !is_data(buf) || buf.len() < DATA_HEADER {
57 return None;
58 }
59 Some(u32::from_le_bytes([
60 buf[EPOCH_OFFSET],
61 buf[EPOCH_OFFSET + 1],
62 buf[EPOCH_OFFSET + 2],
63 buf[EPOCH_OFFSET + 3],
64 ]))
65}
66
67/// `flags` bit: this shard is a parity shard (index `>= k`).
68const FLAG_PARITY: u8 = 0b0000_0001;
69
70/// `flags` bit: this block is a tower outer-parity block - fire-and-forget
71/// cross-block redundancy used opportunistically by the receiver, never
72/// ARQ-tracked (ARQ on the data blocks is the correctness floor).
73const FLAG_OUTER: u8 = 0b0000_0010;
74
75/// `flags` bit: this datagram is an ARQ retransmit. A data shard arriving
76/// with this flag for the first time means its original was dropped, so the
77/// receiver counts it as a wire loss even though ARQ recovered it - the
78/// signal that lets the loss estimator see drops Passthrough hides behind ARQ.
79const FLAG_RETRANSMIT: u8 = 0b0000_0100;
80
81/// High bit set on an outer-parity block id, separating it from the
82/// sequential data-block id space. The low bits encode
83/// `(segment << 8) | outer_index`.
84const OUTER_ID_BIT: u32 = 0x8000_0000;
85
86/// Maximum shards per block (`k + r`); keeps the received-bitmap in one
87/// `u32`.
88pub const MAX_SHARDS: usize = 32;
89
90/// Per-data-shard payload prefix: the real item length in bytes.
91const ITEM_LEN_PREFIX: usize = 2;
92
93/// Sentinel `nak_block` meaning "no retransmit requested".
94pub const NAK_NONE: u32 = u32::MAX;
95
96/// Whether the reordering guard subtracts spurious-retransmit false recoveries
97/// (the D-SACK signal) from the loss estimate. Default on; `SUBETHA_REORDER_GUARD=0`
98/// disables the subtraction for the A/B baseline that shows reordering inflating
99/// the loss estimate without it. Read once and cached.
100fn reorder_guard_enabled() -> bool {
101 static EN: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
102 *EN.get_or_init(|| {
103 std::env::var("SUBETHA_REORDER_GUARD")
104 .map(|v| v != "0")
105 .unwrap_or(true)
106 })
107}
108
109/// The receiver-side control state - ack frontier, selective NAK, and the
110/// fused channel readings - that the bridge carries as `Ack` / `Nak` / `Loss`
111/// frames in a [`crate::control_frame`] CONTROL packet. Kept as a struct
112/// because it is the form the sender's controller already consumes.
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub struct Feedback {
115 /// Next block the receiver still needs (everything below is
116 /// delivered); the sender frees retransmit state below this.
117 pub ack_through: u32,
118 /// Block whose missing shards should be retransmitted, or
119 /// [`NAK_NONE`].
120 pub nak_block: u32,
121 /// Bitmap of MISSING shard indices in `nak_block`.
122 pub nak_mask: u32,
123 /// Estimated loss fraction scaled to `0..=255`.
124 pub loss_x255: u8,
125 /// Estimated burstiness scaled to `0..=255` (clustering of loss).
126 pub burstiness_x255: u8,
127 /// One-way-delay trend class: 0 = falling, 1 = flat, 2 = rising.
128 pub owd_trend_class: u8,
129 /// Loss-class code (0 = no loss, 1 = wireless, 2 = congestion, 3 = mixed)
130 /// from the receiver's [`crate::loss_class_sensor`].
131 pub loss_class: u8,
132}
133
134/// Returns `true` if `buf` is a tower outer-parity datagram.
135pub fn is_outer_datagram(buf: &[u8]) -> bool {
136 buf.len() > DATA_HEADER && buf[0] == PKT_DATA && (buf[8] & FLAG_OUTER) != 0
137}
138
139/// Returns `true` if `buf` is a data datagram (vs feedback).
140pub fn is_data(buf: &[u8]) -> bool {
141 !buf.is_empty() && buf[0] == PKT_DATA
142}
143
144/// A built block held by the sender for possible ARQ retransmission.
145struct PendingBlock {
146 k: u8,
147 r: u8,
148 shard_len: usize,
149 /// `k + r` shard payloads (data first, then parity).
150 shards: Vec<Vec<u8>>,
151}
152
153impl PendingBlock {
154 fn datagram(&self, block_id: u32, idx: usize, epoch: u32) -> Vec<u8> {
155 self.datagram_flagged(block_id, idx, 0, epoch)
156 }
157
158 fn datagram_flagged(
159 &self,
160 block_id: u32,
161 idx: usize,
162 extra_flags: u8,
163 epoch: u32,
164 ) -> Vec<u8> {
165 let mut pkt = Vec::with_capacity(DATA_HEADER + self.shard_len);
166 pkt.push(PKT_DATA);
167 pkt.extend_from_slice(&block_id.to_le_bytes());
168 pkt.push(idx as u8);
169 pkt.push(self.k);
170 pkt.push(self.r);
171 let parity = if idx >= self.k as usize { FLAG_PARITY } else { 0 };
172 pkt.push(parity | extra_flags);
173 pkt.extend_from_slice(&epoch.to_le_bytes());
174 pkt.extend_from_slice(&self.shards[idx]);
175 pkt
176 }
177}
178
179/// A non-zero session epoch, distinct across restarts of this sender.
180///
181/// Mixes the invariant-TSC read with the wall clock and the pid. The TSC
182/// separates encoders built in the same instant, which a wall clock at
183/// tens of milliseconds of granularity cannot; the wall clock separates
184/// encoders built at the same point after different boots, which the TSC
185/// cannot, restarting near zero.
186fn derive_epoch() -> u32 {
187 // Not `default_stamp_kind`: its SharedCounter arm reads as 0 through
188 // `stamp_now`, being a ring ordering atom rather than a clock.
189 let kind = if crate::ordering::has_invariant_tsc() {
190 crate::ordering::StampKind::Tsc
191 } else {
192 crate::ordering::StampKind::Monotonic
193 };
194 let tsc = crate::ordering::stamp_now(kind);
195 let wall = std::time::SystemTime::now()
196 .duration_since(std::time::UNIX_EPOCH)
197 .map(|d| d.as_nanos() as u64)
198 .unwrap_or(0);
199 let mut x = tsc ^ wall.rotate_left(32) ^ ((std::process::id() as u64) << 16);
200 x = (x ^ (x >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
201 x = (x ^ (x >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
202 // Never zero: zero reads as "no epoch recorded" to a peer built
203 // before this field existed.
204 ((x ^ (x >> 31)) as u32) | 1
205}
206
207/// Sender side: groups items into FEC-protected blocks and answers
208/// ARQ retransmit requests.
209pub struct Encoder {
210 k: usize,
211 /// Current parity count; adapts to reported loss between
212 /// [`r_min`](Self::r_min) and [`r_max`](Self::r_max).
213 r: usize,
214 r_min: usize,
215 r_max: usize,
216 /// Usable payload bytes per shard (item + length prefix).
217 shard_len: usize,
218 /// Session epoch stamped into every data datagram this encoder emits.
219 /// Constant for the encoder's life; a restarted peer draws a new one.
220 epoch: u32,
221 next_block: u32,
222 /// Highest `ack_through` reported by the receiver; below this every
223 /// block is delivered.
224 acked_through: u32,
225 /// Max blocks in flight (sent but not yet acked) before the producer
226 /// should apply backpressure; matches the receiver's window.
227 flow_window: u32,
228 /// Items accumulated for the block under construction.
229 staged: Vec<Vec<u8>>,
230 /// Built-but-unacked blocks, keyed by block id, for ARQ.
231 pending: BTreeMap<u32, PendingBlock>,
232 /// Tower outer code dimensions: `(d, r_outer)`; `(0, 0)` = disabled.
233 tower_d: usize,
234 tower_r_outer: usize,
235 /// Data-block infos (the `k` data shards concatenated) accumulated for
236 /// the current segment.
237 seg_infos: Vec<Vec<u8>>,
238 /// Current segment id.
239 seg_id: u32,
240 /// Blocks sealed at zero parity (Passthrough); telemetry that proves the
241 /// controller actually dropped FEC off the wire on a clean link.
242 passthrough_blocks: u64,
243 /// Blocks sealed with parity (r >= 1); telemetry counterpart.
244 fec_blocks: u64,
245}
246
247impl Encoder {
248 /// Create an encoder. `k` data shards per block, initial `r` parity
249 /// shards (clamped to `r_min..=r_max`), `max_item` largest item
250 /// byte length.
251 /// This encoder's session epoch, as stamped into every data datagram
252 /// and announced on the heartbeat.
253 pub fn epoch(&self) -> u32 {
254 self.epoch
255 }
256
257 pub fn new(k: usize, r: usize, max_item: usize) -> Self {
258 // r_min = 0 lets the fusion controller drop to zero parity
259 // (CodingLevel::Passthrough) on a provably-clean link: the block
260 // ships its k data shards with no FEC encode and no parity datagrams,
261 // and ARQ remains the reliability floor. The controller only selects
262 // r=0 after a sustained-clean confidence window and re-arms to r>=1
263 // the instant loss, burstiness, or link stress appears.
264 let r_min = 0;
265 // Cap parity so k + r never exceeds MAX_SHARDS (the per-block received/NAK
266 // bitmap is a u32, and `1 << idx` for idx >= 32 overflows). saturating_sub
267 // with a 0 floor means k == MAX_SHARDS yields r_max = 0 (Passthrough,
268 // ARQ-only) rather than a 1 that would overflow the bitmap. The full
269 // k + r = MAX_SHARDS is decode-sound (Cauchy over GF(256); see
270 // fec::tests::recovery_k16_r16_high_parity), so the only ceiling is the
271 // bitmap - a high-loss block can provision parity up to it.
272 let r_max = MAX_SHARDS.saturating_sub(k);
273 Self {
274 k,
275 r: r.clamp(r_min, r_max),
276 r_min,
277 r_max,
278 shard_len: max_item + ITEM_LEN_PREFIX,
279 epoch: derive_epoch(),
280 next_block: 0,
281 acked_through: 0,
282 flow_window: 256,
283 staged: Vec::with_capacity(k),
284 pending: BTreeMap::new(),
285 tower_d: 0,
286 tower_r_outer: 0,
287 seg_infos: Vec::new(),
288 seg_id: 0,
289 passthrough_blocks: 0,
290 fec_blocks: 0,
291 }
292 }
293
294 /// Blocks sealed at zero parity (Passthrough) so far, and blocks sealed
295 /// with parity. A nonzero first value proves FEC actually switched off on
296 /// the wire; the ratio shows how much of the stream rode unprotected.
297 pub fn coding_counts(&self) -> (u64, u64) {
298 (self.passthrough_blocks, self.fec_blocks)
299 }
300
301 /// Enable the tower outer code: every `d` data blocks ship with
302 /// `r_outer` fire-and-forget outer-parity blocks that recover whole
303 /// lost data blocks without a retransmit. `(0, _)` or `(_, 0)`
304 /// disables it.
305 pub fn enable_tower(&mut self, d: usize, r_outer: usize) {
306 if d == 0 || r_outer == 0 || d + r_outer > MAX_SHARDS {
307 self.tower_d = 0;
308 self.tower_r_outer = 0;
309 } else {
310 self.tower_d = d;
311 self.tower_r_outer = r_outer;
312 }
313 self.seg_infos.clear();
314 }
315
316 /// Set the in-flight flow window (blocks sent but not yet acked).
317 /// Match this to the receiver's [`Decoder::with_window`].
318 pub fn with_flow_window(mut self, blocks: u32) -> Self {
319 self.flow_window = blocks.max(1);
320 self
321 }
322
323 /// Adjust the in-flight flow window at runtime - the bufferbloat pacer
324 /// shrinks it toward the BDP to drain a self-induced queue, and restores it
325 /// when the queue clears. The receiver's window is the hard ceiling, so the
326 /// pacer only ever clamps DOWN from the configured maximum.
327 pub fn set_flow_window(&mut self, blocks: u32) {
328 self.flow_window = blocks.max(1);
329 }
330
331 /// Current in-flight flow window (blocks).
332 pub fn flow_window(&self) -> u32 {
333 self.flow_window
334 }
335
336 /// Blocks sent but not yet acked by the receiver.
337 pub fn in_flight(&self) -> u32 {
338 self.next_block.wrapping_sub(self.acked_through)
339 }
340
341 /// `true` when the producer should pause sending new blocks until an
342 /// ack frees window space (keeps the receiver's bounded window from
343 /// dropping far-ahead blocks).
344 pub fn flow_blocked(&self) -> bool {
345 self.in_flight() >= self.flow_window
346 }
347
348 /// Largest item this encoder accepts.
349 pub fn max_item(&self) -> usize {
350 self.shard_len - ITEM_LEN_PREFIX
351 }
352
353 /// Current parity count.
354 pub fn parity(&self) -> usize {
355 self.r
356 }
357
358 /// The id the NEXT sealed block will take; the block just sealed by a
359 /// non-empty [`push`](Self::push) / [`flush`](Self::flush) is this minus
360 /// one. Lets the sender record a per-block send time for RTT sampling.
361 pub fn next_block_id(&self) -> u32 {
362 self.next_block
363 }
364
365 /// Stage `item` for transmission. Returns the datagrams to send when
366 /// the staged set reaches `k` items (a full block); otherwise an
367 /// empty vec. Call [`flush`](Self::flush) to force a short final
368 /// block.
369 pub fn push(&mut self, item: &[u8]) -> Vec<Vec<u8>> {
370 debug_assert!(item.len() <= self.max_item());
371 // Stage the unpadded shard (length prefix + item). seal_block pads
372 // every shard in the block to the block's largest item - so a block
373 // of small (e.g. schema-compressed) items ships small datagrams.
374 let mut shard = Vec::with_capacity(ITEM_LEN_PREFIX + item.len());
375 shard.extend_from_slice(&(item.len() as u16).to_le_bytes());
376 shard.extend_from_slice(item);
377 self.staged.push(shard);
378 if self.staged.len() == self.k {
379 self.seal_block()
380 } else {
381 Vec::new()
382 }
383 }
384
385 /// Force the staged items (fewer than `k`) into a final padded
386 /// block. Returns its datagrams, or empty if nothing is staged.
387 pub fn flush(&mut self) -> Vec<Vec<u8>> {
388 let mut out = if self.staged.is_empty() {
389 Vec::new()
390 } else {
391 self.seal_block()
392 };
393 // Seal a partial final segment so its blocks get tower protection
394 // too (otherwise a whole-block loss in the tail segment has no
395 // outer parity to recover from).
396 if self.tower_d > 0 && !self.seg_infos.is_empty() {
397 out.extend(self.seal_segment());
398 }
399 out
400 }
401
402 fn seal_block(&mut self) -> Vec<Vec<u8>> {
403 // Per-block shard length: the largest staged shard in this block,
404 // so a block of small items ships small datagrams. The tower's
405 // cross-block outer code needs uniform blocks across a segment, so
406 // when it is enabled the fixed maximum is used instead. The decoder
407 // reads each block's shard length from the datagram size, so no
408 // header field is required.
409 let block_shard_len = if self.tower_d > 0 {
410 self.shard_len
411 } else {
412 self.staged
413 .iter()
414 .map(|s| s.len())
415 .max()
416 .unwrap_or(ITEM_LEN_PREFIX)
417 .max(ITEM_LEN_PREFIX)
418 };
419 for s in &mut self.staged {
420 s.resize(block_shard_len, 0);
421 }
422 // Pad with zero-length items up to k data shards.
423 while self.staged.len() < self.k {
424 let mut pad = vec![0u8; block_shard_len];
425 pad[0..2].copy_from_slice(&0u16.to_le_bytes());
426 self.staged.push(pad);
427 }
428 let r = self.r;
429 let mut shards: Vec<Vec<u8>> = std::mem::take(&mut self.staged);
430 // Capture this block's info (the k data shards) for the tower,
431 // before parity is appended.
432 let tower_info = if self.tower_d > 0 {
433 Some(shards.concat())
434 } else {
435 None
436 };
437 // Passthrough (r=0): ship the k data shards with no parity encode.
438 // ARQ recovers any dropped data shard; the controller only reaches
439 // r=0 on a sustained-clean link.
440 if r == 0 {
441 self.passthrough_blocks += 1;
442 } else {
443 self.fec_blocks += 1;
444 }
445 if r > 0 {
446 let mut parity: Vec<Vec<u8>> = vec![vec![0u8; block_shard_len]; r];
447 {
448 let code = RsCode::new(self.k, r).expect("valid k,r");
449 let data_refs: Vec<&[u8]> = shards.iter().map(|s| s.as_slice()).collect();
450 let mut par_refs: Vec<&mut [u8]> =
451 parity.iter_mut().map(|s| s.as_mut_slice()).collect();
452 code.encode(&data_refs, &mut par_refs).expect("encode");
453 }
454 shards.extend(parity);
455 }
456 let block_id = self.next_block;
457 self.next_block += 1;
458 let pb = PendingBlock {
459 k: self.k as u8,
460 r: r as u8,
461 shard_len: block_shard_len,
462 shards,
463 };
464 let mut datagrams: Vec<Vec<u8>> =
465 (0..self.k + r).map(|i| pb.datagram(block_id, i, self.epoch)).collect();
466 self.pending.insert(block_id, pb);
467 self.staged = Vec::with_capacity(self.k);
468 // Tower: accumulate this block's info; emit outer-parity blocks
469 // when the segment fills.
470 if let Some(info) = tower_info {
471 self.seg_infos.push(info);
472 if self.seg_infos.len() == self.tower_d {
473 datagrams.extend(self.seal_segment());
474 }
475 }
476 datagrams
477 }
478
479 /// Compute and emit the segment's outer-parity blocks (fire-and-forget:
480 /// not added to `pending`, so they are never retransmitted - ARQ on the
481 /// data blocks is the floor).
482 fn seal_segment(&mut self) -> Vec<Vec<u8>> {
483 let r_outer = self.tower_r_outer;
484 let infos = std::mem::take(&mut self.seg_infos);
485 // Use the ACTUAL block count: a full segment has `tower_d`, the
486 // final partial segment (flushed) has fewer. The count is encoded
487 // in the outer id so the receiver protects partial segments too.
488 let d = infos.len();
489 if d == 0 || r_outer == 0 {
490 return Vec::new();
491 }
492 let info_len = infos[0].len();
493 let seg = SegmentCode::new(d, r_outer).expect("valid d,r_outer");
494 let mut outer: Vec<Vec<u8>> = vec![vec![0u8; info_len]; r_outer];
495 {
496 let dref: Vec<&[u8]> = infos.iter().map(|v| v.as_slice()).collect();
497 let mut pref: Vec<&mut [u8]> = outer.iter_mut().map(|v| v.as_mut_slice()).collect();
498 seg.encode(&dref, &mut pref).expect("outer encode");
499 }
500 let seg_id = self.seg_id;
501 self.seg_id += 1;
502 let r = self.r;
503 let mut out = Vec::new();
504 for (oidx, oinfo) in outer.into_iter().enumerate() {
505 // The outer info is k data shards; inner-encode it like any
506 // block so it survives shard loss on the wire too.
507 let mut oshards: Vec<Vec<u8>> =
508 oinfo.chunks(self.shard_len).map(|c| c.to_vec()).collect();
509 let mut oparity: Vec<Vec<u8>> = vec![vec![0u8; self.shard_len]; r];
510 {
511 let code = RsCode::new(self.k, r).expect("valid k,r");
512 let dref: Vec<&[u8]> = oshards.iter().map(|s| s.as_slice()).collect();
513 let mut pref: Vec<&mut [u8]> =
514 oparity.iter_mut().map(|s| s.as_mut_slice()).collect();
515 code.encode(&dref, &mut pref).expect("inner encode outer");
516 }
517 oshards.extend(oparity);
518 let opb = PendingBlock {
519 k: self.k as u8,
520 r: r as u8,
521 shard_len: self.shard_len,
522 shards: oshards,
523 };
524 // Self-describing id: bit31 = OUTER, bits27-30 = d (1..15),
525 // bits24-26 = r_outer (1..7), bits8-23 = segment, bits0-7 =
526 // outer index. The receiver learns the segment structure from
527 // the wire, no out-of-band config.
528 let oid = OUTER_ID_BIT
529 | ((d as u32) << 27)
530 | ((r_outer as u32) << 24)
531 | (seg_id << 8)
532 | oidx as u32;
533 for i in 0..self.k + r {
534 out.push(opb.datagram_flagged(oid, i, FLAG_OUTER, self.epoch));
535 }
536 }
537 out
538 }
539
540 /// Set the parity shards per new block, clamped to the encoder's
541 /// `[r_min, r_max]`. The fusion controller drives this from the
542 /// control table; the encoder no longer self-adapts parity.
543 pub fn set_parity(&mut self, r: usize) {
544 self.r = r.clamp(self.r_min, self.r_max);
545 }
546
547 /// Set parity to at least `floor` (the fusion controller's burst / feed-forward
548 /// signal) AND enough to FEC-recover a `loss` fraction of THIS block: to
549 /// recover a fraction p of the k + r shards, r / (k + r) >= p, i.e.
550 /// r >= p * k / (1 - p). A 20% margin covers a spike above the mean. Capped at
551 /// `r_max` (the bitmap ceiling). Without this, parity tracked only the
552 /// controller's modest floor and a high-loss block fell to ARQ round trips
553 /// instead of recovering in-FEC; this lets block-RS provision to the loss the
554 /// way the sliding-window RLC rate law already does.
555 pub fn set_parity_covering(&mut self, floor: usize, loss: f32) {
556 let p = (loss * 1.2).clamp(0.0, 0.95);
557 let cover = (p * self.k as f32 / (1.0 - p)).ceil() as usize;
558 self.r = floor.max(cover).clamp(self.r_min, self.r_max);
559 }
560
561 /// Apply receiver feedback: free acked blocks and return any ARQ
562 /// retransmit datagrams. Parity adaptation is the controller's job
563 /// (see [`set_parity`](Self::set_parity)), not this method's.
564 pub fn on_feedback(&mut self, fb: &Feedback) -> Vec<Vec<u8>> {
565 if fb.ack_through > self.acked_through {
566 self.acked_through = fb.ack_through;
567 }
568 // Free everything the receiver has fully delivered.
569 let acked: Vec<u32> = self
570 .pending
571 .range(..fb.ack_through)
572 .map(|(&id, _)| id)
573 .collect();
574 for id in acked {
575 self.pending.remove(&id);
576 }
577 // ARQ: retransmit the requested missing shards.
578 let mut out = Vec::new();
579 if fb.nak_block != NAK_NONE
580 && let Some(pb) = self.pending.get(&fb.nak_block)
581 {
582 let n = pb.shards.len();
583 for idx in 0..n {
584 if fb.nak_mask & (1 << idx) != 0 {
585 out.push(pb.datagram_flagged(fb.nak_block, idx, FLAG_RETRANSMIT, self.epoch));
586 }
587 }
588 }
589 out
590 }
591
592 /// Number of unacked blocks held for ARQ.
593 pub fn pending_len(&self) -> usize {
594 self.pending.len()
595 }
596
597 /// The oldest unacked block id - the one the receiver's in-order frontier
598 /// is waiting on - or `None` if everything is acked.
599 pub fn oldest_pending(&self) -> Option<u32> {
600 self.pending.keys().next().copied()
601 }
602
603 /// Retransmit datagrams (flagged `RETRANSMIT`) for the `k` DATA shards of
604 /// one pending block - a liveness probe that also pre-positions the block
605 /// the receiver's frontier is stalled on. Empty if the block is already
606 /// acked.
607 pub fn probe_block(&self, block_id: u32) -> Vec<Vec<u8>> {
608 match self.pending.get(&block_id) {
609 Some(pb) => (0..pb.k as usize)
610 .map(|idx| pb.datagram_flagged(block_id, idx, FLAG_RETRANSMIT, self.epoch))
611 .collect(),
612 None => Vec::new(),
613 }
614 }
615
616 /// Retransmit datagrams (flagged `RETRANSMIT`) for the `k` DATA shards of
617 /// EVERY pending block, oldest-first - the proactive burst on link recovery
618 /// that resends the whole unacked window WITHOUT waiting for the receiver's
619 /// NAKs (the sender already holds the exact unacked set, so no estimation
620 /// is needed). The receiver dedups any datagram it already has via its
621 /// D-SACK / false-recovery path, so over-resending is safe. `k` data shards
622 /// per block suffice to decode a fully-lost block; any shard still missing
623 /// after the burst is recovered by the normal reactive NAK.
624 pub fn retransmit_all_data(&self) -> Vec<Vec<u8>> {
625 let mut out = Vec::new();
626 // BTreeMap iterates in key order, i.e. oldest block first.
627 for (&id, pb) in &self.pending {
628 for idx in 0..pb.k as usize {
629 out.push(pb.datagram_flagged(id, idx, FLAG_RETRANSMIT, self.epoch));
630 }
631 }
632 out
633 }
634}
635
636/// One block being reassembled on the receiver.
637struct RxBlock {
638 k: usize,
639 r: usize,
640 shard_len: usize,
641 /// Received bitmap: bit `i` set means shard `i` is present.
642 mask: AtomicU32,
643 /// Bitmap of shards whose first arrival was an ARQ retransmit (their
644 /// original was dropped) - the wire-loss evidence for the estimator.
645 retransmitted: u32,
646 /// Bitmap of positions where the original (non-retransmit) shard arrived
647 /// AFTER an ARQ retransmit had already filled the slot. A duplicate of an
648 /// already-recovered shard is the D-SACK signal (RFC 2883): "significant
649 /// reordering followed by a false (unnecessary) retransmission", so the
650 /// shard was reordered (late), not lost, and the retransmit-counted loss
651 /// was a false positive the estimator subtracts (reordering vs loss per
652 /// RACK-TLP, RFC 8985).
653 false_recovery: u32,
654 shards: Vec<Option<Vec<u8>>>,
655 decoded: bool,
656}
657
658impl RxBlock {
659 fn new(k: usize, r: usize, shard_len: usize) -> Self {
660 Self {
661 k,
662 r,
663 shard_len,
664 mask: AtomicU32::new(0),
665 retransmitted: 0,
666 false_recovery: 0,
667 shards: vec![None; k + r],
668 decoded: false,
669 }
670 }
671
672 #[inline]
673 fn count(&self) -> u32 {
674 self.mask.load(Ordering::Relaxed).count_ones()
675 }
676}
677
678/// Receiver side: reassembles blocks, FEC-recovers losses, emits items
679/// in order, and produces ARQ feedback.
680pub struct Decoder {
681 /// Session epoch this decoder's state belongs to, learned from the
682 /// first data datagram. `None` before any arrives.
683 session_epoch: Option<u32>,
684 /// The most recent epoch seen that is not [`session_epoch`]. Either a
685 /// restarted peer or a forgery; the receiver challenges it and calls
686 /// [`adopt_epoch`](Self::adopt_epoch) only on a valid answer.
687 unknown_epoch: Option<u32>,
688 window: BTreeMap<u32, RxBlock>,
689 /// Next block id to deliver; everything below is delivered.
690 next_deliver: AtomicU32,
691 /// Highest block id seen, for stall detection.
692 highest_seen: u32,
693 /// Highest DATA block fully decoded. Genuine gaps (blocks needing a
694 /// retransmit) sit only below this: a later block fully arrived, so
695 /// the missing one's shards are lost, not in flight. On a clean link
696 /// this tracks the delivery frontier, so the selective-NAK gap scan is
697 /// empty - that cost is paid only under real loss, not every poll.
698 highest_decoded: u32,
699 /// Rolling loss accounting.
700 total_expected: u64,
701 total_missing: u64,
702 /// Highest loss estimate (0..=255) reached over the receiver's lifetime.
703 /// Diagnostics for the reordering guard.
704 peak_loss: u8,
705 /// Lifetime count of D-SACK false recoveries detected: spurious
706 /// retransmissions whose reordered original later arrived, which the guard
707 /// excludes from the loss estimate. A nonzero value on a reorder-carrying
708 /// link is the guard firing on real reordered traffic. Diagnostics.
709 false_recoveries: u64,
710 /// Max blocks retained before forcing progress / NAK.
711 window_cap: usize,
712 /// Timing estimator fed by sender heartbeats (OWD trend, jitter).
713 temporal: TemporalSensor,
714 /// Loss differentiator (congestion vs wireless): fed shard inter-arrivals
715 /// and heartbeat ROTT, consulted when a block delivers with loss.
716 loss_class: LossClassSensor,
717 /// Gilbert-Elliott burst-loss fit: fed each delivered block's per-shard
718 /// original-loss trace, it yields a REAL mean burst length. When
719 /// `use_ge_burst` is set the reported burstiness is derived from it
720 /// (interleave at least the mean burst), instead of the jitter-ratio
721 /// heuristic - the A/B knob.
722 burst_model: crate::burst_model_sensor::BurstModel,
723 use_ge_burst: bool,
724 /// Receiver-clock microseconds of the previous data-shard arrival, for the
725 /// inter-arrival the loss differentiator's Biaz test needs (`None` until a
726 /// timestamped shard arrives via [`Decoder::on_packet_at`]).
727 last_data_recv_us: Option<u64>,
728 /// Most recent data-shard inter-arrival (microseconds), classified against
729 /// the loss gap when a block delivers.
730 last_interarrival_us: f64,
731 /// Tower segment structure, learned from outer block ids (`0` until
732 /// the first outer block arrives).
733 tower_d: usize,
734 tower_r_outer: usize,
735 /// Inner block geometry, learned from received data blocks.
736 inner_k: usize,
737 inner_shard_len: usize,
738 /// Decoded data-block infos (k data shards concatenated), kept for
739 /// tower recovery until delivered.
740 data_infos: BTreeMap<u32, Vec<u8>>,
741 /// Reassembly buffers for in-flight outer-parity blocks.
742 outer_rx: BTreeMap<u32, RxBlock>,
743 /// Recovered outer infos per segment: `seg_id -> (outer_idx -> info)`.
744 seg_outer: BTreeMap<u32, BTreeMap<u32, Vec<u8>>>,
745 /// Actual data-block count per segment (a partial final segment has
746 /// fewer than `tower_d`).
747 seg_d: BTreeMap<u32, usize>,
748}
749
750impl Default for Decoder {
751 fn default() -> Self {
752 Self::new()
753 }
754}
755
756impl Decoder {
757 /// Create a receiver with a default 256-block reassembly window -
758 /// deep enough to keep the wire full across the ack round-trip while a
759 /// gap recovers in the background (the sender pipelines new blocks and
760 /// the receiver buffers them out of order, draining in order once the
761 /// gap is recovered).
762 pub fn new() -> Self {
763 Self::with_window(256)
764 }
765
766 /// Create a receiver bounding the reassembly window to `window_cap`
767 /// blocks. A sender should use a matching
768 /// [`Encoder::with_flow_window`] so it never transmits beyond what
769 /// the receiver will buffer.
770 pub fn with_window(window_cap: usize) -> Self {
771 Self {
772 session_epoch: None,
773 unknown_epoch: None,
774 window: BTreeMap::new(),
775 next_deliver: AtomicU32::new(0),
776 highest_seen: 0,
777 highest_decoded: 0,
778 total_expected: 0,
779 total_missing: 0,
780 peak_loss: 0,
781 false_recoveries: 0,
782 window_cap: window_cap.max(1),
783 temporal: TemporalSensor::default(),
784 loss_class: LossClassSensor::new(),
785 burst_model: crate::burst_model_sensor::BurstModel::new(),
786 use_ge_burst: false,
787 last_data_recv_us: None,
788 last_interarrival_us: 0.0,
789 tower_d: 0,
790 tower_r_outer: 0,
791 inner_k: 0,
792 inner_shard_len: 0,
793 data_infos: BTreeMap::new(),
794 outer_rx: BTreeMap::new(),
795 seg_outer: BTreeMap::new(),
796 seg_d: BTreeMap::new(),
797 }
798 }
799
800 /// The configured reassembly-window bound, in blocks.
801 pub fn window_cap(&self) -> usize {
802 self.window_cap
803 }
804
805 /// Feed a sender heartbeat's `(send_ts, recv_ts)` pair (microseconds)
806 /// to the timing estimator, so the next feedback reports the OWD
807 /// trend and jitter-derived burstiness.
808 pub fn on_heartbeat(&mut self, send_ts: u64, recv_ts: u64) {
809 self.temporal.observe(send_ts, recv_ts);
810 // The relative one-way trip time (clock offset cancels in the Spike
811 // min/max range) feeds the loss differentiator's Spike (ROTT) input.
812 self.loss_class.observe_owd(recv_ts as f64 - send_ts as f64);
813 }
814
815 /// Current OWD trend slope from the timing estimator (raw, skew-inclusive).
816 pub fn owd_trend(&self) -> f64 {
817 self.temporal.owd_trend()
818 }
819
820 /// Estimated clock skew (the Moon-Skelly-Towsley lower-hull slope) and the
821 /// skew-corrected OWD trend the controller actually consumes (telemetry).
822 pub fn owd_skew(&self) -> f64 {
823 self.temporal.skew()
824 }
825
826 pub fn owd_trend_debiased(&self) -> f64 {
827 self.temporal.owd_trend_debiased()
828 }
829
830 /// Highest loss estimate (0..=255) the receiver has reached (telemetry).
831 pub fn peak_loss_x255(&self) -> u8 {
832 self.peak_loss
833 }
834
835 /// Drive the reported burstiness from the Gilbert-Elliott burst model (a
836 /// real mean burst length) instead of the jitter-ratio heuristic - the A/B
837 /// knob for confirming the model beats the heuristic at sizing interleave.
838 pub fn set_ge_burst(&mut self, on: bool) {
839 self.use_ge_burst = on;
840 }
841
842 /// Fitted mean burst length (consecutive lost shards) from the
843 /// Gilbert-Elliott model, or -1 before the fit converges (telemetry / A/B).
844 pub fn mean_burst_len(&self) -> f32 {
845 self.burst_model.mean_burst_len().map(|m| m as f32).unwrap_or(-1.0)
846 }
847
848 /// Lifetime count of D-SACK false recoveries the reordering guard detected:
849 /// spurious retransmissions whose reordered original later arrived. Zero on
850 /// a clean link; a nonzero value on a reorder-carrying link is the guard
851 /// firing on real reordered traffic (RFC 2883 / RFC 8985).
852 pub fn false_recovery_count(&self) -> u64 {
853 self.false_recoveries
854 }
855
856 /// The session epoch this decoder's state belongs to, once one
857 /// datagram has arrived.
858 pub fn session_epoch(&self) -> Option<u32> {
859 self.session_epoch
860 }
861
862 /// Record an epoch learned off the data path, from the heartbeat
863 /// announce. Recording is not adopting: the caller still challenges it.
864 pub fn note_unknown_epoch(&mut self, epoch: u32) {
865 if self.session_epoch != Some(epoch) {
866 self.unknown_epoch = Some(epoch);
867 }
868 }
869
870 /// An epoch seen that is not the established one, taken and cleared.
871 /// The caller challenges the address it arrived from and adopts only
872 /// on a valid answer.
873 pub fn take_unknown_epoch(&mut self) -> Option<u32> {
874 self.unknown_epoch.take()
875 }
876
877 /// Adopt `epoch` as the session: drop every block, gap and outer-code
878 /// record keyed to the previous session's block-id space, and restart
879 /// the delivery frontier where the new sender's ids begin.
880 ///
881 /// Called only once the challenge for `epoch` has been answered.
882 pub fn adopt_epoch(&mut self, epoch: u32) {
883 self.session_epoch = Some(epoch);
884 self.unknown_epoch = None;
885 self.window.clear();
886 self.next_deliver.store(0, Ordering::Relaxed);
887 self.highest_seen = 0;
888 self.highest_decoded = 0;
889 self.data_infos.clear();
890 self.outer_rx.clear();
891 self.seg_outer.clear();
892 self.seg_d.clear();
893 }
894
895 /// Highest block id seen on the wire. Below `next_needed` it means the
896 /// frontier is waiting on a block that has never arrived at all.
897 pub fn highest_seen(&self) -> u32 {
898 self.highest_seen
899 }
900
901 /// Block id the receiver next needs (everything below is delivered).
902 pub fn next_needed(&self) -> u32 {
903 self.next_deliver.load(Ordering::Relaxed)
904 }
905
906 /// Ingest one data datagram. Returns any items that became
907 /// deliverable, in stream order. Non-data datagrams yield nothing.
908 pub fn on_packet(&mut self, buf: &[u8]) -> Vec<Vec<u8>> {
909 self.ingest(buf, None)
910 }
911
912 /// Like [`on_packet`](Self::on_packet) but with the datagram's receiver-
913 /// clock arrival time (microseconds), which feeds the loss differentiator's
914 /// inter-arrival (Biaz) input. The socket layer supplies it; callers that do
915 /// not time arrivals use [`on_packet`](Self::on_packet) and the
916 /// differentiator falls back to its Spike (ROTT) signal alone.
917 pub fn on_packet_at(&mut self, buf: &[u8], recv_us: u64) -> Vec<Vec<u8>> {
918 self.ingest(buf, Some(recv_us))
919 }
920
921 fn ingest(&mut self, buf: &[u8], recv_us: Option<u64>) -> Vec<Vec<u8>> {
922 if !is_data(buf) || buf.len() < DATA_HEADER {
923 return Vec::new();
924 }
925 let block_id = u32::from_le_bytes([buf[1], buf[2], buf[3], buf[4]]);
926 let shard_index = buf[5] as usize;
927 let k = buf[6] as usize;
928 let r = buf[7] as usize;
929 let is_retransmit = buf[8] & FLAG_RETRANSMIT != 0;
930 let epoch = u32::from_le_bytes([
931 buf[EPOCH_OFFSET],
932 buf[EPOCH_OFFSET + 1],
933 buf[EPOCH_OFFSET + 2],
934 buf[EPOCH_OFFSET + 3],
935 ]);
936 // Session gate, ABOVE the block-id checks below. A restarted peer's
937 // ids start at the bottom again, so those checks read its whole
938 // stream as already-delivered duplicates. Record the epoch for the
939 // receiver to challenge; nothing under it is delivered until the
940 // answer returns and `adopt_epoch` runs.
941 match self.session_epoch {
942 None => self.session_epoch = Some(epoch),
943 Some(current) if current == epoch => {}
944 Some(_) => {
945 self.unknown_epoch = Some(epoch);
946 return Vec::new();
947 }
948 }
949 let payload = &buf[DATA_HEADER..];
950 // r == 0 is the Passthrough block: k data shards, no parity. It is a
951 // valid shape (the block completes when all k data shards arrive, via
952 // ARQ if any drop), so it is NOT rejected here.
953 if k == 0 || k + r > MAX_SHARDS || shard_index >= k + r {
954 return Vec::new();
955 }
956 // Tower outer-parity blocks live in a separate id space; they are
957 // handled opportunistically to recover whole-lost data blocks.
958 if block_id & OUTER_ID_BIT != 0 {
959 self.handle_outer(block_id, shard_index, k, r, payload);
960 return self.drain_in_order();
961 }
962 // A timestamped DATA-shard arrival feeds the loss differentiator's
963 // inter-arrival input (Biaz `T_min` / `T_i`). Outer-parity shards are
964 // excluded above, so this is the data-stream spacing the LDA expects.
965 if let Some(now) = recv_us {
966 if let Some(prev) = self.last_data_recv_us {
967 let ia = now.wrapping_sub(prev) as f64;
968 self.last_interarrival_us = ia;
969 self.loss_class.observe_interarrival(ia);
970 }
971 self.last_data_recv_us = Some(now);
972 }
973 if self.inner_k == 0 {
974 self.inner_k = k;
975 self.inner_shard_len = payload.len();
976 }
977 // Ignore packets for already-delivered blocks (duplicates /
978 // late ARQ).
979 if block_id < self.next_deliver.load(Ordering::Relaxed) {
980 return Vec::new();
981 }
982 // Bound the reassembly window: refuse blocks too far ahead of
983 // the delivery frontier. The sender's flow window
984 // ([`Encoder::in_flight`]) keeps it from outrunning this, so in
985 // correct operation this guard only fires under a bug or a
986 // hostile peer - it caps memory either way.
987 let next = self.next_deliver.load(Ordering::Relaxed);
988 if block_id >= next.saturating_add(self.window_cap as u32) {
989 return Vec::new();
990 }
991 if block_id > self.highest_seen {
992 self.highest_seen = block_id;
993 }
994 let shard_len = payload.len();
995 let blk = self
996 .window
997 .entry(block_id)
998 .or_insert_with(|| RxBlock::new(k, r, shard_len));
999 // FEC operates symbol-wise across equal-length shards; reject a
1000 // packet whose shape disagrees with the block it joins.
1001 if blk.shard_len != shard_len || blk.k != k || blk.r != r {
1002 return self.drain_in_order();
1003 }
1004 let bit = 1u32 << shard_index;
1005 if blk.mask.load(Ordering::Relaxed) & bit == 0 {
1006 blk.mask.fetch_or(bit, Ordering::Relaxed);
1007 blk.shards[shard_index] = Some(payload.to_vec());
1008 // First arrival via ARQ retransmit: its original was dropped, so
1009 // record it as wire loss for the estimator (otherwise a drop that
1010 // ARQ recovered at Passthrough would be invisible).
1011 if is_retransmit {
1012 blk.retransmitted |= bit;
1013 }
1014 } else if !is_retransmit && (blk.retransmitted & bit) != 0 {
1015 // The original arrives AFTER its ARQ retransmit already filled this
1016 // slot - a duplicate of an already-recovered shard. That is the
1017 // D-SACK signal (RFC 2883): reordering followed by a spurious
1018 // retransmission, NOT a loss. Mark it so the estimator discounts
1019 // the retransmit it counted. The slot keeps the retransmit's bytes
1020 // (identical to the original), so delivery is unchanged.
1021 blk.false_recovery |= bit;
1022 }
1023 // FEC-decode as soon as k of k+r shards are present.
1024 let mut decoded_info: Option<Vec<u8>> = None;
1025 if !blk.decoded && blk.count() as usize >= blk.k {
1026 // r == 0 is Passthrough: no parity to recover from, so the block
1027 // is complete exactly when all k data shards have arrived (ARQ
1028 // fills any gap before count reaches k). r > 0 uses RS erasure
1029 // decoding to rebuild missing shards from parity.
1030 let recovered = if blk.r == 0 {
1031 (0..blk.k).all(|i| blk.shards[i].is_some())
1032 } else {
1033 RsCode::new(blk.k, blk.r)
1034 .expect("valid k,r")
1035 .decode(&mut blk.shards)
1036 .is_ok()
1037 };
1038 if recovered {
1039 blk.decoded = true;
1040 // Concatenate the k data shards into the block info with one
1041 // allocation and k memcpys (extend_from_slice), not a clone
1042 // of each shard plus a byte-by-byte flatten - this is the
1043 // receiver's hottest per-block path.
1044 let mut info = Vec::with_capacity(blk.k * blk.shard_len);
1045 for i in 0..blk.k {
1046 if let Some(s) = &blk.shards[i] {
1047 info.extend_from_slice(s);
1048 }
1049 }
1050 decoded_info = Some(info);
1051 }
1052 }
1053 // Keep every decoded block's info available for tower recovery of
1054 // a neighbor in the same segment (bounded to the window by the
1055 // prune in `drain_in_order`).
1056 if let Some(info) = decoded_info {
1057 self.data_infos.insert(block_id, info);
1058 self.highest_decoded = self.highest_decoded.max(block_id);
1059 }
1060 self.drain_in_order()
1061 }
1062
1063 /// Reassemble an outer-parity block; on inner-decode, record its info
1064 /// for the segment so a whole-lost data block can be reconstructed.
1065 fn handle_outer(&mut self, oid: u32, shard_index: usize, k: usize, r: usize, payload: &[u8]) {
1066 let d = ((oid >> 27) & 0xF) as usize;
1067 let r_outer = ((oid >> 24) & 0x7) as usize;
1068 let seg_id = (oid >> 8) & 0xFFFF;
1069 let oidx = oid & 0xFF;
1070 if d == 0 || r_outer == 0 {
1071 return;
1072 }
1073 // `tower_d` tracks the FULL segment size (for segment-id math);
1074 // `seg_d` records this segment's actual data-block count, which is
1075 // smaller for the final partial segment.
1076 self.tower_d = d.max(self.tower_d);
1077 self.tower_r_outer = r_outer;
1078 self.seg_d.insert(seg_id, d);
1079 let shard_len = payload.len();
1080 let blk = self
1081 .outer_rx
1082 .entry(oid)
1083 .or_insert_with(|| RxBlock::new(k, r, shard_len));
1084 if blk.shard_len != shard_len || blk.k != k || blk.r != r {
1085 return;
1086 }
1087 let bit = 1u32 << shard_index;
1088 if blk.mask.load(Ordering::Relaxed) & bit == 0 {
1089 blk.mask.fetch_or(bit, Ordering::Relaxed);
1090 blk.shards[shard_index] = Some(payload.to_vec());
1091 }
1092 if !blk.decoded && blk.count() as usize >= blk.k {
1093 let code = RsCode::new(blk.k, blk.r).expect("valid k,r");
1094 if code.decode(&mut blk.shards).is_ok() {
1095 blk.decoded = true;
1096 let mut info = Vec::with_capacity(blk.k * blk.shard_len);
1097 for i in 0..blk.k {
1098 if let Some(s) = &blk.shards[i] {
1099 info.extend_from_slice(s);
1100 }
1101 }
1102 self.outer_rx.remove(&oid);
1103 self.seg_outer.entry(seg_id).or_default().insert(oidx, info);
1104 }
1105 }
1106 }
1107
1108 /// Attempt to reconstruct a whole-lost data block from its segment's
1109 /// surviving blocks plus outer parity. On success, inserts a decoded
1110 /// block into the window so [`drain_in_order`] delivers it. Returns
1111 /// `true` if the block was recovered.
1112 fn try_tower_recover(&mut self, block_id: u32) -> bool {
1113 let big_d = self.tower_d;
1114 let r_outer = self.tower_r_outer;
1115 if big_d == 0 || r_outer == 0 || self.inner_k == 0 {
1116 return false;
1117 }
1118 // Segment id / base use the full segment size; the segment's
1119 // actual data-block count may be smaller (partial final segment).
1120 let seg_id = block_id / big_d as u32;
1121 let base = seg_id * big_d as u32;
1122 let d = match self.seg_d.get(&seg_id) {
1123 Some(&d) => d,
1124 None => return false,
1125 };
1126 let idx_in_seg = (block_id - base) as usize;
1127 if idx_in_seg >= d {
1128 return false;
1129 }
1130 let outers = match self.seg_outer.get(&seg_id) {
1131 Some(m) => m,
1132 None => return false,
1133 };
1134 // Gather the d data infos and the r_outer outer infos.
1135 let mut blocks: Vec<Option<Vec<u8>>> = Vec::with_capacity(d + r_outer);
1136 for i in 0..d {
1137 blocks.push(self.data_infos.get(&(base + i as u32)).cloned());
1138 }
1139 for j in 0..r_outer {
1140 blocks.push(outers.get(&(j as u32)).cloned());
1141 }
1142 if blocks.iter().filter(|b| b.is_some()).count() < d {
1143 return false;
1144 }
1145 let code = match SegmentCode::new(d, r_outer) {
1146 Ok(c) => c,
1147 Err(_) => return false,
1148 };
1149 if code.decode(&mut blocks).is_err() {
1150 return false;
1151 }
1152 let info = match blocks[idx_in_seg].take() {
1153 Some(v) => v,
1154 None => return false,
1155 };
1156 // Split the recovered info back into k data shards and inject a
1157 // ready-to-deliver block.
1158 let k = self.inner_k;
1159 let shard_len = self.inner_shard_len.max(1);
1160 if info.len() != k * shard_len {
1161 return false;
1162 }
1163 let mut rb = RxBlock::new(k, 0, shard_len);
1164 for i in 0..k {
1165 rb.shards[i] = Some(info[i * shard_len..(i + 1) * shard_len].to_vec());
1166 rb.mask.fetch_or(1u32 << i, Ordering::Relaxed);
1167 }
1168 rb.decoded = true;
1169 self.data_infos.insert(block_id, info);
1170 self.highest_decoded = self.highest_decoded.max(block_id);
1171 self.window.insert(block_id, rb);
1172 true
1173 }
1174
1175 /// Deliver every contiguous decoded block starting at
1176 /// `next_deliver`.
1177 fn drain_in_order(&mut self) -> Vec<Vec<u8>> {
1178 let mut out = Vec::new();
1179 loop {
1180 let id = self.next_deliver.load(Ordering::Relaxed);
1181 let ready = matches!(self.window.get(&id), Some(b) if b.decoded);
1182 if !ready {
1183 // Head block missing or undecoded: try tower recovery
1184 // (reconstruct it from its segment's outer parity) before
1185 // stalling. ARQ remains the fallback if this fails.
1186 if !self.window.contains_key(&id) && self.try_tower_recover(id) {
1187 continue;
1188 }
1189 break;
1190 }
1191 let blk = self.window.remove(&id).unwrap();
1192 // Loss = data shards that did NOT arrive directly and had to be
1193 // recovered: FEC-reconstructed (a data position never received, so
1194 // absent from the mask) plus ARQ-retransmitted (received, but only
1195 // after its original dropped). Parity shards are redundancy, not
1196 // loss, so they are excluded - counting them made a clean link read
1197 // as r/(k+r) loss and pinned FEC on. The counters decay per block
1198 // (~32-block window) so the estimate follows the CURRENT link and
1199 // falls back to zero - and the controller back to Passthrough -
1200 // once loss clears.
1201 let data_mask: u32 = if blk.k >= 32 { u32::MAX } else { (1u32 << blk.k) - 1 };
1202 let data_present = (blk.mask.load(Ordering::Relaxed) & data_mask).count_ones() as u64;
1203 let fec_recovered = (blk.k as u64).saturating_sub(data_present);
1204 // A retransmit whose original later arrived (false_recovery) was a
1205 // spurious retransmission from reordering, not a drop; exclude it
1206 // so reordering does not inflate the estimate and needlessly arm
1207 // FEC (RACK-TLP reordering-vs-loss, RFC 8985). A retransmit with no
1208 // late original is a genuine loss and still counts. The guard's
1209 // subtraction is the A/B knob; the baseline counts every retransmit.
1210 let arq_recovered = if reorder_guard_enabled() {
1211 (blk.retransmitted & !blk.false_recovery & data_mask).count_ones() as u64
1212 } else {
1213 (blk.retransmitted & data_mask).count_ones() as u64
1214 };
1215 // Count the D-SACK false recoveries this block carried (the guard
1216 // firing on real reordered traffic), whether or not the subtraction
1217 // knob is on, so the count reflects detection on the wire.
1218 self.false_recoveries += (blk.false_recovery & data_mask).count_ones() as u64;
1219 self.total_expected = (self.total_expected * 31 / 32) + blk.k as u64;
1220 self.total_missing = (self.total_missing * 31 / 32) + fec_recovered + arq_recovered;
1221 // Differentiate this block's loss congestion-vs-wireless (Biaz +
1222 // Spike hybrid) so the sender treats the two regimes differently.
1223 // The gap is the real lost-shard count (false recoveries already
1224 // excluded from arq_recovered above).
1225 let gap = (fec_recovered + arq_recovered) as u32;
1226 if gap > 0 {
1227 let ia = self.last_interarrival_us;
1228 self.loss_class.classify(gap, ia);
1229 }
1230 // Feed the Gilbert-Elliott burst model the block's per-shard
1231 // original-loss trace in shard order: a shard received on its first
1232 // transmission is `mask & !retransmitted`; everything else (FEC-
1233 // reconstructed or ARQ-retried) was originally lost. At interleave
1234 // depth 1 this is the wire loss order, so the fit sees the native
1235 // burst structure.
1236 let first_tx = blk.mask.load(Ordering::Relaxed) & !blk.retransmitted;
1237 for i in 0..(blk.k + blk.r) {
1238 self.burst_model.observe(first_tx & (1u32 << i) == 0);
1239 }
1240 // Track the peak loss estimate (telemetry).
1241 let cur_loss = self
1242 .total_missing
1243 .saturating_mul(255)
1244 .checked_div(self.total_expected)
1245 .unwrap_or(0)
1246 .min(255) as u8;
1247 self.peak_loss = self.peak_loss.max(cur_loss);
1248 for i in 0..blk.k {
1249 let shard = blk.shards[i].as_ref().expect("decoded data shard");
1250 let item_len =
1251 u16::from_le_bytes([shard[0], shard[1]]) as usize;
1252 if item_len > 0 {
1253 let end = (ITEM_LEN_PREFIX + item_len).min(shard.len());
1254 out.push(shard[ITEM_LEN_PREFIX..end].to_vec());
1255 }
1256 }
1257 self.next_deliver.store(id + 1, Ordering::Relaxed);
1258 }
1259 // Bound bookkeeping to the reassembly window.
1260 let nd = self.next_deliver.load(Ordering::Relaxed);
1261 let keep_from = nd.saturating_sub(self.window_cap as u32);
1262 self.data_infos.retain(|&id, _| id >= keep_from);
1263 if self.tower_d > 0 {
1264 let keep_seg = (keep_from / self.tower_d as u32).saturating_sub(1);
1265 self.seg_outer.retain(|&s, _| s >= keep_seg);
1266 self.seg_d.retain(|&s, _| s >= keep_seg);
1267 self.outer_rx
1268 .retain(|&oid, _| ((oid >> 8) & 0xFFFF) >= keep_seg);
1269 }
1270 out
1271 }
1272
1273 /// Produce a feedback packet: always an ACK of the delivery
1274 /// frontier, plus a NAK for the oldest stalled block.
1275 ///
1276 /// `drive_arq` requests an unconditional NAK of the head block when
1277 /// it is present but undecoded. A receiver sets it on a recv timeout
1278 /// (no fresh data) so the LAST block - which has no newer block to
1279 /// trigger a NAK - still recovers from tail loss. With `drive_arq`
1280 /// false the NAK only fires once a newer block has arrived, which
1281 /// avoids NAKing a block whose shards may still be in flight.
1282 pub fn feedback(&self, drive_arq: bool) -> Feedback {
1283 let ack_through = self.next_deliver.load(Ordering::Relaxed);
1284 let (mut nak_block, mut nak_mask) = (NAK_NONE, 0u32);
1285 // The block we are waiting on is `ack_through`. We chase it once
1286 // it is overdue: a newer block arrived, or the caller is draining
1287 // a stalled tail.
1288 let overdue = drive_arq || self.highest_seen > ack_through;
1289 if overdue {
1290 match self.window.get(&ack_through) {
1291 // Partially received: NAK only the missing shards.
1292 Some(blk) if !blk.decoded => {
1293 let present = blk.mask.load(Ordering::Relaxed);
1294 let full = if blk.k + blk.r >= 32 {
1295 u32::MAX
1296 } else {
1297 (1u32 << (blk.k + blk.r)) - 1
1298 };
1299 nak_block = ack_through;
1300 nak_mask = full & !present;
1301 }
1302 // Entirely missing (zero shards) while later blocks have
1303 // arrived OR the caller is draining the tail: request ALL
1304 // of its shards. The sender clamps the mask to the
1305 // block's real shard count (and ignores a block it does
1306 // not hold). Without this, a head or tail block that
1307 // loses every shard can never be re-requested and
1308 // delivery deadlocks.
1309 None => {
1310 nak_block = ack_through;
1311 nak_mask = u32::MAX;
1312 }
1313 _ => {}
1314 }
1315 }
1316 let loss = self
1317 .total_missing
1318 .saturating_mul(255)
1319 .checked_div(self.total_expected)
1320 .unwrap_or(0)
1321 .min(255) as u8;
1322 // Burstiness proxy: jitter relative to the mean inter-arrival.
1323 // Steady spacing -> ~0; clustered arrivals (bursts) -> toward 1.
1324 let mean_ia = self.temporal.interarrival_micros().max(1.0);
1325 let heuristic = (self.temporal.jitter_micros() / mean_ia).clamp(0.0, 1.0);
1326 // With the Gilbert-Elliott model enabled, derive burstiness from the
1327 // REAL mean burst length (`mean_burst / 16` maps through the sender's
1328 // interleave mapping `depth = burstiness * 16` to `depth = mean_burst`),
1329 // falling back to the jitter heuristic until the fit converges.
1330 let burstiness = if self.use_ge_burst {
1331 self.burst_model
1332 .mean_burst_len()
1333 .map(|mb| (mb / 16.0).clamp(0.0, 1.0))
1334 .unwrap_or(heuristic)
1335 } else {
1336 heuristic
1337 };
1338 // Clock-skew-corrected: a relative clock drift makes the raw OWD slope
1339 // read a false rising / falling trend; the skew estimate removes it, so
1340 // only genuine queueing reaches the controller.
1341 let trend = self.temporal.owd_trend_debiased();
1342 let owd_trend_class = if trend > 0.02 {
1343 2
1344 } else if trend < -0.02 {
1345 0
1346 } else {
1347 1
1348 };
1349 Feedback {
1350 ack_through,
1351 nak_block,
1352 nak_mask,
1353 loss_x255: loss,
1354 burstiness_x255: (burstiness * 255.0) as u8,
1355 owd_trend_class,
1356 loss_class: self.loss_class.class_code(),
1357 }
1358 }
1359
1360 /// Enumerate EVERY gap the reassembly window is holding, as
1361 /// `(block_id, missing_shard_mask)`, so a caller can NAK them all in
1362 /// one feedback cycle instead of one-gap-per-round-trip serial
1363 /// recovery. A block received in part returns its still-missing shards;
1364 /// a block not seen at all returns `u32::MAX` (the sender clamps the
1365 /// mask to the block's real shard count). Gaps strictly below
1366 /// `highest_seen` are always overdue - a later block has arrived, so
1367 /// this one's shards are lost, not merely in flight. The block AT
1368 /// `highest_seen` (the tail) is included only when `drive_tail` is set,
1369 /// matching [`feedback`](Self::feedback)'s single-NAK overdue rule: the
1370 /// tail has no newer block to prove its shards should have arrived, so
1371 /// it is NAK'd only on a recv-timeout drain. The drain ALSO re-requests
1372 /// the head block when `next_deliver` has advanced AT OR ABOVE
1373 /// `highest_seen` - the case where every shard of the next expected
1374 /// (tail) block was lost, so it was never "seen" and sits above the
1375 /// `[next_deliver, highest_seen)` sweep. Without that, delivery
1376 /// deadlocks on a tail block whose whole datagrams were dropped. At
1377 /// most `max` gaps are returned (nearest the delivery frontier first),
1378 /// bounding the feedback burst; the rest are picked up on the next
1379 /// cycle.
1380 pub fn missing_blocks(&self, max: usize, drive_tail: bool) -> Vec<(u32, u32)> {
1381 let nd = self.next_deliver.load(Ordering::Relaxed);
1382 let hi = self.highest_seen;
1383 let mut gaps = Vec::new();
1384 let mut id = nd;
1385 // Genuine gaps sit only below the highest DECODED block: a later
1386 // block fully arrived, proving this one's shards are lost rather
1387 // than still in flight. On a clean link `highest_decoded` tracks
1388 // the delivery frontier, so this loop does nothing - the O(window)
1389 // scan that dominated the receiver is now paid only under real
1390 // loss, not on every poll.
1391 while id <= self.highest_decoded && gaps.len() < max {
1392 self.push_gap(id, &mut gaps);
1393 id = id.saturating_add(1);
1394 }
1395 // Under a drain, chase the block we are BLOCKED on: the tail at
1396 // `highest_seen` (nd <= hi), or the never-seen head above it
1397 // (nd > hi, every shard of the tail block lost). `nd.max(hi)`
1398 // selects whichever it is; a fully-lost tail block returns
1399 // `u32::MAX` (request all shards) so it cannot deadlock delivery.
1400 if drive_tail && gaps.len() < max {
1401 self.push_gap(nd.max(hi), &mut gaps);
1402 }
1403 gaps
1404 }
1405
1406 /// Append `(block_id, missing_mask)` to `gaps` if `block_id` is a gap
1407 /// (received-but-undecoded, or entirely unseen). A decoded block is
1408 /// not a gap and is skipped.
1409 fn push_gap(&self, id: u32, gaps: &mut Vec<(u32, u32)>) {
1410 match self.window.get(&id) {
1411 Some(blk) if !blk.decoded => {
1412 let present = blk.mask.load(Ordering::Relaxed);
1413 let full = if blk.k + blk.r >= 32 {
1414 u32::MAX
1415 } else {
1416 (1u32 << (blk.k + blk.r)) - 1
1417 };
1418 gaps.push((id, full & !present));
1419 }
1420 None => gaps.push((id, u32::MAX)),
1421 _ => {}
1422 }
1423 }
1424
1425 /// Blocks currently held in the reassembly window.
1426 pub fn window_len(&self) -> usize {
1427 self.window.len()
1428 }
1429
1430 /// Give up on the current head block (a gap held past its recovery
1431 /// deadline) and advance delivery past it, returning any items that
1432 /// become deliverable. This is the partial-reliability escape hatch:
1433 /// it skips an unrecoverable gap so the stream is not blocked forever,
1434 /// at the cost of those items. The caller decides the deadline; the
1435 /// transport holds the gap and recovers it via FEC/ARQ until then.
1436 pub fn skip_head(&mut self) -> Vec<Vec<u8>> {
1437 let id = self.next_deliver.load(Ordering::Relaxed);
1438 self.window.remove(&id);
1439 self.data_infos.remove(&id);
1440 self.next_deliver.store(id + 1, Ordering::Relaxed);
1441 self.drain_in_order()
1442 }
1443
1444 /// Diagnostic snapshot of the block currently blocking in-order
1445 /// delivery: `(block_id, received_shards, k, decoded)`, or `None`
1446 /// when that block has not been seen at all (no shard received yet).
1447 pub fn head_status(&self) -> Option<(u32, u32, usize, bool)> {
1448 let id = self.next_deliver.load(Ordering::Relaxed);
1449 self.window
1450 .get(&id)
1451 .map(|b| (id, b.count(), b.k, b.decoded))
1452 }
1453}
1454
1455#[cfg(test)]
1456mod tests {
1457 use super::*;
1458
1459 /// Per-block adaptive shard length: a block of small items ships
1460 /// datagrams sized to the item, not to `max_item`, so schema
1461 /// compression actually reaches the wire. A block sizes to its largest
1462 /// member, and all shards of a block are equal length (the FEC matrix
1463 /// requires it). The decoder reads each block's length from the
1464 /// datagram size, so no header field is added.
1465 #[test]
1466 fn per_block_shard_len_sizes_datagrams_to_items() {
1467 // Generous max_item; small items must NOT be padded up to it.
1468 let mut enc = Encoder::new(8, 2, 256);
1469 let mut dgrams = Vec::new();
1470 for _ in 0..8 {
1471 dgrams.extend(enc.push(&[7u8; 38]));
1472 }
1473 assert_eq!(dgrams.len(), 10, "k+r datagrams per block");
1474 let dlen = dgrams[0].len();
1475 assert_eq!(
1476 dlen,
1477 DATA_HEADER + ITEM_LEN_PREFIX + 38,
1478 "datagram sized to the 38B item, not max_item(256)"
1479 );
1480 assert!(
1481 dgrams.iter().all(|d| d.len() == dlen),
1482 "all shards of a block are equal length"
1483 );
1484
1485 // A block of larger items ships proportionally larger datagrams.
1486 let mut enc2 = Encoder::new(8, 2, 256);
1487 let mut big = Vec::new();
1488 for _ in 0..8 {
1489 big.extend(enc2.push(&[9u8; 200]));
1490 }
1491 assert_eq!(big[0].len(), DATA_HEADER + ITEM_LEN_PREFIX + 200);
1492 assert!(big[0].len() > dlen, "bigger items ship bigger datagrams");
1493
1494 // A mixed-size block sizes to its largest member.
1495 let mut enc3 = Encoder::new(8, 2, 256);
1496 let mut mixed = Vec::new();
1497 for n in [10usize, 50, 20, 40, 30, 12, 8, 25] {
1498 mixed.extend(enc3.push(&vec![1u8; n]));
1499 }
1500 assert_eq!(
1501 mixed[0].len(),
1502 DATA_HEADER + ITEM_LEN_PREFIX + 50,
1503 "block sizes to its 50B max member"
1504 );
1505 }
1506
1507 /// Deterministic LCG so loss / reorder patterns are reproducible.
1508 struct Lcg(u64);
1509 impl Lcg {
1510 fn next_u32(&mut self) -> u32 {
1511 self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
1512 (self.0 >> 33) as u32
1513 }
1514 /// `true` with probability `pct/100`.
1515 fn drop(&mut self, pct: u32) -> bool {
1516 self.next_u32() % 100 < pct
1517 }
1518 }
1519
1520 /// Drive `n` items end-to-end through a channel that drops `loss_pct`
1521 /// of DATA datagrams, with ARQ feedback flowing back. Asserts every
1522 /// item is delivered exactly once, in order.
1523 fn round_trip(n: usize, k: usize, r: usize, loss_pct: u32, seed: u64) {
1524 let mut enc = Encoder::new(k, r, 8);
1525 let mut dec = Decoder::new();
1526 let mut rng = Lcg(seed);
1527 let mut delivered: Vec<u64> = Vec::new();
1528
1529 // Outstanding datagrams from sender to receiver.
1530 let mut wire: Vec<Vec<u8>> = Vec::new();
1531 let send = |wire: &mut Vec<Vec<u8>>, pkts: Vec<Vec<u8>>| wire.extend(pkts);
1532
1533 for i in 0..n as u64 {
1534 send(&mut wire, enc.push(&i.to_le_bytes()));
1535 }
1536 send(&mut wire, enc.flush());
1537
1538 // Pump: deliver (lossily) sender->receiver, feed feedback back,
1539 // until the receiver has everything or we give up.
1540 let mut rounds = 0;
1541 while delivered.len() < n {
1542 rounds += 1;
1543 assert!(rounds < 10_000, "no convergence: {} / {n}", delivered.len());
1544 let batch = std::mem::take(&mut wire);
1545 for pkt in batch {
1546 if rng.drop(loss_pct) {
1547 continue; // packet lost on the wire
1548 }
1549 for item in dec.on_packet(&pkt) {
1550 delivered.push(u64::from_le_bytes(item.try_into().unwrap()));
1551 }
1552 }
1553 // Receiver feedback -> sender (feedback never lost here, so
1554 // ARQ can always make progress; FEC handles the data loss).
1555 // Each pump drives ARQ so a stalled tail is re-requested.
1556 let fb = dec.feedback(true);
1557 send(&mut wire, enc.on_feedback(&fb));
1558 if wire.is_empty() && delivered.len() < n {
1559 // Nothing in flight but still missing: re-request.
1560 let fb = dec.feedback(true);
1561 send(&mut wire, enc.on_feedback(&fb));
1562 if wire.is_empty() {
1563 panic!("stalled with {} / {n} delivered", delivered.len());
1564 }
1565 }
1566 }
1567 let expected: Vec<u64> = (0..n as u64).collect();
1568 assert_eq!(delivered, expected, "ordered exactly-once delivery");
1569 }
1570
1571 #[test]
1572 fn clean_channel_delivers_all() {
1573 round_trip(100, 8, 2, 0, 1);
1574 }
1575
1576 // k + r must be <= MAX_SHARDS (32): the per-block received bitmap is a u32,
1577 // and `1 << idx` for idx >= 32 overflows (a panic in debug, a wrapped mask in
1578 // release -> blocks never complete). k=16 leaves room for r up to 16 (50%
1579 // redundancy), enough for the extreme-loss regime the crossover targets.
1580 #[test]
1581 fn rs_k16_r8_clean() {
1582 round_trip(100, 16, 8, 0, 1);
1583 }
1584
1585 #[test]
1586 fn rs_k16_r16_clean() {
1587 round_trip(100, 16, 16, 0, 1);
1588 }
1589
1590 #[test]
1591 fn rs_k16_r8_loss30() {
1592 round_trip(2000, 16, 8, 30, 7);
1593 }
1594
1595 // k == MAX_SHARDS leaves no room for parity: the encoder must clamp r to 0
1596 // (Passthrough, ARQ-only) rather than emit k + r = 33 shards, which would
1597 // overflow the u32 bitmap (`1 << 32`). Before the r_max fix this panicked /
1598 // stalled; now the clean link delivers via the ARQ floor.
1599 #[test]
1600 fn rs_k32_clamps_to_passthrough() {
1601 round_trip(100, 32, 5, 0, 1);
1602 }
1603
1604 #[test]
1605 fn fec_recovers_light_loss_without_arq() {
1606 // ~10% loss with r=3 over k=8 is within FEC budget most blocks;
1607 // delivery must still be exact.
1608 round_trip(200, 8, 3, 10, 7);
1609 }
1610
1611 #[test]
1612 fn arq_recovers_heavy_loss() {
1613 // 35% loss exceeds any sane parity budget on many blocks; ARQ
1614 // must carry the rest.
1615 round_trip(150, 8, 2, 35, 42);
1616 }
1617
1618 #[test]
1619 fn tiny_blocks_and_flush() {
1620 // n not a multiple of k exercises the padded final block.
1621 round_trip(5, 4, 2, 0, 3);
1622 round_trip(13, 8, 2, 15, 99);
1623 }
1624
1625 #[test]
1626 fn heartbeat_feeds_owd_trend() {
1627 // A genuinely building queue must push the reported trend class to
1628 // "rising" (2). It climbs but dips to a flat baseline periodically -
1629 // a CLEAN linear rise would be indistinguishable from clock skew and
1630 // is removed by the skew correction, so the queue must touch baseline.
1631 let mut dec = Decoder::new();
1632 for i in 0..40u64 {
1633 let send = i * 1000;
1634 let queue = if i % 4 == 0 { 0 } else { i * 60 };
1635 let recv = send + 5000 + queue;
1636 dec.on_heartbeat(send, recv);
1637 }
1638 assert!(dec.owd_trend() > 0.0);
1639 assert_eq!(dec.feedback(true).owd_trend_class, 2, "rising trend reported");
1640 }
1641
1642 #[test]
1643 fn tail_loss_recovered_by_timeout_arq() {
1644 // Drop ALL parity (and one data shard) of the FINAL block - more
1645 // than r losses, and no newer block exists to trigger a NAK.
1646 // Only timeout-driven ARQ (`drive_arq`) can recover it.
1647 let k = 4;
1648 let r = 2;
1649 let mut enc = Encoder::new(k, r, 8);
1650 let mut dec = Decoder::new();
1651 let n = 4; // exactly one block
1652 let mut datagrams = Vec::new();
1653 for i in 0..n as u64 {
1654 datagrams.extend(enc.push(&i.to_le_bytes()));
1655 }
1656 datagrams.extend(enc.flush());
1657 // First pass: deliver only data shards 0,1,2 (drop shard 3 and
1658 // both parity) - block has 3 of 4, cannot FEC-decode.
1659 let mut delivered: Vec<u64> = Vec::new();
1660 for pkt in &datagrams {
1661 let idx = pkt[5];
1662 if idx <= 2 {
1663 for it in dec.on_packet(pkt) {
1664 delivered.push(u64::from_le_bytes(it.try_into().unwrap()));
1665 }
1666 }
1667 }
1668 assert!(delivered.is_empty(), "block not yet recoverable");
1669 // No newer block: a non-driving feedback must NOT NAK.
1670 assert_eq!(dec.feedback(false).nak_block, u32::MAX);
1671 // Timeout-driven feedback NAKs the stalled head.
1672 let fb = dec.feedback(true);
1673 assert_eq!(fb.nak_block, 0);
1674 let arq = enc.on_feedback(&fb);
1675 assert!(!arq.is_empty(), "sender retransmits the missing shards");
1676 for pkt in &arq {
1677 for it in dec.on_packet(pkt) {
1678 delivered.push(u64::from_le_bytes(it.try_into().unwrap()));
1679 }
1680 }
1681 assert_eq!(delivered, vec![0, 1, 2, 3], "tail recovered via ARQ");
1682 }
1683
1684 #[test]
1685 fn missing_head_block_recovered_by_whole_block_nak() {
1686 // A middle block that loses ALL its shards must still be
1687 // re-requested once a later block arrives, or delivery deadlocks
1688 // (the cross-host Direction-2 failure).
1689 let (k, r) = (4usize, 2usize);
1690 let mut enc = Encoder::new(k, r, 8);
1691 let mut dec = Decoder::new();
1692 let mut blocks: Vec<Vec<Vec<u8>>> = Vec::new();
1693 for i in 0..12u64 {
1694 let b = enc.push(&i.to_le_bytes());
1695 if !b.is_empty() {
1696 blocks.push(b);
1697 }
1698 }
1699 assert_eq!(blocks.len(), 3, "12 items / k=4 = 3 blocks");
1700
1701 let mut delivered: Vec<u64> = Vec::new();
1702 let feed = |dec: &mut Decoder, pkts: &[Vec<u8>], out: &mut Vec<u64>| {
1703 for p in pkts {
1704 for it in dec.on_packet(p) {
1705 out.push(u64::from_le_bytes(it.try_into().unwrap()));
1706 }
1707 }
1708 };
1709 // Deliver block 0, DROP all of block 1, deliver block 2.
1710 feed(&mut dec, &blocks[0], &mut delivered);
1711 feed(&mut dec, &blocks[2], &mut delivered);
1712 assert_eq!(delivered, vec![0, 1, 2, 3], "only block 0 deliverable");
1713 assert_eq!(dec.head_status(), None, "block 1 missing entirely");
1714
1715 // Non-drive feedback must now request the whole missing block 1.
1716 let fb = dec.feedback(false);
1717 assert_eq!(fb.nak_block, 1);
1718 assert_eq!(fb.nak_mask, u32::MAX, "request all shards of the lost block");
1719 let rtx = enc.on_feedback(&fb);
1720 assert!(!rtx.is_empty(), "sender retransmits the whole block");
1721 feed(&mut dec, &rtx, &mut delivered);
1722 assert_eq!(delivered, (0..12).collect::<Vec<_>>(), "blocks 1 and 2 delivered");
1723 }
1724
1725 #[test]
1726 fn fully_lost_tail_block_recovered_by_drain_nak() {
1727 // Whole-datagram loss at the TAIL via the selective-NAK path the
1728 // bridge uses (`missing_blocks`). Deliver block 0, then lose EVERY
1729 // shard of the final block 1: `next_deliver` advances to 1 while
1730 // `highest_seen` stays 0, so block 1 sits ABOVE the
1731 // [next_deliver, highest_seen) sweep. The drain must still
1732 // re-request it or delivery deadlocks on the tail - the cross-host
1733 // 30%-loss TIMEOUT this guards against.
1734 let (k, r) = (4usize, 2usize);
1735 let mut enc = Encoder::new(k, r, 8);
1736 let mut dec = Decoder::new();
1737 let mut blocks: Vec<Vec<Vec<u8>>> = Vec::new();
1738 for i in 0..8u64 {
1739 let b = enc.push(&i.to_le_bytes());
1740 if !b.is_empty() {
1741 blocks.push(b);
1742 }
1743 }
1744 assert_eq!(blocks.len(), 2, "8 items / k=4 = 2 blocks");
1745
1746 let mut delivered: Vec<u64> = Vec::new();
1747 let feed = |dec: &mut Decoder, pkts: &[Vec<u8>], out: &mut Vec<u64>| {
1748 for p in pkts {
1749 for it in dec.on_packet(p) {
1750 out.push(u64::from_le_bytes(it.try_into().unwrap()));
1751 }
1752 }
1753 };
1754 // Deliver block 0 fully; DROP every shard of the tail block 1.
1755 feed(&mut dec, &blocks[0], &mut delivered);
1756 assert_eq!(delivered, vec![0, 1, 2, 3], "block 0 delivered, tail unseen");
1757
1758 // Without a drain the unseen tail is not chased (shards could still
1759 // be in flight); under a drain it MUST be re-requested in full.
1760 assert!(
1761 dec.missing_blocks(64, false).is_empty(),
1762 "no drain: unseen tail not yet re-requested"
1763 );
1764 assert_eq!(
1765 dec.missing_blocks(64, true),
1766 vec![(1, u32::MAX)],
1767 "drain re-requests the whole lost tail block"
1768 );
1769
1770 // Sender retransmits block 1; delivery completes to the tail.
1771 let fb = Feedback {
1772 ack_through: 1,
1773 nak_block: 1,
1774 nak_mask: u32::MAX,
1775 loss_x255: 0,
1776 burstiness_x255: 0,
1777 owd_trend_class: 1,
1778 loss_class: 0,
1779 };
1780 let rtx = enc.on_feedback(&fb);
1781 assert!(!rtx.is_empty(), "sender retransmits the lost tail block");
1782 feed(&mut dec, &rtx, &mut delivered);
1783 assert_eq!(
1784 delivered,
1785 (0..8).collect::<Vec<_>>(),
1786 "tail recovered, all delivered"
1787 );
1788 }
1789
1790 #[test]
1791 fn tower_recovers_whole_lost_block_without_arq() {
1792 // A whole data block is erased (every shard). With the tower on,
1793 // the receiver reconstructs it from the segment's surviving blocks
1794 // plus outer parity - no NAK, no ARQ, delivered straight from
1795 // on_packet.
1796 let (k, r) = (4usize, 2usize);
1797 let (d, r_outer) = (4usize, 2usize);
1798 let mut enc = Encoder::new(k, r, 8);
1799 enc.enable_tower(d, r_outer);
1800 let mut dec = Decoder::new();
1801 let n = (d * k) as u64; // one full segment
1802 let mut wire: Vec<Vec<u8>> = Vec::new();
1803 for i in 0..n {
1804 wire.extend(enc.push(&i.to_le_bytes()));
1805 }
1806 let mut delivered: Vec<u64> = Vec::new();
1807 for pkt in &wire {
1808 let bid = u32::from_le_bytes([pkt[1], pkt[2], pkt[3], pkt[4]]);
1809 let is_outer = bid & 0x8000_0000 != 0;
1810 // Erase the ENTIRE second data block (id 1).
1811 if !is_outer && bid == 1 {
1812 continue;
1813 }
1814 for it in dec.on_packet(pkt) {
1815 delivered.push(u64::from_le_bytes(it.try_into().unwrap()));
1816 }
1817 }
1818 assert_eq!(
1819 delivered,
1820 (0..n).collect::<Vec<_>>(),
1821 "tower reconstructed the whole-lost block with no ARQ"
1822 );
1823 }
1824
1825 #[test]
1826 fn window_cap_bounds_far_ahead_blocks() {
1827 // A receiver with a 4-block window must refuse a block 10 ahead
1828 // of the delivery frontier (memory bound / backpressure).
1829 let mut dec = Decoder::with_window(4);
1830 let mut enc = Encoder::new(4, 1, 8);
1831 // Build block id 10 by sealing 10 blocks; keep only its packets.
1832 let mut far = Vec::new();
1833 for b in 0..=10u64 {
1834 let pkts = {
1835 let mut last = Vec::new();
1836 for i in 0..4u64 {
1837 last = enc.push(&(b * 4 + i).to_le_bytes());
1838 }
1839 last
1840 };
1841 if b == 10 {
1842 far = pkts;
1843 }
1844 }
1845 assert!(!far.is_empty(), "sealed block 10");
1846 for pkt in &far {
1847 assert!(dec.on_packet(pkt).is_empty());
1848 }
1849 assert_eq!(dec.window_len(), 0, "block 10 refused by the 4-block window");
1850 assert_eq!(dec.window_cap(), 4);
1851 }
1852
1853 #[test]
1854 fn flow_window_tracks_in_flight() {
1855 let mut enc = Encoder::new(8, 2, 8).with_flow_window(3);
1856 assert_eq!(enc.in_flight(), 0);
1857 for blk in 1..=4u64 {
1858 for i in 0..8u64 {
1859 enc.push(&(blk * 100 + i).to_le_bytes());
1860 }
1861 assert_eq!(enc.in_flight(), blk as u32);
1862 }
1863 assert!(enc.flow_blocked(), "4 in flight exceeds the 3-block window");
1864 // Receiver acks through block 3 (delivered 0,1,2): two remain.
1865 enc.on_feedback(&Feedback {
1866 ack_through: 3,
1867 nak_block: NAK_NONE,
1868 nak_mask: 0,
1869 loss_x255: 0,
1870 burstiness_x255: 0,
1871 owd_trend_class: 1,
1872 loss_class: 0,
1873 });
1874 assert_eq!(enc.in_flight(), 1);
1875 assert!(!enc.flow_blocked());
1876 }
1877
1878 #[test]
1879 fn proactive_retransmit_resends_unacked_oldest_first() {
1880 let k = 8usize;
1881 let mut enc = Encoder::new(k, 2, 8);
1882 // Seal three blocks (0, 1, 2); none acked yet.
1883 for blk in 0..3u64 {
1884 for i in 0..k as u64 {
1885 enc.push(&(blk * 100 + i).to_le_bytes());
1886 }
1887 }
1888 assert_eq!(enc.pending_len(), 3);
1889 assert_eq!(
1890 enc.oldest_pending(),
1891 Some(0),
1892 "block 0 is the frontier the receiver needs first"
1893 );
1894 // A probe of one block is its k data shards, retransmit-flagged.
1895 let probe = enc.probe_block(0);
1896 assert_eq!(probe.len(), k, "probe is the k data shards of the block");
1897 assert!(
1898 probe[0][8] & FLAG_RETRANSMIT != 0,
1899 "probe datagrams are retransmit-flagged for the D-SACK path"
1900 );
1901 assert!(enc.probe_block(99).is_empty(), "no probe for an unknown / acked block");
1902 // The recovery burst is the k data shards of every pending block,
1903 // oldest-first.
1904 let burst = enc.retransmit_all_data();
1905 assert_eq!(burst.len(), 3 * k, "k data shards per pending block");
1906 let lead = u32::from_le_bytes([burst[0][1], burst[0][2], burst[0][3], burst[0][4]]);
1907 assert_eq!(lead, 0, "burst leads with the oldest unacked block");
1908 // After the receiver acks through block 1 (delivered block 0), the
1909 // burst shrinks to the still-unacked blocks.
1910 enc.on_feedback(&Feedback {
1911 ack_through: 1,
1912 nak_block: NAK_NONE,
1913 nak_mask: 0,
1914 loss_x255: 0,
1915 burstiness_x255: 0,
1916 owd_trend_class: 1,
1917 loss_class: 0,
1918 });
1919 assert_eq!(enc.oldest_pending(), Some(1));
1920 assert_eq!(
1921 enc.retransmit_all_data().len(),
1922 2 * k,
1923 "the acked block is dropped from the burst"
1924 );
1925 }
1926
1927 #[test]
1928 fn parity_is_controller_driven_not_self_adapting() {
1929 let mut enc = Encoder::new(8, 1, 8);
1930 assert_eq!(enc.parity(), 1);
1931 // on_feedback must NOT change parity any more - that is the
1932 // fusion controller's job via set_parity.
1933 enc.on_feedback(&Feedback {
1934 ack_through: 0,
1935 nak_block: NAK_NONE,
1936 nak_mask: 0,
1937 loss_x255: (0.25 * 255.0) as u8,
1938 burstiness_x255: 0,
1939 owd_trend_class: 1,
1940 loss_class: 0,
1941 });
1942 assert_eq!(enc.parity(), 1, "feedback no longer self-adapts parity");
1943 enc.set_parity(3);
1944 assert_eq!(enc.parity(), 3, "controller sets parity");
1945 enc.set_parity(99);
1946 // r_max is now the bitmap ceiling MAX_SHARDS - k (k=8 -> 24), not the old
1947 // fixed 8, so a high-loss block can provision parity up to k + r = 32.
1948 assert_eq!(enc.parity(), MAX_SHARDS - 8, "clamped to r_max = MAX_SHARDS - k");
1949 }
1950
1951 #[test]
1952 fn reordered_original_after_retransmit_excluded_from_loss() {
1953 // A shard reordered on the wire: its premature ARQ retransmit arrives
1954 // and fills the slot first, then the late original arrives. Receiving
1955 // the same shard twice is the D-SACK signal (RFC 2883) - reordering,
1956 // not loss - so the estimator must not count it.
1957 let mut enc = Encoder::new(4, 0, 8); // r=0 Passthrough: ARQ-only recovery
1958 let mut dec = Decoder::new();
1959 let mut dgrams = Vec::new();
1960 for i in 0..4u64 {
1961 dgrams.extend(enc.push(&i.to_le_bytes()));
1962 }
1963 assert_eq!(dgrams.len(), 4, "k=4 r=0 -> 4 data datagrams");
1964
1965 // Shard 0 original.
1966 dec.on_packet(&dgrams[0]);
1967 // Shard 1 arrives FIRST as an ARQ retransmit (premature NAK), filling
1968 // the slot and counting as a wire loss.
1969 let mut rtx1 = dgrams[1].clone();
1970 rtx1[8] |= FLAG_RETRANSMIT;
1971 dec.on_packet(&rtx1);
1972 // The late ORIGINAL of shard 1 now arrives: the D-SACK duplicate.
1973 let out = dec.on_packet(&dgrams[1]);
1974 assert!(out.is_empty(), "block still incomplete (2 of 4)");
1975 // Complete the block with the remaining originals; it decodes/delivers.
1976 dec.on_packet(&dgrams[2]);
1977 let delivered = dec.on_packet(&dgrams[3]);
1978 let got: Vec<u64> = delivered
1979 .iter()
1980 .map(|it| u64::from_le_bytes(it.as_slice().try_into().unwrap()))
1981 .collect();
1982 assert_eq!(got, vec![0, 1, 2, 3], "in-order byte-exact delivery preserved");
1983
1984 // The reordered shard's retransmit was a spurious retransmission, so
1985 // the loss estimate - and its running peak - stay at zero.
1986 assert_eq!(
1987 dec.feedback(false).loss_x255,
1988 0,
1989 "reordering not counted as loss"
1990 );
1991 assert_eq!(dec.peak_loss_x255(), 0, "peak loss stays zero under reordering");
1992 assert_eq!(
1993 dec.false_recovery_count(),
1994 1,
1995 "the guard detected exactly one D-SACK false recovery"
1996 );
1997 }
1998
1999 #[test]
2000 fn genuine_retransmit_without_original_counts_as_loss() {
2001 // A shard whose original is truly lost: only its ARQ retransmit
2002 // arrives, with no late original to follow. That is a real drop
2003 // (RACK-TLP keeps it a loss, RFC 8985), so the estimator still counts
2004 // it - the reordering guard must not suppress genuine loss.
2005 let mut enc = Encoder::new(4, 0, 8);
2006 let mut dec = Decoder::new();
2007 let mut dgrams = Vec::new();
2008 for i in 0..4u64 {
2009 dgrams.extend(enc.push(&i.to_le_bytes()));
2010 }
2011 dec.on_packet(&dgrams[0]);
2012 dec.on_packet(&dgrams[1]);
2013 dec.on_packet(&dgrams[2]);
2014 // Shard 3's original was dropped; only its retransmit arrives.
2015 let mut rtx3 = dgrams[3].clone();
2016 rtx3[8] |= FLAG_RETRANSMIT;
2017 let delivered = dec.on_packet(&rtx3);
2018 assert_eq!(delivered.len(), 4, "block completes via the retransmit");
2019 assert!(
2020 dec.feedback(false).loss_x255 > 0,
2021 "a real drop recovered by ARQ is still counted as loss"
2022 );
2023 assert_eq!(
2024 dec.false_recovery_count(),
2025 0,
2026 "a genuine drop is not a D-SACK false recovery"
2027 );
2028 }
2029}