Skip to main content

Crate rhdl_dsig_scalable_core

Crate rhdl_dsig_scalable_core 

Source
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-bundle

The 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.sh

Success 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 slices4 slices8 slices
Ceiling at 205 MHz (one key per 68 clocks per slice)6.0M sig/s12.1M sig/s24.1M sig/s
Measured on the card6,019,465 sig/s (v3)12,038,905 sig/s (v13)model only
Kernel CLB LUT, routed at 205 MHz296,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

  1. The question: what does a DSig signature cost?
  2. Doing less work
  3. A whole chain every clock
  4. Feeding the chains: the 68-cycle timetable
  5. Keys before messages
  6. One Ed25519 signature per 128 keys
  7. More of everything: slices
  8. Meeting the clock
  9. Choosing the chain hash
  10. Living on a shared chip
  11. Proving it
  12. Checking signatures: the verifier
  13. 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:

ModuleOne line
referencethe whole scheme in plain software: the answer key for every block
chain_step, chain_laneone chain step (Haraka-512 with 494 live S-boxes, or BLAKE3, SHA-512)
slice_chain, slice_schedule, slice_factorya whole chain per clock; the 68-cycle timetable; the key factory
cache_bank, slice_signer, packerthe key cache and the signer that turns a message into a frame
slice_merkle, cert_unit, certificate, ed25519subtrees, batch roots and the Ed25519 certificate
slice, engineone slice; the card kernel with its registers and HBM ports
relayregistered links between blocks on different dies
verifythe verifier: records in, verdict bytes out
config, plannercost 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):

BlockCLB LUTWhat dominates
4 slices4 × ~82.8k = 331,450per slice: key factory 65k (three Haraka lanes ~12k each, BLAKE3 lane 16.7k), signer 16.7k, subtree 1k
certificate unit48,476Ed25519 signer 25.9k, BLAKE3 lane 16.3k
frame-beat relay pipes6,263one 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 siblings

A 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.

W-OTS+ (w = 4): one one-time key is 68 hash chains The hash-based one-time signature (HBSS) in DSig's foreground. Each chain is an 18-byte secret hashed 3 times. message digestBLAKE3-XOF(pk_hash ‖ nonce ‖ M), 64 bytes 2 0 3 1 … 2 1 0 2 1 64 message digits (low 2 bits per byte) 4 checksum digits Σ (3 − d) ≤ 192 = 68 digits, each 0..3 one digit per chain depth 0: secret depth 1 depth 2 depth 3: endpoint chain 0 · d = 2 chain 1 · d = 0 chain 2 · d = 3 chain 64 · d = 1 (checksum) secret node 1 node 2 endpoint secret node 1 node 2 endpoint secret node 1 node 2 endpoint ⋮ (68 chains in all) secret node 1 node 2 endpoint endpointcommitment BLAKE3(pk_nonce ‖end₀ ‖ … ‖ end₆₇) 16 + 68 × 18= 1,240 bytes 21 compressions pk_hash (32 B)= Merkle leaf revealed in the signature: the node at depth = digit (68 × 18 B) never revealed: stays in the signer's key cache chain step made at key generation (68 × 3 = 204 per key, ahead of time) the verifier redoes these: 3 − d steps per chain, ~105 per signature Chain step: a 64-byte block (salted pk_nonce ‖ node ‖ zeros) → 18 bytes. Haraka-512 by default; BLAKE3 or SHA-512 by feature.

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

→ slice_chain, chain_lane

§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.

A batch: 128 one-time keys, one Ed25519 signature Foreground: hash-based one-time keys (W-OTS+) sign the messages. Background: one Ed25519 signature over the batch's Merkle root vouches for all 128 keys at once. FOREGROUND · HBSS · per key and per message BACKGROUND · EdDSA · per batch ↑ cert unit · ↓ slices root 128 leaves: pk_hash of keys 0 … 127 Merkle tree of height 7: 127 BLAKE3 parents per batch. A signature carries its key's 7 path siblings (orange), 7 × 32 B. 8 slices: each builds a 16-leaf subtree; the cert unit the top 3 levels. batch envelope (64 B, what is signed)DSIGFP01 ‖ profile ‖ 128 ‖ epoch ‖ batch_id ‖ root Ed25519 sign1 per 128 keys · ~895 clk each, ≤ 32 in flight certificate = envelope ‖ R ‖ S (128 B)copied into all 128 signatures of the batch 1/K = 1/128 Ed25519 signature per DSig signature One DSig signature on the wire: 1,642 bytes (bar to scale) 7 × 32 B 68 revealed W-OTS+ nodes × 18 B = 1,224 B 194 fixed bytes: envelope 64 + Ed25519 R ‖ S 64 + key index 2 + pk_hash 32 + pk_nonce 16 + message nonce 16 1,642 = 68 × 18 + 7 × 32 + 194. The hardware writes it, plus the message index, as 26 beats of a 2 KiB HBM frame slot. background (per batch) index, pk_hash, nonces Merkle siblings foreground one-time signature

→ 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 variableProfileLane latency
DSIG_CHAIN=haraka (default)DSIG_FPGA_W4_HARAKA_BLAKE3_V120
DSIG_CHAIN=blake3DSIG_FPGA_W4_BLAKE3CHAIN_BLAKE3_V128
DSIG_CHAIN=sha512DSIG_FPGA_W4_SHA512CHAIN_BLAKE3_V182

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.

→ chain_lane, slice_factory

§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.

