Skip to main content

rhdl_dsig_scalable_core/
lib.rs

1//! An all-RHDL DSig signer, and a verifier, whose throughput scales with the
2//! number of *slices* (signer) or *units* (verifier) they build.
3//!
4//! # Building for Wolverine's U280
5//!
6//! The signing-engine flow generates RTL from this crate, then packages and
7//! links it on Wolverine. Cargo alone does not produce a card image.
8//!
9//! Run commands from the workspace root. Before compilation or RTL generation,
10//! record ownership in `PROJECT_COORDINATION.md` and
11//! `/home/sahasta/Development/FPGA_COORDINATION.md`, and use the applicable
12//! nonblocking `local-heavy.lock`; `crates/ed25519/tools/bounded.sh` wraps local
13//! Cargo commands with that lock and resource limits. Reserve Wolverine builds
14//! in both ledgers too, then use `crates/dsig/tools/with-u280-lock.sh` to hold
15//! `u280.lock`. Do not bypass a busy lock.
16//!
17//! Inside the reserved, locked build session, generate a fresh bundle (Rust 1.96
18//! is the toolchain used by the repository's DSig bounded runner):
19//!
20//! ```sh
21//! export CARGO_TARGET_DIR="$PWD/target"
22//! cargo build --locked --profile sim -p rhdl_dsig_scalable_core --bin dsig_engine_export
23//! RHDL_HDL_ONLY=1 "$CARGO_TARGET_DIR/sim/dsig_engine_export" /absolute/path/new-bundle
24//! ```
25//!
26//! The card build uses Vivado/Vitis 2023.2, the installed U280 `.xpfm`
27//! platform and XRT. Replace the absolute example paths below with real paths
28//! on Wolverine; the bundle must be copied there with its `BUNDLE.txt` intact,
29//! and the output directory must be new. Keep the repository checkout available
30//! on Wolverine because the scripts use its Tcl and host sources. Building an
31//! image does not program the card or establish measured performance.
32//!
33//! The build script requires 48 GiB available RAM and 30 GiB free disk.
34//! After recording Wolverine ownership in both ledgers, run there:
35//!
36//! ```sh
37//! export HASHSIGS_U280_COORDINATION_ACK=both-ledgers-recorded
38//! crates/dsig/tools/with-u280-lock.sh \
39//!   "$PWD/crates/dsig/tools/wolverine-engine.sh" build \
40//!   /absolute/path/new-bundle /absolute/path/new-build-output \
41//!   /absolute/path/U280-platform.xpfm \
42//!   /home/Xilinx/Vitis/2023.2/settings64.sh /opt/xilinx/xrt/setup.sh
43//! ```
44//!
45//! Success produces `dsig_engine.xclbin`, a host runner, and
46//! `acceptance/route-accepted.txt` after checking routing, setup/hold timing and
47//! the kernel LUT cap. See `crates/dsig/fpga/u280/engine/README.md` for the
48//! separate prepare/run/verify steps. Use a fresh run directory and key range
49//! for every attempt, including after timeout or uncertain launch. Preserve
50//! `DSIG_FPGA_W4_HARAKA_BLAKE3_V1`; a verified burst is not continuous throughput.
51//!
52//! For the verifier and isolated uBFT signature-service probe, use
53//! `crates/dsig/tools/wolverine-verify.sh` and `wolverine-ubft.sh`, respectively,
54//! with the matching exporters and the README under `fpga/u280/verify` or
55//! `fpga/u280/ubft`. The latter is a crypto-service probe, not the full
56//! `rhdl_ubft` consensus implementation. Match host and bundle slice counts.
57//!
58//! DSig signs each message with a fresh one-time hash-based key (W-OTS+,
59//! w = 4) and amortizes one Ed25519 signature over every batch of 128 keys.
60//! This crate is the hardware that does all of it on an Alveo U280. It
61//! *makes* every key, certifies every batch, signs every message and writes
62//! every 1,642-byte signature to HBM; and it checks signatures again at the
63//! same rate.
64//!
65//! **In numbers** (October 2026). The 4-slice signer (build v13) signs
66//! **12,038,905 signatures per second** on the card: a verified burst of
67//! 1,572,864 signatures from one cold start, 17.03 cycles per signature, the
68//! 4-slice ceiling of `4 × 205 MHz / 68`. It routes at 205 MHz (WNS
69//! +0.016 ns) in **388,542 kernel CLB LUT** of the 420,000 cap. Every frame
70//! is re-verified in software before a rate is printed. The 3-unit
71//! verifier (build v3) checks those signatures on the card at **at least
72//! 16,959,195 per second** (1,572,864 verified valid; a tampered burst got
73//! every verdict right), routed at 205 MHz in 368,713 LUT. No continuous
74//! (back-to-back) rate is claimed for either side: every number is a finite
75//! burst from reset.
76//!
77//! | | 2 slices | 4 slices | 8 slices |
78//! |---|---:|---:|---:|
79//! | Ceiling at 205 MHz (one key per 68 clocks per slice) | 6.0M sig/s | 12.1M sig/s | 24.1M sig/s |
80//! | Measured on the card | 6,019,465 sig/s (v3) | **12,038,905 sig/s** (v13) | model only |
81//! | Kernel CLB LUT, routed at 205 MHz | 296,558 (v2) | 388,542 (v13) | model only |
82//!
83//! Reproduce: `tools/wolverine-engine.sh build` then `run` (see
84//! `fpga/u280/engine/README.md`); the simulation gates are in
85//! [Proving it](#chapter-11-proving-it).
86//!
87//! # The story in five minutes
88//!
89//! **The problem.** Ed25519 is the signature everyone trusts, and it is
90//! expensive: this project's Ed25519 verify core checks about 279,000 per
91//! second (native simulation, 735 cycles each at 205 MHz). Some systems
92//! want tens of millions.
93//!
94//! **The bet.** DSig signs each message with a one-time hash-based key and
95//! uses one Ed25519 signature to vouch for a whole batch of 128 such keys.
96//! Hashing is cheap in hardware, so if the hashes are fast enough the
97//! expensive part is paid once per 128 signatures.
98//!
99//! **The first version** made keys one at a time and was slow. Then, one
100//! idea per step: skip the hash work whose inputs are constants (640 → 494
101//! S-boxes per step); run a whole three-step chain every clock; give every
102//! BLAKE3 compression a fixed slot in a 68-cycle timetable; make keys
103//! before messages arrive; tag up to 32 certificate batches in flight; and
104//! copy the whole design four times as *slices*.
105//!
106//! **What broke** was rarely the arithmetic. It was wires: a clock of 205 MHz
107//! leaves 4.9 ns per step, and a wire from one silicon die to the next
108//! eats a large part of it. Builds v5 to v13 were mostly about pinning blocks
109//! to dies and putting registers on every long wire.
110//!
111//! **Where it stands.** The signer makes 12.04 million verified signatures
112//! per second on a real card, the most four slices can do. The verifier
113//! checks them on the card at 16.96 million per second, after a Vivado
114//! netlist bug in the Ed25519 core was found and designed around.
115//!
116//! **Where you come in.** Eight slices (24M/s) need each slice at about
117//! half its size, the verifier needs a fourth unit, and nobody has measured
118//! continuous throughput. Small, checkable
119//! starting points are in [Good first contributions](#good-first-contributions).
120//!
121//! This page tells the story of how the design came to be, one problem at a
122//! time: what we were trying to do, what stood in the way, how we reasoned
123//! about it, and what we built. Each chapter ends with the module that holds
124//! the details. Read it in order once. Afterwards, [Where to go
125//! next](#where-to-go-next) sends you straight to the part you want to
126//! change.
127//!
128//! # Contents
129//!
130//! 1. [The question: what does a DSig signature cost?](#chapter-1-the-question-what-does-a-dsig-signature-cost)
131//! 2. [Doing less work](#chapter-2-doing-less-work)
132//! 3. [A whole chain every clock](#chapter-3-a-whole-chain-every-clock)
133//! 4. [Feeding the chains: the 68-cycle timetable](#chapter-4-feeding-the-chains-the-68-cycle-timetable)
134//! 5. [Keys before messages](#chapter-5-keys-before-messages)
135//! 6. [One Ed25519 signature per 128 keys](#chapter-6-one-ed25519-signature-per-128-keys)
136//! 7. [More of everything: slices](#chapter-7-more-of-everything-slices)
137//! 8. [Meeting the clock](#chapter-8-meeting-the-clock)
138//! 9. [Choosing the chain hash](#chapter-9-choosing-the-chain-hash)
139//! 10. [Living on a shared chip](#chapter-10-living-on-a-shared-chip)
140//! 11. [Proving it](#chapter-11-proving-it)
141//! 12. [Checking signatures: the verifier](#chapter-12-checking-signatures-the-verifier)
142//! 13. [Epilogue: your turn](#epilogue-your-turn)
143//!
144//! # The map
145//!
146//! Read the chapters below in order once; then these are the modules, in
147//! the order a newcomer should open them:
148//!
149//! | Module | One line |
150//! |---|---|
151//! | [`reference`](mod@reference) | the whole scheme in plain software: the answer key for every block |
152//! | [`chain_step`], [`chain_lane`] | one chain step (Haraka-512 with 494 live S-boxes, or BLAKE3, SHA-512) |
153//! | [`slice_chain`], [`slice_schedule`], [`slice_factory`] | a whole chain per clock; the 68-cycle timetable; the key factory |
154//! | [`cache_bank`], [`slice_signer`], [`packer`] | the key cache and the signer that turns a message into a frame |
155//! | [`slice_merkle`], [`cert_unit`], [`certificate`], [`ed25519`] | subtrees, batch roots and the Ed25519 certificate |
156//! | [`slice`](mod@slice), [`engine`] | one slice; the card kernel with its registers and HBM ports |
157//! | [`relay`] | registered links between blocks on different dies |
158//! | [`verify`] | the verifier: records in, verdict bytes out |
159//! | [`config`], `planner` | cost and rate models for the shared chip planner and TUI |
160//!
161//! # The cost model
162//!
163//! Where the signer's 388,542 LUT go (build v13, routed, measured):
164//!
165//! | Block | CLB LUT | What dominates |
166//! |---|---:|---|
167//! | 4 slices | 4 × ~82.8k = 331,450 | per slice: key factory 65k (three Haraka lanes ~12k each, BLAKE3 lane 16.7k), signer 16.7k, subtree 1k |
168//! | certificate unit | 48,476 | Ed25519 signer 25.9k, BLAKE3 lane 16.3k |
169//! | frame-beat relay pipes | 6,263 | one LUT per bit per stage |
170//! | control, reader, writers, toggle links | ~2.4k | |
171//!
172//! The binding constraint is LUTs, not cycles: a slice already makes one
173//! key per 68 clocks with its lanes busy every clock, so throughput grows
174//! only with more slices, and slices come in powers of two (the Merkle
175//! subtree split). Block RAM is second (~270 tiles per slice with T-tables
176//! in rounds 3..6); DSP is unused except by Ed25519.
177//!
178//! # Chapter 1: The question: what does a DSig signature cost?
179//!
180//! The goal was blunt: 30 million measured signatures per second on one
181//! U280, inside a fixed LUT budget. Before designing anything we counted the
182//! work, because *throughput = hardware ÷ work per signature*, and we can
183//! attack both sides.
184//!
185//! ```text
186//! key generation (per one-time key, ahead of time)
187//!   seed, nonces           3 BLAKE3 compressions
188//!   68 chain secrets       20 BLAKE3 compressions (XOF)
189//!   68 hash chains         68 × 3 chain steps (Haraka-512)   ◀ almost all the work
190//!   endpoint commitment    21 BLAKE3 compressions
191//!   Merkle leaf + parents  ~1 BLAKE3 compression
192//! certificate (per 128 keys)
193//!   Merkle top + envelope  a few BLAKE3 compressions, 1 Ed25519 signature
194//! signing (per message)
195//!   message digest         1 BLAKE3 compression → 68 base-4 digits
196//!   reveal                 one stored chain node per chain, 7 Merkle siblings
197//! ```
198//!
199//! A W-OTS+ key has 68 *hash chains*: 64 carry a base-4 digit of the message
200//! digest and 4 carry a checksum. Each chain starts at a secret and is hashed
201//! three times. A signature reveals, for each chain, the node at the
202//! position of its digit. The verifier hashes the rest of the way and checks
203//! that it lands on the public endpoint.
204//!
205#![doc = concat!("<div style=\"overflow-x:auto\">", include_str!("../docs/images/hbss-wots.svg"), "</div>")]
206//!
207//! So a key costs 204 chain steps and about 47 BLAKE3 compressions. Signing
208//! a message is cheap; making the key is the work. **The conclusion that
209//! shaped everything:** build a machine that makes keys at line rate and
210//! signs from a cache of them.
211//!
212//! # Chapter 2: Doing less work
213//!
214//! **Problem.** A Haraka-512 chain step is 40 AES rounds, 640 S-boxes.
215//! S-boxes are 256-entry tables. In LUTs they are the bulk of the area.
216//!
217//! **Reasoning.** A chain block is not arbitrary input. Its layout is fixed:
218//! a 16-byte salted nonce, the 18-byte node, then 30 zero bytes. Every byte
219//! that is constant at compile time makes some S-box inputs constant, and a
220//! constant S-box is no S-box at all.
221//!
222//! **Decision.** Track which bytes are known through every round and keep
223//! only the live S-boxes. 640 become **494**, pinned by a test, so one
224//! signature is about 204 × 494 ≈ 100,800 S-box evaluations, and *signatures
225//! per second ≈ S-boxes per second ÷ 100,800*.
226//!
227//! → [`chain_step`], [`haraka`], [`aes_table`]
228//!
229//! # Chapter 3: A whole chain every clock
230//!
231//! **Problem.** A chain is three *dependent* steps. Hardware that computes
232//! one step and loops back would sit idle while each step travels through
233//! its pipeline.
234//!
235//! **Reasoning.** The 68 chains of a key are independent of each other. If
236//! the three steps of a chain are three separate pipelined lanes in series,
237//! a new chain can enter every clock while earlier ones are still inside.
238//!
239//! **Decision.** A *chain engine* is three chain-step lanes in series, each
240//! a fully pipelined Haraka-512 step (20 cycles). It finishes one whole chain
241//! per clock, all four node positions at once.
242//!
243//! ```text
244//!  secret ──▶ [lane 1] ──▶ [lane 2] ──▶ [lane 3] ──▶ endpoint
245//!    │            │            │            │
246//!    └────────────┴────────────┴────────────┴──▶ 4 nodes → key cache
247//! ```
248//!
249//! → [`slice_chain`], [`chain_lane`]
250//!
251//! # Chapter 4: Feeding the chains: the 68-cycle timetable
252//!
253//! **Problem.** The chain engine wants a new chain secret every clock, so a
254//! key's 68 chains take 68 clocks. Around them, the same key needs about 47
255//! BLAKE3 compressions: seeds, the XOF that makes the secrets, and the
256//! commitment over the 68 endpoints. One BLAKE3 lane finishes a compression
257//! per clock but with a 28-cycle latency, and many of these compressions
258//! depend on each other.
259//!
260//! **Reasoning.** If every key's BLAKE3 work happens at the same fixed
261//! offsets, the timetable for one key repeats every 68 cycles. Overlapping
262//! keys then never collide, as long as no two operations of different keys
263//! land on the same cycle *modulo 68*. That is a *modulo schedule*, the
264//! classic way compilers pipeline loops.
265//!
266//! **Decision.** A small solver, run at compile time, places every BLAKE3
267//! operation of a key on its own residue mod 68 and respects every data
268//! dependency. Up to 16 keys are in flight at once, each in its own
269//! *context* (a slot in small RAMs). Result: **one key per 68 cycles per
270//! slice**, with the BLAKE3 lane and the chain engine both busy every clock.
271//!
272//! ```text
273//!  cycle mod 68:  0 ......................................... 67
274//!  chain engine:  chain 0, chain 1, ...                 chain 67   (key k)
275//!  BLAKE3 lane:   XOF of key k+1, commitment of key k−1, seeds of key k+2 ...
276//! ```
277//!
278//! → [`slice_schedule`], [`slice_factory`]
279//!
280//! # Chapter 5: Keys before messages
281//!
282//! **Problem.** Messages arrive when they want. Keys come out at a steady
283//! rate. Signing must not wait for key generation, and a one-time key must
284//! never sign twice.
285//!
286//! **Reasoning.** Separate the two with a cache. The factory fills it at its
287//! own pace; the signer takes one key per message. A key is spent the
288//! moment it is admitted to a message, and also on timeout, reset or any
289//! uncertain launch. It is never reused.
290//!
291//! **Decision.** Each slice caches 512 keys (68 chains × 4 nodes each) in
292//! dense UltraRAM. The signer turns a message digest into 68 digits, reads
293//! one node per chain and streams the 1,642-byte frame.
294//!
295//! → [`cache_bank`], [`slice_signer`], [`packer`]
296//!
297//! # Chapter 6: One Ed25519 signature per 128 keys
298//!
299//! **Problem.** Every 128 keys need a Merkle tree and one Ed25519 signature
300//! over its root. The embedded Ed25519 core takes about 22,000 cycles per
301//! signature. One at a time, that caps the engine at one batch per 22,000
302//! cycles, far below what the slices make.
303//!
304//! **Reasoning.** *Little's law*: work in flight = throughput × time in
305//! system. To finish one certificate every ~895 cycles with a 22,000-cycle
306//! latency, about 25 must be in flight.
307//!
308//! **Decision.** The certificate unit tags each batch and keeps up to 32 in
309//! flight, then delivers certificates in order.
310//!
311//! The batch below is DSig's split in one picture: the hash-based one-time
312//! keys are the *foreground* that signs every message; one Ed25519
313//! signature per 128 keys is the *background* that vouches for them.
314//!
315#![doc = concat!("<div style=\"overflow-x:auto\">", include_str!("../docs/images/dsig-batch.svg"), "</div>")]
316//!
317//! → [`cert_unit`], [`ed25519`], [`certificate`]
318//!
319//! # Chapter 7: More of everything: slices
320//!
321//! **Problem.** One slice makes 205 MHz ÷ 68 ≈ 3.0M keys per second. The
322//! goal is ten times that.
323//!
324//! **Reasoning.** Chapters 3–6 made a slice that never idles. The only
325//! remaining lever is more of them. Each slice is a complete signer, so
326//! slices need nothing from each other except their share of each batch's
327//! Merkle tree.
328//!
329//! **Decision.** The engine builds `SLICES` (2, 4 or 8) copies. RHDL lowers
330//! the slice once and instantiates it `SLICES` times. Each slice owns
331//! `128 / SLICES` keys of every batch and builds that subtree; the
332//! certificate unit joins the subtree roots.
333//!
334//! ```text
335//!                     ┌──────────── slice (× SLICES) ────────────┐
336//!  seed, epoch ─────▶ │ factory: BLAKE3 lane + 3 chain lanes      │
337//!                     │   68-cycle modulo schedule: 1 key / 68 clk│
338//!                     │ key cache: 512 keys in UltraRAM           │
339//!                     │ subtree: K leaves → 1 root                │──▶ HBM (frames)
340//!  messages (HBM) ──▶ │ signer: digits → nodes → 1,642-byte frame │
341//!                     └───────────────▲──────────────┬────────────┘
342//!                                     │ certificate  │ subtree root
343//!                     ┌───────────────┴──────────────▼────────────┐
344//!                     │ cert unit: Merkle top + Ed25519 (≤32 in   │
345//!                     │ flight, tagged, in-order delivery)        │
346//!                     └────────────────────────────────────────────┘
347//! ```
348//!
349//! → [`slice`](mod@slice), [`slice_merkle`], [`engine`], [`SLICES`]
350//!
351//! # Chapter 8: Meeting the clock
352//!
353//! **Problem.** The first routed 2-slice link used 301,037 kernel CLB LUTs
354//! but failed timing at 205 MHz by **2.167 ns**.
355//!
356//! **Reasoning.** The per-hierarchy report pointed at the key caches. Vivado
357//! had mapped them to about 136 block RAMs per bank, spread across the die,
358//! with unregistered inputs fanning out to all of them.
359//!
360//! **Decision.** Put the caches in UltraRAM (a `RamStyle` patch in the
361//! vendored RHDL lets an RHDL memory ask for it), and register their
362//! inputs. That frees block RAM, which the S-box placement can now use
363//! (Haraka S-boxes in spare UltraRAM and block RAM instead of LUTs).
364//!
365//! **Result.** The second link met timing at 205 MHz (setup and hold) with
366//! 296,558 kernel CLB LUTs.
367//!
368//! The climb to four slices at 205 MHz took nine more builds (v4–v13): table
369//! rounds, a registered reset, hard per-die floorplans, registered links,
370//! kept hierarchy and toggle links. Each is a numbered step with its
371//! measurement in [How the speed adds up](#how-the-speed-adds-up).
372//!
373//! → [`cache_bank`], [`sbox_placement`], [`config`]
374//!
375//! # Chapter 9: Choosing the chain hash
376//!
377//! DSig works with any of three chain hashes. Each is a different signature
378//! scheme with its own profile name; signatures from one never verify under
379//! another. Only chain steps change; everything else stays BLAKE3.
380//!
381//! | Build variable | Profile | Lane latency |
382//! |---|---|---:|
383//! | `DSIG_CHAIN=haraka` (default) | `DSIG_FPGA_W4_HARAKA_BLAKE3_V1` | 20 |
384//! | `DSIG_CHAIN=blake3` | `DSIG_FPGA_W4_BLAKE3CHAIN_BLAKE3_V1` | 28 |
385//! | `DSIG_CHAIN=sha512` | `DSIG_FPGA_W4_SHA512CHAIN_BLAKE3_V1` | 82 |
386//!
387//! **A lesson from SHA-512.** Its lanes are four times slower, so a key's
388//! chain ends leave the engine 248 cycles after they start, more than three
389//! 68-cycle periods. Two assumptions broke. Keys stay in flight longer, so
390//! the factory needs 32 contexts instead of 16. And the commitment prefix
391//! was read from "the current key", which by then is a newer key. The
392//! factory now keeps the last few keys' nonces and picks by age. The Haraka
393//! hardware is unaffected.
394//!
395//! → [`chain_lane`], [`slice_factory`]
396//!
397//! # Chapter 10: Living on a shared chip
398//!
399//! DSig shares the chip with Ed25519 signing and verification, and both
400//! draw on one shared SHA-512 line. A planner models every component's cost
401//! and rate; one TUI, shared with the Ed25519 crate, lets you move budgets
402//! and see sign/s, verify/s, LUT, DSP and latency change.
403//!
404//! In the DSig paper's chip, Ed25519 does one signature per 128 DSig
405//! signatures, and one verification per DSig verification whose batch root
406//! is not already verified: `1 − h` with `h = (b/100) · 127/128`, where `b`
407//! is the best-case expectation.
408//!
409//! → [`config`], `planner` (feature `planner`), `dsig_plan`
410//!
411//! # Chapter 11: Proving it
412//!
413//! ## What the card taught us
414//!
415//! **Problem.** The first card runs of the routed 2-slice kernel never
416//! finished. The kernel clock ran (about 205.2M ticks per second), but the
417//! counters froze: 6 of 8 batches certified, both key caches full, and the
418//! certificate unit's sticky fault set.
419//!
420//! **Reasoning.** Every block had passed bit-exact simulation, but the
421//! engine test signed one batch. With one batch, each slice offers one
422//! subtree root. With eight, each slice keeps making keys up to eight
423//! batches ahead and offers a root for every batch it finishes, while the
424//! certificate unit's root queue holds two. During the Ed25519 key load
425//! nothing is popped, so the third root met a full queue and was dropped.
426//! The 8-batch simulation reproduces it: the same fault at cycle 16,271.
427//!
428//! **Decision.** Root credit. The certificate unit reports how many
429//! batches' roots it has taken ([`cert_unit::CertOutput::roots_popped`]),
430//! and a slice starts a key only while its batch is fewer than
431//! [`cert_unit::ROOT_QUEUE`] ahead. The lesson for every test here: run at
432//! least as many batches as any queue is deep, and check the fault register
433//! every few thousand cycles.
434//!
435//! ## What is proven
436//!
437#![doc = include_str!("../docs/validation.md")]
438//!
439//! ## How the speed adds up
440//!
441#![doc = include_str!("../docs/how-it-got-fast.md")]
442//!
443//! The longer design walk-through, with every budget and decision, is
444//! [`sustained`].
445//!
446//! # Chapter 12: Checking signatures: the verifier
447//!
448//! **Problem.** A signature is only worth what it costs to check, and the
449//! comparison paper prices DSig verification next to Ed25519's on the same
450//! chip. The planner's verify numbers were a model.
451//!
452//! **Reasoning.** Checking is the signer's work run backwards and halved:
453//! each chain needs only `3 − digit` steps (105 of 204 on average), plus 29
454//! BLAKE3 compressions and one Ed25519 check per batch of 128. The same
455//! lanes can do it if they never idle on chains that need no step.
456//!
457//! **Decision.** Verify units of three *recirculating* chain lanes and one
458//! BLAKE3 lane shared by 64 records in flight, a certificate cache in front
459//! of the Ed25519 project's verify core, and records in, verdict bytes out,
460//! in order.
461//!
462//! **Result.** 35.12 cycles per verification per unit in native
463//! simulation, 80,211 LUT per core routed alone at 205 MHz; on the card,
464//! three units verify 16,959,195 signatures per second (build v3, finite
465//! burst of 1,572,864).
466//!
467//! → [`verify`] (its page tells the verifier's own story)
468//!
469//! # Epilogue: your turn
470//!
471//! ## Using it
472//!
473//! The whole kernel is one RHDL [`Synchronous`](rhdl::prelude::Synchronous)
474//! component, [`engine::Engine`]. Its size and chain hash are chosen at
475//! compile time:
476//!
477//! ```text
478//! DSIG_SLICES=2 cargo test --profile sim
479//! cargo run --profile sim --bin dsig_engine_export -- NEW_BUNDLE_DIR      # Verilog bundle
480//! cargo run --release --bin dsig_plan -- --sweep                          # every build
481//! ```
482//!
483//! The U280 flow (XO packaging, `v++` link at 205 MHz, routed acceptance,
484//! card run) is in `fpga/u280/engine/README.md`.
485//!
486//! ## How to work
487//!
488//! | Level | Command | Time |
489//! |---|---|---|
490//! | Leaf blocks, bit-exact vs reference | `DSIG_SLICES=2 DSIG_CHAIN=haraka bash tools/leaf-gate.sh` (inside `tools/dsig-bounded.sh`) | ~25 min |
491//! | Sustained throughput | `DSIG_SLICES=2 cargo test --profile sim --test throughput -- --ignored --nocapture` | ~30 min |
492//! | Verilog bundle + closure check | `DSIG_SLICES=2 bash tools/engine-bundle-gate.sh NEW_DIR` | ~5 min, 4.6 GB |
493//! | Vivado (U280 host) | `tools/wolverine-engine.sh ooc / build / run / report` | hours |
494//!
495//! RHDL kernels must use literal or const loop bounds and const array
496//! indices, keep `Bits` ≤ 128 bits, give a struct at most ~11 synchronous
497//! children, and avoid `/` and `%`. These compile under rustc and fail only
498//! when lowering. Inside `#[kernel]` bodies, use `//` comments, never `///`.
499//! Never run `cargo fmt` over `vendor/` (each snapshot carries a guard).
500//!
501//! ## Open problems
502//!
503//! 1. **Continuous throughput.** Every card number is a finite burst from
504//!    one cold start; back-to-back runs are not measured.
505//! 2. **Past 12M signatures/s.** The next slice count is 8 (24.1M/s), which
506//!    needs slices of ~46k LUT instead of ~83k: S-boxes in block RAM and
507//!    UltraRAM, BLAKE3 adders in DSPs, a smaller signer.
508//! 3. **A fourth verify unit.** Three units (368,713 LUT) verify 16.96M/s
509//!    on the card; a fourth needs each unit ~10k LUT smaller (see [`verify`]).
510//! 4. **The SHA-512 line above 1 block per clock** (goal 1.75) in 65k LUT
511//!    and 20 DSP: see `rhdl_sha512_pipelined`.
512//! 5. **More S-boxes per LUT.** BLAKE3 adders in DSPs and T-tables in URAM
513//!    are estimates in the planner today.
514//!
515//! ## Good first contributions
516//!
517//! Each of these is open today and has a test or a measurement that says
518//! when it is done.
519//!
520//! | Task | Size | What you'll learn | You're done when |
521//! |---|---|---|---|
522//! | Draw the verify unit (reader, core, writer) as an SVG for `docs/verifier.md` | an hour | how a record flows through the verifier | the diagram matches [`verify::unit`] and renders in `cargo doc` |
523//! | Report each verdict class (format, Merkle, chains, certificate) separately in `dsig_verify_host check` | a few hours | the record format and the oracle [`verify::expected_verdict`] | the tampered-record run prints counts per bit that match the oracle |
524//! | Price the verifier's shared cluster once per kernel in the planner, not per unit | a few hours | how the planner and TUI turn measurements into plans | `VERIFY_UNIT_IO_LUT` holds only the reader and writer, and the planner tests pass |
525//! | Make `verify_core_lowers_to_verilog` fit the bounded runner (≤ 5 GB) | a day | how RHDL lowers a design to Verilog | the test passes under `tools/dsig-bounded.sh` in normal class |
526//! | Simulate the verifier with BLAKE3 chains (`DSIG_CHAIN=blake3`) | days | how the chain hash plugs into lanes and rings | `tests/verify_datapath.rs` passes with BLAKE3 records |
527//! | Shrink a verify unit by ~10k LUT so four fit under 420k | weeks | where LUTs go in a routed design | a routed 4-unit kernel at or under the cap |
528//! | Measure continuous signing: back-to-back bursts without a reset | weeks, needs the card | the card flow and one-time-key safety | a measured rate over consecutive verified bursts, reported apart from the burst rate |
529//!
530//! ## Where to go next
531//!
532//! | You want to… | Start here | Then |
533//! |---|---|---|
534//! | make key generation faster or smaller | [`chain_step`] | [`aes_table`], [`sbox_placement`], [`config`] |
535//! | change the chain hash | [`chain_lane`] | [`slice_factory`] (contexts, `PACK_HISTORY`), [`haraka`] |
536//! | change the per-key timetable | [`slice_schedule`] | [`slice_factory`] |
537//! | change how messages are signed | [`slice_signer`] | [`packer`], [`cache_bank`] |
538//! | change batching or certificates | [`cert_unit`] | [`slice_merkle`], [`certificate`], [`ed25519`] |
539//! | change the card interface (HBM, registers) | [`engine`] | [`msg_reader`], [`hbm_writer`], [`axi4`] |
540//! | run on the card | `fpga/u280/engine/README.md` | [`lease`], `dsig_engine_host` |
541//! | check a signature in software | [`reference`](mod@reference) | [`slice_model`] |
542//! | check signatures in hardware | [`verify`] | [`verify::datapath`], `dsig_verify_host` |
543//! | change the planner or TUI numbers | [`config`] | `planner` (feature `planner`) |
544//! | understand a number on this page | [Proving it](#chapter-11-proving-it) | `docs/` |
545//!
546//! ## Glossary
547//!
548//! | Term | Meaning |
549//! |---|---|
550//! | Slice | one complete signer; the engine has `SLICES` of them |
551//! | Batch | 128 one-time keys sharing one Merkle root and one Ed25519 certificate |
552//! | Chain | one of a key's 68 W-OTS+ hash chains: a secret hashed three times |
553//! | Chain step | one compression of a salted chain node by the chain hash |
554//! | Context | one in-flight key's slot in the factory's small RAMs |
555//! | Modulo schedule | a fixed per-residue timetable repeated every 68 cycles |
556//! | Little's law | in flight = throughput × time in system |
557//! | Profile | the name of the exact scheme compiled in (chain hash included) |
558//! | Frame | one 1,642-byte signature plus its message index, padded to 2 KiB |
559//! | Best case `b` | how often a DSig verification finds its batch already verified |
560//! | SLR | one of the U280's three dies; crossing between them costs time |
561//! | Pblock | a placement region; the platform's per-SLR pblocks pin blocks to a die |
562//! | Relay pipe | registers on a valid/ready link so it can cross a die (Carloni) |
563//! | Toggle link | a cheaper registered link for rare items: one bit flips per item |
564//! | Ceiling | the rate a build reaches if every slice makes one key per 68 clocks |
565//! | Verify unit | one HBM reader, one verify core, one verdict writer |
566//! | Record | a signature plus its message, the verifier's input (1,664 bytes) |
567#![forbid(unsafe_code)]
568
569/// The full design walk-through: budgets, slices, caches and decisions.
570#[doc = include_str!("../docs/sustained.md")]
571pub mod sustained {}
572
573/// The compiled-in DSig profile (chosen by the `chain-*` feature).
574pub use chain_lane::PROFILE;
575/// Frozen derivation domain shared by hardware builders and the host oracle.
576pub const SEED_DERIVATION_CONTEXT: &str = "DSig FPGA V1 one-time seed allocation";
577/// Number of independent message digits, taken from low bits of XOF bytes.
578pub const MESSAGE_DIGITS: usize = 64;
579/// Number of little-endian base-four complement-checksum digits.
580pub const CHECKSUM_DIGITS: usize = 4;
581/// Number of independently salted WOTS hash chains.
582pub const CHAINS: usize = MESSAGE_DIGITS + CHECKSUM_DIGITS;
583/// Number of stored positions, including each secret and its public endpoint.
584pub const POSITIONS: usize = 4;
585/// Number of bytes per chain node (144 bits).
586pub const ELEMENT_BYTES: usize = 18;
587/// Number of public keys authenticated by one Ed25519 certificate.
588pub const BATCH_KEYS: usize = 128;
589/// Host-side byte-oriented chain element.
590pub type Element = [u8; ELEMENT_BYTES];
591/// Host-side 256-bit digest.
592pub type Digest = [u8; 32];
593/// Host-side 128-bit salt; message nonces stay signer-private until admission.
594pub type Nonce = [u8; 16];
595/// Number of siblings in an inclusion proof for a fixed-size batch.
596pub const MERKLE_HEIGHT: usize = 7;
597
598// ---------------------------------------------------------------- area knob
599//
600// How many slices the engine builds is the main area/throughput knob. It is
601// chosen at compile time with the DSIG_SLICES build variable:
602//
603//   DSIG_SLICES=2 cargo build   # 127,502 LUT per slice (routed)
604//   DSIG_SLICES=4 cargo build
605//   cargo build                 # 8 slices (default)
606//
607// Every slice still signs its share of each 128-key batch: with fewer slices
608// each one owns more keys per batch, its subtree grows by one level per
609// halving, and the certificate unit's Merkle top shrinks by the same level.
610// The inclusion proof is always MERKLE_HEIGHT = 7 siblings.
611//
612//   SLICES  KEYS_PER_SLICE  SUBTREE_LEVELS  TOP_LEVELS  ceiling @205 MHz
613//      2          64              6              1          6.0M sig/s
614//      4          32              5              2         12.1M sig/s
615//      8          16              4              3         24.1M sig/s
616
617// `SLICES` comes from the `DSIG_SLICES` build variable (2, 4 or 8; default
618// 8), checked by `build.rs`. It is not a Cargo feature because exactly one
619// count must hold, and Cargo features must be safe to enable together.
620include!(concat!(env!("OUT_DIR"), "/engine_shape.rs"));
621
622/// Keys of each batch made, cached and signed by one slice.
623pub const KEYS_PER_SLICE: usize = BATCH_KEYS / SLICES;
624/// Merkle levels inside a slice's subtree (`log2 KEYS_PER_SLICE`).
625pub const SUBTREE_LEVELS: usize = KEYS_PER_SLICE.trailing_zeros() as usize;
626/// Merkle levels joined by the certificate unit (`log2 SLICES`).
627pub const TOP_LEVELS: usize = SLICES.trailing_zeros() as usize;
628
629const _: () = assert!(SLICES * KEYS_PER_SLICE == BATCH_KEYS);
630const _: () = assert!(SUBTREE_LEVELS + TOP_LEVELS == MERKLE_HEIGHT);
631/// Width of a key's number within its slice's batch share (`j`) and of a
632/// Merkle node's index within its level: 6 bits in every batch-engine
633/// build (up to 64 keys per slice), more only when a slice makes larger
634/// batch shares. Kept at 6 rather than shrunk so the measured builds'
635/// hardware does not change.
636pub const KEY_BITS: usize = if SUBTREE_LEVELS > 6 { SUBTREE_LEVELS } else { 6 };
637/// A key's number within its slice's batch share ([`KEY_BITS`] wide).
638pub type KeyIndex = rhdl::prelude::Bits<KEY_BITS>;
639/// `0` as a [`KeyIndex`].
640pub const KEY_ZERO: KeyIndex = rhdl::prelude::bits(0);
641/// `1` as a [`KeyIndex`].
642pub const KEY_ONE: KeyIndex = rhdl::prelude::bits(1);
643/// Merkle levels that have a node base (`0..=KEY_BITS`).
644pub const NODE_LEVELS: usize = KEY_BITS + 1;
645/// Width of a node slot within one ring entry (`2·KEYS_PER_SLICE` slots).
646pub const NODE_SLOT_BITS: usize = KEY_BITS + 1;
647
648const _: () = assert!(KEYS_PER_SLICE <= 1 << KEY_BITS, "key-in-slice numbers fit KEY_BITS");
649
650
651pub mod aes_table;
652pub mod axi4;
653pub mod blake3_core;
654pub mod cache_bank;
655pub mod cert_unit;
656pub mod certificate;
657pub mod chain_lane;
658pub mod chain_step;
659pub mod config;
660pub mod ed25519;
661pub mod engine;
662pub mod haraka;
663pub mod hbm_writer;
664pub mod lease;
665pub mod msg_reader;
666pub mod packer;
667#[cfg(feature = "planner")]
668pub mod planner;
669pub mod reference;
670pub mod relay;
671pub mod slice;
672pub mod slice_chain;
673pub mod slice_factory;
674pub mod slice_merkle;
675pub mod slice_model;
676pub mod slice_schedule;
677pub mod sbox_placement;
678pub mod slice_signer;
679pub mod verify;
680#[cfg(feature = "ubft")]
681#[doc = include_str!("../docs/ubft.md")]
682pub mod ubft;