Skip to main content

rhdl_dsig_scalable_core/
ed25519.rs

1#![doc = include_str!("../docs/certificate-bridge.md")]
2//!
3//! ## Where this bridge sits, and why it is so thin
4//!
5//! Every batch of 128 one-time keys needs one Ed25519 signature over its
6//! 64-byte envelope (see [`crate::certificate`]). The Ed25519 math is large
7//! and was already built and tested by a separate project, so DSig reuses
8//! that core unchanged (vendored as `rhdl_ed25519_scalable_core`) instead of
9//! writing its own. This file is only the plug between the two.
10//!
11//! ```text
12//!   crate::cert_unit (bookkeeping: tags, ≤ 32 in flight, in-order delivery)
13//!        │ CertificateSignerInput: key control + epoch, batch_id, root, tag
14//!        ▼
15//!   ┌──────────── CertificateSigner (this file, no state of its own) ───────┐
16//!   │ certificate_request(): batch_commitment_words() → SignRequest          │
17//!   │        │ CachedSignIn                                                 │
18//!   │        ▼                                                              │
19//!   │ Ed25519SignCoreSmall  (SHA-512, key expansion, scalar multiply)       │
20//!   └────────┬──────────────────────────────────────────────────────────────┘
21//!            │ CachedSignOut: ready, valid pulse, signature, tag, occupancy,
22//!            ▼                internal_error (forwarded unchanged)
23//!   crate::cert_unit
24//! ```
25//!
26//! Why no registers or queues here? Every extra stage would be a new place
27//! where a request could be lost, duplicated or reordered, and a new piece of
28//! state that a reset could leave half-finished. Keeping the bridge stateless
29//! means all policy (credits, fault latching, tag records, draining) lives in
30//! one place, [`crate::cert_unit`], where it can be reviewed as a whole.
31//!
32//! Figures: the core cost 25,383 CLB LUTs in context on the routed `slices-2`
33//! link (the planner's [`ED25519_LUT`](crate::config::ED25519_LUT)) and
34//! 25,911 LUTs and 248 DSPs inside the certificate unit of build v13 (routed,
35//! measured). It finishes one signature per ~895 cycles with at least 32
36//! requests in flight (Ed25519 project, simulation), far more than the one
37//! certificate per 128 signatures, about 94,000 per second at 12M signatures
38//! per second, that the engine needs.
39#![forbid(unsafe_code)]
40
41use crate::certificate::batch_commitment_words;
42use rhdl::prelude::*;
43use rhdl_ed25519_scalable_core::{CachedSignIn, Ed25519SignCoreSmall, SignRequest, Words512};
44
45/// Unmodified cached-signer output, including unbackpressured completion and fault.
46pub use rhdl_ed25519_scalable_core::CachedSignOut;
47/// Actual Ed25519 seed/public-key word type used at the bridge boundary.
48pub use rhdl_ed25519_scalable_core::ScalarWords;
49
50/// Key-control signals and one complete DSig certificate request.
51///
52/// This is the bundle of wires the certificate unit drives every clock. The
53/// key-control fields (`load_key_valid`, `key_seed`, `clear_key`) manage the
54/// one Ed25519 signing key the core caches; the request fields (`valid`,
55/// `epoch`, `batch_id`, `root`, `tag`) describe one envelope to sign.
56///
57/// A request transfers only on `valid && output.ready`. The caller binds its
58/// full namespace/root and trusted signer identity to a unique outstanding
59/// [`Self::tag`], and reserves completion capacity before asserting `valid`.
60#[derive(Clone, Copy, Debug, PartialEq, Digital)]
61pub struct CertificateSignerInput {
62    /// Accept a secret seed only while `output.load_key_ready` is asserted.
63    ///
64    /// One-bit strobe: high for the clock on which `key_seed` is offered.
65    pub load_key_valid: bool,
66    /// Secret 32-byte Ed25519 seed in four little-endian 64-bit words.
67    ///
68    /// Write-only: the core never outputs it, only the derived public key.
69    pub key_seed: ScalarWords,
70    /// Clear the cached key only after every accepted request has retired.
71    ///
72    /// Keep `valid` and `load_key_valid` low during clear. This is not a cancel
73    /// handshake and does not erase every intermediate pipeline or memory bit.
74    pub clear_key: bool,
75    /// Marks the namespace/root/tag as a pending signing request.
76    ///
77    /// Hold the request fields stable while `valid` is high and
78    /// `output.ready` is low.
79    pub valid: bool,
80    /// Durably allocated epoch, included in the signed DSig envelope.
81    pub epoch: b64,
82    /// Batch identity within the epoch, included in the signed envelope.
83    pub batch_id: b64,
84    /// Completed 128-key Merkle root, in its original 32-byte order.
85    pub root: [b8; 32],
86    /// Opaque 16-bit routing tag; it is outside the signed message.
87    ///
88    /// Never truncate an epoch/batch identity into this field as a substitute
89    /// for a full outstanding-request record. Reject unknown or reused tags.
90    pub tag: b16,
91}
92
93impl Default for CertificateSignerInput {
94    /// All strobes low and all data zero: an idle input that requests nothing.
95    fn default() -> Self {
96        Self {
97            load_key_valid: false,
98            key_seed: ScalarWords { words: [b64(0); 4] },
99            clear_key: false,
100            valid: false,
101            epoch: b64(0),
102            batch_id: b64(0),
103            root: [b8(0); 32],
104            tag: b16(0),
105        }
106    }
107}
108
109/// Pack the actual DSig fixed-64 message into the actual Ed25519 request type.
110///
111/// Calls [`batch_commitment_words`] directly. No host prehash, duplicated
112/// domain/profile constants, endian conversion, or intermediate register is
113/// inserted. The returned tag is routing metadata, not a signed field.
114///
115/// Inputs: 64-bit `epoch` and `batch_id`, the 32-byte `root` and the 16-bit
116/// `tag`. Output: the core's `SignRequest` (eight 64-bit message words plus
117/// the tag). Timing: combinational, zero cycles.
118#[kernel]
119pub fn certificate_request(epoch: b64, batch_id: b64, root: [b8; 32], tag: b16) -> SignRequest {
120    SignRequest {
121        message: Words512 {
122            words: batch_commitment_words(epoch, batch_id, root),
123        },
124        tag,
125    }
126}
127
128/// Direct RHDL composition of one DSig packer and exactly one cached signer.
129///
130/// The sole stateful child is the Ed25519 project's scalable RHDL core,
131/// `Ed25519SignCoreSmall` = `Ed25519SignCore<1, 1, 1, 0>` (vendored in
132/// `vendor/ed25519-scalable-core`; one
133/// fixed-base machine, one SHA-512 ring, one scalar ring, 128 contexts). It
134/// accepts up to 32 requests in flight and may finish them out of order. This
135/// wrapper adds no FIFO, register, controller, admission policy, credit state,
136/// fault latch, or output backpressure. Its output is [`CachedSignOut`] itself.
137///
138/// The caller must reserve every completion slot in advance, latch any
139/// `internal_error` before processing a coincident `valid` pulse, bind the
140/// returned tag to the full namespace/root/signer identity, and drain before
141/// clearing. Quarantine outstanding requests across reset; neither reset nor
142/// clear proves cancellation or grants permission to reuse a DSig allocation.
143/// No bank may be marked certified solely because this wrapper emitted bytes.
144#[derive(Clone, Debug, Default, Synchronous, SynchronousDQ)]
145pub struct CertificateSigner {
146    /// The vendored Ed25519 signing core; the only stateful part.
147    signer: Ed25519SignCoreSmall,
148}
149
150impl SynchronousIO for CertificateSigner {
151    /// Input: key control plus one certificate request.
152    type I = CertificateSignerInput;
153    /// Output: the core's own output, unchanged.
154    type O = CachedSignOut;
155    /// The wiring kernel that runs every clock.
156    type Kernel = certificate_signer_kernel;
157}
158
159/// Stateless wiring for [`CertificateSigner`]; clock/reset reaches its child
160/// through RHDL's synchronous hierarchy, and every child output is forwarded.
161///
162/// In RHDL a component's kernel is called every clock with the inputs `input`
163/// and the children's current outputs `q`, and returns this component's
164/// outputs plus the children's next inputs `d`. Here `d` is the input
165/// mapped field by field (the request packed by [`certificate_request`]) and
166/// the output is `q.signer` as is. Timing: combinational; all latency is the
167/// core's.
168#[kernel]
169pub fn certificate_signer_kernel(
170    _clock_reset: ClockReset,
171    input: CertificateSignerInput,
172    q: CertificateSignerQ,
173) -> (CachedSignOut, CertificateSignerD) {
174    // Next inputs for the core: key control passes through, the request is packed.
175    let d = CertificateSignerD {
176        signer: CachedSignIn {
177            load_key_valid: input.load_key_valid,
178            key_seed: input.key_seed,
179            clear_key: input.clear_key,
180            valid: input.valid,
181            request: certificate_request(input.epoch, input.batch_id, input.root, input.tag),
182        },
183    };
184    // Forward the core's output untouched, faults and all.
185    (q.signer, d)
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    /// The kernel forwards every control bit and every child output, even an
193    /// illegal mix, without masking anything.
194    #[test]
195    fn wiring_preserves_coincident_completion_fault_and_control_signals() {
196        let input = CertificateSignerInput {
197            load_key_valid: true,
198            key_seed: ScalarWords {
199                words: [b64(0x81); 4],
200            },
201            clear_key: true,
202            valid: true,
203            epoch: b64(0x8000_0000_0000_0001),
204            batch_id: b64(0xfedc_ba98_7654_3210),
205            root: [b8(0x42); 32],
206            tag: b16(0xf135),
207        };
208        let child = CachedSignOut {
209            load_key_ready: false,
210            key_loaded: true,
211            public_key: ScalarWords {
212                words: [b64(0xab); 4],
213            },
214            ready: true,
215            valid: true,
216            signature: Words512 {
217                words: [b64(0xcd); 8],
218            },
219            tag: b16(0xf135),
220            occupancy: b8(3),
221            internal_error: true,
222        };
223        // Deliberately illegal control coincidence is forwarded, not silently
224        // made safe. This is a combinational wiring test, not a core transaction.
225        let (output, d) = certificate_signer_kernel(
226            clock_reset(clock(false), reset(false)),
227            input,
228            CertificateSignerQ { signer: child },
229        );
230        assert_eq!(output, child);
231        assert!(d.signer.load_key_valid && d.signer.clear_key && d.signer.valid);
232        assert_eq!(d.signer.key_seed, input.key_seed);
233        assert_eq!(
234            d.signer.request,
235            certificate_request(input.epoch, input.batch_id, input.root, input.tag)
236        );
237    }
238}