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