Skip to main content

Module visual_guide

Module visual_guide 

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

  1. Understand the signature.
  2. Follow the parallel hardware.
  3. Read the pipeline.
  4. Picture the FPGA.
  5. 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.

SymbolPlain-language meaningWhere it lives
MThe 64-byte messageSignRequest or VerifyRequest
seedThe private 32-byte starting keyFast64CoreIn::key_seed
a, prefixSecret values derived from the seedCached state in sign
BThe fixed, publicly known base pointmulticomb_table
A = [a]BThe public key; brackets mean point multiplicationFast64CoreOut::cached_public_key
r, R = [r]BA secret per-message scalar and its public pointsign, multicomb
kThe challenge binding R, A, and M togetherscalar_parallel
SThe second half of the signatureCoreResult::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.

From a secret seed and message to an Ed25519 signature Key setup hashes and clamps the seed, then derives public key A using the same point engines used for signing. For each message, hash prefix and message, reduce to r, compute and encode R, hash R A and message, reduce to k, then compute S. The output is R concatenated with S. 01 / The signature recipe Key setup happens once per loaded key. The lower path repeats for each message. KEY SETUP / sign::CachedSignCore 32-byte secret seed SHA-512 + clampProduce a and prefix Compute A = [a]BSame point path as below Cache a, prefix, APublish A; keep a secret SIGN ONE MESSAGE / logical dependencies, not a clock-scaled timeline 1. H(prefix || M)2. Reduce to r3. Point R = [r]B4. Encode R 5. H(R || A || M)6. Reduce to k7. S = r + k*a mod l8. Signature R || S 96 bytes + SHA padding1 compression block 512-bit digest to scalarKeep r for step 7 Fixed-base multi-combCurve point, not bytes yet Share inversion in pairs32 encoded bytes 128 bytes + SHA padding2 compression blocks 512-bit digest to scalarSame reducer as step 2 Multiply, add, reduce32 encoded bytes 64 bytes + original tagThe message is unchanged M is retained locally for both hashes. The secret seed never leaves the core. r and a must remain secret; R, A, S, and the message may be public.
Figure 1. The algorithm's data dependencies. A numbered box is not necessarily a separate circuit.

§Find Each Step in Rust

Recipe stepRHDL implementationWhat to read first
Key setup, scheduling, cached stateCachedSignCoreThe cached_sign_kernel source and its key-state transitions
1 and 5: hardware hashingSha512Pool8, Sha512CompressHardware constructs the padded blocks; no host prehash
2 and 6: digest to scalarScalarReducePipeOne shared hash reducer services both phases
3: multiply the base pointDualMultiComb, MultiCombMulTwo physical engines; 24 in-flight contexts per engine
4: encode the pointDualPointCompress, PointCompressPairTwo engines; each batches a pair to share an expensive inversion
7: compute SScalarWideMulPipe, ScalarReducePipeWide product, registered add, then a separate final reducer
8: retire the signatureResultArbiter, CoreResultPreserve 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.

Concurrent signing and strict verification inside Fast64Core The signing path has eight hash lanes, two fixed-base engines, two pair compressors, two reducers and a scalar multiplier. Verification concurrently decodes A and R, hashes the challenge and computes S times B. Then one of two variable-base engines computes k times A and an equation engine checks the result. Both paths feed a common result arbiter. DSP values are structural estimates and all whole-block LUT values are unknown. 02 / One interface, two independent cryptographic paths Default wide-sign topology. D* = structural multiplier budget; L? = unmapped LUT count. CachedSignCore / 128 contexts StrictVerifyCore / 64 contexts Key load + sign_valid / sign_ready verify_valid / verify_ready: M, A, R, S Sha512Pool8 / 8 physical lanesHash ScalarReducePipe / 576 D* / L?MultiCombMul 0MultiCombMul 1Compress pair 0Compress pair 1ScalarWideMulPipe + add + final reducer 01234567 Key hash; nonce hash; two-block challenge hash0 D* / L? - 16 schedule words per laneNonce r first; challenge k after encoded R is available24 contexts / 600 D*[r]B or key [a]B / L?24 contexts / 600 D*[r]B or key [a]B / L?150 D* / L?Projective point to bytes150 D* / L?Projective point to bytes512 + 576 D* / L?Retained r + k*a mod l; join S with encoded R Signing subtotal: 3,164 D* / L? Dashed return: encoded R schedules the challenge hash. Record canonical-S flag / 0 D* / L?PointDecodePipe APointDecodePipe RSHA pool + reduceMultiCombMulVariableBaseMul 0VariableBaseMul 1EquationPipe + strict-check join 20 contexts / 150 D* / L?Canonical + curve + order20 contexts / 150 D* / L?Canonical + curve + order4 SHA lanes; derive k576 D* / L?R || A || M, two blocks24 contexts; compute [S]B600 D* / L?Runs without waiting for k20 contexts; [k]A600 D* / L?20 contexts; [k]A600 D* / L?20 contexts / 600 D* / L?[S]B = R + [k]A; every strict gate must pass Dispatch to one lane, not both Verification subtotal: 3,276 D* / L? Reject paths return an error, not a fabricated signature. ResultArbiter / 0 D* / L?Tagged sign or verify result; one per cycle; no output backpressure
Figure 2. Physical lanes and logical dependencies, not a wiring-complete schematic. Context memories, mergers, and control logic are included in the design but simplified here.

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 blockSource and resource accounting
Signing banks and dispatchparallel: two four-lane SHA pools, two fixed-base engines, two pair compressors
Each fixed-base enginemulticomb: four field multipliers, 4 * 150 = 600 D*
Each pair compressorpoint_compress: one field multiplier, 150 D*
Each point decoderpoint_decode: one field multiplier, 150 D*
Each variable-base lanevariable_base: four field multipliers, 600 D*
Equation checkequation: four field multipliers, 600 D*; no final inversion
Three scalar reducers totalscalar_parallel: 3 * 576 = 1,728 D*
Signing scalar multiplierScalarWideMulPipe: 512 D*
All field multipliersFieldMulPipe: 28 * 25 * 6 = 4,200 D*
Entire wide core4,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.

