Skip to main content

rhdl_ubft/ctb/
wire.rs

1//! CTB point-to-point slots: what a sender writes into a receiver's memory
2//! and how the receiver reads it back (U21, U22).
3//!
4//! **Problem.** uBFT's tail-p2p receiver copies `size` bytes, a field the
5//! remote writes, into a local buffer without checking it (U21). Its sender
6//! checks a payload against the slot size including the header, so a payload
7//! a header's worth too long overruns into the next slot (U22).
8//!
9//! **Idea.** A slot is one fixed record, written as a tagged subslot
10//! ([`crate::register`]): every word carries the write's tag, so a read
11//! that races a write is seen as torn, never as a value. The record is
12//!
13//! ```text
14//! word 0: kind (1 data, 2 echo, 3 signature)
15//! word 1: length in body words (32 bits, as the remote wrote it)
16//! word 2: index
17//! words 3 .. 3 + BODY: the body
18//! ```
19//!
20//! - The sender checks the body length against the body capacity for the
21//!   kind (data: `P`; echo: exactly 7, the 28-byte digest; signature:
22//!   exactly 16), never against the slot size, and writes nothing when it
23//!   is refused.
24//! - The receiver parses the fixed record. The length is compared, as 32
25//!   bits, with the capacity before any body word is used; body words past
26//!   the length are zeroed. A refused record is a typed result for the
27//!   source, not a copy.
28//!
29//! Slot `k` of a ring of `T` holds sequence numbers `k, k + T, ...`; its
30//! incarnation advances by one per write (as the tags require).
31use rhdl::prelude::*;
32
33use crate::register::{pack_subslot, unpack_subslot};
34
35/// Body words.
36pub const BODY: usize = 16;
37/// Record words.
38pub const REC: usize = 3 + BODY;
39/// Slot words (a tagged subslot of [`REC`] words).
40pub const SLOT: usize = crate::register::subslot_words(REC);
41
42/// Record kinds.
43pub mod kind {
44    /// Broadcaster data.
45    pub const DATA: u128 = 1;
46    /// An echo (a 28-byte digest).
47    pub const ECHO: u128 = 2;
48    /// The broadcaster's signature (64 bytes).
49    pub const SIG: u128 = 3;
50}
51
52/// Words of an echo body and a signature body.
53pub const ECHO_WORDS: u128 = 7;
54/// Words of a signature body.
55pub const SIG_WORDS: u128 = 16;
56
57/// Parse results.
58pub mod status {
59    /// Nothing new in the slot (incarnation below the one expected).
60    pub const EMPTY: u128 = 0;
61    /// A whole record of the expected incarnation, accepted.
62    pub const OK: u128 = 1;
63    /// The read raced a write (tags differ): read again.
64    pub const TORN: u128 = 2;
65    /// A newer incarnation: the sender lapped us; records were lost.
66    pub const AHEAD: u128 = 3;
67    /// A whole record with a bad kind or length: the sender is Byzantine.
68    pub const BAD: u128 = 4;
69}
70
71/// The body capacity for a kind (0 for an unknown kind).
72#[kernel]
73pub fn capacity<const P: usize>(k: b32) -> b32 {
74    if k == b32(kind::DATA) {
75        b32(P as u128)
76    } else if k == b32(kind::ECHO) {
77        b32(ECHO_WORDS)
78    } else if k == b32(kind::SIG) {
79        b32(SIG_WORDS)
80    } else {
81        b32(0)
82    }
83}
84
85/// Whether a body length is exact for its kind: data up to `P`, echoes and
86/// signatures exactly their size.
87#[kernel]
88pub fn length_ok<const P: usize>(k: b32, len: b32) -> bool {
89    let cap = capacity::<P>(k);
90    if k == b32(kind::DATA) {
91        len <= cap
92    } else {
93        cap != b32(0) && len == cap
94    }
95}
96
97/// The sender's side: the slot words for a record, or `false` (and nothing
98/// to write) if the length does not fit the kind.
99#[kernel]
100pub fn encode<const P: usize>(incarnation: b32, k: b32, len: b32, idx: b32, body: [b32; BODY]) -> (bool, [b32; SLOT]) {
101    let ok = length_ok::<P>(k, len) && incarnation != b32(0);
102    let mut rec = [b32(0); REC];
103    rec[0] = k;
104    rec[1] = len;
105    rec[2] = idx;
106    for j in 0..BODY {
107        // Canonical: nothing past the length is sent.
108        rec[3 + j] = if b32(j as u128) < len { body[j] } else { b32(0) };
109    }
110    let slot = pack_subslot::<REC, SLOT>(incarnation, rec);
111    (ok, slot)
112}
113
114/// A parsed slot.
115#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
116pub struct Parsed {
117    /// See [`status`].
118    pub status: b3,
119    pub kind: b32,
120    pub idx: b32,
121    /// Body words used (only meaningful when `status` is OK; at most 16).
122    pub len: b5,
123    /// The body, zero past `len` (all zero unless OK).
124    pub body: [b32; BODY],
125}
126
127/// The receiver's side: classify the slot read against the incarnation
128/// expected there. No body word is used unless the record is whole and its
129/// kind and length are valid.
130#[kernel]
131pub fn parse<const P: usize>(slot: [b32; SLOT], expect: b32) -> Parsed {
132    let inc = slot[0];
133    let (whole, rec) = unpack_subslot::<REC, SLOT>(slot);
134    let mut p = Parsed { status: b3(status::EMPTY), kind: b32(0), idx: b32(0), len: b5(0), body: [b32(0); BODY] };
135    if inc < expect || inc == b32(0) {
136        p.status = b3(status::EMPTY);
137    } else if !whole {
138        p.status = b3(status::TORN);
139    } else if inc > expect {
140        p.status = b3(status::AHEAD);
141    } else if !length_ok::<P>(rec[0], rec[1]) {
142        p.status = b3(status::BAD);
143    } else {
144        p.status = b3(status::OK);
145        p.kind = rec[0];
146        p.idx = rec[2];
147        p.len = rec[1].resize();
148        for j in 0..BODY {
149            p.body[j] = if b32(j as u128) < rec[1] { rec[3 + j] } else { b32(0) };
150        }
151    }
152    p
153}