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}