Pipeline stages and overlapping transactions The six-edge limb cell splits operands, forms six products, partially adds rows, finishes rows, merges rows, and registers the result. A field multiplier adds three coefficient and eight carry stages for 17 cycles. An example wavefront shows three independent products advancing in consecutive cycles. SHA-512 instead reuses three phase registers for 80 rounds; it is not an initiation-interval-one compressor. 03 / What each register boundary is doing Mul51Cell: a single 51 x 51 product. All six small multiplications in stage 2 run together. 1 / Split2 / Multiply3 / Partial rows4 / Row merge5 / Full merge6 / Delay a: 2 x 26 bitsb: 3 x 17 bits6 parallel tiles26 x 17 eachlow + middleretain high termmerge high term2 row productslow row +(high row << 26)align output withvalid pipeline FieldMulPipe / 17 edges / II = 1 6 limb-product stages25 cells in parallel / 150 small products3 coefficient stagessum and fold terms8 carry / reduction stagesnormalize modulo p = 2^255 - 19 Pipeline wavefront / independent field products A, B, C edge tt + 1t + 2...t + 17t + 18t + 19 Product AProduct BProduct C accept Astage 1stage 2in flightresult Aaccept Bstage 1in flightstage 16result Baccept Cin flightstage 15stage 16result C SHA-512 is different / each lane reuses three phases across 80 rounds Phase A / preparePhase B / sumPhase C / update Rotate/XOR, choose, majority;schedule partials and h + KFinish schedule word;combine round-sum partialsUpdate a..h working state;next round, or final accumulation 242-cycle compression latency. Eight signing lanes hide work across requests; one lane is not II=1. This overlay describes primitive timing. It is not an end-to-end signature latency claim.
Figure 3. Register-level stage meanings and an ideal no-stall wavefront. Each colored row is a different transaction.

§Connect the Stages to the Code

PipelineLatency / issue contractImplementation
Limb product6 stages, a new independent pair every cycleMul51Cell and mul51_cell_kernel in field_parallel
Field product17 stages, II=1FieldMulPipe
Field add/subtract4 stages, II=1FieldAddSubPipe
Scalar reduction57 stages, II=1ScalarReducePipe
Wide scalar product22 stages, II=1; final modular reduction is separateScalarWideMulPipe
SHA-512 compression242-cycle lane latency; honor readySha512Compress
Full sign / verifyContext scheduling and queueing affect latency and spacingsimulation, 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.

Conceptual FPGA resource map, not actual placement A logical FPGA region contains signing and verification arithmetic, local memory, and control. Signing has 3164 structural DSP sites and verification 3276, totaling 6440 against a project cap of 9000. Whole-core LUT and register counts are unknown. No external scratch-memory port is present. A measured leaf cell uses six DSP slices, 155 LUT primitives, and 928 external flip flops in Yosys. 04 / Map the design onto kinds of FPGA resources CONCEPTUAL ONLY / not placed, not routed, not to scale Registers + LUT control: ready/valid, tags, schedulers, queues, result arbitration / counts unknown Signing arithmetic 3,164 D* across point + scalar paths 8 SHA lanes use logic and registers 128 request contexts overlap work Actual mapped LUT / FF / DSP: unknown Verification arithmetic 3,276 D* across point + scalar paths 4 SHA lanes; independent of signing 64 request contexts overlap work Actual mapped LUT / FF / DSP: unknown Local storage Tables + contextsBRAM / registersNo scratch HBMNo scratch DDR Outside the core: AXI / PCIe shell, host buffers, clock/reset integration; budget and measure these separately. Project caps / 260,000 LUTs / 520,000 FFs / 9,000 DSPs Signing 3,164 D*Verify 3,276 D*2,560 D* budget margin 6,440 / 9,000 = 71.6% structural DSP budget. This bar does not establish physical fit. Measured leaf, not full core: 6 DSP48E2 + 155 LUT primitives + 928 external FFs Yosys xcup / Mul51Cell. DSP register packing and the full-core register cap are still unresolved.
Figure 4. Resource types and project budgets. D* remains a structural estimate even when drawn inside a chip-shaped region.

