Skip to main content

rhdl_ubft/ctb/
mod.rs

1//! Consistent tail broadcast (CTB): recovery layer 3, workstream W3
2//! (docs/rhdl_ubft-ctb.md).
3//!
4//! Two encodings are fixed here, both as kernels so hardware and tests use
5//! the same bytes:
6//! - [`digest_block`]: the 128-byte SHA-512 input for one message, `"RUB2-CTB"
7//!   ‖ session ‖ b ‖ idx ‖ len ‖ payload`, zero beyond `len`. The digest is
8//!   its first 28 bytes.
9//! - [`ctb_context`]: the 48 signed bytes (sign layout v2, role 2):
10//!   `"RUB2" ‖ role 2 ‖ b ‖ 0 0 ‖ session ‖ idx ‖ digest[0..28]`. The
11//!   broadcaster, index and session are named explicitly, so a signature
12//!   can never be presented as the broadcaster's statement about another
13//!   index, broadcaster or run (U33), and a Byzantine receiver cannot frame
14//!   an honest broadcaster with a signature from elsewhere.
15use rhdl::prelude::*;
16
17pub mod broadcaster;
18pub mod receiver;
19pub mod wire;
20
21/// Most payload words a CTB message carries (64 bytes: the body of one
22/// point-to-point slot, which the fetch engine reads in one request; the
23/// digest input stays one 128-byte SHA-512 message).
24pub const MAX_PAYLOAD: usize = 16;
25
26/// Role byte of a CTB data signature in sign layout v2 (role 1 is the
27/// certify share).
28pub const ROLE_CTB: u128 = 2;
29
30/// `"RUB2-CTB"` as a little-endian word.
31pub const CTB_DOMAIN: u128 = 0x4254_432D_3242_5552;
32
33/// Byte-reverse a 64-bit word (little-endian bytes to a SHA-512 word).
34#[kernel]
35pub fn bswap64(x: b64) -> b64 {
36    let mut y = b64(0);
37    for j in 0..8 {
38        y = y | (((x >> b64((8 * j) as u128)) & b64(0xff)) << b64((56 - 8 * j) as u128));
39    }
40    y
41}
42
43/// The 128-byte digest input as 16 big-endian SHA-512 words. Payload words
44/// at or past `len` are zeroed, so the encoding is canonical.
45#[kernel]
46pub fn digest_block<const P: usize>(session: b64, b: b5, idx: b32, len: b5, payload: [b32; P]) -> [b64; 16] {
47    let mut le = [b64(0); 16];
48    le[0] = b64(CTB_DOMAIN);
49    le[1] = session;
50    le[2] = b.resize::<64>() | (idx.resize::<64>() << b64(32));
51    le[3] = len.resize::<64>();
52    for k in 0..P {
53        let word = if b5(k as u128) < len { payload[k] } else { b32(0) };
54        let w = (k >> 1) + 4;
55        let sh = (k & 1) * 32;
56        le[w] = le[w] | (word.resize::<64>() << b64(sh as u128));
57    }
58    let mut be = [b64(0); 16];
59    for k in 0..16 {
60        be[k] = bswap64(le[k]);
61    }
62    be
63}
64
65/// The digest from a finished SHA-512 state `H0..H7` (big-endian words):
66/// bytes 0..28 as little-endian words, bytes 28..32 zero.
67#[kernel]
68pub fn digest_of(state: [b64; 8]) -> [b64; 4] {
69    let mut d = [b64(0); 4];
70    for k in 0..4 {
71        d[k] = bswap64(state[k]);
72    }
73    d[3] = d[3] & b64(0xFFFF_FFFF);
74    d
75}
76
77/// The 48 signed bytes of a CTB data signature, as six little-endian words
78/// (the services sign them zero-extended to 64 bytes).
79#[kernel]
80pub fn ctb_context(session: b64, b: b5, idx: b32, digest: [b64; 4]) -> [b64; 6] {
81    let mut c = [b64(0); 6];
82    c[0] = b64(0x3242_5552) | (b64(ROLE_CTB) << b64(32)) | (b.resize::<64>() << b64(40));
83    c[1] = session;
84    c[2] = idx.resize::<64>() | (digest[0] << b64(32));
85    c[3] = (digest[0] >> b64(32)) | (digest[1] << b64(32));
86    c[4] = (digest[1] >> b64(32)) | (digest[2] << b64(32));
87    c[5] = (digest[2] >> b64(32)) | (digest[3] << b64(32));
88    c
89}