Expand description
An all-RHDL DSig signer, and a verifier, whose throughput scales with the number of slices (signer) or units (verifier) they build.
§Building for Wolverine’s U280
The signing-engine flow generates RTL from this crate, then packages and links it on Wolverine. Cargo alone does not produce a card image.
Run commands from the workspace root. Before compilation or RTL generation,
record ownership in PROJECT_COORDINATION.md and
/home/sahasta/Development/FPGA_COORDINATION.md, and use the applicable
nonblocking local-heavy.lock; crates/ed25519/tools/bounded.sh wraps local
Cargo commands with that lock and resource limits. Reserve Wolverine builds
in both ledgers too, then use crates/dsig/tools/with-u280-lock.sh to hold
u280.lock. Do not bypass a busy lock.
Inside the reserved, locked build session, generate a fresh bundle (Rust 1.96 is the toolchain used by the repository’s DSig bounded runner):
export CARGO_TARGET_DIR="$PWD/target"
cargo build --locked --profile sim -p rhdl_dsig_scalable_core --bin dsig_engine_export
RHDL_HDL_ONLY=1 "$CARGO_TARGET_DIR/sim/dsig_engine_export" /absolute/path/new-bundleThe card build uses Vivado/Vitis 2023.2, the installed U280 .xpfm
platform and XRT. Replace the absolute example paths below with real paths
on Wolverine; the bundle must be copied there with its BUNDLE.txt intact,
and the output directory must be new. Keep the repository checkout available
on Wolverine because the scripts use its Tcl and host sources. Building an
image does not program the card or establish measured performance.
The build script requires 48 GiB available RAM and 30 GiB free disk. After recording Wolverine ownership in both ledgers, run there:
export HASHSIGS_U280_COORDINATION_ACK=both-ledgers-recorded
crates/dsig/tools/with-u280-lock.sh \
"$PWD/crates/dsig/tools/wolverine-engine.sh" build \
/absolute/path/new-bundle /absolute/path/new-build-output \
/absolute/path/U280-platform.xpfm \
/home/Xilinx/Vitis/2023.2/settings64.sh /opt/xilinx/xrt/setup.shSuccess produces dsig_engine.xclbin, a host runner, and
acceptance/route-accepted.txt after checking routing, setup/hold timing and
the kernel LUT cap. See crates/dsig/fpga/u280/engine/README.md for the
separate prepare/run/verify steps. Use a fresh run directory and key range
for every attempt, including after timeout or uncertain launch. Preserve
DSIG_FPGA_W4_HARAKA_BLAKE3_V1; a verified burst is not continuous throughput.
For the verifier and isolated uBFT signature-service probe, use
crates/dsig/tools/wolverine-verify.sh and wolverine-ubft.sh, respectively,
with the matching exporters and the README under fpga/u280/verify or
fpga/u280/ubft. The latter is a crypto-service probe, not the full
rhdl_ubft consensus implementation. Match host and bundle slice counts.
DSig signs each message with a fresh one-time hash-based key (W-OTS+, w = 4) and amortizes one Ed25519 signature over every batch of 128 keys. This crate is the hardware that does all of it on an Alveo U280. It makes every key, certifies every batch, signs every message and writes every 1,642-byte signature to HBM; and it checks signatures again at the same rate.
In numbers (October 2026). The 4-slice signer (build v13) signs
12,038,905 signatures per second on the card: a verified burst of
1,572,864 signatures from one cold start, 17.03 cycles per signature, the
4-slice ceiling of 4 × 205 MHz / 68. It routes at 205 MHz (WNS
+0.016 ns) in 388,542 kernel CLB LUT of the 420,000 cap. Every frame
is re-verified in software before a rate is printed. The 3-unit
verifier (build v3) checks those signatures on the card at at least
16,959,195 per second (1,572,864 verified valid; a tampered burst got
every verdict right), routed at 205 MHz in 368,713 LUT. No continuous
(back-to-back) rate is claimed for either side: every number is a finite
burst from reset.
| 2 slices | 4 slices | 8 slices | |
|---|---|---|---|
| Ceiling at 205 MHz (one key per 68 clocks per slice) | 6.0M sig/s | 12.1M sig/s | 24.1M sig/s |
| Measured on the card | 6,019,465 sig/s (v3) | 12,038,905 sig/s (v13) | model only |
| Kernel CLB LUT, routed at 205 MHz | 296,558 (v2) | 388,542 (v13) | model only |
Reproduce: tools/wolverine-engine.sh build then run (see
fpga/u280/engine/README.md); the simulation gates are in
Proving it.
§The story in five minutes
The problem. Ed25519 is the signature everyone trusts, and it is expensive: this project’s Ed25519 verify core checks about 279,000 per second (native simulation, 735 cycles each at 205 MHz). Some systems want tens of millions.
The bet. DSig signs each message with a one-time hash-based key and uses one Ed25519 signature to vouch for a whole batch of 128 such keys. Hashing is cheap in hardware, so if the hashes are fast enough the expensive part is paid once per 128 signatures.
The first version made keys one at a time and was slow. Then, one idea per step: skip the hash work whose inputs are constants (640 → 494 S-boxes per step); run a whole three-step chain every clock; give every BLAKE3 compression a fixed slot in a 68-cycle timetable; make keys before messages arrive; tag up to 32 certificate batches in flight; and copy the whole design four times as slices.
What broke was rarely the arithmetic. It was wires: a clock of 205 MHz leaves 4.9 ns per step, and a wire from one silicon die to the next eats a large part of it. Builds v5 to v13 were mostly about pinning blocks to dies and putting registers on every long wire.
Where it stands. The signer makes 12.04 million verified signatures per second on a real card, the most four slices can do. The verifier checks them on the card at 16.96 million per second, after a Vivado netlist bug in the Ed25519 core was found and designed around.
Where you come in. Eight slices (24M/s) need each slice at about half its size, the verifier needs a fourth unit, and nobody has measured continuous throughput. Small, checkable starting points are in Good first contributions.
This page tells the story of how the design came to be, one problem at a time: what we were trying to do, what stood in the way, how we reasoned about it, and what we built. Each chapter ends with the module that holds the details. Read it in order once. Afterwards, Where to go next sends you straight to the part you want to change.
§Contents
- The question: what does a DSig signature cost?
- Doing less work
- A whole chain every clock
- Feeding the chains: the 68-cycle timetable
- Keys before messages
- One Ed25519 signature per 128 keys
- More of everything: slices
- Meeting the clock
- Choosing the chain hash
- Living on a shared chip
- Proving it
- Checking signatures: the verifier
- Epilogue: your turn
§The map
Read the chapters below in order once; then these are the modules, in the order a newcomer should open them:
| Module | One line |
|---|---|
reference | the whole scheme in plain software: the answer key for every block |
chain_step, chain_lane | one chain step (Haraka-512 with 494 live S-boxes, or BLAKE3, SHA-512) |
slice_chain, slice_schedule, slice_factory | a whole chain per clock; the 68-cycle timetable; the key factory |
cache_bank, slice_signer, packer | the key cache and the signer that turns a message into a frame |
slice_merkle, cert_unit, certificate, ed25519 | subtrees, batch roots and the Ed25519 certificate |
slice, engine | one slice; the card kernel with its registers and HBM ports |
relay | registered links between blocks on different dies |
verify | the verifier: records in, verdict bytes out |
config, planner | cost and rate models for the shared chip planner and TUI |
§The cost model
Where the signer’s 388,542 LUT go (build v13, routed, measured):
| Block | CLB LUT | What dominates |
|---|---|---|
| 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 |
| certificate unit | 48,476 | Ed25519 signer 25.9k, BLAKE3 lane 16.3k |
| frame-beat relay pipes | 6,263 | one LUT per bit per stage |
| control, reader, writers, toggle links | ~2.4k |
The binding constraint is LUTs, not cycles: a slice already makes one key per 68 clocks with its lanes busy every clock, so throughput grows only with more slices, and slices come in powers of two (the Merkle subtree split). Block RAM is second (~270 tiles per slice with T-tables in rounds 3..6); DSP is unused except by Ed25519.
§Chapter 1: The question: what does a DSig signature cost?
The goal was blunt: 30 million measured signatures per second on one U280, inside a fixed LUT budget. Before designing anything we counted the work, because throughput = hardware ÷ work per signature, and we can attack both sides.
key generation (per one-time key, ahead of time)
seed, nonces 3 BLAKE3 compressions
68 chain secrets 20 BLAKE3 compressions (XOF)
68 hash chains 68 × 3 chain steps (Haraka-512) ◀ almost all the work
endpoint commitment 21 BLAKE3 compressions
Merkle leaf + parents ~1 BLAKE3 compression
certificate (per 128 keys)
Merkle top + envelope a few BLAKE3 compressions, 1 Ed25519 signature
signing (per message)
message digest 1 BLAKE3 compression → 68 base-4 digits
reveal one stored chain node per chain, 7 Merkle siblingsA W-OTS+ key has 68 hash chains: 64 carry a base-4 digit of the message digest and 4 carry a checksum. Each chain starts at a secret and is hashed three times. A signature reveals, for each chain, the node at the position of its digit. The verifier hashes the rest of the way and checks that it lands on the public endpoint.
So a key costs 204 chain steps and about 47 BLAKE3 compressions. Signing a message is cheap; making the key is the work. The conclusion that shaped everything: build a machine that makes keys at line rate and signs from a cache of them.
§Chapter 2: Doing less work
Problem. A Haraka-512 chain step is 40 AES rounds, 640 S-boxes. S-boxes are 256-entry tables. In LUTs they are the bulk of the area.
Reasoning. A chain block is not arbitrary input. Its layout is fixed: a 16-byte salted nonce, the 18-byte node, then 30 zero bytes. Every byte that is constant at compile time makes some S-box inputs constant, and a constant S-box is no S-box at all.
Decision. Track which bytes are known through every round and keep only the live S-boxes. 640 become 494, pinned by a test, so one signature is about 204 × 494 ≈ 100,800 S-box evaluations, and signatures per second ≈ S-boxes per second ÷ 100,800.
→ chain_step, haraka, aes_table
§Chapter 3: A whole chain every clock
Problem. A chain is three dependent steps. Hardware that computes one step and loops back would sit idle while each step travels through its pipeline.
Reasoning. The 68 chains of a key are independent of each other. If the three steps of a chain are three separate pipelined lanes in series, a new chain can enter every clock while earlier ones are still inside.
Decision. A chain engine is three chain-step lanes in series, each a fully pipelined Haraka-512 step (20 cycles). It finishes one whole chain per clock, all four node positions at once.
secret ──▶ [lane 1] ──▶ [lane 2] ──▶ [lane 3] ──▶ endpoint
│ │ │ │
└────────────┴────────────┴────────────┴──▶ 4 nodes → key cache§Chapter 4: Feeding the chains: the 68-cycle timetable
Problem. The chain engine wants a new chain secret every clock, so a key’s 68 chains take 68 clocks. Around them, the same key needs about 47 BLAKE3 compressions: seeds, the XOF that makes the secrets, and the commitment over the 68 endpoints. One BLAKE3 lane finishes a compression per clock but with a 28-cycle latency, and many of these compressions depend on each other.
Reasoning. If every key’s BLAKE3 work happens at the same fixed offsets, the timetable for one key repeats every 68 cycles. Overlapping keys then never collide, as long as no two operations of different keys land on the same cycle modulo 68. That is a modulo schedule, the classic way compilers pipeline loops.
Decision. A small solver, run at compile time, places every BLAKE3 operation of a key on its own residue mod 68 and respects every data dependency. Up to 16 keys are in flight at once, each in its own context (a slot in small RAMs). Result: one key per 68 cycles per slice, with the BLAKE3 lane and the chain engine both busy every clock.
cycle mod 68: 0 ......................................... 67
chain engine: chain 0, chain 1, ... chain 67 (key k)
BLAKE3 lane: XOF of key k+1, commitment of key k−1, seeds of key k+2 ...→ slice_schedule, slice_factory
§Chapter 5: Keys before messages
Problem. Messages arrive when they want. Keys come out at a steady rate. Signing must not wait for key generation, and a one-time key must never sign twice.
Reasoning. Separate the two with a cache. The factory fills it at its own pace; the signer takes one key per message. A key is spent the moment it is admitted to a message, and also on timeout, reset or any uncertain launch. It is never reused.
Decision. Each slice caches 512 keys (68 chains × 4 nodes each) in dense UltraRAM. The signer turns a message digest into 68 digits, reads one node per chain and streams the 1,642-byte frame.
→ cache_bank, slice_signer, packer
§Chapter 6: One Ed25519 signature per 128 keys
Problem. Every 128 keys need a Merkle tree and one Ed25519 signature over its root. The embedded Ed25519 core takes about 22,000 cycles per signature. One at a time, that caps the engine at one batch per 22,000 cycles, far below what the slices make.
Reasoning. Little’s law: work in flight = throughput × time in system. To finish one certificate every ~895 cycles with a 22,000-cycle latency, about 25 must be in flight.
Decision. The certificate unit tags each batch and keeps up to 32 in flight, then delivers certificates in order.
The batch below is DSig’s split in one picture: the hash-based one-time keys are the foreground that signs every message; one Ed25519 signature per 128 keys is the background that vouches for them.
→ cert_unit, ed25519, certificate
§Chapter 7: More of everything: slices
Problem. One slice makes 205 MHz ÷ 68 ≈ 3.0M keys per second. The goal is ten times that.
Reasoning. Chapters 3–6 made a slice that never idles. The only remaining lever is more of them. Each slice is a complete signer, so slices need nothing from each other except their share of each batch’s Merkle tree.
Decision. The engine builds SLICES (2, 4 or 8) copies. RHDL lowers
the slice once and instantiates it SLICES times. Each slice owns
128 / SLICES keys of every batch and builds that subtree; the
certificate unit joins the subtree roots.
┌──────────── slice (× SLICES) ────────────┐
seed, epoch ─────▶ │ factory: BLAKE3 lane + 3 chain lanes │
│ 68-cycle modulo schedule: 1 key / 68 clk│
│ key cache: 512 keys in UltraRAM │
│ subtree: K leaves → 1 root │──▶ HBM (frames)
messages (HBM) ──▶ │ signer: digits → nodes → 1,642-byte frame │
└───────────────▲──────────────┬────────────┘
│ certificate │ subtree root
┌───────────────┴──────────────▼────────────┐
│ cert unit: Merkle top + Ed25519 (≤32 in │
│ flight, tagged, in-order delivery) │
└────────────────────────────────────────────┘→ slice, slice_merkle, engine, SLICES
§Chapter 8: Meeting the clock
Problem. The first routed 2-slice link used 301,037 kernel CLB LUTs but failed timing at 205 MHz by 2.167 ns.
Reasoning. The per-hierarchy report pointed at the key caches. Vivado had mapped them to about 136 block RAMs per bank, spread across the die, with unregistered inputs fanning out to all of them.
Decision. Put the caches in UltraRAM (a RamStyle patch in the
vendored RHDL lets an RHDL memory ask for it), and register their
inputs. That frees block RAM, which the S-box placement can now use
(Haraka S-boxes in spare UltraRAM and block RAM instead of LUTs).
Result. The second link met timing at 205 MHz (setup and hold) with 296,558 kernel CLB LUTs.
The climb to four slices at 205 MHz took nine more builds (v4–v13): table rounds, a registered reset, hard per-die floorplans, registered links, kept hierarchy and toggle links. Each is a numbered step with its measurement in How the speed adds up.
→ cache_bank, sbox_placement, config
§Chapter 9: Choosing the chain hash
DSig works with any of three chain hashes. Each is a different signature scheme with its own profile name; signatures from one never verify under another. Only chain steps change; everything else stays BLAKE3.
| Build variable | Profile | Lane latency |
|---|---|---|
DSIG_CHAIN=haraka (default) | DSIG_FPGA_W4_HARAKA_BLAKE3_V1 | 20 |
DSIG_CHAIN=blake3 | DSIG_FPGA_W4_BLAKE3CHAIN_BLAKE3_V1 | 28 |
DSIG_CHAIN=sha512 | DSIG_FPGA_W4_SHA512CHAIN_BLAKE3_V1 | 82 |
A lesson from SHA-512. Its lanes are four times slower, so a key’s chain ends leave the engine 248 cycles after they start, more than three 68-cycle periods. Two assumptions broke. Keys stay in flight longer, so the factory needs 32 contexts instead of 16. And the commitment prefix was read from “the current key”, which by then is a newer key. The factory now keeps the last few keys’ nonces and picks by age. The Haraka hardware is unaffected.
§Chapter 10: Living on a shared chip
DSig shares the chip with Ed25519 signing and verification, and both draw on one shared SHA-512 line. A planner models every component’s cost and rate; one TUI, shared with the Ed25519 crate, lets you move budgets and see sign/s, verify/s, LUT, DSP and latency change.
In the DSig paper’s chip, Ed25519 does one signature per 128 DSig
signatures, and one verification per DSig verification whose batch root
is not already verified: 1 − h with h = (b/100) · 127/128, where b
is the best-case expectation.
→ config, planner (feature planner), dsig_plan
§Chapter 11: Proving it
§What the card taught us
Problem. The first card runs of the routed 2-slice kernel never finished. The kernel clock ran (about 205.2M ticks per second), but the counters froze: 6 of 8 batches certified, both key caches full, and the certificate unit’s sticky fault set.
Reasoning. Every block had passed bit-exact simulation, but the engine test signed one batch. With one batch, each slice offers one subtree root. With eight, each slice keeps making keys up to eight batches ahead and offers a root for every batch it finishes, while the certificate unit’s root queue holds two. During the Ed25519 key load nothing is popped, so the third root met a full queue and was dropped. The 8-batch simulation reproduces it: the same fault at cycle 16,271.
Decision. Root credit. The certificate unit reports how many
batches’ roots it has taken (cert_unit::CertOutput::roots_popped),
and a slice starts a key only while its batch is fewer than
cert_unit::ROOT_QUEUE ahead. The lesson for every test here: run at
least as many batches as any queue is deep, and check the fault register
every few thousand cycles.
§What is proven
What is proven, how, and what is not. “Measured” always names where.
| Claim | Kind | Evidence |
|---|---|---|
| Every signer block equals the software reference bit for bit (chain step, chain engine, factory keys, subtree, signer frames, packer) | tested | native RHDL simulation, tests/*.rs (the leaf gate, tools/leaf-gate.sh) |
A whole slice makes its keys from scratch; every frame equals DsigSignature::to_bytes() | tested | tests/slice.rs, 16 and 64 keys per slice |
| The certificate unit certifies with the real Ed25519 core; every certificate verifies under ed25519-dalek, in order | tested | tests/cert_unit.rs (3 batches, one slice lagging) |
| The whole engine signs end to end; all frames verify | tested | tests/engine.rs (128 signatures verified, fault 0, 46,059 cycles from cold start) |
| The kernel exports from RHDL alone, closure-checked | tested | dsig_engine_export (v13 bundle: 2,566 modules) |
| 4-slice kernel (v13) meets 205 MHz: 388,542 kernel CLB LUT of the 420,000 cap, WNS +0.016 ns, WHS +0.002 ns | measured | Vivado 2023.2 routed acceptance, Wolverine dsig-engine-20261017-v13/build-v13/acceptance |
| 12,038,905 signatures/s (v13) and 12,038,926/s (v12), 17.03 cycles/signature | measured | U280 card, verified 1,572,864-signature bursts from one cold start (ranges 14 and 11); every frame re-verified in software by dsig_engine_host verify |
| 2-slice kernel (v3): 6,019,465 signatures/s | measured | U280 card, 786,432-signature burst |
| Clock | measured | on-card tick counter against the host clock: 204.997–205.005 MHz in every run |
| Continuous (back-to-back) throughput | not measured | each burst is one cold start; no claim |
| Rates of 8 slices, other chain hashes, SHA-512 line | model | planner (config, planner) |
The verifier’s own evidence is in crate::verify.
§How the speed adds up
Every number below is marked measured (with where) or model. Card numbers are finite bursts from one cold start, each on a fresh key range; no continuous (back-to-back) rate is claimed.
The rule of thumb that guided every step: signatures per second ≈ S-boxes evaluated per second ÷ 100,800, because one signature’s key costs 204 chain steps of 494 live S-boxes. A step either evaluates more S-boxes per LUT, keeps them busy, or lets the design meet the 205 MHz clock at all.
- Fold the constants out of Haraka.
Problem: a Haraka-512 chain step is 640 S-boxes, the bulk of the area.
Idea: most input bytes of a chain block are fixed (salt layout, zero
padding), so their S-boxes are constants. Track known bytes through
every round. Result: 640 → 494 live S-boxes per step (pinned by
structural_sbox_count_is_pinned). →chain_step - A whole chain every clock.
Problem: a chain is three dependent steps; one looping lane would idle.
Idea: three pipelined lanes in series, one chain enters per clock.
Result: one 3-step chain per clock per slice (simulation, bit-exact).
→
slice_chain - A 68-cycle modulo schedule.
Problem: ~47 dependent BLAKE3 compressions per key must share one
28-cycle lane without collisions. Idea: give every compression a fixed
offset, solved at compile time so no two share a residue mod 68.
Result: one key per 68 cycles per slice, lane and chain engine both
busy every clock. →
slice_schedule - Pipelined certificates.
Problem: the Ed25519 core needs ~22,000 cycles per signature; one batch
at a time caps the engine. Idea: Little’s law: tag up to 32 batches in
flight. Result: the certificate path stops being a limit
(
cert-serialfeature keeps the old design for measurement). →cert_unit - UltraRAM key caches (build v1 → v2).
Problem: the first 2-slice link missed 205 MHz by 2.167 ns
(measured, routed): ~136 block RAMs per cache bank, inputs unregistered.
Idea: caches in UltraRAM, registered inputs. Result: build v2 met
205 MHz at 296,558 kernel LUT (measured, routed).
→
cache_bank - Root credit (v2 → v3), a correctness fix found on the card.
Problem: card runs froze after 6 of 8 batches (FAULT set); a root
queue of two dropped roots during the Ed25519 key load. The one-batch
simulation had hidden it. Idea: slices start a key only while their
batch is fewer than
ROOT_QUEUEahead. Result: build v3 (2 slices): 6,019,465 signatures/s on a verified 786,432-signature burst, 34.06 cycles/signature, the 2-slice ceiling (measured, card). →cert_unit::ROOT_QUEUE - T-tables, so four slices fit (v4 → v12).
Problem: four slices with LUT S-boxes would need ~425k LUT (model)
over the 420k cap. Idea: compute whole AES rounds with block-RAM
T-tables (rounds 3..6,
DSIG_TABLE_MASK=0x078). Rejected: rounds 3..8 (0x1f8): v4 failed placement (4,033 BRAM18 needed, 3,552 available), and its RAM S-boxes would not fit two slices per SLR. Result: a slice of ~83k LUT (measured, v12 routed). →chain_step::TableChainStepLane - Registered resets and resetless datapaths (v5 → v6).
Problem: v5 missed by −0.213 ns; about half its failing paths
started at the platform reset, which fanned out to ~1.2M endpoints.
Idea: register and replicate the reset; datapath registers without
reset (
DFF::resetless). Result: the reset paths left the top list, but v6 got worse (−0.916 ns) for the next reason. - Pin each slice to one die (v6 → v8).
Problem: v6 split slices across the SLR1/SLR2 boundary: 0.4–0.5 ns of
clock skew on top of the crossing. Rejected: soft
USER_SLR_ASSIGNMENT(v8): the placer ignored it (−0.359 ns). Idea: the platform’s hard per-SLR pblocks, with a block-RAM fit check first. →fpga/u280/engine/floorplan.tcl - Register every block boundary (v9 → v10).
Problem: pinning made every combinational handshake a die crossing:
v9 −4.715 ns. Idea: Carloni relay pipes on handshakes, two-register
delay lines elsewhere. →
relay - Keep block hierarchy (v10 → v11 → v12).
Problem: v10 failed placement: “48,267 SLLs required between SLR0 and
SLR1 of 23,040”. Synthesis (
-flatten_hierarchy rebuilt) had merged identical logic across slices, so pblocks no longer held what they named. Idea:keep_hierarchyon every block and slice; then (v11’s[VPL INBB-3]black box) drop RHDL’s empty output-less modules. Result: build v12 met 205 MHz (WNS +0.008 ns) at 410,004 kernel LUT and signed 12,038,926 signatures/s on a verified 1,572,864-signature burst, 17.03 cycles/signature, the 4-slice ceiling (measured, card). →src/bin/dsig_engine_export.rs - Toggle links for wide, rare items (v12 → v13).
Problem: relay stations cost a LUT per bit per stage; the 1,345-bit
certificate links alone took 19,037 LUT (measured, v12). Idea: a
two-phase toggle handshake: data moves through plain registers, one
bit carries the handshake; certificates and messages are rare enough.
Result: links 21,031 → 31 LUT; kernel 388,542 LUT (−21,462), WNS
+0.016 ns, and the same 12,038,905 signatures/s (measured, card).
→
relay::ToggleLink
What did not change throughput: steps 8–12 were about meeting the clock
and the LUT cap. The 4-slice ceiling, 4 × 205 MHz / 68 = 12.06M/s, was
reached by v12. v13’s 31.5k LUT of headroom is far from the next power of
two: 8 slices (~24.1M/s) would need slices of ~46k LUT instead of ~83k.
The longer design walk-through, with every budget and decision, is
sustained.
§Chapter 12: Checking signatures: the verifier
Problem. A signature is only worth what it costs to check, and the comparison paper prices DSig verification next to Ed25519’s on the same chip. The planner’s verify numbers were a model.
Reasoning. Checking is the signer’s work run backwards and halved:
each chain needs only 3 − digit steps (105 of 204 on average), plus 29
BLAKE3 compressions and one Ed25519 check per batch of 128. The same
lanes can do it if they never idle on chains that need no step.
Decision. Verify units of three recirculating chain lanes and one BLAKE3 lane shared by 64 records in flight, a certificate cache in front of the Ed25519 project’s verify core, and records in, verdict bytes out, in order.
Result. 35.12 cycles per verification per unit in native simulation, 80,211 LUT per core routed alone at 205 MHz; on the card, three units verify 16,959,195 signatures per second (build v3, finite burst of 1,572,864).
→ verify (its page tells the verifier’s own story)
§Epilogue: your turn
§Using it
The whole kernel is one RHDL Synchronous
component, engine::Engine. Its size and chain hash are chosen at
compile time:
DSIG_SLICES=2 cargo test --profile sim
cargo run --profile sim --bin dsig_engine_export -- NEW_BUNDLE_DIR # Verilog bundle
cargo run --release --bin dsig_plan -- --sweep # every buildThe U280 flow (XO packaging, v++ link at 205 MHz, routed acceptance,
card run) is in fpga/u280/engine/README.md.
§How to work
| Level | Command | Time |
|---|---|---|
| Leaf blocks, bit-exact vs reference | DSIG_SLICES=2 DSIG_CHAIN=haraka bash tools/leaf-gate.sh (inside tools/dsig-bounded.sh) | ~25 min |
| Sustained throughput | DSIG_SLICES=2 cargo test --profile sim --test throughput -- --ignored --nocapture | ~30 min |
| Verilog bundle + closure check | DSIG_SLICES=2 bash tools/engine-bundle-gate.sh NEW_DIR | ~5 min, 4.6 GB |
| Vivado (U280 host) | tools/wolverine-engine.sh ooc / build / run / report | hours |
RHDL kernels must use literal or const loop bounds and const array
indices, keep Bits ≤ 128 bits, give a struct at most ~11 synchronous
children, and avoid / and %. These compile under rustc and fail only
when lowering. Inside #[kernel] bodies, use // comments, never ///.
Never run cargo fmt over vendor/ (each snapshot carries a guard).
§Open problems
- Continuous throughput. Every card number is a finite burst from one cold start; back-to-back runs are not measured.
- Past 12M signatures/s. The next slice count is 8 (24.1M/s), which needs slices of ~46k LUT instead of ~83k: S-boxes in block RAM and UltraRAM, BLAKE3 adders in DSPs, a smaller signer.
- A fourth verify unit. Three units (368,713 LUT) verify 16.96M/s
on the card; a fourth needs each unit ~10k LUT smaller (see
verify). - The SHA-512 line above 1 block per clock (goal 1.75) in 65k LUT
and 20 DSP: see
rhdl_sha512_pipelined. - More S-boxes per LUT. BLAKE3 adders in DSPs and T-tables in URAM are estimates in the planner today.
§Good first contributions
Each of these is open today and has a test or a measurement that says when it is done.
| Task | Size | What you’ll learn | You’re done when |
|---|---|---|---|
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 |
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 |
| 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 |
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 |
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 |
| 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 |
| 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 |
§Where to go next
| You want to… | Start here | Then |
|---|---|---|
| make key generation faster or smaller | chain_step | aes_table, sbox_placement, config |
| change the chain hash | chain_lane | slice_factory (contexts, PACK_HISTORY), haraka |
| change the per-key timetable | slice_schedule | slice_factory |
| change how messages are signed | slice_signer | packer, cache_bank |
| change batching or certificates | cert_unit | slice_merkle, certificate, ed25519 |
| change the card interface (HBM, registers) | engine | msg_reader, hbm_writer, axi4 |
| run on the card | fpga/u280/engine/README.md | lease, dsig_engine_host |
| check a signature in software | reference | slice_model |
| check signatures in hardware | verify | verify::datapath, dsig_verify_host |
| change the planner or TUI numbers | config | planner (feature planner) |
| understand a number on this page | Proving it | docs/ |
§Glossary
| Term | Meaning |
|---|---|
| Slice | one complete signer; the engine has SLICES of them |
| Batch | 128 one-time keys sharing one Merkle root and one Ed25519 certificate |
| Chain | one of a key’s 68 W-OTS+ hash chains: a secret hashed three times |
| Chain step | one compression of a salted chain node by the chain hash |
| Context | one in-flight key’s slot in the factory’s small RAMs |
| Modulo schedule | a fixed per-residue timetable repeated every 68 cycles |
| Little’s law | in flight = throughput × time in system |
| Profile | the name of the exact scheme compiled in (chain hash included) |
| Frame | one 1,642-byte signature plus its message index, padded to 2 KiB |
Best case b | how often a DSig verification finds its batch already verified |
| SLR | one of the U280’s three dies; crossing between them costs time |
| Pblock | a placement region; the platform’s per-SLR pblocks pin blocks to a die |
| Relay pipe | registers on a valid/ready link so it can cross a die (Carloni) |
| Toggle link | a cheaper registered link for rare items: one bit flips per item |
| Ceiling | the rate a build reaches if every slice makes one key per 68 clocks |
| Verify unit | one HBM reader, one verify core, one verdict writer |
| Record | a signature plus its message, the verifier’s input (1,664 bytes) |
Re-exports§
pub use chain_lane::PROFILE;
Modules§
- aes_
table - AES T-table ROM: one BRAM port replaces one S-box and its MixColumns share.
- axi4
- Packed 512-bit AXI4 memory-master channels for the U280 HBM ports.
- blake3_
core - BLAKE3 compression and a fully pipelined Rust/RHDL implementation.
- cache_
bank - One bank of a slice’s key cache: 34,816 rows of one 18-byte chain node.
- cert_
unit - The engine’s only shared logic: the Merkle top and the Ed25519 certificate.
- certificate
- Versioned, domain-separated Ed25519 batch authentication.
- chain_
lane - The chain hash: Haraka-512, BLAKE3 or SHA-512, chosen at compile time.
- chain_
step - A Haraka lane specialised to exactly one DSig chain step.
- config
- The planner: predicts signatures per second and resources for every build and picks the best one for a LUT budget.
- ed25519
- Direct DSig certificate bridge
- engine
- The sustained-signing RTL kernel: eight slices, one certificate unit, HBM.
- haraka
- Haraka-512 v2 and the 144-bit DSig chain-compression primitive.
- hbm_
writer - Write a slice’s frames into HBM, one 26-beat burst per frame.
- lease
- Durable namespace reservation before deriving or loading one-time keys.
- msg_
reader - Read 16-byte messages from HBM and deal them to the
SLICESslices. - packer
- Pack 18-byte chain nodes into 64-byte words.
- planner
- DSig as a component of the shared chip planner and TUI
([
rhdl_fpga_planner]), next to Ed25519 Sign/Verify and the shared SHA-512. - reference
- Complete byte-oriented reference for the explicitly versioned DSig profile.
- relay
- Relay stations and delay lines: registers that let slice traffic cross dies.
- sbox_
placement - Where each AES S-box lives: LUTs, block RAM or UltraRAM.
- slice
- One complete slice: factory, subtree builder, signer and key cache.
- slice_
chain - A slice’s chain engine: one chain per cycle through three folded lanes.
- slice_
factory - A slice’s key factory: one complete one-time key every 68 cycles.
- slice_
merkle - A slice’s share of the batch Merkle tree: one subtree per batch.
- slice_
model - Bit-exact software model of every compression a slice issues.
- slice_
schedule - The fixed timetable that lets one BLAKE3 lane serve a whole slice.
- slice_
signer - A slice’s signer: one complete 1,642-byte signature every 68 cycles.
- sustained
- The full design walk-through: budgets, slices, caches and decisions.
- ubft
- DSig as the signature service of a uBFT
replica: the same slices, certificate unit and verify core as the batch
kernels, behind four AXI4-Stream ports, so a consensus engine on the same
U280 can sign and verify in microseconds instead of milliseconds. Built
with the
ubftfeature; the hardware iscrate::ubft. - verify
- The DSig verifier: checks signatures at the rate the signer makes them.
Constants§
- BATCH_
KEYS - Number of public keys authenticated by one Ed25519 certificate.
- CHAINS
- Number of independently salted WOTS hash chains.
- CHECKSUM_
DIGITS - Number of little-endian base-four complement-checksum digits.
- ELEMENT_
BYTES - Number of bytes per chain node (144 bits).
- KEYS_
PER_ SLICE - Keys of each batch made, cached and signed by one slice.
- KEY_
BITS - Width of a key’s number within its slice’s batch share (
j) and of a Merkle node’s index within its level: 6 bits in every batch-engine build (up to 64 keys per slice), more only when a slice makes larger batch shares. Kept at 6 rather than shrunk so the measured builds’ hardware does not change. - KEY_ONE
1as aKeyIndex.- KEY_
ZERO 0as aKeyIndex.- MERKLE_
HEIGHT - Number of siblings in an inclusion proof for a fixed-size batch.
- MESSAGE_
BYTES - Bytes of a signed message (
DSIG_MESSAGE_BYTES, default 16). - MESSAGE_
DIGITS - Number of independent message digits, taken from low bits of XOF bytes.
- NODE_
LEVELS - Merkle levels that have a node base (
0..=KEY_BITS). - NODE_
SLOT_ BITS - Width of a node slot within one ring entry (
2·KEYS_PER_SLICEslots). - POSITIONS
- Number of stored positions, including each secret and its public endpoint.
- SEED_
DERIVATION_ CONTEXT - Frozen derivation domain shared by hardware builders and the host oracle.
- SLICES
- Slices in the engine (
DSIG_SLICES, default 8). - SUBTREE_
LEVELS - Merkle levels inside a slice’s subtree (
log2 KEYS_PER_SLICE). - TOP_
LEVELS - Merkle levels joined by the certificate unit (
log2 SLICES). - VERIFY_
UNITS - Verify units in the verifier kernel (
DSIG_VERIFY_UNITS, default 4).