Skip to main content

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}