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