Skip to main content

rhdl_ubft/ctb/
receiver.rs

1//! The CTB receiver for one broadcaster (docs/rhdl_ubft-ctb.md).
2//!
3//! **Problem.** A Byzantine broadcaster can send different messages for the
4//! same index to different receivers. Upstream uBFT stops that with tail
5//! consistent broadcast, but answers every Byzantine input with `throw`
6//! (U10), spins on a tail gap (U09) and trusts remote lengths (U21).
7//!
8//! **Idea.** The same algorithm as a step function with explicit ports, so
9//! every Byzantine case has a defined next state:
10//! - fast path: every other receiver echoes the digest we computed;
11//! - slow path: the broadcaster's signature verifies, our register write is
12//!   visible, and every other receiver's register is checked;
13//! - a proven Byzantine broadcaster (duplicate or out-of-order index,
14//!   duplicate or invalid signature, equivocation, oversized length) is
15//!   **quarantined**: this instance delivers nothing more, and instances for
16//!   other broadcasters are untouched;
17//! - an index that leaves the tail undelivered becomes a `gap` beat in the
18//!   delivery stream, in order, never a silent skip or a spin;
19//! - every response carries the `(slot, index)` it was asked for, and a
20//!   response for an entry that has since left the tail is dropped;
21//! - **catch-up (R-26, U09/N17)**: with [`CatchupIn::enable`], an index
22//!   that would become a gap is first *fetched* from the other receivers
23//!   (each answers from its own tail: data plus the broadcaster's
24//!   signature). A fetched message enters its normal tail entry and must
25//!   pass every check a received one does (digest, the broadcaster's
26//!   signature, echoes or our register write plus every other receiver's
27//!   register), so it can never split receivers. A fetched entry whose
28//!   signature fails is dropped (the answering receiver lied; the
29//!   broadcaster is not blamed). Only when no verified copy arrives within
30//!   [`CatchupIn::wait`] cycles does the index become a gap, as before
31//!   (docs/rhdl_ubft-ctb-catchup.md).
32//!
33//! **Re-initialization.** `cfg.init` bumps a 32-bit generation that every
34//! request carries; a response of another generation is dropped. That
35//! fences responses, not memory: a register write issued before `init`
36//! may still land afterwards. It cannot be mistaken for evidence: its
37//! signature binds the old session, so in the new session it is either
38//! older/other-slot (checked) or fails verification (checked), never a
39//! conflict. The caller must still drain the register writer before
40//! `init` (the writer's own drain contract), so two writes to one
41//! register are never in flight at once.
42//!
43//! **Bounds.** `T` (tail, a power of two, at most 16) and `P` (payload
44//! words, at most 16) are build constants. Indices are 32-bit; an index at
45//! or above `0xFFFF_0000` is refused (no wrap).
46use rhdl::prelude::*;
47
48/// Most replicas (receiver bitmasks are 16 bits).
49pub const MAX_REPLICAS: usize = 16;
50
51/// Fault codes (bits of [`RxState::fault`]); any one quarantines.
52pub mod fault {
53    /// Configuration refused (n, me, b or session).
54    pub const CONFIG: u128 = 1;
55    /// Data index not strictly increasing (duplicate or reordered).
56    pub const ORDER: u128 = 2;
57    /// Data length above `P`.
58    pub const LENGTH: u128 = 4;
59    /// A second signature for an index.
60    pub const DUP_SIG: u128 = 8;
61    /// The broadcaster's signature does not verify.
62    pub const BAD_SIG: u128 = 16;
63    /// Two valid signatures for different digests at one index.
64    pub const EQUIVOCATION: u128 = 32;
65    /// An index at the wrap guard.
66    pub const WRAP: u128 = 64;
67}
68
69/// One-time configuration.
70#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
71pub struct RxConfig {
72    /// Configure this cycle (clears all state).
73    pub init: bool,
74    /// Replicas (3..=16).
75    pub n: b5,
76    /// This receiver.
77    pub me: b5,
78    /// The broadcaster this instance serves (not `me`).
79    pub b: b5,
80    /// The run (nonzero).
81    pub session: b64,
82}
83
84/// A data message from the broadcaster, already length-checked by the wire
85/// parser (the core checks again).
86#[derive(Clone, Copy, Debug, PartialEq, Digital)]
87pub struct DataIn<const P: usize> {
88    pub valid: bool,
89    pub idx: b32,
90    /// Payload words used (0..=P).
91    pub len: b5,
92    pub payload: [b32; P],
93}
94
95/// An echo: receiver `from` saw `digest` at `idx`.
96#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
97pub struct EchoIn {
98    pub valid: bool,
99    pub from: b5,
100    pub idx: b32,
101    pub digest: [b64; 4],
102}
103
104/// The broadcaster's signature for `idx`.
105#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
106pub struct SigIn {
107    pub valid: bool,
108    pub idx: b32,
109    pub sig: [b64; 8],
110}
111
112/// Digest of an entry's data (from the SHA-512 ring, truncated to 32 bytes,
113/// as little-endian words: byte `j` of the digest is byte `j % 8` of word
114/// `j / 8`).
115#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
116pub struct DigestIn {
117    pub valid: bool,
118    /// The receiver's generation (bumped at every configuration); a
119    /// response is taken only with the current one.
120    pub epoch: b32,
121    pub slot: b4,
122    pub idx: b32,
123    pub digest: [b64; 4],
124}
125
126/// A verification result. `conflict`: the check of another receiver's
127/// register, else the broadcaster's signature on our own entry.
128#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
129pub struct VerdictIn {
130    pub valid: bool,
131    /// The receiver's generation (bumped at every configuration); a
132    /// response is taken only with the current one.
133    pub epoch: b32,
134    pub slot: b4,
135    pub idx: b32,
136    pub conflict: bool,
137    pub ok: bool,
138}
139
140/// Our register write for `(slot, idx)` is visible at a majority.
141#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
142pub struct WriteDoneIn {
143    pub valid: bool,
144    /// The receiver's generation (bumped at every configuration); a
145    /// response is taken only with the current one.
146    pub epoch: b32,
147    pub slot: b4,
148    pub idx: b32,
149}
150
151/// The answer to our read of receiver `from`'s register for `slot`, asked
152/// on behalf of our entry `idx`. `ok`: the read completed (a failed read is
153/// retried, never counted). `empty`: the register was never written.
154#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
155pub struct ReadAnswerIn {
156    pub valid: bool,
157    /// The receiver's generation (bumped at every configuration); a
158    /// response is taken only with the current one.
159    pub epoch: b32,
160    pub from: b5,
161    pub slot: b4,
162    pub idx: b32,
163    pub ok: bool,
164    pub empty: bool,
165    pub r_idx: b32,
166    pub r_digest: [b64; 4],
167    pub r_sig: [b64; 8],
168}
169
170/// Catch-up settings, sampled every cycle (default: disabled, which is the
171/// pre-R-26 behaviour).
172#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
173pub struct CatchupIn {
174    pub enable: bool,
175    /// Cycles to wait for a verified copy before reporting a gap.
176    pub wait: b32,
177}
178
179/// Receiver `from` asks us (as a responder) for message `idx` of this
180/// broadcaster; `epoch` is the asker's, echoed back.
181#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
182pub struct FetchReqIn {
183    pub valid: bool,
184    pub from: b5,
185    pub epoch: b32,
186    pub idx: b32,
187}
188
189/// An answer to our fetch from receiver `from`: message `idx` with the
190/// broadcaster's signature, as `from` holds it. Untrusted until it passes
191/// the checks; taken only from the receiver currently asked.
192#[derive(Clone, Copy, Debug, PartialEq, Digital)]
193pub struct FetchAnsIn<const P: usize> {
194    pub valid: bool,
195    pub from: b5,
196    pub epoch: b32,
197    pub idx: b32,
198    pub len: b5,
199    pub payload: [b32; P],
200    pub sig: [b64; 8],
201}
202
203/// Ask receiver `to` for message `idx`. One receiver at a time, rotating
204/// on every re-send, so a fast liar cannot starve an honest copy.
205#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
206pub struct FetchReqOut {
207    pub valid: bool,
208    pub to: b5,
209    pub epoch: b32,
210    pub idx: b32,
211}
212
213/// Our answer to receiver `to`'s fetch.
214#[derive(Clone, Copy, Debug, PartialEq, Digital)]
215pub struct FetchAnsOut<const P: usize> {
216    pub valid: bool,
217    pub to: b5,
218    pub epoch: b32,
219    pub idx: b32,
220    pub len: b5,
221    pub payload: [b32; P],
222    pub sig: [b64; 8],
223}
224
225/// Inputs. Every request output is taken when its `*_ready` is set in the
226/// same cycle.
227#[derive(Clone, Copy, Debug, PartialEq, Digital)]
228pub struct RxIn<const P: usize> {
229    pub cfg: RxConfig,
230    pub data: DataIn<P>,
231    pub echo: EchoIn,
232    pub sig: SigIn,
233    pub digest: DigestIn,
234    pub verdict: VerdictIn,
235    pub wdone: WriteDoneIn,
236    pub rans: ReadAnswerIn,
237    pub catchup: CatchupIn,
238    pub freq: FetchReqIn,
239    pub fans: FetchAnsIn<P>,
240    /// Our fetch request and our fetch answer are taken.
241    pub fetch_ready: bool,
242    pub fans_ready: bool,
243    pub digest_ready: bool,
244    pub echo_ready: bool,
245    pub verify_ready: bool,
246    pub write_ready: bool,
247    pub read_ready: bool,
248    pub deliver_ready: bool,
249}
250
251/// Ask the SHA-512 ring for an entry's digest.
252#[derive(Clone, Copy, Debug, PartialEq, Digital)]
253pub struct DigestReq<const P: usize> {
254    pub valid: bool,
255    /// The receiver's generation (bumped at every configuration); a
256    /// response is taken only with the current one.
257    pub epoch: b32,
258    pub slot: b4,
259    pub idx: b32,
260    pub len: b5,
261    pub payload: [b32; P],
262}
263
264/// Send our echo to every other receiver.
265#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
266pub struct EchoOut {
267    pub valid: bool,
268    pub idx: b32,
269    pub digest: [b64; 4],
270}
271
272/// Verify `sig` under the broadcaster's key for the CTB context of
273/// `digest` (role 2; see [`super::ctb_context`]).
274#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
275pub struct VerifyReq {
276    pub valid: bool,
277    /// The receiver's generation (bumped at every configuration); a
278    /// response is taken only with the current one.
279    pub epoch: b32,
280    pub slot: b4,
281    pub idx: b32,
282    pub conflict: bool,
283    pub digest: [b64; 4],
284    pub sig: [b64; 8],
285}
286
287/// Write our register for `slot`: `(idx, digest, sig)`.
288#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
289pub struct WriteReq {
290    pub valid: bool,
291    /// The receiver's generation (bumped at every configuration); a
292    /// response is taken only with the current one.
293    pub epoch: b32,
294    pub slot: b4,
295    pub idx: b32,
296    pub digest: [b64; 4],
297    pub sig: [b64; 8],
298}
299
300/// Read receiver `from`'s register for `slot`, for our entry `idx`.
301#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
302pub struct ReadReq {
303    pub valid: bool,
304    /// The receiver's generation (bumped at every configuration); a
305    /// response is taken only with the current one.
306    pub epoch: b32,
307    pub from: b5,
308    pub slot: b4,
309    pub idx: b32,
310}
311
312/// One delivery beat, in index order: message `idx`, or a gap: indices
313/// `lo..idx` are lost (they left the tail undelivered, or never arrived
314/// although later ones did). Beats cover every index exactly once.
315#[derive(Clone, Copy, Debug, PartialEq, Digital)]
316pub struct Delivery<const P: usize> {
317    pub valid: bool,
318    pub gap: bool,
319    /// First lost index (gap beats).
320    pub lo: b32,
321    pub idx: b32,
322    pub len: b5,
323    pub payload: [b32; P],
324    /// Delivered by the slow path (for counters).
325    pub slow: bool,
326}
327
328/// Outputs.
329#[derive(Clone, Copy, Debug, PartialEq, Digital)]
330pub struct RxOut<const P: usize> {
331    pub data_ready: bool,
332    pub digest_req: DigestReq<P>,
333    pub echo: EchoOut,
334    pub verify: VerifyReq,
335    pub write: WriteReq,
336    pub read: ReadReq,
337    pub deliver: Delivery<P>,
338    pub fetch: FetchReqOut,
339    pub fans: FetchAnsOut<P>,
340    /// Faults seen (see [`fault`]); nonzero means quarantined.
341    pub fault: b8,
342    pub configured: bool,
343    /// No register write of ours is in flight: `cfg.init` is honored only
344    /// then (a write cannot be cancelled, so a reset must not let a fresh
345    /// write to the same register overlap it).
346    pub quiescent: bool,
347    /// `cfg.init` was presented while not quiescent and was ignored.
348    pub init_refused: bool,
349}
350
351/// One tail slot.
352#[derive(Clone, Copy, Debug, PartialEq, Digital)]
353pub struct Entry<const P: usize> {
354    /// Holds the broadcaster's data for `idx`.
355    pub live: bool,
356    /// Holds only echoes for `idx` (data not yet here).
357    pub early: bool,
358    pub idx: b32,
359    pub len: b5,
360    pub payload: [b32; P],
361    pub dig_sent: bool,
362    pub dig_have: bool,
363    pub digest: [b64; 4],
364    /// Reference for echoes that arrive before the digest is known.
365    pub eref_have: bool,
366    pub eref: [b64; 4],
367    /// Receivers whose echo was counted.
368    pub echo_bits: b16,
369    /// Some echo differed: no fast path for this entry.
370    pub echo_bad: bool,
371    pub echo_sent: bool,
372    pub sig_have: bool,
373    pub sig: [b64; 8],
374    pub ver_sent: bool,
375    pub sig_ok: bool,
376    pub wr_sent: bool,
377    pub wr_done: bool,
378    /// Register reads in flight, and receivers checked.
379    pub rd_sent: b16,
380    pub checked: b16,
381    /// A newer index was found in a register: no slow path.
382    pub stuck: bool,
383    pub delivered: bool,
384    /// Came from a fetch answer, not the broadcaster: a bad signature drops
385    /// it (the answering receiver lied) instead of quarantining `b`.
386    pub fetched: bool,
387}
388
389/// State.
390#[derive(Clone, Copy, Debug, PartialEq, Digital)]
391pub struct RxState<const T: usize, const P: usize> {
392    pub configured: bool,
393    /// Generation: requests carry it, stale responses are dropped.
394    pub epoch: b32,
395    pub n: b5,
396    pub me: b5,
397    pub b: b5,
398    pub session: b64,
399    /// Every receiver but `me` and `b`.
400    pub others: b16,
401    pub fault: b8,
402    pub any_data: bool,
403    pub last_idx: b32,
404    pub e: [Entry<P>; T],
405    /// Our register write per slot is in flight (outlives its entry).
406    pub reg_busy: [bool; T],
407    /// The index whose write is in flight per slot.
408    pub busy_idx: [b32; T],
409    /// One conflicting register under verification at a time.
410    pub conf_busy: bool,
411    pub conf_sent: bool,
412    pub conf_slot: b4,
413    /// The entry the check is for.
414    pub conf_idx: b32,
415    /// The index the other receiver's register claims, and whether it is
416    /// newer than the entry (else: the same index, another digest).
417    pub conf_ridx: b32,
418    pub conf_newer: bool,
419    pub conf_from: b5,
420    pub conf_digest: [b64; 4],
421    pub conf_sig: [b64; 8],
422    /// The next index the consumer gets (as a message or inside a gap).
423    pub next_out: b32,
424    /// Output holding registers: a request, once presented, stays
425    /// unchanged until taken (the stream rule).
426    pub h_dig: DigestReq<P>,
427    pub h_echo: EchoOut,
428    pub h_ver: VerifyReq,
429    pub h_wr: WriteReq,
430    pub h_rd: ReadReq,
431    pub h_dl: Delivery<P>,
432    /// Counters.
433    pub delivered_fast: b32,
434    pub delivered_slow: b32,
435    pub gaps: b32,
436    /// Catch-up (R-26): a cycle counter; the index being fetched, since
437    /// when, when last asked, whether a copy is in its tail entry; the last
438    /// index whose fetch failed (it is not fetched twice); holding
439    /// registers; indices recovered.
440    pub clock: b32,
441    pub f_busy: bool,
442    pub f_idx: b32,
443    pub f_start: b32,
444    pub f_last: b32,
445    pub f_inj: bool,
446    /// The receiver currently asked.
447    pub f_to: b5,
448    pub f_tried: bool,
449    pub f_tried_idx: b32,
450    pub h_fx: FetchReqOut,
451    pub h_fa: FetchAnsOut<P>,
452    pub recovered: b32,
453}
454
455impl<const P: usize> Entry<P> {
456    pub fn empty() -> Self {
457        Self {
458            live: false,
459            early: false,
460            idx: b32(0),
461            len: b5(0),
462            payload: [b32(0); P],
463            dig_sent: false,
464            dig_have: false,
465            digest: [b64(0); 4],
466            eref_have: false,
467            eref: [b64(0); 4],
468            echo_bits: b16(0),
469            echo_bad: false,
470            echo_sent: false,
471            sig_have: false,
472            sig: [b64(0); 8],
473            ver_sent: false,
474            sig_ok: false,
475            wr_sent: false,
476            wr_done: false,
477            rd_sent: b16(0),
478            checked: b16(0),
479            stuck: false,
480            delivered: false,
481            fetched: false,
482        }
483    }
484}
485
486impl<const T: usize, const P: usize> RxState<T, P> {
487    pub fn idle() -> Self {
488        Self {
489            configured: false,
490            epoch: b32(0),
491            n: b5(0),
492            me: b5(0),
493            b: b5(0),
494            session: b64(0),
495            others: b16(0),
496            fault: b8(0),
497            any_data: false,
498            last_idx: b32(0),
499            e: [Entry::empty(); T],
500            reg_busy: [false; T],
501            busy_idx: [b32(0); T],
502            conf_busy: false,
503            conf_sent: false,
504            conf_slot: b4(0),
505            conf_idx: b32(0),
506            conf_ridx: b32(0),
507            conf_newer: false,
508            conf_from: b5(0),
509            conf_digest: [b64(0); 4],
510            conf_sig: [b64(0); 8],
511            next_out: b32(0),
512            h_dig: DigestReq::<P> { valid: false, epoch: b32(0), slot: b4(0), idx: b32(0), len: b5(0), payload: [b32(0); P] },
513            h_echo: EchoOut::default(),
514            h_ver: VerifyReq::default(),
515            h_wr: WriteReq::default(),
516            h_rd: ReadReq::default(),
517            h_dl: Delivery::<P> { valid: false, gap: false, lo: b32(0), idx: b32(0), len: b5(0), payload: [b32(0); P], slow: false },
518            delivered_fast: b32(0),
519            delivered_slow: b32(0),
520            gaps: b32(0),
521            clock: b32(0),
522            f_busy: false,
523            f_idx: b32(0),
524            f_start: b32(0),
525            f_last: b32(0),
526            f_inj: false,
527            f_to: b5(0),
528            f_tried: false,
529            f_tried_idx: b32(0),
530            h_fx: FetchReqOut::default(),
531            h_fa: FetchAnsOut::<P> { valid: false, to: b5(0), epoch: b32(0), idx: b32(0), len: b5(0), payload: [b32(0); P], sig: [b64(0); 8] },
532            recovered: b32(0),
533        }
534    }
535}
536
537impl<const P: usize> Default for Entry<P> {
538    fn default() -> Self {
539        Self::empty()
540    }
541}
542
543impl<const T: usize, const P: usize> Default for RxState<T, P> {
544    fn default() -> Self {
545        Self::idle()
546    }
547}
548
549impl<const P: usize> Default for DataIn<P> {
550    fn default() -> Self {
551        Self { valid: false, idx: b32(0), len: b5(0), payload: [b32(0); P] }
552    }
553}
554
555impl<const P: usize> Default for RxIn<P> {
556    fn default() -> Self {
557        Self {
558            cfg: RxConfig::default(),
559            data: DataIn::default(),
560            echo: EchoIn::default(),
561            sig: SigIn::default(),
562            digest: DigestIn::default(),
563            verdict: VerdictIn::default(),
564            wdone: WriteDoneIn::default(),
565            rans: ReadAnswerIn::default(),
566            catchup: CatchupIn::default(),
567            freq: FetchReqIn::default(),
568            fans: FetchAnsIn::<P> { valid: false, from: b5(0), epoch: b32(0), idx: b32(0), len: b5(0), payload: [b32(0); P], sig: [b64(0); 8] },
569            fetch_ready: false,
570            fans_ready: false,
571            digest_ready: false,
572            echo_ready: false,
573            verify_ready: false,
574            write_ready: false,
575            read_ready: false,
576            deliver_ready: false,
577        }
578    }
579}
580
581/// A zero entry, for use inside kernels.
582#[kernel]
583pub fn empty_entry<const P: usize>() -> Entry<P> {
584    Entry::<P> {
585        live: false,
586        early: false,
587        idx: b32(0),
588        len: b5(0),
589        payload: [b32(0); P],
590        dig_sent: false,
591        dig_have: false,
592        digest: [b64(0); 4],
593        eref_have: false,
594        eref: [b64(0); 4],
595        echo_bits: b16(0),
596        echo_bad: false,
597        echo_sent: false,
598        sig_have: false,
599        sig: [b64(0); 8],
600        ver_sent: false,
601        sig_ok: false,
602        wr_sent: false,
603        wr_done: false,
604        rd_sent: b16(0),
605        checked: b16(0),
606        stuck: false,
607        delivered: false,
608        fetched: false,
609    }
610}
611
612/// An idle fetch answer, for kernels.
613#[kernel]
614pub fn idle_fans<const P: usize>() -> FetchAnsOut<P> {
615    FetchAnsOut::<P> { valid: false, to: b5(0), epoch: b32(0), idx: b32(0), len: b5(0), payload: [b32(0); P], sig: [b64(0); 8] }
616}
617
618/// An idle digest request, for kernels.
619#[kernel]
620pub fn idle_digest_req<const P: usize>() -> DigestReq<P> {
621    DigestReq::<P> { valid: false, epoch: b32(0), slot: b4(0), idx: b32(0), len: b5(0), payload: [b32(0); P] }
622}
623
624/// An idle delivery beat, for kernels.
625#[kernel]
626pub fn idle_delivery<const P: usize>() -> Delivery<P> {
627    Delivery::<P> { valid: false, gap: false, lo: b32(0), idx: b32(0), len: b5(0), payload: [b32(0); P], slow: false }
628}
629
630/// Every replica id below `n` except `me` and `b`.
631#[kernel]
632pub fn others_mask(n: b5, me: b5, b: b5) -> b16 {
633    let mut m = b16(0);
634    for k in 0..16 {
635        let id = b5(k as u128);
636        if id < n && id != me && id != b {
637            m = m | (b16(1) << b16(k as u128));
638        }
639    }
640    m
641}
642
643/// One cycle of the receiver.
644#[kernel]
645pub fn rx_step<const T: usize, const P: usize>(i: RxIn<P>, r: RxState<T, P>) -> (RxOut<P>, RxState<T, P>) {
646    let mut s = r;
647    let mask = b32(T as u128) - b32(1);
648    let tail = b33(T as u128);
649    let mut quiescent = true;
650    for k in 0..T {
651        if r.reg_busy[k] {
652            quiescent = false;
653        }
654    }
655    // A reset is honored only when quiescent; otherwise it is ignored (and
656    // reported) and the instance carries on.
657    let init = i.cfg.init && quiescent;
658
659    // ------------------------------------------------------------ config
660    if init {
661        let mut fresh = r;
662        fresh.configured = true;
663        fresh.epoch = r.epoch + b32(1);
664        fresh.n = i.cfg.n;
665        fresh.me = i.cfg.me;
666        fresh.b = i.cfg.b;
667        fresh.session = i.cfg.session;
668        fresh.others = others_mask(i.cfg.n, i.cfg.me, i.cfg.b);
669        fresh.fault = b8(0);
670        fresh.any_data = false;
671        fresh.last_idx = b32(0);
672        for k in 0..T {
673            fresh.e[k] = empty_entry::<P>();
674            fresh.reg_busy[k] = false;
675            fresh.busy_idx[k] = b32(0);
676        }
677        fresh.h_dig = idle_digest_req::<P>();
678        fresh.h_echo = EchoOut { valid: false, idx: b32(0), digest: [b64(0); 4] };
679        fresh.h_ver = VerifyReq { valid: false, epoch: b32(0), slot: b4(0), idx: b32(0), conflict: false, digest: [b64(0); 4], sig: [b64(0); 8] };
680        fresh.h_wr = WriteReq { valid: false, epoch: b32(0), slot: b4(0), idx: b32(0), digest: [b64(0); 4], sig: [b64(0); 8] };
681        fresh.h_rd = ReadReq { valid: false, epoch: b32(0), from: b5(0), slot: b4(0), idx: b32(0) };
682        fresh.h_dl = idle_delivery::<P>();
683        fresh.conf_busy = false;
684        fresh.conf_sent = false;
685        fresh.next_out = b32(0);
686        fresh.delivered_fast = b32(0);
687        fresh.delivered_slow = b32(0);
688        fresh.gaps = b32(0);
689        fresh.f_busy = false;
690        fresh.f_inj = false;
691        fresh.f_tried = false;
692        fresh.h_fx = FetchReqOut { valid: false, to: b5(0), epoch: b32(0), idx: b32(0) };
693        fresh.h_fa = idle_fans::<P>();
694        fresh.recovered = b32(0);
695        let bad = i.cfg.n < b5(3)
696            || i.cfg.n > b5(16)
697            || i.cfg.me >= i.cfg.n
698            || i.cfg.b >= i.cfg.n
699            || i.cfg.me == i.cfg.b
700            || i.cfg.session == b64(0);
701        if bad {
702            fresh.fault = b8(fault::CONFIG);
703        }
704        s = fresh;
705    }
706    let live_cfg = r.configured && !init;
707    let quarantined = r.fault != b8(0);
708    let mut fault_now = b8(0);
709
710    // ------------------------------------------------------------ responses
711    // Digest of an entry's data.
712    if live_cfg && i.digest.valid && i.digest.epoch == r.epoch {
713        let k = i.digest.slot;
714        let e = s.e[k];
715        if e.live && e.idx == i.digest.idx && !e.dig_have {
716            s.e[k].dig_have = true;
717            s.e[k].digest = i.digest.digest;
718            // Echoes counted before the digest was known were compared
719            // with the first echo; the data must match that too.
720            if e.eref_have && e.eref != i.digest.digest {
721                s.e[k].echo_bad = true;
722            }
723        }
724    }
725    // Verification results.
726    if live_cfg && i.verdict.valid && i.verdict.epoch == r.epoch {
727        let k = i.verdict.slot;
728        let e = s.e[k];
729        if !i.verdict.conflict {
730            if e.live && e.idx == i.verdict.idx && e.ver_sent && !e.sig_ok {
731                if i.verdict.ok {
732                    s.e[k].sig_ok = true;
733                } else if e.fetched {
734                    // The answering receiver lied: drop the copy, keep
735                    // waiting for another (the broadcaster is not blamed).
736                    s.e[k] = empty_entry::<P>();
737                    s.f_inj = false;
738                } else {
739                    fault_now = fault_now | b8(fault::BAD_SIG);
740                }
741            }
742        } else if s.conf_busy && s.conf_sent && s.conf_slot == i.verdict.slot && s.conf_ridx == i.verdict.idx {
743            s.conf_busy = false;
744            s.conf_sent = false;
745            let entry = e.live && e.idx == s.conf_idx;
746            if i.verdict.ok && !s.conf_newer {
747                // Two valid signatures for different digests at one index.
748                fault_now = fault_now | b8(fault::EQUIVOCATION);
749            } else if i.verdict.ok {
750                // The broadcaster really signed a later index for this slot:
751                // the register was recycled, and our index cannot be decided
752                // by the slow path.
753                if entry {
754                    s.e[k].stuck = true;
755                }
756            } else if entry {
757                // That receiver wrote garbage: it is Byzantine, not `b`.
758                s.e[k].checked = e.checked | (b16(1) << s.conf_from.resize::<16>());
759                s.e[k].rd_sent = e.rd_sent & !(b16(1) << s.conf_from.resize::<16>());
760            }
761        }
762    }
763    // Our register write is visible.
764    if live_cfg && i.wdone.valid && i.wdone.epoch == r.epoch {
765        let k = i.wdone.slot;
766        // Only the write in flight for this slot completes it.
767        if s.reg_busy[k] && s.busy_idx[k] == i.wdone.idx {
768            s.reg_busy[k] = false;
769        }
770        if s.e[k].live && s.e[k].idx == i.wdone.idx && s.e[k].wr_sent {
771            s.e[k].wr_done = true;
772        }
773    }
774    // Another receiver's register.
775    if live_cfg && i.rans.valid && i.rans.epoch == r.epoch {
776        let k = i.rans.slot;
777        let e = s.e[k];
778        let bit = b16(1) << i.rans.from.resize::<16>();
779        let ours = e.live && e.idx == i.rans.idx && (e.rd_sent & bit) != b16(0);
780        if ours {
781            if !i.rans.ok {
782                // Retry; a failed read proves nothing.
783                s.e[k].rd_sent = e.rd_sent & !bit;
784            } else if i.rans.empty
785                || i.rans.r_idx < e.idx
786                || (i.rans.r_idx & mask) != (e.idx & mask)
787                || (i.rans.r_digest[3] >> b64(32)) != b64(0)
788            {
789                // Never written for this slot, an older index, an index
790                // that does not belong to this slot at all (a receiver
791                // copying a genuine signature from another slot), or a
792                // non-canonical digest (bytes 28..32 are not signed, so a
793                // nonzero pad is a forgery): nothing here can conflict with
794                // our index.
795                s.e[k].checked = e.checked | bit;
796            } else if i.rans.r_idx == e.idx && e.dig_have && i.rans.r_digest == e.digest {
797                s.e[k].checked = e.checked | bit;
798            } else if !s.conf_busy {
799                // A different digest at our index, or a newer index: only
800                // the broadcaster's signature makes either claim count
801                // (else the claimant is lying, and is checked).
802                s.conf_busy = true;
803                s.conf_sent = false;
804                s.conf_slot = i.rans.slot;
805                s.conf_idx = i.rans.idx;
806                s.conf_ridx = i.rans.r_idx;
807                s.conf_newer = i.rans.r_idx > e.idx;
808                s.conf_from = i.rans.from;
809                s.conf_digest = i.rans.r_digest;
810                s.conf_sig = i.rans.r_sig;
811            } else {
812                // One conflict check at a time: read this one again later.
813                s.e[k].rd_sent = e.rd_sent & !bit;
814            }
815        }
816    }
817
818    // Data is always taken (a quarantined instance drains and drops).
819    let data_ready = live_cfg;
820
821    // ------------------------------------------------------------ requests
822    // Each port presents its holding register; a taken request clears it,
823    // and an empty one is loaded from the entries (marking the work sent).
824    // So a presented request never changes before it is taken, even if its
825    // entry leaves the tail (the response is then dropped by its tag).
826    // On a reset edge nothing is offered: a request taken in that cycle
827    // would be lost with the state the reset clears.
828    let mut o = RxOut::<P> {
829        data_ready: data_ready,
830        digest_req: r.h_dig,
831        echo: r.h_echo,
832        verify: r.h_ver,
833        write: r.h_wr,
834        read: r.h_rd,
835        deliver: r.h_dl,
836        fetch: r.h_fx,
837        fans: r.h_fa,
838        fault: r.fault,
839        configured: r.configured,
840        quiescent: quiescent,
841        init_refused: i.cfg.init && !quiescent,
842    };
843    if init {
844        o.digest_req.valid = false;
845        o.echo.valid = false;
846        o.verify.valid = false;
847        o.write.valid = false;
848        o.read.valid = false;
849        o.deliver.valid = false;
850        o.fetch.valid = false;
851        o.fans.valid = false;
852    }
853    if !init {
854        if r.h_dig.valid && i.digest_ready {
855            s.h_dig.valid = false;
856        }
857        if r.h_echo.valid && i.echo_ready {
858            s.h_echo.valid = false;
859        }
860        if r.h_ver.valid && i.verify_ready {
861            s.h_ver.valid = false;
862        }
863        if r.h_wr.valid && i.write_ready {
864            s.h_wr.valid = false;
865        }
866        if r.h_rd.valid && i.read_ready {
867            s.h_rd.valid = false;
868        }
869        if r.h_dl.valid && i.deliver_ready {
870            s.h_dl.valid = false;
871        }
872        if r.h_fx.valid && i.fetch_ready {
873            s.h_fx.valid = false;
874        }
875        if r.h_fa.valid && i.fans_ready {
876            s.h_fa.valid = false;
877        }
878        s.clock = r.clock + b32(1);
879    }
880    if r.configured && r.fault == b8(0) && !init {
881        let mut d_done = r.h_dig.valid;
882        let mut x_done = r.h_echo.valid;
883        let mut v_done = r.h_ver.valid;
884        let mut w_done = r.h_wr.valid;
885        let mut q_done = r.h_rd.valid;
886        for k in 0..T {
887            let e = r.e[k];
888            let slot = b4(k as u128);
889            if !d_done && e.live && !e.dig_sent {
890                d_done = true;
891                s.h_dig = DigestReq::<P> { valid: true, epoch: r.epoch, slot: slot, idx: e.idx, len: e.len, payload: e.payload };
892                s.e[k].dig_sent = true;
893            }
894            if !x_done && e.live && e.dig_have && !e.echo_sent {
895                x_done = true;
896                s.h_echo = EchoOut { valid: true, idx: e.idx, digest: e.digest };
897                s.e[k].echo_sent = true;
898            }
899            if !v_done && e.live && e.dig_have && e.sig_have && !e.ver_sent {
900                v_done = true;
901                s.h_ver = VerifyReq { valid: true, epoch: r.epoch, slot: slot, idx: e.idx, conflict: false, digest: e.digest, sig: e.sig };
902                s.e[k].ver_sent = true;
903            }
904            if !w_done && e.live && e.sig_ok && !e.wr_sent && !r.reg_busy[k] {
905                w_done = true;
906                s.h_wr = WriteReq { valid: true, epoch: r.epoch, slot: slot, idx: e.idx, digest: e.digest, sig: e.sig };
907                s.e[k].wr_sent = true;
908                s.reg_busy[k] = true;
909                s.busy_idx[k] = e.idx;
910            }
911            let want = r.others & !e.rd_sent & !e.checked;
912            if !q_done && e.live && e.wr_done && !e.stuck && !e.delivered && want != b16(0) {
913                q_done = true;
914                let mut from = b5(0);
915                let mut found = false;
916                for j in 0..16 {
917                    if !found && ((want >> b16(j as u128)) & b16(1)) != b16(0) {
918                        found = true;
919                        from = b5(j as u128);
920                    }
921                }
922                s.h_rd = ReadReq { valid: true, epoch: r.epoch, from: from, slot: slot, idx: e.idx };
923                // From `s`: a read answer this cycle may have cleared a bit.
924                s.e[k].rd_sent = s.e[k].rd_sent | (b16(1) << from.resize::<16>());
925            }
926        }
927        // The conflict check goes out when no own verification does.
928        if !v_done && r.conf_busy && !r.conf_sent {
929            s.h_ver = VerifyReq { valid: true, epoch: r.epoch, slot: r.conf_slot, idx: r.conf_ridx, conflict: true, digest: r.conf_digest, sig: r.conf_sig };
930            s.conf_sent = true;
931        }
932
933        // -------------------------------------------------------- delivery
934        if !r.h_dl.valid {
935            // The entry for `next_out`, if held.
936            let hk: b4 = (r.next_out & mask).resize();
937            let h = r.e[hk];
938            let held = h.live && h.idx == r.next_out;
939            let fast = h.dig_have && !h.echo_bad && h.echo_bits == r.others;
940            let slow = h.sig_ok && h.wr_done && !h.stuck && h.checked == r.others;
941            // Catch-up for `next_out` (R-26): possible while its tail slot
942            // has not been taken by a newer index.
943            let cu = i.catchup.enable && T < 16;
944            let evicted = h.live && h.idx != r.next_out;
945            let pending = r.f_busy && r.f_idx == r.next_out;
946            let expired = pending && r.clock - r.f_start >= i.catchup.wait;
947            let tried = r.f_tried && r.f_tried_idx == r.next_out;
948            if held && !h.delivered && (fast || slow) {
949                s.h_dl = Delivery::<P> { valid: true, gap: false, lo: h.idx, idx: h.idx, len: h.len, payload: h.payload, slow: !fast };
950                s.e[hk].delivered = true;
951                s.next_out = r.next_out + b32(1);
952                if fast {
953                    s.delivered_fast = r.delivered_fast + b32(1);
954                } else {
955                    s.delivered_slow = r.delivered_slow + b32(1);
956                }
957                if h.fetched {
958                    s.recovered = r.recovered + b32(1);
959                }
960                if pending {
961                    s.f_busy = false;
962                    s.f_inj = false;
963                }
964            } else if cu && held && pending && expired && h.fetched && !h.delivered {
965                // A copy is in, but did not complete its checks in time: drop
966                // it and report the gap below on the next cycle.
967                s.e[hk] = empty_entry::<P>();
968                s.f_busy = false;
969                s.f_inj = false;
970                s.f_tried = true;
971                s.f_tried_idx = r.next_out;
972            } else if cu && !held && !evicted && !tried && !pending && r.any_data && r.last_idx >= r.next_out {
973                // Fetch it instead of reporting it lost: ask on the next
974                // fetch-port slot.
975                s.f_busy = true;
976                s.f_idx = r.next_out;
977                s.f_start = r.clock;
978                s.f_last = r.clock - (i.catchup.wait >> b32(4)) - b32(1);
979                s.f_inj = false;
980                // So the first request goes to the first other receiver.
981                s.f_to = b5(31);
982            } else if cu && !held && !evicted && pending && !expired {
983                // Waiting for a verified copy.
984            } else if !held && r.any_data && r.last_idx >= r.next_out {
985                if pending {
986                    s.f_busy = false;
987                    s.f_inj = false;
988                    s.f_tried = true;
989                    s.f_tried_idx = r.next_out;
990                }
991                // Data past `next_out` arrived (the channel is in order), so
992                // `next_out` never will: report the lost run up to the next
993                // index we hold.
994                let mut hi = r.last_idx + b32(1);
995                for k in 0..T {
996                    let e = r.e[k];
997                    if e.live && e.idx > r.next_out && e.idx < hi {
998                        hi = e.idx;
999                    }
1000                }
1001                // With catch-up, only this one index is given up; the next
1002                // missing one gets its own fetch.
1003                if cu && !evicted && hi > r.next_out + b32(1) {
1004                    hi = r.next_out + b32(1);
1005                }
1006                s.h_dl = Delivery::<P> { valid: true, gap: true, lo: r.next_out, idx: hi, len: b5(0), payload: [b32(0); P], slow: false };
1007                s.next_out = hi;
1008                s.gaps = r.gaps + b32(1);
1009            }
1010        }
1011        // Our fetch request: one other receiver at a time, the next one on
1012        // every re-send (every sixteenth of the wait while no copy is in).
1013        let resend = (i.catchup.wait >> b32(4)) + b32(1);
1014        if !r.h_fx.valid && r.f_busy && !r.f_inj && r.clock - r.f_last >= resend {
1015            let mut first = b5(0);
1016            let mut have_first = false;
1017            let mut nxt = b5(0);
1018            let mut have_nxt = false;
1019            for k in 0..16 {
1020                let set = ((r.others >> b16(k as u128)) & b16(1)) != b16(0);
1021                if set && !have_first {
1022                    have_first = true;
1023                    first = b5(k as u128);
1024                }
1025                if set && !have_nxt && b5(k as u128) > r.f_to {
1026                    have_nxt = true;
1027                    nxt = b5(k as u128);
1028                }
1029            }
1030            let to = if have_nxt { nxt } else { first };
1031            if have_first {
1032                s.h_fx = FetchReqOut { valid: true, to: to, epoch: r.epoch, idx: r.f_idx };
1033                s.f_to = to;
1034                s.f_last = r.clock;
1035            }
1036        }
1037        // Responder: answer another receiver's fetch from our tail, with
1038        // the broadcaster's signature (one answer in flight; others are
1039        // dropped and the asker re-sends).
1040        if !r.h_fa.valid && i.freq.valid && i.freq.from < r.n && i.freq.from != r.me && i.freq.from != r.b {
1041            let fk: b4 = (i.freq.idx & mask).resize();
1042            let fe = r.e[fk];
1043            if fe.live && fe.idx == i.freq.idx && fe.sig_have && !fe.fetched {
1044                s.h_fa = FetchAnsOut::<P> { valid: true, to: i.freq.from, epoch: i.freq.epoch, idx: fe.idx, len: fe.len, payload: fe.payload, sig: fe.sig };
1045            }
1046        }
1047    }
1048
1049    // ------------------------------------------------------------ data
1050    if live_cfg && i.data.valid && data_ready {
1051        if quarantined {
1052            // Drain and drop.
1053        } else if i.data.idx >= b32(0xFFFF_0000) {
1054            fault_now = fault_now | b8(fault::WRAP);
1055        } else if s.any_data && i.data.idx <= s.last_idx {
1056            fault_now = fault_now | b8(fault::ORDER);
1057        } else if i.data.len > b5(P as u128) {
1058            fault_now = fault_now | b8(fault::LENGTH);
1059        } else {
1060            s.any_data = true;
1061            s.last_idx = i.data.idx;
1062            let idx33: b33 = i.data.idx.resize();
1063            // Everything at or below idx - T leaves the tail (an undelivered
1064            // one is reported as a gap by the delivery cursor).
1065            for k in 0..T {
1066                let e = s.e[k];
1067                let old = (e.live || e.early) && e.idx.resize::<33>() + tail <= idx33;
1068                if old {
1069                    s.e[k] = empty_entry::<P>();
1070                }
1071            }
1072            let k: b4 = (i.data.idx & mask).resize();
1073            let e = s.e[k];
1074            let mut n = empty_entry::<P>();
1075            // Early echoes for this very index carry over.
1076            if e.early && e.idx == i.data.idx {
1077                n.echo_bits = e.echo_bits;
1078                n.echo_bad = e.echo_bad;
1079                n.eref_have = e.eref_have;
1080                n.eref = e.eref;
1081            }
1082            n.live = true;
1083            n.idx = i.data.idx;
1084            n.len = i.data.len;
1085            n.payload = i.data.payload;
1086            s.e[k] = n;
1087        }
1088    }
1089
1090    // ------------------------------------------------------------ echoes
1091    if live_cfg && i.echo.valid && !quarantined {
1092        let bit = b16(1) << i.echo.from.resize::<16>();
1093        let from_ok = (s.others & bit) != b16(0);
1094        let k: b4 = (i.echo.idx & mask).resize();
1095        let e = s.e[k];
1096        if from_ok && (e.echo_bits & bit) == b16(0) {
1097            if e.live && e.idx == i.echo.idx {
1098                s.e[k].echo_bits = e.echo_bits | bit;
1099                if e.dig_have {
1100                    if i.echo.digest != e.digest {
1101                        s.e[k].echo_bad = true;
1102                    }
1103                } else if e.eref_have {
1104                    if i.echo.digest != e.eref {
1105                        s.e[k].echo_bad = true;
1106                    }
1107                } else {
1108                    s.e[k].eref_have = true;
1109                    s.e[k].eref = i.echo.digest;
1110                }
1111            } else if e.early && e.idx == i.echo.idx {
1112                s.e[k].echo_bits = e.echo_bits | bit;
1113                if i.echo.digest != e.eref {
1114                    s.e[k].echo_bad = true;
1115                }
1116            } else {
1117                // A future index may only take a free slot or replace an
1118                // older early record; it never evicts data (a Byzantine
1119                // receiver must not push messages out of the tail).
1120                let ahead = !s.any_data || i.echo.idx > s.last_idx;
1121                let free = !e.live && (!e.early || e.idx < i.echo.idx);
1122                if ahead && free {
1123                    let mut n = empty_entry::<P>();
1124                    n.early = true;
1125                    n.idx = i.echo.idx;
1126                    n.echo_bits = bit;
1127                    n.eref_have = true;
1128                    n.eref = i.echo.digest;
1129                    s.e[k] = n;
1130                }
1131            }
1132        }
1133    }
1134
1135    // ------------------------------------------------------------ signature
1136    if live_cfg && i.sig.valid && !quarantined {
1137        let k: b4 = (i.sig.idx & mask).resize();
1138        let e = s.e[k];
1139        if e.live && e.idx == i.sig.idx {
1140            if e.sig_have && e.fetched {
1141                // The broadcaster's own signature arriving after a fetched
1142                // copy: not a duplicate by the broadcaster (the slow-path
1143                // register checks catch a real equivocation).
1144            } else if e.sig_have {
1145                fault_now = fault_now | b8(fault::DUP_SIG);
1146            } else {
1147                s.e[k].sig_have = true;
1148                s.e[k].sig = i.sig.sig;
1149            }
1150        }
1151    }
1152
1153    // ------------------------------------------------------------ fetch answer
1154    // A copy of the index we are fetching enters its tail entry (taking
1155    // over early echoes for it), if the slot is still free and the index
1156    // still inside the tail; every check then applies as to received data.
1157    if live_cfg && !quarantined && i.fans.valid && i.fans.epoch == r.epoch && s.f_busy && !s.f_inj && i.fans.from == s.f_to && i.fans.idx == s.f_idx && i.fans.len <= b5(P as u128) {
1158        let fk: b4 = (i.fans.idx & mask).resize();
1159        let fe = s.e[fk];
1160        let in_tail = i.fans.idx.resize::<33>() + tail > s.last_idx.resize::<33>();
1161        let free = !fe.live && (!fe.early || fe.idx == i.fans.idx);
1162        if in_tail && free {
1163            let mut n = empty_entry::<P>();
1164            if fe.early && fe.idx == i.fans.idx {
1165                n.echo_bits = fe.echo_bits;
1166                n.echo_bad = fe.echo_bad;
1167                n.eref_have = fe.eref_have;
1168                n.eref = fe.eref;
1169            }
1170            n.live = true;
1171            n.idx = i.fans.idx;
1172            n.len = i.fans.len;
1173            n.payload = i.fans.payload;
1174            n.sig_have = true;
1175            n.sig = i.fans.sig;
1176            n.fetched = true;
1177            s.e[fk] = n;
1178            s.f_inj = true;
1179        }
1180    }
1181
1182    // A fault quarantines this broadcaster from this edge on.
1183    s.fault = s.fault | fault_now;
1184    (o, s)
1185}