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}