Skip to main content

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}