§What Has Actually Been Counted?

Scope and evidenceDSPsLUT primitivesFFsInterpretation
One six-stage Mul51Cell, Yosys 0.62 xcup, DSP allowed6155928Measured leaf mapping; also 64 CARRY4
The same leaf, -nodsp07,642928Measured leaf mapping; also carries and wide muxes
Whole default RHDL core6,440 D*UnknownUnknownSource structure only; no whole-core fit claim
Project limit9,000260,000520,000Separate 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 storageSize and purposeCode
One logical multi-comb table4 banks * 128 entries * 765 bits = 391,680 bitsmulticomb_table
One physical read-replicated table8 copies * 391,680 = 3,133,440 initialized bitsSame module; replicas enable parallel scans
Three default table instances9,400,320 initialized bits before BRAM packingTwo signing engines + one verification engine
Signing message payloads128 * 64 = 8,192 bytessign; additional context state is separate
Verification message payloads64 * 64 = 4,096 bytesverify; A/R/S and working state are extra
SHA rolling schedules16 * 64 bits per lane; constants are separate logicsha512_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.

Optimization order: prove fit, identify bottleneck, change one thing, remeasure First investigate DSP register absorption and local storage because fit is unresolved. Then measure sustained sign and verification intervals. For a hash-bound signer experiment with more SHA lanes or shorter rounds. For a point-bound design experiment with compression batching and scheduling. Every change must pass cryptographic, protocol, mapping, timing, and throughput checks before becoming a new baseline. 05 / Optimization is a measured loop 1 / Resolve physical fit2 / Find the limiter3 / Change one variable Inspect DSP register absorption.Map storage; count LUTs and FFs.More lanes can worsen this first.Backlog sign and verify together.Measure intervals and occupancy.Pair cycle data with routed MHz.Hash lanes / context count /inversion batch / arithmetic width.Keep strict and secret-safe rules. 4 / Revalidate the entire claim, not only the arithmetic Dalek + malformed cases / ready-valid and tags / RTL equivalence / resources / route / sustained throughput Keep the previous baseline if correctness, a resource cap, or actual performance regresses. Rate = clock frequency / sustained cycles per completed operation A faster simulated schedule and a faster routed clock are separate achievements. Measure both.
Figure 5. A reproducible optimization loop. A hypothesis is not a new performance result.

§Concrete Experiments

Priority / hypothesisWhere to workWhat would count as evidence
First: fit the registers. Encourage DSP internal-register absorption; examine redundant widths/delays.field_parallelSmall 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, EncodedRStoreLower 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, parallelLower 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_compressHigher 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, verifySustained strict-verification gain under mixed traffic; no weaker rejection policy
Trade excess arithmetic for area. Investigate time-sharing low-occupancy scalar pipelines.scalar_parallel, signDSP 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

  1. Read protocol and core. The integration surface is small: key management, independent sign/verify inputs, one result.
  2. Trace one known message through simulation. Confirm its signature with Dalek before changing any arithmetic.
  3. 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.
  4. Change one parameter or stage. Check data widths, register delays, tags, backpressure, malformed inputs, and secret-dependent control.
  5. 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

TermMeaning in this repository
HashA deterministic digest of bytes; SHA-512 is part of signing, not a substitute for a signature
Scalar / field elementIntegers in different modular systems: scalars use group order l; coordinates use prime p
Limb / radixA chunk of a wide number and its place-value base; five 51-bit limbs represent one field element
Point compressionEncode a curve point in 32 bytes; not generic file compression
Multi-combA fixed-base multiplication strategy using precomputed point combinations; see multicomb
LUT / DSP / FF / BRAMConfigurable logic / hardened arithmetic / single-bit register / local block memory
Context / lane / stageStored request state / physical engine / one registered step within an engine
II / latencySpacing between new operations / time for one operation to travel through a block
Synthesis / mappingTurn generated RTL into a graph of device logic and storage resources
Placement / routingChoose physical sites and wires; necessary to establish real timing and fit
MHz / signatures per secondClock 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.