ClaimKindEvidence
Every signer block equals the software reference bit for bit (chain step, chain engine, factory keys, subtree, signer frames, packer)testednative 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()testedtests/slice.rs, 16 and 64 keys per slice
The certificate unit certifies with the real Ed25519 core; every certificate verifies under ed25519-dalek, in ordertestedtests/cert_unit.rs (3 batches, one slice lagging)
The whole engine signs end to end; all frames verifytestedtests/engine.rs (128 signatures verified, fault 0, 46,059 cycles from cold start)
The kernel exports from RHDL alone, closure-checkedtesteddsig_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 nsmeasuredVivado 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/signaturemeasuredU280 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/smeasuredU280 card, 786,432-signature burst
Clockmeasuredon-card tick counter against the host clock: 204.997–205.005 MHz in every run
Continuous (back-to-back) throughputnot measuredeach burst is one cold start; no claim
Rates of 8 slices, other chain hashes, SHA-512 linemodelplanner (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.

  1. 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
  2. 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
  3. 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
  4. 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-serial feature keeps the old design for measurement). → cert_unit
  5. 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
  6. 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_QUEUE ahead. 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
  7. 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
  8. 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.
  9. 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
  10. 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
  11. 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_hierarchy on 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
  12. 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 build

The U280 flow (XO packaging, v++ link at 205 MHz, routed acceptance, card run) is in fpga/u280/engine/README.md.

§How to work

LevelCommandTime
Leaf blocks, bit-exact vs referenceDSIG_SLICES=2 DSIG_CHAIN=haraka bash tools/leaf-gate.sh (inside tools/dsig-bounded.sh)~25 min
Sustained throughputDSIG_SLICES=2 cargo test --profile sim --test throughput -- --ignored --nocapture~30 min
Verilog bundle + closure checkDSIG_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 / reporthours

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

  1. Continuous throughput. Every card number is a finite burst from one cold start; back-to-back runs are not measured.
  2. 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.
  3. 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).
  4. The SHA-512 line above 1 block per clock (goal 1.75) in 65k LUT and 20 DSP: see rhdl_sha512_pipelined.
  5. 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.

TaskSizeWhat you’ll learnYou’re done when
Draw the verify unit (reader, core, writer) as an SVG for docs/verifier.mdan hourhow a record flows through the verifierthe diagram matches verify::unit and renders in cargo doc
Report each verdict class (format, Merkle, chains, certificate) separately in dsig_verify_host checka few hoursthe record format and the oracle verify::expected_verdictthe 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 unita few hourshow the planner and TUI turn measurements into plansVERIFY_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 dayhow RHDL lowers a design to Verilogthe test passes under tools/dsig-bounded.sh in normal class
Simulate the verifier with BLAKE3 chains (DSIG_CHAIN=blake3)dayshow the chain hash plugs into lanes and ringstests/verify_datapath.rs passes with BLAKE3 records
Shrink a verify unit by ~10k LUT so four fit under 420kweekswhere LUTs go in a routed designa routed 4-unit kernel at or under the cap
Measure continuous signing: back-to-back bursts without a resetweeks, needs the cardthe card flow and one-time-key safetya measured rate over consecutive verified bursts, reported apart from the burst rate

§Where to go next

You want to…Start hereThen
make key generation faster or smallerchain_stepaes_table, sbox_placement, config
change the chain hashchain_laneslice_factory (contexts, PACK_HISTORY), haraka
change the per-key timetableslice_scheduleslice_factory
change how messages are signedslice_signerpacker, cache_bank
change batching or certificatescert_unitslice_merkle, certificate, ed25519
change the card interface (HBM, registers)enginemsg_reader, hbm_writer, axi4
run on the cardfpga/u280/engine/README.mdlease, dsig_engine_host
check a signature in softwarereferenceslice_model
check signatures in hardwareverifyverify::datapath, dsig_verify_host
change the planner or TUI numbersconfigplanner (feature planner)
understand a number on this pageProving itdocs/

§Glossary

TermMeaning
Sliceone complete signer; the engine has SLICES of them
Batch128 one-time keys sharing one Merkle root and one Ed25519 certificate
Chainone of a key’s 68 W-OTS+ hash chains: a secret hashed three times
Chain stepone compression of a salted chain node by the chain hash
Contextone in-flight key’s slot in the factory’s small RAMs
Modulo schedulea fixed per-residue timetable repeated every 68 cycles
Little’s lawin flight = throughput × time in system
Profilethe name of the exact scheme compiled in (chain hash included)
Frameone 1,642-byte signature plus its message index, padded to 2 KiB
Best case bhow often a DSig verification finds its batch already verified
SLRone of the U280’s three dies; crossing between them costs time
Pblocka placement region; the platform’s per-SLR pblocks pin blocks to a die
Relay piperegisters on a valid/ready link so it can cross a die (Carloni)
Toggle linka cheaper registered link for rare items: one bit flips per item
Ceilingthe rate a build reaches if every slice makes one key per 68 clocks
Verify unitone HBM reader, one verify core, one verdict writer
Recorda 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 SLICES slices.
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 ubft feature; the hardware is crate::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
1 as a KeyIndex.
KEY_ZERO
0 as a KeyIndex.
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_SLICE slots).
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).

Type Aliases§

Digest
Host-side 256-bit digest.
Element
Host-side byte-oriented chain element.
KeyIndex
A key’s number within its slice’s batch share (KEY_BITS wide).
Nonce
Host-side 128-bit salt; message nonces stay signer-private until admission.