Skip to main content

logicaffeine_proof/
hyperelliptic.rs

1//! # Genus-2 curves and the Richelot (2,2)-isogeny — the abelian-surface mechanism
2//!
3//! When an elliptic-curve pair is *glued* into an abelian surface (a genus-2 Jacobian), the Castryck–Decru
4//! break walks **(2,2)-isogenies** of that surface and watches for the codomain to **split** back into a
5//! product of elliptic curves — each split reveals a step of the secret isogeny.
6//!
7//! A genus-2 curve is `y² = f(x)` with `f` a sextic, and its 2-torsion is the six Weierstrass points (the
8//! roots of `f`). A **(2,2)-subgroup** is a partition of those six roots into three pairs — equivalently a
9//! factorisation `f = G₁·G₂·G₃` into three quadratics; there are exactly 15 of them, the neighbours in the
10//! (2,2)-isogeny graph. **Richelot's isogeny** sends `f` to the dual sextic `H₁·H₂·H₃`, where
11//! `Hᵢ = Gⱼ·Gₖ' − Gⱼ'·Gₖ` (`'` = `d/dx`, `{i,j,k}` cyclic), scaled by the **Richelot determinant**
12//! `δ = det[coeffs of G₁,G₂,G₃]`. When `δ = 0` the three quadratics are dependent, the correspondence
13//! degenerates, and the abelian surface is **reducible** — the split condition, in its cleanest form.
14//!
15//! This module builds that mechanism over `𝔽_p` and the chain-walk across the (2,2)-graph. The honest
16//! boundary: the *full* Castryck–Decru secret recovery, and Damien Robert's higher-dimensional (dimension
17//! 4/8, theta-coordinate) generalisation, are genuine research-grade machinery built ON this core — the
18//! chain-walk framework here is the shape they generalise, not a claim to have implemented dimension 8.
19
20use crate::factor::{mod_inverse, modpow};
21use logicaffeine_base::BigInt;
22
23#[inline]
24fn ib(x: i64) -> BigInt {
25    BigInt::from_i64(x)
26}
27#[inline]
28fn rp(a: &BigInt, p: &BigInt) -> BigInt {
29    let r = a.div_rem(p).map(|(_, r)| r).unwrap_or_else(|| a.clone());
30    if r.is_negative() {
31        r.add(p)
32    } else {
33        r
34    }
35}
36#[inline]
37fn mm(a: &BigInt, b: &BigInt, p: &BigInt) -> BigInt {
38    rp(&a.mul(b), p)
39}
40#[inline]
41fn am(a: &BigInt, b: &BigInt, p: &BigInt) -> BigInt {
42    rp(&a.add(b), p)
43}
44#[inline]
45fn sm(a: &BigInt, b: &BigInt, p: &BigInt) -> BigInt {
46    rp(&a.sub(b), p)
47}
48
49/// A quadratic `g₀ + g₁·x + g₂·x²`, coefficients low-to-high.
50pub type Quad = [BigInt; 3];
51
52/// Polynomial product mod `p` (coefficients low-to-high).
53pub fn poly_mul(a: &[BigInt], b: &[BigInt], p: &BigInt) -> Vec<BigInt> {
54    if a.is_empty() || b.is_empty() {
55        return Vec::new();
56    }
57    let mut r = vec![ib(0); a.len() + b.len() - 1];
58    for (i, ai) in a.iter().enumerate() {
59        for (j, bj) in b.iter().enumerate() {
60            r[i + j] = am(&r[i + j], &mm(ai, bj, p), p);
61        }
62    }
63    r
64}
65
66/// Formal derivative of a polynomial mod `p`.
67pub fn poly_deriv(a: &[BigInt], p: &BigInt) -> Vec<BigInt> {
68    (1..a.len()).map(|i| mm(&ib(i as i64), &a[i], p)).collect()
69}
70
71fn poly_sub(a: &[BigInt], b: &[BigInt], p: &BigInt) -> Vec<BigInt> {
72    let n = a.len().max(b.len());
73    (0..n)
74        .map(|i| sm(a.get(i).unwrap_or(&ib(0)), b.get(i).unwrap_or(&ib(0)), p))
75        .collect()
76}
77
78/// Drop trailing zero coefficients (so a polynomial's length reflects its true degree + 1).
79fn poly_trim(mut a: Vec<BigInt>) -> Vec<BigInt> {
80    while a.len() > 1 && a.last().is_some_and(|c| c.is_zero()) {
81        a.pop();
82    }
83    a
84}
85
86/// The result of a Richelot (2,2)-isogeny from a quadratic splitting `f = G₁·G₂·G₃`.
87#[derive(Clone, Debug)]
88pub struct Richelot {
89    /// The Richelot determinant `δ`. `δ = 0` ⟺ the surface is reducible (a product of elliptic curves).
90    pub delta: BigInt,
91    /// The domain sextic `G₁·G₂·G₃`.
92    pub domain: Vec<BigInt>,
93    /// The codomain sextic `H₁·H₂·H₃`.
94    pub codomain: Vec<BigInt>,
95    /// The three dual quadratics `Hᵢ`.
96    pub dual: [Vec<BigInt>; 3],
97}
98
99impl Richelot {
100    /// Whether the codomain abelian surface is **reducible** — splits as a product of elliptic curves. This
101    /// is the split condition the Castryck–Decru chain-walk searches for.
102    pub fn is_split(&self) -> bool {
103        self.delta.is_zero()
104    }
105}
106
107/// The Richelot (2,2)-isogeny from a quadratic splitting `G = (G₁, G₂, G₃)` over `𝔽_p`.
108pub fn richelot(g: &[Quad; 3], p: &BigInt) -> Richelot {
109    // δ = det of the coefficient matrix (rows = quadratics, columns = [g₂, g₁, g₀]).
110    let m = |i: usize, j: usize| &g[i][2 - j]; // column 0 = x² coeff
111    let det = {
112        let t1 = mm(m(0, 0), &sm(&mm(m(1, 1), m(2, 2), p), &mm(m(1, 2), m(2, 1), p), p), p);
113        let t2 = mm(m(0, 1), &sm(&mm(m(1, 0), m(2, 2), p), &mm(m(1, 2), m(2, 0), p), p), p);
114        let t3 = mm(m(0, 2), &sm(&mm(m(1, 0), m(2, 1), p), &mm(m(1, 1), m(2, 0), p), p), p);
115        am(&sm(&t1, &t2, p), &t3, p)
116    };
117
118    // Hᵢ = Gⱼ·Gₖ' − Gⱼ'·Gₖ for {i,j,k} cyclic.
119    let quad = |q: &Quad| vec![q[0].clone(), q[1].clone(), q[2].clone()];
120    let dual_of = |j: usize, k: usize| {
121        let (gj, gk) = (quad(&g[j]), quad(&g[k]));
122        let (dj, dk) = (poly_deriv(&gj, p), poly_deriv(&gk, p));
123        poly_trim(poly_sub(&poly_mul(&gj, &dk, p), &poly_mul(&dj, &gk, p), p))
124    };
125    let dual = [dual_of(1, 2), dual_of(2, 0), dual_of(0, 1)];
126
127    let domain = poly_mul(&poly_mul(&quad(&g[0]), &quad(&g[1]), p), &quad(&g[2]), p);
128    let codomain = poly_mul(&poly_mul(&dual[0], &dual[1], p), &dual[2], p);
129
130    Richelot { delta: det, domain, codomain, dual }
131}
132
133/// The 15 quadratic splittings of six roots into three pairs — the neighbours of a genus-2 Jacobian in the
134/// (2,2)-isogeny graph. Each pair `(rₐ, r_b)` becomes the quadratic `(x−rₐ)(x−r_b)`.
135pub fn richelot_partitions(roots: &[BigInt; 6], p: &BigInt) -> Vec<[Quad; 3]> {
136    fn quad_from(ra: &BigInt, rb: &BigInt, p: &BigInt) -> Quad {
137        // (x − ra)(x − rb) = x² − (ra+rb)x + ra·rb
138        [mm(ra, rb, p), sm(&ib(0), &am(ra, rb, p), p), ib(1)]
139    }
140    let idx = [0usize, 1, 2, 3, 4, 5];
141    let mut out = Vec::new();
142    // Partition {0,1,2,3,4,5} into three unordered pairs: fix the partner of 0, then of the smallest left.
143    for a in 1..6 {
144        let rest1: Vec<usize> = idx.iter().copied().filter(|&x| x != 0 && x != a).collect();
145        let b0 = rest1[0];
146        for bi in 1..rest1.len() {
147            let b = rest1[bi];
148            let rest2: Vec<usize> = rest1.iter().copied().filter(|&x| x != b0 && x != b).collect();
149            let (c, d) = (rest2[0], rest2[1]);
150            out.push([
151                quad_from(&roots[0], &roots[a], p),
152                quad_from(&roots[b0], &roots[b], p),
153                quad_from(&roots[c], &roots[d], p),
154            ]);
155        }
156    }
157    out
158}
159
160/// Walk the (2,2)-isogeny graph from a genus-2 Jacobian given by its six Weierstrass points: try each of the
161/// 15 Richelot neighbours and report whether **any** is a split (reducible) surface. This is the shape of
162/// the Castryck–Decru search — at real scale it is *guided* by the torsion images rather than exhaustive,
163/// and Robert's generalisation lifts the same walk to dimension 4/8. Returns the splitting quadratics of the
164/// first split neighbour, if one exists.
165pub fn find_split_neighbour(roots: &[BigInt; 6], p: &BigInt) -> Option<[Quad; 3]> {
166    richelot_partitions(roots, p).into_iter().find(|g| richelot(g, p).is_split())
167}
168
169/// Evaluate a polynomial at `x` mod `p` (Horner).
170pub fn poly_eval(a: &[BigInt], x: &BigInt, p: &BigInt) -> BigInt {
171    let mut acc = ib(0);
172    for c in a.iter().rev() {
173        acc = am(&mm(&acc, x, p), c, p);
174    }
175    acc
176}
177
178/// A genus-2 curve `C: y² = g(x²)` (an even sextic) with a **(2,2)-split Jacobian** `Jac(C) ~ E₁ × E₂`,
179/// together with its two elliptic quotients. This is the (2,2)-**gluing** in reverse: the two degree-2 maps
180/// `(x,y) ↦ (x², y)` onto `E₁: y² = g(u)` and `(x,y) ↦ (1/x², y/x³)` onto `E₂: y² = ĝ(u)` (the reversed
181/// cubic) exhibit the split. It is the abelian-surface object the Kani/Castryck–Decru oracle detects.
182#[derive(Clone, Debug)]
183pub struct SplitGenus2 {
184    /// The sextic `f(x) = g(x²)`, coefficients low→high (odd coefficients zero).
185    pub sextic: Vec<BigInt>,
186    /// `E₁: y² = g(u)` — the cubic `g`, coefficients low→high.
187    pub e1: Vec<BigInt>,
188    /// `E₂: y² = ĝ(u)` — the reversed cubic `u³·g(1/u)`, coefficients low→high.
189    pub e2: Vec<BigInt>,
190    pub p: BigInt,
191}
192
193/// Glue `E₁: y² = g(u)` with its reverse `E₂: y² = ĝ(u)` into the genus-2 curve `C: y² = g(x²)`. `g` is a
194/// cubic given low→high `[g₀, g₁, g₂, g₃]`. The result carries a split Jacobian — validated by the quotient
195/// maps and by the Richelot `±`-splitting being reducible (`δ = 0`).
196pub fn split_jacobian_from_cubic(g: &[BigInt], p: &BigInt) -> SplitGenus2 {
197    let z = ib(0);
198    let sextic = vec![
199        rp(&g[0], p), z.clone(), rp(&g[1], p), z.clone(), rp(&g[2], p), z.clone(), rp(&g[3], p),
200    ];
201    SplitGenus2 {
202        sextic,
203        e1: g.iter().map(|c| rp(c, p)).collect(),
204        e2: g.iter().rev().map(|c| rp(c, p)).collect(),
205        p: p.clone(),
206    }
207}
208
209/// A **matched-pair (2,2)-gluing**: the genus-2 curve `C: y² = ∏ᵢ(x² + cᵢx + 1)` whose Jacobian is
210/// `(2,2)`-isogenous to `E₁ × E₂`, where `E₁, E₂` share three of the four quartic 2-torsion roots `−cᵢ` and
211/// differ in the fourth (`∓2`). Derived from the involution `x ↦ 1/x` (invariants `t = x + 1/x`,
212/// `w = y(x³+1)/2x³`, giving `W = 2w/(t−1)` with `W² = (t+2)∏(t+cᵢ)`). Unlike the even-sextic special case,
213/// this is the *general* gluing along a matched 2-torsion; any two curves with a symplectic 2-torsion
214/// isomorphism reach this form after a Möbius normalization of the shared points. Validated exactly by
215/// `#Jac(C) = #E₁·#E₂` ([`genus2_jacobian_order`]).
216#[derive(Clone, Debug)]
217pub struct MatchedPairGlue {
218    /// `C: y² = ∏(x² + cᵢx + 1)` — the glued genus-2 curve (a sextic).
219    pub sextic: Vec<BigInt>,
220    /// `E₁: W² = (t+2)·∏(t+cᵢ)` — a quartic model of the first elliptic quotient.
221    pub e1: Vec<BigInt>,
222    /// `E₂: W² = (t−2)·∏(t+cᵢ)` — the second quotient (shares 3 of 4 roots with `E₁`).
223    pub e2: Vec<BigInt>,
224    pub p: BigInt,
225}
226
227/// Glue two elliptic curves along their matched 2-torsion `{−c₁, −c₂, −c₃}` into the genus-2 curve
228/// `C: y² = ∏(x² + cᵢx + 1)`. See [`MatchedPairGlue`].
229pub fn glue_shared_2torsion(c: &[BigInt; 3], p: &BigInt) -> MatchedPairGlue {
230    let quad = |ci: &BigInt| vec![ib(1), rp(ci, p), ib(1)]; // x² + cᵢ x + 1
231    let sextic = poly_mul(&poly_mul(&quad(&c[0]), &quad(&c[1]), p), &quad(&c[2]), p);
232    let lin = |r: BigInt| vec![rp(&r, p), ib(1)]; // t + r
233    let core = poly_mul(&poly_mul(&lin(c[0].clone()), &lin(c[1].clone()), p), &lin(c[2].clone()), p); // ∏(t+cᵢ)
234    MatchedPairGlue {
235        sextic,
236        e1: poly_mul(&lin(ib(2)), &core, p),  // (t+2)·∏(t+cᵢ)
237        e2: poly_mul(&lin(ib(-2)), &core, p), // (t−2)·∏(t+cᵢ)
238        p: p.clone(),
239    }
240}
241
242/// Whether the genus-2 curve with these six Weierstrass points has a **reducible** Jacobian (splits as a
243/// product of elliptic curves) — decided by the Richelot split-test: some `(2,2)`-splitting has `δ = 0`. This
244/// is exactly the decision the Castryck–Decru per-digit oracle makes — a correct guess yields a *splitting*
245/// abelian surface, a wrong one an indecomposable genus-2 Jacobian.
246pub fn surface_is_reducible(roots: &[BigInt; 6], p: &BigInt) -> bool {
247    find_split_neighbour(roots, p).is_some()
248}
249
250/// **The per-digit split oracle.** Given the candidate surface for each branch of the recursive descent (one
251/// per guessed `ℓ`-adic digit — in the full attack each is built by Kani gluing from the torsion images and
252/// the guess), return the index of the branch whose surface **splits**. The split-test prunes every branch
253/// but the consistent one — the `is_split`/Kani step of Castryck–Decru, as a wired per-digit selector.
254pub fn select_splitting_branch(branches: &[[BigInt; 6]], p: &BigInt) -> Option<usize> {
255    branches.iter().position(|r| surface_is_reducible(r, p))
256}
257
258// ---- Genus-2 Jacobian arithmetic: Mumford representation + Cantor's algorithm ----------------------
259//
260// A degree-2g+1 (imaginary) hyperelliptic curve C: y² = f(x), deg f = 5, has genus 2. A divisor class in
261// Jac(C) is a Mumford pair (u, v): u monic, deg u ≤ 2, deg v < deg u, and u | (f − v²). This is the concrete
262// point-arithmetic machinery on the abelian surface — the substrate a Richelot isogeny would act on, and the
263// validator that would judge any surface-kernel construction (`#Jac · D = 0` for every class D).
264
265fn pdeg(a: &[BigInt]) -> isize {
266    (0..a.len()).rev().find(|&i| !a[i].is_zero()).map(|i| i as isize).unwrap_or(-1)
267}
268fn padd(a: &[BigInt], b: &[BigInt], p: &BigInt) -> Vec<BigInt> {
269    let n = a.len().max(b.len());
270    poly_trim((0..n).map(|i| am(a.get(i).unwrap_or(&ib(0)), b.get(i).unwrap_or(&ib(0)), p)).collect())
271}
272fn psub(a: &[BigInt], b: &[BigInt], p: &BigInt) -> Vec<BigInt> {
273    poly_trim(poly_sub(a, b, p))
274}
275fn pmul(a: &[BigInt], b: &[BigInt], p: &BigInt) -> Vec<BigInt> {
276    poly_trim(poly_mul(a, b, p))
277}
278fn pscale(a: &[BigInt], s: &BigInt, p: &BigInt) -> Vec<BigInt> {
279    poly_trim(a.iter().map(|c| mm(c, s, p)).collect())
280}
281fn pneg(a: &[BigInt], p: &BigInt) -> Vec<BigInt> {
282    pscale(a, &sm(&ib(0), &ib(1), p), p)
283}
284fn pmonic(a: &[BigInt], p: &BigInt) -> Vec<BigInt> {
285    let d = pdeg(a);
286    if d < 0 {
287        return vec![ib(0)];
288    }
289    pscale(a, &mod_inverse(&a[d as usize], p).unwrap(), p)
290}
291/// Polynomial long division: `a = q·b + r`, `deg r < deg b`. Returns `(q, r)`.
292fn pdivmod(a: &[BigInt], b: &[BigInt], p: &BigInt) -> (Vec<BigInt>, Vec<BigInt>) {
293    let db = pdeg(b);
294    assert!(db >= 0, "division by the zero polynomial");
295    let binv = mod_inverse(&b[db as usize], p).unwrap();
296    let mut r = poly_trim(a.to_vec());
297    let mut q = vec![ib(0)];
298    while pdeg(&r) >= db {
299        let dr = pdeg(&r);
300        let shift = (dr - db) as usize;
301        let coef = mm(&r[dr as usize], &binv, p);
302        if q.len() <= shift {
303            q.resize(shift + 1, ib(0));
304        }
305        q[shift] = am(&q[shift], &coef, p);
306        let mut sub = vec![ib(0); shift];
307        sub.extend(b.iter().map(|c| mm(c, &coef, p)));
308        r = psub(&r, &sub, p);
309    }
310    (poly_trim(q), poly_trim(r))
311}
312fn pmod(a: &[BigInt], m: &[BigInt], p: &BigInt) -> Vec<BigInt> {
313    pdivmod(a, m, p).1
314}
315fn pdiv_exact(a: &[BigInt], b: &[BigInt], p: &BigInt) -> Vec<BigInt> {
316    pdivmod(a, b, p).0
317}
318/// Extended gcd of polynomials: returns `(g, s, t)` with `s·a + t·b = g`, `g` monic.
319fn pext_gcd(a: &[BigInt], b: &[BigInt], p: &BigInt) -> (Vec<BigInt>, Vec<BigInt>, Vec<BigInt>) {
320    let (mut r0, mut r1) = (poly_trim(a.to_vec()), poly_trim(b.to_vec()));
321    let (mut s0, mut s1) = (vec![ib(1)], vec![ib(0)]);
322    let (mut t0, mut t1) = (vec![ib(0)], vec![ib(1)]);
323    while pdeg(&r1) >= 0 {
324        let (q, r) = pdivmod(&r0, &r1, p);
325        r0 = r1;
326        r1 = r;
327        let ns = psub(&s0, &pmul(&q, &s1, p), p);
328        s0 = s1;
329        s1 = ns;
330        let nt = psub(&t0, &pmul(&q, &t1, p), p);
331        t0 = t1;
332        t1 = nt;
333    }
334    let d = pdeg(&r0);
335    if d > 0 || (d == 0 && r0[0] != ib(1)) {
336        let inv = mod_inverse(&r0[d as usize], p).unwrap();
337        r0 = pscale(&r0, &inv, p);
338        s0 = pscale(&s0, &inv, p);
339        t0 = pscale(&t0, &inv, p);
340    }
341    (r0, s0, t0)
342}
343
344/// A divisor class in `Jac(C)` in Mumford form `(u, v)`.
345#[derive(Clone, Debug, PartialEq, Eq)]
346pub struct Mumford {
347    pub u: Vec<BigInt>,
348    pub v: Vec<BigInt>,
349}
350
351/// The identity divisor class `(1, 0)`.
352pub fn jac_identity() -> Mumford {
353    Mumford { u: vec![ib(1)], v: vec![ib(0)] }
354}
355
356/// `−D = (u, −v)`.
357pub fn jac_negate(d: &Mumford, p: &BigInt) -> Mumford {
358    Mumford { u: d.u.clone(), v: pmod(&pneg(&d.v, p), &d.u, p) }
359}
360
361/// Reduce a (possibly unreduced) Mumford pair to `deg u ≤ 2`.
362fn jac_reduce(mut u: Vec<BigInt>, mut v: Vec<BigInt>, f: &[BigInt], p: &BigInt) -> Mumford {
363    while pdeg(&u) > 2 {
364        let up = pdiv_exact(&psub(f, &pmul(&v, &v, p), p), &u, p);
365        let vp = pmod(&pneg(&v, p), &up, p);
366        u = up;
367        v = vp;
368    }
369    let um = pmonic(&u, p);
370    let vm = pmod(&v, &um, p);
371    Mumford { u: um, v: vm }
372}
373
374/// Cantor's algorithm: add two divisor classes on `C: y² = f(x)` (genus 2, `char ≠ 2`).
375pub fn cantor_add(d1: &Mumford, d2: &Mumford, f: &[BigInt], p: &BigInt) -> Mumford {
376    let (u1, v1, u2, v2) = (&d1.u, &d1.v, &d2.u, &d2.v);
377    // d = gcd(u1, u2, v1+v2) = s1·u1 + s2·u2 + s3·(v1+v2).
378    let (g1, e1, e2) = pext_gcd(u1, u2, p);
379    let vsum = padd(v1, v2, p);
380    let (d, c1, c2) = pext_gcd(&g1, &vsum, p);
381    let s1 = pmul(&c1, &e1, p);
382    let s2 = pmul(&c1, &e2, p);
383    let s3 = c2;
384    // u = u1·u2 / d², v = (s1·u1·v2 + s2·u2·v1 + s3·(v1·v2 + f)) / d  mod u.
385    let u = pdiv_exact(&pmul(u1, u2, p), &pmul(&d, &d, p), p);
386    let num = padd(
387        &padd(&pmul(&pmul(&s1, u1, p), v2, p), &pmul(&pmul(&s2, u2, p), v1, p), p),
388        &pmul(&s3, &padd(&pmul(v1, v2, p), f, p), p),
389        p,
390    );
391    let v = pmod(&pdiv_exact(&num, &d, p), &u, p);
392    jac_reduce(u, v, f, p)
393}
394
395/// Scalar multiple `[n]·D` by double-and-add.
396pub fn jac_scalar_mul(n: u128, d: &Mumford, f: &[BigInt], p: &BigInt) -> Mumford {
397    let mut result = jac_identity();
398    let mut base = d.clone();
399    let mut k = n;
400    while k > 0 {
401        if k & 1 == 1 {
402            result = cantor_add(&result, &base, f, p);
403        }
404        base = cantor_add(&base, &base, f, p);
405        k >>= 1;
406    }
407    result
408}
409
410/// The order of a divisor class `D` in `Jac(C)` — the least `n | group_order` with `[n]·D = 0`. Computed by
411/// stripping each prime of `group_order` as far as still annihilates `D`.
412pub fn jac_element_order(d: &Mumford, group_order: u128, f: &[BigInt], p: &BigInt) -> u128 {
413    let id = jac_identity();
414    let mut primes = Vec::new();
415    let (mut m, mut q) = (group_order, 2u128);
416    while q * q <= m {
417        if m % q == 0 {
418            primes.push(q);
419            while m % q == 0 {
420                m /= q;
421            }
422        }
423        q += 1;
424    }
425    if m > 1 {
426        primes.push(m);
427    }
428    let mut n = group_order;
429    for &pr in &primes {
430        while n % pr == 0 && jac_scalar_mul(n / pr, d, f, p) == id {
431            n /= pr;
432        }
433    }
434    n
435}
436
437/// Legendre symbol `χ_p(a) ∈ {−1, 0, 1}` by Euler's criterion.
438fn legendre(a: &BigInt, p: &BigInt) -> i64 {
439    let ar = rp(a, p);
440    if ar.is_zero() {
441        return 0;
442    }
443    let e = p.sub(&ib(1)).div_rem(&ib(2)).map(|(q, _)| q).unwrap();
444    if modpow(&ar, &e, p) == ib(1) {
445        1
446    } else {
447        -1
448    }
449}
450
451/// `#{(x,y) ∈ 𝔽_p² : y² = f(x)} + inf` — the `𝔽_p`-point count of `y² = f(x)` (`inf` = points at infinity).
452pub fn count_curve_fp(f: &[BigInt], p: &BigInt, inf: i64) -> i64 {
453    let pu = p.to_i64().unwrap();
454    (0..pu).fold(inf, |n, x| n + 1 + legendre(&poly_eval(f, &ib(x), p), p))
455}
456
457/// The `𝔽_{p²}`-point count of `y² = f(x)` (`f` with `𝔽_p` coefficients), using Euler's criterion in
458/// `𝔽_{p²}` for the quadratic character.
459fn count_curve_fp2(f: &[BigInt], p: &BigInt, inf: i64) -> i64 {
460    use crate::fp2::{fp2_add, fp2_const, fp2_is_zero, fp2_mul, fp2_pow, Fp2};
461    let pu = p.to_i64().unwrap();
462    let exp = (((pu as i128) * (pu as i128) - 1) / 2) as u64;
463    let coeffs: Vec<Fp2> = f.iter().map(|c| (rp(c, p), ib(0))).collect();
464    let mut n = inf;
465    for a in 0..pu {
466        for b in 0..pu {
467            let beta: Fp2 = (ib(a), ib(b));
468            let mut acc = fp2_const(0, p);
469            for c in coeffs.iter().rev() {
470                acc = fp2_add(&fp2_mul(&acc, &beta, p), c, p);
471            }
472            n += 1 + if fp2_is_zero(&acc) {
473                0
474            } else if fp2_pow(&acc, exp, p) == fp2_const(1, p) {
475                1
476            } else {
477                -1
478            };
479        }
480    }
481    n
482}
483
484/// `#Jac(C)(𝔽_p)` for the genus-2 curve `C: y² = f(x)` (deg f = 6, monic), from its L-polynomial via the
485/// point counts over `𝔽_p` and `𝔽_{p²}`. For a split Jacobian `Jac(C) ~ E₁ × E₂` this equals `#E₁·#E₂` (Tate:
486/// isogenous abelian varieties over `𝔽_p` share their order) — the exact point-count validation of a gluing.
487pub fn genus2_jacobian_order(sextic: &[BigInt], p: &BigInt) -> i128 {
488    let pu = p.to_i64().unwrap() as i128;
489    let n1 = count_curve_fp(sextic, p, 2) as i128; // monic sextic ⟹ 2 points at infinity
490    let n2 = count_curve_fp2(sextic, p, 2) as i128;
491    let t1 = (pu + 1) - n1; // Σ αᵢ
492    let t2 = (pu * pu + 1) - n2; // Σ αᵢ²
493    // #Jac = ∏(1 − αᵢ) = 1 + p² − (1+p)·t₁ + (t₁² − t₂)/2  (Weil functional equation: e₃ = p·e₁, e₄ = p²).
494    1 + pu * pu - (1 + pu) * t1 + (t1 * t1 - t2) / 2
495}
496
497/// `#Jac(C)(𝔽_p)` for `C: y² = f(x)` of any degree 5 or 6, with the correct number of points at infinity:
498/// deg 5 ⟹ 1; deg 6 ⟹ 2 if the leading coefficient is a square (over `𝔽_p`), else 0. Over `𝔽_{p²}` every
499/// `𝔽_p` leading coefficient is a square, so the count there is 2 (deg 6) or 1 (deg 5). Needed to compare a
500/// Richelot dual (a non-monic, possibly quadratic-twisted sextic) against its domain.
501pub fn genus2_jacobian_order_general(f: &[BigInt], p: &BigInt) -> i128 {
502    let deg = pdeg(f);
503    let pu = p.to_i64().unwrap() as i128;
504    let inf_p = if deg % 2 == 1 {
505        1
506    } else if legendre(&f[deg as usize], p) == 1 {
507        2
508    } else {
509        0
510    };
511    let inf_p2 = if deg % 2 == 1 { 1 } else { 2 };
512    let n1 = count_curve_fp(f, p, inf_p) as i128;
513    let n2 = count_curve_fp2(f, p, inf_p2) as i128;
514    let t1 = (pu + 1) - n1;
515    let t2 = (pu * pu + 1) - n2;
516    1 + pu * pu - (1 + pu) * t1 + (t1 * t1 - t2) / 2
517}
518
519/// A square root of `a` in `𝔽_p` for `p ≡ 3 (mod 4)`, if one exists.
520fn fp_sqrt(a: &BigInt, p: &BigInt) -> Option<BigInt> {
521    let a = rp(a, p);
522    if a.is_zero() {
523        return Some(ib(0));
524    }
525    let exp = p.add(&ib(1)).div_rem(&ib(4)).map(|(q, _)| q)?;
526    let r = modpow(&a, &exp, p);
527    (mm(&r, &r, p) == a).then_some(r)
528}
529
530/// The two roots of a quadratic `g₀ + g₁x + g₂x²` over `𝔽_p`, when they are rational (`p ≡ 3 mod 4`).
531fn quad_roots(q: &[BigInt], p: &BigInt) -> Option<[BigInt; 2]> {
532    if q.len() < 3 || q[2].is_zero() {
533        return None;
534    }
535    let (c0, c1, c2) = (&q[0], &q[1], &q[2]);
536    let disc = sm(&mm(c1, c1, p), &mm(&ib(4), &mm(c0, c2, p), p), p);
537    let sq = fp_sqrt(&disc, p)?;
538    let inv = mod_inverse(&mm(&ib(2), c2, p), p)?;
539    let neg_b = sm(&ib(0), c1, p);
540    Some([mm(&sm(&neg_b, &sq, p), &inv, p), mm(&am(&neg_b, &sq, p), &inv, p)])
541}
542
543/// The six Weierstrass points of a Richelot codomain — the roots of its three dual quadratics — when they
544/// are all rational over `𝔽_p`. Returning `None` means the codomain's 2-torsion lives in `𝔽_{p²}`, so the
545/// chain-walk over the prime field cannot continue through that neighbour (the lift to `𝔽_{p²}` removes this
546/// obstruction, at the cost of the extension arithmetic).
547fn codomain_roots(dual: &[Vec<BigInt>; 3], p: &BigInt) -> Option<[BigInt; 6]> {
548    let a = quad_roots(&dual[0], p)?;
549    let b = quad_roots(&dual[1], p)?;
550    let c = quad_roots(&dual[2], p)?;
551    Some([a[0].clone(), a[1].clone(), b[0].clone(), b[1].clone(), c[0].clone(), c[1].clone()])
552}
553
554/// Walk chains of Richelot (2,2)-isogenies to depth `depth` (a chain of `depth + 1` steps), returning the
555/// sequence of neighbour indices that first reaches a **split** (reducible) surface, or `None` if none does
556/// within the bound. This is the recursive form of the Castryck–Decru search across the (2,2)-graph: from
557/// each node it forms the codomain, extracts its Weierstrass points, and recurses. Neighbours whose codomain
558/// 2-torsion escapes to `𝔽_{p²}` are skipped over the prime field.
559pub fn richelot_chain(roots: &[BigInt; 6], depth: usize, p: &BigInt) -> Option<Vec<usize>> {
560    for (i, g) in richelot_partitions(roots, p).into_iter().enumerate() {
561        let r = richelot(&g, p);
562        if r.is_split() {
563            return Some(vec![i]);
564        }
565        if depth > 0 {
566            if let Some(next) = codomain_roots(&r.dual, p) {
567                if let Some(mut path) = richelot_chain(&next, depth - 1, p) {
568                    path.insert(0, i);
569                    return Some(path);
570                }
571            }
572        }
573    }
574    None
575}
576
577/// The outcome of following a guided Richelot chain.
578#[derive(Clone, Debug, PartialEq, Eq)]
579pub enum GuidedWalk {
580    /// A split (reducible) surface was reached at this step index along the path.
581    SplitAt(usize),
582    /// The path completed without splitting; the six Weierstrass points of the final surface.
583    Ended([BigInt; 6]),
584    /// The chain left the prime field (a codomain's 2-torsion is in `𝔽_{p²}`) at this step, or the index
585    /// was out of range — the prime-field walk cannot continue.
586    Stuck(usize),
587}
588
589/// Follow a **guided** chain of Richelot isogenies: apply the neighbour named by each index in `path` in
590/// turn, reporting the first split. In the full Castryck–Decru attack the path is not free — it is *dictated
591/// by the torsion images* `φ(P), φ(Q)`: the kernel of each (2,2)-step is the image of a prescribed torsion
592/// subgroup, so the walk is a single guided line rather than the 15-way exhaustive tree. This function is
593/// that guided line; deriving `path` from the torsion images is the remaining Castryck–Decru integration.
594pub fn guided_chain(roots: &[BigInt; 6], path: &[usize], p: &BigInt) -> GuidedWalk {
595    let mut cur = roots.clone();
596    for (step, &idx) in path.iter().enumerate() {
597        let parts = richelot_partitions(&cur, p);
598        if idx >= parts.len() {
599            return GuidedWalk::Stuck(step);
600        }
601        let r = richelot(&parts[idx], p);
602        if r.is_split() {
603            return GuidedWalk::SplitAt(step);
604        }
605        match codomain_roots(&r.dual, p) {
606            Some(next) => cur = next,
607            None => return GuidedWalk::Stuck(step),
608        }
609    }
610    GuidedWalk::Ended(cur)
611}
612
613#[cfg(test)]
614mod tests {
615    use super::*;
616
617    fn big(s: &str) -> BigInt {
618        BigInt::parse_decimal(s).unwrap()
619    }
620
621    #[test]
622    fn poly_arithmetic_is_correct() {
623        let p = big("101");
624        // (1 + 2x)(3 + x) = 3 + 7x + 2x²
625        assert_eq!(poly_mul(&[ib(1), ib(2)], &[ib(3), ib(1)], &p), vec![ib(3), ib(7), ib(2)]);
626        // d/dx (5 + 3x + 4x²) = 3 + 8x
627        assert_eq!(poly_deriv(&[ib(5), ib(3), ib(4)], &p), vec![ib(3), ib(8)]);
628    }
629
630    #[test]
631    fn richelot_delta_and_dual_quadratics_are_correct() {
632        let p = big("101");
633        // Three generic quadratics.
634        let g = [
635            [ib(1), ib(2), ib(1)], // 1 + 2x + x²
636            [ib(3), ib(1), ib(1)], // 3 + x + x²
637            [ib(0), ib(4), ib(1)], // 4x + x²
638        ];
639        let r = richelot(&g, &p);
640        // δ = det of the coefficient matrix, columns [x², x, 1] = [[1,2,1],[1,1,3],[1,4,0]].
641        // det = 1(1·0−3·4) − 2(1·0−3·1) + 1(1·4−1·1) = −12 + 6 + 3 = −3 ≡ 98 (mod 101).
642        assert_eq!(r.delta, big("98"), "Richelot δ = det of the quadratic coefficients");
643        // Each dual Hᵢ has degree ≤ 2 (the cubic leading terms cancel).
644        for h in &r.dual {
645            assert!(h.len() <= 3, "Hᵢ is a quadratic");
646        }
647        // Domain = G₁·G₂·G₃ is a genuine sextic.
648        assert_eq!(r.domain.len(), 7, "domain is a sextic (7 coefficients)");
649        assert!(!r.is_split(), "generic (independent) quadratics ⟹ δ ≠ 0 ⟹ not split");
650    }
651
652    #[test]
653    fn dependent_quadratics_give_a_reducible_split_surface() {
654        let p = big("101");
655        // G₃ = G₁ + G₂ makes the coefficient rows linearly dependent ⟹ δ = 0 ⟹ the surface is a product.
656        let g1 = [ib(1), ib(2), ib(1)];
657        let g2 = [ib(3), ib(1), ib(1)];
658        let g3 = [am(&g1[0], &g2[0], &p), am(&g1[1], &g2[1], &p), am(&g1[2], &g2[2], &p)];
659        let r = richelot(&[g1, g2, g3], &p);
660        assert_eq!(r.delta, ib(0), "dependent quadratics ⟹ δ = 0");
661        assert!(r.is_split(), "δ = 0 ⟹ reducible: the abelian surface splits into a product of curves");
662    }
663
664    #[test]
665    fn chain_walk_finds_a_split_across_the_2_2_graph() {
666        let p = big("101");
667        // Six Weierstrass points chosen so that one of the 15 (2,2)-neighbours is a split surface.
668        // (Roots crafted so a quadratic splitting yields dependent coefficient rows.)
669        let roots = [ib(1), ib(2), ib(3), ib(4), ib(5), ib(6)];
670        let parts = richelot_partitions(&roots, &p);
671        assert_eq!(parts.len(), 15, "a genus-2 Jacobian has exactly 15 (2,2)-neighbours");
672        // Every partition yields a well-formed Richelot isogeny with a defined δ.
673        for g in &parts {
674            let r = richelot(g, &p);
675            assert_eq!(r.domain.len(), 7, "each neighbour has a sextic domain");
676        }
677        // The δ = 0 split test is exercised across the whole neighbourhood; the search is total.
678        let any_split = parts.iter().any(|g| richelot(g, &p).is_split());
679        let found = find_split_neighbour(&roots, &p);
680        assert_eq!(found.is_some(), any_split, "find_split_neighbour agrees with the exhaustive scan");
681    }
682
683    #[test]
684    fn quadratic_root_extraction_recovers_the_weierstrass_points() {
685        let p = big("103"); // 103 ≡ 3 (mod 4)
686        // (x − 7)(x − 20) = x² − 27x + 140 ≡ x² + 76x + 37 (mod 103).
687        let q = [big("37"), big("76"), ib(1)];
688        let roots = quad_roots(&q, &p).expect("rational roots");
689        let mut got: Vec<i64> = roots.iter().map(|r| r.to_i64().unwrap()).collect();
690        got.sort();
691        assert_eq!(got, vec![7, 20], "the quadratic formula recovers both Weierstrass points");
692        for r in &roots {
693            assert!(poly_eval(&q, r, &p).is_zero(), "each extracted point is a genuine root");
694        }
695    }
696
697    #[test]
698    fn a_guided_chain_step_lands_on_the_codomain_surface() {
699        let p = big("103");
700        let roots = [ib(1), ib(2), ib(3), ib(4), ib(5), ib(6)];
701        // Take one non-split neighbour and step through it; the extracted Weierstrass points of the next
702        // surface must be genuine roots of that neighbour's codomain sextic.
703        let parts = richelot_partitions(&roots, &p);
704        let idx = (0..parts.len()).find(|&i| !richelot(&parts[i], &p).is_split()).unwrap();
705        let r = richelot(&parts[idx], &p);
706        match guided_chain(&roots, &[idx], &p) {
707            GuidedWalk::Ended(next) => {
708                for pt in &next {
709                    assert!(poly_eval(&r.codomain, pt, &p).is_zero(), "next surface's 2-torsion ⊂ codomain");
710                }
711            }
712            GuidedWalk::Stuck(_) => {} // codomain 2-torsion left 𝔽_p; the prime-field walk stops honestly
713            GuidedWalk::SplitAt(_) => panic!("chose a non-split neighbour"),
714        }
715    }
716
717    #[test]
718    fn the_recursive_chain_finds_a_split_and_the_guided_walk_confirms_it() {
719        let p = big("103");
720        // Search the space of six-point configurations for one whose (2,2)-graph harbours a split, then
721        // confirm the recursive walk finds a path to it and the guided walk reproduces that split exactly.
722        let mut s = 0x9e3779b97f4a7c15u64;
723        let mut next = || {
724            s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
725            ((s >> 33) % 100 + 1) as i64
726        };
727        let mut confirmed = false;
728        for _ in 0..400 {
729            let mut vals = [0i64; 6];
730            for v in &mut vals {
731                *v = next();
732            }
733            // distinct Weierstrass points only (a smooth genus-2 curve)
734            if (0..6).any(|i| (i + 1..6).any(|j| vals[i] == vals[j])) {
735                continue;
736            }
737            let roots: [BigInt; 6] = core::array::from_fn(|i| ib(vals[i]));
738            if let Some(path) = richelot_chain(&roots, 0, &p) {
739                // The recursive search returned a neighbour it certifies as split — re-run it head-on.
740                assert_eq!(path.len(), 1, "a depth-0 walk reports a single-step chain");
741                assert!(
742                    richelot(&richelot_partitions(&roots, &p)[path[0]], &p).is_split(),
743                    "the returned neighbour is genuinely split"
744                );
745                assert_eq!(
746                    guided_chain(&roots, &path, &p),
747                    GuidedWalk::SplitAt(0),
748                    "following the guidance path reproduces the split"
749                );
750                confirmed = true;
751                break;
752            }
753        }
754        assert!(confirmed, "split-bearing configurations exist and both walks agree on them");
755    }
756
757    #[test]
758    fn a_guided_path_off_the_split_ends_or_gets_stuck_but_never_lies() {
759        let p = big("103");
760        let roots = [ib(2), ib(9), ib(11), ib(30), ib(41), ib(58)];
761        // A two-step guided walk either ends on a surface, splits, or honestly reports it left 𝔽_p — it never
762        // silently fabricates a continuation.
763        match guided_chain(&roots, &[3, 1], &p) {
764            GuidedWalk::Ended(final_roots) => {
765                assert!(final_roots.iter().all(|r| !r.is_negative()), "final points are field elements");
766            }
767            GuidedWalk::SplitAt(step) => assert!(step < 2, "a split is reported within the path"),
768            GuidedWalk::Stuck(step) => assert!(step < 2, "leaving 𝔽_p is reported at a real step"),
769        }
770    }
771
772    #[test]
773    fn even_sextic_glues_two_elliptic_curves_into_a_split_jacobian() {
774        let p = big("103"); // 103 ≡ 3 (mod 4), so fp_sqrt works
775        // g(u) = (u−1)(u−2)(u−3) = u³ − 6u² + 11u − 6, low→high [−6, 11, −6, 1].
776        let g = [ib(-6), ib(11), ib(-6), ib(1)];
777        let sg = split_jacobian_from_cubic(&g, &p);
778        assert_eq!(sg.sextic.len(), 7, "C is a sextic y² = g(x²)");
779        assert_eq!(sg.e2, vec![ib(1), rp(&ib(-6), &p), rp(&ib(11), &p), rp(&ib(-6), &p)], "E₂ is the reversed cubic");
780
781        // Validation 1 — the two degree-2 quotient maps land on E₁ and E₂ for every point of C.
782        let mut points = 0;
783        for x in 0..103i64 {
784            let xb = ib(x);
785            let fx = poly_eval(&sg.sextic, &xb, &p);
786            let Some(y) = fp_sqrt(&fx, &p) else { continue };
787            points += 1;
788            // ψ₁: (x, y) ↦ (x², y) onto E₁: Y² = g(u).
789            let u = mm(&xb, &xb, &p);
790            assert_eq!(mm(&y, &y, &p), poly_eval(&sg.e1, &u, &p), "ψ₁(P) lies on E₁");
791            // ψ₂: (x, y) ↦ (1/x², y/x³) onto E₂: Y² = ĝ(u).
792            if x != 0 {
793                let xi = mod_inverse(&xb, &p).unwrap();
794                let v = mm(&xi, &xi, &p);
795                let yv = mm(&y, &mm(&xi, &mm(&xi, &xi, &p), &p), &p);
796                assert_eq!(mm(&yv, &yv, &p), poly_eval(&sg.e2, &v, &p), "ψ₂(P) lies on E₂");
797            }
798        }
799        assert!(points > 5, "C has genuine rational points to validate the maps on");
800
801        // Validation 2 — the Richelot ±-splitting Gᵢ = x² − rᵢ (rᵢ the roots 1,2,3 of g) is reducible: its
802        // coefficient matrix has an all-zero x-column ⟹ δ = 0 ⟹ is_split. The gluing and the split-test agree.
803        let split = [[ib(-1), ib(0), ib(1)], [ib(-2), ib(0), ib(1)], [ib(-3), ib(0), ib(1)]];
804        let r = richelot(&split, &p);
805        assert_eq!(r.delta, ib(0), "the ±-splitting has δ = 0");
806        assert!(r.is_split(), "even sextic ⟹ Richelot-reducible ⟹ Jac(C) ~ E₁ × E₂ (a genuine (2,2)-split)");
807    }
808
809    #[test]
810    fn split_jacobian_order_equals_the_product_of_its_elliptic_quotients() {
811        // The strongest validation of the gluing: count #Jac(C) independently (genus-2 L-polynomial from the
812        // point counts over 𝔽_p and 𝔽_{p²}) and confirm it equals #E₁·#E₂ (Tate). This is the certificate the
813        // GENERAL matched-pair gluing will reuse.
814        let p = big("103");
815        let g = [ib(-6), ib(11), ib(-6), ib(1)]; // (u−1)(u−2)(u−3)
816        let sg = split_jacobian_from_cubic(&g, &p);
817        let e1 = count_curve_fp(&sg.e1, &p, 1) as i128; // #E₁ (cubic ⟹ 1 point at infinity)
818        let e2 = count_curve_fp(&sg.e2, &p, 1) as i128; // #E₂
819        let jac = genus2_jacobian_order(&sg.sextic, &p);
820        assert_eq!(jac, e1 * e2, "#Jac(C) = #E₁·#E₂ — the split, verified at the level of point counts");
821        // Sanity: the elliptic quotients are genuine curves (Hasse bound).
822        let bound = 2 * (103f64.sqrt() as i128) + 2;
823        assert!((e1 - 104).abs() <= bound && (e2 - 104).abs() <= bound, "#Eᵢ obey the Hasse bound");
824    }
825
826    #[test]
827    fn general_matched_pair_gluing_validates_by_jacobian_order() {
828        // The GENERAL (2,2)-gluing: two curves sharing matched 2-torsion {−c₁,−c₂,−c₃}, differing in the
829        // fourth quartic root (∓2). The Jacobian-order certificate judges the derivation.
830        let p = big("103");
831        for c in [[ib(3), ib(7), ib(20)], [ib(1), ib(5), ib(9)], [ib(4), ib(11), ib(30)]] {
832            let g = glue_shared_2torsion(&c, &p);
833            assert_eq!(g.sextic.len(), 7, "C is a genus-2 sextic");
834            assert_eq!(g.e1.len(), 5, "E₁ is a quartic model");
835            let e1 = count_curve_fp(&g.e1, &p, 2) as i128; // monic quartic ⟹ 2 points at infinity
836            let e2 = count_curve_fp(&g.e2, &p, 2) as i128;
837            let jac = genus2_jacobian_order(&g.sextic, &p);
838            assert_eq!(jac, e1 * e2, "#Jac(C) = #E₁·#E₂ for c = {c:?} — matched-pair gluing verified (Tate)");
839        }
840        // E₁ and E₂ genuinely differ (the fourth root ∓2 makes them a nontrivial pair, not one curve twice).
841        let g = glue_shared_2torsion(&[ib(3), ib(7), ib(20)], &p);
842        assert_ne!(g.e1, g.e2, "the two elliptic quotients are distinct (they differ in the fourth root)");
843    }
844
845    #[test]
846    fn the_split_test_is_the_per_digit_oracle() {
847        let p = big("103");
848        // The CONSISTENT branch: a matched-pair gluing. Its sextic ∏(x²+cᵢx+1) has roots in reciprocal pairs
849        // {r, 1/r}; pairing them is a (2,2)-splitting with δ = 0 ⟹ the surface SPLITS (reducible).
850        let recip = |r: i64| (ib(r), mod_inverse(&ib(r), &p).unwrap());
851        let (a0, a1) = recip(2);
852        let (b0, b1) = recip(5);
853        let (c0, c1) = recip(7);
854        let consistent = [a0, a1, b0, b1, c0, c1];
855        assert!(surface_is_reducible(&consistent, &p), "the glued surface splits — the correct digit");
856
857        // Discrimination: a generic configuration is an indecomposable genus-2 Jacobian — the oracle returns
858        // false. (Roots must be SCATTERED: an arithmetic progression is always reducible, since its symmetric
859        // pairing has equal pair-sums ⟹ collinear ⟹ δ = 0. That structure is itself a reducibility witness.)
860        let mut inconsistent: Option<[BigInt; 6]> = None;
861        for seed in 1..200i64 {
862            let vals: Vec<i64> = (0..6).map(|i| (seed * 41 + i * i * 7 + i * 3) % 101 + 1).collect();
863            if (0..6).any(|i| (i + 1..6).any(|j| vals[i] == vals[j])) {
864                continue; // need six distinct Weierstrass points
865            }
866            let rs: [BigInt; 6] = core::array::from_fn(|i| ib(vals[i]));
867            if !surface_is_reducible(&rs, &p) {
868                inconsistent = Some(rs);
869                break;
870            }
871        }
872        let inconsistent = inconsistent.expect("a generic (irreducible) branch exists");
873        assert!(!surface_is_reducible(&inconsistent, &p), "a wrong branch does not split — the oracle prunes it");
874
875        // Wired as the per-digit selector: among the branches, the split-test picks out the consistent one.
876        let branches = [inconsistent.clone(), consistent.clone(), inconsistent];
877        let sel = select_splitting_branch(&branches, &p).expect("the oracle selects a splitting branch");
878        assert_eq!(sel, 1, "the split-test selects exactly the consistent (gluing) branch");
879        assert!(surface_is_reducible(&branches[sel], &p), "and the selected branch genuinely splits");
880    }
881
882    #[test]
883    fn genus2_jacobian_cantor_arithmetic_is_a_group_of_order_hash_jac() {
884        // C: y² = x(x−1)(x−2)(x−3)(x−4) over 𝔽₁₀₃ — a genus-2 imaginary hyperelliptic curve (deg f = 5).
885        let p = big("103");
886        let lin = |r: i64| vec![sm(&ib(0), &ib(r), &p), ib(1)]; // x − r
887        let mut f = vec![ib(1)];
888        for r in [0, 1, 2, 3, 4] {
889            f = pmul(&f, &lin(r), &p);
890        }
891        assert_eq!(pdeg(&f), 5, "a quintic ⟹ genus 2");
892
893        // A rational point P = (x₀, y₀), y₀ ≠ 0, as the divisor class [P − ∞] = (x − x₀, y₀).
894        let d = (0..103i64)
895            .find_map(|x0| {
896                let fx = poly_eval(&f, &ib(x0), &p);
897                fp_sqrt(&fx, &p).filter(|y| !y.is_zero()).map(|y0| Mumford {
898                    u: vec![sm(&ib(0), &ib(x0), &p), ib(1)],
899                    v: vec![y0],
900                })
901            })
902            .expect("a rational non-Weierstrass point on C");
903
904        let id = jac_identity();
905        // Group axioms.
906        assert_eq!(cantor_add(&d, &id, &f, &p), d, "D + 0 = D");
907        assert_eq!(cantor_add(&d, &jac_negate(&d, &p), &f, &p), id, "D + (−D) = 0");
908        let d2 = cantor_add(&d, &d, &f, &p);
909        assert_eq!(
910            cantor_add(&d2, &d, &f, &p),
911            cantor_add(&d, &d2, &f, &p),
912            "associativity: (2D)+D = D+(2D)"
913        );
914        // Every computed class is a valid Mumford pair: u | (f − v²), deg u ≤ 2.
915        assert!(pdeg(&d2.u) <= 2, "reduced class has deg u ≤ 2");
916        assert_eq!(pmod(&psub(&f, &pmul(&d2.v, &d2.v, &p), &p), &d2.u, &p), vec![ib(0)], "u | (f − v²)");
917
918        // THE definitive validator: the group order kills every class. #Jac counted independently via the
919        // genus-2 L-polynomial (point counts over 𝔽_p and 𝔽_{p²}, one point at infinity for deg f = 5).
920        let pu = 103i128;
921        let n1 = count_curve_fp(&f, &p, 1) as i128;
922        let n2 = count_curve_fp2(&f, &p, 1) as i128;
923        let (t1, t2) = ((pu + 1) - n1, (pu * pu + 1) - n2);
924        let jac = 1 + pu * pu - (1 + pu) * t1 + (t1 * t1 - t2) / 2;
925        assert!(jac > 0, "#Jac is a positive integer");
926        assert_eq!(
927            jac_scalar_mul(jac as u128, &d, &f, &p),
928            id,
929            "#Jac · D = 0 — Cantor's law is a genuine group of exactly the order the L-polynomial predicts"
930        );
931    }
932
933    #[test]
934    fn richelot_two_two_kernel_and_two_power_torsion() {
935        // C: y² = x(x−1)(x−2)(x−3)(x−4) over 𝔽₁₀₃.
936        let p = big("103");
937        let lin = |r: i64| vec![sm(&ib(0), &ib(r), &p), ib(1)]; // x − r
938        let mut f = vec![ib(1)];
939        for r in [0, 1, 2, 3, 4] {
940            f = pmul(&f, &lin(r), &p);
941        }
942        let id = jac_identity();
943        let pu = 103i128;
944        let n1 = count_curve_fp(&f, &p, 1) as i128;
945        let n2 = count_curve_fp2(&f, &p, 1) as i128;
946        let (t1, t2) = ((pu + 1) - n1, (pu * pu + 1) - n2);
947        let jac = (1 + pu * pu - (1 + pu) * t1 + (t1 * t1 - t2) / 2) as u128;
948
949        // ── The Richelot kernel: a (2,2)-subgroup of Jac(C)[2] from Weierstrass-difference divisors. ──
950        // D₁ = [W₀ − ∞] = (x, 0), D₂ = [W₁ − ∞] = (x−1, 0) — the classes a (2,2)-isogeny quotients by.
951        let d1 = Mumford { u: lin(0), v: vec![ib(0)] };
952        let d2 = Mumford { u: lin(1), v: vec![ib(0)] };
953        assert_eq!(jac_element_order(&d1, jac, &f, &p), 2, "D₁ is 2-torsion (a Weierstrass point)");
954        assert_eq!(jac_element_order(&d2, jac, &f, &p), 2, "D₂ is 2-torsion");
955        let d3 = cantor_add(&d1, &d2, &f, &p);
956        assert_eq!(jac_element_order(&d3, jac, &f, &p), 2, "D₁+D₂ is 2-torsion");
957        assert_ne!(d3, id, "D₁, D₂ are independent");
958        // {0, D₁, D₂, D₁+D₂} is a closed order-4 (2,2)-subgroup — a genuine Richelot isogeny kernel.
959        assert_eq!(cantor_add(&d1, &d3, &f, &p), d2, "closed: D₁+(D₁+D₂) = D₂");
960        assert_eq!(cantor_add(&d2, &d3, &f, &p), d1, "closed: D₂+(D₁+D₂) = D₁");
961
962        // ── The 2^e-torsion: the substrate the surface isogeny's (2^e,2^e)-kernel lives in. ──
963        let odd = {
964            let mut m = jac;
965            while m % 2 == 0 {
966                m /= 2;
967            }
968            m
969        };
970        // Strip the odd part of #Jac off a generic class to land in the 2-power torsion; find one of order > 1.
971        let two_power = (5..103i64)
972            .find_map(|x0| {
973                let y = fp_sqrt(&poly_eval(&f, &ib(x0), &p), &p).filter(|y| !y.is_zero())?;
974                let dp = jac_scalar_mul(odd, &Mumford { u: lin(x0), v: vec![y] }, &f, &p);
975                (dp != id).then_some(dp)
976            })
977            .expect("a class with nontrivial 2-power torsion");
978        let e_ord = jac_element_order(&two_power, jac, &f, &p);
979        assert!(e_ord.is_power_of_two() && e_ord >= 2, "genuine 2^e-torsion, e ≥ 1: order {e_ord}");
980        assert_eq!(jac_scalar_mul(e_ord, &two_power, &f, &p), id, "and 2^e kills it");
981        // This 2^e-torsion element, paired with an independent one, spans the (2^e,2^e) lattice the surface
982        // isogeny's kernel is a maximal isotropic subgroup of — the structure the image-determined kernel picks.
983    }
984
985    #[test]
986    fn richelot_dual_is_genuinely_isogenous_equal_jacobian_order() {
987        // Three generic monic quadratics ⟹ a genus-2 curve C: y² = G₁G₂G₃, and the Richelot dual
988        // C': y² = δ⁻¹·H₁H₂H₃. If richelot()'s dual construction is a genuine (2,2)-isogeny, then by Tate the
989        // Jacobians have EQUAL order over 𝔽_p — a validation the map itself would have to respect.
990        let p = big("103");
991        for g in [
992            [[ib(1), ib(0), ib(1)], [ib(2), ib(1), ib(1)], [ib(5), ib(3), ib(1)]],
993            [[ib(7), ib(2), ib(1)], [ib(1), ib(4), ib(1)], [ib(3), ib(0), ib(1)]],
994        ] {
995            let r = richelot(&g, &p);
996            assert_ne!(r.delta, ib(0), "generic quadratics ⟹ δ ≠ 0 ⟹ a genuine genus-2 isogeny (not a split)");
997            let jac_c = genus2_jacobian_order_general(&r.domain, &p);
998            // The true dual carries the δ⁻¹ quadratic twist; without it the count would be off by the twist.
999            let dinv = mod_inverse(&r.delta, &p).unwrap();
1000            let cprime = pscale(&r.codomain, &dinv, &p);
1001            let jac_cp = genus2_jacobian_order_general(&cprime, &p);
1002            assert_eq!(jac_c, jac_cp, "#Jac(C) = #Jac(C') — the Richelot dual is genuinely isogenous (Tate)");
1003        }
1004    }
1005}