Skip to main content

rhdl_ed25519_scalable_core/
ed25519_sign_core.rs

1//! [`Ed25519SignCore`]: the Ed25519 signer with the `CachedSignIn` /
2//! `CachedSignOut` ports, for embedding in another RHDL design (DSig's
3//! certificate unit uses [`Ed25519SignCoreSmall`]).
4//!
5//! [`Ed25519SignCoreSmall`] = `Ed25519SignCore<1, 1, 1, 0>`: one fused fixed-base machine,
6//! one SHA-512 ring, one scalar ring, 128 sign contexts. Embed it as a child
7//! field; never import generated Verilog.
8//!
9//! The wrapper adds no logic of its own beyond port renaming: it fixes the
10//! controller's context width to [`SMALL_CTX_BITS`], disables the external
11//! SHA-512 port (`SH = 0`), and saturates the 12-bit in-flight count to the
12//! 8-bit `occupancy` field of the original port layout.
13//!
14//! ```rust,ignore
15//! #[derive(Clone, Debug, Synchronous, SynchronousDQ, Default)]
16//! pub struct MyDesign {
17//!     signer: rhdl_ed25519_scalable_core::Ed25519SignCoreSmall,
18//! }
19//! ```
20//!
21//! # Handshake
22//!
23//! - `load_key_valid` + `key_seed`: load a seed when `load_key_ready` (high
24//!   only when idle **and** no key is loaded; `clear_key` with all requests
25//!   drained before loading another).
26//! - `valid` + `request` (64-byte `message`, 16-bit `tag`): accepted when
27//!   `ready` is high that cycle. `ready` is not high every cycle even with
28//!   room; offer whenever it is.
29//! - `valid` out is a one-cycle pulse with `tag` and `signature` (`R ‖ S`):
30//!   no backpressure, at most one per cycle, possibly out of order.
31//! - `occupancy`: requests in flight, saturated at 255. `internal_error` is a
32//!   sticky invariant violation.
33//!
34//! The message is signed exactly as given (no prehash, no domain prefix).
35//!
36//! # Performance (native simulation, 205 MHz)
37//!
38//! | Quantity | Value |
39//! |---|---:|
40//! | key load | 21,509 cycles, once |
41//! | one request, in → out | 22,240 cycles |
42//! | saturated | ~895 cycles/signature (~229k/s) with ~32 in flight |
43//! | CLB LUT / FF / DSP / BRAM36 | 26,963 / 29,973 / 248 / 88 (+2 BRAM18), Vivado routed out of context |
44//! | timing at 4.878 ns | WNS −0.273 ns before the scalar-ring fix; re-route pending |
45//! | lowering as a child | ~190 MB with the HDL-only export ([`crate::export`]) |
46
47use rhdl::prelude::*;
48
49use super::sign_core::{ScalableSignCore, ScalableSignIn};
50
51/// Sign-context index width of [`Ed25519SignCore`]: 128 contexts. Fused mode
52/// admits at most `32 * NFB` requests (one credit per fixed-base context),
53/// so more contexts would only cost BRAM.
54///
55/// The controller asserts at construction that its admission credits fit in
56/// the contexts, so with this width only `NFB <= 4` and `NCP = 0` (fused
57/// mode) can be built; a compress machine needs 2,048 contexts.
58pub const SMALL_CTX_BITS: usize = 7;
59use crate::sha_ring::{ShaDone, ShaRingOut};
60use crate::wire::{CachedSignIn, CachedSignOut};
61
62/// The small size: one fused fixed-base machine, one SHA-512
63/// ring, one scalar ring, no batch-compress machine.
64pub type Ed25519SignCoreSmall = Ed25519SignCore<1, 1, 1, 0>;
65
66/// `CachedSignCore`-compatible wrapper around [`ScalableSignCore`].
67///
68/// `NFB`, `NSHA`, `NSC` and `NCP` are the counts of fixed-base machines,
69/// SHA-512 rings, scalar rings and batch-compress machines, passed through
70/// to the controller unchanged.
71#[derive(Clone, Debug, Synchronous, SynchronousDQ, Default)]
72pub struct Ed25519SignCore<const NFB: usize, const NSHA: usize, const NSC: usize, const NCP: usize>
73{
74    /// The controller, with [`SMALL_CTX_BITS`] sign contexts and its own
75    /// SHA-512 rings (`SH = 0`).
76    core: ScalableSignCore<NFB, NSHA, NSC, NCP, SMALL_CTX_BITS, 0>,
77}
78
79/// Binds the `CachedSignIn` / `CachedSignOut` ports and the adapter kernel.
80impl<const NFB: usize, const NSHA: usize, const NSC: usize, const NCP: usize> SynchronousIO
81    for Ed25519SignCore<NFB, NSHA, NSC, NCP>
82{
83    type I = CachedSignIn;
84    type O = CachedSignOut;
85    type Kernel = ed25519_sign_kernel<NFB, NSHA, NSC, NCP>;
86}
87
88/// Port adapter for [`Ed25519SignCore`].
89///
90/// Purely combinational: inputs go to the controller unchanged, outputs come
91/// back with `in_flight` saturated to 8 bits. The controller's shared-SHA
92/// input is tied idle because it is built with `SH = 0` and ignores it.
93#[kernel]
94pub fn ed25519_sign_kernel<const NFB: usize, const NSHA: usize, const NSC: usize, const NCP: usize>(
95    _cr: ClockReset,
96    i: CachedSignIn,
97    q: Ed25519SignCoreQ<NFB, NSHA, NSC, NCP>,
98) -> (CachedSignOut, Ed25519SignCoreD<NFB, NSHA, NSC, NCP>) {
99    // Controller outputs for this cycle.
100    let o = q.core;
101    // The 12-bit in-flight count saturated to the 8-bit `occupancy` field.
102    // With 128 contexts it never exceeds 128, so the clamp only matters for
103    // other context widths.
104    let occupancy = if o.in_flight > b12(255) {
105        b8(255)
106    } else {
107        o.in_flight.resize::<8>()
108    };
109    let out = CachedSignOut {
110        load_key_ready: o.load_key_ready,
111        key_loaded: o.key_loaded,
112        public_key: o.public_key,
113        ready: o.ready,
114        valid: o.valid,
115        signature: o.signature,
116        tag: o.tag,
117        occupancy: occupancy,
118        internal_error: o.internal_error,
119    };
120    let d = Ed25519SignCoreD::<NFB, NSHA, NSC, NCP> {
121        core: ScalableSignIn {
122            load_key_valid: i.load_key_valid,
123            key_seed: i.key_seed,
124            clear_key: i.clear_key,
125            valid: i.valid,
126            request: i.request,
127            // No shared SHA-512 port: never ready, never a completed digest.
128            sha_ext: ShaRingOut { ready: false, done: ShaDone { valid: false, tag: b16(0), state: [b64(0); 8] } },
129        },
130    };
131    (out, d)
132}