Skip to main content

miden_precompiles/math/curve/
glv.rs

1//! secp256k1 GLV endomorphism: scalar decomposition and the constants it needs.
2//!
3//! The endomorphism `φ(x, y) = (β·x mod p, y)` acts as multiplication by `λ` on the group
4//! (`φ(P) = λ·P`), so any scalar multiplication `k·P` can be rewritten `k₁·P + k₂·φ(P)` with
5//! `k₁, k₂` roughly half the bit-width of `k`. [`glv_decompose`] performs the split natively
6//! (host side, untrusted advice); the in-circuit certificate binding `φ(P)` to `P` and the split
7//! back to the original scalar is the caller's responsibility.
8
9use ruint::Uint;
10
11use super::{CurvePoint, SECP256K1_GENERATOR_X, SECP256K1_GENERATOR_Y};
12use crate::math::{
13    k1_scalar::K1Scalar,
14    uint::{Limbs, UintDomain},
15};
16
17/// secp256k1 GLV endomorphism scalar `λ` (`λ³ ≡ 1 mod n`, `n` the curve order): `φ(P) = λ·P`.
18pub const SECP256K1_LAMBDA: Limbs = [
19    0x1b23bd72, 0xdf02967c, 0x20816678, 0x122e22ea, 0x8812645a, 0xa5261c02, 0xc05c30e0, 0x5363ad4c,
20];
21
22/// secp256k1 GLV base-field constant `β` (`β³ ≡ 1 mod p`, `p` the base field modulus):
23/// `φ(x, y) = (β·x mod p, y)`.
24pub const SECP256K1_BETA: Limbs = [
25    0x719501ee, 0xc1396c28, 0x12f58995, 0x9cf04975, 0xac3434e9, 0x6e64479e, 0x657c0710, 0x7ae96a2b,
26];
27
28/// The secp256k1 GLV endomorphism image of the generator, `φ(G) = (β·G_x mod p, G_y)` — a test
29/// vector for cross-checking an in-circuit `φ(G)` computation (e.g. `intro_endo`'s value relation)
30/// against this independent host-side derivation.
31pub fn phi_generator() -> CurvePoint {
32    CurvePoint::Affine {
33        x: UintDomain::K1Base.mul(SECP256K1_BETA, SECP256K1_GENERATOR_X),
34        y: SECP256K1_GENERATOR_Y,
35    }
36}
37
38/// A magnitude type wide enough to hold every intermediate value [`glv_decompose`]'s Babai
39/// rounding produces. The largest is a short-basis-coefficient-by-scalar product (a ~128-bit
40/// basis magnitude times a ~256-bit scalar), which peaks at 384 bits — this leaves 128 bits of
41/// headroom.
42type Wide = Uint<512, 8>;
43
44fn wide_zero() -> Wide {
45    Wide::from_limbs([0; 8])
46}
47
48fn wide_one() -> Wide {
49    let mut limbs = [0u64; 8];
50    limbs[0] = 1;
51    Wide::from_limbs(limbs)
52}
53
54fn limbs_to_wide(limbs: Limbs) -> Wide {
55    let mut u64_limbs = [0u64; 8];
56    for i in 0..4 {
57        u64_limbs[i] = (limbs[2 * i] as u64) | ((limbs[2 * i + 1] as u64) << 32);
58    }
59    Wide::from_limbs(u64_limbs)
60}
61
62/// Converts a reduced `Wide` value back to `Limbs`. Panics if the value doesn't actually fit in
63/// 256 bits — every value this module ever converts back is a GLV magnitude bounded well under
64/// the curve order, so a nonzero high limb indicates a bug in the reduction above, not a valid
65/// (if merely suboptimal) result.
66fn wide_to_limbs(v: Wide) -> Limbs {
67    let u64_limbs = v.as_limbs();
68    assert!(u64_limbs[4..].iter().all(|&l| l == 0), "GLV magnitude must fit in 256 bits");
69    core::array::from_fn(|i| {
70        let word = u64_limbs[i / 2];
71        if i % 2 == 0 { word as u32 } else { (word >> 32) as u32 }
72    })
73}
74
75/// A sign-magnitude integer over [`Wide`] — the GLV lattice arithmetic below needs signed
76/// intermediate values (the extended-Euclid Bézout coefficients), while the moduli and remainders
77/// stay unsigned.
78#[derive(Clone, Copy)]
79struct Signed {
80    neg: bool,
81    mag: Wide,
82}
83
84impl Signed {
85    fn new(neg: bool, mag: Wide) -> Self {
86        // Canonicalize the sign of zero so equality/negation stay simple.
87        if mag == wide_zero() {
88            Signed { neg: false, mag }
89        } else {
90            Signed { neg, mag }
91        }
92    }
93
94    fn negate(self) -> Self {
95        Signed::new(!self.neg, self.mag)
96    }
97
98    fn add(self, other: Self) -> Self {
99        if self.neg == other.neg {
100            Signed::new(self.neg, self.mag + other.mag)
101        } else if self.mag >= other.mag {
102            Signed::new(self.neg, self.mag - other.mag)
103        } else {
104            Signed::new(other.neg, other.mag - self.mag)
105        }
106    }
107
108    fn sub(self, other: Self) -> Self {
109        self.add(other.negate())
110    }
111
112    fn mul(self, other: Self) -> Self {
113        Signed::new(self.neg != other.neg, self.mag * other.mag)
114    }
115
116    /// `round(self / n)` as a signed integer. Ties round up in magnitude; the exact tie-breaking
117    /// rule is a performance choice, not a soundness one — the recompose relation this feeds
118    /// holds for *any* integer quotient (see [`glv_decompose`]'s doc comment).
119    fn div_round(self, n: Wide) -> Self {
120        let q = self.mag / n;
121        let r = self.mag % n;
122        let q = if r + r >= n { q + wide_one() } else { q };
123        Signed::new(self.neg, q)
124    }
125}
126
127/// The short lattice basis `(a1, b1), (a2, b2)` [`glv_decompose`] rounds against — the result of
128/// applying a half extended-Euclid shortest-lattice-vector reduction to `(n, λ)` followed by one
129/// step of comparing candidate short vectors by norm (Hankerson–Menezes–Vanstone, Algorithm
130/// 3.74), computed once here since `n` and `λ` are fixed.
131const GLV_BASIS: [(bool, Limbs); 4] = [
132    // a1
133    (false, [0x9284eb15, 0xe86c90e4, 0xa7d46bcd, 0x3086d221, 0, 0, 0, 0]),
134    // b1
135    (true, [0x0abfe4c3, 0x6f547fa9, 0x010e8828, 0xe4437ed6, 0, 0, 0, 0]),
136    // a2
137    (false, [0x9d44cfd8, 0x57c1108d, 0xa8e2f3f6, 0x14ca50f7, 0x00000001, 0, 0, 0]),
138    // b2
139    (false, [0x9284eb15, 0xe86c90e4, 0xa7d46bcd, 0x3086d221, 0, 0, 0, 0]),
140];
141
142/// Splits `k` (implicitly reduced mod `n`, the secp256k1 scalar-field order) into a signed short
143/// pair `[(neg_a, mag_a), (neg_b, mag_b)]` with `k ≡ (±mag_a) + λ·(±mag_b) (mod n)`, each
144/// magnitude bounded well under `n` — typically close to half its bit-width — by one Babai
145/// rounding step against the precomputed short lattice basis (Hankerson–Menezes–Vanstone,
146/// Algorithm 3.74).
147///
148/// The in-circuit certificate this decomposition feeds re-derives the same congruence from the
149/// returned halves and accepts it unconditionally: a less-than-optimal rounding here only costs
150/// the addition chain some extra bit-width, it can never make the certificate unsound.
151pub fn glv_decompose(k: Limbs) -> [(bool, Limbs); 2] {
152    let n = limbs_to_wide(K1Scalar::MODULUS);
153    let [(a1_neg, a1_mag), (b1_neg, b1_mag), (a2_neg, a2_mag), (b2_neg, b2_mag)] = GLV_BASIS;
154    let a1 = Signed::new(a1_neg, limbs_to_wide(a1_mag));
155    let b1 = Signed::new(b1_neg, limbs_to_wide(b1_mag));
156    let a2 = Signed::new(a2_neg, limbs_to_wide(a2_mag));
157    let b2 = Signed::new(b2_neg, limbs_to_wide(b2_mag));
158
159    let k_s = Signed::new(false, limbs_to_wide(k));
160    let c1 = b2.mul(k_s).div_round(n);
161    let c2 = b1.negate().mul(k_s).div_round(n);
162    let k1 = k_s.sub(c1.mul(a1)).sub(c2.mul(a2));
163    let k2 = c1.negate().mul(b1).sub(c2.mul(b2));
164
165    [(k1.neg, wide_to_limbs(k1.mag)), (k2.neg, wide_to_limbs(k2.mag))]
166}
167
168/// Computes `a * b mod n`, the secp256k1 scalar-field order.
169pub fn scalar_mul_mod_n(a: Limbs, b: Limbs) -> Limbs {
170    let n = limbs_to_wide(K1Scalar::MODULUS);
171    wide_to_limbs((limbs_to_wide(a) * limbs_to_wide(b)) % n)
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    fn limbs_from_u64(v: u64) -> Limbs {
179        [v as u32, (v >> 32) as u32, 0, 0, 0, 0, 0, 0]
180    }
181
182    /// Recomposes a GLV split via wide (unreduced) arithmetic and checks it lands back on `k`
183    /// modulo `n` — the property the in-circuit recompose certificate re-checks per signature.
184    fn recompose(split: [(bool, Limbs); 2]) -> Wide {
185        let n = limbs_to_wide(K1Scalar::MODULUS);
186        let lambda = limbs_to_wide(SECP256K1_LAMBDA);
187        let to_signed = |(neg, mag): (bool, Limbs)| Signed::new(neg, limbs_to_wide(mag));
188        let a = to_signed(split[0]);
189        let b = to_signed(split[1]);
190        let term = Signed::new(false, lambda).mul(b);
191        let sum = a.add(term);
192        // Reduce the signed sum mod n into [0, n).
193        let mag_mod_n = sum.mag % n;
194        if sum.neg && mag_mod_n != wide_zero() {
195            n - mag_mod_n
196        } else {
197            mag_mod_n
198        }
199    }
200
201    /// Re-derives the GLV short lattice basis from `(n, λ)` via a half extended-Euclid
202    /// shortest-lattice-vector reduction followed by a norm comparison between the two candidate
203    /// short vectors (Hankerson–Menezes–Vanstone, Algorithm 3.74).
204    #[test]
205    fn glv_basis_matches_extended_euclid_reduction() {
206        let n = limbs_to_wide(K1Scalar::MODULUS);
207        let lambda = limbs_to_wide(SECP256K1_LAMBDA);
208
209        let below_sqrt_n = |r: Wide| r * r < n;
210        let (mut r0, mut r1) = (n, lambda);
211        let (mut t0, mut t1) = (Signed::new(false, wide_zero()), Signed::new(false, wide_one()));
212        while !below_sqrt_n(r1) {
213            let q = r0 / r1;
214            let r2 = r0 - q * r1;
215            let t2 = t0.sub(Signed::new(false, q).mul(t1));
216            (r0, r1, t0, t1) = (r1, r2, t1, t2);
217        }
218
219        let (a1, b1) = (Signed::new(false, r1), t1.negate());
220        let q = r0 / r1;
221        let r2 = r0 - q * r1;
222        let t2 = t0.sub(Signed::new(false, q).mul(t1));
223        let norm = |r: Wide, t: Wide| r * r + t * t;
224        let (a2, b2) = if norm(r0, t0.mag) <= norm(r2, t2.mag) {
225            (Signed::new(false, r0), t0.negate())
226        } else {
227            (Signed::new(false, r2), t2.negate())
228        };
229
230        let derived = [a1, b1, a2, b2].map(|s| (s.neg, wide_to_limbs(s.mag)));
231        assert_eq!(
232            derived, GLV_BASIS,
233            "GLV_BASIS is stale relative to the extended-Euclid reduction of (n, lambda)"
234        );
235    }
236
237    #[test]
238    fn glv_decompose_recomposes_small_scalars() {
239        for k in [0u64, 1, 2, 12345, u64::MAX] {
240            let split = glv_decompose(limbs_from_u64(k));
241            assert_eq!(recompose(split), limbs_to_wide(limbs_from_u64(k)), "failed for k={k}");
242        }
243    }
244
245    #[test]
246    fn glv_decompose_recomposes_full_width_scalar() {
247        let k: Limbs = [
248            0x12345678, 0x9abcdef0, 0x0fedcba9, 0x87654321, 0x11223344, 0x55667788, 0x99aabbcc,
249            0x00112233,
250        ];
251        let split = glv_decompose(k);
252        assert_eq!(recompose(split), limbs_to_wide(k));
253    }
254
255    #[test]
256    fn glv_decompose_halves_are_short() {
257        // The shortest-vector reduction should keep both magnitudes comfortably under the full
258        // 256-bit scalar width -- otherwise the split buys no ladder-height win at all.
259        let k: Limbs = [
260            0x12345678, 0x9abcdef0, 0x0fedcba9, 0x87654321, 0x11223344, 0x55667788, 0x99aabbcc,
261            0x00112233,
262        ];
263        // 2^132: comfortably above the ~128-bit halves, comfortably below the full 256 bits.
264        let mut bound_limbs = [0u32; 8];
265        bound_limbs[4] = 0x10;
266        let bound = limbs_to_wide(bound_limbs);
267        for (_, mag) in glv_decompose(k) {
268            assert!(limbs_to_wide(mag) < bound, "GLV half is not short: {mag:?}");
269        }
270    }
271}