Expand description
Illustrated introduction to Ed25519, the parallel datapath, and FPGA resources.
§A Visual Tour of Ed25519 in RHDL
Start here if this is your first FPGA or cryptography project. Follow the
numbered recipe first, then zoom into the hardware. Every named block below
has a Rustdoc link to its implementation. Diagrams describe the default
wide-sign build for exactly 64-byte messages, not arbitrary-length Ed25519.
- Understand the signature.
- Follow the parallel hardware.
- Read the pipeline.
- Picture the FPGA.
- Find the next improvement.
§Understand the Signature
Ed25519 makes a digital signature: a short proof attached to a message. Only the holder of a secret key should be able to create a valid proof, while anyone with the corresponding public key can check it. It does not encrypt the message. A public key is 32 bytes; a signature is 64 bytes.
Think of the operations in three groups. SHA-512 turns bytes into a digest. Scalar arithmetic handles large integers with a wraparound rule. Point arithmetic combines points on a particular elliptic curve. Rust/RHDL describes physical circuits for all three; the host only supplies bytes and reads results.
| Symbol | Plain-language meaning | Where it lives |
|---|---|---|
M | The 64-byte message | SignRequest or VerifyRequest |
seed | The private 32-byte starting key | Fast64CoreIn::key_seed |
a, prefix | Secret values derived from the seed | Cached state in sign |
B | The fixed, publicly known base point | multicomb_table |
A = [a]B | The public key; brackets mean point multiplication | Fast64CoreOut::cached_public_key |
r, R = [r]B | A secret per-message scalar and its public point | sign, multicomb |
k | The challenge binding R, A, and M together | scalar_parallel |
S | The second half of the signature | CoreResult::signature |
H below means SHA-512, || means byte concatenation, and mod l means reduce
to the canonical range of the Ed25519 scalar arithmetic. Encoding a point turns
it into 32 bytes. The key/signature construction follows
RFC 8032, sections 5.1.5-5.1.7.
§Find Each Step in Rust
| Recipe step | RHDL implementation | What to read first |
|---|---|---|
| Key setup, scheduling, cached state | CachedSignCore | The cached_sign_kernel source and its key-state transitions |
| 1 and 5: hardware hashing | Sha512Pool8, Sha512Compress | Hardware constructs the padded blocks; no host prehash |
| 2 and 6: digest to scalar | ScalarReducePipe | One shared hash reducer services both phases |
| 3: multiply the base point | DualMultiComb, MultiCombMul | Two physical engines; 24 in-flight contexts per engine |
| 4: encode the point | DualPointCompress, PointCompressPair | Two engines; each batches a pair to share an expensive inversion |
| 7: compute S | ScalarWideMulPipe, ScalarReducePipe | Wide product, registered add, then a separate final reducer |
| 8: retire the signature | ResultArbiter, CoreResult | Preserve the request tag when completion order changes |
§What Verification Does
The verifier receives M + A + (R || S). It does not know the seed, a, or
prefix. It recomputes k = H(R || A || M) mod l and checks the equation
[S]B = R + [k]A. That is possible because signing made S = r + k*a mod l.
Changing the message or signature should make the equation fail.
Our strict path also rejects noncanonical S, malformed or noncanonical
point encodings, and small-order A or R. A small-order point belongs to a
tiny subgroup; accepting it could weaken the intended check. These gates are
part of the contract, not optional performance shortcuts. See
StrictVerifyCore and the
error-code table.
§Follow the Parallel Hardware
There are three different kinds of parallelism here:
- Lanes: several physical engines can work simultaneously, such as eight SHA-512 compressors in signing.
- Contexts: an engine keeps state for several requests and rotates among them while dependent arithmetic is in flight. A context is not a whole engine.
- Pipeline stages: registers divide arithmetic into smaller steps. Different requests occupy different steps at the same time.
D* in the next diagram counts DSP-friendly multiplier sites in the source,
not measured DSP utilization. L? means LUT utilization is not yet measured.
0 D* means no multipliers in that block, not free hardware. Every block
also needs routing, registers, control, and usually LUTs.
The left and right sides really are separate components in
Fast64Core. Verification’s hash and fixed-base jobs are
independent of point decompression; [k]A waits for both k and decoded A.
The equation waits for its operands. Check flags accumulate alongside the
arithmetic and are joined with the equation result at retirement; this
implementation does not short-circuit immediately on a noncanonical S.
The final join selects a successful decision or the appropriate rejection code.
Do not interpret this picture as a constant-latency promise under queueing.
| Diagram block | Source and resource accounting |
|---|---|
| Signing banks and dispatch | parallel: two four-lane SHA pools, two fixed-base engines, two pair compressors |
| Each fixed-base engine | multicomb: four field multipliers, 4 * 150 = 600 D* |
| Each pair compressor | point_compress: one field multiplier, 150 D* |
| Each point decoder | point_decode: one field multiplier, 150 D* |
| Each variable-base lane | variable_base: four field multipliers, 600 D* |
| Equation check | equation: four field multipliers, 600 D*; no final inversion |
| Three scalar reducers total | scalar_parallel: 3 * 576 = 1,728 D* |
| Signing scalar multiplier | ScalarWideMulPipe: 512 D* |
| All field multipliers | FieldMulPipe: 28 * 25 * 6 = 4,200 D* |
| Entire wide core | 4,200 + 1,728 + 512 = 6,440 D*; actual LUT/FF/DSP/BRAM totals await full mapping |
The compact feature profile removes four signing SHA lanes, one signing fixed-base engine, and one pair compressor. Its structural count is 5,690 D*. It does not remove strict verification. Resource counts here describe the cryptographic core, not an AXI/PCIe shell or platform reservation.
§Read the Pipeline
A clock edge captures intermediate results in registers. Breaking a long
calculation into stages can shorten the work between edges, making a faster
clock possible. But extra registers consume chip area and must preserve the
alignment of data, valid, and tags.
Latency is how long one request takes. Initiation interval (II) is how often another request can start. A 17-cycle multiplier with II=1 can hold 17 independent products in flight; it does not finish one product in one cycle. A complete signature is much larger than a single field multiplication.
§Connect the Stages to the Code
| Pipeline | Latency / issue contract | Implementation |
|---|---|---|
| Limb product | 6 stages, a new independent pair every cycle | Mul51Cell and mul51_cell_kernel in field_parallel |
| Field product | 17 stages, II=1 | FieldMulPipe |
| Field add/subtract | 4 stages, II=1 | FieldAddSubPipe |
| Scalar reduction | 57 stages, II=1 | ScalarReducePipe |
| Wide scalar product | 22 stages, II=1; final modular reduction is separate | ScalarWideMulPipe |
| SHA-512 compression | 242-cycle lane latency; honor ready | Sha512Compress |
| Full sign / verify | Context scheduling and queueing affect latency and spacing | simulation, sign, verify |
For RHDL readers, q is current state and child output, while d is next state
and child input. Assigning d does not immediately change q. This is why six
assignments involving different registers in one Rust kernel can describe six
clocked stages, rather than six sequential host-software statements.
The shared multiplier can work on another context while a point calculation waits for its previous product. However, a second operation from the same dependent point calculation must still wait. More contexts hide latency; they do not remove mathematical dependencies or create more multipliers.
§Picture the FPGA
An FPGA is a grid of configurable resources joined by programmable wires. LUTs implement small logic functions, FFs hold individual bits, DSP slices provide hardened arithmetic, and BRAM stores local arrays. Those resources are not interchangeable percentages of one budget.
The depiction below is a logical allocation sketch, not a Vivado device view, Pblock constraint, actual placement, or proof of fit. Box sizes are not proportional to area. The caps are this project’s allocation, not a claim about all resources available on an Alveo U280 or in its selected platform shell.
§What Has Actually Been Counted?
| Scope and evidence | DSPs | LUT primitives | FFs | Interpretation |
|---|---|---|---|---|
One six-stage Mul51Cell, Yosys 0.62 xcup, DSP allowed | 6 | 155 | 928 | Measured leaf mapping; also 64 CARRY4 |
The same leaf, -nodsp | 0 | 7,642 | 928 | Measured leaf mapping; also carries and wide muxes |
| Whole default RHDL core | 6,440 D* | Unknown | Unknown | Source structure only; no whole-core fit claim |
| Project limit | 9,000 | 260,000 | 520,000 | Separate caps, not a single shared percentage |
The leaf LUT count sums LUT1 through LUT6 primitives; it is not an estimated
“logic cell” count. Full details and exact mapper inventories are in
reports/mapping/MUL51.md and reports/mapping/mul51-xcup-{dsp,lut}.json.
These are historical mapping results, not regenerated by building the docs.
Why the caution? In that Yosys mapping, the DSP internal pipeline registers are unused. Scaling 928 external FFs to the wide core’s 700 limb cells alone gives 649,600 FFs, already above the 520,000 cap. That extrapolation is a warning, not a full design count: a different mapper may absorb registers, and other logic changes the total. A full Vivado report is still required. The relevant hardened-register controls are documented in AMD’s DSP48E2 primitive reference.
§A Lookup Table Is Not a Logic LUT
Our fixed-base point lookup table is stored as data in inferred BRAM. An FPGA logic LUT is a small configurable truth table that implements logic. The names sound similar, but these are different resources.
| Local storage | Size and purpose | Code |
|---|---|---|
| One logical multi-comb table | 4 banks * 128 entries * 765 bits = 391,680 bits | multicomb_table |
| One physical read-replicated table | 8 copies * 391,680 = 3,133,440 initialized bits | Same module; replicas enable parallel scans |
| Three default table instances | 9,400,320 initialized bits before BRAM packing | Two signing engines + one verification engine |
| Signing message payloads | 128 * 64 = 8,192 bytes | sign; additional context state is separate |
| Verification message payloads | 64 * 64 = 4,096 bytes | verify; A/R/S and working state are extra |
| SHA rolling schedules | 16 * 64 bits per lane; constants are separate logic | sha512_parallel |
These are payload sizes, not exact BRAM block counts. Port width, replication, packing, and control change physical usage. Secret data selects among registered candidates; it never supplies the fixed-base RAM address.
There is no external-memory port on Fast64Core, so the
cryptographic core has zero external scratch-memory traffic. A shell must
still transport requests and results: a signing message is 64 bytes, its
signature is 64 bytes, and a verification request carries another 32-byte key
and 64-byte signature. Metadata, AXI padding, host copies, and PCIe transactions
are additional. “No scratch traffic” does not mean “no system traffic.”
§Find the Next Improvement
The useful question is not “where can we add more hardware?” It is which measured bottleneck prevents the next correct result from finishing sooner, and can we remove it within every resource cap?
The native wide-sign benchmark matched 384/384 Dalek signatures. Its backlogged
sample averaged 90.984375 cycles/sign. At an assumed, not achieved,
205 MHz, that projects to about 2.253 million signs/s. Whole-core fit,
routed timing, and the corresponding on-card rate remain unproven. The raw
measurement and warmup/drain treatment are in reports/bench/README.md and
reports/bench/native-wide-steady.json.
§Concrete Experiments
| Priority / hypothesis | Where to work | What would count as evidence |
|---|---|---|
| First: fit the registers. Encourage DSP internal-register absorption; examine redundant widths/delays. | field_parallel | Small Vivado OOC report with DSP register settings, then whole-field and full-core FF counts; preserve the six/17-edge contracts or update all delays and tests |
| Reduce storage logic. Move the encoded-R two-write register store toward scheduled BRAM writes. | sign, EncodedRStore | Lower mapped FF/LUT count without dropped results or a worse steady interval |
| Signing looks close to hash capacity. Compare more SHA lanes with changes to the three-phase compressor. | sha512_parallel, parallel | Lower sustained cycles/sign, acceptable LUT/FF cost, and no clock regression |
| Amortize point encoding. Try inversion batches of four or eight instead of pairs. | point_compress | Higher completion rate at realistic queue occupancy, including odd and short bursts |
| Tune verification separately. Measure variable-base lane occupancy and equation/decode queues. | variable_base, verify | Sustained strict-verification gain under mixed traffic; no weaker rejection policy |
| Trade excess arithmetic for area. Investigate time-sharing low-occupancy scalar pipelines. | scalar_parallel, sign | DSP savings with bounded arbitration and no tag/throughput regression |
Why focus on SHA? A cached 64-byte sign needs three SHA compression blocks.
With eight lanes and a 242-cycle block latency, an optimistic hash-only bound
is 8 / (3 * 242) signs/cycle, or one sign every 90.75 cycles. The measured
90.984375-cycle sample is close. This is a bottleneck hypothesis based on source
and simulation, not a routed timing result or a proof that other stages can
support unlimited extra lanes.
At 205 MHz, 4M/s needs at most 51.25 cycles/sign and 5M/s needs at most 41 cycles/sign. Sixteen unchanged SHA lanes have an optimistic hash-only limit of about 4.52M/s at that assumed clock. Simply doubling lanes therefore does not establish 5M/s, and neither experiment is established to fit.
Use the resource explorer and optimization guide for bounded searches and source-bound measurements. Its sliders express resource allowances, not a guaranteed implementation or automatic timing closure. The paper library explains multi-comb multiplication, arithmetic choices, and other research directions with primary-source links.
§Your First Code Change
- Read
protocolandcore. The integration surface is small: key management, independent sign/verify inputs, one result. - Trace one known message through
simulation. Confirm its signature with Dalek before changing any arithmetic. - Open the relevant block from the tables above. In Rustdoc, its Source link takes you to the exact Rust definition. Start with a leaf, not the entire core.
- Change one parameter or stage. Check data widths, register delays, tags, backpressure, malformed inputs, and secret-dependent control.
- Run the bounded developer workflow. Keep evidence for native behavior, generated RTL, resource mapping, and clock timing separate.
The core accepts a request only on a rising edge with both valid and ready
high. Hold the payload stable until acceptance. Results carry the original tag,
may complete out of order, and cannot be backpressured; the shell must consume
every result.valid pulse. Clear/reload the cached key only after draining
accepted signs and completing key expansion, with request-valid inputs low.
Cached-key clear is not a promise to erase every physical intermediate register.
§Small Glossary
| Term | Meaning in this repository |
|---|---|
| Hash | A deterministic digest of bytes; SHA-512 is part of signing, not a substitute for a signature |
| Scalar / field element | Integers in different modular systems: scalars use group order l; coordinates use prime p |
| Limb / radix | A chunk of a wide number and its place-value base; five 51-bit limbs represent one field element |
| Point compression | Encode a curve point in 32 bytes; not generic file compression |
| Multi-comb | A fixed-base multiplication strategy using precomputed point combinations; see multicomb |
| LUT / DSP / FF / BRAM | Configurable logic / hardened arithmetic / single-bit register / local block memory |
| Context / lane / stage | Stored request state / physical engine / one registered step within an engine |
| II / latency | Spacing between new operations / time for one operation to travel through a block |
| Synthesis / mapping | Turn generated RTL into a graph of device logic and storage resources |
| Placement / routing | Choose physical sites and wires; necessary to establish real timing and fit |
| MHz / signatures per second | Clock edges per second / completed operations per second; not the same unit |
For the detailed contracts behind these pictures, continue with
architecture and the validation record.
All diagrams in this guide are authored documentation;
none is presented as a screenshot of placed hardware.