rhdl_ed25519_scalable_core/fe17.rs
1//! Radix-`2^17` field arithmetic units for the field machines.
2//!
3//! A [`Fe17`] holds fifteen 17-bit digits, least significant first, and is
4//! always **canonical** (strictly below `p = 2^255 - 19`) at unit boundaries.
5//! Fifteen digits of 17 bits are exactly 255 bits, so `2^255 = 19 (mod p)`
6//! folds column `k + 15` onto column `k` with a factor of 19.
7//!
8//! [`FieldMulPar`] computes the fifteen folded columns
9//! `c_k = sum_i a_i * b'_{k-i}` with `b'_j = b_j` or `19 b_j` (wrapped), as
10//! fifteen 15-deep multiply-accumulate chains. Every product is
11//! `17 x 22` bits, which fits the DSP48E2's `18 x 27` signed multiplier, and
12//! every chain stage has the shape `P = PCIN + A * B`, which maps to a DSP
13//! cascade without fabric adders. Operands are skewed by delay registers so
14//! chain stage `i` sees its operands exactly `i` cycles after stage 0.
15//! Three lazy carry passes, one 256-bit carry-propagate, and a two-candidate
16//! final reduction return a canonical result after [`MUL_LATENCY`] cycles.
17//!
18//! [`FieldAddSub`] adds or subtracts two canonical elements in
19//! [`ADD_LATENCY`] cycles. Subtraction adds `p - b` instead of negating, so
20//! both operations share one adder and one final reduction.
21//!
22//! Both units accept a new request every clock and have a fixed latency with
23//! no stalls. Each request carries an opaque 16-bit tag that comes back with
24//! its result, which is how the field machine routes write-backs.
25
26use rhdl::prelude::*;
27use rhdl_fpga::core::dff::DFF;
28
29use super::host::Fp;
30
31/// Canonical field element in fifteen 17-bit digits (255 bits).
32///
33/// Value is `sum_k d[k] * 2^(17 k)`, always below `p` where a unit produces
34/// or consumes it. Radix `2^17` matches the 17-bit unsigned operand that a
35/// DSP48E2's 18-bit signed port accepts.
36#[derive(Copy, Clone, Debug, PartialEq, Digital)]
37pub struct Fe17 {
38 /// Digits, least significant first.
39 pub d: [b17; 15],
40}
41
42impl Fe17 {
43 /// The field element zero (all digits zero).
44 pub fn zero() -> Self {
45 Fe17 { d: [b17(0); 15] }
46 }
47 /// Convert from the host model [`Fp`] by splitting it into 17-bit digits.
48 pub fn from_fp(value: Fp) -> Self {
49 let digits = value.digits17();
50 Fe17 {
51 d: digits.map(|x| b17(u128::from(x))),
52 }
53 }
54 /// Convert back to the host model [`Fp`].
55 pub fn to_fp(self) -> Fp {
56 Fp::from_digits17(&self.d.map(|x| x.raw() as u32))
57 }
58}
59
60/// Multiplier latency in clock edges: a request presented to
61/// [`FieldMulPar`] before clock edge `t` (captured at `t`) is visible on the
62/// output after edge `t + 22`, i.e. it occupies 23 register stages. The
63/// field machine relies on this being fixed, because it routes results only
64/// by tag and never waits for them.
65pub const MUL_LATENCY: usize = 23;
66/// Fixed add/subtract latency, counted the same way as [`MUL_LATENCY`]: five
67/// register stages, so a request captured at edge `t` is on the output after
68/// edge `t + 4`. The field machine routes results by tag and its barrel
69/// (32 contexts) hides any latency up to the multiplier's, so this is free.
70pub const ADD_LATENCY: usize = 5;
71
72/// Multiplier request, sampled every clock (no handshake: the unit is
73/// initiation-interval one).
74#[derive(Copy, Clone, Debug, PartialEq, Digital)]
75pub struct FieldMulIn {
76 /// Request valid. Invalid requests still flow through the datapath, but
77 /// their result is not flagged valid.
78 pub valid: bool,
79 /// First factor (canonical).
80 pub a: Fe17,
81 /// Second factor (canonical).
82 pub b: Fe17,
83 /// Opaque tag returned with the product. The field machine packs the
84 /// context and destination register into it.
85 pub tag: b16,
86}
87
88/// Multiplier result, [`MUL_LATENCY`] cycles after the request.
89#[derive(Copy, Clone, Debug, PartialEq, Digital)]
90pub struct FieldMulOut {
91 /// Result valid (the delayed request `valid`).
92 pub valid: bool,
93 /// Canonical product `a * b mod p`.
94 pub r: Fe17,
95 /// Tag captured with the request.
96 pub tag: b16,
97}
98
99/// Operand row held by one chain stage: `a[0]` is this stage's multiplier
100/// digit, `v[k]` the factor for column `k`, and `w` the 19-fold digits that
101/// rotate in at column 0 as the stages advance.
102///
103/// Stage `s` multiplies `a_s` by `v[k] = b_{k-s}` for `k >= s` and by
104/// `v[k] = 19 b_{k+15-s}` for `k < s`, which is the wrapped schoolbook
105/// product folded with `2^255 = 19 (mod p)`. Each stage shifts the row by
106/// one place, so no stage needs a multiplexer to pick its operands.
107#[derive(Copy, Clone, Debug, PartialEq, Digital)]
108pub struct MulRow {
109 /// Remaining multiplier digits (this stage's digit first; consumed
110 /// digits shift out and the tail fills with zeros).
111 pub a: [b17; 15],
112 /// Column factors `b_{k-s}` or `19 b_{k+15-s}`, 22 bits wide because
113 /// `19 * (2^17 - 1) < 2^22`.
114 pub v: [b22; 15],
115 /// 19-fold digits still to enter, highest first: `w[0]` becomes `v[0]`
116 /// at the next stage.
117 pub w: [b22; 15],
118}
119
120/// Input and output of one [`MulStage`]: the previous stage's registered
121/// row and partial sums.
122///
123/// The two halves are skewed by one clock: the `row` belonging to a request
124/// arrives one cycle before its `acc`. A stage registers the row first and
125/// then, on the next cycle, combines that registered row with the arriving
126/// sums, so each stage's multiply operands come straight from its own
127/// registers.
128#[derive(Copy, Clone, Debug, PartialEq, Digital)]
129pub struct MulStageIo {
130 /// Operand row (one cycle ahead of `acc`).
131 pub row: MulRow,
132 /// Column partial sums `c_k` accumulated so far. 43 bits hold the full
133 /// sum of fifteen `17 x 22`-bit products.
134 pub acc: [b43; 15],
135}
136
137/// Registers of one [`MulStage`].
138#[derive(Copy, Clone, Debug, PartialEq, Digital)]
139#[doc(hidden)]
140pub struct MulStageRegs {
141 /// Shifted operand row for the request whose sums arrive next cycle.
142 row: MulRow,
143 /// Partial sums after this stage's products were added.
144 acc: [b43; 15],
145}
146
147/// One multiply-accumulate stage of all fifteen column chains.
148///
149/// Every column adds `a[0] * v[k]` to the incoming partial sum: one DSP per
150/// column with `PCIN` from the previous stage. The row advances by one digit.
151/// Output is the registered state, so each stage is one clock of latency.
152#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
153pub struct MulStage {
154 /// Row and partial-sum registers.
155 regs: DFF<MulStageRegs>,
156}
157
158/// Resets every register to zero.
159impl Default for MulStage {
160 fn default() -> Self {
161 Self {
162 regs: DFF::new(MulStageRegs {
163 row: MulRow {
164 a: [b17(0); 15],
165 v: [b22(0); 15],
166 w: [b22(0); 15],
167 },
168 acc: [b43(0); 15],
169 }),
170 }
171 }
172}
173
174/// Port types: a stage passes a [`MulStageIo`] down the chain.
175impl SynchronousIO for MulStage {
176 type I = MulStageIo;
177 type O = MulStageIo;
178 type Kernel = mul_stage_kernel;
179}
180
181/// State transition for [`MulStage`].
182///
183/// `i.acc` belongs to the same request as the registered row `q.regs.row`
184/// (see [`MulStageIo`]), so the product uses the registered row while the
185/// incoming `i.row` is only shifted and stored for the next request.
186#[kernel]
187pub fn mul_stage_kernel(_cr: ClockReset, i: MulStageIo, q: MulStageQ) -> (MulStageIo, MulStageD) {
188 let r = q.regs;
189 // One MACC per column: P = PCIN + A * B with A = this stage's digit.
190 let mut acc = [b43(0); 15];
191 for k in 0..15 {
192 acc[k] = i.acc[k] + r.row.a[0].resize::<43>() * r.row.v[k].resize::<43>();
193 }
194 // Shift the incoming row for the next stage: drop the consumed digit
195 // a[0], move every column factor up one column, and feed the next
196 // 19-fold digit into column 0. The vacated tail slots stay zero.
197 let mut a = [b17(0); 15];
198 let mut w = [b22(0); 15];
199 let mut v = [b22(0); 15];
200 for j in 0..14 {
201 a[j] = i.row.a[j + 1];
202 w[j] = i.row.w[j + 1];
203 v[j + 1] = i.row.v[j];
204 }
205 v[0] = i.row.w[0];
206 // Output the registered state (row still one cycle ahead of the sums).
207 (
208 MulStageIo {
209 row: r.row,
210 acc: r.acc,
211 },
212 MulStageD {
213 regs: MulStageRegs {
214 row: MulRow { a: a, v: v, w: w },
215 acc: acc,
216 },
217 },
218 )
219}
220
221/// Seven chained [`MulStage`]s (kept small so every kernel's port bundle
222/// lowers to Verilog constants that common front ends can scan).
223#[derive(Clone, Debug, Synchronous, SynchronousDQ, Default)]
224pub struct MulChain7 {
225 /// Stages in order; `stages[0]` takes the chain input.
226 stages: [MulStage; 7],
227}
228
229/// Port types: the chain has the same interface as one stage, with seven
230/// clocks of latency.
231impl SynchronousIO for MulChain7 {
232 type I = MulStageIo;
233 type O = MulStageIo;
234 type Kernel = mul_chain7_kernel;
235}
236
237/// Wiring for [`MulChain7`]: each stage feeds the next, and the last
238/// stage's output is the chain output.
239#[kernel]
240pub fn mul_chain7_kernel(_cr: ClockReset, i: MulStageIo, q: MulChain7Q) -> (MulStageIo, MulChain7D) {
241 // Outputs of the seven stages (already registered inside each stage).
242 let st = q.stages;
243 (
244 st[6],
245 MulChain7D {
246 stages: [i, st[0], st[1], st[2], st[3], st[4], st[5]],
247 },
248 )
249}
250
251/// Pipeline registers of [`MulNorm`], one group per stage.
252#[derive(Copy, Clone, Debug, PartialEq, Digital)]
253#[doc(hidden)]
254pub struct MulNormRegs {
255 /// Stage 1: columns after the first carry pass. Column 0 receives 19
256 /// times the large carry out of column 14, hence 32 bits.
257 pass1: [b32; 15],
258 /// Stage 2: columns after the second carry pass; the carries are now
259 /// small, so 18 bits suffice.
260 pass2: [b18; 15],
261 /// Stage 3: columns after the third pass, each at most one bit above 17.
262 pass3: [b18; 15],
263 /// Stages 4 and 5: `low + over` and `low + over + 19` (see the kernel)
264 /// after limb 0 and after limb 1.
265 w4: NormWave,
266 w5: NormWave,
267 /// Stage 6: canonical product digits.
268 result: [b17; 15],
269}
270
271/// The limb-serial final add of [`MulNorm`]: operands, the two sums' low
272/// limbs so far and their carries.
273#[derive(Copy, Clone, Debug, PartialEq, Digital, Default)]
274#[doc(hidden)]
275pub struct NormWave {
276 /// `low` as limbs.
277 pub low: [b85; 3],
278 /// `over` as limbs (`over + 19` differs only in limb 0).
279 pub over: [b85; 3],
280 /// `low + over`, low limbs so far.
281 pub s: [b85; 3],
282 /// `low + over + 19`, low limbs so far.
283 pub s19: [b85; 3],
284 /// Carry into the next limb of `s`.
285 pub c_s: bool,
286 /// Carry into the next limb of `s19`.
287 pub c_s19: bool,
288}
289
290/// Carry normalization and canonical reduction of the fifteen column sums.
291///
292/// Six register stages: three lazy carry passes, then the carry-propagate
293/// add and the final reduction together, one 85-bit limb per stage
294/// (stages 4-6). Each pass is fifteen independent small adds; the final
295/// add forms `V = low + over` and `V + 19` side by side, and the canonical
296/// value is the low 255 bits of `V + 19` when either sum reaches `2^255`,
297/// else `V`. So no stage holds more than one 85-bit carry chain. (Until
298/// 2026-10-17 stages 4 and 5 were whole 255-bit chains, 33 CARRY8 levels:
299/// the fixed-base machine's worst routed path once the adder was split.)
300#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
301pub struct MulNorm {
302 /// Stage registers.
303 regs: DFF<MulNormRegs>,
304}
305
306/// Resets every register to zero.
307impl Default for MulNorm {
308 fn default() -> Self {
309 Self {
310 regs: DFF::new(MulNormRegs {
311 pass1: [b32(0); 15],
312 pass2: [b18(0); 15],
313 pass3: [b18(0); 15],
314 w4: NormWave::default(),
315 w5: NormWave::default(),
316 result: [b17(0); 15],
317 }),
318 }
319 }
320}
321
322/// Port types: fifteen 43-bit column sums in, fifteen canonical digits out.
323impl SynchronousIO for MulNorm {
324 type I = [b43; 15];
325 type O = [b17; 15];
326 type Kernel = mul_norm_kernel;
327}
328
329/// State transition for [`MulNorm`]: three lazy carry passes (column 14
330/// wraps with a factor of 19), then a limb-serial carry-propagate and
331/// canonical result.
332#[kernel]
333pub fn mul_norm_kernel(_cr: ClockReset, c: [b43; 15], q: MulNormQ) -> ([b17; 15], MulNormD) {
334 let r = q.regs;
335 let mut n = r;
336 // Pass 1 (from the chain output): keep each column's low 17 bits and add
337 // the carry from the column below. Column 0 takes column 14's carry
338 // times 19, because that carry has weight 2^255 = 19 (mod p).
339 // (h << 4) + (h << 1) + h is 19 h without a multiplier.
340 for k in 0..15 {
341 let carry = if k == 0 {
342 let h = (c[14] >> 17).resize::<32>();
343 (h << 4) + (h << 1) + h
344 } else {
345 (c[k - 1] >> 17).resize::<32>()
346 };
347 n.pass1[k] = (c[k] & b43(0x1ffff)).resize::<32>() + carry;
348 }
349 // Pass 2 (from the registered pass 1): the same step on smaller values.
350 for k in 0..15 {
351 let carry = if k == 0 {
352 let h = (r.pass1[14] >> 17).resize::<18>();
353 (h << 4) + (h << 1) + h
354 } else {
355 (r.pass1[k - 1] >> 17).resize::<18>()
356 };
357 n.pass2[k] = (r.pass1[k] & b32(0x1ffff)).resize::<18>() + carry;
358 }
359 // Pass 3 (from the registered pass 2): leaves every column at most one
360 // bit above 17.
361 for k in 0..15 {
362 let carry = if k == 0 {
363 let h = (r.pass2[14] >> 17).resize::<18>();
364 (h << 4) + (h << 1) + h
365 } else {
366 (r.pass2[k - 1] >> 17).resize::<18>()
367 };
368 n.pass3[k] = (r.pass2[k] & b18(0x1ffff)).resize::<18>() + carry;
369 }
370 // Stage 4 (from the registered pass 3): split each column into its low
371 // 17 bits (`low`) and its overflow bit, moved up one column (`over`;
372 // column 14's overflow folds to column 0 times 19). `V = low + over`
373 // equals the value mod p, and `V < 2^255 + 2^239` (`over` is small).
374 let mut low = [b17(0); 15];
375 let mut over = [b17(0); 15];
376 for k in 0..15 {
377 low[k] = (r.pass3[k] & b18(0x1ffff)).resize::<17>();
378 let h = (r.pass3[k] >> 17).resize::<17>();
379 if k == 14 {
380 over[0] = over[0] + (h << 4) + (h << 1) + h;
381 } else {
382 over[k + 1] = h;
383 }
384 }
385 // Limb 0 of V and of V + 19 (over[0] <= 19, so + 19 stays in limb 0).
386 let mut w = r.w4;
387 w.low = limbs_from_digits(low);
388 w.over = limbs_from_digits(over);
389 let (s0, c_s) = add_limb(w.low[0], w.over[0], false);
390 let (t0, c_s19) = add_limb(w.low[0], w.over[0] + b85(19), false);
391 w.s[0] = s0;
392 w.s19[0] = t0;
393 w.c_s = c_s;
394 w.c_s19 = c_s19;
395 n.w4 = w;
396 // Stage 5: limb 1 of both sums.
397 let mut w = r.w4;
398 let (s1, c_s) = add_limb(w.low[1], w.over[1], w.c_s);
399 let (t1, c_s19) = add_limb(w.low[1], w.over[1], w.c_s19);
400 w.s[1] = s1;
401 w.s19[1] = t1;
402 w.c_s = c_s;
403 w.c_s19 = c_s19;
404 n.w5 = w;
405 // Stage 6: limb 2 and the choice. If V >= 2^255, V mod p = low255(V) +
406 // 19 = low255(V + 19) (canonical: low255(V) < 2^239); if V < 2^255
407 // and V + 19 >= 2^255, V >= p and V - p = low255(V + 19); else V.
408 let w = r.w5;
409 let (s2, top) = add_limb(w.low[2], w.over[2], w.c_s);
410 let (t2, top19) = add_limb(w.low[2], w.over[2], w.c_s19);
411 n.result = if top || top19 {
412 digits_from_limbs([w.s19[0], w.s19[1], t2])
413 } else {
414 digits_from_limbs([w.s[0], w.s[1], s2])
415 };
416 (r.result, MulNormD { regs: n })
417}
418
419/// Registers of [`FieldMulPar`] outside the chain and normalizer.
420#[derive(Copy, Clone, Debug, PartialEq, Digital)]
421#[doc(hidden)]
422pub struct MulRegs {
423 /// Valid shift register; `valid[s]` is the request captured `s` edges ago.
424 valid: [bool; 23],
425 /// Tag shift register, aligned with `valid`.
426 tag: [b16; 23],
427 /// Captured first factor (edge 1).
428 a0: [b17; 15],
429 /// Captured second factor (edge 1).
430 b0: [b17; 15],
431 /// Stage-0 operand row built from `a0`/`b0` (edge 2).
432 row0: MulRow,
433 /// Stage-0 products `a_0 * b_k` (edge 3), the start of every column chain.
434 acc0: [b43; 15],
435}
436
437/// Reset value of [`MulRegs`]: everything zero and no valid requests.
438fn mul_reset() -> MulRegs {
439 MulRegs {
440 valid: [false; 23],
441 tag: [b16(0); 23],
442 a0: [b17(0); 15],
443 b0: [b17(0); 15],
444 row0: MulRow {
445 a: [b17(0); 15],
446 v: [b22(0); 15],
447 w: [b22(0); 15],
448 },
449 acc0: [b43(0); 15],
450 }
451}
452
453/// Initiation-interval-one, 225-DSP canonical field multiplier.
454///
455/// Edge 1 captures the operands, edge 2 forms stage 0's row (with the
456/// 19-fold digits), edges 3..17 run the fifteen chain stages (stage 0 here,
457/// stages 1..14 in two [`MulChain7`] children), and [`MulNorm`] finishes at 23.
458#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
459#[rhdl(dq_no_prefix)]
460pub struct FieldMulPar {
461 /// Operand capture, stage 0, and the valid/tag delay line.
462 regs: DFF<MulRegs>,
463 /// Chain stages 1..7 (`chains[0]`) and 8..14 (`chains[1]`).
464 chains: [MulChain7; 2],
465 /// Carry normalization and final reduction (six stages).
466 norm: MulNorm,
467}
468
469/// Resets every register to zero.
470impl Default for FieldMulPar {
471 fn default() -> Self {
472 Self {
473 regs: DFF::new(mul_reset()),
474 chains: [MulChain7::default(), MulChain7::default()],
475 norm: MulNorm::default(),
476 }
477 }
478}
479
480/// Port types: [`FieldMulIn`] in, [`FieldMulOut`] out.
481impl SynchronousIO for FieldMulPar {
482 type I = FieldMulIn;
483 type O = FieldMulOut;
484 type Kernel = field_mul_par_kernel;
485}
486
487/// `x * 19` for a 17-bit digit, as `16x + 2x + x` (shifts and adds, no DSP).
488/// The result is below `2^22`.
489#[kernel]
490pub fn times19_digit(x: b17) -> b22 {
491 let w = x.resize::<22>();
492 (w << 4) + (w << 1) + w
493}
494
495/// Three 85-bit limbs (five digits each) of a 255-bit integer.
496///
497/// Pure rewiring: limb `m` is digits `5m..5m+4` concatenated. Wide adds use
498/// limbs so each carry chain is three 85-bit pieces rather than 15 digits.
499#[kernel]
500pub fn limbs_from_digits(d: [b17; 15]) -> [b85; 3] {
501 // Digit 5m + j sits at bit 17 j of limb m.
502 let mut out = [b85(0); 3];
503 for m in 0..3 {
504 for j in 0..5 {
505 out[m] = out[m] | (d[5 * m + j].resize::<85>() << ((17 * j) as u128));
506 }
507 }
508 out
509}
510
511/// Inverse of [`limbs_from_digits`] (also pure rewiring).
512#[kernel]
513pub fn digits_from_limbs(l: [b85; 3]) -> [b17; 15] {
514 let mut out = [b17(0); 15];
515 for m in 0..3 {
516 for j in 0..5 {
517 out[5 * m + j] = (l[m] >> ((17 * j) as u128)).resize::<17>();
518 }
519 }
520 out
521}
522
523/// 255-bit addition of limb vectors; returns the sum modulo `2^255` and bit 255.
524///
525/// Combinational. Inputs are unsigned 255-bit integers, so the sum is below
526/// `2^256` and bit 255 is the only carry out.
527#[kernel]
528pub fn add255(a: [b85; 3], b: [b85; 3]) -> ([b85; 3], bool) {
529 let mut out = [b85(0); 3];
530 // Carry between limbs, 0 or 1 (86 bits only to match the sum width).
531 let mut carry = b86(0);
532 for m in 0..3 {
533 let s = a[m].resize::<86>() + b[m].resize::<86>() + carry;
534 out[m] = s.resize::<85>();
535 carry = s >> 85;
536 }
537 (out, carry != b86(0))
538}
539
540/// Canonical value of `lo + hi * 2^255` for `lo + 19 * hi < 2p`.
541///
542/// `w1 = lo + 19 hi` and `w2 = w1 + 19` are formed in parallel; when `w2`
543/// reaches `2^255`, `w1 >= p` and the answer is `w2 - 2^255 = w1 - p`.
544///
545/// Combinational. `hi` is the overflow above bit 255 (3 bits wide, callers
546/// pass 0 or 1). Computing both candidates at once avoids a compare before
547/// the subtraction.
548#[kernel]
549pub fn canonical255(lo: [b85; 3], hi: b3) -> [b17; 15] {
550 // 19 * hi: the value of hi * 2^255 mod p.
551 let h = hi.resize::<85>();
552 let fold = (h << 4) + (h << 1) + h;
553 let w1 = add255(lo, [fold, b85(0), b85(0)]);
554 let w2 = add255(lo, [fold + b85(19), b85(0), b85(0)]);
555 let (w1_low, w1_top) = w1;
556 let (w2_low, w2_top) = w2;
557 // w1_top means w1 >= 2^255 > p; w2_top alone means p <= w1 < 2^255.
558 // Either way w2 mod 2^255 = w1 - p, which is canonical given the bound.
559 if w1_top || w2_top {
560 digits_from_limbs(w2_low)
561 } else {
562 digits_from_limbs(w1_low)
563 }
564}
565
566/// State transition for [`FieldMulPar`].
567///
568/// Only stage 0 and the operand capture live here; stages 1..14 are the two
569/// [`MulChain7`] children and the reduction is [`MulNorm`].
570#[kernel]
571pub fn field_mul_par_kernel(_cr: ClockReset, i: FieldMulIn, q: Q) -> (FieldMulOut, D) {
572 let r = q.regs;
573 let mut n = r;
574 // Valid and tag ride a 22-entry shift register alongside the data, so
575 // the output flags line up with the product without any counters.
576 n.valid[0] = i.valid;
577 n.tag[0] = i.tag;
578 for s in 1..23 {
579 n.valid[s] = r.valid[s - 1];
580 n.tag[s] = r.tag[s - 1];
581 }
582 // Edge 1: capture the operands.
583 n.a0 = i.a.d;
584 n.b0 = i.b.d;
585 // Edge 2: stage-0 row: v[k] = b_k, and the 19-fold digits enter highest first.
586 let mut v = [b22(0); 15];
587 let mut w = [b22(0); 15];
588 for k in 0..15 {
589 v[k] = r.b0[k].resize::<22>();
590 w[k] = times19_digit(r.b0[14 - k]);
591 }
592 n.row0 = MulRow { a: r.a0, v: v, w: w };
593 // Edge 3: stage-0 products a_0 * b_k start every column (no PCIN).
594 for k in 0..15 {
595 n.acc0[k] = r.row0.a[0].resize::<43>() * r.row0.v[k].resize::<43>();
596 }
597 // The chain receives row0 one cycle before acc0 of the same request,
598 // matching the row/sum skew described in MulStageIo: r.row0 here already
599 // belongs to the next request when r.acc0 belongs to this one.
600 let ch = q.chains;
601 let d = D {
602 regs: n,
603 chains: [
604 MulStageIo {
605 row: r.row0,
606 acc: r.acc0,
607 },
608 ch[0],
609 ],
610 // Only the final sums enter the normalizer; the row is spent.
611 norm: ch[1].acc,
612 };
613 // Edge 23: normalizer output with the delayed valid and tag.
614 let out = FieldMulOut {
615 valid: r.valid[22],
616 r: Fe17 { d: q.norm },
617 tag: r.tag[22],
618 };
619 (out, d)
620}
621
622/// Add/subtract request, sampled every clock (initiation interval one).
623#[derive(Copy, Clone, Debug, PartialEq, Digital)]
624pub struct FieldAddIn {
625 /// Request valid.
626 pub valid: bool,
627 /// Subtract (`a - b`) instead of add.
628 pub sub: bool,
629 /// First operand (canonical).
630 pub a: Fe17,
631 /// Second operand (canonical).
632 pub b: Fe17,
633 /// Opaque tag returned with the result.
634 pub tag: b16,
635}
636
637/// Add/subtract result, [`ADD_LATENCY`] cycles after the request.
638#[derive(Copy, Clone, Debug, PartialEq, Digital)]
639pub struct FieldAddOut {
640 /// Result valid (the delayed request `valid`).
641 pub valid: bool,
642 /// Canonical `a + b` or `a - b` mod `p`.
643 pub r: Fe17,
644 /// Tag captured with the request.
645 pub tag: b16,
646}
647
648/// One limb step of a 255-bit addition: `x + y + c` over 85 bits, with the
649/// carry out. Combinational; one 85-bit carry chain.
650#[kernel]
651pub fn add_limb(x: b85, y: b85, c: bool) -> (b85, bool) {
652 let s = x.resize::<86>() + y.resize::<86>() + if c { b86(1) } else { b86(0) };
653 (s.resize::<85>(), (s >> 85) != b86(0))
654}
655
656/// A limb (the `low` one or a higher one) of the constant added to `x`
657/// (`b`, or `!b` for subtraction) to form `nb`: `!b + (2^255 - 18) = p - b` mod `2^255`, or `b + 0`.
658#[kernel]
659pub fn nb_const(sub: bool, low: bool) -> b85 {
660 let mask = ((b86(1) << 85) - b86(1)).resize::<85>();
661 if sub {
662 if low { ((b86(1) << 85) - b86(18)).resize::<85>() } else { mask }
663 } else {
664 b85(0)
665 }
666}
667
668/// A limb of the constant added to `x` to form `nb + 19`:
669/// `!b + 1 = 2^255 - b = p - b + 19`, or `b + 19`.
670#[kernel]
671pub fn nb19_const(sub: bool, low: bool) -> b85 {
672 if low {
673 if sub { b85(1) } else { b85(19) }
674 } else {
675 b85(0)
676 }
677}
678
679/// One stage of the limb-serial add/subtract wavefront. Fields fill in limb
680/// by limb as the request moves down the pipeline; limbs not yet formed
681/// (and the operands once spent) are don't-care.
682#[derive(Copy, Clone, Debug, PartialEq, Digital, Default)]
683#[doc(hidden)]
684pub struct AddWave {
685 /// Subtract (`a - b`) instead of add.
686 pub sub: bool,
687 /// First operand as limbs.
688 pub a: [b85; 3],
689 /// `b`, or `!b` (255 bits) for subtraction.
690 pub x: [b85; 3],
691 /// `nb` = `b`, or `p - b` for subtraction (mod `2^255`).
692 pub nb: [b85; 3],
693 /// `nb + 19` mod `2^255`: `b + 19`, or `2^255 - b`.
694 pub n19: [b85; 3],
695 /// Carry into the next limb of `nb`.
696 pub c_nb: bool,
697 /// Carry into the next limb of `n19`.
698 pub c_19: bool,
699 /// Bit 255 of `nb + 19` (set only when subtracting `b = 0`: `2^255`).
700 pub n19_top: bool,
701 /// `a + nb`, low limbs so far.
702 pub s: [b85; 3],
703 /// `a + nb + 19`, low limbs so far.
704 pub s19: [b85; 3],
705 /// Carry into the next limb of `s`.
706 pub c_s: bool,
707 /// Carry into the next limb of `s19`.
708 pub c_s19: bool,
709}
710
711/// Pipeline registers of [`FieldAddSub`].
712#[derive(Copy, Clone, Debug, PartialEq, Digital)]
713#[doc(hidden)]
714pub struct AddRegs {
715 /// Valid shift register; `valid[s]` is the request captured `s` edges ago.
716 valid: [bool; 5],
717 /// Tag shift register, aligned with `valid`.
718 tag: [b16; 5],
719 /// Edge 1: the captured request.
720 sub: bool,
721 a0: [b85; 3],
722 b0: [b85; 3],
723 /// Edges 2-4: the wavefront after limb 0, 1 and 2 of `nb`.
724 w2: AddWave,
725 w3: AddWave,
726 w4: AddWave,
727 /// Edge 5: canonical result digits.
728 result: [b17; 15],
729}
730
731/// Five-cycle canonical modular add/subtract with no carry chain longer
732/// than one 85-bit limb per stage.
733///
734/// `a + b` and `a - b = a + (p - b)` are both `a + nb` with `nb <= p`, so
735/// the sum is below `2p` and the canonical result is `a + nb` or
736/// `a + nb - p`. The unit forms `nb` and `nb + 19` limb by limb (edges
737/// 2-4), and from them `a + nb` and `a + nb + 19` one limb behind (edges
738/// 3-5). `a + nb >= p` exactly when `a + nb + 19 >= 2^255`, and then the
739/// result is the low 255 bits of `a + nb + 19`; no compare is needed.
740/// (Before 2026-10-17 each of three stages held a whole 255-bit chain; that
741/// chain was the worst path of DSig v8's in-chip route, -0.351 ns.)
742#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
743pub struct FieldAddSub {
744 /// Pipeline registers.
745 regs: DFF<AddRegs>,
746}
747
748/// Resets every register to zero.
749impl Default for FieldAddSub {
750 fn default() -> Self {
751 Self {
752 regs: DFF::new(AddRegs {
753 valid: [false; 5],
754 tag: [b16(0); 5],
755 sub: false,
756 a0: [b85(0); 3],
757 b0: [b85(0); 3],
758 w2: AddWave::default(),
759 w3: AddWave::default(),
760 w4: AddWave::default(),
761 result: [b17(0); 15],
762 }),
763 }
764 }
765}
766
767/// Port types: [`FieldAddIn`] in, [`FieldAddOut`] out.
768impl SynchronousIO for FieldAddSub {
769 type I = FieldAddIn;
770 type O = FieldAddOut;
771 type Kernel = field_add_sub_kernel;
772}
773
774/// `p - b` for canonical `b`, computed as `!b - 18` over 255 bits.
775///
776/// `!b = 2^255 - 1 - b`, so `!b - 18 = 2^255 - 19 - b = p - b`. Combinational.
777/// For `b = 0` the result is `p` itself, which is not canonical; callers
778/// that need a canonical negation must special-case zero (see the machine's
779/// `field_neg`).
780#[kernel]
781pub fn p_minus(b: [b85; 3]) -> [b85; 3] {
782 // 85 one bits; the complement is taken at 86 bits and masked back.
783 let mask = (b86(1) << 85) - b86(1);
784 let nb = [
785 (!b[0].resize::<86>() & mask).resize::<85>(),
786 (!b[1].resize::<86>() & mask).resize::<85>(),
787 (!b[2].resize::<86>() & mask).resize::<85>(),
788 ];
789 // Adding 2^255 - 18 modulo 2^255 subtracts 18.
790 let minus18 = [
791 ((b86(1) << 85) - b86(18)).resize::<85>(),
792 mask.resize::<85>(),
793 mask.resize::<85>(),
794 ];
795 let (out, _) = add255(nb, minus18);
796 out
797}
798
799/// State transition for [`FieldAddSub`].
800#[kernel]
801pub fn field_add_sub_kernel(
802 _cr: ClockReset,
803 i: FieldAddIn,
804 q: FieldAddSubQ,
805) -> (FieldAddOut, FieldAddSubD) {
806 let r = q.regs;
807 let mut n = r;
808 // Valid and tag delay line, aligned with the data stages.
809 n.valid[0] = i.valid;
810 n.tag[0] = i.tag;
811 for s in 1..5 {
812 n.valid[s] = r.valid[s - 1];
813 n.tag[s] = r.tag[s - 1];
814 }
815 // Edge 1: capture (digits to limbs is only rewiring).
816 n.sub = i.sub;
817 n.a0 = limbs_from_digits(i.a.d);
818 n.b0 = limbs_from_digits(i.b.d);
819 // Edge 2: x = b or !b; limb 0 of nb and nb + 19.
820 // Every field of the wave is written before it is read downstream.
821 let mut w = r.w2;
822 w.sub = r.sub;
823 w.a = r.a0;
824 for m in 0..3 {
825 w.x[m] = if r.sub { !r.b0[m] } else { r.b0[m] };
826 }
827 let (nb0, c_nb) = add_limb(w.x[0], nb_const(r.sub, true), false);
828 let (n190, c_19) = add_limb(w.x[0], nb19_const(r.sub, true), false);
829 w.nb[0] = nb0;
830 w.n19[0] = n190;
831 w.c_nb = c_nb;
832 w.c_19 = c_19;
833 n.w2 = w;
834 // Edge 3: limb 1 of nb and nb + 19; limb 0 of the two sums.
835 let mut w = r.w2;
836 let (nb1, c_nb) = add_limb(w.x[1], nb_const(w.sub, false), w.c_nb);
837 let (n191, c_19) = add_limb(w.x[1], nb19_const(w.sub, false), w.c_19);
838 let (s0, c_s) = add_limb(w.a[0], w.nb[0], false);
839 let (t0, c_s19) = add_limb(w.a[0], w.n19[0], false);
840 w.nb[1] = nb1;
841 w.n19[1] = n191;
842 w.c_nb = c_nb;
843 w.c_19 = c_19;
844 w.s[0] = s0;
845 w.s19[0] = t0;
846 w.c_s = c_s;
847 w.c_s19 = c_s19;
848 n.w3 = w;
849 // Edge 4: limb 2 of nb (its carry out is dropped: mod 2^255) and of
850 // nb + 19 (carry out = bit 255, set only for b = 0 in a subtraction);
851 // limb 1 of the sums.
852 let mut w = r.w3;
853 let (nb2, _) = add_limb(w.x[2], nb_const(w.sub, false), w.c_nb);
854 let (n192, top) = add_limb(w.x[2], nb19_const(w.sub, false), w.c_19);
855 let (s1, c_s) = add_limb(w.a[1], w.nb[1], w.c_s);
856 let (t1, c_s19) = add_limb(w.a[1], w.n19[1], w.c_s19);
857 w.nb[2] = nb2;
858 w.n19[2] = n192;
859 w.n19_top = top;
860 w.s[1] = s1;
861 w.s19[1] = t1;
862 w.c_s = c_s;
863 w.c_s19 = c_s19;
864 n.w4 = w;
865 // Edge 5: limb 2 of the sums and the choice. a + nb + 19 < 2^256, so
866 // its bit 255 is the limb-2 carry or nb + 19's own bit 255 (never
867 // both); when set, a + nb >= p and the low 255 bits are a + nb - p.
868 let w = r.w4;
869 let (s2, _) = add_limb(w.a[2], w.nb[2], w.c_s);
870 let (t2, c_top) = add_limb(w.a[2], w.n19[2], w.c_s19);
871 n.result = if c_top || w.n19_top {
872 digits_from_limbs([w.s19[0], w.s19[1], t2])
873 } else {
874 digits_from_limbs([w.s[0], w.s[1], s2])
875 };
876 let out = FieldAddOut {
877 valid: r.valid[4],
878 r: Fe17 { d: r.result },
879 tag: r.tag[4],
880 };
881 (out, FieldAddSubD { regs: n })
882}
883
884/// Cycle-level tests of both units against the host model.
885#[cfg(test)]
886mod tests {
887 use super::*;
888 use crate::simulation::CycleSim;
889
890 /// `n` test elements: edge cases (0, 1, -1, -19, and the raw-limb
891 /// constant `2^255 - 20 = p - 1`) followed by xorshift-generated values.
892 fn sample(n: usize) -> Vec<Fp> {
893 // xorshift64 state; fixed seed so failures reproduce.
894 let mut s = 0x9e37_79b9_7f4a_7c15u64;
895 let mut next = || {
896 s ^= s << 13;
897 s ^= s >> 7;
898 s ^= s << 17;
899 s
900 };
901 let mut v = vec![
902 Fp::ZERO,
903 Fp::ONE,
904 Fp::ZERO.sub(Fp::ONE),
905 Fp::ZERO.sub(Fp([19, 0, 0, 0])),
906 Fp([u64::MAX, u64::MAX, u64::MAX, 0x7fff_ffff_ffff_ffec]),
907 ];
908 while v.len() < n {
909 let bytes: [u8; 32] = std::array::from_fn(|_| next() as u8);
910 v.push(Fp::from_bytes(&bytes));
911 }
912 v
913 }
914
915 /// Feeds one request per clock to both units (subtracting on odd
916 /// cycles) and checks every result, matched by tag, against [`Fp`].
917 /// Back-to-back issue checks that the pipelines keep requests separate.
918 #[test]
919 fn mul_and_addsub_match_host_back_to_back() {
920 let xs = sample(40);
921 let pairs: Vec<(Fp, Fp)> = xs
922 .iter()
923 .flat_map(|a| xs.iter().take(8).map(move |b| (*a, *b)))
924 .collect();
925 // 40 extra idle cycles drain both pipelines.
926 let mut mul = CycleSim::new(FieldMulPar::default());
927 let mut add = CycleSim::new(FieldAddSub::default());
928 let mut got_m = Vec::new();
929 let mut got_a = Vec::new();
930 for cycle in 0..pairs.len() + 40 {
931 let (valid, a, b) = match pairs.get(cycle) {
932 Some((a, b)) => (true, Fe17::from_fp(*a), Fe17::from_fp(*b)),
933 None => (false, Fe17::zero(), Fe17::zero()),
934 };
935 let tag = b16((cycle & 0xffff) as u128);
936 let o = mul.tick(FieldMulIn { valid, a, b, tag });
937 if o.valid {
938 got_m.push((o.tag.raw() as usize, o.r.to_fp()));
939 }
940 let o = add.tick(FieldAddIn {
941 valid,
942 sub: cycle % 2 == 1,
943 a,
944 b,
945 tag,
946 });
947 if o.valid {
948 got_a.push((o.tag.raw() as usize, o.r.to_fp()));
949 }
950 }
951 assert_eq!(got_m.len(), pairs.len());
952 assert_eq!(got_a.len(), pairs.len());
953 for (idx, r) in got_m {
954 let (a, b) = pairs[idx];
955 assert_eq!(r, a.mul(b), "mul {idx}");
956 }
957 for (idx, r) in got_a {
958 let (a, b) = pairs[idx];
959 let want = if idx % 2 == 1 { a.sub(b) } else { a.add(b) };
960 assert_eq!(r, want, "addsub {idx}");
961 }
962 }
963
964 /// Both operations on every pair of edge values and a few random ones
965 /// (so `a - 0`, `0 - 0`, `(p-1) + (p-1)` and sums landing on `p`), and
966 /// the result exactly [`ADD_LATENCY`] cycles after the request.
967 #[test]
968 fn addsub_edges_and_latency() {
969 let mut xs = sample(12);
970 // a + b = p and a - b = 0 cases: p - 19 + 19, p - 1 + 1.
971 xs.push(Fp([19, 0, 0, 0]));
972 let mut reqs = Vec::new();
973 for a in &xs {
974 for b in &xs {
975 reqs.push((*a, *b, false));
976 reqs.push((*a, *b, true));
977 }
978 }
979 let mut add = CycleSim::new(FieldAddSub::default());
980 let mut got = Vec::new();
981 for cycle in 0..reqs.len() + 10 {
982 let (valid, sub, a, b) = match reqs.get(cycle) {
983 Some((a, b, sub)) => (true, *sub, Fe17::from_fp(*a), Fe17::from_fp(*b)),
984 None => (false, false, Fe17::zero(), Fe17::zero()),
985 };
986 let o = add.tick(FieldAddIn { valid, sub, a, b, tag: b16((cycle & 0xffff) as u128) });
987 if o.valid {
988 let idx = o.tag.raw() as usize;
989 // CycleSim reports the output after each edge; the request edge is idx.
990 assert_eq!(cycle - idx, ADD_LATENCY - 1, "latency of {idx}");
991 got.push((idx, o.r));
992 }
993 }
994 assert_eq!(got.len(), reqs.len());
995 for (idx, r) in got {
996 let (a, b, sub) = reqs[idx];
997 let want = if sub { a.sub(b) } else { a.add(b) };
998 assert_eq!(r, Fe17::from_fp(want), "{} {idx}: digits must be canonical", if sub { "sub" } else { "add" });
999 }
1000 }
1001
1002 /// The adder lowers to Verilog (kernel restrictions bite only here).
1003 #[test]
1004 fn addsub_lowers_to_verilog() -> Result<(), RHDLError> {
1005 let text = FieldAddSub::default().descriptor("field_add_sub".into())?.hdl_unchecked()?.modules.pretty();
1006 assert!(text.contains("module field_add_sub"));
1007 Ok(())
1008 }
1009
1010 /// The normalizer's final add and reduction on column sums placed at the
1011 /// edges: values just below, at and above `p`, just below `2^255`, and
1012 /// with a carry out of column 14 (worth 19) or every column overflowing.
1013 #[test]
1014 fn normalizer_edges() {
1015 use num_bigint::BigUint;
1016 let p = (BigUint::from(1u8) << 255u32) - BigUint::from(19u8);
1017 let digits = |v: &BigUint| -> [b43; 15] {
1018 std::array::from_fn(|k| b43(((v >> (17 * k as u32)) & BigUint::from(0x1ffffu32)).iter_u64_digits().next().unwrap_or(0) as u128))
1019 };
1020 let top = BigUint::from(1u8) << 255u32;
1021 let mut cases: Vec<([b43; 15], BigUint)> = Vec::new();
1022 for v in [
1023 BigUint::from(0u8),
1024 p.clone() - 1u8,
1025 p.clone(),
1026 p.clone() + 1u8,
1027 p.clone() + 18u8,
1028 top.clone() - 1u8,
1029 ] {
1030 cases.push((digits(&v), v % &p));
1031 }
1032 // Column 14 carries 3 (3 * 2^255 = 57 mod p) on top of p - 1.
1033 let mut c = digits(&(p.clone() - 1u8));
1034 c[14] = c[14] + (b43(3) << 17);
1035 cases.push((c, (p.clone() - 1u8 + 57u8) % &p));
1036 // Every column at its largest 43-bit value.
1037 let max = [b43((1u128 << 43) - 1); 15];
1038 let mut v = BigUint::from(0u8);
1039 for k in 0..15u32 {
1040 v += BigUint::from((1u64 << 43) - 1) << (17 * k);
1041 }
1042 cases.push((max, v % &p));
1043 let mut norm = CycleSim::new(MulNorm::default());
1044 let lat = 6;
1045 let mut outs = Vec::new();
1046 for cycle in 0..cases.len() + lat + 2 {
1047 let input = cases.get(cycle).map_or([b43(0); 15], |c| c.0);
1048 outs.push(norm.tick(input));
1049 }
1050 for (k, (_, want)) in cases.iter().enumerate() {
1051 let got = outs[k + lat - 1];
1052 let mut v = BigUint::from(0u8);
1053 for (j, d) in got.iter().enumerate() {
1054 v += BigUint::from(d.raw() as u64) << (17 * j as u32);
1055 }
1056 assert_eq!(&v, want, "case {k}");
1057 }
1058 }
1059
1060 /// Checks that a single multiply comes out exactly [`MUL_LATENCY`] cycles
1061 /// after it is presented, which the field machine depends on.
1062 #[test]
1063 fn mul_latency_is_fixed() {
1064 let mut mul = CycleSim::new(FieldMulPar::default());
1065 let one = Fe17::from_fp(Fp::ONE);
1066 let mut seen = None;
1067 for cycle in 0..40 {
1068 let o = mul.tick(FieldMulIn {
1069 valid: cycle == 0,
1070 a: one,
1071 b: one,
1072 tag: b16(7),
1073 });
1074 if o.valid {
1075 seen = Some(cycle);
1076 }
1077 }
1078 // CycleSim reports the output after each edge; the request edge is 0.
1079 assert_eq!(seen, Some(MUL_LATENCY - 1));
1080 }
1081}