Skip to main content

rhdl_ed25519_scalable_core/
sign_core.rs

1//! The controller: how requests flow through the engines, and why the flow
2//! cannot deadlock.
3//!
4//! [`ScalableSignCore`] instantiates `NFB` fixed-base machines, `NSHA`
5//! SHA-512 rings, `NSC` scalar rings and `NCP` (0 or 1) batch-compress
6//! machines, and moves every request through them:
7//!
8#![doc = concat!("<div style=\"overflow-x:auto\">", include_str!("../docs/images/dataflow.svg"), "</div>")]
9//!
10//! # Handshakes and parking
11//!
12//! Every arrow is a ready/valid handshake. A producer that cannot hand off
13//! *parks*: rings keep the finished job circulating, machines retry their
14//! `OUT` bundle. Nothing is dropped and no queue can overflow.
15//!
16//! # Credits: the deadlock argument
17//!
18//! The SHA-512 and scalar rings are shared by the first half of signing
19//! (nonce hash, `r`) and the second (challenge hash, `S`). Without care, the
20//! loop SHA → scalar → fixed-base → compress → SHA can fill up and stop.
21//! The rule that prevents it: **a request is admitted only with a credit**, a
22//! reserved batch-compress slot (in fused mode, a fixed-base context),
23//! returned when the request leaves that stage. With its slot reserved,
24//! every fixed-base output has somewhere to go, the fixed-base machines always
25//! drain, and every remaining wait points strictly downstream.
26//!
27//! # Credits: the throughput limit
28//!
29//! A credit is held from admission until its compress batch retires: about
30//! 12.5k cycles of fixed-base program (392 bundles × 32 contexts), 27k of
31//! batch compression (423 bundles × 64 contexts) and ~8k of hashing,
32//! queueing and filling. By Little's law the credit pool caps throughput at
33//! *credits ÷ hold time*: with 2,048 credits, about one signature per 23
34//! cycles. (With the first design's 1,024 credits and longer hold, the cap
35//! was ~44 cycles, which is why 12/6/2/1 and 13/8/2/1 measured 45.7 and 44.1
36//! however many machines they had.)
37//!
38//! # Contexts
39//!
40//! Messages, `r`, `R` and user tags live in BRAM indexed by a sign context.
41//! There are `2^CB` contexts: [`CTX_BITS`] = 11 (2,048) for throughput
42//! profiles; [`Ed25519SignCore`](crate::ed25519_sign_core::Ed25519SignCore) uses 7, since fused
43//! mode never admits more than `32 · NFB` requests.
44//!
45//! # Other behaviour
46//!
47//! - Fixed-base outputs fill one compress batch of [`COMPRESS_BATCH`] points
48//!   at a time; a partial batch is flushed after 64 idle cycles when enough
49//!   credits remain to pay for its empty slots, so a trickle still completes.
50//! - Key expansion uses the same SHA ring, scalar ring and fixed-base
51//!   machine; `load_key_ready` requires an idle signer with no key loaded.
52//! - Scalars are recoded in hardware by [`recode_mixed`].
53//! - A single request takes ~22k cycles on 1/1/1/0 and ~45–50k on 15/11/3/1;
54//!   throughput needs many requests in flight.
55//!
56//! # Job tags
57//!
58//! Every SHA-512, scalar, fixed-base and compress job carries a 16-bit tag:
59//! bits 15..11 are the job kind (`KIND_NONCE` = 0, `KIND_CHALLENGE` = 1,
60//! `KIND_KEY` = 2) and bits 10..0 the sign context. A completion therefore
61//! says which step finished and where its request's state lives, so no
62//! engine needs to finish in order. Key jobs use context 0; they only run
63//! while no request is in flight, so they cannot collide with a request.
64//!
65//! # Steps of one request
66//!
67//! 1. Admission: take a free context and a credit, store `M` and the user
68//!    tag, start `SHA-512(prefix || M)` (kind nonce).
69//! 2. Scalar ring: `r = digest mod l`; `r` is stored for step 5.
70//! 3. Fixed-base machine: `[r]B`, then either affine conversion in the same
71//!    machine (fused, `NCP = 0`) or batch conversion in the compress stage.
72//! 4. The encoding `R` is stored and `SHA-512(R || A || M)` starts (kind
73//!    challenge).
74//! 5. Scalar ring: `S = k * a + r mod l`.
75//! 6. Output `R || S` with the user tag and return the context.
76//!
77//! Key loading runs the same engines with kind key: `SHA-512(seed)`,
78//! `a mod l`, `[a]B`, and the encoding becomes the cached public key.
79
80use rhdl::prelude::*;
81use rhdl_fpga::core::{
82    dff::DFF,
83    ram::synchronous::{In as RamIn, SyncBRAM, Write as RamWrite},
84};
85
86use super::{
87    compress_stage::{CompressIn, CompressStage, PointIn},
88    fe17::Fe17,
89    machine::{DigitsWrite, FieldMachine, InputWrite, MachineIn, StartReq, TableWrite, no_key},
90    programs::{fixed_base, fixed_base_affine},
91    scalar_ring::{ScalarJob, ScalarRing, ScalarRingIn},
92    sha_ring::{ShaDone, ShaJob, ShaRing, ShaRingIn, ShaRingOut},
93};
94use crate::{
95    digest::{prefix_from_digest, reverse_bytes, secret_from_digest, words256},
96    wire::{ScalarWords, SignRequest, Words512},
97};
98
99/// Fixed-base machine: 32 contexts (`KB = 5`), a 1024-bundle program ROM
100/// (`PB = 10`) and a `TB = 9` table scanner.
101///
102/// Bank widths follow [`FieldMachine`]'s `index * K + ctx` addressing:
103/// `RA = 9` gives 16 bank-M/A registers per context and `IA = 5` one bank-I
104/// register per context (the fixed-base program takes no inputs; its scalar
105/// arrives as scan digits).
106pub type FbMachine = FieldMachine<5, 32, 9, 5, 10, 9, 1>;
107// Compress-stage sizes and machine type, re-exported so users of the
108// controller need not import `compress_stage`.
109pub use crate::compress_stage::{COMPRESS_BATCH, COMPRESS_CONTEXTS, CpMachine};
110/// Sign-context index width of the throughput profiles (2048 requests in
111/// flight); the `CB` parameter of [`ScalableSignCore`] may be smaller.
112/// Context fields in registers and tags stay 11 bits wide regardless of
113/// `CB`; they are resized to `CB` only at the memory ports.
114pub const CTX_BITS: usize = 11;
115
116/// Tag kind (bits 15..11): `SHA-512(prefix || M)`, then `r` and `[r]B`.
117const KIND_NONCE: u128 = 0;
118/// Tag kind: `SHA-512(R || A || M)`, then `S`.
119const KIND_CHALLENGE: u128 = 1;
120/// Tag kind: key expansion, `SHA-512(seed)`, then `a` and `A = [a]B`.
121const KIND_KEY: u128 = 2;
122
123/// Public input of [`ScalableSignCore`].
124#[derive(Copy, Clone, Debug, PartialEq, Digital)]
125pub struct ScalableSignIn {
126    /// Load and expand `key_seed` when `load_key_ready` is high in the same
127    /// cycle.
128    pub load_key_valid: bool,
129    /// Secret Ed25519 seed, sampled in the cycle the load is accepted.
130    pub key_seed: ScalarWords,
131    /// Clear the cached key (prefix, `a`, public key) at the next edge.
132    pub clear_key: bool,
133    /// Sign `request` when `ready` is high in the same cycle.
134    pub valid: bool,
135    /// Signing request.
136    pub request: SignRequest,
137    /// The shared SHA-512 port (`SH = 1`; ignored otherwise), as a ring's
138    /// outputs: see [`crate::sha_client::SharedShaPort`].
139    pub sha_ext: ShaRingOut,
140}
141
142/// Public output of [`ScalableSignCore`].
143#[derive(Copy, Clone, Debug, PartialEq, Digital)]
144pub struct ScalableSignOut {
145    /// A key load may be presented: the signer is idle, no key is loaded and
146    /// a SHA-512 port can take the seed block this cycle. Combinational.
147    pub load_key_ready: bool,
148    /// A key is cached.
149    pub key_loaded: bool,
150    /// Cached public key (valid while `key_loaded`).
151    pub public_key: ScalarWords,
152    /// A signing request may be presented. Combinational: it needs a cached
153    /// key, a free context, a credit, a SHA-512 port ready this cycle, and no
154    /// challenge hash waiting for that port.
155    pub ready: bool,
156    /// One-cycle completion pulse.
157    pub valid: bool,
158    /// Tag of the completed request.
159    pub tag: b16,
160    /// `R || S`: words 0..4 are `R`, words 4..8 are `S`.
161    pub signature: Words512,
162    /// Sticky internal invariant violation: the credit count left its pool
163    /// (only checked invariant so far).
164    pub internal_error: bool,
165    /// Sign contexts currently in use (admitted, not yet output). A key load
166    /// is not counted.
167    pub in_flight: b12,
168    /// Jobs for the shared SHA-512 port (`SH = 1`; idle otherwise).
169    pub sha_ext: ShaRingIn,
170}
171
172/// Challenge-hash stage: holds `R` for one request until a SHA-512 port
173/// takes `R || A || M`.
174#[derive(Copy, Clone, Debug, PartialEq, Digital)]
175#[doc(hidden)]
176pub struct H2Stage {
177    /// The stage holds a request.
178    valid: bool,
179    /// [`Self::msg`] has been loaded; the `msg` BRAM read issued when the
180    /// stage was filled returns one cycle later.
181    has_msg: bool,
182    /// Sign context of the request.
183    ctx: b11,
184    /// Encoded `R`, little-endian words.
185    r_enc: [b64; 4],
186    /// The request's 64-byte message.
187    msg: Words512,
188}
189
190/// Scalar-ring dispatch stage: one `(x * y + z) mod l` job waiting for a
191/// free scalar ring.
192#[derive(Copy, Clone, Debug, PartialEq, Digital)]
193#[doc(hidden)]
194pub struct ScStage {
195    /// The stage holds a job.
196    valid: bool,
197    /// Challenge job still waiting for its addend `r` from the `r_sc` BRAM
198    /// (read issued the cycle the stage was filled); blocks dispatch.
199    needs_r: bool,
200    /// Job tag (kind and sign context).
201    tag: b16,
202    /// Multiplicand, 31 little-endian 17-bit digits (527 bits): the
203    /// 512-bit digest, or the 255-bit clamped secret for a key job.
204    x: [b17; 31],
205    /// Multiplier is the cached secret `a` (challenge jobs); otherwise 1.
206    y_is_a: bool,
207    /// Addend: `r` for challenge jobs, zero otherwise.
208    z: [b17; 15],
209}
210
211/// Fixed-base dispatch stage: one recoded scalar waiting for a free
212/// fixed-base context.
213#[derive(Copy, Clone, Debug, PartialEq, Digital)]
214#[doc(hidden)]
215pub struct FbStage {
216    /// The stage holds a scalar.
217    valid: bool,
218    /// Job tag (kind nonce or key, and the sign context).
219    tag: b16,
220    /// Signed digits from [`recode_mixed`]; entries 54..64 are zero.
221    digits: [b6; 64],
222}
223
224/// Signature output stage: lasts exactly one cycle, while the `r_out` and
225/// `user_tag` reads issued the cycle before return.
226#[derive(Copy, Clone, Debug, PartialEq, Digital)]
227#[doc(hidden)]
228pub struct FinStage {
229    /// A signature is output this cycle.
230    valid: bool,
231    /// Sign context, returned to the free list this cycle.
232    ctx: b11,
233    /// `S`, fifteen 17-bit digits (canonical, below `l`).
234    s: [b17; 15],
235}
236
237/// Fused-mode (`NCP = 0`) point stage: affine `y` of `[r]B` or `[a]B`
238/// waiting for its `x` sign and then for the challenge stage. Its layout
239/// mirrors the compress stage's output register; `has_tag` here means the
240/// sign of `x` has been read.
241#[derive(Copy, Clone, Debug, PartialEq, Digital)]
242#[doc(hidden)]
243pub struct CpOutStage {
244    /// The stage holds a point.
245    valid: bool,
246    /// [`Self::x_negative`] has been loaded from the `xsign` BRAM (one cycle
247    /// after the read was issued).
248    has_tag: bool,
249    /// Fixed-base job tag (kind and sign context).
250    tag: b16,
251    /// Set to true on load and not read by this controller.
252    keep: bool,
253    /// Sign of affine `x`.
254    x_negative: bool,
255    /// Canonical affine `y`.
256    y: Fe17,
257}
258
259/// All controller registers of [`ScalableSignCore`].
260#[derive(Copy, Clone, Debug, PartialEq, Digital)]
261#[doc(hidden)]
262pub struct SignRegs<const NFB: usize> {
263    // Key cache.
264    /// A key is cached and requests may be admitted.
265    key_loaded: bool,
266    /// Key expansion is running (seed accepted, public key not yet encoded).
267    keygen: bool,
268    /// A key clear arrived while an expansion was running: until the
269    /// expansion ends, everything it writes is wiped each cycle and its
270    /// result is discarded.
271    key_cancel: bool,
272    /// Nonce prefix: upper half of `SHA-512(seed)`.
273    prefix: ScalarWords,
274    /// Secret scalar `a mod l`, multiplier of every challenge job.
275    a_digits: [b17; 15],
276    /// Encoded public key `A`, little-endian words.
277    public_key: [b64; 4],
278    // Free sign-context FIFO (BRAM storage).
279    /// [`Self::free_head`] holds a free context ready for the next admission.
280    free_head_valid: bool,
281    /// The next context to hand out; kept in a register so admission does
282    /// not wait for a BRAM read.
283    free_head: b11,
284    /// A `free_list` read was issued last cycle; its data arrives this cycle.
285    free_fetching: bool,
286    /// FIFO read pointer. Only its low `CB` bits address the memory, so the
287    /// 11-bit counter wraps correctly for any `CB`.
288    free_rd: b11,
289    /// FIFO write pointer, same wrapping as [`Self::free_rd`].
290    free_wr: b11,
291    /// Entries in FIFO storage, not counting the head or a fetch in flight.
292    free_count: b12,
293    /// Number of contexts, `2^CB`; constant after reset.
294    ctx_total: b12,
295    // Pipeline stages.
296    /// Challenge-hash stage.
297    h2: H2Stage,
298    /// Scalar-ring dispatch stage.
299    sc: ScStage,
300    /// Fixed-base dispatch stage.
301    fb: FbStage,
302    /// Signature output stage.
303    fin: FinStage,
304    // Fixed-base contexts (bit set = free) and compress-slot credits.
305    /// Per fixed-base machine, bit `c` set when context `c` is free.
306    fb_free: [b32; NFB],
307    /// Admission credits left: free fixed-base contexts (fused mode) or
308    /// unreserved compress slots (compress mode).
309    credits: b12,
310    // Fused-mode point stage (named after the batch-compress output stage
311    // it replaces).
312    /// Fused-mode point stage.
313    cpo: CpOutStage,
314    /// Sticky error flag behind `internal_error`; reset to false and not
315    /// set by the current kernel.
316    error: bool,
317}
318
319
320/// Controller memories (all inferred BRAM), each `2^CB` deep and indexed by
321/// sign context, except `free_list`, which is indexed by FIFO position.
322/// Every read has one cycle of latency, which is why several stages wait a
323/// cycle (`has_msg`, `needs_r`, `has_tag`) before using read data.
324#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
325pub struct SignMems<const CB: usize>
326where
327    rhdl::bits::W<CB>: BitWidth,
328{
329    /// Message of each request, read again for the challenge hash.
330    msg: SyncBRAM<Words512, CB>,
331    /// Nonce scalar `r`, written by the nonce job, read as the challenge
332    /// job's addend.
333    r_sc: SyncBRAM<[b17; 15], CB>,
334    /// Encoded `R`, written before the challenge hash, read at output.
335    r_out: SyncBRAM<[b64; 4], CB>,
336    /// The caller's tag, written at admission, read at output.
337    user_tag: SyncBRAM<b16, CB>,
338    /// Free-context FIFO storage, preloaded with every context.
339    free_list: SyncBRAM<b11, CB>,
340    /// Fused mode: sign of affine `x`, written when `x` leaves a fixed-base
341    /// machine and read when `y` follows.
342    xsign: SyncBRAM<bool, CB>,
343}
344
345/// Port bundle of [`SignMems`].
346#[derive(Copy, Clone, Debug, PartialEq, Digital)]
347pub struct SignMemsIn<const CB: usize>
348where
349    rhdl::bits::W<CB>: BitWidth,
350{
351    /// Messages by sign context.
352    pub msg: RamIn<Words512, CB>,
353    /// Nonce scalars `r`.
354    pub r_sc: RamIn<[b17; 15], CB>,
355    /// Encoded `R`.
356    pub r_out: RamIn<[b64; 4], CB>,
357    /// User tags.
358    pub user_tag: RamIn<b16, CB>,
359    /// Free-context FIFO storage.
360    pub free_list: RamIn<b11, CB>,
361    /// Fused mode: sign of affine x by sign context.
362    pub xsign: RamIn<bool, CB>,
363}
364
365/// Read data of [`SignMems`], valid one cycle after the read address.
366#[derive(Copy, Clone, Debug, PartialEq, Digital)]
367pub struct SignMemsOut {
368    /// Message.
369    pub msg: Words512,
370    /// Nonce scalar.
371    pub r_sc: [b17; 15],
372    /// Encoded `R`.
373    pub r_out: [b64; 4],
374    /// User tag.
375    pub user_tag: b16,
376    /// Free-list entry.
377    pub free_list: b11,
378    /// Sign of x.
379    pub xsign: bool,
380}
381
382/// Binds the memory ports and their wiring kernel.
383impl<const CB: usize> SynchronousIO for SignMems<CB>
384where
385    rhdl::bits::W<CB>: BitWidth,
386{
387    type I = SignMemsIn<CB>;
388    type O = SignMemsOut;
389    type Kernel = sign_mems_kernel<CB>;
390}
391
392/// Wiring for [`SignMems`]: ports pass straight to the six BRAMs.
393#[kernel]
394pub fn sign_mems_kernel<const CB: usize>(
395    _cr: ClockReset,
396    i: SignMemsIn<CB>,
397    q: SignMemsQ<CB>,
398) -> (SignMemsOut, SignMemsD<CB>)
399where
400    rhdl::bits::W<CB>: BitWidth,
401{
402    let mut d = SignMemsD::<CB>::dont_care();
403    d.msg = i.msg;
404    d.r_sc = i.r_sc;
405    d.r_out = i.r_out;
406    d.user_tag = i.user_tag;
407    d.free_list = i.free_list;
408    d.xsign = i.xsign;
409    (
410        SignMemsOut {
411            msg: q.msg,
412            r_sc: q.r_sc,
413            r_out: q.r_out,
414            user_tag: q.user_tag,
415            free_list: q.free_list,
416            xsign: q.xsign,
417        },
418        d,
419    )
420}
421
422/// Resource-scalable cached-key Ed25519 signer with `2^CB` sign contexts
423/// (`CB <= 11`; [`CTX_BITS`] for the throughput profiles).
424///
425/// - `NFB`: fixed-base machines (32 contexts each).
426/// - `NSHA`: SHA-512 rings (still instantiated when `SH = 1`, but unused).
427/// - `NSC`: scalar rings.
428/// - `NCP`: batch-compress stages, 0 (fused: each fixed-base machine converts
429///   its own result to affine) or 1.
430/// - `CB`: sign-context index width.
431/// - `SH`: 1 sends all hashing to the external port `sha_ext`.
432#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
433#[rhdl(dq_no_prefix)]
434pub struct ScalableSignCore<
435    const NFB: usize,
436    const NSHA: usize,
437    const NSC: usize,
438    const NCP: usize,
439    const CB: usize,
440    const SH: usize,
441> where
442    rhdl::bits::W<CB>: BitWidth,
443{
444    /// Controller registers.
445    regs: DFF<SignRegs<NFB>>,
446    /// Per-context BRAMs and the free-context FIFO.
447    mems: SignMems<CB>,
448    /// Fixed-base machines.
449    fb: [FbMachine; NFB],
450    /// SHA-512 rings.
451    sha: [ShaRing; NSHA],
452    /// Scalar multiply-add rings modulo `l`.
453    sc: [ScalarRing; NSC],
454    /// Batch-compress stage (zero or one).
455    cp: [CompressStage; NCP],
456}
457
458/// Reset state: no key, every context in the free list, all credits
459/// available, and each fixed-base machine loaded with the program for the
460/// chosen mode. Panics if the parameters break the credit invariants.
461impl<const NFB: usize, const NSHA: usize, const NSC: usize, const NCP: usize, const CB: usize, const SH: usize>
462    Default for ScalableSignCore<NFB, NSHA, NSC, NCP, CB, SH>
463where
464    rhdl::bits::W<CB>: BitWidth,
465{
466    fn default() -> Self {
467        assert!(NFB >= 1 && NSHA >= 1 && NSC >= 1 && NCP <= 1 && SH <= 1);
468        assert!(CB <= CTX_BITS);
469        // Admission credits must never exceed the sign contexts: 32 per
470        // fixed-base machine in fused mode, 2,048 compress slots otherwise.
471        assert!(NFB * 32 <= 1 << CB);
472        assert!(NCP == 0 || COMPRESS_CONTEXTS * COMPRESS_BATCH <= 1 << CB);
473        // Fused mode ends each fixed-base program with its own inversion
474        // and outputs affine x, y; compress mode outputs projective X, Y, Z.
475        let fb_program = if NCP == 0 {
476            fixed_base_affine()
477        } else {
478            fixed_base()
479        };
480        Self {
481            regs: DFF::new(SignRegs {
482                key_loaded: false,
483                keygen: false,
484                key_cancel: false,
485                prefix: ScalarWords { words: [b64(0); 4] },
486                a_digits: [b17(0); 15],
487                public_key: [b64(0); 4],
488                free_head_valid: false,
489                free_head: b11(0),
490                free_fetching: false,
491                free_rd: b11(0),
492                free_wr: b11(0),
493                free_count: b12(1 << CB),
494                ctx_total: b12(1 << CB),
495                h2: H2Stage {
496                    valid: false,
497                    has_msg: false,
498                    ctx: b11(0),
499                    r_enc: [b64(0); 4],
500                    msg: Words512::zero(),
501                },
502                sc: ScStage {
503                    valid: false,
504                    needs_r: false,
505                    tag: b16(0),
506                    x: [b17(0); 31],
507                    y_is_a: false,
508                    z: [b17(0); 15],
509                },
510                fb: FbStage {
511                    valid: false,
512                    tag: b16(0),
513                    digits: [b6(0); 64],
514                },
515                fin: FinStage {
516                    valid: false,
517                    ctx: b11(0),
518                    s: [b17(0); 15],
519                },
520                fb_free: [b32(0xffff_ffff); NFB],
521                // One credit per fixed-base context (fused) or per compress
522                // slot (compress mode).
523                credits: b12(if NCP == 0 {
524                    (32 * NFB) as u128
525                } else {
526                    (COMPRESS_CONTEXTS * COMPRESS_BATCH) as u128
527                }),
528                cpo: CpOutStage {
529                    valid: false,
530                    has_tag: false,
531                    tag: b16(0),
532                    keep: false,
533                    x_negative: false,
534                    y: Fe17::zero(),
535                },
536                error: false,
537            }),
538            mems: SignMems {
539                msg: SyncBRAM::default(),
540                r_sc: SyncBRAM::default(),
541                r_out: SyncBRAM::default(),
542                user_tag: SyncBRAM::default(),
543                // Entry k holds context k: the FIFO starts full.
544                free_list: SyncBRAM::new((0..1u128 << CB).map(|k| (bits::<CB>(k), b11(k)))),
545                xsign: SyncBRAM::default(),
546            },
547            // The first `URAM_MACHINES` machines keep their banks in
548            // UltraRAM; the rest take the block-RAM fallback (the planner's
549            // placement when a build runs out of UltraRAM).
550            fb: std::array::from_fn(|k| FbMachine::with_memory(&fb_program, crate::machine::placement(k))),
551            sha: std::array::from_fn(|_| ShaRing::default()),
552            sc: std::array::from_fn(|_| ScalarRing::default()),
553            cp: std::array::from_fn(|_| CompressStage::default()),
554        }
555    }
556}
557
558/// Binds the public ports and the controller kernel.
559impl<const NFB: usize, const NSHA: usize, const NSC: usize, const NCP: usize, const CB: usize, const SH: usize>
560    SynchronousIO for ScalableSignCore<NFB, NSHA, NSC, NCP, CB, SH>
561where
562    rhdl::bits::W<CB>: BitWidth,
563{
564    type I = ScalableSignIn;
565    type O = ScalableSignOut;
566    type Kernel = scalable_sign_kernel<NFB, NSHA, NSC, NCP, CB, SH>;
567}
568
569/// Little-endian 64-bit words to thirty-one 17-bit digits.
570///
571/// Digit `k` is bits `17k .. 17k + 17` of the 512-bit value (527 bits of
572/// digits, the top 15 zero). This is the multiplicand format of the scalar
573/// ring.
574#[kernel]
575pub fn words_to_digits31(w: [b64; 8]) -> [b17; 31] {
576    let mut out = [b17(0); 31];
577    for k in 0..31 {
578        for word in 0..8 {
579            // Digit k starts in this word. The loop bounds are constants, so
580            // after unrolling exactly one word matches per digit.
581            if 17 * k >= 64 * word && 17 * k < 64 * word + 64 {
582                let offset = 17 * k - 64 * word;
583                // Join this word with the next so a digit that crosses the
584                // word boundary is extracted in one shift.
585                let low = w[word].resize::<128>();
586                let high = if word + 1 < 8 {
587                    w[word + 1].resize::<128>()
588                } else {
589                    b128(0)
590                };
591                out[k] = ((low | (high << 64)) >> (offset as u128)).resize::<17>();
592            }
593        }
594    }
595    out
596}
597
598/// Four little-endian words (bits 0..255) to fifteen digits (bit 255 dropped).
599///
600/// Fifteen 17-bit digits hold 255 bits, the format of field elements and of
601/// scalars below `l`.
602#[kernel]
603pub fn words_to_digits15(w: [b64; 4]) -> [b17; 15] {
604    // Zero-extend to 512 bits and reuse the 31-digit split.
605    let mut wide = [b64(0); 8];
606    for k in 0..4 {
607        wide[k] = w[k];
608    }
609    let d = words_to_digits31(wide);
610    let mut out = [b17(0); 15];
611    for k in 0..15 {
612        out[k] = d[k];
613    }
614    out
615}
616
617/// Fifteen digits to four little-endian words.
618///
619/// Inverse of [`words_to_digits15`]; bit 255 of the result is zero.
620#[kernel]
621pub fn digits15_to_words(d: [b17; 15]) -> [b64; 4] {
622    let mut out = [b64(0); 4];
623    for k in 0..15 {
624        for word in 0..4 {
625            // Digit k starts in this word (one match per digit after
626            // unrolling).
627            if 17 * k >= 64 * word && 17 * k < 64 * word + 64 {
628                let offset = 17 * k - 64 * word;
629                // Shift in 128 bits; the part above bit 63 spills into the
630                // next word.
631                let wide = d[k].resize::<128>() << (offset as u128);
632                out[word] = out[word] | wide.resize::<64>();
633                if word + 1 < 4 {
634                    out[word + 1] = out[word + 1] | (wide >> 64).resize::<64>();
635                }
636            }
637        }
638    }
639    out
640}
641
642/// Signed mixed-radix recoding ([`super::host::recode`]) of a scalar below
643/// `2^253`: digits 0..39 are radix 32, 39..54 radix 16, sign-magnitude
644/// (bit 5 = sign); entries 54..64 are zero.
645///
646/// Carry `c[i+1] = (n[i] >= h) | (n[i] == h - 1 & c[i])` with `h` half the
647/// digit's radix is evaluated as the carry chain of `(g | p) + g`, i.e. one
648/// ordinary 64-bit addition.
649///
650/// The digit radixes match the fixed-base tables: each digit selects a
651/// signed multiple of a precomputed point, so a radix-32 digit uses magnitudes
652/// 0..=16 and a radix-16 digit 0..=8.
653#[kernel]
654pub fn recode_mixed(s: [b17; 15]) -> [b6; 64] {
655    let w = digits15_to_words(s);
656    // Unsigned digits before carries: 39 five-bit digits (bits 0..195), then
657    // 15 four-bit digits (bits 195..255).
658    let mut nib = [b6(0); 54];
659    for k in 0..54 {
660        // Bit position of digit k.
661        let offset = if k < 39 { 5 * k } else { 195 + 4 * (k - 39) };
662        for word in 0..4 {
663            if offset >= 64 * word && offset < 64 * word + 64 {
664                let low = w[word].resize::<128>();
665                let high = if word + 1 < 4 {
666                    w[word + 1].resize::<128>()
667                } else {
668                    b128(0)
669                };
670                let shift = offset - 64 * word;
671                let raw = ((low | (high << 64)) >> (shift as u128)).resize::<6>();
672                nib[k] = if k < 39 { raw & b6(31) } else { raw & b6(15) };
673            }
674        }
675    }
676    // Carry generate and propagate, one bit per digit: digit k generates a
677    // carry if it is at least half its radix (it will be made negative), and
678    // propagates an incoming carry if it is exactly one below half (adding
679    // the carry makes it reach half).
680    let mut g = b64(0);
681    let mut p = b64(0);
682    for k in 0..54 {
683        let half = if k < 39 { b6(16) } else { b6(8) };
684        if nib[k] >= half {
685            g = g | (b64(1) << (k as u128));
686        }
687        if nib[k] == half - b6(1) {
688            p = p | (b64(1) << (k as u128));
689        }
690    }
691    let a = g | p;
692    // carry_in[i]: bit i of (a + g) ^ a ^ g.
693    let carry_in = (a + g) ^ a ^ g;
694    let mut out = [b6(0); 64];
695    for k in 0..54 {
696        let half = if k < 39 { b6(16) } else { b6(8) };
697        let cin = ((carry_in >> (k as u128)) & b64(1)).resize::<6>();
698        // Digit value after its incoming carry, 0..=2 * half.
699        let v = nib[k] + cin;
700        // The top digit never carries out: the scalar is below 2^253, so it
701        // is at most 3 + 1, below half = 8.
702        let cout = if k < 53 { v >= half } else { false };
703        out[k] = if cout {
704            // v - radix is negative; store its magnitude with the sign bit.
705            // v == radix gives zero, which is stored without a sign.
706            let magnitude = half + half - v;
707            if magnitude == b6(0) {
708                b6(0)
709            } else {
710                magnitude | b6(32)
711            }
712        } else {
713            v
714        };
715    }
716    out
717}
718
719/// Base-point digits in slots 0..64 of the machine's 128 digit slots.
720///
721/// Slots 64..128 are for a second (key) scalar, which the fixed-base
722/// program does not use, so they stay zero.
723#[kernel]
724pub fn widen_digits(d: [b6; 64]) -> [b6; 128] {
725    let mut out = [b6(0); 128];
726    for k in 0..64 {
727        out[k] = d[k];
728    }
729    out
730}
731
732/// No key-table write.
733///
734/// The fixed-base and compress machines never load key tables; this idle
735/// value ties off their `table` port.
736#[kernel]
737pub fn idle_table_write() -> TableWrite {
738    TableWrite {
739        valid: false,
740        row: bits(0),
741        odd: false,
742        coord: b2(0),
743        value: Fe17 { d: [b17(0); 15] },
744    }
745}
746
747/// Encoded `R` from canonical affine `y` and the sign of `x`.
748///
749/// RFC 8032 point encoding: `y` little-endian with the sign of `x` in bit
750/// 255 (free because `y < p < 2^255`).
751#[kernel]
752pub fn encode_point(y: Fe17, x_negative: bool) -> [b64; 4] {
753    let mut w = digits15_to_words(y.d);
754    if x_negative {
755        w[3] = w[3] | b64(0x8000000000000000);
756    }
757    w
758}
759
760/// Big-endian SHA words of `prefix || M` with padding (96-byte message).
761///
762/// The 96 bytes fit one 128-byte block together with SHA-512 padding, so
763/// the nonce hash needs a single compression.
764#[cfg(not(feature = "msg1"))]
765#[kernel]
766pub fn nonce_block(prefix: ScalarWords, msg: Words512) -> [b64; 16] {
767    let mut block = [b64(0); 16];
768    // Words 0..4: the 32-byte prefix.
769    let p = words256(prefix);
770    for k in 0..4 {
771        block[k] = p[k];
772    }
773    // Words 4..12: the 64-byte message, byte-swapped to SHA order.
774    for k in 0..8 {
775        block[k + 4] = reverse_bytes(msg.words[k]);
776    }
777    // The 0x80 end marker at byte 96, then the bit length 768 in the last
778    // word of the 128-bit length field.
779    block[12] = b64(0x8000000000000000);
780    block[15] = b64(0x300);
781    block
782}
783
784/// Big-endian SHA words of `R || A || M` (first of two blocks).
785///
786/// The 128 bytes fill the block exactly; the ring appends the constant
787/// padding block of a 1,024-bit message when the job sets `second_pad`.
788#[cfg(not(feature = "msg1"))]
789#[kernel]
790pub fn challenge_block(r_enc: [b64; 4], a: [b64; 4], msg: Words512) -> [b64; 16] {
791    let mut block = [b64(0); 16];
792    for k in 0..4 {
793        block[k] = reverse_bytes(r_enc[k]);
794        block[k + 4] = reverse_bytes(a[k]);
795    }
796    for k in 0..8 {
797        block[k + 8] = reverse_bytes(msg.words[k]);
798    }
799    block
800}
801
802/// The challenge hash needs the ring's second (padding) block: `R‖A‖M` is
803/// exactly 128 bytes for a 64-byte message.
804#[cfg(not(feature = "msg1"))]
805#[kernel]
806pub fn challenge_second_pad() -> bool {
807    true
808}
809
810// ---------------------------------------------------------------------
811// Benchmark specialization `msg1`: one-byte messages, the input size of
812// ubft_new's HLS Ed25519 wrappers, so the two can be compared doing the
813// same work. M is byte 0 of the request's message (the low byte of word
814// 0). `prefix‖M` (33 bytes) and `R‖A‖M` (65 bytes) each fit one padded
815// SHA-512 block, so the challenge takes one compression, not two. Off by
816// default; never a protocol build.
817
818/// `prefix ‖ M` for a one-byte M, padded: one block, bit length 264.
819#[cfg(feature = "msg1")]
820#[kernel]
821pub fn nonce_block(prefix: ScalarWords, msg: Words512) -> [b64; 16] {
822    let mut block = [b64(0); 16];
823    let p = words256(prefix);
824    for k in 0..4 {
825        block[k] = p[k];
826    }
827    let m: b64 = msg.words[0] & b64(0xff);
828    block[4] = (m << b64(56)) | b64(0x0080_0000_0000_0000);
829    block[15] = b64(264);
830    block
831}
832
833/// `R ‖ A ‖ M` for a one-byte M, padded: one block, bit length 520.
834#[cfg(feature = "msg1")]
835#[kernel]
836pub fn challenge_block(r_enc: [b64; 4], a: [b64; 4], msg: Words512) -> [b64; 16] {
837    let mut block = [b64(0); 16];
838    for k in 0..4 {
839        block[k] = reverse_bytes(r_enc[k]);
840        block[k + 4] = reverse_bytes(a[k]);
841    }
842    let m: b64 = msg.words[0] & b64(0xff);
843    block[8] = (m << b64(56)) | b64(0x0080_0000_0000_0000);
844    block[15] = b64(520);
845    block
846}
847
848/// A one-byte challenge is a single padded block: no second block.
849#[cfg(feature = "msg1")]
850#[kernel]
851pub fn challenge_second_pad() -> bool {
852    false
853}
854
855/// Padded single block of a 32-byte seed.
856#[kernel]
857pub fn seed_block(seed: ScalarWords) -> [b64; 16] {
858    let mut block = [b64(0); 16];
859    let s = words256(seed);
860    for k in 0..4 {
861        block[k] = s[k];
862    }
863    // The 0x80 end marker at byte 32 and the bit length 256.
864    block[4] = b64(0x8000000000000000);
865    block[15] = b64(0x100);
866    block
867}
868
869/// Digest (big-endian state words) to the little-endian 512-bit integer.
870///
871/// RFC 8032 reads the 64-byte digest as a little-endian integer; swapping
872/// the bytes of each word gives that integer in this crate's word order.
873#[kernel]
874pub fn digest_le(state: [b64; 8]) -> [b64; 8] {
875    let mut out = [b64(0); 8];
876    for k in 0..8 {
877        out[k] = reverse_bytes(state[k]);
878    }
879    out
880}
881
882/// Memory port that reads `addr` and writes nothing.
883///
884/// `filler` only gives the disabled write a value of the right type; callers
885/// pass the memory's current read data.
886#[kernel]
887pub fn ram_read<T: Digital, const N: usize>(addr: Bits<N>, filler: T) -> RamIn<T, N>
888where
889    rhdl::bits::W<N>: BitWidth,
890{
891    RamIn::<T, N> {
892        read_addr: addr,
893        write: RamWrite::<T, N> {
894            addr: addr,
895            value: filler,
896            enable: false,
897        },
898    }
899}
900
901/// State transition for [`ScalableSignCore`].
902///
903/// One pass per cycle through every step, in the order below. Later steps
904/// read the `r` (current) registers and so see the state before this
905/// cycle's earlier steps, except where they explicitly use `n` or a local
906/// such as `credits`, `free_count` or the `*_free` flags, which carry
907/// this cycle's earlier decisions forward.
908#[kernel]
909pub fn scalable_sign_kernel<
910    const NFB: usize,
911    const NSHA: usize,
912    const NSC: usize,
913    const NCP: usize,
914    const CB: usize,
915    const SH: usize,
916>(
917    _cr: ClockReset,
918    i: ScalableSignIn,
919    q: Q<NFB, NSHA, NSC, NCP, CB, SH>,
920) -> (ScalableSignOut, D<NFB, NSHA, NSC, NCP, CB, SH>)
921where
922    rhdl::bits::W<CB>: BitWidth,
923{
924    let r = q.regs;
925    let mut n = r;
926    let mut d = D::<NFB, NSHA, NSC, NCP, CB, SH>::dont_care();
927    // The scalar 1, multiplier of nonce and key jobs (which only reduce
928    // their input mod l).
929    let one15 = {
930        let mut o = [b17(0); 15];
931        o[0] = b17(1);
932        o
933    };
934
935    // Default memory ports: read address 0, no writes. Steps below override
936    // the read address or the write of the port they use. The free list
937    // always reads at its read pointer, so a fetch only has to wait for
938    // the data.
939    let zero_ctx = bits::<CB>(0);
940    d.mems.msg = ram_read::<Words512, CB>(zero_ctx, q.mems.msg);
941    d.mems.r_sc = ram_read::<[b17; 15], CB>(zero_ctx, q.mems.r_sc);
942    d.mems.r_out = ram_read::<[b64; 4], CB>(zero_ctx, q.mems.r_out);
943    d.mems.user_tag = ram_read::<b16, CB>(zero_ctx, q.mems.user_tag);
944    d.mems.free_list = ram_read::<b11, CB>(r.free_rd.resize::<CB>(), q.mems.free_list);
945    d.mems.xsign = ram_read::<bool, CB>(zero_ctx, q.mems.xsign);
946
947    // ================= Free sign-context FIFO =================
948    // The head register is refilled in the background: a read issued one
949    // cycle lands in free_head the next. A context taken in cycle t is
950    // replaced by cycle t + 3, so admissions run at most one per three
951    // cycles.
952    let mut free_count = r.free_count;
953    if r.free_fetching {
954        n.free_head = q.mems.free_list;
955        n.free_head_valid = true;
956        n.free_fetching = false;
957    }
958    // Head empty and storage nonempty: the read at free_rd is on the port
959    // this cycle, so advance the pointer and count the entry as removed.
960    if !r.free_head_valid && !r.free_fetching && r.free_count != b12(0) {
961        n.free_fetching = true;
962        n.free_rd = r.free_rd + b11(1);
963        free_count = free_count - b12(1);
964    }
965
966    // ================= SHA ring availability =================
967    // At most one SHA-512 job is issued per cycle, to the lowest-numbered
968    // ready ring, or to the external port when SH = 1 (the internal rings
969    // are then never picked). sha_pick == NSHA denotes the external port.
970    let mut sha_any = false;
971    let mut sha_pick = b8(0);
972    let shared = SH == 1;
973    for k in 0..NSHA {
974        if q.sha[k].ready && !sha_any && !shared {
975            sha_any = true;
976            sha_pick = b8(k as u128);
977        }
978    }
979    if shared && i.sha_ext.ready {
980        sha_any = true;
981        sha_pick = b8(NSHA as u128);
982    }
983    // The one job this cycle, chosen by priority below: challenge hash, then
984    // key seed, then a new request.
985    let mut sha_job = ShaJob {
986        valid: false,
987        tag: b16(0),
988        block: [b64(0); 16],
989        second_pad: false,
990    };
991
992    // H2 stage: wait one cycle for the message, then dispatch.
993    // The challenge hash has first claim on the SHA-512 port: it belongs to a
994    // request that already holds a context and a credit, and draining such
995    // work first keeps new admissions from starving it.
996    if r.h2.valid && !r.h2.has_msg {
997        n.h2.msg = q.mems.msg;
998        n.h2.has_msg = true;
999    }
1000    // The challenge block can be offered this cycle.
1001    let h2_ready = r.h2.valid && r.h2.has_msg;
1002    // A job other than a new request has claimed the SHA port this cycle.
1003    let mut sha_taken = false;
1004    if h2_ready && sha_any {
1005        sha_job = ShaJob {
1006            valid: true,
1007            tag: (b16(KIND_CHALLENGE) << 11) | r.h2.ctx.resize::<16>(),
1008            block: challenge_block(r.h2.r_enc, r.public_key, r.h2.msg),
1009            // R || A || M is exactly one block; the ring adds the padding
1010            // block (none for the one-byte benchmark build).
1011            second_pad: challenge_second_pad(),
1012        };
1013        sha_taken = true;
1014        n.h2.valid = false;
1015    }
1016
1017    // Key load: one seed block.
1018    // Contexts in use: all contexts minus those in FIFO storage, in the head
1019    // register, or being fetched into it.
1020    let in_flight = r.ctx_total - r.free_count - if r.free_head_valid {
1021        b12(1)
1022    } else {
1023        b12(0)
1024    } - if r.free_fetching { b12(1) } else { b12(0) };
1025    // No request or key expansion anywhere in the pipeline. A request holds
1026    // its context until output, so in_flight == 0 covers every engine.
1027    let idle = in_flight == b12(0) && !r.keygen && !r.h2.valid && !r.sc.valid && !r.fb.valid;
1028    // Credit count, updated in place through the cycle: admissions take one,
1029    // fixed-base completions (fused) or the compress stage give them back.
1030    let mut credits = r.credits;
1031    let load_key_ready = idle && !r.key_loaded && sha_any && !sha_taken;
1032    if i.load_key_valid && load_key_ready {
1033        // The key's point passes through a fixed-base context and, in
1034        // compress mode, a compress slot, so it pays a credit like a request.
1035        credits = credits - b12(1);
1036        sha_job = ShaJob {
1037            valid: true,
1038            tag: b16(KIND_KEY) << 11,
1039            block: seed_block(i.key_seed),
1040            second_pad: false,
1041        };
1042        sha_taken = true;
1043        n.keygen = true;
1044        n.key_loaded = false;
1045    }
1046
1047    // New signing request (lowest SHA priority).
1048    // Requires a cached key, no key expansion, a free context in the head
1049    // register, a credit, and a SHA port that the challenge hash does not
1050    // want this cycle. It cannot coincide with a key load, which needs
1051    // key_loaded low.
1052    let sign_ready = r.key_loaded
1053        && !r.keygen
1054        && r.free_head_valid
1055        && r.credits != b12(0)
1056        && sha_any
1057        && !h2_ready;
1058    if i.valid && sign_ready {
1059        credits = credits - b12(1);
1060        // Take the head context; the FIFO logic above refills it.
1061        let ctx = r.free_head;
1062        n.free_head_valid = false;
1063        // Store the message (needed again for the challenge hash) and the
1064        // caller's tag (needed at output).
1065        d.mems.msg.write = RamWrite::<Words512, CB> {
1066            addr: ctx.resize::<CB>(),
1067            value: i.request.message,
1068            enable: true,
1069        };
1070        d.mems.user_tag.write = RamWrite::<b16, CB> {
1071            addr: ctx.resize::<CB>(),
1072            value: i.request.tag,
1073            enable: true,
1074        };
1075        sha_job = ShaJob {
1076            valid: true,
1077            tag: (b16(KIND_NONCE) << 11) | ctx.resize::<16>(),
1078            block: nonce_block(r.prefix, i.request.message),
1079            second_pad: false,
1080        };
1081    }
1082    // Broadcast the job to every SHA port; only the picked one sees valid.
1083    // out_ready is set later when a digest is taken.
1084    let mut sha_ext = ShaRingIn {
1085        job: ShaJob {
1086            valid: sha_job.valid && shared && sha_pick == b8(NSHA as u128),
1087            tag: sha_job.tag,
1088            block: sha_job.block,
1089            second_pad: sha_job.second_pad,
1090        },
1091        out_ready: false,
1092    };
1093    for k in 0..NSHA {
1094        d.sha[k] = ShaRingIn {
1095            job: ShaJob {
1096                valid: sha_job.valid && b8(k as u128) == sha_pick,
1097                tag: sha_job.tag,
1098                block: sha_job.block,
1099                second_pad: sha_job.second_pad,
1100            },
1101            out_ready: false,
1102        };
1103    }
1104
1105    // ================= SHA completions -> scalar stage =================
1106    // A challenge job's addend r was requested from r_sc last cycle; it is
1107    // on the read port now.
1108    if r.sc.valid && r.sc.needs_r {
1109        n.sc.z = q.mems.r_sc;
1110        n.sc.needs_r = false;
1111    }
1112    // Lowest-numbered scalar ring that can take a job.
1113    let mut sc_any = false;
1114    let mut sc_pick = b8(0);
1115    for k in 0..NSC {
1116        if q.sc[k].ready && !sc_any {
1117            sc_any = true;
1118            sc_pick = b8(k as u128);
1119        }
1120    }
1121    let sc_dispatch = r.sc.valid && !r.sc.needs_r && sc_any;
1122    // Broadcast the stage's job; only the picked ring sees valid. out_ready
1123    // is set below when a result is taken.
1124    for k in 0..NSC {
1125        d.sc[k] = ScalarRingIn {
1126            job: ScalarJob {
1127                valid: sc_dispatch && b8(k as u128) == sc_pick,
1128                tag: r.sc.tag,
1129                x: r.sc.x,
1130                y: if r.sc.y_is_a { r.a_digits } else { one15 },
1131                z: r.sc.z,
1132            },
1133            out_ready: false,
1134        };
1135    }
1136    if sc_dispatch {
1137        n.sc.valid = false;
1138    }
1139    // The scalar stage can be refilled this cycle (empty, or emptied now).
1140    let sc_stage_free = !r.sc.valid || sc_dispatch;
1141    // The first finished digest: a ring's, or the shared port's.
1142    // At most one digest is taken per cycle, and only when the scalar stage
1143    // has room; other rings keep theirs circulating (parked) until a later
1144    // cycle.
1145    let mut done = ShaDone { valid: false, tag: b16(0), state: [b64(0); 8] };
1146    for k in 0..NSHA {
1147        let ring_done = q.sha[k].done;
1148        if ring_done.valid && sc_stage_free && !done.valid {
1149            done = ring_done;
1150            d.sha[k].out_ready = true;
1151        }
1152    }
1153    if shared && i.sha_ext.done.valid && sc_stage_free && !done.valid {
1154        done = i.sha_ext.done;
1155        sha_ext.out_ready = true;
1156    }
1157    if done.valid {
1158        // Tag layout: kind in bits 15..11, sign context in bits 10..0.
1159        let kind = done.tag >> 11;
1160        let ctx = done.tag.resize::<11>();
1161        // The digest as the little-endian 512-bit integer RFC 8032 reduces.
1162        let le = digest_le(done.state);
1163        if kind == b16(KIND_KEY) {
1164            // SHA-512(seed): the clamped low half is the secret scalar, the
1165            // high half the nonce prefix. The job computes a mod l (y = 1,
1166            // z = 0).
1167            let secret = secret_from_digest(done.state);
1168            n.prefix = prefix_from_digest(done.state);
1169            let mut x = [b64(0); 8];
1170            for w in 0..4 {
1171                x[w] = secret.words[w];
1172            }
1173            n.sc = ScStage {
1174                valid: true,
1175                needs_r: false,
1176                tag: b16(KIND_KEY) << 11,
1177                x: words_to_digits31(x),
1178                y_is_a: false,
1179                z: [b17(0); 15],
1180            };
1181        } else if kind == b16(KIND_NONCE) {
1182            // r = SHA-512(prefix || M) mod l.
1183            n.sc = ScStage {
1184                valid: true,
1185                needs_r: false,
1186                tag: done.tag,
1187                x: words_to_digits31(le),
1188                y_is_a: false,
1189                z: [b17(0); 15],
1190            };
1191        } else {
1192            // Challenge: S = k * a + r mod l with k the digest. Fetch r now;
1193            // needs_r holds the job one cycle until it arrives.
1194            d.mems.r_sc.read_addr = ctx.resize::<CB>();
1195            n.sc = ScStage {
1196                valid: true,
1197                needs_r: true,
1198                tag: done.tag,
1199                x: words_to_digits31(le),
1200                y_is_a: true,
1201                z: [b17(0); 15],
1202            };
1203        }
1204    }
1205
1206    // ================= Scalar completions =================
1207    // Fixed-base dispatch stage drains first.
1208    // Pick the lowest-numbered machine with any free context.
1209    let mut fb_any = false;
1210    let mut fb_pick = b8(0);
1211    for k in 0..NFB {
1212        if r.fb_free[k] != b32(0) && !fb_any {
1213            fb_any = true;
1214            fb_pick = b8(k as u128);
1215        }
1216    }
1217    let fb_dispatch = r.fb.valid && fb_any;
1218    for k in 0..NFB {
1219        // Lowest free context of the chosen machine.
1220        let mask = r.fb_free[k];
1221        let mut ctx = b5(0);
1222        let mut found = false;
1223        for c in 0..32 {
1224            if !found && ((mask >> (c as u128)) & b32(1)) != b32(0) {
1225                found = true;
1226                ctx = b5(c as u128);
1227            }
1228        }
1229        // Start the context and write its digits in the same cycle; the
1230        // job tag travels with the context to its outputs.
1231        let go = fb_dispatch && b8(k as u128) == fb_pick;
1232        d.fb[k] = MachineIn::<5> {
1233            start: StartReq::<5> {
1234                valid: go,
1235                ctx: ctx,
1236                tag: r.fb.tag,
1237                key: no_key(), entry: b12(0),
1238            },
1239            digits: DigitsWrite::<5> {
1240                valid: go,
1241                ctx: ctx,
1242                digits: widen_digits(r.fb.digits),
1243            },
1244            input: InputWrite::<5> {
1245                valid: false,
1246                ctx: b5(0),
1247                index: b7(0),
1248                value: Fe17 { d: [b17(0); 15] },
1249            },
1250            table: idle_table_write(),
1251            out_accept: false,
1252        };
1253        // Mark the context busy; it is freed by the machine's done pulse.
1254        if go {
1255            n.fb_free[k] = mask & !(b32(1) << (ctx.resize::<32>()));
1256        }
1257    }
1258    if fb_dispatch {
1259        n.fb.valid = false;
1260    }
1261    // The fixed-base stage takes a scalar result only when it was empty at
1262    // the start of the cycle. Refilling it in the cycle it dispatches would
1263    // chain the 8 x 32-bit free search into the 255-bit r_sc write (routed
1264    // -0.37 ns); the stage sees about one job per 46 cycles, so the extra
1265    // cycle costs nothing.
1266    let fb_stage_free = !r.fb.valid;
1267
1268    // Signature output stage.
1269    // fin was filled last cycle together with the r_out and user_tag reads,
1270    // so R and the caller's tag are on the read ports now. The output has
1271    // no backpressure, so the stage always empties.
1272    let mut out_valid = false;
1273    let mut out_tag = b16(0);
1274    let mut out_sig = Words512 { words: [b64(0); 8] };
1275    let mut push_free = false;
1276    let mut push_ctx = b11(0);
1277    if r.fin.valid {
1278        out_valid = true;
1279        out_tag = q.mems.user_tag;
1280        let rw = q.mems.r_out;
1281        let sw = digits15_to_words(r.fin.s);
1282        out_sig = Words512 {
1283            words: [rw[0], rw[1], rw[2], rw[3], sw[0], sw[1], sw[2], sw[3]],
1284        };
1285        // The request is finished: return its context to the free list.
1286        push_free = true;
1287        push_ctx = r.fin.ctx;
1288        n.fin.valid = false;
1289    }
1290    // Take at most one scalar result per cycle (lowest ring first).
1291    let mut sc_done_taken = false;
1292    for k in 0..NSC {
1293        let done = q.sc[k].done;
1294        let kind = done.tag >> 11;
1295        let ctx = done.tag.resize::<11>();
1296        // Where the result goes: S to the output stage, which is always free
1297        // because it lasts one cycle; r and a to the fixed-base stage.
1298        let target_free = if kind == b16(KIND_CHALLENGE) {
1299            true
1300        } else {
1301            fb_stage_free
1302        };
1303        if done.valid && target_free && !sc_done_taken {
1304            sc_done_taken = true;
1305            d.sc[k].out_ready = true;
1306            if kind == b16(KIND_CHALLENGE) {
1307                // Read R and the user tag now for output next cycle.
1308                d.mems.r_out.read_addr = ctx.resize::<CB>();
1309                d.mems.user_tag.read_addr = ctx.resize::<CB>();
1310                n.fin = FinStage {
1311                    valid: true,
1312                    ctx: ctx,
1313                    s: done.s,
1314                };
1315            } else {
1316                // Keep a for challenge jobs, or store r as this request's
1317                // challenge addend; either way compute its point next.
1318                if kind == b16(KIND_KEY) {
1319                    n.a_digits = done.s;
1320                } else {
1321                    d.mems.r_sc.write = RamWrite::<[b17; 15], CB> {
1322                        addr: ctx.resize::<CB>(),
1323                        value: done.s,
1324                        enable: true,
1325                    };
1326                }
1327                n.fb = FbStage {
1328                    valid: true,
1329                    tag: done.tag,
1330                    digits: recode_mixed(done.s),
1331                };
1332            }
1333        }
1334    }
1335
1336    // ================= Free-list push =================
1337    // free_wr runs free_count entries ahead of free_rd (mod 2^CB), and at
1338    // most 2^CB contexts exist, so a push never overwrites an entry that is
1339    // still waiting to be read.
1340    if push_free {
1341        d.mems.free_list.write = RamWrite::<b11, CB> {
1342            addr: r.free_wr.resize::<CB>(),
1343            value: push_ctx,
1344            enable: true,
1345        };
1346        n.free_wr = r.free_wr + b11(1);
1347        free_count = free_count + b12(1);
1348    }
1349    n.free_count = free_count;
1350
1351    // ================= Fixed-base completions and outputs =================
1352    // A done pulse frees the context's bit (after any dispatch this cycle,
1353    // since it updates n). In fused mode the credit is the fixed-base
1354    // context, so it is returned here.
1355    for k in 0..NFB {
1356        let o = q.fb[k];
1357        if o.done {
1358            n.fb_free[k] = n.fb_free[k] | (b32(1) << (o.done_ctx.resize::<32>()));
1359            if NCP == 0 {
1360                credits = credits + b12(1);
1361            }
1362        }
1363    }
1364    // ================= Fixed-base outputs =================
1365    // Fused mode: affine x (its sign) then y go to the output stage.
1366    // Compress mode: X, Y, Z go to the compress stage, which only takes X
1367    // when a batch slot is free.
1368    // The compress stage's registered X acceptance (false without one).
1369    let mut accept_x = false;
1370    for c in 0..NCP {
1371        accept_x = q.cp[c].accept_x;
1372    }
1373    // Coordinate forwarded to the compress stage this cycle.
1374    let mut point = PointIn {
1375        valid: false,
1376        tag: b16(0),
1377        index: b2(0),
1378        value: Fe17 { d: [b17(0); 15] },
1379    };
1380    // At most one machine output per cycle (lowest machine first); a
1381    // machine whose output is not accepted holds it for a later cycle.
1382    let mut fb_out_taken = false;
1383    for k in 0..NFB {
1384        let o = q.fb[k].out;
1385        let is_x = o.index == b6(0);
1386        if NCP == 0 {
1387            // Fused: x only records its sign, so it is always accepted; y
1388            // needs the single-entry point stage to be empty.
1389            if o.valid && !fb_out_taken && (is_x || !r.cpo.valid) {
1390                fb_out_taken = true;
1391                d.fb[k].out_accept = true;
1392                let sign_ctx = o.tag.resize::<11>();
1393                // x is canonical, so its least significant bit is the sign.
1394                if is_x {
1395                    d.mems.xsign.write = RamWrite::<bool, CB> {
1396                        addr: sign_ctx.resize::<CB>(),
1397                        value: (o.value.d[0] & b17(1)) != b17(0),
1398                        enable: true,
1399                    };
1400                } else {
1401                    // y follows its x, so the sign is stored; read it back
1402                    // for next cycle.
1403                    d.mems.xsign.read_addr = sign_ctx.resize::<CB>();
1404                    n.cpo = CpOutStage {
1405                        valid: true,
1406                        has_tag: false,
1407                        tag: o.tag,
1408                        keep: true,
1409                        x_negative: false,
1410                        y: o.value,
1411                    };
1412                }
1413            }
1414        } else if o.valid && !fb_out_taken && (!is_x || accept_x) {
1415            // Compress mode: X needs a batch slot; Y and Z are always taken.
1416            // The credit held since admission guarantees the slot exists.
1417            fb_out_taken = true;
1418            d.fb[k].out_accept = true;
1419            point = PointIn {
1420                valid: true,
1421                tag: o.tag,
1422                index: o.index.resize::<2>(),
1423                value: o.value,
1424            };
1425        }
1426    }
1427
1428    // ================= Encoded R -> challenge hash =================
1429    // The challenge stage can be refilled: empty, or its job was issued to
1430    // SHA this cycle.
1431    let h2_free = !r.h2.valid || (sha_taken && h2_ready);
1432    let mut cpo_done = false;
1433    if NCP == 0 {
1434        // Fused: the xsign read issued when y arrived returns now.
1435        if r.cpo.valid && !r.cpo.has_tag {
1436            n.cpo.x_negative = q.mems.xsign;
1437            n.cpo.has_tag = true;
1438        }
1439        if r.cpo.valid && r.cpo.has_tag {
1440            let kind = r.cpo.tag >> 11;
1441            let ctx = r.cpo.tag.resize::<11>();
1442            let r_enc = encode_point(r.cpo.y, r.cpo.x_negative);
1443            if kind == b16(KIND_KEY) {
1444                // Key expansion ends: [a]B is the public key.
1445                n.public_key = r_enc;
1446                n.key_loaded = true;
1447                n.keygen = false;
1448                // A cancelled expansion ends here; the key clear below
1449                // still wipes this cycle's result.
1450                n.key_cancel = false;
1451                cpo_done = true;
1452            } else if h2_free {
1453                // Store R for the signature and start the challenge hash;
1454                // the message is read now and lands in h2 next cycle.
1455                d.mems.r_out.write = RamWrite::<[b64; 4], CB> {
1456                    addr: ctx.resize::<CB>(),
1457                    value: r_enc,
1458                    enable: true,
1459                };
1460                n.h2 = H2Stage {
1461                    valid: true,
1462                    has_msg: false,
1463                    ctx: ctx,
1464                    r_enc: r_enc,
1465                    msg: Words512 { words: [b64(0); 8] },
1466                };
1467                d.mems.msg.read_addr = ctx.resize::<CB>();
1468                cpo_done = true;
1469            }
1470        }
1471        if cpo_done {
1472            n.cpo.valid = false;
1473        }
1474    }
1475    // Compress mode: the same hand-off from the compress stage's encoded
1476    // output. `take` is its out_ready; an untaken encoding stays held.
1477    for c in 0..NCP {
1478        let done = q.cp[c].done;
1479        let mut take = false;
1480        if done.valid {
1481            let kind = done.tag >> 11;
1482            let ctx = done.tag.resize::<11>();
1483            if kind == b16(KIND_KEY) {
1484                n.public_key = done.enc;
1485                n.key_loaded = true;
1486                n.keygen = false;
1487                n.key_cancel = false;
1488                take = true;
1489            } else if h2_free {
1490                d.mems.r_out.write = RamWrite::<[b64; 4], CB> {
1491                    addr: ctx.resize::<CB>(),
1492                    value: done.enc,
1493                    enable: true,
1494                };
1495                n.h2 = H2Stage {
1496                    valid: true,
1497                    has_msg: false,
1498                    ctx: ctx,
1499                    r_enc: done.enc,
1500                    msg: Words512 { words: [b64(0); 8] },
1501                };
1502                d.mems.msg.read_addr = ctx.resize::<CB>();
1503                take = true;
1504            }
1505        }
1506        // The stage sees the registered credits (before this cycle's
1507        // admission) to decide whether a partial batch may flush, and keeps
1508        // a margin for the admissions its registered spend lags; its spend
1509        // and return are applied afterwards.
1510        d.cp[c] = CompressIn {
1511            point: point,
1512            credits: r.credits,
1513            out_ready: take,
1514        };
1515        credits = credits - q.cp[c].credit_spend + q.cp[c].credit_return;
1516    }
1517    n.credits = credits;
1518    // Invariant: credits stay within the pool (at most 2,048). An underflow
1519    // would wrap the 12-bit count above it. Sticky.
1520    if credits > b12(2048) {
1521        n.error = true;
1522    }
1523
1524    // ================= Key clear =================
1525    // Last, so it overrides any key update made above this cycle. A clear
1526    // during an expansion (running, or starting this cycle) cancels it: the
1527    // expansion still runs to completion so its contexts and credits come
1528    // back, but every key value it writes meanwhile is wiped here, and its
1529    // result is discarded.
1530    if i.clear_key && (r.keygen || n.keygen) {
1531        n.key_cancel = true;
1532    }
1533    if i.clear_key || r.key_cancel {
1534        n.key_loaded = false;
1535        n.prefix = ScalarWords { words: [b64(0); 4] };
1536        n.a_digits = [b17(0); 15];
1537        n.public_key = [b64(0); 4];
1538    }
1539
1540    d.regs = n;
1541    // ready and load_key_ready are this cycle's combinational decisions;
1542    // key_loaded and public_key are the registered values.
1543    let output = ScalableSignOut {
1544        load_key_ready: load_key_ready,
1545        key_loaded: r.key_loaded,
1546        public_key: ScalarWords {
1547            words: r.public_key,
1548        },
1549        ready: sign_ready,
1550        valid: out_valid,
1551        tag: out_tag,
1552        signature: out_sig,
1553        internal_error: r.error,
1554        in_flight: in_flight,
1555        sha_ext: sha_ext,
1556    };
1557    (output, d)
1558}
1559
1560/// The one-byte benchmark blocks hash to exactly SHA-512 of the bytes they
1561/// stand for: `prefix ‖ M` and `R ‖ A ‖ M`, for every one of the 256 values
1562/// of M and several prefixes, points and keys. Checked with an independent
1563/// software compression against the `sha2` crate.
1564#[cfg(all(test, feature = "msg1"))]
1565mod msg1_tests {
1566    use super::*;
1567    use crate::sha_client::tests::{IV, compress};
1568    use sha2::{Digest, Sha512};
1569
1570    fn words_from(bytes: [u8; 32]) -> [b64; 4] {
1571        std::array::from_fn(|k| b64(u128::from(u64::from_le_bytes(bytes[8 * k..8 * k + 8].try_into().unwrap()))))
1572    }
1573
1574    fn digest(block: [b64; 16]) -> [u8; 64] {
1575        let s = compress(IV, block.map(|w| w.raw() as u64));
1576        let mut out = [0u8; 64];
1577        for k in 0..8 {
1578            out[8 * k..8 * k + 8].copy_from_slice(&s[k].to_be_bytes());
1579        }
1580        out
1581    }
1582
1583    #[test]
1584    fn every_one_byte_message_hashes_like_sha512() {
1585        assert!(!challenge_second_pad());
1586        for key in 0..4u8 {
1587            let prefix: [u8; 32] = std::array::from_fn(|i| (i as u8).wrapping_mul(37).wrapping_add(key * 61));
1588            let r_enc: [u8; 32] = std::array::from_fn(|i| (i as u8).wrapping_mul(11) ^ key);
1589            let a: [u8; 32] = std::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(key));
1590            for m in 0..=255u8 {
1591                let mut msg = Words512 { words: [b64(0); 8] };
1592                // Upper bytes of the message word must be ignored.
1593                msg.words[0] = b64(u128::from(m) | 0xdead_beef_0000_0000);
1594                msg.words[1] = b64(0x1234);
1595                let nb = nonce_block(ScalarWords { words: words_from(prefix) }, msg);
1596                let mut want = Sha512::new();
1597                want.update(prefix);
1598                want.update([m]);
1599                assert_eq!(digest(nb)[..], want.finalize()[..], "nonce, key {key}, m {m}");
1600                let cb = challenge_block(words_from(r_enc), words_from(a), msg);
1601                let mut want = Sha512::new();
1602                want.update(r_enc);
1603                want.update(a);
1604                want.update([m]);
1605                assert_eq!(digest(cb)[..], want.finalize()[..], "challenge, key {key}, m {m}");
1606            }
1607        }
1608    }
1609}