Skip to main content

rhdl_ed25519_scalable_core/
scalar_ring.rs

1//! Eight-context digit-serial scalar multiply-add modulo the group order `l`.
2//!
3//! A job computes `(X * Y + Z) mod l` for a 527-bit `X` (31 digits of 17
4//! bits, stored least significant first and consumed most significant
5//! first), a multiplier `Y < l`, and an addend `Z < l`. Signing uses two
6//! jobs per signature:
7//!
8//! - `r = H(prefix || M) mod l` with `Y = 1`, `Z = 0`;
9//! - `S = H(R || A || M) * a + r mod l`, which equals `k * a + r` for
10//!   `k = H(R || A || M) mod l` without a separate reduction.
11//!
12//! Each Horner step is `u = acc * 2^17 + x_d * Y`, then, with
13//! `l = 2^252 + c`, `acc' = (u mod 2^252) + l - (u >> 252) * c`. Because
14//! `2^252 = -c (mod l)`, `acc'` is congruent to `u`. The invariant
15//! `acc < 2^254` keeps every quantity bounded: `u < 2^271 + 2^17 * l < 2^272`
16//! (sixteen digits), so `t = u >> 252 < 2^20` and `t * c < 2^145 < l`, which
17//! makes `l - t * c` positive and `acc' < 2^252 + l < 2^254`. A step is an
18//! eight-stage pipeline, and eight independent contexts circulate through
19//! it, so the datapath retires one step per cycle: 15 + 8 DSP-sized products
20//! and no wide fabric multiply.
21//!
22//! # Final reduction
23//!
24//! After 31 steps the finisher forms `S = acc + Z` (below `2^254 + l`) in
25//! one clock and reduces it modulo `l` in two more. Because `l = 2^252 + c` with `c < 2^125`, the
26//! estimate `k0 = S >> 252` is the true quotient or one more, so the first
27//! clock computes `S - k0·l` (one 256-bit subtraction; `k0·l` is a small
28//! constant table, see [`k_times_l_low`]) and the second adds `l` back if
29//! that went negative. The earlier single-clock version subtracted four
30//! multiples of `l` at 340 bits and picked one, which Vivado routed at
31//! 5.13 ns, 0.27 ns over the 205 MHz period; it was the only failing path in
32//! the small core.
33//!
34//! The finisher holds one job at a time. A context that completes its last
35//! step while the finisher is busy *parks*: it keeps its slot and its final
36//! accumulator, and retries each time it comes around to stage 8.
37
38use rhdl::prelude::*;
39use rhdl_fpga::core::dff::DFF;
40
41/// One multiply-add job `(X * Y + Z) mod l`, presented on [`ScalarRingIn::job`].
42#[derive(Copy, Clone, Debug, PartialEq, Digital)]
43pub struct ScalarJob {
44    /// A job is being presented this cycle.
45    pub valid: bool,
46    /// Opaque tag, returned unchanged with the result so the caller can match
47    /// results to jobs (results can leave in a different order than jobs
48    /// entered, because contexts park while the finisher is busy).
49    pub tag: b16,
50    /// Multiplicand digits, least significant first (31 x 17 = 527 bits,
51    /// enough for a 512-bit hash). The ring consumes `x[30]` first.
52    pub x: [b17; 31],
53    /// Multiplier `Y < l`, least significant digit first.
54    pub y: [b17; 15],
55    /// Addend `Z < l`, least significant digit first.
56    pub z: [b17; 15],
57}
58
59/// Completed result, held until `out_ready`.
60#[derive(Copy, Clone, Debug, PartialEq, Digital)]
61pub struct ScalarDone {
62    /// A result is present and has not been accepted yet.
63    pub valid: bool,
64    /// Tag of the job this result belongs to.
65    pub tag: b16,
66    /// Canonical result `< l`, least significant digit first.
67    pub s: [b17; 15],
68}
69
70/// Ring inputs.
71#[derive(Copy, Clone, Debug, PartialEq, Digital)]
72pub struct ScalarRingIn {
73    /// New job; accepted when [`ScalarRingOut::ready`].
74    pub job: ScalarJob,
75    /// Consumer accepts the result presented on [`ScalarRingOut::done`].
76    pub out_ready: bool,
77}
78
79/// Ring outputs.
80#[derive(Copy, Clone, Debug, PartialEq, Digital)]
81pub struct ScalarRingOut {
82    /// A job may be presented: the pipeline slot entering stage 1 this cycle
83    /// is free. Derived from registered state only, so it does not depend on
84    /// this cycle's inputs.
85    pub ready: bool,
86    /// Result, valid until the cycle `out_ready` is high.
87    pub done: ScalarDone,
88}
89
90/// One context: the state of a job as it circulates through the eight
91/// pipeline stages.
92#[derive(Copy, Clone, Debug, PartialEq, Digital)]
93#[doc(hidden)]
94pub struct ScalarCtx {
95    /// The slot holds a job.
96    valid: bool,
97    /// All 31 steps are done but the finisher was busy; the context keeps
98    /// its slot and final `acc` until the finisher frees up.
99    parked: bool,
100    /// Horner steps still to run (31 at admission).
101    remaining: b5,
102    /// The job's tag.
103    tag: b16,
104    /// Running accumulator, `< 2^254`, least significant digit first.
105    acc: [b17; 15],
106    /// Remaining multiplicand digits; `x[30]` is the next digit, and the
107    /// array shifts up by one digit each step.
108    x: [b17; 31],
109    /// Multiplier `Y`.
110    y: [b17; 15],
111    /// Addend `Z`, used only by the finisher.
112    z: [b17; 15],
113}
114
115/// All pipeline registers. `sK` is the context entering stage `K`, and the
116/// `sK_*` fields are the intermediate values stage `K` consumes.
117#[derive(Copy, Clone, Debug, PartialEq, Digital)]
118#[doc(hidden)]
119pub struct ScalarRegs {
120    /// Context entering stage 1.
121    s1: ScalarCtx,
122    /// Context entering stage 2.
123    s2: ScalarCtx,
124    /// The fifteen 34-bit products `x_d * y_j` from stage 1.
125    s2_p: [b34; 15],
126    /// Context entering stage 3.
127    s3: ScalarCtx,
128    /// Limbs 0 and 1 of `u` (bits 0..170), complete.
129    s3_lo: [b85; 2],
130    /// Carry from limb 1 into limb 2 (0..=2).
131    s3_cin: b2,
132    /// Limbs 2 and 3 of the three digit vectors, each summed on its own
133    /// (no carry between them yet).
134    s3_hi: [b87; 2],
135    /// Context entering stage 4.
136    s4: ScalarCtx,
137    /// `t = u >> 252` (below `2^20`).
138    s4_t: b20,
139    /// `u mod 2^252`.
140    s4_low: [b85; 4],
141    /// Context entering stage 5.
142    s5: ScalarCtx,
143    /// The eight 37-bit products `t * c_j`.
144    s5_q: [b37; 8],
145    /// `u mod 2^252`, carried along.
146    s5_low: [b85; 4],
147    /// Context entering stage 6.
148    s6: ScalarCtx,
149    /// Sum of product rows 0 and 1 (170 bits).
150    s6_s: [b85; 2],
151    /// Product row 2, added in stage 6.
152    s6_row2: [b85; 2],
153    /// `u mod 2^252`, carried along.
154    s6_low: [b85; 4],
155    /// Context entering stage 7.
156    s7: ScalarCtx,
157    /// `t * c` (below `2^145`).
158    s7_tc: [b85; 2],
159    /// `u mod 2^252`, carried along.
160    s7_low: [b85; 4],
161    /// Context entering stage 8.
162    s8: ScalarCtx,
163    /// Stage 8 inputs. `acc' = low + l - tc`, with `low + l` precomputed in
164    /// stage 7: its low two limbs, and its high two limbs for a borrow of 0
165    /// (`s8_hp`) or 1 (`s8_hm`) out of `low - tc`'s low limbs.
166    s8_lo: [b85; 2],
167    s8_tc: [b85; 2],
168    s8_hp: [b85; 2],
169    s8_hm: [b85; 2],
170    /// Finisher entry holds a job (its final `acc` and its `Z`).
171    f0_valid: bool,
172    /// Tag of the job at the finisher entry.
173    f0_tag: b16,
174    /// The job's final accumulator.
175    f0_acc: [b85; 4],
176    /// The job's addend `Z`.
177    f0_z: [b85; 4],
178    /// Finisher stage 1 holds a job.
179    f1_valid: bool,
180    /// Tag of the job in finisher stage 1.
181    f1_tag: b16,
182    /// `S = acc + Z` (below `2^254 + l`).
183    f1_sum: [b85; 4],
184    /// Finisher stage 2 holds a job.
185    f2_valid: bool,
186    /// Tag of the job in finisher stage 2.
187    f2_tag: b16,
188    /// `S - k0 * l mod 2^256` as two 128-bit words (low, high).
189    f2_d: [b128; 2],
190    /// `S - k0 * l` was negative, so stage 2 must add `l` back.
191    f2_negative: bool,
192    /// Output register, held until `out_ready`.
193    done: ScalarDone,
194}
195
196/// An empty (invalid) context, used for reset and to fill free slots.
197fn empty_ctx() -> ScalarCtx {
198    ScalarCtx {
199        valid: false,
200        parked: false,
201        remaining: b5(0),
202        tag: b16(0),
203        acc: [b17(0); 15],
204        x: [b17(0); 31],
205        y: [b17(0); 15],
206        z: [b17(0); 15],
207    }
208}
209
210/// Digit-serial scalar ring: eight contexts, one Horner step per cycle, and
211/// a two-clock finisher.
212#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
213#[rhdl(dq_no_prefix)]
214pub struct ScalarRing {
215    /// Every pipeline and finisher register, in one flip-flop bundle.
216    regs: DFF<ScalarRegs>,
217}
218
219/// Reset state: every slot empty, every intermediate zero.
220impl Default for ScalarRing {
221    fn default() -> Self {
222        let e = empty_ctx();
223        Self {
224            regs: DFF::new(ScalarRegs {
225                s1: e,
226                s2: e,
227                s2_p: [b34(0); 15],
228                s3: e,
229                s3_lo: [b85(0); 2],
230                s3_cin: b2(0),
231                s3_hi: [b87(0); 2],
232                s4: e,
233                s4_t: b20(0),
234                s4_low: [b85(0); 4],
235                s5: e,
236                s5_q: [b37(0); 8],
237                s5_low: [b85(0); 4],
238                s6: e,
239                s6_s: [b85(0); 2],
240                s6_row2: [b85(0); 2],
241                s6_low: [b85(0); 4],
242                s7: e,
243                s7_tc: [b85(0); 2],
244                s7_low: [b85(0); 4],
245                s8: e,
246                s8_lo: [b85(0); 2],
247                s8_tc: [b85(0); 2],
248                s8_hp: [b85(0); 2],
249                s8_hm: [b85(0); 2],
250                f0_valid: false,
251                f0_tag: b16(0),
252                f0_acc: [b85(0); 4],
253                f0_z: [b85(0); 4],
254                f1_valid: false,
255                f1_tag: b16(0),
256                f1_sum: [b85(0); 4],
257                f2_valid: false,
258                f2_tag: b16(0),
259                f2_d: [b128(0); 2],
260                f2_negative: false,
261                done: ScalarDone {
262                    valid: false,
263                    tag: b16(0),
264                    s: [b17(0); 15],
265                },
266            }),
267        }
268    }
269}
270
271/// Binds the ring's input, output and kernel types.
272impl SynchronousIO for ScalarRing {
273    type I = ScalarRingIn;
274    type O = ScalarRingOut;
275    type Kernel = scalar_ring_kernel;
276}
277
278/// `c = l - 2^252` in eight 17-bit digits, least significant first.
279/// The top digit is 41, so `c < 2^(7 * 17 + 6) = 2^125`.
280#[kernel]
281pub fn order_c_digits() -> [b17; 8] {
282    [
283        b17(119789),
284        b17(77434),
285        b17(39110),
286        b17(51970),
287        b17(96717),
288        b17(62743),
289        b17(97255),
290        b17(41),
291    ]
292}
293
294/// `l` in fifteen 17-bit digits: the digits of `c` plus `2^252`, which is
295/// bit 14 of digit 14 (`252 = 14 * 17 + 14`).
296#[kernel]
297pub fn order_l_digits() -> [b17; 15] {
298    let c = order_c_digits();
299    let mut l = [b17(0); 15];
300    for j in 0..8 {
301        l[j] = c[j];
302    }
303    l[14] = b17(16384);
304    l
305}
306
307/// Sixteen digits into four 85-bit limbs (the last limb holds one digit).
308///
309/// Five digits fill a limb exactly, so the limbs are the same binary number
310/// in a wider radix. Wide additions run on limbs, which needs one carry per
311/// 85 bits instead of one per digit.
312#[kernel]
313pub fn pack16(d: [b17; 16]) -> [b85; 4] {
314    let mut out = [b85(0); 4];
315    for m in 0..4 {
316        for j in 0..5 {
317            if 5 * m + j < 16 {
318                out[m] = out[m] | (d[5 * m + j].resize::<85>() << ((17 * j) as u128));
319            }
320        }
321    }
322    out
323}
324
325/// Inverse of [`pack16`]: bits above 272 are dropped.
326#[kernel]
327pub fn unpack16(l: [b85; 4]) -> [b17; 16] {
328    let mut out = [b17(0); 16];
329    for m in 0..4 {
330        for j in 0..5 {
331            if 5 * m + j < 16 {
332                out[5 * m + j] = (l[m] >> ((17 * j) as u128)).resize::<17>();
333            }
334        }
335    }
336    out
337}
338
339/// Fifteen digits widened to sixteen (top digit zero), so a 255-bit value
340/// can go through [`pack16`].
341#[kernel]
342pub fn widen15(d: [b17; 15]) -> [b17; 16] {
343    let mut out = [b17(0); 16];
344    for k in 0..15 {
345        out[k] = d[k];
346    }
347    out
348}
349
350/// Four-limb addition modulo `2^340`.
351#[kernel]
352pub fn add_l4(a: [b85; 4], b: [b85; 4]) -> [b85; 4] {
353    let mut out = [b85(0); 4];
354    // Carry out of the previous limb (0 or 1), in a width that matches `s`.
355    let mut carry = b86(0);
356    for m in 0..4 {
357        // 86 bits: one limb sum plus its carry-out in bit 85.
358        let s = a[m].resize::<86>() + b[m].resize::<86>() + carry;
359        out[m] = s.resize::<85>();
360        carry = s >> 85;
361    }
362    out
363}
364
365/// Four-limb subtraction; returns `a - b mod 2^340` and whether it borrowed.
366#[kernel]
367pub fn sub_l4(a: [b85; 4], b: [b85; 4]) -> ([b85; 4], bool) {
368    let mut out = [b85(0); 4];
369    // Borrow into the current limb (0 or 1).
370    let mut borrow = b86(0);
371    for m in 0..4 {
372        // The 86-bit difference wraps when it goes negative, which sets
373        // bit 85: that bit is the borrow into the next limb.
374        let s = a[m].resize::<86>() - b[m].resize::<86>() - borrow;
375        out[m] = s.resize::<85>();
376        borrow = (s >> 85) & b86(1);
377    }
378    (out, borrow != b86(0))
379}
380
381/// Two-limb addition modulo `2^170`.
382#[kernel]
383pub fn add_l2(a: [b85; 2], b: [b85; 2]) -> [b85; 2] {
384    // Low limb with its carry in bit 85, which feeds the high limb.
385    let s0 = a[0].resize::<86>() + b[0].resize::<86>();
386    let s1 = a[1].resize::<86>() + b[1].resize::<86>() + (s0 >> 85);
387    [s0.resize::<85>(), s1.resize::<85>()]
388}
389
390/// Bits 0..256 of four 85-bit limbs as two 128-bit words.
391#[kernel]
392pub fn limbs_to_w256(l: [b85; 4]) -> [b128; 2] {
393    // Bits 0..128: all of limb 0 and the low 43 bits of limb 1.
394    let lo = l[0].resize::<128>() | (l[1].resize::<128>() << 85);
395    // Bits 128..256: the top 42 bits of limb 1, all of limb 2 (bits
396    // 170..254) and bit 0 of limb 3 (bit 255).
397    let hi = (l[1] >> 43).resize::<128>() | (l[2].resize::<128>() << 42) | (l[3].resize::<128>() << 127);
398    [lo, hi]
399}
400
401/// Low 128 bits of `k * l` for `k = 0..8` (the high 128 bits are `k << 124`).
402///
403/// `k * l = k * 2^252 + k * c`, and `k * c < 8 * 2^125 = 2^128`, so the low
404/// word is exactly `k * c` and the high word is `k * 2^252 / 2^128`. Entry 1
405/// is `c` itself, which the finisher's add-back step reuses.
406#[kernel]
407pub fn k_times_l_low(k: b3) -> b128 {
408    match k {
409        Bits::<3>(0) => b128(0),
410        Bits::<3>(1) => b128(0x14def9dea2f79cd65812631a5cf5d3ed),
411        Bits::<3>(2) => b128(0x29bdf3bd45ef39acb024c634b9eba7da),
412        Bits::<3>(3) => b128(0x3e9ced9be8e6d6830837294f16e17bc7),
413        Bits::<3>(4) => b128(0x537be77a8bde735960498c6973d74fb4),
414        Bits::<3>(5) => b128(0x685ae1592ed6102fb85bef83d0cd23a1),
415        Bits::<3>(6) => b128(0x7d39db37d1cdad06106e529e2dc2f78e),
416        _ => b128(0x9218d51674c549dc6880b5b88ab8cb7b),
417    }
418}
419
420/// Fifteen 17-bit digits of a value below `2^255` held in two 128-bit words.
421#[kernel]
422pub fn w256_to_digits15(w: [b128; 2]) -> [b17; 15] {
423    let mut out = [b17(0); 15];
424    for k in 0..15 {
425        if 17 * k + 17 <= 128 {
426            // Digit entirely inside the low word.
427            out[k] = (w[0] >> ((17 * k) as u128)).resize::<17>();
428        } else if 17 * k >= 128 {
429            // Digit entirely inside the high word.
430            out[k] = (w[1] >> ((17 * k - 128) as u128)).resize::<17>();
431        } else {
432            // Digit 7 (bits 119..136) straddles the two words: its low bits
433            // come from the top of `w[0]` and its high bits from the bottom
434            // of `w[1]`.
435            let low = (w[0] >> ((17 * k) as u128)).resize::<17>();
436            let high = (w[1] << ((128 - 17 * k) as u128)).resize::<17>();
437            out[k] = low | high;
438        }
439    }
440    out
441}
442
443/// State transition for [`ScalarRing`].
444///
445/// Each cycle every stage advances its context by one register: stage `K`
446/// reads `sK` and its intermediates and writes `s(K+1)`, and stage 8 writes
447/// back to `s1`. The finisher runs alongside on its own registers.
448#[kernel]
449pub fn scalar_ring_kernel(_cr: ClockReset, i: ScalarRingIn, q: Q) -> (ScalarRingOut, D) {
450    let r = q.regs;
451    // Next state starts as a copy of the current one; each stage overwrites
452    // the registers it owns.
453    let mut n = r;
454
455    // ---- Result handshake and finishing pipeline.
456    // The consumer took the result presented this cycle (`r.done`).
457    if i.out_ready {
458        n.done.valid = false;
459    }
460    // Reduction of S = acc + Z (< 2^254 + l < 2^255) modulo l in two
461    // clocks. With l = 2^252 + c (c < 2^125), k0 = S >> 252 is at least the
462    // true quotient and at most one above it, so S - k0*l lies in (-l, l):
463    // f1 subtracts k0*l, f2 adds l back if that went negative. Each clock
464    // has one 256-bit carry chain.
465    //
466    // Finisher stage 2: moves into `done` only when the output register is
467    // empty (checked on the registered value, so a result accepted this
468    // cycle frees the output register next cycle).
469    if r.f2_valid && !r.done.valid {
470        // Low word plus c (the low word of 1 * l) when the difference was
471        // negative; the result is reduced mod 2^256 either way.
472        let lo = r.f2_d[0] + if r.f2_negative { k_times_l_low(b3(1)) } else { b128(0) };
473        // The low-word addition overflowed exactly when the sum wrapped
474        // below its input.
475        let carry = if r.f2_negative && lo < r.f2_d[0] { b128(1) } else { b128(0) };
476        // High word plus 2^252 / 2^128 = 2^124, plus the carry. Wrapping
477        // mod 2^128 cancels the two's-complement negative value.
478        let hi = r.f2_d[1] + if r.f2_negative { b128(1) << 124 } else { b128(0) } + carry;
479        n.done = ScalarDone {
480            valid: true,
481            tag: r.f2_tag,
482            s: w256_to_digits15([lo, hi]),
483        };
484        n.f2_valid = false;
485    }
486    // Finisher entry: S = acc + Z. The finisher holds one job, so stage 1
487    // is empty whenever the entry is full. (Until 2026-10-17 stage 8 formed
488    // S itself, right after acc' = low + m: two 254-bit adds in series, the
489    // small signer's worst routed path once the field adder was split.)
490    if r.f0_valid {
491        n.f1_valid = true;
492        n.f1_tag = r.f0_tag;
493        n.f1_sum = add_l4(r.f0_acc, r.f0_z);
494        n.f0_valid = false;
495    }
496    // Finisher stage 1: advances when stage 2 is empty or is moving out this
497    // cycle.
498    if r.f1_valid && (!r.f2_valid || !r.done.valid) {
499        // S as two 128-bit words.
500        let a = limbs_to_w256(r.f1_sum);
501        // k0 = S >> 252: bits 124..127 of the high word, at most 3 bits
502        // because S < 2^255.
503        let k0 = (a[1] >> 124).resize::<3>();
504        // k0 * l split into low and high words.
505        let b_lo = k_times_l_low(k0);
506        let b_hi = k0.resize::<128>() << 124;
507        // 256-bit subtraction S - k0 * l as two 128-bit word subtractions
508        // with an explicit borrow.
509        let borrow = a[0] < b_lo;
510        let lo = a[0] - b_lo;
511        let hi = a[1] - b_hi - if borrow { b128(1) } else { b128(0) };
512        // The full 256-bit difference is negative (S < k0 * l).
513        let negative = a[1] < b_hi || (a[1] == b_hi && borrow);
514        n.f2_valid = true;
515        n.f2_tag = r.f1_tag;
516        n.f2_d = [lo, hi];
517        n.f2_negative = negative;
518        n.f1_valid = false;
519    }
520    // A context may enter the finisher only when the whole finisher,
521    // including the output register, is empty, so it holds one job at a
522    // time and a new S is never stalled inside it.
523    let finish_free = !r.f0_valid && !r.f1_valid && !r.f2_valid && !r.done.valid;
524
525    // ---- Stage 1: x_d * Y (fifteen 17 x 17 products).
526    let c1 = r.s1;
527    // The current (most significant remaining) multiplicand digit.
528    let xd = c1.x[30];
529    for j in 0..15 {
530        n.s2_p[j] = xd.resize::<34>() * c1.y[j].resize::<34>();
531    }
532    n.s2 = c1;
533
534    // ---- Stage 2: acc * 2^17 + low halves.
535    let c2 = r.s2;
536    // acc * 2^17: acc moved up one digit.
537    let mut shifted = [b17(0); 16];
538    // Low 17 bits of product j, at digit j.
539    let mut lo = [b17(0); 16];
540    // High 17 bits of product j, at digit j + 1.
541    let mut hi = [b17(0); 16];
542    for j in 0..15 {
543        shifted[j + 1] = c2.acc[j];
544        lo[j] = r.s2_p[j].resize::<17>();
545        hi[j + 1] = (r.s2_p[j] >> 17).resize::<17>();
546    }
547    // Splitting each product into halves turns the sum of fifteen
548    // overlapping products into three non-overlapping digit vectors. All
549    // three are added here for limbs 0 and 1 with the carry between them;
550    // limbs 2 and 3 are summed on their own, and stage 3 carries into them.
551    // (Until 2026-10-17 this stage added two vectors over all 340 bits and
552    // stage 3 the third: a 272-bit carry chain into the t * c DSPs, the
553    // 8,0,2,1 signer's worst routed path, -0.373 ns.)
554    let a = pack16(shifted);
555    let b = pack16(lo);
556    let h = pack16(hi);
557    let s0 = a[0].resize::<87>() + b[0].resize::<87>() + h[0].resize::<87>();
558    let s1 = a[1].resize::<87>() + b[1].resize::<87>() + h[1].resize::<87>() + (s0 >> 85);
559    n.s3_lo = [s0.resize::<85>(), s1.resize::<85>()];
560    n.s3_cin = (s1 >> 85).resize::<2>();
561    n.s3_hi = [
562        a[2].resize::<87>() + b[2].resize::<87>() + h[2].resize::<87>(),
563        a[3].resize::<87>() + b[3].resize::<87>() + h[3].resize::<87>(),
564    ];
565    n.s3 = c2;
566
567    // ---- Stage 3: u, then t = u >> 252 and u mod 2^252.
568    // u = acc * 2^17 + x_d * Y, below 2^272 (sixteen digits).
569    // Only bits 170..272 are left to carry (u < 2^272).
570    let v2 = r.s3_hi[0] + r.s3_cin.resize::<87>();
571    let v3 = r.s3_hi[1] + (v2 >> 85);
572    let u = unpack16([r.s3_lo[0], r.s3_lo[1], v2.resize::<85>(), v3.resize::<85>()]);
573    // Bit 252 is bit 14 of digit 14: t takes the top 3 bits of digit 14 and
574    // all 17 bits of digit 15 (20 bits).
575    n.s4_t = (u[14] >> 14).resize::<20>() | (u[15].resize::<20>() << 3);
576    // u mod 2^252: clear bits 252 and up.
577    let mut low = u;
578    low[14] = u[14] & b17(0x3fff);
579    low[15] = b17(0);
580    n.s4_low = pack16(low);
581    n.s4 = r.s3;
582
583    // ---- Stage 4: t * c (eight 20 x 17 products).
584    let c = order_c_digits();
585    for j in 0..8 {
586        n.s5_q[j] = r.s4_t.resize::<37>() * c[j].resize::<37>();
587    }
588    n.s5_low = r.s4_low;
589    n.s5 = r.s4;
590
591    // ---- Stage 5: three non-overlapping product rows (offsets 0, 17, 34).
592    // t * c = sum_j q_j * 2^(17 j). Row `row` collects products
593    // j = row, row + 3, row + 6. Within a row the offsets are 51 bits apart
594    // and each product is 37 bits wide, so the products do not overlap and
595    // can be OR-ed into place; only the three rows need real additions.
596    let qv = r.s5_q;
597    // Each row as a 170-bit two-limb value.
598    let mut rows = [[b85(0); 2]; 3];
599    for row in 0..3 {
600        for step in 0..3 {
601            let j = 3 * step + row;
602            if j < 8 {
603                let q85 = qv[j].resize::<85>();
604                // Bit position of product j in t * c.
605                let offset = 17 * j;
606                if offset < 85 {
607                    // Starts in limb 0; the bits above bit 85 spill into
608                    // limb 1.
609                    rows[row][0] = rows[row][0] | (q85 << (offset as u128));
610                    if offset + 37 > 85 {
611                        rows[row][1] = rows[row][1] | (q85 >> ((85 - offset) as u128));
612                    }
613                } else {
614                    // Entirely in limb 1.
615                    rows[row][1] = rows[row][1] | (q85 << ((offset - 85) as u128));
616                }
617            }
618        }
619    }
620    n.s6_s = add_l2(rows[0], rows[1]);
621    n.s6_row2 = rows[2];
622    n.s6_low = r.s5_low;
623    n.s6 = r.s5;
624
625    // ---- Stage 6: tc.
626    // The third row completes t * c.
627    n.s7_tc = add_l2(r.s6_s, r.s6_row2);
628    n.s7_low = r.s6_low;
629    n.s7 = r.s6;
630
631    // ---- Stage 7: low + l, split for stage 8 (tc < 2^145 < l).
632    // Adding l before subtracting t * c keeps acc' positive, so the ring
633    // never needs a sign or a conditional correction per step. tc spans two
634    // limbs, so stage 8 subtracts it from the low half only and picks the
635    // high half by its borrow: hp = high(low + l), hm = hp - 1 (l's limb 2
636    // holds bit 252, so l_hi - 1 does not borrow into limb 3).
637    // Why: a whole four-limb add in stage 8 (low + (l - tc)) placed as four
638    // carry chains far apart; it was the 12-machine signer's worst routed
639    // path (-0.358 ns, 2026-10-18), into stage 1 and the finisher entry.
640    let l = pack16(widen15(order_l_digits()));
641    let low = r.s7_low;
642    let p0 = low[0].resize::<86>() + l[0].resize::<86>();
643    let p1 = low[1].resize::<86>() + l[1].resize::<86>() + (p0 >> 85);
644    let c = p1 >> 85;
645    let h2 = low[2].resize::<86>() + l[2].resize::<86>() + c;
646    let h3 = low[3].resize::<86>() + l[3].resize::<86>() + (h2 >> 85);
647    let m2 = low[2].resize::<86>() + (l[2] - b85(1)).resize::<86>() + c;
648    let m3 = low[3].resize::<86>() + l[3].resize::<86>() + (m2 >> 85);
649    n.s8_lo = [p0.resize::<85>(), p1.resize::<85>()];
650    n.s8_tc = r.s7_tc;
651    n.s8_hp = [h2.resize::<85>(), h3.resize::<85>()];
652    n.s8_hm = [m2.resize::<85>(), m3.resize::<85>()];
653    n.s8 = r.s7;
654
655    // ---- Stage 8: acc' = low + m; advance, finish, park, or admit.
656    let c8 = r.s8;
657    // acc' = (u mod 2^252) + l - t * c, below 2^254.
658    let d0 = r.s8_lo[0].resize::<86>() - r.s8_tc[0].resize::<86>();
659    let d1 = r.s8_lo[1].resize::<86>() - r.s8_tc[1].resize::<86>() - ((d0 >> 85) & b86(1));
660    let hi = if (d1 >> 85) & b86(1) != b86(0) { r.s8_hm } else { r.s8_hp };
661    let acc_next = unpack16([d0.resize::<85>(), d1.resize::<85>(), hi[0], hi[1]]);
662    // The context written back to stage 1.
663    let mut next = c8;
664    // The context hands its accumulator to the finisher this cycle.
665    let mut leaving = false;
666    if c8.valid && !c8.parked {
667        // A step completed: commit acc' and shift the next digit into
668        // x[30].
669        for k in 0..15 {
670            next.acc[k] = acc_next[k];
671        }
672        for k in 1..31 {
673            next.x[k] = c8.x[k - 1];
674        }
675        next.x[0] = b17(0);
676        next.remaining = c8.remaining - b5(1);
677        if c8.remaining == b5(1) {
678            // That was the last step: finish now, or park and keep the
679            // final accumulator until the finisher is free.
680            if finish_free {
681                leaving = true;
682            } else {
683                next.parked = true;
684            }
685        }
686    } else if c8.valid && c8.parked && finish_free {
687        // A parked context retries each lap; its `acc` is already final.
688        leaving = true;
689    }
690    if leaving {
691        // The finisher entry forms S = acc + Z (below 2^254 + l) next cycle.
692        n.f0_valid = true;
693        n.f0_tag = c8.tag;
694        n.f0_acc = pack16(widen15(next.acc));
695        n.f0_z = pack16(widen15(c8.z));
696    }
697    // The slot being written to stage 1 is empty or just emptied, so a new
698    // job can take it.
699    let slot_free = !c8.valid || leaving;
700    if slot_free {
701        next.valid = false;
702        if i.job.valid {
703            // Admit: 31 steps from acc = 0.
704            next = ScalarCtx {
705                valid: true,
706                parked: false,
707                remaining: b5(31),
708                tag: i.job.tag,
709                acc: [b17(0); 15],
710                x: i.job.x,
711                y: i.job.y,
712                z: i.job.z,
713            };
714        }
715    }
716    n.s1 = next;
717    let out = ScalarRingOut {
718        ready: slot_free,
719        done: r.done,
720    };
721    (out, D { regs: n })
722}
723
724/// Host helper: split little-endian bytes into `D` 17-bit digits, least
725/// significant first. Bits past the end of `bytes` read as zero.
726pub fn digits17<const D: usize>(bytes: &[u8]) -> [b17; D] {
727    std::array::from_fn(|k| {
728        let mut v = 0u128;
729        for b in 0..17 {
730            // Absolute bit index of bit `b` of digit `k`.
731            let bit = 17 * k + b;
732            if bit / 8 < bytes.len() && (bytes[bit / 8] >> (bit % 8)) & 1 == 1 {
733                v |= 1 << b;
734            }
735        }
736        b17(v)
737    })
738}
739
740/// Host helper: digits back to 32 little-endian bytes. Fifteen digits
741/// cover bits 0..255, so the `bit < 256` guard never drops a set bit.
742pub fn bytes_from_digits(d: &[b17; 15]) -> [u8; 32] {
743    let mut out = [0u8; 32];
744    for (k, digit) in d.iter().enumerate() {
745        for b in 0..17 {
746            // Absolute bit index of bit `b` of digit `k`.
747            let bit = 17 * k + b;
748            if (digit.raw() >> b) & 1 == 1 && bit < 256 {
749                out[bit / 8] |= 1 << (bit % 8);
750            }
751        }
752    }
753    out
754}
755
756/// Cycle-level check of the ring against big-integer arithmetic.
757#[cfg(test)]
758mod tests {
759    use super::*;
760    use crate::simulation::CycleSim;
761    use num_bigint::BigUint;
762
763    /// The group order as a big integer.
764    fn l() -> BigUint {
765        BigUint::from_bytes_le(&crate::host::L_BYTES)
766    }
767
768    /// Forty jobs with random and edge-case operands (an all-ones 512-bit
769    /// `X`, `Y = l - 1`, `Z = l - 1`), with `out_ready` low every fourth
770    /// cycle so results back up and contexts park. Every result must match
771    /// `(X * Y + Z) mod l` and arrive exactly once under its tag.
772    #[test]
773    fn ring_matches_bignum_with_backpressure() {
774        // xorshift64 state; a fixed seed keeps the test deterministic.
775        let mut state = 0x243f_6a88_85a3_08d3u64;
776        let mut rand_bytes = |n: usize| -> Vec<u8> {
777            (0..n)
778                .map(|_| {
779                    state ^= state << 13;
780                    state ^= state >> 7;
781                    state ^= state << 17;
782                    state as u8
783                })
784                .collect()
785        };
786        // l - 1, the largest allowed Y and Z.
787        let lm1 = (l() - 1u32).to_bytes_le();
788        let mut jobs = Vec::new();
789        for k in 0..40 {
790            let x = if k == 0 { vec![0xff; 64] } else { rand_bytes(64) };
791            let y = if k % 5 == 1 {
792                lm1.clone()
793            } else {
794                (BigUint::from_bytes_le(&rand_bytes(32)) % l()).to_bytes_le()
795            };
796            let z = if k % 7 == 2 {
797                lm1.clone()
798            } else {
799                (BigUint::from_bytes_le(&rand_bytes(32)) % l()).to_bytes_le()
800            };
801            jobs.push((x, y, z));
802        }
803        let mut sim = CycleSim::new(ScalarRing::default());
804        let idle = ScalarJob {
805            valid: false,
806            tag: b16(0),
807            x: [b17(0); 31],
808            y: [b17(0); 15],
809            z: [b17(0); 15],
810        };
811        let mut out = sim.tick(ScalarRingIn {
812            job: idle,
813            out_ready: false,
814        });
815        // Index of the next job to present.
816        let mut next = 0;
817        // Result per tag; a second result for the same tag fails the test.
818        let mut got = vec![None; jobs.len()];
819        for cycle in 0..40_000 {
820            let mut job = idle;
821            // Present a job only when the ring reported ready on the
822            // previous output.
823            if out.ready && next < jobs.len() {
824                let (x, y, z) = &jobs[next];
825                job = ScalarJob {
826                    valid: true,
827                    tag: b16(next as u128),
828                    x: digits17::<31>(x),
829                    y: digits17::<15>(y),
830                    z: digits17::<15>(z),
831                };
832                next += 1;
833            }
834            // Backpressure: refuse results one cycle in four.
835            let out_ready = cycle % 4 != 0;
836            if out.done.valid && out_ready {
837                let tag = out.done.tag.raw() as usize;
838                assert!(got[tag].is_none());
839                got[tag] = Some(bytes_from_digits(&out.done.s));
840            }
841            out = sim.tick(ScalarRingIn { job, out_ready });
842        }
843        for (k, (x, y, z)) in jobs.iter().enumerate() {
844            let want = (BigUint::from_bytes_le(x) * BigUint::from_bytes_le(y)
845                + BigUint::from_bytes_le(z))
846                % l();
847            let mut w = [0u8; 32];
848            let b = want.to_bytes_le();
849            w[..b.len()].copy_from_slice(&b);
850            assert_eq!(got[k].expect("missing"), w, "job {k}");
851        }
852    }
853}