rhdl_dsig_scalable_core/slice_merkle.rs
1//! A slice's share of the batch Merkle tree: one subtree per batch.
2//!
3//! # The problem
4//!
5//! DSig pays for one expensive Ed25519 signature per *batch* of 128 one-time
6//! keys, not per key. To make that one signature cover 128 keys, the keys'
7//! public commitments (`pk_hash`, one 32-byte hash per key) become the leaves
8//! of a Merkle tree: a binary tree where every parent is the hash of its two
9//! children. Ed25519 signs only the root. A signature then proves that its key
10//! belongs to the batch by carrying the 7 *siblings* on the path from its leaf
11//! to the root (the *inclusion proof*); a verifier rehashes up the path and
12//! compares with the signed root.
13//!
14//! The 128 keys of a batch are not made in one place. Each
15//! [slice](crate::slice) makes its own share, so each slice can build the
16//! part of the tree that sits above its own keys, without talking to any
17//! other slice. Only the last few levels need everyone's results; they are
18//! built by the shared [cert unit](crate::cert_unit).
19//!
20//! ```text
21//! one batch, 8-slice build (128 keys, 7 levels)
22//!
23//! level 7 batch root ──▶ Ed25519 (cert unit)
24//! levels 5..7 ┌──── Merkle top: 4 + 2 + 1 hashes (cert unit) ────┐
25//! level 4 r0 r1 r2 r3 r4 r5 r6 r7 subtree roots
26//! levels 1..4 [s0] [s1] [s2] [s3] [s4] [s5] [s6] [s7] 15 parents each
27//! level 0 (leaves) 0..15 16..31 32..47 ... 112..127 keys
28//! └─ this module, one copy per slice ─┘
29//! ```
30//!
31//! # What this module does
32//!
33//! A batch of 128 keys has a 7-level Merkle tree. Slice `s` owns keys
34//! `K·s .. K·s + K` with `K =` [`KEYS_PER_SLICE`] (16, 32 or 64 for 8, 4 or 2
35//! slices), which form exactly one subtree of height [`SUBTREE_LEVELS`]. For
36//! the 8-slice build:
37//!
38//! ```text
39//! level 4 root ──▶ shared Merkle top (3 more levels)
40//! level 3 ┌─────────┴─────────┐
41//! level 2 ┌────┴────┐ ┌────┴────┐
42//! level 1 ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐
43//! level 0 (leaf) j=0 1 2 3 … 15 pk_hash of each key
44//! ```
45//!
46//! With 4 slices the subtree is one level taller and the certificate unit's
47//! top one level shorter; the inclusion proof is always 7 siblings.
48//!
49//! [`SubtreeBuilder`] receives each finished key's `pk_hash` from the
50//! [`slice_factory`](crate::slice_factory), stores it, and as soon as both
51//! children of a node exist it queues that parent. Queued parents go into the
52//! factory's Merkle slot one at a time (one per 68-cycle period, enough for
53//! `K − 1` parents per `K` keys). A finished top-level node leaves as the
54//! slice's **subtree root**.
55//!
56//! # Why there is no BLAKE3 core here
57//!
58//! A parent is one BLAKE3 compression of `left ‖ right`. The factory's
59//! [timetable](crate::slice_schedule) already reserves one free "Merkle slot"
60//! on its BLAKE3 lane every 68-cycle key period. A subtree of `K` keys needs
61//! only `K − 1` parents per `K` keys, so that one slot is enough, and the
62//! slice saves a whole hash core. This module only decides *what* to hash
63//! and stores the results.
64//!
65//! # When a parent is ready
66//!
67//! Keys finish in order `j = 0, 1, 2, …`. When an odd leaf `j` arrives, its
68//! even sibling `j − 1` is already stored, so parent `j / 2` at level 1 can be
69//! queued at once. The same rule repeats one level up: a parent result with an
70//! odd index means its even sibling finished earlier (jobs are served in
71//! order), so the grandparent is queued. When the level-[`SUBTREE_LEVELS`]
72//! result arrives, it is the subtree root.
73//!
74//! ```text
75//! leaf j=0 ─ store
76//! leaf j=1 ─ store, queue (level 1, index 0)
77//! leaf j=2 ─ store
78//! leaf j=3 ─ store, queue (level 1, index 1)
79//! result (1,0) ─ store
80//! result (1,1) ─ store, queue (level 2, index 0) … and so on up to the root
81//! ```
82//!
83//! For each queued job the builder reads the two children from its node RAM
84//! (one cycle each), then offers `left ‖ right` to the factory until the
85//! factory takes it. The 16-bit request tag carries `(ring, level, index)` so
86//! the result can be stored where it belongs when it comes back.
87//!
88//! # Storage
89//!
90//! Every node is kept, because a signature's inclusion proof needs one sibling
91//! per level. The signer reads siblings through the separate proof port. Nodes
92//! live in a ring of [`NODE_RING_BATCHES`] batches, the same number of batches
93//! the key cache holds. The slice's admission limit keeps one whole batch of
94//! slack, so a batch's nodes are never overwritten while one of its keys can
95//! still sign (see [`slice`](crate::slice)).
96//!
97//! Node address = `ring · 2K + NODE_BASE[level] + index`, with
98//! `NODE_BASE[level] = 2K − 2K / 2^level` (for 16 keys: 0, 16, 24, 28, 30).
99//! All `2K − 1` nodes of a batch fit in its `2K` slots, and ring entries ×
100//! slots is always 1,024, a 10-bit address.
101//!
102//! ```text
103//! one ring entry, 16 keys (32 slots of 32 bytes)
104//! slot: 0 ............ 15 | 16 ...... 23 | 24 .. 27 | 28 29 | 30 | 31
105//! level 0 (leaves) | level 1 | level 2 | lvl 3 | root | spare
106//! ```
107//!
108//! The node RAM exists twice ([`SubtreeBuilder`]'s `job_nodes` and
109//! `proof_nodes`) with identical writes. A synchronous RAM has one read port;
110//! the builder and the signer read at unrelated times, so each gets its own
111//! copy instead of arbitrating for one port.
112
113use crate::slice_factory::{CACHE_SLOTS, KeyDone, SlotRequest, SlotResult};
114use crate::{KEY_BITS, KEY_ONE, KEY_ZERO, KEYS_PER_SLICE, KeyIndex, NODE_LEVELS, NODE_SLOT_BITS, SUBTREE_LEVELS};
115use rhdl::prelude::*;
116use rhdl_fpga::core::{
117 dff::DFF,
118 ram::synchronous::{In as RamIn, SyncBRAM, Write as RamWrite},
119};
120
121/// Batches whose subtree nodes are kept (ring size): one per cache batch.
122///
123/// 512 cache slots ÷ 16 keys = 32 entries in the 8-slice build. Matching the
124/// key cache means a batch's proof nodes live exactly as long as its keys.
125pub const NODE_RING_BATCHES: usize = CACHE_SLOTS / KEYS_PER_SLICE;
126/// Node slots per ring entry: `2·KEYS_PER_SLICE` (`2K − 1` nodes, one spare).
127pub const NODES_PER_ENTRY: usize = 2 * KEYS_PER_SLICE;
128/// Mask turning a batch number into its ring entry.
129///
130/// The ring size is a power of two, so `batch mod NODE_RING_BATCHES` is just
131/// the low bits of the batch number (RHDL kernels avoid `%`).
132pub const RING_MASK: b5 = b5((NODE_RING_BATCHES - 1) as u128);
133/// Level of the subtree root (`log2 KEYS_PER_SLICE`).
134pub const ROOT_LEVEL: b3 = b3(SUBTREE_LEVELS as u128);
135
136/// First node slot of each level inside a ring entry:
137/// `2K − 2K / 2^level`. For 16 keys: 0, 16, 24, 28, 30.
138///
139/// [`NODE_LEVELS`] entries (seven in every batch-engine build) so that any
140/// level up to [`KEY_BITS`] has a defined base; levels above
141/// [`ROOT_LEVEL`] are never used.
142pub const NODE_BASE: [NodeSlot; NODE_LEVELS] = node_bases();
143/// A node slot within one ring entry ([`NODE_SLOT_BITS`] wide).
144pub type NodeSlot = Bits<NODE_SLOT_BITS>;
145
146/// Compute [`NODE_BASE`] at compile time: level `l` starts after all the
147/// nodes of levels `0..l` (`2K − 2K >> l`).
148const fn node_bases() -> [NodeSlot; NODE_LEVELS] {
149 let mut bases = [bits(0); NODE_LEVELS];
150 let mut level = 0;
151 while level < NODE_LEVELS {
152 bases[level] = bits((NODES_PER_ENTRY - (NODES_PER_ENTRY >> level)) as u128);
153 level += 1;
154 }
155 bases
156}
157
158// Ring entries × node slots always fill exactly the 10-bit node address.
159const _: () = assert!(NODE_RING_BATCHES * NODES_PER_ENTRY == 1 << 10);
160// Levels are 3-bit fields, and the index sits at bits 8.. of the 16-bit tag.
161const _: () = assert!(NODE_LEVELS <= 8 && KEY_BITS <= 8);
162// The ring entry number must fit in the 5-bit `ring` fields.
163const _: () = assert!(NODE_RING_BATCHES <= 32);
164/// Pending-parent queue depth.
165///
166/// Parents are produced in short bursts (a leaf can complete a whole chain
167/// of ancestors) but drained at one per 68-cycle period. Eight entries absorb
168/// the bursts; if the queue ever fills, `overflow` latches as a design error.
169pub const JOB_QUEUE: usize = 8;
170/// `JOB_QUEUE - 1`, named because RHDL loop bounds must be a literal or a const.
171const JOB_SHIFTS: usize = JOB_QUEUE - 1;
172
173/// One queued parent: ring entry, level (`1..=SUBTREE_LEVELS`) and index
174/// within the level.
175///
176/// Together these name a node uniquely, so the job also serves as the
177/// request tag ([`job_tag_kernel`]) that comes back with the hash result.
178#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
179pub struct Job {
180 /// Ring entry of the batch.
181 pub ring: b5,
182 /// Tree level of the parent.
183 pub level: b3,
184 /// Index of the parent within its level.
185 pub index: KeyIndex,
186}
187
188/// Inputs from the factory and the signer.
189#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
190pub struct SubtreeInput {
191 /// Finished keys (leaves).
192 pub key_done: KeyDone,
193 /// The factory took the offered Merkle request this cycle.
194 pub merkle_taken: bool,
195 /// Results of the factory's slot requests (non-message ones are ours).
196 pub slot_result: SlotResult,
197 /// Signer proof read address (`ring << 5 | node`), data next cycle.
198 pub proof_address: b10,
199}
200
201/// A finished subtree root.
202#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
203pub struct SubtreeRoot {
204 /// Root present this cycle.
205 pub valid: bool,
206 /// Batch number (relative to the run's first batch).
207 pub batch: b32,
208 /// The level-4 node.
209 pub root: [b8; 32],
210}
211
212/// Outputs to the factory, the shared Merkle top and the signer.
213#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
214pub struct SubtreeOutput {
215 /// Merkle request offered to the factory.
216 pub merkle: SlotRequest,
217 /// Finished subtree root.
218 pub root: SubtreeRoot,
219 /// Node read through the proof port.
220 pub proof_data: [b8; 32],
221 /// The job queue overflowed (a design error; should never assert).
222 pub overflow: bool,
223}
224
225/// Resettable control.
226#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
227pub struct SubtreeControl {
228 /// Pending parents, oldest at index 0.
229 pub jobs: [Job; JOB_QUEUE],
230 /// Number of pending parents.
231 pub count: b4,
232 /// Fetch phase of the head job: 0 idle, 1 left read, 2 right read, 3 offered.
233 pub phase: b2,
234 /// Batch number of each ring entry.
235 ///
236 /// Recorded when leaf `j = 0` of a batch arrives, so the root can be
237 /// labeled with its full 32-bit batch number later.
238 pub batches: [b32; NODE_RING_BATCHES],
239 /// Sticky overflow.
240 pub overflow: bool,
241}
242
243/// Resetless datapath: the two children of the job being offered.
244#[derive(Clone, Copy, Debug, PartialEq, Digital)]
245pub struct SubtreeData {
246 /// Left child.
247 pub left: [b8; 32],
248 /// Right child.
249 pub right: [b8; 32],
250}
251
252/// The subtree builder.
253///
254/// Two node RAMs (one per reader) plus a small control register and the two
255/// child registers of the job being offered. Timing: a leaf or parent
256/// result is written the cycle it arrives; a queued job takes four cycles to
257/// fetch its children and then waits for the factory's Merkle slot.
258#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
259pub struct SubtreeBuilder {
260 /// Copy read by the parent jobs.
261 job_nodes: SyncBRAM<[b8; 32], 10>,
262 /// Copy read by the signer.
263 proof_nodes: SyncBRAM<[b8; 32], 10>,
264 /// Queue, fetch phase, ring labels and overflow flag (reset to empty).
265 control: DFF<SubtreeControl>,
266 /// Children of the job being offered (no reset needed).
267 data: DFF<SubtreeData>,
268}
269
270impl Default for SubtreeBuilder {
271 /// Empty queue, idle phase; node RAMs start with unknown contents (every
272 /// node is written before anyone reads it).
273 fn default() -> Self {
274 Self {
275 job_nodes: SyncBRAM::default(),
276 proof_nodes: SyncBRAM::default(),
277 control: DFF::new(SubtreeControl::default()),
278 data: DFF::resetless(),
279 }
280 }
281}
282
283/// Input: leaves, parent results and proof reads. Output: Merkle requests,
284/// subtree roots and proof data.
285impl SynchronousIO for SubtreeBuilder {
286 type I = SubtreeInput;
287 type O = SubtreeOutput;
288 type Kernel = subtree_kernel;
289}
290
291/// The ring entry holding batch `batch`'s nodes.
292///
293/// `batch mod NODE_RING_BATCHES`, done by keeping the low bits.
294#[kernel]
295pub fn ring_of_kernel(batch: b32) -> b5 {
296 batch.resize::<5>() & RING_MASK
297}
298
299/// Node address of `(level, index)` in ring entry `ring`:
300/// `ring · NODES_PER_ENTRY + NODE_BASE[level] + index`.
301///
302/// Used by the builder (writes and child reads) and by the signer (sibling
303/// reads, through [`sibling_address_kernel`](crate::slice_signer::sibling_address_kernel)).
304/// The multiply is a shift because `NODES_PER_ENTRY = 2^(SUBTREE_LEVELS + 1)`.
305#[allow(clippy::needless_range_loop)]
306#[kernel]
307pub fn node_address_kernel(ring: b5, level: b3, index: KeyIndex) -> b10 {
308 // Look up NODE_BASE[level]; a loop over constants because RHDL kernels
309 // need constant array indices.
310 let mut base: NodeSlot = bits(0);
311 for l in 0..NODE_LEVELS {
312 if level == b3(l as u128) {
313 base = NODE_BASE[l];
314 }
315 }
316 (ring.resize::<10>() << ((SUBTREE_LEVELS + 1) as u128))
317 | (base + index.resize::<NODE_SLOT_BITS>()).resize::<10>()
318}
319
320/// Pack a job into the 16-bit slot tag and back.
321///
322/// Layout: bits 0..5 ring, 5..8 level, 8.. index ([`KEY_BITS`] wide). The factory returns the
323/// tag unchanged with the result; [`subtree_kernel`] unpacks it with shifts.
324#[kernel]
325pub fn job_tag_kernel(job: Job) -> b16 {
326 job.ring.resize::<16>() | (job.level.resize::<16>() << 5) | (job.index.resize::<16>() << 8)
327}
328
329/// Words of `left ‖ right`, little-endian.
330///
331/// The 64-byte block of one Merkle parent hash, as the sixteen 32-bit words
332/// the BLAKE3 lane takes.
333#[allow(clippy::needless_range_loop)]
334#[kernel]
335pub fn pair_block_kernel(left: [b8; 32], right: [b8; 32]) -> [b32; 16] {
336 let mut bytes = [b8(0); 64];
337 for b in 0..32 {
338 bytes[b] = left[b];
339 bytes[32 + b] = right[b];
340 }
341 crate::slice_factory::words_from_bytes_kernel(bytes)
342}
343
344/// The builder's wiring.
345///
346/// Each cycle it does three things:
347///
348/// 1. **Write** at most one node: a new leaf from `key_done`, else a parent
349/// result from the factory. Maybe queue the next parent, or emit the root.
350/// 2. **Fetch/offer** the head job: phase 0 → read left child, 1 → read right
351/// child, 2 → hold both, 3 → offer to the factory until `merkle_taken`.
352/// 3. **Update the queue**: pop the offered job when taken, push a new one.
353///
354/// A leaf and a parent result never arrive in the same cycle: both are
355/// results of the factory's single BLAKE3 lane, which returns at most one
356/// result per cycle. The `if`/`else if` below still gives the leaf priority.
357#[allow(clippy::needless_range_loop)]
358#[kernel]
359pub fn subtree_kernel(
360 clock_reset: ClockReset,
361 input: SubtreeInput,
362 q: SubtreeBuilderQ,
363) -> (SubtreeOutput, SubtreeBuilderD) {
364 let resetting = clock_reset.reset.any();
365 // Working copies of the registers; written back at the end.
366 let mut control = q.control;
367 let mut data = q.data;
368
369 // ------------------------------------------------ writes (leaf / parent)
370 // The leaf (if any) and the ring entry of its batch.
371 let leaf = input.key_done;
372 let leaf_ring = ring_of_kernel(leaf.batch);
373 // A non-message slot result is one of our parent hashes.
374 let parent = input.slot_result.valid && !input.slot_result.message;
375 // Unpack the tag (see `job_tag_kernel`) to find where the parent goes.
376 let p_ring: b5 = input.slot_result.tag.resize();
377 let p_level: b3 = (input.slot_result.tag >> 5).resize();
378 let p_index: KeyIndex = (input.slot_result.tag >> 8).resize();
379 // The parent node is the first 32 bytes of the 64-byte hash output.
380 let p_bytes = crate::slice_factory::bytes_from_words_kernel(input.slot_result.words);
381 let mut p_node = [b8(0); 32];
382 for b in 0..32 {
383 p_node[b] = p_bytes[b];
384 }
385 // The single node write of this cycle (same for both RAM copies).
386 let mut write_address = b10(0);
387 let mut write_value = leaf.pk_hash;
388 let mut write_enable = false;
389 // A new parent to queue (from the leaf or the parent result).
390 let mut push = false;
391 let mut pushed = Job::default();
392 // The subtree root, if this cycle's parent is the top node.
393 let mut root = SubtreeRoot::default();
394 if leaf.valid {
395 write_address = node_address_kernel(leaf_ring, b3(0), leaf.j);
396 write_enable = true;
397 // First leaf of a batch: remember which batch now owns this entry.
398 if leaf.j == KEY_ZERO {
399 for e in 0..NODE_RING_BATCHES {
400 if leaf_ring == b5(e as u128) {
401 control.batches[e] = leaf.batch;
402 }
403 }
404 }
405 // Odd leaf: its even sibling is stored, so the level-1 parent is ready.
406 if (leaf.j & KEY_ONE) == KEY_ONE {
407 push = true;
408 pushed = Job {
409 ring: leaf_ring,
410 level: b3(1),
411 index: leaf.j >> 1,
412 };
413 }
414 } else if parent {
415 write_address = node_address_kernel(p_ring, p_level, p_index);
416 write_value = p_node;
417 write_enable = true;
418 if p_level == ROOT_LEVEL {
419 // Top of the subtree: label it with the entry's batch and send it.
420 let mut batch = b32(0);
421 for e in 0..NODE_RING_BATCHES {
422 if p_ring == b5(e as u128) {
423 batch = q.control.batches[e];
424 }
425 }
426 root = SubtreeRoot {
427 valid: true,
428 batch,
429 root: p_node,
430 };
431 } else if (p_index & KEY_ONE) == KEY_ONE {
432 // Odd parent: its sibling is done, so queue the next level up.
433 push = true;
434 pushed = Job {
435 ring: p_ring,
436 level: p_level + b3(1),
437 index: p_index >> 1,
438 };
439 }
440 }
441
442 // ------------------------------------------------ head job fetch/offer
443 // The oldest queued parent; children are at level − 1, indices 2i, 2i + 1.
444 let head = q.control.jobs[0];
445 let mut read_address = b10(0);
446 // The head job was taken by the factory this cycle.
447 let mut pop = false;
448 if q.control.phase == b2(0) && q.control.count != b4(0) {
449 // Read the left child.
450 read_address = node_address_kernel(head.ring, head.level - b3(1), head.index << 1);
451 control.phase = b2(1);
452 } else if q.control.phase == b2(1) {
453 // Left data arrives; read the right child.
454 data.left = q.job_nodes;
455 read_address =
456 node_address_kernel(head.ring, head.level - b3(1), (head.index << 1) | KEY_ONE);
457 control.phase = b2(2);
458 } else if q.control.phase == b2(2) {
459 // Right data arrives; the request is complete from next cycle on.
460 data.right = q.job_nodes;
461 control.phase = b2(3);
462 } else if q.control.phase == b2(3) && input.merkle_taken {
463 control.phase = b2(0);
464 pop = true;
465 }
466 // The request held on the factory's Merkle slot while in phase 3.
467 let offered = SlotRequest {
468 valid: q.control.phase == b2(3) && !resetting,
469 block: pair_block_kernel(q.data.left, q.data.right),
470 tag: job_tag_kernel(head),
471 tail: false,
472 cv: [b32(0); 8],
473 };
474
475 // ------------------------------------------------ queue update
476 // Pop first (shift everything down), then push at the new tail, so a pop
477 // and a push in the same cycle work.
478 let mut jobs = q.control.jobs;
479 let mut count = q.control.count;
480 if pop {
481 for k in 0..JOB_SHIFTS {
482 jobs[k] = jobs[k + 1];
483 }
484 count = count - b4(1);
485 }
486 if push {
487 for k in 0..JOB_QUEUE {
488 if count == b4(k as u128) {
489 jobs[k] = pushed;
490 }
491 }
492 // Full queue: drop the job and latch the fault (should never happen).
493 if count == b4(JOB_QUEUE as u128) {
494 control.overflow = true;
495 } else {
496 count = count + b4(1);
497 }
498 }
499 control.jobs = jobs;
500 control.count = count;
501
502 // Both RAM copies get the same write; only their read addresses differ.
503 let d = SubtreeBuilderD {
504 job_nodes: RamIn::<[b8; 32], 10> {
505 read_addr: read_address,
506 write: RamWrite::<[b8; 32], 10> {
507 addr: write_address,
508 value: write_value,
509 enable: write_enable,
510 },
511 },
512 proof_nodes: RamIn::<[b8; 32], 10> {
513 read_addr: input.proof_address,
514 write: RamWrite::<[b8; 32], 10> {
515 addr: write_address,
516 value: write_value,
517 enable: write_enable,
518 },
519 },
520 control,
521 data,
522 };
523 (
524 SubtreeOutput {
525 merkle: offered,
526 root,
527 proof_data: q.proof_nodes,
528 overflow: q.control.overflow,
529 },
530 d,
531 )
532}