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}