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