Skip to main content

rhdl_dsig_scalable_core/
slice.rs

1//! One complete slice: factory, subtree builder, signer and key cache.
2//!
3//! # Why slices exist
4//!
5//! Almost all of DSig's work is hashing, and almost all of the hashing is
6//! making one-time keys (68 W-OTS+ chains per key). The way to sign faster is
7//! to make keys faster, and the way to do *that* within a LUT budget is to
8//! build one compact unit that makes and spends keys on its own, then copy it.
9//! That unit is a **slice**. Each slice makes one key per 68 clocks, so the
10//! engine's signing rate grows with the number of slices
11//! ([`SLICES`](crate::SLICES)).
12//!
13//! This is the unit the [sustained engine](crate::sustained) repeats eight
14//! times. Everything a signature needs is made and spent inside it, except the
15//! batch certificate, which comes from the shared [`cert_unit`](crate::cert_unit).
16//!
17//! ```text
18//!                 ┌──────────────────────── Slice ───────────────────────────┐
19//!   config ──────▶│ SliceFactory ──cache_write──▶ 4 × UltraRAM cache banks    │
20//!                 │   │ key_done      ▲ Merkle/message slots      │ nodes     │
21//!                 │   ▼               │                           ▼           │
22//!                 │ SubtreeBuilder ───┘          SliceSigner ◀── proof port   │
23//!                 │   │ subtree root               │   ▲ certificate        │
24//!                 └───┼────────────────────────────┼───┼─────────────────────┘
25//!                     ▼                            ▼   │
26//!               cert_unit (shared)            frame beats ─▶ HBM writer
27//! ```
28//!
29//! | Part | Module | Role in the slice |
30//! |---|---|---|
31//! | `factory` | [`crate::slice_factory`] | makes one key per 68 clocks; also hashes Merkle parents and messages for the others |
32//! | `bank0..bank3` | [`crate::cache_bank`] | hold chain positions 0..3 of 512 keys until they are spent |
33//! | `subtree` | [`crate::slice_merkle`] | builds this slice's subtree of every batch, keeps the nodes for proofs |
34//! | `signer` | [`crate::slice_signer`] | burns a key per message and writes a 1,642-byte signature frame |
35//!
36//! This file contains almost no logic. It is wiring: it connects the parts
37//! and computes one signal of its own, `space` (cache admission, below).
38//!
39//! The same slice drawn as dataflow, with its clock budget and its routed LUT
40//! cost. The three chain lanes are what make it fast and what make it big:
41//!
42#![doc = concat!("<div style=\"overflow-x:auto\">", include_str!("../docs/images/dsig-slice.svg"), "</div>")]
43//!
44//! # The life of one key
45//!
46//! ```text
47//!   factory starts key n ───▶ 68 chains written to cache slot n mod 512
48//!        (needs `space`)          │ key_done (pk_hash, nonces, batch, j)
49//!                                 ├──▶ signer: metadata RAM
50//!                                 └──▶ subtree: leaf j → … → subtree root ─▶ cert unit
51//!                                                                  │
52//!   cert unit ─▶ certificate for the batch ─▶ signer FIFO ◀────────┘ (much later)
53//!   message arrives ─▶ signer BURNS key n (consumed += 1) ─▶ reads nodes ─▶ frame
54//! ```
55//!
56//! A key is spent ("burned") the moment the signer accepts a message for it,
57//! before any output is produced. If a frame is lost afterwards, the key is
58//! still gone. The project rule is that a one-time key is never reused after
59//! admission, timeout, reset or an uncertain launch; burning first is what
60//! makes that true here, because the counter only moves forward.
61//!
62//! # Cache admission
63//!
64//! The factory assigns cache slots in a ring of
65//! [`CACHE_SLOTS`](crate::slice_factory::CACHE_SLOTS) = 512 keys (32 batches).
66//! Each bank stores them densely, 68 rows per key
67//! ([`cache_address_kernel`](crate::slice_factory::cache_address_kernel)).
68//! The factory may start key `n` only while `started − consumed <`
69//! [`ADMIT_LIMIT`] `= 512 − KEYS_PER_SLICE`, so at least one whole batch of
70//! slack remains:
71//!
72//! * slot `n mod 512` is free, because key `n − 512` was burned long ago;
73//! * the subtree ring entry of batch `b` is free, because **every** key of the
74//!   batch that last used it has been burned. Leaf `j` and its parents overwrite nodes
75//!   that are *siblings* of other keys in the old batch, so freeing only key
76//!   `n − 512` would not be enough.
77//!
78//! ```text
79//!   key numbers in one slice, 8-slice build (512 slots, 16 keys per batch)
80//!
81//!   … burned │ in flight: made, not burned │ may still start │ kept free │
82//!   ─────────┼─────────────────────────────┼─────────────────┼───────────┼──▶ key n
83//!        consumed                       started     consumed + 496  consumed + 512
84//!
85//!   start allowed while started − consumed < ADMIT_LIMIT = 512 − 16 = 496
86//! ```
87//!
88//! When the signer falls behind (HBM stalls, or certificates still in flight),
89//! the factory simply skips key starts: every compression of a skipped key
90//! becomes an idle slot.
91
92use crate::ELEMENT_BYTES;
93use crate::cache_bank::CacheBank;
94use crate::slice_factory::{
95    CACHE_ADDRESS_BITS, CACHE_ROWS, CACHE_SLOTS, FactoryConfig, SliceFactory, SliceFactoryInput,
96    TWO_BLOCK_MESSAGE, cache_address_kernel,
97};
98use crate::slice_merkle::NODE_RING_BATCHES;
99use crate::slice_merkle::{SubtreeBuilder, SubtreeInput, SubtreeRoot};
100use crate::slice_signer::{Certificate, FrameBeat, Message, SignerInput, SliceSigner};
101use crate::cert_unit::ROOT_QUEUE;
102use crate::{KEYS_PER_SLICE, SLICES, SUBTREE_LEVELS};
103use rhdl::prelude::*;
104use rhdl_fpga::core::ram::synchronous::{In as RamIn, Write as RamWrite};
105
106// The slices together cover exactly one batch of 128 keys.
107const _: () = assert!(SLICES * KEYS_PER_SLICE == crate::BATCH_KEYS);
108// The subtree ring and the cache ring advance together, one batch per KEYS_PER_SLICE keys.
109const _: () = assert!(NODE_RING_BATCHES * KEYS_PER_SLICE == CACHE_SLOTS);
110// Every cache row must be reachable with the bank's 16-bit address.
111const _: () = assert!(CACHE_ROWS <= 1 << CACHE_ADDRESS_BITS);
112
113/// Keys that may be started but not yet burned: the cache less one batch.
114///
115/// 512 − 16 = 496 in the 8-slice build. The missing batch is the safety
116/// margin that guarantees a reused slot or subtree ring entry belongs only to
117/// keys that are already burned (see the module docs).
118pub const ADMIT_LIMIT: usize = CACHE_SLOTS - KEYS_PER_SLICE;
119
120/// Slice inputs.
121#[derive(Clone, Copy, Debug, PartialEq, Digital)]
122pub struct SliceInput {
123    /// Key-factory configuration (includes this slice's number).
124    pub config: FactoryConfig,
125    /// Allow key starts.
126    pub run: bool,
127    /// Next message.
128    pub message: Message,
129    /// Certificate offered by the shared unit.
130    pub certificate: Certificate,
131    /// The HBM writer accepts the presented beat.
132    pub beat_ready: bool,
133    /// Batches whose roots the cert unit has taken
134    /// ([`CertOutput::roots_popped`](crate::cert_unit::CertOutput::roots_popped)):
135    /// root credit for the factory.
136    pub roots_popped: b32,
137}
138
139/// Slice outputs.
140#[derive(Clone, Copy, Debug, PartialEq, Digital)]
141pub struct SliceOutput {
142    /// Subtree root pulse for the shared unit.
143    pub root: SubtreeRoot,
144    /// The offered certificate was accepted.
145    pub certificate_taken: bool,
146    /// The message was consumed.
147    pub message_taken: bool,
148    /// Frame beat for the HBM writer.
149    pub beat: FrameBeat,
150    /// Real keys started.
151    pub keys_started: b32,
152    /// Keys burned.
153    pub keys_consumed: b32,
154    /// Frames handed to the writer.
155    pub frames_out: b32,
156    /// Subtree job queue overflowed (design error).
157    pub fault: bool,
158}
159
160/// One slice of the sustained engine.
161///
162/// `TABLE_MASK` is the chain-lane area knob, passed through to the factory:
163/// it chooses which AES rounds of the Haraka lane use table ROMs (see
164/// [`crate::chain_step`]).
165#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
166pub struct Slice<const TABLE_MASK: usize> {
167    /// Makes keys; also runs the subtree's and signer's BLAKE3 requests.
168    factory: SliceFactory<TABLE_MASK>,
169    /// Builds this slice's Merkle subtree of each batch.
170    subtree: SubtreeBuilder,
171    /// Spends keys: one signature frame per message.
172    signer: SliceSigner,
173    /// Key cache, chain position 0 of every cached key.
174    bank0: CacheBank,
175    /// Key cache, chain position 1.
176    bank1: CacheBank,
177    /// Key cache, chain position 2.
178    bank2: CacheBank,
179    /// Key cache, chain position 3.
180    bank3: CacheBank,
181}
182
183impl<const TABLE_MASK: usize> Default for Slice<TABLE_MASK> {
184    /// Every part in its own reset state.
185    fn default() -> Self {
186        Self {
187            factory: SliceFactory::default(),
188            subtree: SubtreeBuilder::default(),
189            signer: SliceSigner::default(),
190            bank0: CacheBank::default(),
191            bank1: CacheBank::default(),
192            bank2: CacheBank::default(),
193            bank3: CacheBank::default(),
194        }
195    }
196}
197
198/// Input: config, messages, certificates and writer back-pressure. Output:
199/// subtree roots, frame beats and counters.
200impl<const TABLE_MASK: usize> SynchronousIO for Slice<TABLE_MASK> {
201    type I = SliceInput;
202    type O = SliceOutput;
203    type Kernel = slice_kernel<TABLE_MASK>;
204}
205
206/// One cache bank's port: write position `p` of a chain, read the signer's address.
207///
208/// * `write_valid`, `write_address`, `value`: the factory's write of one
209///   18-byte node (the same address for all four banks, a different `value`).
210/// * `read_address`: the signer's read row (the same for all four banks).
211/// * Returns the bank's RAM input. Combinational; no registers.
212#[kernel]
213pub fn bank_port_kernel(
214    write_valid: bool,
215    write_address: b16,
216    value: [b8; ELEMENT_BYTES],
217    read_address: b16,
218) -> RamIn<[b8; ELEMENT_BYTES], 16> {
219    RamIn::<[b8; ELEMENT_BYTES], 16> {
220        read_addr: read_address,
221        write: RamWrite::<[b8; ELEMENT_BYTES], 16> {
222            addr: write_address,
223            value,
224            enable: write_valid,
225        },
226    }
227}
228
229/// `log2 KEYS_PER_SLICE` as a shift amount: `keys_started >> SUBTREE_SHIFT`
230/// is the batch of the next key (RHDL shifts take `Bits`, not `usize`).
231pub const SUBTREE_SHIFT: b32 = b32(SUBTREE_LEVELS as u128);
232
233/// Pure wiring between the slice's parts.
234///
235/// In an RHDL kernel, `q.factory`, `q.signer` and so on are the parts'
236/// current outputs, and the fields of `d` are their next inputs. This kernel
237/// only connects outputs to inputs. The only signals it computes are `space`
238/// (the admission test) and the shared cache write address.
239#[kernel]
240pub fn slice_kernel<const TABLE_MASK: usize>(
241    _clock_reset: ClockReset,
242    input: SliceInput,
243    q: SliceQ<TABLE_MASK>,
244) -> (SliceOutput, SliceD<TABLE_MASK>) {
245    // Current outputs of the factory and the signer.
246    let factory = q.factory;
247    let signer = q.signer;
248    // Keys made (or being made) but not yet burned. 32-bit counters; the
249    // subtraction stays correct across wrap-around.
250    let in_flight = factory.keys_started - signer.consumed;
251    // Batch (relative to the run) of the next key this slice would start:
252    // each slice makes KEYS_PER_SLICE keys of every batch, in order.
253    let next_batch: b32 = factory.keys_started >> SUBTREE_SHIFT;
254    // Root credit: the cert unit's root queue holds ROOT_QUEUE roots per
255    // slice, so only batches below `roots_popped + ROOT_QUEUE` may be made.
256    // Wrapping subtraction keeps this right across counter wrap-around.
257    let root_room = (next_batch - input.roots_popped) < b32(ROOT_QUEUE as u128);
258    // The factory may start another key only below the admission limit and
259    // with root credit.
260    let space = in_flight < b32(ADMIT_LIMIT as u128) && root_room;
261    // The factory's cache write this cycle: four positions of one chain.
262    let write = factory.cache_write;
263    // Row `slot × 68 + chain`, shared by all four banks.
264    let write_address = cache_address_kernel(write.slot, write.chain);
265    let d = SliceD::<TABLE_MASK> {
266        // Factory: config, the admission flag and the two borrowed hash
267        // slots (Merkle parents from the subtree, message hashes from the
268        // signer).
269        factory: SliceFactoryInput {
270            config: input.config,
271            run: input.run,
272            space,
273            merkle: q.subtree.merkle,
274            message: signer.request,
275        },
276        // Subtree: leaves and parent results from the factory, proof reads
277        // from the signer.
278        subtree: SubtreeInput {
279            key_done: factory.key_done,
280            merkle_taken: factory.merkle_taken,
281            slot_result: factory.slot_result,
282            proof_address: signer.proof_address,
283        },
284        // Signer: messages and certificates from outside, key metadata and
285        // message hashes from the factory, nodes from the banks, siblings
286        // from the subtree.
287        signer: SignerInput {
288            slice: input.config.slice,
289            message: input.message,
290            key_done: factory.key_done,
291            certificate: input.certificate,
292            message_taken: factory.message_taken,
293            slot_result: factory.slot_result,
294            cache_data: [q.bank0, q.bank1, q.bank2, q.bank3],
295            proof_data: q.subtree.proof_data,
296            beat_ready: input.beat_ready,
297        },
298        // Bank p stores position p of the chain the factory just finished.
299        bank0: bank_port_kernel(
300            write.valid,
301            write_address,
302            write.nodes[0],
303            signer.cache_address,
304        ),
305        bank1: bank_port_kernel(
306            write.valid,
307            write_address,
308            write.nodes[1],
309            signer.cache_address,
310        ),
311        bank2: bank_port_kernel(
312            write.valid,
313            write_address,
314            write.nodes[2],
315            signer.cache_address,
316        ),
317        bank3: bank_port_kernel(
318            write.valid,
319            write_address,
320            write.nodes[3],
321            signer.cache_address,
322        ),
323    };
324    (
325        SliceOutput {
326            root: q.subtree.root,
327            certificate_taken: signer.certificate_taken,
328            // Profile 2: the factory also takes tail blocks, which consume no
329            // message; the signer's flag is true only when a first block
330            // burns a key. Profile 1 has no tails, so the two agree.
331            message_taken: if TWO_BLOCK_MESSAGE { signer.message_taken } else { factory.message_taken },
332            beat: signer.beat,
333            keys_started: factory.keys_started,
334            keys_consumed: signer.consumed,
335            frames_out: signer.frames_out,
336            fault: q.subtree.overflow,
337        },
338        d,
339    )
340}