Skip to main content

logicaffeine_proof/
elliptic.rs

1//! # Elliptic-curve primitives over ℤ/N — the additive→multiplicative lift
2//!
3//! Pollard's `p−1` gets a single shot at a smooth group order: the multiplicative group `(ℤ/p)*` has the
4//! *fixed* order `p−1`, so if that one number isn't smooth, you lose. Lenstra's **Elliptic Curve Method**
5//! lifts the problem onto `E(𝔽_p)`, whose order — by Hasse — lies in `[p+1−2√p, p+1+2√p]` and, crucially,
6//! **changes when you change the curve.** Every curve is a fresh smoothness lottery ticket, and the cost
7//! scales with the size of the *smallest* factor, not `N` — which makes ECM the champion for prying a
8//! small-to-medium prime out of an otherwise-huge modulus.
9//!
10//! We work in **Montgomery form** `B·y² = x³ + A·x² + x` with **x-only `(X:Z)` projective coordinates**.
11//! Two payoffs: point arithmetic never needs a modular inverse (only `+`, `−`, `×` mod `N`), and the ONE
12//! place an inverse *would* be forced — when a point becomes the identity mod `p` but not mod `q`, so its
13//! `Z` is `≡ 0 (mod p)` but `≢ 0 (mod N)` — is exactly where `gcd(Z, N)` hands us the factor. The "failure"
14//! of the group law *is* the discovery.
15//!
16//! This module is the reusable substrate: [`xdbl`], [`xadd`], and [`ladder`] are ordinary elliptic-curve
17//! group operations mod `N` and underpin ECDLP / pairing / isogeny work later; [`ecm_factor`] is the
18//! flagship application.
19
20use logicaffeine_base::BigInt;
21use crate::factor::{gcd, mod_inverse, modpow};
22use std::collections::HashMap;
23
24#[inline]
25fn i(x: i64) -> BigInt {
26    BigInt::from_i64(x)
27}
28
29/// The non-negative residue `a mod n` (handles a negative `a`).
30#[inline]
31fn rem_pos(a: &BigInt, n: &BigInt) -> BigInt {
32    let r = a.div_rem(n).map(|(_, r)| r).unwrap_or_else(|| a.clone());
33    if r.is_negative() {
34        r.add(n)
35    } else {
36        r
37    }
38}
39
40#[inline]
41fn mulmod(a: &BigInt, b: &BigInt, n: &BigInt) -> BigInt {
42    rem_pos(&a.mul(b), n)
43}
44#[inline]
45fn addmod(a: &BigInt, b: &BigInt, n: &BigInt) -> BigInt {
46    rem_pos(&a.add(b), n)
47}
48#[inline]
49fn submod(a: &BigInt, b: &BigInt, n: &BigInt) -> BigInt {
50    rem_pos(&a.sub(b), n)
51}
52
53/// x-only **doubling** on a Montgomery curve mod `n`, given `a24 = (A+2)/4`. Maps `(X:Z)` to `2·(X:Z)`
54/// using only field `+ − ×` (no inversion).
55pub fn xdbl(x: &BigInt, z: &BigInt, a24: &BigInt, n: &BigInt) -> (BigInt, BigInt) {
56    let xpz = addmod(x, z, n);
57    let xmz = submod(x, z, n);
58    let t1 = mulmod(&xpz, &xpz, n); // (X+Z)²
59    let t2 = mulmod(&xmz, &xmz, n); // (X−Z)²
60    let x2 = mulmod(&t1, &t2, n); // (X+Z)²(X−Z)²
61    let t3 = submod(&t1, &t2, n); // 4XZ = (X+Z)² − (X−Z)²
62    let inner = addmod(&t2, &mulmod(a24, &t3, n), n); // (X−Z)² + a24·4XZ
63    let z2 = mulmod(&t3, &inner, n);
64    (x2, z2)
65}
66
67/// x-only **differential addition** on a Montgomery curve mod `n`: given `P=(Xp:Zp)`, `Q=(Xq:Zq)` and their
68/// known difference `P−Q = (Xd:Zd)`, return `P+Q`. Again inversion-free.
69pub fn xadd(
70    xp: &BigInt,
71    zp: &BigInt,
72    xq: &BigInt,
73    zq: &BigInt,
74    xd: &BigInt,
75    zd: &BigInt,
76    n: &BigInt,
77) -> (BigInt, BigInt) {
78    let a = mulmod(&submod(xp, zp, n), &addmod(xq, zq, n), n); // (Xp−Zp)(Xq+Zq)
79    let b = mulmod(&addmod(xp, zp, n), &submod(xq, zq, n), n); // (Xp+Zp)(Xq−Zq)
80    let s = addmod(&a, &b, n);
81    let d = submod(&a, &b, n);
82    let xr = mulmod(zd, &mulmod(&s, &s, n), n); // Zd·(a+b)²
83    let zr = mulmod(xd, &mulmod(&d, &d, n), n); // Xd·(a−b)²
84    (xr, zr)
85}
86
87/// Scalar multiplication `k·(X:Z)` on a Montgomery curve mod `n` by the **Montgomery ladder** — a uniform
88/// sequence of one double and one differential-add per bit, maintaining the invariant `R1 − R0 = P`.
89pub fn ladder(k: u64, x: &BigInt, z: &BigInt, a24: &BigInt, n: &BigInt) -> (BigInt, BigInt) {
90    if k == 0 {
91        return (i(0), i(0)); // the identity (a point with Z = 0)
92    }
93    if k == 1 {
94        return (x.clone(), z.clone());
95    }
96    let mut r0 = (x.clone(), z.clone()); // P
97    let mut r1 = xdbl(x, z, a24, n); // 2P  (R1 − R0 = P)
98    for bit in (0..(64 - k.leading_zeros() - 1)).rev() {
99        if (k >> bit) & 1 == 1 {
100            r0 = xadd(&r0.0, &r0.1, &r1.0, &r1.1, x, z, n);
101            r1 = xdbl(&r1.0, &r1.1, a24, n);
102        } else {
103            r1 = xadd(&r0.0, &r0.1, &r1.0, &r1.1, x, z, n);
104            r0 = xdbl(&r0.0, &r0.1, a24, n);
105        }
106    }
107    r0
108}
109
110/// Primes `≤ b` by the sieve of Eratosthenes.
111fn primes_up_to(b: u64) -> Vec<u64> {
112    if b < 2 {
113        return Vec::new();
114    }
115    let mut sieve = vec![true; (b as usize) + 1];
116    sieve[0] = false;
117    sieve[1] = false;
118    let mut p = 2usize;
119    while p * p <= b as usize {
120        if sieve[p] {
121            let mut m = p * p;
122            while m <= b as usize {
123                sieve[m] = false;
124                m += p;
125            }
126        }
127        p += 1;
128    }
129    (2..=b).filter(|&x| sieve[x as usize]).collect()
130}
131
132/// **ECM stage 2** — the baby-step/giant-step continuation. After stage 1 has multiplied the base point by
133/// every prime power `≤ b1` (leaving `q = (qx:qz)`), stage 2 catches a factor `p` whose group order
134/// `#E(𝔽_p)` is `b1`-smooth EXCEPT for a *single* prime `ℓ ∈ (b1, b2]` — such an `ℓ` satisfies
135/// `ℓ·q ≡ O (mod p)`. Writing each such prime as `ℓ = i·d ± j`, that is `x((i·d)·q) = x(j·q) (mod p)`; we
136/// accumulate the product of all cross-differences `X(T_i)·Z(S_j) − X(S_j)·Z(T_i)` mod `n` and take ONE gcd,
137/// so the whole interval costs a single inversion. This is the standard, large power boost over stage 1.
138fn ecm_stage2(qx: &BigInt, qz: &BigInt, a24: &BigInt, b1: u64, b2: u64, n: &BigInt) -> Option<BigInt> {
139    let one = i(1);
140    let d = 210u64; // 2·3·5·7 — primes cluster as i·d ± j with |j| ≤ 105
141    let half = (d / 2) as usize;
142
143    // Baby steps S[j] = j·q for j = 1..=half (built by the differential-addition chain).
144    let mut sx = vec![i(0); half + 1];
145    let mut sz = vec![i(0); half + 1];
146    sx[1] = qx.clone();
147    sz[1] = qz.clone();
148    if half >= 2 {
149        let s2 = xdbl(qx, qz, a24, n);
150        sx[2] = s2.0;
151        sz[2] = s2.1;
152        for j in 3..=half {
153            let s = xadd(&sx[j - 1], &sz[j - 1], qx, qz, &sx[j - 2], &sz[j - 2], n);
154            sx[j] = s.0;
155            sz[j] = s.1;
156        }
157    }
158
159    // Bin each prime in (b1, b2] by its nearest giant index i and offset j: ℓ = i·d ± j.
160    let mut bins: HashMap<u64, Vec<usize>> = HashMap::new();
161    let (mut i_lo, mut i_hi) = (u64::MAX, 0u64);
162    for &ell in primes_up_to(b2).iter().filter(|&&p| p > b1) {
163        let ii = (ell + d / 2) / d;
164        let j = (ell as i64 - (ii * d) as i64).unsigned_abs() as usize;
165        if (1..=half).contains(&j) {
166            bins.entry(ii).or_default().push(j);
167            i_lo = i_lo.min(ii);
168            i_hi = i_hi.max(ii);
169        }
170    }
171    if i_lo > i_hi {
172        return None; // no primes in range
173    }
174
175    // Giant steps T_i = (i·d)·q, advanced by the differential addition T_{i+1} = T_i + d·q (diff T_{i-1}).
176    let dq = ladder(d, qx, qz, a24, n);
177    let mut t_prev = ladder(i_lo.saturating_sub(1) * d, qx, qz, a24, n);
178    let mut t_cur = ladder(i_lo * d, qx, qz, a24, n);
179    let mut accum = one.clone();
180    for ii in i_lo..=i_hi {
181        if ii > i_lo {
182            let t_next = xadd(&t_cur.0, &t_cur.1, &dq.0, &dq.1, &t_prev.0, &t_prev.1, n);
183            t_prev = t_cur;
184            t_cur = t_next;
185        }
186        if let Some(js) = bins.get(&ii) {
187            for &j in js {
188                let cross = submod(&mulmod(&t_cur.0, &sz[j], n), &mulmod(&sx[j], &t_cur.1, n), n);
189                if !cross.is_zero() {
190                    accum = mulmod(&accum, &cross, n);
191                }
192            }
193        }
194    }
195    let g = gcd(&accum, n);
196    (g != one && g != *n).then_some(g)
197}
198
199/// Run stage 1 (and stage 2 when `b2 > b1`) on ONE Suyama curve `σ`. Returns a factor or `None`.
200fn ecm_one_curve(n: &BigInt, sigma_u: u64, b1: u64, b2: u64, primes1: &[u64]) -> Option<BigInt> {
201    let one = i(1);
202    let sigma = i(sigma_u as i64);
203    let u = submod(&mulmod(&sigma, &sigma, n), &i(5), n); // σ² − 5
204    let v = mulmod(&i(4), &sigma, n); // 4σ
205    let u3 = mulmod(&mulmod(&u, &u, n), &u, n);
206    let v3 = mulmod(&mulmod(&v, &v, n), &v, n);
207    let vmu = submod(&v, &u, n);
208    let vmu3 = mulmod(&mulmod(&vmu, &vmu, n), &vmu, n);
209    let num = mulmod(&vmu3, &addmod(&mulmod(&i(3), &u, n), &v, n), n); // (v−u)³(3u+v)
210    let den = mulmod(&mulmod(&i(4), &u3, n), &v, n); // 4u³v
211
212    // The one forced inverse; a nontrivial gcd(den, n) IS a factor (bonus).
213    let g = gcd(&den, n);
214    if g != one && g != *n {
215        return Some(g);
216    }
217    let a24 = mulmod(&num, &mod_inverse(&den, n)?, n);
218    let mut pt = (u3, v3);
219
220    // Stage 1: multiply by ∏_{p ≤ b1} p^{⌊log_p b1⌋}, one prime power at a time.
221    for &p in primes1 {
222        let mut q = p;
223        while q <= b1 {
224            pt = ladder(p, &pt.0, &pt.1, &a24, n);
225            q = q.saturating_mul(p);
226        }
227    }
228    let g = gcd(&pt.1, n);
229    if g != one && g != *n {
230        return Some(g);
231    }
232    if b2 > b1 {
233        return ecm_stage2(&pt.0, &pt.1, &a24, b1, b2, n);
234    }
235    None
236}
237
238/// The per-curve Suyama seed: `σ ≥ 6`, varied by curve index and `seed`.
239#[inline]
240fn suyama_sigma(seed: u64, c: usize) -> u64 {
241    6 + seed.wrapping_add(c as u64).wrapping_mul(2_654_435_761) % 1_000_000_000
242}
243
244/// **Lenstra's ECM, stage 1 only.** Try `curves` Suyama curves at smoothness bound `b1`; a nontrivial
245/// factor of `n`, or `None`. Cost tracks the *smallest* factor, not `n`. `None` for `n ≤ 3`.
246pub fn ecm_factor(n: &BigInt, b1: u64, curves: usize, seed: u64) -> Option<BigInt> {
247    if n.to_i64().is_some_and(|v| v <= 3) {
248        return None;
249    }
250    if !n.is_odd() {
251        return Some(i(2));
252    }
253    let primes = primes_up_to(b1);
254    (0..curves).find_map(|c| ecm_one_curve(n, suyama_sigma(seed, c), b1, b1, &primes))
255}
256
257/// **ECM with stage 1 + stage 2.** Stage 2 (bound `b2 > b1`) extends each curve's reach to a group order
258/// that is `b1`-smooth apart from one prime `≤ b2` — a large boost per curve for a small extra cost.
259pub fn ecm_two_stage(n: &BigInt, b1: u64, b2: u64, curves: usize, seed: u64) -> Option<BigInt> {
260    if n.to_i64().is_some_and(|v| v <= 3) {
261        return None;
262    }
263    if !n.is_odd() {
264        return Some(i(2));
265    }
266    let primes = primes_up_to(b1);
267    (0..curves).find_map(|c| ecm_one_curve(n, suyama_sigma(seed, c), b1, b2, &primes))
268}
269
270/// **ECM stage 3 — the escalating driver.** Stages 1 and 2 are the algorithm; this is the orchestration that
271/// makes ECM a complete tool (à la GMP-ECM): run the two-stage method at a schedule of increasing bounds
272/// `(b1, b2 = 100·b1)` with growing curve counts, so a factor of *any* size is found at the cheapest level
273/// that reaches it — small factors fall almost immediately, larger ones as the bounds climb, without ever
274/// paying the big-bound cost up front. `budget` caps the curves per level. `None` if the whole schedule
275/// finishes without a factor.
276pub fn ecm(n: &BigInt, budget: usize, seed: u64) -> Option<BigInt> {
277    const SCHEDULE: &[(u64, usize)] = &[(2_000, 25), (11_000, 90), (50_000, 300), (250_000, 700)];
278    for (level, &(b1, curves)) in SCHEDULE.iter().enumerate() {
279        let curves = curves.min(budget.max(1));
280        if let Some(f) = ecm_two_stage(n, b1, b1.saturating_mul(100), curves, seed.wrapping_add(level as u64)) {
281            return Some(f);
282        }
283    }
284    None
285}
286
287// ---- Full Weierstrass arithmetic over 𝔽_p and the ECDLP ---------------------------------------------
288//
289// ECM *factors*; the elliptic-curve DISCRETE LOG is the other, deeper problem — the one ECC's security
290// rests on. Here we build full affine point arithmetic over a prime field and (a) solve the ECDLP by
291// baby-step/giant-step, which runs in O(√n) and so *proves the generic wall* (exponential in bit length —
292// why sound ECC resists and gets away with small keys), and (b) audit a curve for the exact structural
293// symmetries that DO break it: anomalous (Smart, polynomial), supersingular (MOV), smooth order (Pohlig–
294// Hellman). The generic ECDLP has NO sub-exponential attack — unlike factoring/DLP — which is the honest
295// reason ECC is, per bit, the strongest of the classical assumptions.
296
297/// A short Weierstrass curve `y² = x³ + a·x + b` over the prime field `𝔽_p`.
298#[derive(Clone, Debug, PartialEq, Eq)]
299pub struct Curve {
300    pub a: BigInt,
301    pub b: BigInt,
302    pub p: BigInt,
303}
304
305/// An affine point on a curve, or the point at infinity (the group identity).
306#[derive(Clone, Debug, PartialEq, Eq)]
307pub enum Point {
308    Infinity,
309    Affine(BigInt, BigInt),
310}
311
312impl Curve {
313    pub fn new(a: BigInt, b: BigInt, p: BigInt) -> Curve {
314        Curve { a, b, p }
315    }
316
317    /// Whether `pt` satisfies the curve equation.
318    pub fn is_on_curve(&self, pt: &Point) -> bool {
319        match pt {
320            Point::Infinity => true,
321            Point::Affine(x, y) => {
322                let lhs = mulmod(y, y, &self.p);
323                let x3 = mulmod(&mulmod(x, x, &self.p), x, &self.p);
324                let rhs = addmod(&addmod(&x3, &mulmod(&self.a, x, &self.p), &self.p), &self.b, &self.p);
325                lhs == rhs
326            }
327        }
328    }
329
330    /// The group inverse `−P`.
331    pub fn negate(&self, pt: &Point) -> Point {
332        match pt {
333            Point::Infinity => Point::Infinity,
334            Point::Affine(x, y) => Point::Affine(x.clone(), submod(&i(0), y, &self.p)),
335        }
336    }
337
338    /// The group law `P + Q`.
339    pub fn add(&self, p1: &Point, p2: &Point) -> Point {
340        match (p1, p2) {
341            (Point::Infinity, _) => p2.clone(),
342            (_, Point::Infinity) => p1.clone(),
343            (Point::Affine(x1, y1), Point::Affine(x2, y2)) => {
344                if x1 == x2 {
345                    if addmod(y1, y2, &self.p).is_zero() {
346                        return Point::Infinity; // P = −Q
347                    }
348                    return self.double(p1); // P = Q
349                }
350                let den = mod_inverse(&submod(x2, x1, &self.p), &self.p).expect("distinct x invertible over 𝔽_p");
351                let lam = mulmod(&submod(y2, y1, &self.p), &den, &self.p);
352                let x3 = submod(&submod(&mulmod(&lam, &lam, &self.p), x1, &self.p), x2, &self.p);
353                let y3 = submod(&mulmod(&lam, &submod(x1, &x3, &self.p), &self.p), y1, &self.p);
354                Point::Affine(x3, y3)
355            }
356        }
357    }
358
359    /// Point doubling `2P`.
360    pub fn double(&self, pt: &Point) -> Point {
361        match pt {
362            Point::Infinity => Point::Infinity,
363            Point::Affine(x, y) => {
364                if y.is_zero() {
365                    return Point::Infinity; // vertical tangent
366                }
367                let num = addmod(&mulmod(&i(3), &mulmod(x, x, &self.p), &self.p), &self.a, &self.p);
368                let den = mod_inverse(&mulmod(&i(2), y, &self.p), &self.p).expect("2y invertible over 𝔽_p");
369                let lam = mulmod(&num, &den, &self.p);
370                let x3 = submod(&mulmod(&lam, &lam, &self.p), &mulmod(&i(2), x, &self.p), &self.p);
371                let y3 = submod(&mulmod(&lam, &submod(x, &x3, &self.p), &self.p), y, &self.p);
372                Point::Affine(x3, y3)
373            }
374        }
375    }
376
377    /// Scalar multiplication `k·P` by double-and-add (`k ≥ 0`).
378    pub fn mul(&self, k: &BigInt, pt: &Point) -> Point {
379        let mut result = Point::Infinity;
380        let mut addend = pt.clone();
381        let (_, bytes) = k.to_le_bytes();
382        for byte in bytes {
383            for b in 0..8 {
384                if (byte >> b) & 1 == 1 {
385                    result = self.add(&result, &addend);
386                }
387                addend = self.double(&addend);
388            }
389        }
390        result
391    }
392
393    /// `#E(𝔽_p)` by direct point counting (includes the identity). `O(p)` — small `p` only.
394    pub fn count_points(&self) -> u64 {
395        let p_u = self.p.to_i64().expect("small prime") as u64;
396        let exp = self.p.sub(&i(1)).div_rem(&i(2)).unwrap().0; // (p−1)/2 — the Legendre symbol exponent
397        let one = i(1);
398        let mut count = 1u64; // infinity
399        for xu in 0..p_u {
400            let x = i(xu as i64);
401            let x3 = mulmod(&mulmod(&x, &x, &self.p), &x, &self.p);
402            let rhs = addmod(&addmod(&x3, &mulmod(&self.a, &x, &self.p), &self.p), &self.b, &self.p);
403            if rhs.is_zero() {
404                count += 1;
405            } else if modpow(&rhs, &exp, &self.p) == one {
406                count += 2; // a quadratic residue has two square roots
407            }
408        }
409        count
410    }
411
412    /// The order of `pt` — least `k > 0` with `k·pt = O`, searched up to `bound`; `None` if larger.
413    pub fn point_order(&self, pt: &Point, bound: u64) -> Option<u64> {
414        let mut cur = pt.clone();
415        for k in 1..=bound {
416            if cur == Point::Infinity {
417                return Some(k);
418            }
419            cur = self.add(&cur, pt);
420        }
421        None
422    }
423}
424
425fn point_key(pt: &Point) -> Vec<u8> {
426    match pt {
427        Point::Infinity => vec![0xff],
428        Point::Affine(x, y) => {
429            let (_, xb) = x.to_le_bytes();
430            let (_, yb) = y.to_le_bytes();
431            let mut k = vec![0x00, xb.len() as u8];
432            k.extend(xb);
433            k.extend(yb);
434            k
435        }
436    }
437}
438
439/// Solve the **elliptic-curve discrete log** `Q = k·P` by baby-step/giant-step, given the order `n` of `P`.
440/// `O(√n)` group operations — the best generic attack, and exponential in the bit length. This is exactly
441/// why a sound (large prime-order) curve resists, and why ECC uses far smaller keys than RSA. `None` if `Q`
442/// is not a multiple of `P`.
443pub fn ecdlp_bsgs(curve: &Curve, base: &Point, target: &Point, n: u64) -> Option<BigInt> {
444    let m = (n as f64).sqrt() as u64 + 1;
445    let mut baby: HashMap<Vec<u8>, u64> = HashMap::new();
446    let mut cur = Point::Infinity;
447    for j in 0..m {
448        baby.entry(point_key(&cur)).or_insert(j);
449        cur = curve.add(&cur, base);
450    }
451    let neg_mp = curve.negate(&curve.mul(&i(m as i64), base)); // −(m·P)
452    let mut gamma = target.clone();
453    for step in 0..=m {
454        if let Some(&j) = baby.get(&point_key(&gamma)) {
455            return Some(i((step * m + j) as i64));
456        }
457        gamma = curve.add(&gamma, &neg_mp);
458    }
459    None
460}
461
462/// The known STRUCTURAL weaknesses that make an elliptic curve's ECDLP breakable — the symmetries a sound
463/// curve must avoid.
464#[derive(Clone, Debug, PartialEq, Eq)]
465pub enum CurveWeakness {
466    /// `#E(𝔽_p) = p` (trace 1): **anomalous** — Smart's attack solves the ECDLP in *polynomial* time.
467    Anomalous,
468    /// `#E(𝔽_p) = p + 1` (trace 0): **supersingular** — small embedding degree; MOV maps to a sub-exp DLP.
469    Supersingular,
470    /// The order's largest prime factor is small: **Pohlig–Hellman** shrinks the ECDLP to that subgroup.
471    SmoothOrder { largest_prime_factor: u64 },
472}
473
474/// Audit a curve of known order `#E` over `𝔽_p` for the structural ECDLP weaknesses. `None` means the only
475/// attacks that apply are the generic `O(√·)` ones — the honest ceiling, not a proof of security.
476pub fn curve_security(p: u64, order: u64) -> Option<CurveWeakness> {
477    if order == p {
478        return Some(CurveWeakness::Anomalous);
479    }
480    if order == p + 1 {
481        return Some(CurveWeakness::Supersingular);
482    }
483    let lpf = largest_prime_factor(order);
484    if lpf.saturating_mul(lpf) < order {
485        return Some(CurveWeakness::SmoothOrder { largest_prime_factor: lpf });
486    }
487    None
488}
489
490fn largest_prime_factor(mut m: u64) -> u64 {
491    let mut largest = 1u64;
492    let mut d = 2u64;
493    while d * d <= m {
494        while m % d == 0 {
495            largest = d;
496            m /= d;
497        }
498        d += 1;
499    }
500    largest.max(m)
501}
502
503// ---- Isogenies: the maps BETWEEN elliptic curves — where the new mathematics lives -------------------
504//
505// An isogeny φ: E → E' is a nonconstant morphism that is also a group homomorphism; its kernel is a finite
506// subgroup, and its degree is the kernel size. Isogenies are the edges of the isogeny graph, the object
507// underneath CSIDH and SIKE — and the place a symmetry break (Castryck–Decru, 2022) collapsed a whole
508// post-quantum finalist by exploiting hidden torsion/endomorphism structure. Two curves are the same
509// vertex iff they share a **j-invariant** (the isomorphism symmetry). Given a kernel, **Vélu's formulas**
510// build φ and the codomain E'. A deep invariant we exploit as a test oracle: isogenous curves have the
511// SAME number of points (Tate) — the group order is an isogeny invariant.
512
513impl Curve {
514    /// The **j-invariant** `1728 · 4a³ / (4a³ + 27b²)` — the complete isomorphism invariant of the curve
515    /// (two curves are isomorphic over the algebraic closure iff their j-invariants agree). `None` if the
516    /// curve is singular (`4a³ + 27b² ≡ 0`).
517    pub fn j_invariant(&self) -> Option<BigInt> {
518        let p = &self.p;
519        let a3 = mulmod(&mulmod(&self.a, &self.a, p), &self.a, p);
520        let four_a3 = mulmod(&i(4), &a3, p);
521        let disc = addmod(&four_a3, &mulmod(&i(27), &mulmod(&self.b, &self.b, p), p), p);
522        if disc.is_zero() {
523            return None;
524        }
525        Some(mulmod(&mulmod(&i(1728), &four_a3, p), &mod_inverse(&disc, p)?, p))
526    }
527}
528
529/// A separable isogeny `φ: E → E'` of odd prime degree `ℓ`, built from a kernel generator by Vélu's
530/// formulas. Stores the codomain and, for each kernel representative `Q` (one per `±` pair), the Vélu
531/// quantities `(xQ, vQ = 6xQ²+2a, uQ = 4yQ²)` used to evaluate `φ`.
532#[derive(Clone, Debug)]
533pub struct Isogeny {
534    pub domain: Curve,
535    pub codomain: Curve,
536    pub degree: u64,
537    kernel: Vec<(BigInt, BigInt, BigInt)>, // (xQ, vQ, uQ)
538}
539
540impl Isogeny {
541    /// Build the `ℓ`-isogeny with kernel `⟨gen⟩` by **Vélu's formulas** (`ℓ` an odd prime, `gen` of order
542    /// exactly `ℓ`). The codomain is `y² = x³ + (a − 5v)x + (b − 7w)` where `v = Σ vQ`, `w = Σ (uQ + xQ·vQ)`
543    /// over the `(ℓ−1)/2` kernel representatives. `None` unless `ℓ` is an odd prime and `gen` has order `ℓ`.
544    pub fn from_kernel(curve: &Curve, gen: &Point, ell: u64) -> Option<Isogeny> {
545        if ell < 3 || ell % 2 == 0 {
546            return None;
547        }
548        let p = &curve.p;
549        let mut kernel = Vec::with_capacity(((ell - 1) / 2) as usize);
550        let mut cur = gen.clone();
551        for _ in 0..((ell - 1) / 2) {
552            match &cur {
553                Point::Affine(xq, yq) => {
554                    let vq = addmod(&mulmod(&i(6), &mulmod(xq, xq, p), p), &mulmod(&i(2), &curve.a, p), p);
555                    let uq = mulmod(&i(4), &mulmod(yq, yq, p), p);
556                    kernel.push((xq.clone(), vq, uq));
557                }
558                Point::Infinity => return None, // gen's order is smaller than ℓ
559            }
560            cur = curve.add(&cur, gen);
561        }
562        // Confirm the order is exactly ℓ (odd prime): ℓ·gen = O and gen ≠ O.
563        let _ = cur;
564        if matches!(gen, Point::Infinity) || curve.mul(&i(ell as i64), gen) != Point::Infinity {
565            return None;
566        }
567        let (mut vsum, mut wsum) = (i(0), i(0));
568        for (xq, vq, uq) in &kernel {
569            vsum = addmod(&vsum, vq, p);
570            wsum = addmod(&wsum, &addmod(uq, &mulmod(xq, vq, p), p), p);
571        }
572        let a2 = submod(&curve.a, &mulmod(&i(5), &vsum, p), p);
573        let b2 = submod(&curve.b, &mulmod(&i(7), &wsum, p), p);
574        Some(Isogeny { domain: curve.clone(), codomain: Curve::new(a2, b2, p.clone()), degree: ell, kernel })
575    }
576
577    /// Evaluate `φ` at a point. Kernel points map to the identity. For `P = (x, y) ∉ ker`, the x-map is
578    /// `X = x + Σ [ vQ/(x−xQ) + uQ/(x−xQ)² ]` and — since a normalized isogeny pulls back the invariant
579    /// differential (`φ*(dX/Y) = dx/y`) — the y-coordinate is `Y = y · X'(x) = y·[1 − Σ (vQ/(x−xQ)² +
580    /// 2uQ/(x−xQ)³)]`.
581    pub fn eval(&self, pt: &Point) -> Point {
582        let p = &self.domain.p;
583        match pt {
584            Point::Infinity => Point::Infinity,
585            Point::Affine(x, y) => {
586                let mut xnew = x.clone();
587                let mut yfac = i(1);
588                for (xq, vq, uq) in &self.kernel {
589                    let d = submod(x, xq, p);
590                    if d.is_zero() {
591                        return Point::Infinity; // P = ±Q ∈ ker
592                    }
593                    let di = mod_inverse(&d, p).expect("nonzero over 𝔽_p");
594                    let di2 = mulmod(&di, &di, p);
595                    let di3 = mulmod(&di2, &di, p);
596                    xnew = addmod(&xnew, &addmod(&mulmod(vq, &di, p), &mulmod(uq, &di2, p), p), p);
597                    yfac = submod(&yfac, &addmod(&mulmod(vq, &di2, p), &mulmod(&mulmod(&i(2), uq, p), &di3, p), p), p);
598                }
599                Point::Affine(xnew, mulmod(y, &yfac, p))
600            }
601        }
602    }
603}
604
605// ---- The Weil pairing: the ultimate symmetry on torsion --------------------------------------------
606//
607// `e_N: E[N] × E[N] → μ_N` is the canonical pairing on the N-torsion — bilinear, alternating
608// (`e(P,P)=1`), non-degenerate, and Galois/isogeny-compatible: `e_N(φP, φQ) = e_N(P,Q)^{deg φ}`. That last
609// law is the symmetry the torsion images in SIKE are FORCED to obey, and the lever the Castryck–Decru
610// break pulls. We compute it by Miller's algorithm (a double-and-add accumulating line-function values).
611
612/// The pair `(ℓ(Q), v(Q))` for the DOUBLING step: the tangent line at `T` and the vertical at `2T`,
613/// evaluated at `Q = (xq, yq)`.
614fn miller_double_lines(c: &Curve, t: &Point, xq: &BigInt, yq: &BigInt) -> Option<(BigInt, BigInt)> {
615    let p = &c.p;
616    let (xt, yt) = match t {
617        Point::Affine(x, y) => (x, y),
618        Point::Infinity => return None,
619    };
620    if yt.is_zero() {
621        return None; // 2-torsion; avoided for odd order
622    }
623    let lam = mulmod(
624        &addmod(&mulmod(&i(3), &mulmod(xt, xt, p), p), &c.a, p),
625        &mod_inverse(&mulmod(&i(2), yt, p), p)?,
626        p,
627    );
628    let ell = submod(&submod(yq, yt, p), &mulmod(&lam, &submod(xq, xt, p), p), p);
629    let t2 = c.double(t);
630    let vert = match &t2 {
631        Point::Affine(x2, _) => submod(xq, x2, p),
632        Point::Infinity => i(1), // 2T = O: no vertical factor
633    };
634    Some((ell, vert))
635}
636
637/// The pair `(ℓ(Q), v(Q))` for the ADDITION step: the chord through `T` and `P`, and the vertical at
638/// `T + P`, evaluated at `Q`.
639fn miller_add_lines(c: &Curve, t: &Point, pp: &Point, xq: &BigInt, yq: &BigInt) -> Option<(BigInt, BigInt)> {
640    let p = &c.p;
641    let ((xt, yt), (xpp, ypp)) = match (t, pp) {
642        (Point::Affine(a, b), Point::Affine(cc, d)) => ((a, b), (cc, d)),
643        _ => return None,
644    };
645    if xt == xpp {
646        if yt == ypp {
647            return miller_double_lines(c, t, xq, yq); // T = P
648        }
649        // T = −P ⟹ T + P = O: the line is the vertical through xt.
650        return Some((submod(xq, xt, p), i(1)));
651    }
652    let lam = mulmod(&submod(ypp, yt, p), &mod_inverse(&submod(xpp, xt, p), p)?, p);
653    let ell = submod(&submod(yq, yt, p), &mulmod(&lam, &submod(xq, xt, p), p), p);
654    let sum = c.add(t, pp);
655    let vert = match &sum {
656        Point::Affine(x3, _) => submod(xq, x3, p),
657        Point::Infinity => i(1),
658    };
659    Some((ell, vert))
660}
661
662/// Miller's function `f_{n,P}(Q)` as a field element (numerator·denominator⁻¹, batched to one inversion).
663fn miller(c: &Curve, pp: &Point, qq: &Point, n: u64) -> Option<BigInt> {
664    let p = &c.p;
665    let (xq, yq) = match qq {
666        Point::Affine(x, y) => (x, y),
667        Point::Infinity => return None,
668    };
669    let (mut num, mut den) = (i(1), i(1));
670    let mut t = pp.clone();
671    for bit in (0..(64 - n.leading_zeros() - 1)).rev() {
672        let (ell, vert) = miller_double_lines(c, &t, xq, yq)?;
673        num = mulmod(&mulmod(&num, &num, p), &ell, p);
674        den = mulmod(&mulmod(&den, &den, p), &vert, p);
675        t = c.double(&t);
676        if (n >> bit) & 1 == 1 {
677            let (ell, vert) = miller_add_lines(c, &t, pp, xq, yq)?;
678            num = mulmod(&num, &ell, p);
679            den = mulmod(&den, &vert, p);
680            t = c.add(&t, pp);
681        }
682    }
683    if den.is_zero() {
684        return None;
685    }
686    Some(mulmod(&num, &mod_inverse(&den, p)?, p))
687}
688
689/// The **Weil pairing** `e_N(P, Q) ∈ μ_N` for independent `N`-torsion points `P, Q`, via Miller:
690/// `e_N(P,Q) = (−1)ᴺ · f_{N,P}(Q) / f_{N,Q}(P)`. Bilinear, alternating, non-degenerate, and (the SIKE-
691/// relevant law) isogeny-compatible. `None` if a Miller evaluation degenerates (e.g. dependent points).
692pub fn weil_pairing(c: &Curve, pp: &Point, qq: &Point, n: u64) -> Option<BigInt> {
693    if pp == qq || matches!(pp, Point::Infinity) || matches!(qq, Point::Infinity) {
694        return Some(i(1)); // e(P,P)=1 and the degenerate cases
695    }
696    let fp = miller(c, pp, qq, n)?;
697    let fq = miller(c, qq, pp, n)?;
698    let ratio = mulmod(&fp, &mod_inverse(&fq, &c.p)?, &c.p);
699    let e = if n % 2 == 1 { submod(&i(0), &ratio, &c.p) } else { ratio }; // (−1)ᴺ
700    Some(e)
701}
702
703/// A square root of `a` mod `p` for `p ≡ 3 (mod 4)` (the SIDH regime): `a^{(p+1)/4}`; `None` for a
704/// non-residue.
705fn sqrt_fp(a: &BigInt, p: &BigInt) -> Option<BigInt> {
706    let exp = p.add(&i(1)).div_rem(&i(4))?.0;
707    let r = modpow(a, &exp, p);
708    (mulmod(&r, &r, p) == rem_pos(a, p)).then_some(r)
709}
710
711/// Every affine point of the curve (requires `p ≡ 3 (mod 4)`). `O(p)`.
712fn all_affine_points(curve: &Curve) -> Vec<Point> {
713    let pu = curve.p.to_i64().expect("small prime") as u64;
714    let mut v = Vec::new();
715    for xu in 0..pu {
716        let x = i(xu as i64);
717        let x3 = mulmod(&mulmod(&x, &x, &curve.p), &x, &curve.p);
718        let rhs = addmod(&addmod(&x3, &mulmod(&curve.a, &x, &curve.p), &curve.p), &curve.b, &curve.p);
719        if rhs.is_zero() {
720            v.push(Point::Affine(x, i(0)));
721        } else if let Some(y) = sqrt_fp(&rhs, &curve.p) {
722            v.push(Point::Affine(x.clone(), submod(&i(0), &y, &curve.p)));
723            v.push(Point::Affine(x, y));
724        }
725    }
726    v
727}
728
729/// A **torsion basis** of `E[n]` (`n` prime): two independent points of order `n` generating the full
730/// `n`-torsion `(ℤ/n)²`, or `None` if the `n`-torsion is not fully rational. Requires `p ≡ 3 (mod 4)`. This
731/// is the public torsion basis whose images an SIDH/SIKE key publishes.
732pub fn torsion_basis(curve: &Curve, n: u64) -> Option<(Point, Point)> {
733    let order_n: Vec<Point> = all_affine_points(curve)
734        .into_iter()
735        .filter(|pt| curve.mul(&i(n as i64), pt) == Point::Infinity)
736        .collect();
737    for p in &order_n {
738        let span: Vec<Point> = (0..n).map(|k| curve.mul(&i(k as i64), p)).collect();
739        if let Some(q) = order_n.iter().find(|q| !span.contains(q)) {
740            return Some((p.clone(), q.clone()));
741        }
742    }
743    None
744}
745
746/// A single point of prime order `ell` on the curve (a candidate isogeny-kernel generator), or `None` if
747/// there is no rational `ell`-torsion. Requires `p ≡ 3 (mod 4)`.
748pub fn point_of_order(curve: &Curve, ell: u64) -> Option<Point> {
749    all_affine_points(curve).into_iter().find(|pt| curve.mul(&i(ell as i64), pt) == Point::Infinity)
750}
751
752/// The SIDH kernel generator `P + [s]·Q` from a torsion basis `(P, Q)` and the secret scalar `s`. The secret
753/// isogeny is the quotient by `⟨P + [s]Q⟩`; Kani's gluing is what lets an attacker recover `s` from the
754/// published torsion images, after which [`derive_isogeny_path`] unfolds the whole chain.
755pub fn kernel_generator(curve: &Curve, p_pt: &Point, q_pt: &Point, s: &BigInt) -> Point {
756    curve.add(p_pt, &curve.mul(s, q_pt))
757}
758
759/// A single `ℓ`-isogeny step in a chain: the `domain` curve, the order-`ℓ` `kernel` point quotiented at this
760/// step, and the resulting `codomain`.
761#[derive(Clone, Debug, PartialEq, Eq)]
762pub struct IsogenyStep {
763    pub domain: Curve,
764    pub kernel: Point,
765    pub codomain: Curve,
766}
767
768/// **Torsion-image → path derivation.** A degree-`ℓᵃ` isogeny is pinned down by a *single* kernel generator
769/// `gen` of order `ℓᵃ` — the datum the SIDH/SIKE torsion images reconstruct (via Kani's gluing). This unfolds
770/// that one generator into the explicit chain of `a` prime-degree `ℓ`-isogenies: at step `i` the kernel is
771/// the order-`ℓ` point `[ℓ^{a−1−i}]·genᵢ`, and `gen` is pushed forward through each step so its order descends
772/// `ℓᵃ → ℓᵃ⁻¹ → … → 1`. The entire secret path pops out of the one meta-datum — the generator is the rule
773/// that emits the per-step rules. Requires `ℓ` an odd prime and `gen` of order exactly `ℓᵃ`.
774pub fn derive_isogeny_path(curve: &Curve, gen: &Point, ell: u64, a: u32) -> Option<Vec<IsogenyStep>> {
775    let mut steps = Vec::with_capacity(a as usize);
776    let mut e = curve.clone();
777    let mut g = gen.clone();
778    for step in 0..a {
779        let mut mult = i(1);
780        for _ in 0..(a - 1 - step) {
781            mult = mult.mul(&i(ell as i64));
782        }
783        let k = e.mul(&mult, &g); // [ℓ^{a−1−step}]·g — order exactly ℓ
784        let iso = Isogeny::from_kernel(&e, &k, ell)?;
785        steps.push(IsogenyStep { domain: e.clone(), kernel: k, codomain: iso.codomain.clone() });
786        g = iso.eval(&g);
787        e = iso.codomain;
788    }
789    Some(steps)
790}
791
792#[cfg(test)]
793mod tests {
794    use super::*;
795
796    fn big(s: &str) -> BigInt {
797        BigInt::parse_decimal(s).unwrap()
798    }
799
800    // Projective equality X1·Z2 ≡ X2·Z1 (mod n): two (X:Z) points are the same affine x.
801    fn proj_eq(a: &(BigInt, BigInt), b: &(BigInt, BigInt), n: &BigInt) -> bool {
802        mulmod(&a.0, &b.1, n) == mulmod(&b.0, &a.1, n)
803    }
804
805    #[test]
806    fn ladder_is_a_consistent_scalar_multiplication() {
807        // Over a prime field, the ladder must respect the group law: (a·b)·P = a·(b·P), and it must be
808        // additive, k·P via the ladder equals stepping the differential-addition chain by hand.
809        let n = big("1000003"); // prime
810        let a24 = i(7);
811        let p = (i(2), i(1));
812        for (a, b) in [(7u64, 11u64), (13, 5), (100, 37), (1, 999), (255, 256)] {
813            let lhs = ladder(a * b, &p.0, &p.1, &a24, &n);
814            let rhs = ladder(a, &ladder(b, &p.0, &p.1, &a24, &n).0, &ladder(b, &p.0, &p.1, &a24, &n).1, &a24, &n);
815            assert!(proj_eq(&lhs, &rhs, &n), "(a·b)·P = a·(b·P) for a={a} b={b}");
816        }
817        // A hand-built addition chain: P, 2P, 3P=2P+P, 5P=3P+2P — must match the ladder.
818        let p2 = xdbl(&p.0, &p.1, &a24, &n);
819        let p3 = xadd(&p2.0, &p2.1, &p.0, &p.1, &p.0, &p.1, &n); // 2P + P, difference P
820        let p5 = xadd(&p3.0, &p3.1, &p2.0, &p2.1, &p.0, &p.1, &n); // 3P + 2P, difference P
821        assert!(proj_eq(&p2, &ladder(2, &p.0, &p.1, &a24, &n), &n));
822        assert!(proj_eq(&p3, &ladder(3, &p.0, &p.1, &a24, &n), &n));
823        assert!(proj_eq(&p5, &ladder(5, &p.0, &p.1, &a24, &n), &n));
824    }
825
826    #[test]
827    fn ecm_pulls_a_small_factor_from_a_semiprime() {
828        // N = p·q with p ≈ 10⁶ (a factor ECM finds via a smooth curve order, where trial division would
829        // grind through ~10⁶ candidates). The result must be a genuine nontrivial divisor.
830        let p = big("1000003");
831        let q = big("1000000000039");
832        let n = p.mul(&q);
833        let f = ecm_factor(&n, 50_000, 60, 12345).expect("ECM finds a factor");
834        assert!(f != i(1) && f != n, "a nontrivial factor");
835        assert!(n.div_rem(&f).unwrap().1.is_zero(), "and it actually divides N");
836        assert!(f == p || f == q, "recovering one of the true primes");
837    }
838
839    #[test]
840    fn ecm_pulls_a_factor_from_a_larger_modulus() {
841        // A bigger modulus with a moderate factor — ECM's cost tracks the FACTOR size, not N.
842        let p = big("100000007"); // ~27-bit prime
843        let q = big("340282366920938463463374607431768211507"); // a large prime
844        let n = p.mul(&q);
845        let f = ecm_factor(&n, 100_000, 80, 999).expect("ECM finds the moderate factor");
846        assert_eq!(f, p, "ECM pulls the smaller prime out of a huge modulus");
847        assert!(n.div_rem(&f).unwrap().1.is_zero());
848    }
849
850    #[test]
851    fn ecm_declines_on_a_prime() {
852        // A prime has no nontrivial factor; ECM must never fabricate one.
853        let prime = big("1000000000039");
854        assert_eq!(ecm_factor(&prime, 10_000, 40, 7), None, "no factor of a prime");
855    }
856
857    #[test]
858    fn ecm_two_stage_and_driver_factor() {
859        let p = big("100003"); // ~17-bit factor
860        let q = big("1000000000039");
861        let n = p.mul(&q);
862        // Stage 1 + stage 2 (b2 ≫ b1) recovers the factor.
863        let f = ecm_two_stage(&n, 500, 20_000, 40, 2024).expect("two-stage ECM finds the factor");
864        assert!(f != i(1) && f != n && n.div_rem(&f).unwrap().1.is_zero(), "a real divisor");
865        assert!(f == p || f == q);
866        // The escalating driver (stage 3) finds it with NO hand-tuned bounds.
867        let g = ecm(&n, 60, 5).expect("the ECM driver factors by escalation");
868        assert!(g != i(1) && g != n && n.div_rem(&g).unwrap().1.is_zero());
869    }
870
871    // A textbook curve y² = x³ + 2x + 2 over 𝔽₁₇, with the on-curve point (5,1).
872    fn curve17() -> (Curve, Point) {
873        (Curve::new(i(2), i(2), i(17)), Point::Affine(i(5), i(1)))
874    }
875
876    #[test]
877    fn ecdlp_group_law_is_a_real_abelian_group() {
878        let (c, p) = curve17();
879        assert!(c.is_on_curve(&p));
880        // Inverse: P + (−P) = O.
881        assert_eq!(c.add(&p, &c.negate(&p)), Point::Infinity);
882        // Doubling agrees with self-addition, and stays on the curve.
883        let p2 = c.double(&p);
884        assert_eq!(p2, c.add(&p, &p));
885        assert!(c.is_on_curve(&p2));
886        // Scalar mult = repeated addition.
887        assert_eq!(c.mul(&i(3), &p), c.add(&c.add(&p, &p), &p));
888        // Associativity across three multiples.
889        let (q, r) = (c.mul(&i(2), &p), c.mul(&i(5), &p));
890        assert_eq!(c.add(&c.add(&p, &q), &r), c.add(&p, &c.add(&q, &r)), "the group law is associative");
891        // Lagrange: the point order annihilates it.
892        let ord = c.point_order(&p, 100).expect("a small order");
893        assert_eq!(c.mul(&i(ord as i64), &p), Point::Infinity, "ord(P)·P = O");
894    }
895
896    #[test]
897    fn ecdlp_bsgs_recovers_the_discrete_log() {
898        let (c, p) = curve17();
899        let n = c.point_order(&p, 100).unwrap();
900        for k in [3u64, 7, 11, 15] {
901            let q = c.mul(&i(k as i64), &p);
902            let recovered = ecdlp_bsgs(&c, &p, &q, n).expect("Q is in ⟨P⟩");
903            assert_eq!(c.mul(&recovered, &p), q, "k·P = Q for the recovered k (k mod ord(P))");
904        }
905    }
906
907    #[test]
908    fn point_count_is_hasse_valid_and_annihilates_every_point() {
909        let (c, p) = curve17();
910        let order = c.count_points();
911        // Hasse: |#E − (p+1)| ≤ 2√p. For p=17, #E ∈ [10, 26].
912        assert!((10..=26).contains(&order), "#E = {order} within Hasse");
913        // #E · P = O for any point (the group order annihilates).
914        assert_eq!(c.mul(&i(order as i64), &p), Point::Infinity);
915    }
916
917    #[test]
918    fn curve_security_flags_exactly_the_weak_structures() {
919        // Anomalous (#E = p): Smart's polynomial-time attack.
920        assert_eq!(curve_security(23, 23), Some(CurveWeakness::Anomalous));
921        // Supersingular (#E = p+1): MOV.
922        assert_eq!(curve_security(23, 24), Some(CurveWeakness::Supersingular));
923        // Smooth order 100 = 2²·5² (largest prime 5): Pohlig–Hellman.
924        assert_eq!(curve_security(101, 100), Some(CurveWeakness::SmoothOrder { largest_prime_factor: 5 }));
925        // Prime order → only generic O(√n): no structural weakness.
926        assert_eq!(curve_security(101, 103), None, "a prime-order curve resists");
927
928        // And it agrees with a REAL supersingular curve found by point counting.
929        let mut found = false;
930        'search: for pp in [11u64, 19, 23] {
931            for a in 0..pp {
932                for b in 0..pp {
933                    let c = Curve::new(i(a as i64), i(b as i64), i(pp as i64));
934                    // skip singular curves (discriminant 4a³+27b² ≡ 0)
935                    let disc = (4 * a * a * a + 27 * b * b) % pp;
936                    if disc == 0 {
937                        continue;
938                    }
939                    if c.count_points() == pp + 1 {
940                        assert_eq!(curve_security(pp, pp + 1), Some(CurveWeakness::Supersingular));
941                        found = true;
942                        break 'search;
943                    }
944                }
945            }
946        }
947        assert!(found, "supersingular curves exist and are detected");
948    }
949
950    // √a mod p for p ≡ 3 (mod 4): a^{(p+1)/4}, verified by squaring.
951    fn sqrt_mod(a: &BigInt, p: &BigInt) -> Option<BigInt> {
952        let exp = p.add(&i(1)).div_rem(&i(4)).unwrap().0;
953        let r = modpow(a, &exp, p);
954        (mulmod(&r, &r, p) == rem_pos(a, p)).then_some(r)
955    }
956
957    fn rhs(c: &Curve, x: &BigInt) -> BigInt {
958        let x3 = mulmod(&mulmod(x, x, &c.p), x, &c.p);
959        addmod(&addmod(&x3, &mulmod(&c.a, x, &c.p), &c.p), &c.b, &c.p)
960    }
961
962    fn curve_points(c: &Curve) -> Vec<Point> {
963        let pu = c.p.to_i64().unwrap() as u64;
964        let mut v = vec![Point::Infinity];
965        for xu in 0..pu {
966            let x = i(xu as i64);
967            let r = rhs(c, &x);
968            if r.is_zero() {
969                v.push(Point::Affine(x, i(0)));
970            } else if let Some(y) = sqrt_mod(&r, &c.p) {
971                v.push(Point::Affine(x.clone(), submod(&i(0), &y, &c.p)));
972                v.push(Point::Affine(x, y));
973            }
974        }
975        v
976    }
977
978    // Find a curve over 𝔽_p (p ≡ 3 mod 4) with a rational point of exact odd-prime order ℓ ∈ {3,5,7}.
979    fn find_ell_isogeny(pval: u64) -> Option<(Curve, Point, u64)> {
980        let p = i(pval as i64);
981        for a in 1..pval {
982            for b in 1..pval {
983                let c = Curve::new(i(a as i64), i(b as i64), p.clone());
984                if c.j_invariant().is_none() {
985                    continue; // singular
986                }
987                let order = c.count_points();
988                for ell in [3u64, 5, 7] {
989                    if order % ell != 0 {
990                        continue;
991                    }
992                    let cof = order / ell;
993                    for pt in curve_points(&c) {
994                        let q = c.mul(&i(cof as i64), &pt);
995                        if q != Point::Infinity && c.mul(&i(ell as i64), &q) == Point::Infinity {
996                            return Some((c, q, ell));
997                        }
998                    }
999                }
1000            }
1001        }
1002        None
1003    }
1004
1005    #[test]
1006    fn j_invariant_is_an_isomorphism_invariant() {
1007        let p = i(103);
1008        let c = Curve::new(i(2), i(3), p.clone());
1009        let j = c.j_invariant().unwrap();
1010        // The isomorphism (x,y) ↦ (u²x, u³y) sends (a,b) ↦ (u⁴a, u⁶b) and must preserve j.
1011        let u2 = mulmod(&i(5), &i(5), &p);
1012        let u4 = mulmod(&u2, &u2, &p);
1013        let u6 = mulmod(&u4, &u2, &p);
1014        let twist = Curve::new(mulmod(&u4, &i(2), &p), mulmod(&u6, &i(3), &p), p.clone());
1015        assert_eq!(twist.j_invariant().unwrap(), j, "j is invariant under isomorphism");
1016        assert_ne!(Curve::new(i(1), i(1), p).j_invariant().unwrap(), j, "different curves, different j");
1017    }
1018
1019    // Find a curve over 𝔽_p (p ≡ 1 mod n, so μ_n ⊂ 𝔽_p) with FULL rational n-torsion E[n] ≅ (ℤ/n)²,
1020    // returning a torsion basis (P, Q) of two independent order-n points.
1021    fn find_full_torsion(primes: &[u64], n: u64) -> Option<(Curve, Point, Point)> {
1022        for &pval in primes {
1023            let p = i(pval as i64);
1024            for a in 1..pval {
1025                for b in 1..pval {
1026                    let c = Curve::new(i(a as i64), i(b as i64), p.clone());
1027                    if c.j_invariant().is_none() {
1028                        continue;
1029                    }
1030                    if c.count_points() % (n * n) != 0 {
1031                        continue;
1032                    }
1033                    let onp: Vec<Point> = curve_points(&c)
1034                        .into_iter()
1035                        .filter(|pt| *pt != Point::Infinity && c.mul(&i(n as i64), pt) == Point::Infinity)
1036                        .collect();
1037                    if (onp.len() as u64) < n * n - 1 {
1038                        continue; // not the full n-torsion
1039                    }
1040                    for pp in &onp {
1041                        let span: Vec<Point> = (0..n).map(|k| c.mul(&i(k as i64), pp)).collect();
1042                        if let Some(qq) = onp.iter().find(|q| !span.contains(q)) {
1043                            return Some((c.clone(), pp.clone(), qq.clone()));
1044                        }
1045                    }
1046                }
1047            }
1048        }
1049        None
1050    }
1051
1052    #[test]
1053    fn weil_pairing_is_bilinear_alternating_and_nondegenerate() {
1054        let n = 3u64;
1055        let (c, pp, qq) = find_full_torsion(&[7u64, 13, 19, 31], n).expect("a curve with full n-torsion");
1056        let e = weil_pairing(&c, &pp, &qq, n).unwrap();
1057        // Non-degenerate: independent points give a PRIMITIVE nth root of unity.
1058        assert_ne!(e, i(1), "independent points ⟹ nontrivial pairing");
1059        assert_eq!(modpow(&e, &i(n as i64), &c.p), i(1), "e(P,Q) ∈ μ_n");
1060        // Alternating.
1061        assert_eq!(weil_pairing(&c, &pp, &pp, n).unwrap(), i(1), "e(P,P) = 1");
1062        // Antisymmetric: e(Q,P) = e(P,Q)⁻¹.
1063        assert_eq!(weil_pairing(&c, &qq, &pp, n).unwrap(), mod_inverse(&e, &c.p).unwrap());
1064        // Bilinear: e(kP, Q) = e(P,Q)ᵏ.
1065        for k in 1..n {
1066            let ek = weil_pairing(&c, &c.mul(&i(k as i64), &pp), &qq, n).unwrap();
1067            assert_eq!(ek, modpow(&e, &i(k as i64), &c.p), "e({k}P, Q) = e(P,Q)^{k}");
1068        }
1069    }
1070
1071    #[test]
1072    fn velu_isogeny_is_a_homomorphism_kills_its_kernel_and_preserves_order() {
1073        let (c, gen, ell) = find_ell_isogeny(103).expect("an ℓ-torsion instance exists");
1074        let iso = Isogeny::from_kernel(&c, &gen, ell).expect("Vélu builds the isogeny");
1075        assert_eq!(iso.degree, ell);
1076        let cod = &iso.codomain;
1077
1078        // Tate's theorem: isogenous curves have the SAME number of points — a deep, exact oracle.
1079        assert_eq!(c.count_points(), cod.count_points(), "isogenous ⟹ equal order");
1080
1081        // φ annihilates the whole kernel ⟨gen⟩.
1082        let mut k = gen.clone();
1083        for _ in 0..ell {
1084            assert_eq!(iso.eval(&k), Point::Infinity, "φ kills its kernel");
1085            k = c.add(&k, &gen);
1086        }
1087
1088        // φ is a genuine group homomorphism onto the codomain, over a sweep of points.
1089        let pts = curve_points(&c);
1090        for u in pts.iter().take(9) {
1091            for v in pts.iter().take(9) {
1092                let (fu, fv) = (iso.eval(u), iso.eval(v));
1093                assert!(cod.is_on_curve(&fu), "φ(P) lands on the codomain");
1094                assert_eq!(iso.eval(&c.add(u, v)), cod.add(&fu, &fv), "φ(P+Q) = φ(P)+φ(Q)");
1095            }
1096        }
1097    }
1098
1099    // The largest `a` for which the curve has a point of order exactly `ℓᵃ`, together with such a generator.
1100    fn largest_prime_power_generator(curve: &Curve, ell: u64) -> Option<(Point, u32)> {
1101        (1u32..=4).rev().find_map(|a| {
1102            let n = ell.pow(a);
1103            all_affine_points(curve)
1104                .into_iter()
1105                .find(|pt| curve.point_order(pt, n + 1) == Some(n))
1106                .map(|g| (g, a))
1107        })
1108    }
1109
1110    #[test]
1111    fn torsion_image_generator_unfolds_into_the_secret_isogeny_path() {
1112        // Supersingular-shaped: p = 107 ≡ 3 (mod 4) (so √ works over 𝔽_p) and ≡ 2 (mod 3) (so x ↦ x³ is a
1113        // bijection ⟹ y² = x³ + 1 has #E = p+1 = 108 = 2²·3³), giving a deep 3-power isogeny chain.
1114        let p = big("107");
1115        let e = Curve::new(i(0), i(1), p.clone());
1116        assert_eq!(e.count_points(), 108, "#E = p + 1 (supersingular)");
1117
1118        // The single reconstructed kernel generator — the datum the torsion images pin down.
1119        let (gen, a) = largest_prime_power_generator(&e, 3).expect("a 3-power torsion generator");
1120        assert!(a >= 2, "the 3-Sylow (order 27) forces a point of order ≥ 9 ⟹ a genuine multi-step chain");
1121
1122        // The whole secret path pops out of that one generator.
1123        let path = derive_isogeny_path(&e, &gen, 3, a).expect("a valid 3-isogeny chain");
1124        assert_eq!(path.len() as u32, a, "one ℓ-isogeny per unit of the exponent");
1125
1126        // Every step quotients a genuine order-3 subgroup, and the chain is connected domain → codomain.
1127        for (idx, step) in path.iter().enumerate() {
1128            assert_ne!(step.kernel, Point::Infinity, "step {idx} kernel is nontrivial");
1129            assert_eq!(step.domain.mul(&i(3), &step.kernel), Point::Infinity, "step {idx} kernel has order 3");
1130            assert!(step.domain.is_on_curve(&step.kernel), "the kernel point lies on the step's domain");
1131            if idx > 0 {
1132                assert_eq!(step.domain, path[idx - 1].codomain, "the chain is connected");
1133            }
1134        }
1135
1136        // The composite is exactly the ℓᵃ-isogeny with kernel ⟨gen⟩: pushing gen through every step lands on
1137        // O, while a point outside ⟨gen⟩ survives (the composite is not the zero map).
1138        let outside = all_affine_points(&e)
1139            .into_iter()
1140            .find(|pt| (0..3u64.pow(a)).all(|k| e.mul(&i(k as i64), &gen) != *pt))
1141            .expect("a point outside ⟨gen⟩");
1142        let (mut in_img, mut out_img) = (gen.clone(), outside);
1143        for step in &path {
1144            let iso = Isogeny::from_kernel(&step.domain, &step.kernel, 3).unwrap();
1145            in_img = iso.eval(&in_img);
1146            out_img = iso.eval(&out_img);
1147        }
1148        assert_eq!(in_img, Point::Infinity, "gen generates the kernel of the whole composite");
1149        assert_ne!(out_img, Point::Infinity, "a point outside ⟨gen⟩ survives the composite");
1150    }
1151
1152    #[test]
1153    fn the_secret_scalar_selects_the_isogeny_through_a_torsion_basis() {
1154        // A genuine rank-2 basis E[3] = ⟨P, Q⟩ needs 3 | p−1 (the Weil pairing forces the cube roots of unity
1155        // into the field). Search 𝔽_p (p ≡ 3 mod 4 for √, p ≡ 1 mod 3 for full 3-torsion) for such a curve.
1156        let (c, p_pt, q_pt) = [19u64, 31, 43, 67, 79, 103]
1157            .iter()
1158            .find_map(|&pp| {
1159                let p = big(&pp.to_string());
1160                (0..pp).find_map(|a| {
1161                    (1..pp).find_map(|b| {
1162                        // Skip singular curves (discriminant 4a³ + 27b² ≡ 0).
1163                        let disc = addmod(
1164                            &mulmod(&i(4), &mulmod(&mulmod(&i(a as i64), &i(a as i64), &p), &i(a as i64), &p), &p),
1165                            &mulmod(&i(27), &mulmod(&i(b as i64), &i(b as i64), &p), &p),
1166                            &p,
1167                        );
1168                        if disc.is_zero() {
1169                            return None;
1170                        }
1171                        let c = Curve::new(i(a as i64), i(b as i64), p.clone());
1172                        torsion_basis(&c, 3)
1173                            .filter(|(pt, qt)| {
1174                                c.point_order(pt, 4) == Some(3) && c.point_order(qt, 4) == Some(3)
1175                            })
1176                            .map(|(pt, qt)| (c.clone(), pt, qt))
1177                    })
1178                })
1179            })
1180            .expect("a curve with a rank-2 3-torsion basis");
1181
1182        // The secret s selects the kernel line ⟨P + [s]Q⟩; each of the four order-3 lines is a distinct,
1183        // valid 3-isogeny. This is the torsion-guided walk: one secret, one directed path.
1184        let mut lines = std::collections::HashSet::new();
1185        for s in 0..3u64 {
1186            let gen = kernel_generator(&c, &p_pt, &q_pt, &i(s as i64));
1187            assert!(c.is_on_curve(&gen), "P + [s]Q lies on the curve");
1188            assert_eq!(c.point_order(&gen, 4), Some(3), "P + [s]Q is a genuine order-3 kernel generator");
1189            let path = derive_isogeny_path(&c, &gen, 3, 1).expect("secret s selects a valid 3-isogeny");
1190            assert_eq!(path.len(), 1, "a single secret step is one ℓ-isogeny");
1191            // Record the kernel line ⟨gen⟩ as a canonical set of its points.
1192            let mut line: Vec<Vec<u8>> = (0..3).map(|k| point_key(&c.mul(&i(k), &gen))).collect();
1193            line.sort();
1194            lines.insert(line);
1195        }
1196        assert!(lines.len() >= 2, "distinct secrets select distinct kernel lines ⟹ distinct isogenies");
1197    }
1198}