Skip to main content

rhdl_ed25519_scalable_core/
lib.rs

1//! An all-RHDL Ed25519 signer and known-key verifier whose throughput scales
2//! with its LUT and DSP budget.
3//!
4//! # Building for Wolverine's U280
5//!
6//! Generate the card bundle from Rust/RHDL, then package and link it with Vitis.
7//! The `card` feature enables the exporter; `export_scalable` alone exports a
8//! core rather than the complete card kernel.
9//!
10//! Run commands from the workspace root. Before compilation or RTL generation,
11//! record ownership in `PROJECT_COORDINATION.md` and
12//! `/home/sahasta/Development/FPGA_COORDINATION.md`, and use the applicable
13//! nonblocking `local-heavy.lock`; `crates/ed25519/tools/bounded.sh` wraps local
14//! Cargo commands with that lock and resource limits. Reserve Wolverine builds
15//! in both ledgers too, then use `crates/dsig/tools/with-u280-lock.sh` to hold
16//! `u280.lock`. Do not bypass a busy lock.
17//!
18//! Inside the reserved, locked build session:
19//!
20//! ```sh
21//! export CARGO_TARGET_DIR="$PWD/target"
22//! cargo build --locked --profile sim -p rhdl_ed25519_scalable_core \
23//!   --features card --bin export_card
24//! "$CARGO_TARGET_DIR/sim/export_card" /absolute/path/new-bundle
25//! ```
26//!
27//! Select `ED25519_SCALABLE_PROFILE` before compilation if changing the signer
28//! configuration, and retain the resulting profile in `BUNDLE.txt`.
29//!
30//! The card build uses Vivado/Vitis 2023.2, the installed U280 `.xpfm`
31//! platform and XRT. Replace the absolute example paths below with real paths
32//! on Wolverine; the bundle must be copied there with its `BUNDLE.txt` intact,
33//! and the output directory must be new. Keep the repository checkout available
34//! on Wolverine because the scripts use its Tcl and host sources. Building an
35//! image does not program the card or establish measured performance.
36//!
37//! The Ed25519 link script requires at least 40 GiB free disk. After recording
38//! Wolverine ownership in both ledgers, run there:
39//!
40//! ```sh
41//! export HASHSIGS_U280_COORDINATION_ACK=both-ledgers-recorded
42//! crates/dsig/tools/with-u280-lock.sh \
43//!   "$PWD/crates/ed25519/fpga/u280/card/wolverine-card.sh" build \
44//!   /absolute/path/new-bundle /absolute/path/new-build-output \
45//!   /absolute/path/U280-platform.xpfm \
46//!   /home/Xilinx/Vitis/2023.2/settings64.sh
47//! ```
48//!
49//! This produces `ed25519_card.xclbin`, `xclbin.sha256` and `STEPS.txt`.
50//! The script packages and links only; review routed timing/utilization and
51//! complete the host verification flow before claiming hardware success or a
52//! rate. See `docs/ed25519-card-evidence-journey.md` in the repository for the
53//! card evidence requirements. Card programming is a separate operation.
54//!
55//! **In numbers.** Each row below is one build. LUT, DSP and WNS come from
56//! Vivado 2023.2 out of context, routed at 4.878 ns (205 MHz). Rates come
57//! from native simulation, where every signature equals `ed25519-dalek`.
58//!
59//! | Build | Rate at 205 MHz | Cycles/op | CLB LUT | DSP | WNS (ns) |
60//! |---|---:|---:|---:|---:|---:|
61//! | small signer [`Ed25519SignCoreSmall`] (1/1/1/0) | 229k sign/s | 895 | 29,055 | 248 | +0.306 |
62//! | signer 8/0/2/1 (the 100 % plan of 2026-10-17) | 4.41M sign/s | 46.46 | 148,691 | 2,071 | +0.218 |
63//! | signer 12/0/3/1 (planned for 200 %) | 6.42M sign/s | 31.95 (before the timing fixes) | 254,817 | 2,994 | **+0.050** |
64//! | known-key verifier 6/0/1/1 | 1.52M verify/s | 134.81 | 150,200 | 1,823 | +0.043 |
65//! | known-key verifier 1/1/1/1 (DSig's certificate misses) | 0.279M verify/s, 257 µs alone | 735 | 82,184 | 698 | +0.094 |
66//! | signer 15/11/3/1 (largest simulated) | 7.76M sign/s | 26.43 | ~264k (synthesis only) | 3,669 | not routed |
67//!
68//! The 0 in a profile means the core hashes on the shared SHA-512 line.
69//! [Chapter 10](#chapter-10-meeting-205-mhz) tells how these builds met
70//! timing. `measure/results.tsv` and `measure/native-results.tsv` in the
71//! repository hold every number.
72//!
73//! Verification is [`KnownKeyVerifyCore`]: strict Ed25519 verification for
74//! up to eight public keys loaded in advance, equal to `ed25519-dalek`'s
75//! `verify_strict` on every case we test.
76//!
77//! **The story in one breath.** A signature is about 391 multiplications of
78//! 255-bit numbers. So we built a multiplier that starts one every clock, gave
79//! it 32 signatures to take turns on so it never waits, and wrote the
80//! algorithms as Rust programs that compile into the machine. Then we cut the
81//! multiplications per signature, kept more signatures in flight, and put one
82//! register at a time into the paths that missed 205 MHz. Every step was
83//! measured before we believed it, and each chapter below is one of those
84//! steps. The last chapter is the one you can write.
85//!
86//! This page tells the story of how the design came to be, one problem at a
87//! time: what we were trying to do, what stood in the way, how we reasoned
88//! about it, and what we built. Each chapter ends in the module that holds
89//! the details. Read it in order once; afterwards the
90//! [validation](#chapter-11-proving-it) table and the
91//! [open problems](#epilogue-your-turn) are the parts you will come back to.
92//!
93//! # Contents
94//!
95//! 1. [The question: what does a signature cost?](#chapter-1-the-question-what-does-a-signature-cost)
96//! 2. [A multiplier made of DSPs](#chapter-2-a-multiplier-made-of-dsps)
97//! 3. [Keeping the multiplier busy](#chapter-3-keeping-the-multiplier-busy)
98//! 4. [Telling the machine what to do](#chapter-4-telling-the-machine-what-to-do)
99//! 5. [Doing less work](#chapter-5-doing-less-work)
100//! 6. [More hardware did not help](#chapter-6-more-hardware-did-not-help)
101//! 7. [Measuring honestly](#chapter-7-measuring-honestly)
102//! 8. [Verification](#chapter-8-verification)
103//! 9. [Living on a shared chip](#chapter-9-living-on-a-shared-chip)
104//! 10. [Meeting 205 MHz](#chapter-10-meeting-205-mhz)
105//! 11. [Proving it](#chapter-11-proving-it)
106//! 12. [Beside a consensus protocol](#chapter-12-beside-a-consensus-protocol)
107//! 13. [Epilogue: your turn](#epilogue-your-turn)
108//! 14. [Where to go next](#where-to-go-next)
109//! 15. [Glossary](#glossary)
110//!
111//! # Chapter 1: The question: what does a signature cost?
112//!
113//! The goal was blunt: as many Ed25519 signatures per second as one FPGA can
114//! produce at 205 MHz, scaling smoothly with however many LUTs and DSPs we
115//! are given. Before designing anything we counted the work, because
116//! *throughput = hardware / work per signature*, and we can attack both.
117//!
118//! ```text
119//! r = SHA-512(prefix || M) mod l      1 SHA-512 block, 1 scalar job
120//! R = [r]B, encoded                   fixed-base multiplication + inversion
121//! k = SHA-512(R || A || M)            2 SHA-512 blocks
122//! S = k * a + r mod l                 1 scalar job          signature = R || S
123//! ```
124//!
125//! `[r]B` "multiplies" the fixed curve point `B` by the secret `r`. It is
126//! almost all of the arithmetic: 378 field multiplications (modulo
127//! `2^255 - 19`), plus about 13 more to encode `R` when 32 points share one
128//! inversion. The hashing and the small `mod l` jobs are cheap by
129//! comparison and run on their own small hardware.
130//!
131#![doc = concat!("<div style=\"overflow-x:auto\">", include_str!("../docs/images/signature-cost.svg"), "</div>")]
132//!
133//! **The conclusion that shaped everything:** signatures per second ≈ field
134//! multiplications per second ÷ 391. So the design is, at heart, a way to
135//! deliver as many field multiplications per clock as the chip allows and
136//! to waste none of them. Every later chapter is one of those two halves:
137//! *more multiplications per clock*, or *fewer multiplications per
138//! signature*.
139//!
140//! # Chapter 2: A multiplier made of DSPs
141//!
142//! **Problem.** A 255-bit multiplication in LUTs is enormous and slow. The
143//! FPGA has thousands of DSP48E2 blocks, each a hardened 27 × 18 multiplier
144//! with a 48-bit accumulator and a dedicated cascade to its neighbor.
145//!
146//! **Reasoning.** If the multiplication can be phrased as exactly the shape
147//! a DSP cascade computes, `P = PCIN + A × B`, the fabric does almost
148//! nothing: no LUT adders, short wires, timing closed by construction.
149//!
150//! **Decision.** Split a field element into fifteen 17-bit digits. Because
151//! `2^255 ≡ 19 (mod p)`, the high half of the product folds back onto the
152//! low half, so each of the fifteen result columns is a 15-deep chain of
153//! `17 × 22`-bit products: one DSP cascade per column, 225 DSPs per
154//! multiplier, a new product every clock, 23 clocks of latency. → [`fe17`]
155//!
156//! # Chapter 3: Keeping the multiplier busy
157//!
158//! **Problem.** A multiplier that starts a product every clock is useless if
159//! the next product needs the previous result: the scalar-multiplication
160//! formulas are long dependency chains, and the result takes 23 clocks to
161//! arrive.
162//!
163//! **Reasoning.** One signature cannot keep the multiplier busy, but many
164//! signatures can, and signatures never depend on each other. If 32 of them
165//! take turns, one instruction each per clock in rotation, every result is
166//! back before its signature's next turn.
167//!
168//! **Decision.** *Barrel threading.* A field machine holds 32 contexts and
169//! issues one bundle per clock from each in turn. There is no stall logic
170//! and no hazard detection, because hazards cannot happen; all per-signature
171//! state lives in block RAM indexed by context instead of in LUTs. →
172//! [`machine`]
173//!
174#![doc = concat!("<div style=\"overflow-x:auto\">", include_str!("../docs/images/barrel-machine.svg"), "</div>")]
175//!
176//! # Chapter 4: Telling the machine what to do
177//!
178//! **Problem.** The curve algorithms change often while we optimize them,
179//! and a hand-written state machine for each would take days and hide bugs.
180//!
181//! **Reasoning.** Treat the field machine as a tiny VLIW processor and the
182//! algorithms as software: if a program can be written in ordinary Rust,
183//! checked against `ed25519-dalek` on the host in seconds, and then compiled
184//! into the machine's instruction ROM, changing an algorithm costs minutes.
185//!
186//! **Decision.** Programs are Rust ([`programs`]) built on a small
187//! microcode compiler ([`microcode`]) that list-schedules them into bundles
188//! of one multiply, one add/sub/output and one table step. Interleaving two
189//! independent addition chains lifted multiplier use from 88 % to 98.8 %.
190//! The host interpreter runs the same bundles, so hardware is compared with
191//! the interpreter bit for bit.
192//!
193//! # Chapter 5: Doing less work
194//!
195//! **Problem.** With the multiplier busy, the only way left to go faster per
196//! machine is fewer multiplications per signature.
197//!
198//! **Reasoning and decisions.**
199//!
200//! - **Encoding `R` needs `1/Z`**, 265 multiplications on its own. Inversions
201//!   can be shared: Montgomery's trick inverts 32 values with one inversion
202//!   and three multiplications each. A separate compress machine collects 32
203//!   points and inverts them together: 13.2 multiplications per point. →
204//!   [`programs::batch_compress`], [`compress_stage`]
205//! - **`[r]B` with precomputed tables.** `B` never changes, so its multiples
206//!   live in ROM. One table per digit removes every doubling; a mixed
207//!   radix-32/16 recoding balances table scans against multiplications; two
208//!   lookup slots and a searched lookup order finish the job. 492 → 392
209//!   bundles. → [`programs`]
210//!
211//! # Chapter 6: More hardware did not help
212//!
213//! **Problem.** The first full runs, 12/6/2/1 at 45.7 cycles per signature
214//! and 13/8/2/1 at 44.1, barely improved when we added machines. Every unit
215//! looked idle some of the time, and nothing looked saturated.
216//!
217//! **Reasoning.** When adding units does nothing, the limit is not a unit but
218//! the *system*. Little's law says in flight = throughput × time in system.
219//! A request held one of 1,024 admission credits for about 45k cycles, so at
220//! most 1,024 ÷ 45k ≈ one request every 44 clocks could enter: exactly the
221//! measured rate. The machines were starving behind a queue we had sized.
222//!
223//! **Decision.** 2,048 credits (13/8/2/1 then measured 37.97 with machines at
224//! 99.7 %), and a compress machine of 64 contexts × 32 points so a credit is
225//! held for 27k instead of 44k cycles. Credits also make the design
226//! deadlock-free: a request is admitted only when every resource it will
227//! need later is reserved, so nothing ever waits on something that waits on
228//! it. → [`sign_core`]
229//!
230#![doc = concat!("<div style=\"overflow-x:auto\">", include_str!("../docs/images/dataflow.svg"), "</div>")]
231//!
232//! # Chapter 7: Measuring honestly
233//!
234//! **Problem.** A 700-signature run said 32.6 cycles per signature; a
235//! 6,000-signature run of the same design said 26.43.
236//!
237//! **Reasoning.** A run shorter than the credit pool (2,048 requests) only
238//! measures the start-up transient: the pipeline is still filling. Steady
239//! state needs several times the pool.
240//!
241//! **Decision.** Throughput is always measured over 6,000 signatures, every
242//! one compared with `ed25519-dalek`: **26.43 cycles = 7.76M signatures/s at
243//! 205 MHz**. → [`simulation`]
244//!
245#![doc = concat!("<div style=\"overflow-x:auto\">", include_str!("../docs/images/road-to-7m.svg"), "</div>")]
246//!
247//! # Chapter 8: Verification
248//!
249//! **Problem.** Verifying `[S]B = R + [k]A` costs about four times a
250//! signature, because `[k]A` multiplies a point we have never seen: no
251//! precomputed tables, ~3,050 bundles instead of 392.
252//!
253//! **Reasoning.** In the workloads we serve (and in DSig in particular),
254//! verifiers know the signers' public keys long before the signatures
255//! arrive. A key known in advance can get its own tables, built once, and
256//! then `[k]A` is just another fixed-base walk.
257//!
258//! **Decision.** [`KnownKeyVerifyCore`] loads up to eight keys (about 100k
259//! cycles each, on an auxiliary machine that decodes `A` and builds its
260//! tables) and verifies with two fixed-base walks: 775 bundles. Strictness is
261//! not negotiable: canonical `S`, `A` and `R`, no low-order points, and the
262//! equation without the cofactor, with the same error codes as the reference.
263//! The common case is cheap (encode `[S]B − [k]A` and compare bytes with
264//! `R`); only a mismatch pays for decoding `R` to tell a malformed `R` from a
265//! failed equation. → [`verify_core`]
266//!
267//! # Chapter 9: Living on a shared chip
268//!
269//! **Problem.** The chip is shared: with Ed25519 verification, with DSig,
270//! with a SHA-512 line on its own allowance. Two things changed.
271//!
272//! - **Hashing.** Each module used to carry its own SHA-512 rings (~4.6k
273//!   LUTs each, eleven of them in the large signer). A shared, fully
274//!   pipelined SHA-512 line does one block per clock for everyone. A module
275//!   talks to it through a client that looks exactly like a ring, holds a
276//!   credit per block in flight (the line cannot stall), and resubmits the
277//!   constant padding block of a two-block message itself. → [`sha_client`]
278//! - **Memory.** On a full chip, block RAM ran out before LUTs or DSPs. A
279//!   fixed-base machine used 72 BRAM36, mostly register banks and constants.
280//!   Banks written at run time move to UltraRAM (barely used elsewhere);
281//!   constants move to a small LUT ROM; the compress machine's input bank,
282//!   once its identity defaults are written at reset instead of preloaded,
283//!   moves to UltraRAM too. → [`machine::MachineMemory`]
284//!
285//! The planner ([`frontier`], `planner`) turns all of this into numbers for
286//! any LUT and DSP budget.
287//!
288//! # Chapter 10: Meeting 205 MHz
289//!
290//! **Problem.** Every block met 205 MHz on its own, and no whole core did.
291//! The first routed whole cores missed by 0.6 to 2.6 ns
292//! (2026-10-17). A whole core is where long paths meet: a 255-bit carry, a
293//! signal that fans out to every machine, or a decision that waits on
294//! another decision.
295//!
296//! **Approach.** Route a core, read its worst path, and break that path with
297//! one register in the architecture, not a Vivado option. Then simulate
298//! again, because a register can change behavior. Each step below is one
299//! such path, with its slack before and after.
300//!
301//! 1. **The 255-bit add.** *Path:* the field adder's final reduction
302//!    (−0.592 ns in signer 8/0/2/1). *Idea:* add one limb per cycle, five
303//!    cycles in all. *Result:* the adder alone routes at +2.036.
304//!    The verifier's point-decode negations took the same treatment
305//!    (−2.165 and −2.577 before). → `e62418a`, [`fe17`]
306//! 2. **The multiplier's normalizer**, made limb-serial too
307//!    (`MUL_LATENCY` 23). *Result:* the multiplier routes at +1.832.
308//!    → `9934e35`
309//! 3. **SHA ring ready from registers.** *Path:* the ring's combinational
310//!    ready. *Result:* the SHA ring routes at +1.995. → `f9c1900`, [`sha_ring`]
311//! 4. **Register the machine's write-only inputs.** *Path:* dispatch to
312//!    every machine's clock enable. → `70295a5`, [`machine`]
313//! 5. **Take scalar results only when the stage is empty.** *Path:*
314//!    `fb_stage_free` into the scalar-result UltraRAM write (−0.37).
315//!    → `e9fe5cd`, [`sign_core`]
316//! 6. **Pick the scan digit a cycle earlier.** *Path:* inside a machine,
317//!    from the 128:1 digit pick, through the compare, to the 765-bit
318//!    accumulator select (−0.627 / −0.581). → `e9c1e2a`
319//! 7. **Registered credit spend.** *Path:* the controller's credits, then
320//!    the compress flush decision, then back to the credits (verifier
321//!    −0.641). → `3c67f00`, [`compress_stage`]
322//! 8. **A three-operand add in the scalar ring.** *Path:* a 272-bit carry
323//!    into the `t·c` DSP product (−0.373). → `e50dfa8`, [`scalar_ring`]
324//! 9. **Read the ROM two slots ahead.** *Path:* the program ROM into the
325//!    table digit, then the digit pick (verifier −0.370). *Result:* the
326//!    whole cores for 100 % of the chip meet timing: signer 8/0/2/1 at
327//!    +0.218 and verifier 6/0/1/1 at +0.043. → `e0c9caa`
328//! 10. **Carry-select in scalar-ring stage 8.** *Path:* at 12 machines the
329//!     four-limb add's carry chains are placed far apart (−0.358). *Idea:*
330//!     stage 7 computes both high halves, and stage 8 only finishes the low
331//!     half and picks one. *Result:* signer 12/0/3/1 at **+0.050**, the
332//!     largest core to meet timing. → `06bd8d3`
333//! 11. **A registered verify-output arbiter.** *Path:* 24 machines'
334//!     offers, chained through a priority chain into the compress loader
335//!     (−5.763). *Idea:* pick from registered offers into a registered
336//!     point. *Result:* placement −1.236.
337//!     - *What simulation caught:* the first version lost a point's `X`. A
338//!       flush could close the batch between the pick and the offer, and
339//!       verification 585 of 3,000 failed with code 6.
340//!     - The fix: a machine is accepted only when the compress stage takes
341//!       its point (`200f4e8`).
342//!     → [`verify_core`]
343//! 12. **Take an encoded point only when the compare register is empty.**
344//!     *Path:* the admission key match, then the result register, then the
345//!     compare's retire, then the compress stage's `out_ready`, which fans
346//!     out over its output stage (verifier 6/0/1/1 −0.190). *Result:*
347//!     +0.019, at 151,645 LUT (job 007zzh). → `dcb2e6e`
348//! 13. **Register what grows with the machine count.** *Path:* the
349//!     first-free-machine pick over 24 × 32 free bits, and paths that meet
350//!     at 6 machines and miss at 24 because a die crossing eats the cycle
351//!     (24/0/2/1 −2.225, −1.047 with the hard route directives). *Idea:* a
352//!     registered free-machine flag, a two-step decode root, an admission
353//!     register, and chained key-table writes. *Result:* the 24-machine
354//!     verifier meets 205 MHz at **+0.008** ns, 827,202 LUT (job 007zzq,
355//!     hard route directives). → `74fdc93`
356//!
357//! **Each fix is cheap in throughput.** All of them were re-simulated:
358//! signer 8/0/2/1 went from 46.37 to 46.46 cycles, and verifier 6/0/1/1
359//! from 134.81 to 134.93.
360//!
361//! **Each fix costs LUTs.** The verify controller grew from 17,540 to
362//! 30,657 LUT. The planner's costs were refit to the RTL that meets timing,
363//! and checked on cores routed later (+0.2 %, −0.4 %, −1.0 % and +2.3 %
364//! LUT).
365//!
366//! **Met, with no room to spare:** the 24-machine verifier (planned for
367//! 200 %). Steps 11–13 took it from −5.763 ns to +0.008 ns. Step 12 left it
368//! at −2.225 ns (842,345 LUT, job 007zzg), with 207,219 endpoints failing
369//! and 82 % of the worst path's delay in wire. Step 13 registered the
370//! paths that grow with the machine count. Three caveats: the +0.008 needs
371//! the hard route directives, it is on fix13, and fix14's constant ROM adds
372//! LUTs (the 1,1,1,1 verifier lost 0.022 ns of slack to it). A fix14 route
373//! of this core is the next check.
374//!
375//! # Chapter 11: Proving it
376//!
377//! Every claim climbs the same ladder: the host model equals the reference,
378//! the RHDL equals the host model, Vivado maps it, the planner is
379//! recalibrated. The table says which rung each claim is on.
380//!
381//! | Claim | Evidence |
382//! |---|---|
383//! | Programs equal ed25519-dalek (incl. `0`, `1`, `l − 1`) | host interpreter, `programs` tests |
384//! | Machines equal the interpreter bit for bit; rings equal `sha2` / big integers | native RHDL simulation |
385//! | Whole signers equal ed25519-dalek (1/1/1/1, 2/1/1/0, `Ed25519SignCoreSmall` on 64 envelopes) | native RHDL simulation |
386//! | 15/11/3/1: 26.43 cycles/signature | native simulation, 6,000 signatures |
387//! | Known-key verifier equals strict verification and `verify_strict` (valid; wrong message; `S + l`; `R` non-canonical, off-curve, low order; unknown key) | native RHDL simulation, `tests::verifier::known_key_verifier_matches_reference` |
388//! | Shared SHA-512 port: clients, arbiter, credits and pad-block resubmission give correct digests with a stalling consumer | native RHDL simulation against an 82-cycle pipeline model, `sha_client` tests |
389//! | Sign and verify through one shared SHA-512: signatures equal ed25519-dalek, verifier accepts them and rejects a tampered one | same, `tests::shared_sha::sign_and_verify_share_one_sha_pipeline` |
390//! | Exports to Verilog (37.0 MB) and lints clean | `export_scalable`, Verilator 5.040 |
391//! | Small core: 29,055 CLB LUT, 248 DSP, 40 BRAM36, 32 URAM, WNS +0.306 ns | Vivado 2023.2 out of context, routed at 4.878 ns (`measure/results.tsv`, job 007zd) |
392//! | Every block meets 205 MHz on the fix9 RTL: fixed-base machine +0.848 (UltraRAM banks), +0.544 (block RAM), +0.450 (LUT RAM); verify machine +0.609; compress machine +0.679; key loader +0.863; SHA ring +1.995 ns | same, one routed run per block (jobs 007zq–007zzb) |
393//! | Whole cores meet 205 MHz: signers 4/0/1/1 +0.024, 8/0/2/0 +0.397, 8/0/2/1 +0.218 (150-DSP multiplier +0.103), 8/0/3/1 +0.159, 9/0/2/1 +0.114, 12/0/3/1 +0.050; verifiers 1/1/1/1 +0.094 (82,184 LUT; +0.072 and 84,435 on fix14), 6/0/1/1 +0.019, 12/0/1/1 +0.031 (233,052 LUT), 24/0/2/1 +0.008 (827,202 LUT, hard route directives) | same, whole cores routed ([Chapter 10](#chapter-10-meeting-205-mhz)) |
394//! | 24-machine verifier: 827,202 LUT, **+0.008 ns** (fix13, hard route directives; fix12 −2.225, −1.047 hard; fix9 −5.763); planner LUT +1.5 % | routed, meets (job 007zzq; misses: 007zzg, 007zzj); **not routed on fix14** |
395//! | Planned builds' throughput: 8/0/2/1 46.46, 12/0/3/1 31.95 cycles/signature; verifier 6/0/1/1 134.93 cycles/verification | native simulation, 3,000–6,000 operations, every result checked |
396//! | Large core 15/11/3/1: 263,810 LUT, 3,669 DSP | Vivado synthesis only; **not routed** |
397//! | Specialized memory layout (UltraRAM banks, LUT-ROM constants) and its block-RAM and LUT-RAM fallbacks | routed (above) and simulated in every whole core |
398//! | Exported Verilog behaves like the model | Icarus replay of RHDL vectors (DSig, fix12): matches |
399//! | Synthesized netlist behaves like the model (1/1/1/1 key load, 20,000 cycles) | post-synthesis xsim replay, `measure/gate/`: fix14 0 mismatches; fix12 fails at cycle 8,871, as the card did |
400//! | On-card rate | **not yet measured** |
401//!
402//! **What the card caught.** DSig's verifier v3, the first card build with
403//! our key loader, rejected a valid key. Every simulation we had passed:
404//! RHDL, Icarus on the exported Verilog, the whole kernel. DSig replayed
405//! our RHDL vectors against the routed netlist and found it at cycle 65
406//! of the aux machine: the constant `d` read back with 16 wrong bits.
407//! Vivado 2023.2 had mapped the write-disabled constant bank to LUT RAM,
408//! dropped the bit columns that are zero in every entry, and kept the INIT
409//! strings in the old layout. The fix is in RHDL: the constant bank is now
410//! a literal and a mux ([`machine::ConstRom`], fix14), which leaves Vivado
411//! no RAM to infer. It costs +1,658 CLB LUT on the 1/1/1/1 verifier after
412//! synthesis. The lesson is the new rung on the ladder: replay the vectors
413//! after synthesis (`measure/gate/`), before a link.
414//!
415//! # Chapter 12: Beside a consensus protocol
416//!
417//! **Problem.** uBFT ([YiJiang0908/ubft_new]) runs Byzantine consensus as
418//! an HLS kernel on the same U280. On its slow path, each replica signs a
419//! certificate share for every slot. It then checks its peers' shares until
420//! it holds a quorum of `2f + 1`. Today it calls the Vitis Security
421//! library's Ed25519 inline. One signature takes 3.07M cycles and one
422//! verification 10.4M (HLS C/RTL cosimulation, measured by DSig). The
423//! state machine waits for each one, and that signer is the kernel's worst
424//! timing path: it pulled uBFT's clock from 300 MHz down to 161.3 MHz.
425//! uBFT tolerates `f = (n − 1) / 2` faults, so a quorum is nearly every
426//! replica. At three replicas a slot spends about 148 ms on crypto, and
427//! at seven about 406 ms. The library call
428//! also hashes only the first byte of the 8-byte slot summary.
429//!
430//! **Idea.** Don't call crypto inside the state machine; give it a service
431//! beside it. `ubft::UbftEd25519` (feature `ubft`) takes requests on four AXI streams:
432//! a message in and a signature out, a share in and a verdict out. Up to 16
433//! requests per direction stay in flight, each with a tag that uBFT
434//! chooses. uBFT keeps its loop. It sends a request and goes on with
435//! other work, then picks up the answer by tag. The service is the small
436//! signer of [chapter 6](#chapter-6-more-hardware-did-not-help) and one
437//! known-key verifier of [chapter 8](#chapter-8-verification), and needs
438//! nothing new beyond tag tables and two FIFOs. Peers' keys are loaded once
439//! per session into key slots. uBFT allows up to 12 replicas, more than the
440//! verifier's 8 slots, so the feature `key-slots-16` widens the slot
441//! number to 4 bits ([`machine::KEY_SLOTS`]). It is a feature, not the
442//! default, because 16 slots double every verify machine's key tables.
443//! That is cheap for this one-machine service and costly for the
444//! 24-machine verifier of chapter 10. DSig offers the same four streams
445//! with the same header layout, so uBFT can be built with either scheme by
446//! changing only sizes.
447//!
448//! **Result.** In the planner's uBFT mode (`Mode::UbftEd25519`), the
449//! replica takes its routed footprint off the device (346k LUTs, an upper
450//! bound because it includes the shell). The service needs about 120k more.
451//! A slot's crypto then takes about 367 µs at three replicas and 397 µs at
452//! eleven, against 148 ms and 665 ms inline: 400 to 1,700 times less.
453//!
454//! **What is proven, what is not.** The signer's cost and its 22,240-cycle
455//! latency are measured. The 1,1,1,1 verifier is routed at 205 MHz (fix14,
456//! +0.072 ns). The simulation `ubft::tests::ubft_service_signs_and_verifies`
457//! drives the whole service through its streams. It checks three
458//! signatures against ed25519-dalek, and four verdicts: valid, tampered,
459//! wrong slot and empty slot. It passes: 26 minutes of native
460//! simulation, about 290k cycles, most of them the two key loads. Not yet measured: the service logic (the
461//! planner estimates 6k LUTs), the 16-slot key tables (+57 BRAM36 per
462//! machine, a model), the verifier's latency inside the service, and
463//! anything on the card. The slot model also ignores network time and
464//! assumes peers' shares arrive together.
465//!
466//! [YiJiang0908/ubft_new]: https://github.com/YiJiang0908/ubft_new
467//!
468//! # Epilogue: your turn
469//!
470//! ## Using the cores
471//!
472//! Both signers and the verifier are ordinary RHDL
473//! [`Synchronous`](rhdl::prelude::Synchronous) components; embed them as
474//! child fields.
475//!
476//! ```rust,ignore
477//! use rhdl_ed25519_scalable_core::{Ed25519SignCoreSmall, KnownKeyVerifyCore, ScalableSignCore, sign_core::CTX_BITS};
478//!
479//! let small = Ed25519SignCoreSmall::default();                          // 1/1/1/0, 128 contexts
480//! let large = ScalableSignCore::<15, 11, 3, 1, CTX_BITS, 0>::default(); // own SHA rings
481//! let verify = KnownKeyVerifyCore::<2, 1, 1, 0>::default();             // 2 verify machines
482//! ```
483//!
484//! The last const parameter `SH` picks own SHA rings (0) or the shared line
485//! (1). Single requests are slow (~22k cycles on the small core) and many
486//! requests are fast, so keep many in flight. [`ed25519_sign_core`] documents
487//! the small core's ports; [`sign_core`] and [`verify_core`] the large ones.
488//!
489//! ## How to work
490//!
491//! Prove each change at the cheapest level that covers it, then climb:
492//!
493//! | Level | Command | Time |
494//! |---|---|---|
495//! | Host model: programs and planner | `cd tools/hostmodel && nice cargo test --offline -j2` | ~20 s |
496//! | RHDL units, signers, verifier | `cargo test --profile sim --lib` | ~4 min (+ two ~18 min verify tests) |
497//! | Throughput | `SCALABLE_BENCH_COUNT=6000 cargo test --profile sim --lib tests::signer::sustained_throughput_15_11_3_1 -- --ignored --nocapture` | ~40 min |
498//! | Verilog export | `cargo run --profile sim --bin export_scalable [-- --small \| --block NAME]` | 1–2 min |
499//! | Vivado out of context | `tools/vivado_ooc.tcl` (on the U280 host) | hours |
500//!
501//! On the shared host, run heavy commands through `tools/bounded.sh` (memory
502//! and CPU caps, one heavy job at a time) and record each run in the
503//! coordination ledger.
504//!
505//! RHDL has sharp edges that compile under rustc and fail only when
506//! lowering: no `/` or `%` in kernels (even on loop constants), literal or
507//! const-generic loop bounds, `Bits` of at most 128 bits, at most ~11
508//! synchronous children per struct, and a component's generated `Q`/`D`
509//! types carry its name as a prefix when a module holds several
510//! components. [`export`] describes the lowering-memory patch.
511//!
512//! ## Open problems
513//!
514//! 1. **Keep the 24-machine verifier at 205 MHz.** Fix13 meets it by
515//!    0.008 ns with the hard route directives (Chapter 10, step 13). Route
516//!    it on fix14 (constant ROM, more LUTs) and with default directives,
517//!    and win back some margin.
518//! 2. **Wire the shared SHA-512 line for real** (`rhdl_sha512_pipelined`) in
519//!    one chip and route it.
520//! 3. **A reduce-only scalar ring** for verification. (The 150-DSP
521//!    Karatsuba multiplier is built and meets 205 MHz on the timing-fixed
522//!    RTL: signer 8/0/2/1 at +0.103, 188,537 LUT.)
523//! 4. **Latency**: the compress machine's batch wait dominates; a smaller
524//!    batch or an early flush trades throughput for latency.
525//! 5. **Gate every core after synthesis.** The gate covers the 1/1/1/1
526//!    verifier's key load. Record vectors for a signer and for a
527//!    verification, and run them on every core before it ships.
528//! 6. Strict verification for arbitrary keys (variable-base), if needed.
529//!
530//! Smaller items found while documenting every kernel (none is a known bug):
531//!
532//! - **Admission is capped at one request per three cycles** by the free-context
533//!   FIFO's refill latency (the credit limit is ~23 cycles, so it is not binding
534//!   today).
535//! - **Fixed-priority arbitration.** Verify-machine outputs and ring results are
536//!   taken lowest index first, not round-robin; a starvation bound would need
537//!   checking if a module ever runs saturated for long.
538//! - **Combinational `ready` with `SH = 1`.** A core's `ready` depends on the
539//!   shared port's `ready` in the same cycle. This is safe because
540//!   [`sha_client::SharedShaPort`] drives that from registers, but it is a
541//!   contract for any other port. The verifier's `ready` also depends on
542//!   `load_key_valid` in the same cycle (a key load goes first).
543//! - **Caller contracts.** Nothing guards these:
544//!   - a field machine must not be started on a running context;
545//!   - the compress stage expects each point's `X` before its `Y` and `Z`.
546//! - **`Ed25519SignCore` builds only small sizes.** With `CB = 7` it is limited
547//!   to `NFB <= 4` and fused mode; larger sizes use [`ScalableSignCore`].
548//! - **`interpret` runs one program at a time.** It ignores intermediate `end`
549//!   flags, so it cannot start a [`microcode::concat`] ROM at a later entry.
550//! - **Duplicate encoding helpers.** `compress_stage::encode_y` repeats
551//!   `sign_core`'s point encoding.
552//! - **Fitted model constants.** `config::CREDIT_OVERHEAD` was fitted to older
553//!   programs and not refitted. The verify utilization is assumed equal to
554//!   signing's, not measured.
555//!
556//! # Where to go next
557//!
558//! Pick the row that matches what you want to do. Each row lists the pages to
559//! read, in order, and the first command to run.
560//!
561//! | You want to... | Read, in order | Then run |
562//! |---|---|---|
563//! | **understand how a signature is computed** | [`programs`] (the algorithm as Rust), [`microcode`] (how it becomes bundles), [`host`] (the reference model) | `cd tools/hostmodel && nice cargo test --offline -j2` |
564//! | **make signing faster per machine** | [Chapter 5](#chapter-5-doing-less-work), [`programs::fixed_base`], [`microcode`] | the host model; count bundles before touching hardware |
565//! | **make signing faster per chip** | [Chapter 6](#chapter-6-more-hardware-did-not-help), [`sign_core`] (credits, contexts), [`config`] (cycle model) | `tests::signer::sustained_throughput_15_11_3_1` (40 min) |
566//! | **understand the hardware datapath** | [`fe17`] (multiplier), [`machine`] (barrel machine, memories), [`sign_core`] (controller) | `cargo test --profile sim --lib tests::machines::fixed_base_machine_matches_interpreter` |
567//! | **work on verification** | [Chapter 8](#chapter-8-verification), [`verify_core`], [`programs::verify_known_key`], the key-load programs in [`programs`] | `tests::verifier::known_key_verifier_matches_reference` (~18 min) |
568//! | **save BRAM, LUTs or DSPs** | [`machine`] ("Where the memories live"), [`frontier`] (costs, marked measured or projected), `mapping/vivado-20261014` (per-block reports) | `cargo run --profile sim --bin export_scalable -- --block NAME`, then Vivado |
569//! | **connect to the shared SHA-512 line** | [Chapter 9](#chapter-9-living-on-a-shared-chip), [`sha_client`], the `SH` parameter of [`sign_core`] and [`verify_core`] | `tests::shared_sha::sign_and_verify_share_one_sha_pipeline` |
570//! | **change the planner or its numbers** | [`frontier`], `planner` (feature `tuner`), and the repository guide's "How a plan is made" | `cargo test --features tuner --lib planner` |
571//! | **serve uBFT (or another protocol) over streams** | [Chapter 12](#chapter-12-beside-a-consensus-protocol), `ubft` (feature `ubft`: streams, register map, session), `planner::ubft` (feature `tuner`) | `cargo test --profile sim --features ubft --lib ubft -- --ignored` |
572//! | **embed a core in your own design** | [Using the cores](#using-the-cores), [`ed25519_sign_core`] (ports and handshake), [`wire`] (port types) | `tests::signer::small_core_signs_dsig_envelopes` |
573//! | **get Verilog for Vivado** | [`export`] (what is generated and why it fits in memory) | `cargo run --profile sim --bin export_scalable -- --small` |
574//! | **pick something to work on** | [Open problems](#open-problems), and the repository guide's "Good first contributions" (sized tasks with a clear finish line) | write down its work count and binding resource first |
575//!
576//! # Glossary
577//!
578//! | Term | Meaning |
579//! |---|---|
580//! | Barrel threading | many jobs share one pipeline, one per clock in rotation |
581//! | Bundle | one VLIW instruction: a multiply, an add/sub/output, a table step |
582//! | Context | one job resident in a machine (32 per fixed-base machine) |
583//! | Credit | permission to admit a request; reserves every later resource it needs |
584//! | Fused mode | no compress machine; each program inverts its own point |
585//! | Known-key verification | verification for public keys whose tables were built in advance |
586//! | Little's law | in flight = throughput × time in system |
587//! | MACC chain | DSP cascade `P = PCIN + A × B` |
588//! | Mixed radix | 39 five-bit and 15 four-bit signed digits |
589//! | Profile `fb/sha/sc/cp` | build size: fixed-base machines, SHA rings, scalar rings, compress machine (1) or fused (0) |
590//! | Ring | a small pipeline with several jobs circulating |
591#![warn(missing_docs)]
592// RHDL lowers fixed-bound range loops directly into hardware. Rewriting them
593// as host iterators, as Clippy suggests, would leave the synthesizable subset.
594#![allow(clippy::needless_range_loop)]
595// Explicit hardware expressions are kept uniform inside `#[kernel]` code.
596#![allow(clippy::assign_op_pattern, clippy::unnecessary_cast)]
597
598// ---- Host model (no hardware): the reference everything is checked against.
599pub mod host;
600pub mod microcode;
601pub mod programs;
602
603// ---- Hardware (RHDL components).
604pub mod compress_stage;
605pub mod digest;
606pub mod ed25519_sign_core;
607pub mod fe17;
608pub mod fe17_kara;
609pub mod machine;
610pub mod scalar_ring;
611pub mod sha_client;
612pub mod sha_ring;
613pub mod sign_core;
614#[cfg(feature = "ubft")]
615pub mod ubft;
616#[cfg(feature = "card")]
617pub mod card;
618pub mod verify_core;
619pub mod wire;
620
621// ---- Sizing, export and simulation tools.
622pub mod config;
623pub mod export;
624pub mod frontier;
625#[cfg(feature = "tuner")]
626pub mod planner;
627pub mod simulation;
628
629#[cfg(test)]
630mod tests;
631
632pub use ed25519_sign_core::{Ed25519SignCore, Ed25519SignCoreSmall};
633pub use sign_core::{ScalableSignCore, ScalableSignIn, ScalableSignOut};
634pub use verify_core::{KnownKeyVerifyCore, VerifyIn, VerifyOut};
635pub use wire::{CachedSignIn, CachedSignOut, ScalarWords, SignRequest, VerifyRequest, Words512};