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}