Skip to main content

nautilus_lighter/signing/curve/
ecgfp5.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! ECgFp5 elliptic curve over `Fp5`.
17//!
18//! Implements the prime-order group `G ⊂ E(Fp5)` from Pornin's design
19//! (eprint 2022/274), the curve underpinning Lighter's L2 Schnorr signer.
20//! The curve equation is `y^2 = x*(x^2 + a*x + b)` with `a = 2`, `b = 263*z`.
21//! The neutral element is the unique point of order 2 on the underlying
22//! Weierstrass curve, `N = (0, 0)`; the encoding is the canonical `w = y/x`
23//! mapping (computed as `t/u` from the fractional `(x, u) = (x, x/y)` form),
24//! and decoding rejects malformed encodings.
25//!
26//! Internal representation uses fractional `(x, u) = (X/Z, U/T)` coordinates
27//! so the addition formulas are complete (no special cases) at a cost of
28//! `10M` per add. Doubling and `n`-fold doubling have specialized formulas
29//! that share intermediate Frobenius-style products.
30//!
31//! Scalar multiplication uses a windowed signed-digit table of affine
32//! `(x, u)` points with window width `5`. The window prep batches
33//! `Fp5` inversions through Montgomery's trick, paying a single inversion for
34//! the entire table. Both [`lookup`] and [`lookup_var_time`] mirror the
35//! upstream Go behaviour: [`lookup_var_time`] short-circuits on the digit
36//! and is suited to public-input top-window selection, while [`lookup`]
37//! walks the table with a constant shape but assigns through a digit-derived
38//! branch. [`Point::scalar_mul`] composes these and is therefore documented
39//! variable-time, intended for verification-style operations where scalars
40//! are public. The Schnorr signing path (secret nonce, secret key) routes
41//! through [`Point::scalar_mul_ct`] instead, which uses the masked-select
42//! [`lookup_ct`] for both the top digit and every inner step so neither
43//! the table walk nor the final negation branches on the secret scalar.
44
45use core::ops::{Add, AddAssign, Mul, Neg};
46use std::sync::OnceLock;
47
48use super::scalar::Scalar;
49use crate::signing::field::{Fp, Fp5};
50
51/// Curve coefficient `a = 2` (lifted into `Fp5`).
52const A: Fp5 = Fp5([
53    Fp::from_u64_reduce(2),
54    Fp::ZERO,
55    Fp::ZERO,
56    Fp::ZERO,
57    Fp::ZERO,
58]);
59
60/// Base scalar for `b`: `b = 263 * z` in `Fp5`. The factor `263` was chosen by
61/// Pornin so that `(1, 1, 0, 0, 0)` lies on the curve (the canonical generator
62/// is then `w = 4`).
63const B1: u64 = 263;
64
65/// Curve coefficient `b = 263 * z` (lifted into `Fp5`).
66const B: Fp5 = Fp5([
67    Fp::ZERO,
68    Fp::from_u64_reduce(B1),
69    Fp::ZERO,
70    Fp::ZERO,
71    Fp::ZERO,
72]);
73
74/// `2 * b` precomputed for the addition formulas.
75const B_MUL2: Fp5 = Fp5([
76    Fp::ZERO,
77    Fp::from_u64_reduce(2 * B1),
78    Fp::ZERO,
79    Fp::ZERO,
80    Fp::ZERO,
81]);
82
83/// `4 * b` precomputed for the doubling formulas.
84const B_MUL4: Fp5 = Fp5([
85    Fp::ZERO,
86    Fp::from_u64_reduce(4 * B1),
87    Fp::ZERO,
88    Fp::ZERO,
89    Fp::ZERO,
90]);
91
92/// `16 * b` precomputed for the n-fold doubling tail.
93const B_MUL16: Fp5 = Fp5([
94    Fp::ZERO,
95    Fp::from_u64_reduce(16 * B1),
96    Fp::ZERO,
97    Fp::ZERO,
98    Fp::ZERO,
99]);
100
101/// `4` lifted into `Fp5`, used as a curve-formula constant in `mdouble`.
102const FOUR_FP5: Fp5 = Fp5([
103    Fp::from_u64_reduce(4),
104    Fp::ZERO,
105    Fp::ZERO,
106    Fp::ZERO,
107    Fp::ZERO,
108]);
109
110/// Window width for the precomputed multi-scalar table.
111const WINDOW: u32 = 5;
112
113/// Number of affine entries in the window table: `2^(WINDOW - 1)`.
114const WIN_SIZE: usize = 1 << (WINDOW - 1);
115
116/// Number of signed-digit slots required to span a 319-bit scalar at width
117/// [`WINDOW`].
118const NUM_DIGITS: usize = (319 + WINDOW as usize) / WINDOW as usize;
119
120/// A point on the ECgFp5 curve in fractional `(x, u) = (X/Z, U/T)` coordinates.
121///
122/// The neutral element is encoded as `(0, 1, 0, 1)`. Two values are equal in
123/// the group whenever `u1 * t2 == u2 * t1`; the [`Self::eq_point`] helper
124/// performs that cross-multiplication without an inversion. `PartialEq` is
125/// deliberately not derived because numerically distinct `(X, Z, U, T)` tuples
126/// can represent the same group element.
127#[derive(Clone, Copy, Debug)]
128pub struct Point {
129    /// `X` coordinate of the projective `(X, Z)` representation.
130    pub x: Fp5,
131    /// `Z` denominator of the `(X, Z)` representation.
132    pub z: Fp5,
133    /// `U` coordinate of the projective `(U, T)` representation.
134    pub u: Fp5,
135    /// `T` denominator of the `(U, T)` representation.
136    pub t: Fp5,
137}
138
139/// A point on the curve in affine `(x, u)` coordinates. Used as window entries
140/// in scalar multiplication; the affine form skips the `Z` and `T` denominators.
141#[derive(Clone, Copy, Debug)]
142pub struct AffinePoint {
143    /// Affine `x` coordinate.
144    pub x: Fp5,
145    /// Affine `u` coordinate (`u = x / y`).
146    pub u: Fp5,
147}
148
149impl AffinePoint {
150    /// Affine encoding of the neutral element `N = (0, 0)`.
151    pub const NEUTRAL: Self = Self {
152        x: Fp5::ZERO,
153        u: Fp5::ZERO,
154    };
155
156    /// Lift to fractional `(X, Z, U, T)` coordinates with `Z = T = 1`.
157    #[must_use]
158    pub fn to_point(self) -> Point {
159        Point {
160            x: self.x,
161            z: Fp5::ONE,
162            u: self.u,
163            t: Fp5::ONE,
164        }
165    }
166
167    /// Negate the point in place by negating the `u` coordinate.
168    pub fn set_neg(&mut self) {
169        self.u = -self.u;
170    }
171}
172
173impl Neg for AffinePoint {
174    type Output = Self;
175
176    fn neg(self) -> Self {
177        Self {
178            x: self.x,
179            u: -self.u,
180        }
181    }
182}
183
184impl Point {
185    /// Neutral element `N = (0, 0)` in projective `(X, Z, U, T) = (0, 1, 0, 1)`.
186    pub const NEUTRAL: Self = Self {
187        x: Fp5::ZERO,
188        z: Fp5::ONE,
189        u: Fp5::ZERO,
190        t: Fp5::ONE,
191    };
192
193    /// Canonical generator of the prime-order subgroup, `w = 4`.
194    pub const GENERATOR: Self = Self {
195        x: Fp5([
196            Fp::from_u64_reduce(12_883_135_586_176_881_569),
197            Fp::from_u64_reduce(4_356_519_642_755_055_268),
198            Fp::from_u64_reduce(5_248_930_565_894_896_907),
199            Fp::from_u64_reduce(2_165_973_894_480_315_022),
200            Fp::from_u64_reduce(2_448_410_071_095_648_785),
201        ]),
202        z: Fp5::ONE,
203        u: Fp5::ONE,
204        t: Fp5([
205            Fp::from_u64_reduce(4),
206            Fp::ZERO,
207            Fp::ZERO,
208            Fp::ZERO,
209            Fp::ZERO,
210        ]),
211    };
212
213    /// Group equality: returns `true` when `self` and `rhs` denote the same
214    /// curve point. Implemented as `u1 * t2 == u2 * t1` to avoid inversion.
215    #[must_use]
216    pub fn eq_point(self, rhs: Self) -> bool {
217        self.u * rhs.t == rhs.u * self.t
218    }
219
220    /// Test whether the point is the neutral element `N`. The neutral has
221    /// `u == 0` in the fractional encoding.
222    #[inline]
223    #[must_use]
224    pub fn is_neutral(self) -> bool {
225        self.u.is_zero()
226    }
227
228    /// Encode the point to its canonical 40-byte little-endian `Fp5`
229    /// representation `w = t / u`. The neutral encodes to `0`.
230    #[must_use]
231    pub fn encode(self) -> Fp5 {
232        self.t * self.u.invert()
233    }
234
235    /// Decode a curve point from its canonical `w = y/x` encoding.
236    ///
237    /// Returns `Some(point)` for any element of the prime-order group (the
238    /// zero encoding maps to [`Self::NEUTRAL`]). Returns `None` for encodings
239    /// that do not correspond to a valid group element. Decoding succeeds iff
240    /// `w == 0` or `(w^2 - a)^2 - 4*b` is a quadratic residue in `Fp5`.
241    #[must_use]
242    pub fn decode(w: Fp5) -> Option<Self> {
243        let e = w.square() - A;
244        let delta = e.square() - B_MUL4;
245        let r_opt = delta.canonical_sqrt();
246        let success = r_opt.is_some();
247        let r = r_opt.unwrap_or(Fp5::ZERO);
248
249        if !success {
250            // Per Pornin: when delta is not a square, the only valid encoding
251            // is `w = 0`, which represents the neutral. Anything else fails.
252            return if w.is_zero() {
253                Some(Self::NEUTRAL)
254            } else {
255                None
256            };
257        }
258
259        let two_inv = Fp5::from_u64s_reduce([2, 0, 0, 0, 0]).invert();
260        let x1 = (e + r) * two_inv;
261        let x2 = (e - r) * two_inv;
262        // Pick the candidate whose Legendre symbol is `-1` (non-square): that
263        // is the unique pre-image under the encoding.
264        let x = if x1.legendre() == Fp::ONE { x2 } else { x1 };
265
266        Some(Self {
267            x,
268            z: Fp5::ONE,
269            u: Fp5::ONE,
270            t: w,
271        })
272    }
273
274    /// Group addition `self ⊕ rhs` via the complete `10M` formulas of
275    /// Pornin's paper. Handles all combinations including the neutral.
276    #[must_use]
277    pub fn add_point(self, rhs: Self) -> Self {
278        let (x1, z1, u1, t1_) = (self.x, self.z, self.u, self.t);
279        let (x2, z2, u2, t2_) = (rhs.x, rhs.z, rhs.u, rhs.t);
280
281        let t1 = x1 * x2;
282        let t2 = z1 * z2;
283        let t3 = u1 * u2;
284        let t4 = t1_ * t2_;
285        let t5 = (x1 + z1) * (x2 + z2) - t1 - t2;
286        let t6 = (u1 + t1_) * (u2 + t2_) - t3 - t4;
287        let t7 = t1 + t2 * B;
288        let t8 = t4 * t7;
289        let t9 = t3 * (t5 * B_MUL2 + t7.double());
290        let t10 = (t4 + t3.double()) * (t5 + t7);
291
292        let x_new = (t10 - t8) * B;
293        let z_new = t8 - t9;
294        let u_new = t6 * (t2 * B - t1);
295        let t_new = t8 + t9;
296
297        Self {
298            x: x_new,
299            z: z_new,
300            u: u_new,
301            t: t_new,
302        }
303    }
304
305    /// Add an affine `(x, u)` point. Cost: `8M` (two fewer multiplies than the
306    /// general add since `z2 == t2 == 1`).
307    #[must_use]
308    pub fn add_affine(self, rhs: AffinePoint) -> Self {
309        let (x1, z1, u1, t1_) = (self.x, self.z, self.u, self.t);
310        let (x2, u2) = (rhs.x, rhs.u);
311
312        let t1 = x1 * x2;
313        let t2 = z1;
314        let t3 = u1 * u2;
315        let t4 = t1_;
316        let t5 = x1 + x2 * z1;
317        let t6 = u1 + u2 * t1_;
318        let t7 = t1 + t2 * B;
319        let t8 = t4 * t7;
320        let t9 = t3 * (t5 * B_MUL2 + t7.double());
321        let t10 = (t4 + t3.double()) * (t5 + t7);
322
323        Self {
324            x: (t10 - t8) * B,
325            u: t6 * (t2 * B - t1),
326            z: t8 - t9,
327            t: t8 + t9,
328        }
329    }
330
331    /// Group doubling `2 * self`. Cost: `4M + 5S`.
332    #[must_use]
333    pub fn double(self) -> Self {
334        let mut p = self;
335        p.set_double();
336        p
337    }
338
339    /// In-place doubling. Splitting `Self::double` into a setter saves a
340    /// struct copy in the inner loop of `mdouble`.
341    pub fn set_double(&mut self) {
342        let x = self.x;
343        let z = self.z;
344        let u = self.u;
345        let t = self.t;
346
347        let t1 = z * t;
348        let t2 = t1 * t;
349        let x1 = t2.square();
350        let z1 = t1 * u;
351        let t3 = u.square();
352        let w1 = t2 - t3 * (x + z).double();
353        let t4 = z1.square();
354
355        let x_new = t4 * B_MUL4;
356        let z_new = w1.square();
357        let u_new = (w1 + z1).square() - t4 - z_new;
358        let t_new = x1.double() - (t4 * FOUR_FP5 + z_new);
359
360        self.x = x_new;
361        self.z = z_new;
362        self.u = u_new;
363        self.t = t_new;
364    }
365
366    /// `n`-fold doubling, returning a new point. For `n >= 2`, uses a
367    /// share-the-doubling formulation that avoids reconstructing the
368    /// intermediate `(X, Z, U, T)` tuple between rounds.
369    #[must_use]
370    pub fn mdouble(self, n: u32) -> Self {
371        let mut p = self;
372        p.set_mdouble(n);
373        p
374    }
375
376    /// In-place `n`-fold doubling. Cost: `n * (2M + 5S) + 2M + 1S`.
377    pub fn set_mdouble(&mut self, n: u32) {
378        if n == 0 {
379            return;
380        }
381
382        if n == 1 {
383            self.set_double();
384            return;
385        }
386
387        let x0 = self.x;
388        let z0 = self.z;
389        let u0 = self.u;
390        let t0 = self.t;
391
392        let t1 = z0 * t0;
393        let t2 = t1 * t0;
394        let x1 = t2.square();
395        let z1 = t1 * u0;
396        let t3 = u0.square();
397        let w1 = t2 - (x0 + z0).double() * t3;
398        let t4 = w1.square();
399        let t5 = z1.square();
400
401        let mut x_state = t5.square() * B_MUL16;
402        let mut w_state = x1.double() - (t5 * FOUR_FP5 + t4);
403        let mut z_state = (w1 + z1).square() - t4 - t5;
404
405        for _ in 2..n {
406            mdouble_inner_round(&mut x_state, &mut w_state, &mut z_state);
407        }
408
409        let t1f = w_state.square();
410        let t2f = z_state.square();
411        let t3f = (w_state + z_state).square() - t1f - t2f;
412        let w1f = t1f - (x_state + t2f).double();
413
414        let z_out = w1f.square();
415        self.x = t3f.square() * B;
416        self.z = z_out;
417        self.u = t3f * w1f;
418        self.t = t1f.double() * (t1f - t2f.double()) - z_out;
419    }
420
421    /// Build a `WINDOW`-bit window of affine multiples
422    /// `[1*P, 2*P, ..., 2^(WINDOW-1) * P]` for the supplied base point.
423    #[must_use]
424    pub fn make_window_affine(self) -> Vec<AffinePoint> {
425        let mut tmp = Vec::with_capacity(WIN_SIZE);
426        tmp.push(self);
427
428        for i in 1..WIN_SIZE {
429            if (i & 1) == 0 {
430                let last = tmp[i - 1];
431                tmp.push(last.add_point(self));
432            } else {
433                let half = tmp[i >> 1];
434                tmp.push(half.double());
435            }
436        }
437        batch_to_affine(&tmp)
438    }
439
440    /// Variable-time scalar multiplication `s * self`.
441    ///
442    /// Builds a windowed table of `(x, u)` affine multiples and performs a
443    /// signed-digit double-and-add. This routine MUST NOT be called on secret
444    /// scalars: the top-window [`lookup_var_time`] step short-circuits on the
445    /// digit, and the per-window [`lookup`] still branches on each entry's
446    /// match (see its doc). Use [`Self::scalar_mul_ct`] for the secret-scalar
447    /// path that the Schnorr signer follows.
448    #[must_use]
449    pub fn scalar_mul(self, s: Scalar) -> Self {
450        let win = self.make_window_affine();
451        let mut digits = [0i32; NUM_DIGITS];
452        s.recode_signed(&mut digits, WINDOW);
453        scalar_mul_with_window_var_time(&win, &digits)
454    }
455
456    /// Constant-time scalar multiplication `s * self`.
457    ///
458    /// Same algorithm as [`Self::scalar_mul`] but routes every window lookup
459    /// through [`lookup_ct`], which uses [`Fp5::ct_select`] in place of the
460    /// data-dependent assignment in [`lookup`]. The window-prep, recoding,
461    /// `mdouble`, and `add_affine` steps execute as straight-line fixed-shape
462    /// sequences over the secret scalar's limbs, so the routine leaks no
463    /// timing information about `s`. The base point `self` is treated as
464    /// public; this is the form Schnorr uses (`k * G`, `sk * G`).
465    #[must_use]
466    pub fn scalar_mul_ct(self, s: Scalar) -> Self {
467        let win = self.make_window_affine();
468        let mut digits = [0i32; NUM_DIGITS];
469        s.recode_signed(&mut digits, WINDOW);
470        scalar_mul_with_window_ct(&win, &digits)
471    }
472
473    /// Constant-time scalar multiplication on the canonical generator
474    /// `s * G`, using a precomputed affine window cached for the process
475    /// lifetime.
476    ///
477    /// Functionally identical to `Point::GENERATOR.scalar_mul_ct(s)` but skips
478    /// the per-call [`Self::make_window_affine`] step. The Schnorr signer's
479    /// `r = k * G` and `pk = sk * G` derivations both route through here.
480    #[must_use]
481    pub fn mulgen_ct(s: Scalar) -> Self {
482        let win = generator_window();
483        let mut digits = [0i32; NUM_DIGITS];
484        s.recode_signed(&mut digits, WINDOW);
485        scalar_mul_with_window_ct(win, &digits)
486    }
487
488    /// Variable-time scalar multiplication on the canonical generator `s * G`.
489    ///
490    /// Companion to [`Self::mulgen_ct`] for verification's `s * G` term, where
491    /// the scalar is public. Reuses the same precomputed affine window.
492    #[must_use]
493    pub fn mulgen(s: Scalar) -> Self {
494        let win = generator_window();
495        let mut digits = [0i32; NUM_DIGITS];
496        s.recode_signed(&mut digits, WINDOW);
497        scalar_mul_with_window_var_time(win, &digits)
498    }
499}
500
501// Shared variable-time loop body. Factored out so per-base scalar_mul and
502// the precomputed-generator mulgen path can share digit recoding.
503fn scalar_mul_with_window_var_time(win: &[AffinePoint], digits: &[i32; NUM_DIGITS]) -> Point {
504    let mut p = lookup_var_time(win, digits[NUM_DIGITS - 1]).to_point();
505    for i in (0..NUM_DIGITS - 1).rev() {
506        p.set_mdouble(WINDOW);
507        let entry = lookup(win, digits[i]);
508        p = p.add_affine(entry);
509    }
510    p
511}
512
513// Constant-time companion: every lookup routes through `lookup_ct` so the
514// table walk and final negation do not branch on the secret digit.
515fn scalar_mul_with_window_ct(win: &[AffinePoint], digits: &[i32; NUM_DIGITS]) -> Point {
516    let mut p = lookup_ct(win, digits[NUM_DIGITS - 1]).to_point();
517    for i in (0..NUM_DIGITS - 1).rev() {
518        p.set_mdouble(WINDOW);
519        let entry = lookup_ct(win, digits[i]);
520        p = p.add_affine(entry);
521    }
522    p
523}
524
525// Affine multiples of the canonical generator, lazily initialised on first
526// use and shared across every `mulgen` / `mulgen_ct` call in the process.
527fn generator_window() -> &'static [AffinePoint; WIN_SIZE] {
528    static WINDOW_CACHE: OnceLock<[AffinePoint; WIN_SIZE]> = OnceLock::new();
529    WINDOW_CACHE.get_or_init(|| {
530        let v = Point::GENERATOR.make_window_affine();
531        let mut out = [AffinePoint::NEUTRAL; WIN_SIZE];
532        for (slot, entry) in out.iter_mut().zip(v.iter()) {
533            *slot = *entry;
534        }
535        out
536    })
537}
538
539/// Inner round of [`Point::set_mdouble`] for indices `2..n`. Mutates the `x`,
540/// `w`, `z` carry triple in place.
541fn mdouble_inner_round(x: &mut Fp5, w: &mut Fp5, z: &mut Fp5) {
542    let t1 = z.square();
543    let t2 = t1.square();
544    let t3 = w.square();
545    let t4 = t3.square();
546    let t5 = (*w + *z).square() - t1 - t3;
547
548    *z = t5 * ((*x + t1).double() - t3);
549    *x = t2 * t4 * B_MUL16;
550    *w = -(t4 + t2 * (B_MUL4 - FOUR_FP5));
551}
552
553/// Convert a slice of projective points to affine `(x, u)` form using
554/// Montgomery's trick: a single `Fp5` inversion services all entries.
555#[must_use]
556pub fn batch_to_affine(src: &[Point]) -> Vec<AffinePoint> {
557    let n = src.len();
558    if n == 0 {
559        return Vec::new();
560    }
561
562    if n == 1 {
563        let p = src[0];
564        let m1 = (p.z * p.t).invert();
565        return vec![AffinePoint {
566            x: p.x * p.t * m1,
567            u: p.u * p.z * m1,
568        }];
569    }
570
571    let mut res = vec![
572        AffinePoint {
573            x: Fp5::ZERO,
574            u: Fp5::ZERO,
575        };
576        n
577    ];
578    let mut m = src[0].z * src[0].t;
579
580    for i in 1..n {
581        let x_partial = m;
582        m *= src[i].z;
583        let u_partial = m;
584        m *= src[i].t;
585
586        res[i] = AffinePoint {
587            x: x_partial,
588            u: u_partial,
589        };
590    }
591
592    m = m.invert();
593
594    for i in (1..n).rev() {
595        res[i].u = src[i].u * res[i].u * m;
596        m *= src[i].t;
597        res[i].x = src[i].x * res[i].x * m;
598        m *= src[i].z;
599    }
600    res[0].u = src[0].u * src[0].z * m;
601    m *= src[0].t;
602    res[0].x = src[0].x * m;
603
604    res
605}
606
607/// Window lookup: returns the affine point matching the signed digit `k`.
608///
609/// `k > 0` selects `k * P`, `k < 0` selects `-k * P`, and `k == 0` returns
610/// the affine neutral `(0, 0)`. Mirrors the upstream Go `Lookup` byte-for-byte.
611///
612/// Despite the constant-shape walk over `win`, both the per-entry copy and
613/// the final negation branch on the digit's value, so secret digits leak
614/// through timing. A true constant-time variant requires masked-select Fp5
615/// primitives and lands in Phase D alongside the secret-scalar Schnorr path.
616#[must_use]
617pub fn lookup(win: &[AffinePoint], k: i32) -> AffinePoint {
618    let sign = (k >> 31) as u32;
619    let ka = ((k as u32) ^ sign).wrapping_sub(sign);
620    let km1 = ka.wrapping_sub(1);
621
622    let mut x = Fp5::ZERO;
623    let mut u = Fp5::ZERO;
624
625    for (i, entry) in win.iter().enumerate() {
626        let m = km1.wrapping_sub(i as u32);
627        let c1 = (m | (!m).wrapping_add(1)) >> 31;
628        let c = (u64::from(c1)).wrapping_sub(1);
629        if c != 0 {
630            x = entry.x;
631            u = entry.u;
632        }
633    }
634
635    let neg_mask = u64::from(sign) | (u64::from(sign) << 32);
636    if neg_mask != 0 {
637        u = -u;
638    }
639    AffinePoint { x, u }
640}
641
642/// Constant-time window lookup. Same semantics as [`lookup`] but every
643/// per-entry assignment and the final `u` negation route through
644/// [`Fp5::ct_select`], so neither the table walk nor the sign branch leaks
645/// the secret digit. Used by [`Point::scalar_mul_ct`].
646#[must_use]
647pub fn lookup_ct(win: &[AffinePoint], k: i32) -> AffinePoint {
648    let sign = (k >> 31) as u32;
649    let ka = ((k as u32) ^ sign).wrapping_sub(sign);
650    let km1 = ka.wrapping_sub(1);
651
652    let mut x = Fp5::ZERO;
653    let mut u = Fp5::ZERO;
654
655    for (i, entry) in win.iter().enumerate() {
656        let m = km1.wrapping_sub(i as u32);
657        let c1 = (m | (!m).wrapping_add(1)) >> 31;
658        let mask = u64::from(c1).wrapping_sub(1);
659        x = Fp5::ct_select(mask, x, entry.x);
660        u = Fp5::ct_select(mask, u, entry.u);
661    }
662
663    let neg_mask = sign as i32 as i64 as u64;
664    let neg_u = -u;
665    u = Fp5::ct_select(neg_mask, u, neg_u);
666    AffinePoint { x, u }
667}
668
669/// Variable-time window lookup. Same semantics as [`lookup`] but with explicit
670/// short-circuit branches; only suitable for public-input scalar multiplication.
671#[must_use]
672pub fn lookup_var_time(win: &[AffinePoint], k: i32) -> AffinePoint {
673    if k == 0 {
674        AffinePoint::NEUTRAL
675    } else if k > 0 {
676        win[(k - 1) as usize]
677    } else {
678        let mut res = win[(-k - 1) as usize];
679        res.set_neg();
680        res
681    }
682}
683
684impl Add for Point {
685    type Output = Self;
686
687    fn add(self, rhs: Self) -> Self {
688        self.add_point(rhs)
689    }
690}
691
692impl AddAssign for Point {
693    fn add_assign(&mut self, rhs: Self) {
694        *self = self.add_point(rhs);
695    }
696}
697
698impl Mul<Scalar> for Point {
699    type Output = Self;
700
701    fn mul(self, rhs: Scalar) -> Self {
702        self.scalar_mul(rhs)
703    }
704}
705
706impl Mul<Point> for Scalar {
707    type Output = Point;
708
709    fn mul(self, rhs: Point) -> Point {
710        rhs.scalar_mul(self)
711    }
712}
713
714#[cfg(test)]
715mod tests {
716    use proptest::prelude::*;
717    use rstest::rstest;
718    use serde::Deserialize;
719
720    use super::*;
721    use crate::signing::fixtures::{
722        arb_point, arb_scalar, bytes_to_hex, decode_fp5_bytes, hex_to_bytes,
723    };
724
725    const VECTORS_JSON: &str = include_str!(concat!(
726        env!("CARGO_MANIFEST_DIR"),
727        "/test_data/signing_curve_ecgfp5_vectors.json",
728    ));
729
730    #[derive(Debug, Deserialize)]
731    struct VectorsFile {
732        vectors: Vectors,
733    }
734
735    #[derive(Debug, Deserialize)]
736    struct Vectors {
737        decode: Vec<DecodeVector>,
738        add: Vec<AddVector>,
739        scalar_mul: Vec<ScalarMulVector>,
740        scalar_ops: Vec<ScalarOpVector>,
741    }
742
743    #[derive(Debug, Deserialize)]
744    struct DecodeVector {
745        w: String,
746        decodes: bool,
747        is_neutral: bool,
748        encoded_back: String,
749    }
750
751    #[derive(Debug, Deserialize)]
752    struct AddVector {
753        a_w: String,
754        b_w: String,
755        sum_w: String,
756        a_double_w: String,
757    }
758
759    #[derive(Debug, Deserialize)]
760    struct ScalarMulVector {
761        base_w: String,
762        scalar_le: String,
763        out_w: String,
764    }
765
766    #[derive(Debug, Deserialize)]
767    struct ScalarOpVector {
768        a: String,
769        b: String,
770        add: String,
771        sub: String,
772        mul: String,
773        neg_a: String,
774        a_is_zero: bool,
775    }
776
777    fn decode_scalar(hex: &str) -> Scalar {
778        let bytes = hex_to_bytes(hex);
779        let mut buf = [0u8; super::super::scalar::SCALAR_BYTES];
780        buf.copy_from_slice(&bytes);
781        Scalar::from_le_bytes_reduce(buf)
782    }
783
784    fn fp5(c: [u64; 5]) -> Fp5 {
785        Fp5::from_u64s_reduce(c)
786    }
787
788    #[rstest]
789    fn neutral_decodes_from_zero() {
790        let p = Point::decode(Fp5::ZERO).expect("zero decodes to neutral");
791        assert!(p.is_neutral());
792    }
793
794    #[rstest]
795    fn generator_is_canonical() {
796        let g = Point::GENERATOR;
797        assert_eq!(g.encode(), fp5([4, 0, 0, 0, 0]));
798    }
799
800    #[rstest]
801    fn neutral_encodes_to_zero() {
802        // The encoding is t/u, which is 1/0 = 0 by the InverseOrZero convention.
803        assert_eq!(Point::NEUTRAL.encode(), Fp5::ZERO);
804    }
805
806    #[rstest]
807    fn add_with_neutral_is_identity() {
808        let g = Point::GENERATOR;
809        let g_plus_n = g.add_point(Point::NEUTRAL);
810        assert!(g_plus_n.eq_point(g));
811
812        let n_plus_g = Point::NEUTRAL.add_point(g);
813        assert!(n_plus_g.eq_point(g));
814    }
815
816    #[rstest]
817    fn double_matches_self_addition() {
818        let g = Point::GENERATOR;
819        let g2 = g.double();
820        let g_plus_g = g.add_point(g);
821        assert!(g2.eq_point(g_plus_g));
822    }
823
824    #[rstest]
825    fn mdouble_matches_iterated_double() {
826        let g = Point::GENERATOR;
827
828        for n in 0..10u32 {
829            let mut iter = g;
830            for _ in 0..n {
831                iter = iter.double();
832            }
833            let bulk = g.mdouble(n);
834            assert!(
835                iter.eq_point(bulk),
836                "mdouble({n}) diverged from iterated double"
837            );
838        }
839    }
840
841    #[rstest]
842    fn add_affine_matches_general_add() {
843        let g = Point::GENERATOR;
844        let g2 = g.double();
845        let g2_affine = AffinePoint {
846            x: g2.x * g2.z.invert(),
847            u: g2.u * g2.t.invert(),
848        };
849        let sum_affine = g.add_affine(g2_affine);
850        let sum_point = g.add_point(g2);
851        assert!(sum_affine.eq_point(sum_point));
852    }
853
854    #[rstest]
855    fn scalar_one_is_identity_under_mul() {
856        let g = Point::GENERATOR;
857        let g1 = g * Scalar::ONE;
858        assert!(g1.eq_point(g));
859    }
860
861    #[rstest]
862    fn scalar_mul_ct_matches_scalar_mul() {
863        let g = Point::GENERATOR;
864        // Top-window stress: `ORDER - 1` exercises the topmost signed digit
865        // and forces non-trivial carries through the recoding window.
866        let mut order_minus_one = super::super::scalar::ORDER.to_limbs();
867        order_minus_one[0] -= 1;
868
869        let scalars = [
870            Scalar::ZERO,
871            Scalar::ONE,
872            Scalar::from_limbs([2, 0, 0, 0, 0]),
873            Scalar::from_limbs([0xDEAD_BEEF, 0, 0, 0, 0]),
874            Scalar::from_limbs([
875                0x0123_4567_89AB_CDEF,
876                0xFEDC_BA98_7654_3210,
877                0x1111_2222_3333_4444,
878                0x5555_6666_7777_8888,
879                0x0000_0001_0000_0001,
880            ]),
881            Scalar::from_limbs(order_minus_one),
882        ];
883
884        for (i, s) in scalars.iter().enumerate() {
885            let var = g.scalar_mul(*s);
886            let ct = g.scalar_mul_ct(*s);
887            assert!(var.eq_point(ct), "scalar {i}: var-time vs CT diverged");
888            assert_eq!(
889                var.encode().to_le_bytes(),
890                ct.encode().to_le_bytes(),
891                "scalar {i}: encoded outputs diverged",
892            );
893        }
894    }
895
896    #[rstest]
897    #[case(0)]
898    #[case(1)]
899    #[case(-1)]
900    #[case(2)]
901    #[case(-2)]
902    #[case(7)]
903    #[case(-7)]
904    #[case(8)]
905    #[case(-8)]
906    #[case(15)]
907    #[case(-15)]
908    #[case(16)]
909    #[case(-16)]
910    fn lookup_ct_matches_lookup(#[case] k: i32) {
911        let win = Point::GENERATOR.make_window_affine();
912        let var = lookup(&win, k);
913        let ct = lookup_ct(&win, k);
914        assert_eq!(var.x, ct.x, "k={k}: x coordinate diverged");
915        assert_eq!(var.u, ct.u, "k={k}: u coordinate diverged");
916    }
917
918    #[rstest]
919    fn batch_to_affine_handles_empty() {
920        let out = batch_to_affine(&[]);
921        assert!(out.is_empty(), "empty input must produce empty output");
922    }
923
924    #[rstest]
925    fn batch_to_affine_handles_single() {
926        let g2 = Point::GENERATOR.double();
927        let out = batch_to_affine(&[g2]);
928        assert_eq!(out.len(), 1);
929        // Compare against the naive (x * z.invert(), u * t.invert()).
930        let expected = AffinePoint {
931            x: g2.x * g2.z.invert(),
932            u: g2.u * g2.t.invert(),
933        };
934        assert_eq!(out[0].x, expected.x, "single-batch x diverged");
935        assert_eq!(out[0].u, expected.u, "single-batch u diverged");
936    }
937
938    /// `ORDER * G == NEUTRAL`: the group order kills every point. Computed
939    /// as `(ORDER - 1) * G + G` to avoid passing a non-canonical scalar
940    /// through `scalar_mul`.
941    #[rstest]
942    fn order_times_generator_is_neutral() {
943        let mut order_minus_one_limbs = super::super::scalar::ORDER.to_limbs();
944        order_minus_one_limbs[0] -= 1;
945        let order_minus_one = Scalar::from_limbs(order_minus_one_limbs);
946
947        let neg_g = Point::GENERATOR * order_minus_one;
948        let total = neg_g.add_point(Point::GENERATOR);
949        assert!(total.is_neutral(), "(ORDER - 1) * G + G must equal NEUTRAL",);
950    }
951
952    /// `(ORDER - 1) * G == -G` (additive inverse).
953    #[rstest]
954    fn order_minus_one_times_generator_is_neg_generator() {
955        let mut order_minus_one_limbs = super::super::scalar::ORDER.to_limbs();
956        order_minus_one_limbs[0] -= 1;
957        let order_minus_one = Scalar::from_limbs(order_minus_one_limbs);
958
959        let neg_g = Point::GENERATOR * order_minus_one;
960        // Adding `-G` to `G` lands on the neutral.
961        let identity = neg_g.add_point(Point::GENERATOR);
962        assert!(identity.is_neutral());
963    }
964
965    proptest! {
966        /// `scalar_mul_ct` and `scalar_mul` MUST produce the same group
967        /// element on every canonical scalar and every base point. Encoded
968        /// outputs MUST also be byte-identical.
969        #[rstest]
970        fn prop_scalar_mul_ct_matches_scalar_mul(
971            base in arb_point(),
972            s in arb_scalar(),
973        ) {
974            let var = base.scalar_mul(s);
975            let ct = base.scalar_mul_ct(s);
976            prop_assert!(var.eq_point(ct));
977            prop_assert_eq!(var.encode().to_le_bytes(), ct.encode().to_le_bytes());
978        }
979
980        /// `lookup` and `lookup_ct` agree on every signed digit in the
981        /// window range `[-WIN_SIZE, WIN_SIZE]` (`WIN_SIZE = 16`).
982        #[rstest]
983        fn prop_lookup_ct_matches_lookup(k in -16i32..=16) {
984            let win = Point::GENERATOR.make_window_affine();
985            let var = lookup(&win, k);
986            let ct = lookup_ct(&win, k);
987            prop_assert_eq!(var.x, ct.x);
988            prop_assert_eq!(var.u, ct.u);
989        }
990
991        /// `Point::mulgen_ct(s)` matches `Point::GENERATOR.scalar_mul_ct(s)`
992        /// byte-for-byte on every canonical scalar. Pins the precomputed
993        /// generator window against the per-call window-prep path.
994        #[rstest]
995        fn prop_mulgen_ct_matches_scalar_mul_ct(s in arb_scalar()) {
996            let baseline = Point::GENERATOR.scalar_mul_ct(s);
997            let cached = Point::mulgen_ct(s);
998            prop_assert!(baseline.eq_point(cached));
999            prop_assert_eq!(
1000                baseline.encode().to_le_bytes(),
1001                cached.encode().to_le_bytes(),
1002            );
1003        }
1004
1005        /// `Point::mulgen(s)` matches `Point::GENERATOR.scalar_mul(s)`
1006        /// byte-for-byte on every canonical scalar.
1007        #[rstest]
1008        fn prop_mulgen_matches_scalar_mul(s in arb_scalar()) {
1009            let baseline = Point::GENERATOR.scalar_mul(s);
1010            let cached = Point::mulgen(s);
1011            prop_assert!(baseline.eq_point(cached));
1012            prop_assert_eq!(
1013                baseline.encode().to_le_bytes(),
1014                cached.encode().to_le_bytes(),
1015            );
1016        }
1017    }
1018
1019    proptest! {
1020        // Group-law and round-trip properties. These run scalar_mul once or
1021        // twice per case, so we keep cases at the proptest default (256).
1022
1023        /// Group addition is commutative: `a + b == b + a`.
1024        #[rstest]
1025        fn prop_add_commutative(a in arb_point(), b in arb_point()) {
1026            prop_assert!(a.add_point(b).eq_point(b.add_point(a)));
1027        }
1028
1029        /// Adding the neutral element is the identity.
1030        #[rstest]
1031        fn prop_neutral_is_identity(p in arb_point()) {
1032            prop_assert!(p.add_point(Point::NEUTRAL).eq_point(p));
1033            prop_assert!(Point::NEUTRAL.add_point(p).eq_point(p));
1034        }
1035
1036        /// `p + p == p.double()`.
1037        #[rstest]
1038        fn prop_double_via_add(p in arb_point()) {
1039            prop_assert!(p.add_point(p).eq_point(p.double()));
1040        }
1041
1042        /// `add_affine` matches `add_point` after lifting the second operand
1043        /// to affine via inversion.
1044        #[rstest]
1045        fn prop_add_affine_matches_general_add(p in arb_point(), q in arb_point()) {
1046            // Skip cases where q is the neutral: `t == 1, u == 0` makes the
1047            // affine lift undefined (division by zero through `u.invert()`).
1048            prop_assume!(!q.is_neutral());
1049            let q_affine = AffinePoint {
1050                x: q.x * q.z.invert(),
1051                u: q.u * q.t.invert(),
1052            };
1053            prop_assert!(p.add_affine(q_affine).eq_point(p.add_point(q)));
1054        }
1055
1056        /// `Point::decode(p.encode()) == p` for every group point.
1057        #[rstest]
1058        fn prop_encode_decode_round_trip(p in arb_point()) {
1059            let w = p.encode();
1060            let p2 = Point::decode(w).expect("encoded group point must decode");
1061            prop_assert!(p2.eq_point(p));
1062        }
1063
1064        /// `Scalar::ONE * p == p`.
1065        #[rstest]
1066        fn prop_scalar_one_is_identity(p in arb_point()) {
1067            prop_assert!((p * Scalar::ONE).eq_point(p));
1068        }
1069
1070        /// `Scalar::ZERO * p == NEUTRAL`.
1071        #[rstest]
1072        fn prop_scalar_zero_kills_point(p in arb_point()) {
1073            prop_assert!((p * Scalar::ZERO).is_neutral());
1074        }
1075    }
1076
1077    proptest! {
1078        // Heavier algebraic identities; each case runs 2-3 scalar muls.
1079        // 64 cases is enough to catch systematic regressions while keeping
1080        // total runtime bounded.
1081        #![proptest_config(ProptestConfig {
1082            cases: 64,
1083            ..ProptestConfig::default()
1084        })]
1085
1086        /// Group addition is associative.
1087        #[rstest]
1088        fn prop_add_associative(a in arb_point(), b in arb_point(), c in arb_point()) {
1089            let lhs = a.add_point(b).add_point(c);
1090            let rhs = a.add_point(b.add_point(c));
1091            prop_assert!(lhs.eq_point(rhs));
1092        }
1093
1094        /// Scalar multiplication distributes over scalar addition:
1095        /// `(a + b) * P == a * P + b * P`.
1096        #[rstest]
1097        fn prop_scalar_mul_distributive(
1098            a in arb_scalar(),
1099            b in arb_scalar(),
1100            p in arb_point(),
1101        ) {
1102            let lhs = p * (a + b);
1103            let rhs = (p * a).add_point(p * b);
1104            prop_assert!(lhs.eq_point(rhs));
1105        }
1106
1107        /// Scalar multiplication is associative:
1108        /// `a * (b * P) == (a * b) * P`.
1109        #[rstest]
1110        fn prop_scalar_mul_associative(
1111            a in arb_scalar(),
1112            b in arb_scalar(),
1113            p in arb_point(),
1114        ) {
1115            let lhs = (p * b) * a;
1116            let rhs = p * (a * b);
1117            prop_assert!(lhs.eq_point(rhs));
1118        }
1119
1120        /// `mdouble(n)` matches `n` iterated `double` calls for small `n`.
1121        #[rstest]
1122        fn prop_mdouble_via_double(p in arb_point(), n in 0u32..6) {
1123            let mut iter = p;
1124            for _ in 0..n {
1125                iter = iter.double();
1126            }
1127            prop_assert!(p.mdouble(n).eq_point(iter));
1128        }
1129
1130        /// `batch_to_affine` produces the same `(x, u)` as the naive
1131        /// per-point `(x * z.invert(), u * t.invert())` for any small batch.
1132        #[rstest]
1133        fn prop_batch_to_affine_matches_naive(
1134            seeds in proptest::collection::vec(arb_scalar(), 1..8),
1135        ) {
1136            let pts: Vec<Point> = seeds
1137                .iter()
1138                .map(|s| Point::GENERATOR.scalar_mul(*s))
1139                .filter(|p| !p.is_neutral())
1140                .collect();
1141            prop_assume!(!pts.is_empty());
1142
1143            let batched = batch_to_affine(&pts);
1144            prop_assert_eq!(batched.len(), pts.len());
1145
1146            for (i, p) in pts.iter().enumerate() {
1147                let expected = AffinePoint {
1148                    x: p.x * p.z.invert(),
1149                    u: p.u * p.t.invert(),
1150                };
1151                prop_assert_eq!(batched[i].x, expected.x, "x at {} diverged", i);
1152                prop_assert_eq!(batched[i].u, expected.u, "u at {} diverged", i);
1153            }
1154        }
1155    }
1156
1157    #[rstest]
1158    fn scalar_two_matches_double() {
1159        let g = Point::GENERATOR;
1160        let g2 = g * Scalar::from_limbs([2, 0, 0, 0, 0]);
1161        assert!(g2.eq_point(g.double()));
1162    }
1163
1164    #[rstest]
1165    fn add_is_commutative() {
1166        let g = Point::GENERATOR;
1167        let g3 = g.double().add_point(g);
1168        let g3_alt = g.add_point(g.double());
1169        assert!(g3.eq_point(g3_alt));
1170    }
1171
1172    #[rstest]
1173    fn decode_matches_go_reference_vectors() {
1174        let suite: VectorsFile = serde_json::from_str(VECTORS_JSON).expect("parse vectors");
1175        assert!(!suite.vectors.decode.is_empty(), "decode vectors empty");
1176
1177        for (i, v) in suite.vectors.decode.iter().enumerate() {
1178            let w = decode_fp5_bytes(&v.w);
1179            let decoded = Point::decode(w);
1180
1181            assert_eq!(decoded.is_some(), v.decodes, "vector {i}: decode success");
1182
1183            if let Some(p) = decoded {
1184                assert_eq!(p.is_neutral(), v.is_neutral, "vector {i}: is_neutral");
1185
1186                let encoded = p.encode();
1187                assert_eq!(
1188                    bytes_to_hex(&encoded.to_le_bytes()),
1189                    v.encoded_back,
1190                    "vector {i}: re-encode round trip",
1191                );
1192            }
1193        }
1194    }
1195
1196    #[rstest]
1197    fn add_and_double_match_go_reference_vectors() {
1198        let suite: VectorsFile = serde_json::from_str(VECTORS_JSON).expect("parse vectors");
1199        assert!(!suite.vectors.add.is_empty(), "add vectors empty");
1200
1201        for (i, v) in suite.vectors.add.iter().enumerate() {
1202            let a_w = decode_fp5_bytes(&v.a_w);
1203            let b_w = decode_fp5_bytes(&v.b_w);
1204            let a = Point::decode(a_w).unwrap_or_else(|| panic!("vector {i}: decode a"));
1205            let b = Point::decode(b_w).unwrap_or_else(|| panic!("vector {i}: decode b"));
1206
1207            let sum = a.add_point(b);
1208            assert_eq!(
1209                bytes_to_hex(&sum.encode().to_le_bytes()),
1210                v.sum_w,
1211                "vector {i}: a + b",
1212            );
1213
1214            let double_a = a.double();
1215            assert_eq!(
1216                bytes_to_hex(&double_a.encode().to_le_bytes()),
1217                v.a_double_w,
1218                "vector {i}: 2 * a",
1219            );
1220        }
1221    }
1222
1223    #[rstest]
1224    fn scalar_mul_matches_go_reference_vectors() {
1225        let suite: VectorsFile = serde_json::from_str(VECTORS_JSON).expect("parse vectors");
1226        assert!(
1227            !suite.vectors.scalar_mul.is_empty(),
1228            "scalar_mul vectors empty"
1229        );
1230
1231        for (i, v) in suite.vectors.scalar_mul.iter().enumerate() {
1232            let base_w = decode_fp5_bytes(&v.base_w);
1233            let base = Point::decode(base_w).unwrap_or_else(|| panic!("vector {i}: decode base"));
1234            let s = decode_scalar(&v.scalar_le);
1235
1236            let out = base * s;
1237            assert_eq!(
1238                bytes_to_hex(&out.encode().to_le_bytes()),
1239                v.out_w,
1240                "vector {i}: s * base",
1241            );
1242        }
1243    }
1244
1245    #[rstest]
1246    fn scalar_ops_match_go_reference_vectors() {
1247        let suite: VectorsFile = serde_json::from_str(VECTORS_JSON).expect("parse vectors");
1248        assert!(
1249            !suite.vectors.scalar_ops.is_empty(),
1250            "scalar_ops vectors empty"
1251        );
1252
1253        for (i, v) in suite.vectors.scalar_ops.iter().enumerate() {
1254            let a = decode_scalar(&v.a);
1255            let b = decode_scalar(&v.b);
1256
1257            assert_eq!(
1258                bytes_to_hex(&(a + b).to_le_bytes()),
1259                v.add,
1260                "vector {i}: a + b",
1261            );
1262            assert_eq!(
1263                bytes_to_hex(&(a - b).to_le_bytes()),
1264                v.sub,
1265                "vector {i}: a - b",
1266            );
1267            assert_eq!(
1268                bytes_to_hex(&(a * b).to_le_bytes()),
1269                v.mul,
1270                "vector {i}: a * b",
1271            );
1272            assert_eq!(bytes_to_hex(&(-a).to_le_bytes()), v.neg_a, "vector {i}: -a",);
1273            assert_eq!(a.is_zero(), v.a_is_zero, "vector {i}: is_zero");
1274        }
1275    }
1276}