Skip to main content

rhdl_ed25519_scalable_core/
verify_core.rs

1//! Known-key strict verification core.
2//!
3//! [`KnownKeyVerifyCore`] verifies Ed25519 signatures of 64-byte messages
4//! for up to [`KEY_SLOTS`] public keys loaded in advance (8, or 16 with the
5//! feature `key-slots-16`), with the strict rules and
6//! error codes of the `StrictVerifyCore` in `rhdl_ed25519_fast_core`: `S` must
7//! be canonical, `A` and `R` must be canonical curve points and not of low
8//! order, and `[S]B = R + [k]A` must hold (no cofactor).
9//!
10//! # Dataflow
11//!
12//! ```text
13//! request ──> admission ──> SHA ring: H(R || A || M) ──> scalar ring: k = H mod l
14//!               │ (key slot, S < l, R canonical, R low order)        │
15//!               │                                                    v
16//!               │         verify machine: [S]B + [k](-A) <── recode S and k
17//!               │                       │ X, Y, Z
18//!               v                       v
19//!            result <── compare with R <── CompressStage (32 points per inversion)
20//!               ^                  │ mismatch
21//!               └── aux machine: decode R (BAD_R or EQUATION_FAILURE)
22//! ```
23//!
24//! Each verify machine scans two tables per verification: the base-point
25//! table (ROM) for `S` and the key's table for `k`, so the equation costs two
26//! fixed-base walks (775 bundles) instead of a variable-base multiplication.
27//! The key table holds multiples of `-A`, so the machine computes
28//! `[S]B + [k](-A)`, which equals `R` exactly when the equation holds.
29//!
30//! # Contexts and tags
31//!
32//! A request is accepted into a one-entry admission register, where its key
33//! is matched and its immediate result decided. On the next cycle (or as
34//! soon as a context, a credit and a SHA resource are free) it is admitted:
35//! an admitted request takes one of 2,048 *contexts* from a free list. The
36//! context number indexes the per-request memories ([`VerifyMems`]: `R`,
37//! `S` and the request facts), so only the 11-bit context and the 3- or 4-bit key
38//! slot travel with the job. They are packed into the 16-bit tag as
39//! `slot << 11 | ctx` through the SHA and scalar rings; the verify machine
40//! and the compress stage carry the context alone. The context returns to
41//! the free list when its result is reported. Requests that fail at
42//! admission (unknown key, invalid key, non-canonical `R`) never take one.
43//!
44//! # Key loading
45//!
46//! `load_key_valid` with an encoded `A` and a slot starts a key load on the
47//! aux machine: decode `A` ([`decode_candidate`]), compute the 54 table bases
48//! of `-A` ([`table_bases`]), then 54 digit jobs ([`digit_table`]) whose
49//! outputs are written into every verify machine's key table. The writes
50//! pass from machine to machine, one per cycle, so a slot becomes usable
51//! `NV + 2` cycles after its last digit job retires (about 105k cycles in
52//! all). A key that does not decode is kept as *invalid*, so its requests
53//! get [`VERIFY_BAD_PUBLIC_KEY`]. `ready` is low while any key loads, and
54//! while a key load is offered: a load offered in the same cycle as a
55//! request goes first. A load starts only with no request held or in
56//! flight.
57//!
58//! The aux machine's 32 contexts are split by job: context 0 decodes (a key
59//! or an `R`), context 1 computes table bases, and contexts 2..32 run digit
60//! jobs, up to 30 at once. Its tags carry the job kind in bits 8.. (one of
61//! the `AUX_*` constants) and, for digit jobs, the digit number in bits 0..8.
62//!
63//! In the key tables, digit `i` occupies the rows from [`digit_row`]`(i)`:
64//! eight rows (16 entries, two per row) for the 39 radix-32 digits and four
65//! rows (8 entries) for the 15 radix-16 digits, 372 rows per key. Key slot
66//! `s` owns rows `s * 512 ..`.
67//!
68//! # Errors
69//!
70//! In priority order: [`VERIFY_BAD_PUBLIC_KEY`], [`VERIFY_BAD_R`],
71//! [`VERIFY_SMALL_PUBLIC_KEY`], [`VERIFY_SMALL_R`], [`VERIFY_NONCANONICAL_S`],
72//! [`VERIFY_EQUATION_FAILURE`]; [`VERIFY_UNKNOWN_KEY`] for a key that is not
73//! loaded. When the computed point does not encode to `R`, the aux machine
74//! decodes `R` to tell an invalid `R` from a failed equation (a slow path
75//! only invalid signatures take).
76//!
77//! The errors are found in three places:
78//!
79//! - **At admission**: unknown key, invalid key, and `R` whose `y` is not
80//!   below `p` ([`VERIFY_BAD_R`]). These are reported at once.
81//! - **At the compare**: if the computed point encodes to `R`, then `R` is a
82//!   valid point encoding, so [`VERIFY_BAD_R`] cannot apply and the result
83//!   is the first of the small-key, small-`R` and non-canonical-`S` facts
84//!   recorded at admission, or [`VERIFY_OK`].
85//! - **After the `R` decode** (mismatch only): [`VERIFY_BAD_R`] if `R` does
86//!   not decode, else the same admission facts, else
87//!   [`VERIFY_EQUATION_FAILURE`].
88//!
89//! The admission facts are not reported at admission because
90//! [`VERIFY_BAD_R`] outranks them and needs the decode, which the compare
91//! avoids in the common case. So every request past admission runs the
92//! equation, even one whose `S` is already known to be non-canonical; its
93//! computed point is then irrelevant.
94//!
95//! # Deadlock freedom
96//!
97//! Admission takes a compress credit, as in [`crate::sign_core`], so every
98//! verify-machine output finds a compress slot. The rest of the flow has no
99//! cycle: compare waits only for the result register, which drains every
100//! cycle, and the aux machine's jobs (key loading, `R` decodes) never wait on
101//! the verification path.
102
103use rhdl::prelude::*;
104use rhdl_fpga::core::{
105    dff::DFF,
106    ram::synchronous::{In as RamIn, SyncBRAM, Write as RamWrite},
107};
108
109use crate::{
110    compress_stage::{COMPRESS_SLOTS, CompressIn, CompressStage, PointIn},
111    fe17::{Fe17, add_limb, digits_from_limbs, limbs_from_digits, nb_const},
112    machine::{
113        DigitsWrite, FieldMachine, InputWrite, KEY_SLOTS, KEY_TABLE_BITS, KeySlot, MachineIn,
114        StartReq, TableWrite,
115    },
116    microcode::concat,
117    programs::{decode_candidate, digit_table, table_bases, verify_known_key},
118    scalar_ring::{ScalarJob, ScalarRing, ScalarRingIn},
119    sha_ring::{ShaDone, ShaJob, ShaRing, ShaRingIn, ShaRingOut},
120    sign_core::{
121        challenge_block, challenge_second_pad, digest_le, idle_table_write, ram_read, recode_mixed,
122        words_to_digits15, words_to_digits31,
123    },
124    wire::{ScalarWords, VerifyRequest, Words512},
125};
126
127/// Verification succeeded.
128pub const VERIFY_OK: u128 = 0;
129/// The public key is not a canonical curve point.
130pub const VERIFY_BAD_PUBLIC_KEY: u128 = 1;
131/// `R` is not a canonical curve point.
132pub const VERIFY_BAD_R: u128 = 2;
133/// The public key has low order.
134pub const VERIFY_SMALL_PUBLIC_KEY: u128 = 3;
135/// `R` has low order.
136pub const VERIFY_SMALL_R: u128 = 4;
137/// `S` is not below `l`.
138pub const VERIFY_NONCANONICAL_S: u128 = 5;
139/// `[S]B = R + [k]A` does not hold.
140pub const VERIFY_EQUATION_FAILURE: u128 = 6;
141/// The public key is not loaded in any slot.
142pub const VERIFY_UNKNOWN_KEY: u128 = 7;
143
144/// Verify machine: 32 contexts, base-point table, key tables for 8 keys.
145///
146/// Parameters: `KB = 5` (32 contexts), bank-M/A address width 9 (16
147/// registers per context), bank-I width 5 (the program has no inputs),
148/// program-counter width 10 (775 bundles), base-point table `2^9` rows (372
149/// used) and key tables `2^KEY_TABLE_BITS` rows ([`KEY_SLOTS`] slots of 512).
150pub type VerifyMachine = FieldMachine<5, 32, 9, 5, 10, 9, KEY_TABLE_BITS>;
151/// Aux machine: key loading and `R` decoding (four programs in one ROM).
152///
153/// Parameters: 32 contexts, bank-M/A width 11 (64 registers per context),
154/// bank-I width 9 (16 input registers per context), program-counter width
155/// 12, and no table scanner (`TB = KT = 1`).
156pub type AuxMachine = FieldMachine<5, 32, 11, 9, 12, 1, 1>;
157
158/// Key slot state: nothing loaded; the slot matches no request.
159const KEY_EMPTY: u128 = 0;
160/// Key slot state: tables are being built. Admission is closed meanwhile.
161const KEY_LOADING: u128 = 1;
162/// Key slot state: tables complete, requests are verified.
163const KEY_READY: u128 = 2;
164/// Key slot state: the key did not decode; requests get
165/// [`VERIFY_BAD_PUBLIC_KEY`].
166const KEY_INVALID: u128 = 3;
167
168/// Aux job kind (tag bits 8..): point decode on context 0.
169const AUX_DECODE: u128 = 0;
170/// Aux job kind: table bases on context 1.
171const AUX_BASES: u128 = 1;
172/// Aux job kind: one digit's table on a context in 2..32; tag bits 0..8
173/// hold the digit.
174const AUX_DIGIT: u128 = 2;
175
176/// Key-load state: no load in progress (admission open).
177const KL_IDLE: u128 = 0;
178/// Key-load state: decoding `A` on aux context 0.
179const KL_DECODE: u128 = 1;
180/// Key-load state: feeding and running the table-bases job of `-A`.
181const KL_BASES: u128 = 2;
182/// Key-load state: feeding and running the 54 digit jobs.
183const KL_DIGITS: u128 = 3;
184
185/// Decode-collector owner: free.
186const DEC_NONE: u128 = 0;
187/// Decode-collector owner: the key load (decoding `A`).
188const DEC_KEY: u128 = 1;
189/// Decode-collector owner: the slow path (decoding a request's `R`).
190const DEC_R: u128 = 2;
191
192/// Inputs of [`KnownKeyVerifyCore`].
193#[derive(Copy, Clone, Debug, PartialEq, Digital)]
194pub struct VerifyIn {
195    /// Verify `request` when [`VerifyOut::ready`] (taken in the same cycle).
196    pub valid: bool,
197    /// Request: 64-byte message, encoded `A`, `R || S`, and a tag.
198    pub request: VerifyRequest,
199    /// Load `load_key` into `load_slot` when [`VerifyOut::load_key_ready`]
200    /// (taken in the same cycle). A slot's previous key is replaced.
201    pub load_key_valid: bool,
202    /// Encoded public key `A` (32 bytes as four little-endian words).
203    /// Requests match a slot by these exact bytes.
204    pub load_key: ScalarWords,
205    /// Key slot, 0..[`KEY_SLOTS`].
206    pub load_slot: KeySlot,
207    /// The shared SHA-512 port (`SH = 1`; ignored otherwise), as a ring's
208    /// outputs: see [`crate::sha_client::SharedShaPort`].
209    pub sha_ext: ShaRingOut,
210}
211
212/// Outputs of [`KnownKeyVerifyCore`].
213#[derive(Copy, Clone, Debug, PartialEq, Digital)]
214pub struct VerifyOut {
215    /// A request offered this cycle is accepted. Never depends on the
216    /// offered request or on `valid`: it depends on the core's registers and
217    /// on `load_key_valid` (a key load offered in the same cycle goes first).
218    pub ready: bool,
219    /// One-cycle result pulse. There is no back-pressure: the consumer must
220    /// take every result.
221    pub valid: bool,
222    /// Verification succeeded.
223    pub ok: bool,
224    /// Error code (0 on success).
225    pub error: b8,
226    /// Tag of the request.
227    pub tag: b16,
228    /// Requests in flight: contexts taken and not yet returned (0..=2048).
229    /// Requests rejected at admission are not counted.
230    pub in_flight: b12,
231    /// A key load can start: no load is in progress and the aux machine's
232    /// decode path is idle.
233    pub load_key_ready: bool,
234    /// Slot states: 0 empty, 1 loading, 2 ready, 3 invalid key.
235    pub key_state: [b2; KEY_SLOTS],
236    /// Sticky invariant violation: the compress credit count left its pool
237    /// (the only invariant checked so far).
238    pub internal_error: bool,
239    /// Jobs for the shared SHA-512 port (`SH = 1`; idle otherwise).
240    pub sha_ext: ShaRingIn,
241}
242
243/// Request facts recorded at admission, per context, for the final error
244/// decision.
245#[derive(Copy, Clone, Debug, PartialEq, Digital)]
246#[doc(hidden)]
247pub struct Meta {
248    /// The request's tag, reported with the result.
249    tag: b16,
250    /// Key slot the request matched.
251    slot: KeySlot,
252    /// The key's encoding is one of the eight low-order encodings.
253    a_small: bool,
254    /// `R`'s encoding is one of the eight low-order encodings.
255    r_small: bool,
256    /// `S < l`.
257    canonical_s: bool,
258}
259
260/// A challenge digest waiting for a scalar ring (`k = H mod l`).
261#[derive(Copy, Clone, Debug, PartialEq, Digital)]
262#[doc(hidden)]
263pub struct ScStage {
264    /// The register holds a digest.
265    valid: bool,
266    /// `slot << 11 | ctx`, as given to the SHA job.
267    tag: b16,
268    /// The 512-bit digest as 31 little-endian 17-bit digits.
269    x: [b17; 31],
270}
271
272/// A request whose `k` is known, waiting for a free verify-machine context.
273#[derive(Copy, Clone, Debug, PartialEq, Digital)]
274#[doc(hidden)]
275pub struct VmStage {
276    /// The register holds a request.
277    valid: bool,
278    /// `s` has been read from memory and recoded (one cycle after `valid`).
279    has_mem: bool,
280    /// Context of the request.
281    ctx: b11,
282    /// Recoded digits of `k` (sign-magnitude, 54 used).
283    k: [b6; 64],
284    /// Recoded digits of `S` (sign-magnitude, 54 used).
285    s: [b6; 64],
286    /// Key slot: selects the key table the machine scans.
287    slot: KeySlot,
288}
289
290/// An encoded result point waiting to be compared with its `R`.
291#[derive(Copy, Clone, Debug, PartialEq, Digital)]
292#[doc(hidden)]
293pub struct CmpStage {
294    /// The register holds a point.
295    valid: bool,
296    /// Context of the request.
297    ctx: b11,
298    /// Encoding of `[S]B + [k](-A)` (four little-endian words).
299    enc: [b64; 4],
300}
301
302/// The result register: drives the result ports for one cycle.
303#[derive(Copy, Clone, Debug, PartialEq, Digital)]
304#[doc(hidden)]
305pub struct FinStage {
306    /// A result is reported this cycle.
307    valid: bool,
308    /// Return `ctx` to the free list (false for admission rejects, which
309    /// took no context).
310    free_ctx: bool,
311    /// Context to return.
312    ctx: b11,
313    /// The request's tag.
314    tag: b16,
315    /// One of the `VERIFY_*` codes.
316    error: b8,
317}
318
319/// Registers of [`KnownKeyVerifyCore`] (`NV` verify machines).
320#[derive(Copy, Clone, Debug, PartialEq, Digital)]
321#[doc(hidden)]
322pub struct VerifyRegs<const NV: usize> {
323    // Key slots.
324    /// Encoded key of each slot, compared byte for byte with requests' `A`.
325    key_enc: [[b64; 4]; KEY_SLOTS],
326    /// `KEY_*` state of each slot.
327    key_state: [b2; KEY_SLOTS],
328    /// The slot's key encoding is of low order (reported per request as
329    /// [`VERIFY_SMALL_PUBLIC_KEY`]).
330    key_small: [bool; KEY_SLOTS],
331    // Key-load state machine.
332    /// `KL_*` state; one key loads at a time.
333    kl_state: b2,
334    /// Slot being loaded.
335    kl_slot: KeySlot,
336    /// The bases job has been started (stops the `KL_BASES` feed).
337    kl_issued: bool,
338    /// Bases outputs received so far, which is also the next write address
339    /// in the bases memory (54 bases x 4 coordinates = 216).
340    kl_count: b8,
341    /// Next digit job to feed (0..54; 54 means all fed).
342    kl_feed_digit: b6,
343    /// Step of the input feed: 0..2 in `KL_BASES`, 0..5 in `KL_DIGITS`.
344    kl_feed_step: b3,
345    /// Aux context chosen for the digit job being fed.
346    kl_feed_ctx: b5,
347    /// Digit jobs retired (54 completes the load).
348    kl_digits_done: b6,
349    /// Aux contexts 2..32 running a digit job, one bit per context.
350    aux_busy: b32,
351    /// After the last digit job retires, cycles left until its table writes
352    /// have passed down `tbl` into every machine (0: no drain running). The
353    /// slot turns READY when it reaches 0.
354    kl_drain: b7,
355    /// Key-table writes on their way down the machines: machine `k` takes
356    /// `tbl[k]`, and `tbl[k]` moves to `tbl[k + 1]` the next cycle. Every
357    /// machine holds a copy of every table, so all of them see the same
358    /// writes. Driving them in the same cycle let Vivado merge the machines'
359    /// identical input registers into one, which fed table memories all over
360    /// the die (−2.12 ns in the routed L200 verifier, 2026-10-19). Shifted
361    /// one machine per cycle, no two registers are equal, and each hop goes
362    /// to a neighbour.
363    tbl: [TableWrite; NV],
364    /// First bundle of each aux program: decode, bases, 16-entry digit
365    /// table, 8-entry digit table (see [`aux_programs`]).
366    aux_entry: [b12; 4],
367    // Decode collector (key or R, one at a time).
368    /// `DEC_*`: whose decode runs on aux context 0.
369    dec_owner: b2,
370    /// Write `dec_y` and start the decode program next cycle.
371    dec_feed: bool,
372    /// `y` of the point being decoded; during a key load it stays `y` of
373    /// `A` (equal to `y` of `-A`) for the bases job.
374    dec_y: Fe17,
375    /// Sign bit of the encoding being decoded (the parity of `x`).
376    dec_sign: bool,
377    /// Context of the request whose `R` is being decoded.
378    dec_ctx: b11,
379    /// Decode outputs `x`, `x * sqrt(-1)`, `v x^2`, `u`. After a key decode,
380    /// entry 0 is replaced by `x` of `-A` for the bases job.
381    dec_vals: [Fe17; 4],
382    /// Decode outputs received (0..4), then the finishing steps: 4, 5 and 6
383    /// form limb 0, 1 and 2 of `p - x`, `p - x sqrt(-1)` and `p - u`
384    /// (`dec_neg`); at 7 the two root tests are registered (`dec_plain`,
385    /// `dec_flipped`) and at 8 the root is chosen.
386    dec_count: b4,
387    /// `v x^2 = u` (the candidate `x` is a root), registered at step 7.
388    dec_plain: bool,
389    /// `v x^2 = -u` (`x sqrt(-1)` is a root), registered at step 7.
390    /// Comparing and selecting in one cycle (255-bit compare, then the root
391    /// and parity selects) was a −2.16 ns path of the routed L200 verifier
392    /// (2026-10-19).
393    dec_flipped: bool,
394    /// `p - x`, `p - x sqrt(-1)` and `p - u` as limbs, formed one limb per
395    /// cycle so no cycle holds more than one 85-bit carry chain. (One-cycle
396    /// negations in series were the worst path of the routed verifiers:
397    /// -2.165 ns at 6 machines, 2026-10-17.)
398    dec_neg: [[b85; 3]; 3],
399    /// Carries into the next limb of each `dec_neg` entry.
400    dec_neg_c: [bool; 3],
401    /// A decode program is running on aux context 0 (from its start until
402    /// it retires). A new decode waits for it: its input write would
403    /// otherwise overwrite the running program's input.
404    dec_running: bool,
405    // Admission register.
406    /// `adm_req` holds an accepted request that has not yet taken a context.
407    adm_valid: bool,
408    /// The accepted request.
409    adm_req: VerifyRequest,
410    /// Its immediate result (`VERIFY_OK` if it goes on to be verified),
411    /// decided when it was accepted.
412    adm_error: b8,
413    /// Its admission facts, decided when it was accepted.
414    adm_meta: Meta,
415    // Free contexts.
416    /// `free_head` holds a free context ready for admission.
417    free_head_valid: bool,
418    /// Next free context, prefetched so admission needs no RAM latency.
419    free_head: b11,
420    /// A free-list read was issued last cycle.
421    free_fetching: bool,
422    /// Free-list read pointer (wraps at 2048).
423    free_rd: b11,
424    /// Free-list write pointer (wraps at 2048).
425    free_wr: b11,
426    /// Entries in the free-list RAM, not counting `free_head` or one being
427    /// fetched.
428    free_count: b12,
429    /// Compress-stage credits: point slots this core may still fill.
430    credits: b12,
431    // Stages.
432    /// Challenge digest waiting for a scalar ring.
433    sc: ScStage,
434    /// Request waiting for a verify-machine context.
435    vm: VmStage,
436    /// Free contexts of each verify machine, one bit per context.
437    vm_free: [b32; NV],
438    /// `vm_free[k] != 0`, registered: the dispatch picks a machine from one
439    /// bit per machine instead of OR-ing 32 free bits of every machine in
440    /// the same cycle (with 24 machines that pick, 14 levels from `vm_free`
441    /// back to `vm_free`, was the routed L200 verifier's worst path,
442    /// −2.225 ns, 2026-10-19).
443    vm_has_free: [bool; NV],
444    /// Verify-machine outputs as seen last cycle and not granted then:
445    /// offer and whether it is an `X`. A machine holds `out` until accepted,
446    /// so a registered offer is still valid (same coordinate) this cycle.
447    out_req: [bool; NV],
448    out_req_x: [bool; NV],
449    /// The coordinate picked last cycle, offered to the compress stage now,
450    /// and the machine it came from (one-hot). That machine keeps offering
451    /// it until the stage takes it.
452    pt: PointIn,
453    pt_src: [bool; NV],
454    /// Encoded point waiting for its compare.
455    cmp: CmpStage,
456    /// A mismatched request waits for the aux decode collector.
457    slow_valid: bool,
458    /// Its context.
459    slow_ctx: b11,
460    /// Its encoded `R`.
461    slow_r: [b64; 4],
462    /// Its admission facts.
463    slow_meta: Meta,
464    /// Admission facts of the request whose `R` is being decoded.
465    dec_meta: Meta,
466    /// Result register.
467    fin: FinStage,
468    /// Sticky internal error: set when the credit count leaves its pool.
469    error: bool,
470}
471
472/// Per-context memories, plus the free list and the key-load bases.
473///
474/// All are block RAMs with a one-cycle read latency, indexed by context
475/// (2,048 entries) except `bases`.
476#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
477pub struct VerifyMems {
478    /// Encoded `R` of each request, read at the compare.
479    r_enc: SyncBRAM<[b64; 4], 11>,
480    /// `S` of each request, read when the verify machine is fed.
481    s_words: SyncBRAM<[b64; 4], 11>,
482    /// Admission facts of each request, read at the compare.
483    meta: SyncBRAM<Meta, 11>,
484    /// Ring buffer of free context numbers, preloaded with 0..2048.
485    free_list: SyncBRAM<b11, 11>,
486    /// The 54 table bases of `-A` during a key load, `X, Y, Z, T` of base
487    /// `i` at `4i..4i + 4`.
488    bases: SyncBRAM<Fe17, 8>,
489}
490
491/// Port bundle of [`VerifyMems`].
492#[derive(Copy, Clone, Debug, PartialEq, Digital)]
493pub struct VerifyMemsIn {
494    /// Encoded `R`, by context.
495    pub r_enc: RamIn<[b64; 4], 11>,
496    /// `S`, by context.
497    pub s_words: RamIn<[b64; 4], 11>,
498    /// Request facts, by context.
499    pub meta: RamIn<Meta, 11>,
500    /// Free contexts (ring buffer).
501    pub free_list: RamIn<b11, 11>,
502    /// Table bases during key loading.
503    pub bases: RamIn<Fe17, 8>,
504}
505
506/// Read data of [`VerifyMems`].
507#[derive(Copy, Clone, Debug, PartialEq, Digital)]
508pub struct VerifyMemsOut {
509    /// Encoded `R` at last cycle's read address.
510    pub r_enc: [b64; 4],
511    /// `S` at last cycle's read address.
512    pub s_words: [b64; 4],
513    /// Request facts at last cycle's read address.
514    pub meta: Meta,
515    /// Free context at last cycle's read address.
516    pub free_list: b11,
517    /// Table base coordinate at last cycle's read address.
518    pub bases: Fe17,
519}
520
521/// Ports and kernel of [`VerifyMems`].
522impl SynchronousIO for VerifyMems {
523    /// Input port bundle.
524    type I = VerifyMemsIn;
525    /// Output port bundle.
526    type O = VerifyMemsOut;
527    /// Kernel computing the outputs and the next state.
528    type Kernel = verify_mems_kernel;
529}
530
531/// Wiring for [`VerifyMems`]: each port goes straight to its RAM.
532#[kernel]
533pub fn verify_mems_kernel(_cr: ClockReset, i: VerifyMemsIn, q: VerifyMemsQ) -> (VerifyMemsOut, VerifyMemsD) {
534    let mut d = VerifyMemsD::dont_care();
535    d.r_enc = i.r_enc;
536    d.s_words = i.s_words;
537    d.meta = i.meta;
538    d.free_list = i.free_list;
539    d.bases = i.bases;
540    (
541        VerifyMemsOut { r_enc: q.r_enc, s_words: q.s_words, meta: q.meta, free_list: q.free_list, bases: q.bases },
542        d,
543    )
544}
545
546/// Known-key strict verifier with `NV` verify machines, `NSHA` SHA-512 rings
547/// and `NSC` scalar rings; with `SH = 1` it hashes through the shared
548/// SHA-512 port ([`VerifyOut::sha_ext`]) instead and its rings stay idle
549/// (use `NSHA = 1`; synthesis removes an idle ring).
550///
551/// Limits (checked in `Default`): `1 <= NV <= 64`, `NSHA >= 1`, `NSC >= 1`,
552/// `SH <= 1`.
553#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
554#[rhdl(dq_no_prefix)]
555pub struct KnownKeyVerifyCore<const NV: usize, const NSHA: usize, const NSC: usize, const SH: usize> {
556    /// Control state.
557    regs: DFF<VerifyRegs<NV>>,
558    /// Per-context memories, free list, bases.
559    mems: VerifyMems,
560    /// Verify machines, each with its own copy of all eight key tables.
561    vm: [VerifyMachine; NV],
562    /// SHA-512 rings for the challenge (idle when `SH = 1`).
563    sha: [ShaRing; NSHA],
564    /// Scalar rings for `k = H mod l`.
565    sc: [ScalarRing; NSC],
566    /// Batch point encoding of the verify machines' results.
567    cps: CompressStage,
568    /// Key loading and `R` decoding.
569    aux: AuxMachine,
570}
571
572/// The aux machine's ROM and the entry of each program (decode, bases,
573/// 16-entry digit table, 8-entry digit table).
574///
575/// Radix-32 digits need 16 table entries and radix-16 digits 8, hence two
576/// builds of [`digit_table`]. The entries end up in `aux_entry`, in this
577/// order.
578pub fn aux_programs() -> (crate::microcode::Program, Vec<usize>) {
579    let dec = decode_candidate();
580    let bases = table_bases();
581    let wide = digit_table(16);
582    let narrow = digit_table(8);
583    concat("aux", &[&dec, &bases, &wide, &narrow])
584}
585
586/// Reset state: all slots empty, all 2,048 contexts free, full compress
587/// credits, every verify-machine context free.
588impl<const NV: usize, const NSHA: usize, const NSC: usize, const SH: usize> Default for KnownKeyVerifyCore<NV, NSHA, NSC, SH> {
589    /// Builds the reset state.
590    fn default() -> Self {
591        // Parameter limits; SH selects internal rings (0) or the shared
592        // port (1).
593        assert!(NV >= 1 && NV <= 64 && NSHA >= 1 && NSC >= 1 && SH <= 1);
594        let (aux_rom, entries) = aux_programs();
595        let zero = Fe17::zero();
596        let regs = VerifyRegs::<NV> {
597            key_enc: [[b64(0); 4]; KEY_SLOTS],
598            key_state: [b2(KEY_EMPTY); KEY_SLOTS],
599            key_small: [false; KEY_SLOTS],
600            kl_state: b2(KL_IDLE),
601            kl_slot: bits(0),
602            kl_issued: false,
603            kl_count: b8(0),
604            kl_feed_digit: b6(0),
605            kl_feed_step: b3(0),
606            kl_feed_ctx: b5(0),
607            kl_digits_done: b6(0),
608            aux_busy: b32(0),
609            kl_drain: b7(0),
610            tbl: [idle_table_write(); NV],
611            aux_entry: std::array::from_fn(|k| b12(entries[k] as u128)),
612            dec_owner: b2(DEC_NONE),
613            dec_feed: false,
614            dec_y: zero,
615            dec_sign: false,
616            dec_ctx: b11(0),
617            dec_vals: [zero; 4],
618            dec_count: b4(0),
619            dec_plain: false,
620            dec_flipped: false,
621            dec_neg: [[b85(0); 3]; 3],
622            dec_neg_c: [false; 3],
623            dec_running: false,
624            adm_valid: false,
625            adm_req: VerifyRequest {
626                message: Words512 { words: [b64(0); 8] },
627                public_key: ScalarWords { words: [b64(0); 4] },
628                signature: Words512 { words: [b64(0); 8] },
629                tag: b16(0),
630            },
631            adm_error: b8(0),
632            adm_meta: Meta { tag: b16(0), slot: bits(0), a_small: false, r_small: false, canonical_s: false },
633            free_head_valid: false,
634            free_head: b11(0),
635            free_fetching: false,
636            free_rd: b11(0),
637            free_wr: b11(0),
638            // The free list starts full: its RAM is preloaded with 0..2048.
639            free_count: b12(2048),
640            credits: b12(COMPRESS_SLOTS as u128),
641            sc: ScStage { valid: false, tag: b16(0), x: [b17(0); 31] },
642            vm: VmStage { valid: false, has_mem: false, ctx: b11(0), k: [b6(0); 64], s: [b6(0); 64], slot: bits(0) },
643            vm_free: [b32(0xffff_ffff); NV],
644            vm_has_free: [true; NV],
645            out_req: [false; NV],
646            out_req_x: [false; NV],
647            pt: PointIn { valid: false, tag: b16(0), index: b2(0), value: zero },
648            pt_src: [false; NV],
649            cmp: CmpStage { valid: false, ctx: b11(0), enc: [b64(0); 4] },
650            slow_valid: false,
651            slow_ctx: b11(0),
652            slow_r: [b64(0); 4],
653            slow_meta: Meta { tag: b16(0), slot: bits(0), a_small: false, r_small: false, canonical_s: false },
654            dec_meta: Meta { tag: b16(0), slot: bits(0), a_small: false, r_small: false, canonical_s: false },
655            fin: FinStage { valid: false, free_ctx: false, ctx: b11(0), tag: b16(0), error: b8(0) },
656            error: false,
657        };
658        let program = verify_known_key();
659        Self {
660            regs: DFF::new(regs),
661            mems: VerifyMems {
662                r_enc: SyncBRAM::default(),
663                s_words: SyncBRAM::default(),
664                meta: SyncBRAM::default(),
665                // Entry k holds context k.
666                free_list: SyncBRAM::new((0..2048u128).map(|k| (b11(k), b11(k)))),
667                bases: SyncBRAM::default(),
668            },
669            // As in the signer: the first `URAM_MACHINES` in UltraRAM, the
670            // rest in the block-RAM fallback.
671            vm: std::array::from_fn(|k| VerifyMachine::with_memory(&program, crate::machine::placement(k))),
672            sha: std::array::from_fn(|_| ShaRing::default()),
673            sc: std::array::from_fn(|_| ScalarRing::default()),
674            cps: CompressStage::default(),
675            aux: AuxMachine::new(&aux_rom),
676        }
677    }
678}
679
680/// Ports and kernel of [`KnownKeyVerifyCore`].
681impl<const NV: usize, const NSHA: usize, const NSC: usize, const SH: usize> SynchronousIO for KnownKeyVerifyCore<NV, NSHA, NSC, SH> {
682    /// Input port bundle.
683    type I = VerifyIn;
684    /// Output port bundle.
685    type O = VerifyOut;
686    /// Kernel computing the outputs and the next state.
687    type Kernel = known_key_verify_kernel<NV, NSHA, NSC, SH>;
688}
689
690/// `y < p` for a 32-byte encoding (sign bit ignored).
691///
692/// `p = 2^255 - 19` has words `ffff..ffed, ffff..ffff, ffff..ffff,
693/// 7fff..ffff` (least significant first), so `y < p` unless the top three
694/// words are at their maximum and the low word is at least `ffff..ffed`.
695#[kernel]
696pub fn canonical_y(words: [b64; 4]) -> bool {
697    // Top word without the sign bit.
698    let high = words[3] & b64(0x7fff_ffff_ffff_ffff);
699    high < b64(0x7fff_ffff_ffff_ffff)
700        || (high == b64(0x7fff_ffff_ffff_ffff)
701            && words[2] == b64(0xffff_ffff_ffff_ffff)
702            && words[1] == b64(0xffff_ffff_ffff_ffff)
703            && words[0] < b64(0xffff_ffff_ffff_ffed))
704}
705
706/// The lowest non-empty slot whose key encoding equals `key`, if any (slot
707/// 0 otherwise). Two copies because a kernel's loop bound must be a literal.
708#[cfg(not(feature = "key-slots-16"))]
709#[kernel]
710fn find_slot(key: [b64; 4], enc: [[b64; 4]; KEY_SLOTS], state: [b2; KEY_SLOTS]) -> (bool, KeySlot) {
711    let mut found = false;
712    let mut slot: KeySlot = bits(0);
713    for k in 0..8 {
714        if !found && (state[k] != b2(KEY_EMPTY)) && words_equal(key, enc[k]) {
715            found = true;
716            slot = bits(k as u128);
717        }
718    }
719    (found, slot)
720}
721
722/// The lowest non-empty slot whose key encoding equals `key` (16 slots).
723#[cfg(feature = "key-slots-16")]
724#[kernel]
725fn find_slot(key: [b64; 4], enc: [[b64; 4]; KEY_SLOTS], state: [b2; KEY_SLOTS]) -> (bool, KeySlot) {
726    let mut found = false;
727    let mut slot: KeySlot = bits(0);
728    for k in 0..16 {
729        if !found && (state[k] != b2(KEY_EMPTY)) && words_equal(key, enc[k]) {
730            found = true;
731            slot = bits(k as u128);
732        }
733    }
734    (found, slot)
735}
736
737/// Equality of two 256-bit values.
738#[kernel]
739fn words_equal(a: [b64; 4], b: [b64; 4]) -> bool {
740    a[0] == b[0] && a[1] == b[1] && a[2] == b[2] && a[3] == b[3]
741}
742
743/// The eight low-order encodings strict verification rejects.
744///
745/// The list is the one `rhdl_ed25519_fast_core` checks (see
746/// `host::small_order_encoding`): the identity (`y = 1`), the point of order
747/// 2 (`y = p - 1`), the two order-4 points (`y = 0`, both signs), and the
748/// four order-8 points (two `y` values, both signs). The comparison is on
749/// the encoding bytes, so it is only meaningful for canonical encodings.
750#[kernel]
751pub fn small_order_encoding(w: [b64; 4]) -> bool {
752    words_equal(w, [b64(1), b64(0), b64(0), b64(0)])
753        || words_equal(w, [b64(0x4fd84d3d706a17c7), b64(0x0f67100d760b3cba), b64(0xc6cc392cfa53202a), b64(0x7a03ac9277fdc74e)])
754        || words_equal(w, [b64(0), b64(0), b64(0), b64(0x8000_0000_0000_0000)])
755        || words_equal(w, [b64(0xb027b2c28f95e826), b64(0xf098eff289f4c345), b64(0x3933c6d305acdfd5), b64(0x05fc536d880238b1)])
756        || words_equal(w, [b64(0xffff_ffff_ffff_ffec), b64(0xffff_ffff_ffff_ffff), b64(0xffff_ffff_ffff_ffff), b64(0x7fff_ffff_ffff_ffff)])
757        || words_equal(w, [b64(0xb027b2c28f95e826), b64(0xf098eff289f4c345), b64(0x3933c6d305acdfd5), b64(0x85fc536d880238b1)])
758        || words_equal(w, [b64(0), b64(0), b64(0), b64(0)])
759        || words_equal(w, [b64(0x4fd84d3d706a17c7), b64(0x0f67100d760b3cba), b64(0xc6cc392cfa53202a), b64(0xfa03ac9277fdc74e)])
760}
761
762/// `S < l` for four little-endian words, `l = 2^252 +
763/// 27742317777372353535851937790883648493` the group order.
764#[kernel]
765pub fn below_l(s: [b64; 4]) -> bool {
766    let l = [b64(0x5812631a5cf5d3ed), b64(0x14def9dea2f79cd6), b64(0), b64(0x1000000000000000)];
767    // Lexicographic compare from the most significant word: the first word
768    // that differs decides; equal values are not below `l`.
769    let mut less = false;
770    let mut decided = false;
771    for k in 0..4 {
772        let w = 3 - k;
773        if !decided && s[w] != l[w] {
774            decided = true;
775            less = s[w] < l[w];
776        }
777    }
778    less
779}
780
781/// `y` of an encoding (sign bit cleared) as field digits.
782///
783/// The result is a canonical field element only if [`canonical_y`] holds.
784#[kernel]
785pub fn encoding_y(w: [b64; 4]) -> Fe17 {
786    let mut y = w;
787    y[3] = y[3] & b64(0x7fff_ffff_ffff_ffff);
788    Fe17 { d: words_to_digits15(y) }
789}
790
791/// Equality of two field elements, digit by digit. Valid because the field
792/// units always produce canonical values.
793#[kernel]
794fn fe_equal(a: Fe17, b: Fe17) -> bool {
795    let mut eq = true;
796    for k in 0..15 {
797        if a.d[k] != b.d[k] {
798            eq = false;
799        }
800    }
801    eq
802}
803
804/// One limb of `p - v = !v + (2^255 - 18)` mod `2^255`: limb `step` of
805/// the result from limb `step` of `v` and the carry out of the limbs below
806/// (`c`); the other limbs of `acc` pass through. One 85-bit carry chain.
807#[kernel]
808pub fn p_minus_step(v: [b85; 3], acc: [b85; 3], c: bool, step: b2) -> ([b85; 3], bool) {
809    let mut out = acc;
810    let (s0, c0) = add_limb(!v[0], nb_const(true, true), false);
811    let (s1, c1) = add_limb(!v[1], nb_const(true, false), c);
812    let (s2, c2) = add_limb(!v[2], nb_const(true, false), c);
813    let mut carry = c0;
814    if step == b2(0) {
815        out[0] = s0;
816    } else if step == b2(1) {
817        out[1] = s1;
818        carry = c1;
819    } else {
820        out[2] = s2;
821        carry = c2;
822    }
823    (out, carry)
824}
825
826/// `-v` from the limbs of `p - v` (as `field_neg`: zero stays zero).
827#[kernel]
828pub fn neg_from(v: Fe17, p_minus_v: [b85; 3]) -> Fe17 {
829    if fe_is_zero(v) { v } else { Fe17 { d: digits_from_limbs(p_minus_v) } }
830}
831
832/// A canonical field element is zero.
833#[kernel]
834fn fe_is_zero(a: Fe17) -> bool {
835    let mut z = true;
836    for k in 0..15 {
837        if a.d[k] != b17(0) {
838            z = false;
839        }
840    }
841    z
842}
843
844/// Row of digit `i`'s first entry pair within a key's 512-row table.
845///
846/// Rows hold two entries each. Radix-32 digits 0..39 have 16 entries (8
847/// rows), so digit `i` starts at row `8i`; radix-16 digits 39..54 have 8
848/// entries (4 rows) and start after the 312 rows of the radix-32 digits.
849#[kernel]
850pub fn digit_row(i: b6) -> b9 {
851    if i < b6(39) {
852        i.resize::<9>() << 3
853    } else {
854        b9(312) + ((i - b6(39)).resize::<9>() << 2)
855    }
856}
857
858/// `(entry, coordinate)` of a digit-table output index `3e + c`.
859///
860/// `e` is the 0-based entry (the table point `(e + 1) * Q`), `c` the Niels
861/// coordinate (0 `y+x`, 1 `y-x`, 2 `2dxy`). Computed by comparing `index`
862/// with all 48 constant values rather than dividing by 3.
863#[kernel]
864pub fn split_entry(index: b6) -> (b5, b2) {
865    let mut e = b5(0);
866    let mut c = b2(0);
867    for entry in 0..16 {
868        for coord in 0..3 {
869            if index == b6((3 * entry + coord) as u128) {
870                e = b5(entry as u128);
871                c = b2(coord as u128);
872            }
873        }
874    }
875    (e, c)
876}
877
878/// Machine inputs that do nothing: no start, no digits, no input or table
879/// write, output not accepted. `filler` is the value carried by the idle
880/// input write.
881#[kernel]
882fn idle_machine_in(filler: Fe17) -> MachineIn<5> {
883    MachineIn::<5> {
884        start: StartReq::<5> { valid: false, ctx: b5(0), tag: b16(0), key: bits(0), entry: b12(0) },
885        digits: DigitsWrite::<5> { valid: false, ctx: b5(0), digits: [b6(0); 128] },
886        input: InputWrite::<5> { valid: false, ctx: b5(0), index: b7(0), value: filler },
887        table: idle_table_write(),
888        out_accept: false,
889    }
890}
891
892/// Key and base-point digits in the machine's 128 slots.
893///
894/// The verify program reads `S`'s digits (base-point table) from slots
895/// 0..64 and `k`'s digits (key table) from slots 64..128.
896#[kernel]
897pub fn join_digits(s: [b6; 64], k: [b6; 64]) -> [b6; 128] {
898    let mut out = [b6(0); 128];
899    for j in 0..64 {
900        out[j] = s[j];
901        out[64 + j] = k[j];
902    }
903    out
904}
905
906/// State transition for [`KnownKeyVerifyCore`].
907///
908/// The sections below run every cycle, in dataflow order: free contexts, SHA
909/// availability, admission, SHA -> scalar, scalar -> verify machine, verify
910/// machine -> compress, compare, aux machine (slow path and key loading),
911/// and the result register. A stage takes new work only when it is empty or
912/// hands its content on in the same cycle.
913#[kernel]
914pub fn known_key_verify_kernel<const NV: usize, const NSHA: usize, const NSC: usize, const SH: usize>(
915    _cr: ClockReset,
916    i: VerifyIn,
917    q: Q<NV, NSHA, NSC, SH>,
918) -> (VerifyOut, D<NV, NSHA, NSC, SH>) {
919    let r = q.regs;
920    let mut n = r;
921    let mut d = D::<NV, NSHA, NSC, SH>::dont_care();
922    let zero = Fe17 { d: [b17(0); 15] };
923    // The scalar 1 in 15 digits: multiplier Y = 1 makes the scalar ring
924    // compute k = H mod l.
925    let one15 = {
926        let mut o = [b17(0); 15];
927        o[0] = b17(1);
928        o
929    };
930    // Default memory ports: no writes, read address 0. Stages below override
931    // the read address when they need data next cycle. The free list always
932    // reads at its read pointer.
933    d.mems.r_enc = ram_read::<[b64; 4], 11>(b11(0), q.mems.r_enc);
934    d.mems.s_words = ram_read::<[b64; 4], 11>(b11(0), q.mems.s_words);
935    d.mems.meta = ram_read::<Meta, 11>(b11(0), q.mems.meta);
936    d.mems.free_list = ram_read::<b11, 11>(r.free_rd, q.mems.free_list);
937    d.mems.bases = ram_read::<Fe17, 8>(b8(0), q.mems.bases);
938    for k in 0..NV {
939        d.vm[k] = idle_machine_in(zero);
940    }
941    d.aux = idle_machine_in(zero);
942
943    // ================= Free contexts =================
944    // A one-entry prefetch (`free_head`) in front of the free-list RAM, so
945    // admission can take a context without waiting for a read.
946    // `free_count` collects this cycle's pops and pushes.
947    let mut free_count = r.free_count;
948    if r.free_fetching {
949        n.free_head = q.mems.free_list;
950        n.free_head_valid = true;
951        n.free_fetching = false;
952    }
953    if !r.free_head_valid && !r.free_fetching && r.free_count != b12(0) {
954        n.free_fetching = true;
955        n.free_rd = r.free_rd + b11(1);
956        free_count = free_count - b12(1);
957    }
958    // Contexts not in the RAM, the head or the read under way are in use.
959    let in_flight = b12(2048) - r.free_count - if r.free_head_valid { b12(1) } else { b12(0) }
960        - if r.free_fetching { b12(1) } else { b12(0) };
961    // Compress credits after this cycle's admission; passed to the compress
962    // stage, then updated with its spends and returns.
963    let mut credits = r.credits;
964
965    // ================= SHA availability =================
966    // `sha_any`: some SHA resource can take a job; `sha_pick`: which ring,
967    // or `NSHA` for the shared port. With SH = 1 the rings are never picked.
968    let mut sha_any = false;
969    let mut sha_pick = b8(0);
970    let shared = SH == 1;
971    for k in 0..NSHA {
972        if q.sha[k].ready && !sha_any && !shared {
973            sha_any = true;
974            sha_pick = b8(k as u128);
975        }
976    }
977    if shared && i.sha_ext.ready {
978        sha_any = true;
979        sha_pick = b8(NSHA as u128);
980    }
981    let mut sha_job = ShaJob { valid: false, tag: b16(0), block: [b64(0); 16], second_pad: false };
982
983    // ================= Admission =================
984    // Two steps. Accept: the request's key is matched and its immediate
985    // result and facts are decided, into the admission register (`adm_*`).
986    // Admit: the registered request takes a context, a credit and a SHA
987    // resource (or reports its immediate result). Matching and admitting in
988    // the same cycle ran eight 256-bit key compares into the memory writes
989    // and SHA job: −2.13 ns in the routed L200 verifier (2026-10-19).
990    let req = i.request;
991    let in_r = [req.signature.words[0], req.signature.words[1], req.signature.words[2], req.signature.words[3]];
992    let in_s = [req.signature.words[4], req.signature.words[5], req.signature.words[6], req.signature.words[7]];
993    // Find the slot holding exactly this key encoding (the lowest one if a
994    // key was loaded twice). Empty slots never match.
995    let (slot_found, slot) = find_slot(req.public_key.words, r.key_enc, r.key_state);
996    // Meaningful only when `slot_found`; slot 0 otherwise.
997    let slot_state = r.key_state[slot];
998    // No request can reach a LOADING slot: requests are accepted only with
999    // no key load running or offered, and a key load starts only with no
1000    // request held or in flight.
1001    // Immediate results: unknown key, invalid key, non-canonical R.
1002    // UNKNOWN_KEY is outside the strict priority list and is checked first;
1003    // BAD_PUBLIC_KEY before BAD_R follows the strict priority.
1004    let in_error = if !slot_found {
1005        b8(VERIFY_UNKNOWN_KEY)
1006    } else if slot_state == b2(KEY_INVALID) {
1007        b8(VERIFY_BAD_PUBLIC_KEY)
1008    } else if !canonical_y(in_r) {
1009        b8(VERIFY_BAD_R)
1010    } else {
1011        b8(VERIFY_OK)
1012    };
1013    // The held request.
1014    let req = r.adm_req;
1015    let r_words = [req.signature.words[0], req.signature.words[1], req.signature.words[2], req.signature.words[3]];
1016    let s_words = [req.signature.words[4], req.signature.words[5], req.signature.words[6], req.signature.words[7]];
1017    let immediate_error = r.adm_error;
1018    let immediate = immediate_error != b8(VERIFY_OK);
1019    // It is admitted when it is rejected outright, or when a context, a
1020    // credit and a SHA resource are all available.
1021    let admit = r.adm_valid && (immediate || (r.free_head_valid && r.credits != b12(0) && sha_any));
1022    // `ready` never depends on the offered request (ready/valid): the
1023    // register must be empty or emptying, and no key load may be running or
1024    // offered this cycle.
1025    let ready = (!r.adm_valid || admit) && r.kl_state == b2(KL_IDLE) && !i.load_key_valid;
1026    if admit {
1027        n.adm_valid = false;
1028    }
1029    if i.valid && ready {
1030        n.adm_valid = true;
1031        n.adm_req = i.request;
1032        n.adm_error = in_error;
1033        n.adm_meta = Meta {
1034            tag: i.request.tag,
1035            slot: slot,
1036            a_small: r.key_small[slot],
1037            r_small: small_order_encoding(in_r),
1038            canonical_s: below_l(in_s),
1039        };
1040    }
1041    let accepted = admit;
1042    // Someone wrote the result register this cycle. Priority: admission
1043    // rejects, then the compare, then the slow-path decode; the later ones
1044    // wait a cycle.
1045    let mut fin_taken = false;
1046    if accepted && immediate {
1047        // Rejected at admission: no context, no credit, no hashing.
1048        n.fin = FinStage { valid: true, free_ctx: false, ctx: b11(0), tag: req.tag, error: immediate_error };
1049        fin_taken = true;
1050    }
1051    if accepted && !immediate {
1052        // Take the prefetched context and one compress credit (the point
1053        // this request will send), and record R, S and the facts the final
1054        // error decision needs.
1055        let ctx = r.free_head;
1056        n.free_head_valid = false;
1057        credits = credits - b12(1);
1058        d.mems.r_enc.write = RamWrite::<[b64; 4], 11> { addr: ctx, value: r_words, enable: true };
1059        d.mems.s_words.write = RamWrite::<[b64; 4], 11> { addr: ctx, value: s_words, enable: true };
1060        d.mems.meta.write = RamWrite::<Meta, 11> { addr: ctx, value: r.adm_meta, enable: true };
1061        // Tags carry the context and the key slot through the rings.
1062        // The challenge R || A || M is 128 bytes: one full block, then the
1063        // constant padding block (`second_pad`).
1064        let tag = (r.adm_meta.slot.resize::<16>() << 11) | ctx.resize::<16>();
1065        sha_job = ShaJob { valid: true, tag: tag, block: challenge_block(r_words, req.public_key.words, req.message), second_pad: challenge_second_pad() };
1066    }
1067    // The job goes to exactly one SHA resource: the picked ring, or the
1068    // shared port when `sha_pick == NSHA`. Results are taken further down.
1069    let mut sha_ext = ShaRingIn {
1070        job: ShaJob { valid: sha_job.valid && shared && sha_pick == b8(NSHA as u128), tag: sha_job.tag, block: sha_job.block, second_pad: sha_job.second_pad },
1071        out_ready: false,
1072    };
1073    for k in 0..NSHA {
1074        d.sha[k] = ShaRingIn {
1075            job: ShaJob { valid: sha_job.valid && b8(k as u128) == sha_pick, tag: sha_job.tag, block: sha_job.block, second_pad: sha_job.second_pad },
1076            out_ready: false,
1077        };
1078    }
1079
1080    // ================= SHA done -> scalar stage =================
1081    // First hand the held digest to a free scalar ring: k = H * 1 + 0 mod l.
1082    let mut sc_any = false;
1083    let mut sc_pick = b8(0);
1084    for k in 0..NSC {
1085        if q.sc[k].ready && !sc_any {
1086            sc_any = true;
1087            sc_pick = b8(k as u128);
1088        }
1089    }
1090    let sc_dispatch = r.sc.valid && sc_any;
1091    for k in 0..NSC {
1092        d.sc[k] = ScalarRingIn {
1093            job: ScalarJob { valid: sc_dispatch && b8(k as u128) == sc_pick, tag: r.sc.tag, x: r.sc.x, y: one15, z: [b17(0); 15] },
1094            out_ready: false,
1095        };
1096    }
1097    if sc_dispatch {
1098        n.sc.valid = false;
1099    }
1100    // The stage can take a digest if it is empty or empties now. One digest
1101    // per cycle, lowest-numbered ring first; the shared port when SH = 1.
1102    let sc_free = !r.sc.valid || sc_dispatch;
1103    let mut sha_taken = false;
1104    for k in 0..NSHA {
1105        let done = q.sha[k].done;
1106        if done.valid && sc_free && !sha_taken {
1107            sha_taken = true;
1108            d.sha[k].out_ready = true;
1109            n.sc = ScStage { valid: true, tag: done.tag, x: words_to_digits31(digest_le(done.state)) };
1110        }
1111    }
1112    let ext_done: ShaDone = i.sha_ext.done;
1113    if shared && ext_done.valid && sc_free && !sha_taken {
1114        sha_ext.out_ready = true;
1115        n.sc = ScStage { valid: true, tag: ext_done.tag, x: words_to_digits31(digest_le(ext_done.state)) };
1116    }
1117
1118    // ================= Scalar done -> verify-machine stage =================
1119    // The stage reads S and the slot, then dispatches to a free context.
1120    // Cycle 1 (below, at the take): k is recoded and S's read is issued.
1121    // Cycle 2 (here): S arrives from the RAM and is recoded.
1122    if r.vm.valid && !r.vm.has_mem {
1123        n.vm.s = recode_mixed(words_to_digits15(q.mems.s_words));
1124        n.vm.has_mem = true;
1125    }
1126    let (vm_any, vm_pick) = pick_machine::<NV>(r.vm_free, r.vm_has_free);
1127    let vm_dispatch = r.vm.valid && r.vm.has_mem && vm_any;
1128    for k in 0..NV {
1129        // Lowest free context of machine k.
1130        let mask = r.vm_free[k];
1131        let mut c = b5(0);
1132        let mut found = false;
1133        for j in 0..32 {
1134            if !found && ((mask >> (j as u128)) & b32(1)) != b32(0) {
1135                found = true;
1136                c = b5(j as u128);
1137            }
1138        }
1139        // Start the verify program (entry 0) with the digits loaded in the
1140        // same cycle. The machine's tag is the context; `key` selects the
1141        // slot's key table.
1142        let go = vm_dispatch && b8(k as u128) == vm_pick;
1143        d.vm[k].start = StartReq::<5> { valid: go, ctx: c, tag: r.vm.ctx.resize::<16>(), key: r.vm.slot, entry: b12(0) };
1144        d.vm[k].digits = DigitsWrite::<5> { valid: go, ctx: c, digits: join_digits(r.vm.s, r.vm.k) };
1145        if go {
1146            n.vm_free[k] = mask & !(b32(1) << (c.resize::<32>()));
1147        }
1148        // A retiring context is free again (its outputs have been emitted).
1149        if q.vm[k].done {
1150            n.vm_free[k] = n.vm_free[k] | (b32(1) << (q.vm[k].done_ctx.resize::<32>()));
1151        }
1152        n.vm_has_free[k] = n.vm_free[k] != b32(0);
1153    }
1154    if vm_dispatch {
1155        n.vm.valid = false;
1156    }
1157    // Take one scalar result per cycle into the stage when it is free.
1158    let vm_stage_free = !r.vm.valid || vm_dispatch;
1159    let mut sc_taken = false;
1160    for k in 0..NSC {
1161        let done = q.sc[k].done;
1162        if done.valid && vm_stage_free && !sc_taken {
1163            sc_taken = true;
1164            d.sc[k].out_ready = true;
1165            // Tag bits 0..11 are the context, bits 11..14 the key slot.
1166            let ctx = done.tag.resize::<11>();
1167            d.mems.s_words.read_addr = ctx;
1168            n.vm = VmStage { valid: true, has_mem: false, ctx: ctx, k: recode_mixed(done.s), s: [b6(0); 64], slot: (done.tag >> 11).resize() };
1169        }
1170    }
1171
1172    // ================= Verify-machine outputs -> compress =================
1173    // One coordinate per two cycles at most, first machine first, picked
1174    // from registered offers into the registered `pt`. X opens a batch slot,
1175    // so it is picked only while `accept_x` holds; Y and Z are always taken.
1176    // The credit taken at admission guarantees the slot exists eventually.
1177    // The machine is told only when the stage takes `pt` (`pt_taken` is the
1178    // stage's own take condition, from its registered `accept_x`). An X
1179    // picked while `accept_x` held can still be refused the next cycle (a
1180    // partial-batch flush closes the filling context); it is then dropped
1181    // from `pt` and picked again later, the machine still holding it.
1182    // Why registered: picking straight from the machines chained the offers
1183    // of all machines into the stage's loader. With 24 machines spread over
1184    // the die that was the routed verifier's worst path (-5.76 ns,
1185    // 2026-10-18).
1186    let accept_x = q.cps.accept_x;
1187    let pt_taken = r.pt.valid && (r.pt.index != b2(0) || accept_x);
1188    let mut point = PointIn { valid: false, tag: b16(0), index: b2(0), value: zero };
1189    let mut vm_out_taken = r.pt.valid;
1190    for k in 0..NV {
1191        let o = q.vm[k].out;
1192        let accepted = r.pt_src[k] && pt_taken;
1193        if accepted {
1194            d.vm[k].out_accept = true;
1195        }
1196        let grant = r.out_req[k] && !vm_out_taken && (!r.out_req_x[k] || accept_x);
1197        if grant {
1198            vm_out_taken = true;
1199            point = PointIn { valid: true, tag: o.tag, index: o.index.resize::<2>(), value: o.value };
1200        }
1201        n.pt_src[k] = grant;
1202        n.out_req[k] = o.valid && !grant && !accepted;
1203        n.out_req_x[k] = o.index == b6(0);
1204    }
1205    n.pt = point;
1206
1207    // ================= Compare =================
1208    // A compare stage reads R and the request facts, then either retires
1209    // (point encodes to R) or hands the context to the slow path.
1210    // The read for r.cmp.ctx was issued last cycle (on take, or while waiting).
1211    let mut cmp_done = false;
1212    if r.cmp.valid {
1213        let m = q.mems.meta;
1214        if words_equal(r.cmp.enc, q.mems.r_enc) {
1215            // Match: R is a valid point encoding (the compress stage only
1216            // produces those), so BAD_R cannot apply. Report the first
1217            // admission fact, in strict priority order, or OK.
1218            if !fin_taken {
1219                let error = if m.a_small {
1220                    b8(VERIFY_SMALL_PUBLIC_KEY)
1221                } else if m.r_small {
1222                    b8(VERIFY_SMALL_R)
1223                } else if !m.canonical_s {
1224                    b8(VERIFY_NONCANONICAL_S)
1225                } else {
1226                    b8(VERIFY_OK)
1227                };
1228                n.fin = FinStage { valid: true, free_ctx: true, ctx: r.cmp.ctx, tag: m.tag, error: error };
1229                fin_taken = true;
1230                cmp_done = true;
1231            }
1232        } else if !r.slow_valid {
1233            // Mismatch: BAD_R or EQUATION_FAILURE, which only an R decode
1234            // can tell apart. Park the request in the one-entry slow
1235            // register; the context stays in use until the decode reports.
1236            n.slow_valid = true;
1237            n.slow_ctx = r.cmp.ctx;
1238            n.slow_r = q.mems.r_enc;
1239            n.slow_meta = m;
1240            cmp_done = true;
1241        }
1242        // Still waiting (result register taken, or slow register full):
1243        // re-read so the data is valid again next cycle.
1244        if !cmp_done {
1245            d.mems.r_enc.read_addr = r.cmp.ctx;
1246            d.mems.meta.read_addr = r.cmp.ctx;
1247        }
1248    }
1249    if cmp_done {
1250        n.cmp.valid = false;
1251    }
1252    // Take an encoded point when the compare register is empty, issuing the
1253    // reads of R and the facts it will compare against next cycle.
1254    // Why only when empty (not also when it retires this cycle): retiring
1255    // depends on `fin_taken`, i.e. on the admission key match, and
1256    // `take_done` is the compress stage's `out_ready`, which fans out over
1257    // its output stage. That chain was the routed 6,0,1,1 worst path
1258    // (-0.190 ns, 2026-10-18). A point arrives every ~20 cycles, so the
1259    // extra cycle of a back-to-back take is not visible in throughput.
1260    let done = q.cps.done;
1261    let take_done = done.valid && !r.cmp.valid;
1262    if take_done {
1263        let ctx = done.tag.resize::<11>();
1264        d.mems.r_enc.read_addr = ctx;
1265        d.mems.meta.read_addr = ctx;
1266        n.cmp = CmpStage { valid: true, ctx: ctx, enc: done.enc };
1267    }
1268    // The stage sees the registered credits and keeps a margin for this
1269    // cycle's admission and the next, so it never flushes a partial batch
1270    // with credits already promised to a request.
1271    d.cps = CompressIn { point: r.pt, credits: r.credits, out_ready: take_done };
1272    credits = credits - q.cps.credit_spend + q.cps.credit_return;
1273    n.credits = credits;
1274    // Invariant: credits stay within the compress pool; an underflow would
1275    // wrap the 12-bit count above it. Sticky.
1276    if credits > b12(COMPRESS_SLOTS as u128) {
1277        n.error = true;
1278    }
1279
1280    // ================= Aux machine =================
1281    // Context 0 decodes (a key or an R), context 1 computes table bases,
1282    // contexts 2..32 build digit tables.
1283    let mut aux_in = idle_machine_in(zero);
1284    // The aux machine has one input-write port; the first user this cycle
1285    // wins, in order: decode feed, bases feed, digit feed.
1286    let mut aux_input_used = false;
1287    // Slow path: start an R decode when the collector is free.
1288    // (Not between the key decode and the bases job, which still needs -A.)
1289    if r.slow_valid
1290        && r.dec_owner == b2(DEC_NONE)
1291        && !r.dec_running
1292        && (r.kl_state == b2(KL_IDLE) || r.kl_state == b2(KL_DIGITS))
1293    {
1294        n.dec_owner = b2(DEC_R);
1295        n.dec_ctx = r.slow_ctx;
1296        n.dec_y = encoding_y(r.slow_r);
1297        n.dec_sign = (r.slow_r[3] >> 63) != b64(0);
1298        n.dec_meta = r.slow_meta;
1299        n.dec_feed = true;
1300        n.dec_count = b4(0);
1301        n.slow_valid = false;
1302    }
1303    // Key load: start.
1304    // Needs the decode collector, so no R decode may be running or waiting.
1305    // The slot's encoding, state and low-order flag are set at once; the
1306    // slot is LOADING until the last digit job retires.
1307    // It also waits until no verification is in flight: loading rewrites the
1308    // slot's key tables, which a request in flight may still be reading.
1309    let load_key_ready =
1310        r.kl_state == b2(KL_IDLE) && r.dec_owner == b2(DEC_NONE) && !r.dec_running && !r.slow_valid && in_flight == b12(0) && !r.adm_valid;
1311    // A public key whose y is not below p is invalid (strict decoding):
1312    // marked at once, without running the decode program.
1313    let key_canonical = canonical_y(i.load_key.words);
1314    if i.load_key_valid && load_key_ready && !key_canonical {
1315        n.key_enc[i.load_slot] = i.load_key.words;
1316        n.key_state[i.load_slot] = b2(KEY_INVALID);
1317        n.key_small[i.load_slot] = false;
1318    }
1319    if i.load_key_valid && load_key_ready && key_canonical {
1320        n.kl_state = b2(KL_DECODE);
1321        n.kl_slot = i.load_slot;
1322        n.key_enc[i.load_slot] = i.load_key.words;
1323        n.key_state[i.load_slot] = b2(KEY_LOADING);
1324        n.key_small[i.load_slot] = small_order_encoding(i.load_key.words);
1325        n.dec_owner = b2(DEC_KEY);
1326        n.dec_y = encoding_y(i.load_key.words);
1327        n.dec_sign = (i.load_key.words[3] >> 63) != b64(0);
1328        n.dec_feed = true;
1329        n.dec_count = b4(0);
1330    }
1331    // Feed the decode input (y) and start the decode program on context 0.
1332    // The input write and the start go out in the same cycle.
1333    if r.dec_feed && !aux_input_used {
1334        aux_in.input = InputWrite::<5> { valid: true, ctx: b5(0), index: b7(0), value: r.dec_y };
1335        aux_in.start = StartReq::<5> { valid: true, ctx: b5(0), tag: b16(AUX_DECODE) << 8, key: bits(0), entry: r.aux_entry[0] };
1336        aux_input_used = true;
1337        n.dec_feed = false;
1338        n.dec_running = true;
1339    }
1340    // Key load: bases job on context 1 once the decode has produced -A.
1341    if r.kl_state == b2(KL_BASES) && !r.kl_issued && !aux_input_used {
1342        // Inputs 0 and 1 (x of -A, y) are written over two cycles.
1343        let value = if r.kl_feed_step == b3(0) { r.dec_vals[0] } else { r.dec_y };
1344        aux_in.input = InputWrite::<5> { valid: true, ctx: b5(1), index: r.kl_feed_step.resize::<7>(), value: value };
1345        aux_input_used = true;
1346        if r.kl_feed_step == b3(1) {
1347            aux_in.start = StartReq::<5> { valid: true, ctx: b5(1), tag: b16(AUX_BASES) << 8, key: bits(0), entry: r.aux_entry[1] };
1348            n.kl_issued = true;
1349            n.kl_feed_step = b3(0);
1350            n.kl_count = b8(0);
1351        } else {
1352            n.kl_feed_step = r.kl_feed_step + b3(1);
1353        }
1354    }
1355    // Key load: digit jobs, inputs read from the bases memory (1-cycle latency).
1356    // Per digit, five steps: step 0 picks a free context in 2..32 and reads
1357    // base word 0; steps 1..4 write input `step - 1` (X, Y, Z, T of the
1358    // digit's base, read the cycle before) while reading the next word; step
1359    // 4 also starts the job with the 16- or 8-entry table program.
1360    // While a digit's feed is between steps 1 and 4 its next input is the
1361    // bases word read last cycle. If the feed pauses this cycle (another
1362    // user took the input port), re-read that same word so it is still on
1363    // the read port when the feed resumes.
1364    if r.kl_state == b2(KL_DIGITS) && r.kl_feed_step != b3(0) {
1365        d.mems.bases.read_addr = (r.kl_feed_digit.resize::<8>() << 2) + (r.kl_feed_step - b3(1)).resize::<8>();
1366    }
1367    if r.kl_state == b2(KL_DIGITS) && r.kl_feed_digit != b6(54) && !aux_input_used {
1368        // After step 0 the context chosen then is kept.
1369        let mut free_ctx = r.kl_feed_ctx;
1370        let mut found = r.kl_feed_step != b3(0);
1371        if !found {
1372            for j in 2..32 {
1373                if !found && ((r.aux_busy >> (j as u128)) & b32(1)) == b32(0) {
1374                    found = true;
1375                    free_ctx = b5(j as u128);
1376                }
1377            }
1378        }
1379        n.kl_feed_ctx = free_ctx;
1380        // With no free context at step 0, the feed retries next cycle.
1381        if found {
1382            let step = r.kl_feed_step;
1383            // Word `step` of base `digit` (the step-4 read is not used).
1384            let base_addr = (r.kl_feed_digit.resize::<8>() << 2) + step.resize::<8>();
1385            d.mems.bases.read_addr = base_addr;
1386            if step != b3(0) {
1387                aux_in.input = InputWrite::<5> { valid: true, ctx: free_ctx, index: (step - b3(1)).resize::<7>(), value: q.mems.bases };
1388                aux_input_used = true;
1389            }
1390            if step == b3(4) {
1391                // Radix-32 digits get the 16-entry table, radix-16 the 8-entry
1392                // one. The tag carries the digit to route the outputs.
1393                let entry = if r.kl_feed_digit < b6(39) { r.aux_entry[2] } else { r.aux_entry[3] };
1394                aux_in.start = StartReq::<5> {
1395                    valid: true,
1396                    ctx: free_ctx,
1397                    tag: (b16(AUX_DIGIT) << 8) | r.kl_feed_digit.resize::<16>(),
1398                    key: bits(0),
1399                    entry: entry,
1400                };
1401                n.aux_busy = r.aux_busy | (b32(1) << (free_ctx.resize::<32>()));
1402                n.kl_feed_digit = r.kl_feed_digit + b6(1);
1403                n.kl_feed_step = b3(0);
1404            } else {
1405                n.kl_feed_step = step + b3(1);
1406            }
1407        }
1408    }
1409    // Aux outputs: always accepted.
1410    // Every consumer below can take a value each cycle, so the aux machine
1411    // never waits on the verification path.
1412    let ao = q.aux.out;
1413    let mut table_write = idle_table_write();
1414    if ao.valid {
1415        aux_in.out_accept = true;
1416        let kind = ao.tag >> 8;
1417        if kind == b16(AUX_DECODE) {
1418            // Decode outputs 0..4 go to the collector by index.
1419            n.dec_vals[ao.index.resize::<2>()] = ao.value;
1420            n.dec_count = r.dec_count + b4(1);
1421        } else if kind == b16(AUX_BASES) {
1422            // The bases program emits X, Y, Z, T per base in digit order, so
1423            // a running count is the address `4i + c`.
1424            d.mems.bases.write = RamWrite::<Fe17, 8> { addr: r.kl_count, value: ao.value, enable: true };
1425            n.kl_count = r.kl_count + b8(1);
1426        } else {
1427            // Digit-table output: entry e, coordinate c of digit `digit`.
1428            // Entries pair up by row: entry e sits in row `digit_row + e / 2`,
1429            // in the even image if e is even (multiple e + 1 odd) and in the
1430            // odd image otherwise.
1431            let digit = ao.tag.resize::<6>();
1432            let (e, c) = split_entry(ao.index);
1433            let row: Bits<KEY_TABLE_BITS> = ((r.kl_slot.resize::<16>() << 9) | (digit_row(digit) + (e >> 1).resize::<9>()).resize::<16>()).resize();
1434            table_write = TableWrite { valid: true, row: row, odd: (e & b5(1)) != b5(0), coord: c, value: ao.value };
1435        }
1436    }
1437    // Every verify machine holds its own copy of the key tables, so each
1438    // table write goes to all of them, one machine per cycle (`tbl`).
1439    for k in 0..NV {
1440        d.vm[k].table = r.tbl[k];
1441    }
1442    n.tbl[0] = table_write;
1443    for k in 1..NV {
1444        n.tbl[k] = r.tbl[k - 1];
1445    }
1446    // Key load: finish once the last table writes have drained.
1447    if r.kl_drain != b7(0) {
1448        n.kl_drain = r.kl_drain - b7(1);
1449        if r.kl_drain == b7(1) {
1450            n.key_state[r.kl_slot] = b2(KEY_READY);
1451            n.kl_state = b2(KL_IDLE);
1452        }
1453    }
1454    // Aux retirements.
1455    // A decode's result is complete when all four outputs have arrived
1456    // (`dec_count`); its retirement frees aux context 0 for the next decode.
1457    if q.aux.done {
1458        let kind = q.aux.done_tag >> 8;
1459        if kind == b16(AUX_DECODE) {
1460            n.dec_running = false;
1461        }
1462        if kind == b16(AUX_DIGIT) {
1463            // `n.aux_busy` may already include a start from this cycle.
1464            n.aux_busy = n.aux_busy & !(b32(1) << (q.aux.done_ctx.resize::<32>()));
1465            let done_digits = r.kl_digits_done + b6(1);
1466            n.kl_digits_done = done_digits;
1467            // A job's table writes precede its retirement, so the slot's
1468            // last write is in `tbl` here. It reaches machine NV - 1 after NV
1469            // more cycles and its memory one later.
1470            if done_digits == b6(54) {
1471                n.kl_drain = b7(NV as u128) + b7(2);
1472            }
1473        } else if kind == b16(AUX_BASES) {
1474            // All 216 bases are in memory: start feeding digit jobs.
1475            n.kl_state = b2(KL_DIGITS);
1476            n.kl_feed_digit = b6(0);
1477            n.kl_feed_step = b3(0);
1478            n.kl_digits_done = b6(0);
1479        }
1480    }
1481    // Decode result.
1482    // RFC 8032 decoding: x^2 = u / v with u = y^2 - 1, v = d y^2 + 1. The
1483    // program gives a candidate x with v x^2 = +-u; the choice is made here
1484    // so the program has no branches.
1485    // Steps 4-6 negate x, x sqrt(-1) and u one limb per cycle; step 7
1486    // compares and step 8 selects.
1487    let neg_src = [r.dec_vals[0], r.dec_vals[1], r.dec_vals[3]];
1488    if r.dec_owner != b2(DEC_NONE) && (r.dec_count == b4(4) || r.dec_count == b4(5) || r.dec_count == b4(6)) {
1489        let step = (r.dec_count - b4(4)).resize::<2>();
1490        for j in 0..3 {
1491            let (o, c) = p_minus_step(limbs_from_digits(neg_src[j].d), r.dec_neg[j], r.dec_neg_c[j], step);
1492            n.dec_neg[j] = o;
1493            n.dec_neg_c[j] = c;
1494        }
1495        n.dec_count = r.dec_count + b4(1);
1496    }
1497    if r.dec_count == b4(7) && r.dec_owner != b2(DEC_NONE) {
1498        // v x^2 = u: the candidate x is a root.
1499        n.dec_plain = fe_equal(r.dec_vals[2], r.dec_vals[3]);
1500        // v x^2 = -u: x * sqrt(-1) is a root.
1501        n.dec_flipped = fe_equal(r.dec_vals[2], neg_from(r.dec_vals[3], r.dec_neg[2]));
1502        n.dec_count = b4(8);
1503    }
1504    if r.dec_count == b4(8) && r.dec_owner != b2(DEC_NONE) {
1505        let x = r.dec_vals[0];
1506        let xi = r.dec_vals[1];
1507        let plain = r.dec_plain;
1508        let flipped = r.dec_flipped;
1509        let root = if plain { x } else { xi };
1510        let neg_root = if plain { neg_from(x, r.dec_neg[0]) } else { neg_from(xi, r.dec_neg[1]) };
1511        // Choose the root whose parity matches the encoding's sign bit: the
1512        // chosen root is `root` or `neg_root`, and -A's x the other one.
1513        let odd = (root.d[0] & b17(1)) != b17(0);
1514        let swap = odd != r.dec_sign;
1515        // Valid: some root exists, and not the encoding x = 0 with the sign
1516        // bit set (it has no matching root, since -0 = 0 is even).
1517        let valid = (plain || flipped) && !(fe_is_zero(root) && r.dec_sign);
1518        if r.dec_owner == b2(DEC_KEY) {
1519            n.dec_owner = b2(DEC_NONE);
1520            n.dec_count = b4(0);
1521            if valid {
1522                // -A = (-x, y): x of -A goes to dec_vals[0] for the bases job.
1523                n.dec_vals[0] = if swap { root } else { neg_root };
1524                n.kl_state = b2(KL_BASES);
1525                n.kl_issued = false;
1526                n.kl_feed_step = b3(0);
1527            } else {
1528                // The slot keeps the encoding so its requests get
1529                // BAD_PUBLIC_KEY instead of UNKNOWN_KEY.
1530                n.key_state[r.kl_slot] = b2(KEY_INVALID);
1531                n.kl_state = b2(KL_IDLE);
1532            }
1533        } else if !fin_taken {
1534            // R decode: report the slow-path result in strict priority
1535            // order. If the result register is taken this cycle, the
1536            // collector keeps the result and tries again next cycle.
1537            let m = r.dec_meta;
1538            let error = if !valid {
1539                b8(VERIFY_BAD_R)
1540            } else if m.a_small {
1541                b8(VERIFY_SMALL_PUBLIC_KEY)
1542            } else if m.r_small {
1543                b8(VERIFY_SMALL_R)
1544            } else if !m.canonical_s {
1545                b8(VERIFY_NONCANONICAL_S)
1546            } else {
1547                b8(VERIFY_EQUATION_FAILURE)
1548            };
1549            n.fin = FinStage { valid: true, free_ctx: true, ctx: r.dec_ctx, tag: m.tag, error: error };
1550            fin_taken = true;
1551            n.dec_owner = b2(DEC_NONE);
1552            n.dec_count = b4(0);
1553        }
1554    }
1555    d.aux = aux_in;
1556
1557    // ================= Result =================
1558    // The register is shown on the ports for one cycle and then cleared,
1559    // unless a producer wrote a new result this cycle. A reported request
1560    // returns its context to the free list.
1561    let mut push_free = false;
1562    let mut push_ctx = b11(0);
1563    if r.fin.valid {
1564        if r.fin.free_ctx {
1565            push_free = true;
1566            push_ctx = r.fin.ctx;
1567        }
1568        if !fin_taken {
1569            n.fin.valid = false;
1570        }
1571    }
1572    if push_free {
1573        d.mems.free_list.write = RamWrite::<b11, 11> { addr: r.free_wr, value: push_ctx, enable: true };
1574        n.free_wr = r.free_wr + b11(1);
1575        free_count = free_count + b12(1);
1576    }
1577    n.free_count = free_count;
1578    d.regs = n;
1579    let out = VerifyOut {
1580        ready: ready,
1581        valid: r.fin.valid,
1582        ok: r.fin.error == b8(VERIFY_OK),
1583        error: r.fin.error,
1584        tag: r.fin.tag,
1585        in_flight: in_flight,
1586        load_key_ready: load_key_ready,
1587        key_state: r.key_state,
1588        internal_error: r.error,
1589        sha_ext: sha_ext,
1590    };
1591    (out, d)
1592}
1593
1594
1595/// The verify machine a new request goes to: the one with the most free
1596/// contexts, the lowest index on a tie (R-11). Fixed priority filled
1597/// machine 0's 32 contexts before any other, so with the uBFT service's 16
1598/// tags outstanding a second machine never received work.
1599#[kernel]
1600pub fn pick_machine<const NV: usize>(vm_free: [b32; NV], has_free: [bool; NV]) -> (bool, b8) {
1601    let mut any = false;
1602    let mut pick = b8(0);
1603    let mut best = b6(0);
1604    for k in 0..NV {
1605        let mut free = b6(0);
1606        for j in 0..32 {
1607            if ((vm_free[k] >> (j as u128)) & b32(1)) != b32(0) {
1608                free = free + b6(1);
1609            }
1610        }
1611        if has_free[k] && (!any || free > best) {
1612            any = true;
1613            pick = b8(k as u128);
1614            best = free;
1615        }
1616    }
1617    (any, pick)
1618}
1619
1620#[cfg(test)]
1621mod pick_machine_tests {
1622    use super::*;
1623
1624    /// Sixteen requests in a row with no completions: two machines share
1625    /// them 8/8, four machines 4/4/4/4 (R-11 regression).
1626    fn spread<const NV: usize>() -> [usize; NV] {
1627        let mut free = [b32(0xffff_ffff); NV];
1628        let mut counts = [0usize; NV];
1629        for _ in 0..16 {
1630            let has: [bool; NV] = std::array::from_fn(|k| free[k] != b32(0));
1631            let (any, pick) = pick_machine::<NV>(free, has);
1632            assert!(any);
1633            let k = pick.raw() as usize;
1634            counts[k] += 1;
1635            // Take its lowest free context.
1636            let f = free[k].raw() as u32;
1637            free[k] = b32((f & (f - 1)) as u128);
1638        }
1639        counts
1640    }
1641
1642    #[test]
1643    fn sixteen_tags_reach_every_machine() {
1644        assert_eq!(spread::<1>(), [16]);
1645        assert_eq!(spread::<2>(), [8, 8]);
1646        assert_eq!(spread::<4>(), [4, 4, 4, 4]);
1647    }
1648
1649    #[test]
1650    fn a_full_machine_is_skipped_and_none_free_is_reported() {
1651        let (any, pick) = pick_machine::<2>([b32(0), b32(1)], [false, true]);
1652        assert!(any && pick == b8(1));
1653        let (any, _) = pick_machine::<2>([b32(0), b32(0)], [false, false]);
1654        assert!(!any);
1655    }
1656
1657    #[test]
1658    fn pick_machine_lowers() {
1659        use rhdl::core::{CompilationMode, compile_design};
1660        compile_design::<pick_machine<4>>(CompilationMode::Synchronous).map_err(|e| format!("{e:?}")).unwrap();
1661    }
1662}