1use crate::factor::mod_inverse;
14use logicaffeine_base::BigInt;
15
16pub type Fp2 = (BigInt, BigInt);
18
19#[inline]
20fn ib(x: i64) -> BigInt {
21 BigInt::from_i64(x)
22}
23
24#[inline]
25fn rem_pos(a: &BigInt, p: &BigInt) -> BigInt {
26 let r = a.div_rem(p).map(|(_, r)| r).unwrap_or_else(|| a.clone());
27 if r.is_negative() {
28 r.add(p)
29 } else {
30 r
31 }
32}
33
34#[inline]
35fn m(a: &BigInt, b: &BigInt, p: &BigInt) -> BigInt {
36 rem_pos(&a.mul(b), p)
37}
38#[inline]
39fn s(a: &BigInt, b: &BigInt, p: &BigInt) -> BigInt {
40 rem_pos(&a.sub(b), p)
41}
42#[inline]
43fn a_(a: &BigInt, b: &BigInt, p: &BigInt) -> BigInt {
44 rem_pos(&a.add(b), p)
45}
46
47pub fn fp2_const(k: i64, p: &BigInt) -> Fp2 {
49 (rem_pos(&ib(k), p), ib(0))
50}
51
52pub fn fp2_add(x: &Fp2, y: &Fp2, p: &BigInt) -> Fp2 {
53 (a_(&x.0, &y.0, p), a_(&x.1, &y.1, p))
54}
55pub fn fp2_sub(x: &Fp2, y: &Fp2, p: &BigInt) -> Fp2 {
56 (s(&x.0, &y.0, p), s(&x.1, &y.1, p))
57}
58pub fn fp2_neg(x: &Fp2, p: &BigInt) -> Fp2 {
59 (s(&ib(0), &x.0, p), s(&ib(0), &x.1, p))
60}
61
62pub fn fp2_mul(x: &Fp2, y: &Fp2, p: &BigInt) -> Fp2 {
64 let ac = m(&x.0, &y.0, p);
65 let bd = m(&x.1, &y.1, p);
66 let ad = m(&x.0, &y.1, p);
67 let bc = m(&x.1, &y.0, p);
68 (s(&ac, &bd, p), a_(&ad, &bc, p))
69}
70
71pub fn fp2_inv(x: &Fp2, p: &BigInt) -> Option<Fp2> {
73 let norm = a_(&m(&x.0, &x.0, p), &m(&x.1, &x.1, p), p);
74 let ninv = mod_inverse(&norm, p)?;
75 Some((m(&x.0, &ninv, p), m(&s(&ib(0), &x.1, p), &ninv, p)))
76}
77
78pub fn fp2_is_zero(x: &Fp2) -> bool {
79 x.0.is_zero() && x.1.is_zero()
80}
81
82fn phi2_in_y(j: &Fp2, p: &BigInt) -> [Fp2; 4] {
88 let j2 = fp2_mul(j, j, p);
89 let j3 = fp2_mul(&j2, j, p);
90 let k = |c: i64| fp2_const(c, p);
91 let c3 = k(1);
92 let c2 = fp2_sub(&fp2_add(&fp2_neg(&j2, p), &fp2_mul(&k(1488), j, p), p), &k(162000), p);
94 let c1 = fp2_add(
96 &fp2_add(&fp2_mul(&k(1488), &j2, p), &fp2_mul(&k(40773375), j, p), p),
97 &k(8_748_000_000),
98 p,
99 );
100 let c0 = fp2_sub(
102 &fp2_add(&fp2_sub(&j3, &fp2_mul(&k(162000), &j2, p), p), &fp2_mul(&k(8_748_000_000), j, p), p),
103 &k(157_464_000_000_000),
104 p,
105 );
106 [c0, c1, c2, c3]
107}
108
109fn poly_eval(coeffs: &[Fp2], y: &Fp2, p: &BigInt) -> Fp2 {
111 let mut acc = fp2_const(0, p);
112 for c in coeffs.iter().rev() {
113 acc = fp2_add(&fp2_mul(&acc, y, p), c, p);
114 }
115 acc
116}
117
118fn poly_div_linear(coeffs: &[Fp2], r: &Fp2, p: &BigInt) -> (Vec<Fp2>, Fp2) {
120 let mut q = vec![fp2_const(0, p); coeffs.len().saturating_sub(1)];
121 let mut carry = fp2_const(0, p);
122 for k in (0..coeffs.len()).rev() {
123 let cur = fp2_add(&coeffs[k], &fp2_mul(&carry, r, p), p);
124 if k == 0 {
125 return (q, cur); }
127 q[k - 1] = cur.clone();
128 carry = cur;
129 }
130 (q, fp2_const(0, p))
131}
132
133fn fp2_elements(p: &BigInt) -> Vec<Fp2> {
135 let pu = p.to_i64().expect("small prime") as i64;
136 let mut v = Vec::new();
137 for a in 0..pu {
138 for b in 0..pu {
139 v.push((ib(a), ib(b)));
140 }
141 }
142 v
143}
144
145fn cubic_roots(coeffs: &[Fp2; 4], p: &BigInt) -> Vec<(Fp2, usize)> {
149 let mut out = Vec::new();
150 for y in fp2_elements(p) {
151 if fp2_is_zero(&poly_eval(coeffs, &y, p)) {
152 let mut poly: Vec<Fp2> = coeffs.to_vec();
154 let mut mult = 0usize;
155 loop {
156 let (q, rem) = poly_div_linear(&poly, &y, p);
157 if !fp2_is_zero(&rem) {
158 break;
159 }
160 mult += 1;
161 poly = q;
162 if poly.len() <= 1 {
163 break;
164 }
165 }
166 out.push((y, mult));
167 }
168 }
169 out
170}
171
172#[derive(Clone, Debug)]
175pub struct SsVertex {
176 pub j: Fp2,
177 pub neighbors: Vec<Fp2>,
178}
179
180pub fn supersingular_graph(p: &BigInt) -> Option<Vec<SsVertex>> {
185 if rem_pos(p, &ib(4)) != ib(3) {
186 return None;
187 }
188 let start = fp2_const(1728, p);
189 let mut visited: Vec<Fp2> = Vec::new();
190 let mut queue: Vec<Fp2> = vec![start];
191 let mut graph: Vec<SsVertex> = Vec::new();
192 while let Some(j) = queue.pop() {
193 if visited.iter().any(|v| v == &j) {
194 continue;
195 }
196 visited.push(j.clone());
197 let mut neighbors = Vec::new();
198 for (root, mult) in cubic_roots(&phi2_in_y(&j, p), p) {
199 for _ in 0..mult {
200 neighbors.push(root.clone());
201 }
202 if !visited.iter().any(|v| v == &root) && !queue.iter().any(|v| v == &root) {
203 queue.push(root);
204 }
205 }
206 graph.push(SsVertex { j, neighbors });
207 }
208 Some(graph)
209}
210
211#[derive(Clone, Debug, PartialEq, Eq)]
220pub struct Curve2 {
221 pub a: Fp2,
222 pub b: Fp2,
223 pub p: BigInt,
224}
225
226#[derive(Clone, Debug, PartialEq, Eq)]
228pub enum Point2 {
229 Infinity,
230 Affine(Fp2, Fp2),
231}
232
233impl Curve2 {
234 pub fn new(a: Fp2, b: Fp2, p: BigInt) -> Curve2 {
235 Curve2 { a, b, p }
236 }
237
238 pub fn is_on_curve(&self, pt: &Point2) -> bool {
239 match pt {
240 Point2::Infinity => true,
241 Point2::Affine(x, y) => {
242 let lhs = fp2_mul(y, y, &self.p);
243 let x3 = fp2_mul(&fp2_mul(x, x, &self.p), x, &self.p);
244 let rhs = fp2_add(&fp2_add(&x3, &fp2_mul(&self.a, x, &self.p), &self.p), &self.b, &self.p);
245 lhs == rhs
246 }
247 }
248 }
249
250 pub fn negate(&self, pt: &Point2) -> Point2 {
251 match pt {
252 Point2::Infinity => Point2::Infinity,
253 Point2::Affine(x, y) => Point2::Affine(x.clone(), fp2_neg(y, &self.p)),
254 }
255 }
256
257 pub fn add(&self, p1: &Point2, p2: &Point2) -> Point2 {
258 match (p1, p2) {
259 (Point2::Infinity, _) => p2.clone(),
260 (_, Point2::Infinity) => p1.clone(),
261 (Point2::Affine(x1, y1), Point2::Affine(x2, y2)) => {
262 if x1 == x2 {
263 if fp2_is_zero(&fp2_add(y1, y2, &self.p)) {
264 return Point2::Infinity; }
266 return self.double(p1); }
268 let den = fp2_inv(&fp2_sub(x2, x1, &self.p), &self.p).expect("distinct x invertible");
269 let lam = fp2_mul(&fp2_sub(y2, y1, &self.p), &den, &self.p);
270 let x3 = fp2_sub(&fp2_sub(&fp2_mul(&lam, &lam, &self.p), x1, &self.p), x2, &self.p);
271 let y3 = fp2_sub(&fp2_mul(&lam, &fp2_sub(x1, &x3, &self.p), &self.p), y1, &self.p);
272 Point2::Affine(x3, y3)
273 }
274 }
275 }
276
277 pub fn double(&self, pt: &Point2) -> Point2 {
278 match pt {
279 Point2::Infinity => Point2::Infinity,
280 Point2::Affine(x, y) => {
281 if fp2_is_zero(y) {
282 return Point2::Infinity;
283 }
284 let num = fp2_add(&fp2_mul(&fp2_const(3, &self.p), &fp2_mul(x, x, &self.p), &self.p), &self.a, &self.p);
285 let den = fp2_inv(&fp2_mul(&fp2_const(2, &self.p), y, &self.p), &self.p).expect("2y invertible");
286 let lam = fp2_mul(&num, &den, &self.p);
287 let x3 = fp2_sub(&fp2_mul(&lam, &lam, &self.p), &fp2_mul(&fp2_const(2, &self.p), x, &self.p), &self.p);
288 let y3 = fp2_sub(&fp2_mul(&lam, &fp2_sub(x, &x3, &self.p), &self.p), y, &self.p);
289 Point2::Affine(x3, y3)
290 }
291 }
292 }
293
294 pub fn mul(&self, k: &BigInt, pt: &Point2) -> Point2 {
296 let mut result = Point2::Infinity;
297 let mut addend = pt.clone();
298 let (_, bytes) = k.to_le_bytes();
299 for byte in bytes {
300 for bit in 0..8 {
301 if (byte >> bit) & 1 == 1 {
302 result = self.add(&result, &addend);
303 }
304 addend = self.double(&addend);
305 }
306 }
307 result
308 }
309
310 pub fn j_invariant(&self) -> Option<Fp2> {
312 let a3 = fp2_mul(&fp2_mul(&self.a, &self.a, &self.p), &self.a, &self.p);
313 let four_a3 = fp2_mul(&fp2_const(4, &self.p), &a3, &self.p);
314 let disc = fp2_add(&four_a3, &fp2_mul(&fp2_const(27, &self.p), &fp2_mul(&self.b, &self.b, &self.p), &self.p), &self.p);
315 if fp2_is_zero(&disc) {
316 return None;
317 }
318 Some(fp2_mul(&fp2_mul(&fp2_const(1728, &self.p), &four_a3, &self.p), &fp2_inv(&disc, &self.p)?, &self.p))
319 }
320}
321
322fn fp_sqrt(a: &BigInt, p: &BigInt) -> Option<BigInt> {
326 let ap = rem_pos(a, p);
327 if ap.is_zero() {
328 return Some(ib(0));
329 }
330 let exp = p.add(&ib(1)).div_rem(&ib(4))?.0;
331 let r = crate::factor::modpow(&ap, &exp, p);
332 (m(&r, &r, p) == ap).then_some(r)
333}
334
335pub fn fp2_sqrt(alpha: &Fp2, p: &BigInt) -> Option<Fp2> {
338 let (a, b) = (&alpha.0, &alpha.1);
339 if b.is_zero() {
340 if let Some(r) = fp_sqrt(a, p) {
341 return Some((r, ib(0)));
342 }
343 return fp_sqrt(&s(&ib(0), a, p), p).map(|r| (ib(0), r)); }
345 let norm = a_(&m(a, a, p), &m(b, b, p), p);
346 let sn = fp_sqrt(&norm, p)?;
347 let inv2 = crate::factor::mod_inverse(&ib(2), p)?;
348 for signed in [sn.clone(), s(&ib(0), &sn, p)] {
349 let t = m(&a_(a, &signed, p), &inv2, p); if let Some(x) = fp_sqrt(&t, p) {
351 if x.is_zero() {
352 continue;
353 }
354 let y = m(b, &crate::factor::mod_inverse(&m(&ib(2), &x, p), p)?, p);
355 let cand = (x, y);
356 if fp2_mul(&cand, &cand, p) == *alpha {
357 return Some(cand);
358 }
359 }
360 }
361 None
362}
363
364fn points_on_curve2(c: &Curve2) -> Vec<Point2> {
367 let pu = c.p.to_i64().expect("small prime");
368 let mut v = vec![Point2::Infinity];
369 for xa in 0..pu {
370 for xb in 0..pu {
371 let x = (ib(xa), ib(xb));
372 let x3 = fp2_mul(&fp2_mul(&x, &x, &c.p), &x, &c.p);
373 let rhs = fp2_add(&fp2_add(&x3, &fp2_mul(&c.a, &x, &c.p), &c.p), &c.b, &c.p);
374 if fp2_is_zero(&rhs) {
375 v.push(Point2::Affine(x, fp2_const(0, &c.p)));
376 } else if let Some(y) = fp2_sqrt(&rhs, &c.p) {
377 v.push(Point2::Affine(x.clone(), fp2_neg(&y, &c.p)));
378 v.push(Point2::Affine(x, y));
379 }
380 }
381 }
382 v
383}
384
385pub fn point_of_order2(c: &Curve2, ell: u64) -> Option<Point2> {
387 points_on_curve2(c).into_iter().find(|pt| *pt != Point2::Infinity && c.mul(&ib(ell as i64), pt) == Point2::Infinity)
388}
389
390pub fn torsion_basis2(c: &Curve2, n: u64) -> Option<(Point2, Point2)> {
393 let onp: Vec<Point2> = points_on_curve2(c)
394 .into_iter()
395 .filter(|pt| *pt != Point2::Infinity && c.mul(&ib(n as i64), pt) == Point2::Infinity)
396 .collect();
397 for p in &onp {
398 let span: Vec<Point2> = (0..n).map(|k| c.mul(&ib(k as i64), p)).collect();
399 if let Some(q) = onp.iter().find(|q| !span.contains(q)) {
400 return Some((p.clone(), q.clone()));
401 }
402 }
403 None
404}
405
406#[derive(Clone, Debug)]
408pub struct Isogeny2 {
409 pub domain: Curve2,
410 pub codomain: Curve2,
411 pub degree: u64,
412 kernel: Vec<(Fp2, Fp2, Fp2)>, }
414
415impl Isogeny2 {
416 pub fn from_kernel(curve: &Curve2, gen: &Point2, ell: u64) -> Option<Isogeny2> {
417 if ell < 3 || ell % 2 == 0 {
418 return None;
419 }
420 let p = &curve.p;
421 let mut kernel = Vec::new();
422 let mut cur = gen.clone();
423 for _ in 0..((ell - 1) / 2) {
424 match &cur {
425 Point2::Affine(xq, yq) => {
426 let vq = fp2_add(
427 &fp2_mul(&fp2_const(6, p), &fp2_mul(xq, xq, p), p),
428 &fp2_mul(&fp2_const(2, p), &curve.a, p),
429 p,
430 );
431 let uq = fp2_mul(&fp2_const(4, p), &fp2_mul(yq, yq, p), p);
432 kernel.push((xq.clone(), vq, uq));
433 }
434 Point2::Infinity => return None,
435 }
436 cur = curve.add(&cur, gen);
437 }
438 if matches!(gen, Point2::Infinity) || curve.mul(&ib(ell as i64), gen) != Point2::Infinity {
439 return None;
440 }
441 let (mut vsum, mut wsum) = (fp2_const(0, p), fp2_const(0, p));
442 for (xq, vq, uq) in &kernel {
443 vsum = fp2_add(&vsum, vq, p);
444 wsum = fp2_add(&wsum, &fp2_add(uq, &fp2_mul(xq, vq, p), p), p);
445 }
446 let a2 = fp2_sub(&curve.a, &fp2_mul(&fp2_const(5, p), &vsum, p), p);
447 let b2 = fp2_sub(&curve.b, &fp2_mul(&fp2_const(7, p), &wsum, p), p);
448 Some(Isogeny2 { domain: curve.clone(), codomain: Curve2::new(a2, b2, p.clone()), degree: ell, kernel })
449 }
450
451 pub fn eval(&self, pt: &Point2) -> Point2 {
452 let p = &self.domain.p;
453 match pt {
454 Point2::Infinity => Point2::Infinity,
455 Point2::Affine(x, y) => {
456 let mut xnew = x.clone();
457 let mut yfac = fp2_const(1, p);
458 for (xq, vq, uq) in &self.kernel {
459 let d = fp2_sub(x, xq, p);
460 if fp2_is_zero(&d) {
461 return Point2::Infinity;
462 }
463 let di = fp2_inv(&d, p).expect("nonzero");
464 let di2 = fp2_mul(&di, &di, p);
465 let di3 = fp2_mul(&di2, &di, p);
466 xnew = fp2_add(&xnew, &fp2_add(&fp2_mul(vq, &di, p), &fp2_mul(uq, &di2, p), p), p);
467 let tail = fp2_add(&fp2_mul(vq, &di2, p), &fp2_mul(&fp2_mul(&fp2_const(2, p), uq, p), &di3, p), p);
468 yfac = fp2_sub(&yfac, &tail, p);
469 }
470 Point2::Affine(xnew, fp2_mul(y, &yfac, p))
471 }
472 }
473 }
474}
475
476fn mline_double2(c: &Curve2, t: &Point2, xq: &Fp2, yq: &Fp2) -> Option<(Fp2, Fp2)> {
477 let p = &c.p;
478 let (xt, yt) = match t {
479 Point2::Affine(x, y) => (x, y),
480 Point2::Infinity => return None,
481 };
482 if fp2_is_zero(yt) {
483 return None;
484 }
485 let num = fp2_add(&fp2_mul(&fp2_const(3, p), &fp2_mul(xt, xt, p), p), &c.a, p);
486 let lam = fp2_mul(&num, &fp2_inv(&fp2_mul(&fp2_const(2, p), yt, p), p)?, p);
487 let ell = fp2_sub(&fp2_sub(yq, yt, p), &fp2_mul(&lam, &fp2_sub(xq, xt, p), p), p);
488 let vert = match c.double(t) {
489 Point2::Affine(x2, _) => fp2_sub(xq, &x2, p),
490 Point2::Infinity => fp2_const(1, p),
491 };
492 Some((ell, vert))
493}
494
495fn mline_add2(c: &Curve2, t: &Point2, pp: &Point2, xq: &Fp2, yq: &Fp2) -> Option<(Fp2, Fp2)> {
496 let p = &c.p;
497 let ((xt, yt), (xpp, ypp)) = match (t, pp) {
498 (Point2::Affine(a, b), Point2::Affine(cc, d)) => ((a, b), (cc, d)),
499 _ => return None,
500 };
501 if xt == xpp {
502 if yt == ypp {
503 return mline_double2(c, t, xq, yq);
504 }
505 return Some((fp2_sub(xq, xt, p), fp2_const(1, p)));
506 }
507 let lam = fp2_mul(&fp2_sub(ypp, yt, p), &fp2_inv(&fp2_sub(xpp, xt, p), p)?, p);
508 let ell = fp2_sub(&fp2_sub(yq, yt, p), &fp2_mul(&lam, &fp2_sub(xq, xt, p), p), p);
509 let vert = match c.add(t, pp) {
510 Point2::Affine(x3, _) => fp2_sub(xq, &x3, p),
511 Point2::Infinity => fp2_const(1, p),
512 };
513 Some((ell, vert))
514}
515
516fn miller2(c: &Curve2, pp: &Point2, qq: &Point2, n: u64) -> Option<Fp2> {
517 let p = &c.p;
518 let (xq, yq) = match qq {
519 Point2::Affine(x, y) => (x, y),
520 Point2::Infinity => return None,
521 };
522 let (mut num, mut den) = (fp2_const(1, p), fp2_const(1, p));
523 let mut t = pp.clone();
524 for bit in (0..(64 - n.leading_zeros() - 1)).rev() {
525 let (ell, vert) = mline_double2(c, &t, xq, yq)?;
526 num = fp2_mul(&fp2_mul(&num, &num, p), &ell, p);
527 den = fp2_mul(&fp2_mul(&den, &den, p), &vert, p);
528 t = c.double(&t);
529 if (n >> bit) & 1 == 1 {
530 let (ell, vert) = mline_add2(c, &t, pp, xq, yq)?;
531 num = fp2_mul(&num, &ell, p);
532 den = fp2_mul(&den, &vert, p);
533 t = c.add(&t, pp);
534 }
535 }
536 if fp2_is_zero(&den) {
537 return None;
538 }
539 Some(fp2_mul(&num, &fp2_inv(&den, p)?, p))
540}
541
542pub fn weil_pairing2(c: &Curve2, pp: &Point2, qq: &Point2, n: u64) -> Option<Fp2> {
544 if pp == qq || matches!(pp, Point2::Infinity) || matches!(qq, Point2::Infinity) {
545 return Some(fp2_const(1, &c.p));
546 }
547 let fp = miller2(c, pp, qq, n)?;
548 let fq = miller2(c, qq, pp, n)?;
549 let ratio = fp2_mul(&fp, &fp2_inv(&fq, &c.p)?, &c.p);
550 Some(if n % 2 == 1 { fp2_neg(&ratio, &c.p) } else { ratio })
551}
552
553pub fn product_weil_pairing(
558 c1: &Curve2,
559 c2: &Curve2,
560 p: (&Point2, &Point2),
561 q: (&Point2, &Point2),
562 n: u64,
563) -> Option<Fp2> {
564 let e1 = weil_pairing2(c1, p.0, q.0, n)?;
565 let e2 = weil_pairing2(c2, p.1, q.1, n)?;
566 Some(fp2_mul(&e1, &e2, &c1.p))
567}
568
569pub fn fp2_pow(alpha: &Fp2, k: u64, p: &BigInt) -> Fp2 {
571 let mut result = fp2_const(1, p);
572 let mut base = alpha.clone();
573 let mut e = k;
574 while e > 0 {
575 if e & 1 == 1 {
576 result = fp2_mul(&result, &base, p);
577 }
578 base = fp2_mul(&base, &base, p);
579 e >>= 1;
580 }
581 result
582}
583
584pub fn point_order2(c: &Curve2, pt: &Point2, bound: u64) -> Option<u64> {
588 let mut cur = pt.clone();
589 for k in 1..=bound {
590 if cur == Point2::Infinity {
591 return Some(k);
592 }
593 cur = c.add(&cur, pt);
594 }
595 None
596}
597
598pub fn kernel_generator2(c: &Curve2, p_pt: &Point2, q_pt: &Point2, s: &BigInt) -> Point2 {
600 c.add(p_pt, &c.mul(s, q_pt))
601}
602
603pub fn full_order_basis2(c: &Curve2, ell: u64, a: u32) -> Option<(Point2, Point2)> {
606 let n = ell.pow(a);
607 let full: Vec<Point2> = points_on_curve2(c)
608 .into_iter()
609 .filter(|pt| point_order2(c, pt, n + 1) == Some(n))
610 .collect();
611 for p in &full {
612 let span: Vec<Point2> = (0..n).map(|k| c.mul(&ib(k as i64), p)).collect();
613 if let Some(q) = full.iter().find(|q| !span.contains(q)) {
614 return Some((p.clone(), q.clone()));
615 }
616 }
617 None
618}
619
620#[derive(Clone, Debug, PartialEq)]
622pub struct IsogenyStep2 {
623 pub domain: Curve2,
624 pub kernel: Point2,
625 pub codomain: Curve2,
626}
627
628pub fn derive_isogeny_path2(c: &Curve2, gen: &Point2, ell: u64, a: u32) -> Option<Vec<IsogenyStep2>> {
631 let mut steps = Vec::with_capacity(a as usize);
632 let mut e = c.clone();
633 let mut g = gen.clone();
634 for step in 0..a {
635 let mut mult = ib(1);
636 for _ in 0..(a - 1 - step) {
637 mult = mult.mul(&ib(ell as i64));
638 }
639 let k = e.mul(&mult, &g);
640 let iso = Isogeny2::from_kernel(&e, &k, ell)?;
641 steps.push(IsogenyStep2 { domain: e.clone(), kernel: k, codomain: iso.codomain.clone() });
642 g = iso.eval(&g);
643 e = iso.codomain;
644 }
645 Some(steps)
646}
647
648pub fn push_through2(steps: &[IsogenyStep2], ell: u64, pt: &Point2) -> Option<Point2> {
650 let mut cur = pt.clone();
651 for step in steps {
652 cur = Isogeny2::from_kernel(&step.domain, &step.kernel, ell)?.eval(&cur);
653 }
654 Some(cur)
655}
656
657pub fn recover_secret2(
662 e0: &Curve2,
663 basis_a: (&Point2, &Point2),
664 ell: u64,
665 a: u32,
666 basis_b: (&Point2, &Point2),
667 images: (&Point2, &Point2),
668) -> Option<(BigInt, Point2, Vec<IsogenyStep2>)> {
669 let n = ell.pow(a);
670 for s in 0..n {
671 let sb = ib(s as i64);
672 let gen = kernel_generator2(e0, basis_a.0, basis_a.1, &sb);
673 if point_order2(e0, &gen, n + 1) != Some(n) {
674 continue;
675 }
676 let Some(path) = derive_isogeny_path2(e0, &gen, ell, a) else { continue };
677 match (push_through2(&path, ell, basis_b.0), push_through2(&path, ell, basis_b.1)) {
678 (Some(ip), Some(iq)) if &ip == images.0 && &iq == images.1 => return Some((sb, gen, path)),
679 _ => {}
680 }
681 }
682 None
683}
684
685#[allow(clippy::too_many_arguments)]
689fn walk_keyspace_tree(
690 e0: &Curve2,
691 ba: (&Point2, &Point2),
692 ell: u64,
693 a: u32,
694 n: u64,
695 bb: (&Point2, &Point2),
696 images: (&Point2, &Point2),
697 digits: &mut Vec<u64>,
698) -> Option<(BigInt, Point2)> {
699 if digits.len() as u32 == a {
700 let (mut s, mut place) = (ib(0), ib(1));
701 for &d in digits.iter() {
702 s = s.add(&place.mul(&ib(d as i64)));
703 place = place.mul(&ib(ell as i64));
704 }
705 let gen = kernel_generator2(e0, ba.0, ba.1, &s);
706 if point_order2(e0, &gen, n + 1) != Some(n) {
707 return None;
708 }
709 let path = derive_isogeny_path2(e0, &gen, ell, a)?;
710 let (ip, iq) = (push_through2(&path, ell, bb.0)?, push_through2(&path, ell, bb.1)?);
711 return (&ip == images.0 && &iq == images.1).then_some((s, gen));
712 }
713 for d in 0..ell {
714 digits.push(d);
715 if let Some(found) = walk_keyspace_tree(e0, ba, ell, a, n, bb, images, digits) {
716 return Some(found);
717 }
718 digits.pop();
719 }
720 None
721}
722
723pub fn recover_secret_recursive2(
728 e0: &Curve2,
729 basis_a: (&Point2, &Point2),
730 ell: u64,
731 a: u32,
732 basis_b: (&Point2, &Point2),
733 images: (&Point2, &Point2),
734) -> Option<(Vec<u64>, BigInt, Point2)> {
735 let n = ell.pow(a);
736 let mut digits: Vec<u64> = Vec::with_capacity(a as usize);
737 let (s, gen) = walk_keyspace_tree(e0, basis_a, ell, a, n, basis_b, images, &mut digits)?;
738 Some((digits, s, gen))
739}
740
741pub fn aut_1728(p: &BigInt, pt: &Point2) -> Point2 {
745 match pt {
746 Point2::Infinity => Point2::Infinity,
747 Point2::Affine(x, y) => {
748 let i_unit = (ib(0), ib(1)); Point2::Affine(fp2_neg(x, p), fp2_mul(&i_unit, y, p))
750 }
751 }
752}
753
754pub fn keyspace_codomain_classes(
759 e0: &Curve2,
760 basis_a: (&Point2, &Point2),
761 ell: u64,
762 a: u32,
763) -> Vec<(Fp2, Vec<u64>)> {
764 let n = ell.pow(a);
765 let mut classes: Vec<(Fp2, Vec<u64>)> = Vec::new();
766 for s in 0..n {
767 let gen = kernel_generator2(e0, basis_a.0, basis_a.1, &ib(s as i64));
768 if point_order2(e0, &gen, n + 1) != Some(n) {
769 continue;
770 }
771 let Some(path) = derive_isogeny_path2(e0, &gen, ell, a) else { continue };
772 let Some(j) = path.last().and_then(|st| st.codomain.j_invariant()) else { continue };
773 match classes.iter().position(|(cj, _)| *cj == j) {
774 Some(idx) => classes[idx].1.push(s),
775 None => classes.push((j, vec![s])),
776 }
777 }
778 classes
779}
780
781fn ell_isogeny_neighbors(c: &Curve2, ell: u64) -> Vec<Curve2> {
783 let Some((r, s)) = full_order_basis2(c, ell, 1) else { return Vec::new() };
784 let mut gens: Vec<Point2> = (0..ell).map(|i| c.add(&r, &c.mul(&ib(i as i64), &s))).collect();
785 gens.push(s);
786 gens.into_iter().filter_map(|g| Isogeny2::from_kernel(c, &g, ell).map(|iso| iso.codomain)).collect()
787}
788
789pub fn isogeny_graph_distances(target: &Curve2, ell: u64, max_depth: u32) -> Vec<(Fp2, u32)> {
793 let Some(j0) = target.j_invariant() else { return Vec::new() };
794 let mut dist: Vec<(Fp2, u32)> = vec![(j0, 0)];
795 let mut frontier = vec![target.clone()];
796 for d in 1..=max_depth {
797 let mut next = Vec::new();
798 for c in &frontier {
799 for nbr in ell_isogeny_neighbors(c, ell) {
800 if let Some(j) = nbr.j_invariant() {
801 if !dist.iter().any(|(jj, _)| *jj == j) {
802 dist.push((j, d));
803 next.push(nbr);
804 }
805 }
806 }
807 }
808 if next.is_empty() {
809 break;
810 }
811 frontier = next;
812 }
813 dist
814}
815
816#[allow(clippy::too_many_arguments)]
822fn walk_pruned(
823 e0: &Curve2,
824 ba: (&Point2, &Point2),
825 ell: u64,
826 a: u32,
827 n: u64,
828 bb: (&Point2, &Point2),
829 images: (&Point2, &Point2),
830 dists: &[(Fp2, u32)],
831 digits: &mut Vec<u64>,
832 leaves: &mut usize,
833) -> Option<(BigInt, Point2)> {
834 let k = digits.len() as u32;
835 let (mut p, mut place) = (ib(0), ib(1));
836 for &d in digits.iter() {
837 p = p.add(&place.mul(&ib(d as i64)));
838 place = place.mul(&ib(ell as i64));
839 }
840 if k > 0 {
841 let gen_p = kernel_generator2(e0, ba.0, ba.1, &p);
842 let path = derive_isogeny_path2(e0, &gen_p, ell, a)?;
843 let jk = path[(k - 1) as usize].codomain.j_invariant()?;
844 match dists.iter().find(|(jj, _)| *jj == jk).map(|(_, d)| *d) {
845 Some(dk) if dk <= a - k => {} _ => return None, }
848 }
849 if k == a {
850 *leaves += 1;
851 let gen = kernel_generator2(e0, ba.0, ba.1, &p);
852 if point_order2(e0, &gen, n + 1) != Some(n) {
853 return None;
854 }
855 let path = derive_isogeny_path2(e0, &gen, ell, a)?;
856 let (ip, iq) = (push_through2(&path, ell, bb.0)?, push_through2(&path, ell, bb.1)?);
857 return (&ip == images.0 && &iq == images.1).then_some((p, gen));
858 }
859 for d in 0..ell {
860 digits.push(d);
861 if let Some(found) = walk_pruned(e0, ba, ell, a, n, bb, images, dists, digits, leaves) {
862 return Some(found);
863 }
864 digits.pop();
865 }
866 None
867}
868
869pub fn recover_secret_pruned2(
875 e0: &Curve2,
876 basis_a: (&Point2, &Point2),
877 ell: u64,
878 a: u32,
879 basis_b: (&Point2, &Point2),
880 images: (&Point2, &Point2),
881 target: &Curve2,
882) -> Option<(BigInt, Point2, usize)> {
883 let n = ell.pow(a);
884 let dists = isogeny_graph_distances(target, ell, a);
885 let mut digits: Vec<u64> = Vec::with_capacity(a as usize);
886 let mut leaves = 0usize;
887 let (s, gen) = walk_pruned(e0, basis_a, ell, a, n, basis_b, images, &dists, &mut digits, &mut leaves)?;
888 Some((s, gen, leaves))
889}
890
891#[cfg(test)]
892mod tests {
893 use super::*;
894
895 fn big(s: &str) -> BigInt {
896 BigInt::parse_decimal(s).unwrap()
897 }
898
899 #[test]
900 fn fp2_is_a_field() {
901 let p = big("103"); let one = fp2_const(1, &p);
903 let ii = (ib(0), ib(1)); assert_eq!(fp2_mul(&ii, &ii, &p), fp2_neg(&one, &p), "i² = −1");
906 let elts: Vec<Fp2> = (0..7)
908 .flat_map(|a| (0..7).map(move |b| (ib(a * 13 + 1), ib(b * 11 + 2))))
909 .collect();
910 for x in &elts {
911 for y in &elts {
912 assert_eq!(fp2_mul(x, y, &p), fp2_mul(y, x, &p), "× commutes");
913 assert_eq!(fp2_add(x, y, &p), fp2_add(y, x, &p), "+ commutes");
914 let lhs = fp2_mul(x, &fp2_add(y, &one, &p), &p);
916 let rhs = fp2_add(&fp2_mul(x, y, &p), x, &p);
917 assert_eq!(lhs, rhs, "distributive");
918 }
919 if !fp2_is_zero(x) {
920 assert_eq!(fp2_mul(x, &fp2_inv(x, &p).unwrap(), &p), one, "x·x⁻¹ = 1");
921 }
922 }
923 }
924
925 fn ss_mass(p: u64) -> u64 {
927 p / 12
928 + match p % 12 {
929 1 => 0,
930 5 | 7 => 1,
931 11 => 2,
932 _ => 0,
933 }
934 }
935
936 #[test]
937 fn supersingular_graph_has_the_right_mass_is_3_regular_and_connected() {
938 for &pu in &[11u64, 19, 23, 31, 43, 47] {
939 let p = big(&pu.to_string());
940 let g = supersingular_graph(&p).expect("p ≡ 3 (mod 4)");
941 assert_eq!(g.len() as u64, ss_mass(pu), "supersingular mass for p = {pu}");
944 for v in &g {
946 assert_eq!(v.neighbors.len(), 3, "each vertex has out-degree 3 (p = {pu})");
947 }
948 let js: Vec<&Fp2> = g.iter().map(|v| &v.j).collect();
950 for v in &g {
951 for nb in &v.neighbors {
952 assert!(js.iter().any(|j| *j == nb), "neighbour is a supersingular vertex");
953 }
954 }
955 assert!(js.iter().any(|j| **j == fp2_const(1728, &p)), "1728 is supersingular for p = {pu}");
957 }
958 }
959
960 fn embed(pt: &crate::elliptic::Point) -> Point2 {
961 match pt {
962 crate::elliptic::Point::Infinity => Point2::Infinity,
963 crate::elliptic::Point::Affine(x, y) => Point2::Affine((x.clone(), ib(0)), (y.clone(), ib(0))),
964 }
965 }
966
967 #[test]
968 fn curve2_agrees_with_the_prime_field_on_rational_points() {
969 let p = big("43");
971 let cfp = crate::elliptic::Curve::new(ib(2), ib(25), p.clone());
972 let base = crate::elliptic::point_of_order(&cfp, 5).expect("a rational 5-torsion point");
973 let c2 = Curve2::new(fp2_const(2, &p), fp2_const(25, &p), p.clone());
974 let base2 = embed(&base);
975 assert!(c2.is_on_curve(&base2), "the embedded point lies on the 𝔽_{{p²}} curve");
976 for k in 1..=5i64 {
977 let via_fp = embed(&cfp.mul(&ib(k), &base));
978 let via_fp2 = c2.mul(&ib(k), &base2);
979 assert_eq!(via_fp2, via_fp, "Curve2 matches Curve on {k}·P");
980 }
981 }
982
983 fn all_points2(c: &Curve2) -> Vec<Point2> {
985 let pu = c.p.to_i64().unwrap();
986 let mut v = vec![Point2::Infinity];
987 for xa in 0..pu {
988 for xb in 0..pu {
989 let x = (ib(xa), ib(xb));
990 for ya in 0..pu {
991 for yb in 0..pu {
992 let pt = Point2::Affine(x.clone(), (ib(ya), ib(yb)));
993 if c.is_on_curve(&pt) {
994 v.push(pt);
995 }
996 }
997 }
998 }
999 }
1000 v
1001 }
1002
1003 #[test]
1004 fn supersingular_curve_over_fp2_carries_full_torsion() {
1005 let p = big("11");
1008 let c = Curve2::new(fp2_const(1, &p), fp2_const(0, &p), p.clone());
1009 assert_eq!(c.j_invariant().unwrap(), fp2_const(1728, &p), "j(y²=x³+x) = 1728, a supersingular vertex");
1010 let pts = all_points2(&c);
1011 assert_eq!(pts.len(), 144, "#E(𝔽_{{p²}}) = (p+1)² = 12²");
1012 for a in pts.iter().take(6) {
1014 assert_eq!(c.add(a, &c.negate(a)), Point2::Infinity, "P + (−P) = O");
1015 for b in pts.iter().take(6) {
1016 for d in pts.iter().take(6) {
1017 assert_eq!(c.add(&c.add(a, b), d), c.add(a, &c.add(b, d)), "associative");
1018 }
1019 }
1020 }
1021 let e3 = pts.iter().filter(|pt| c.mul(&ib(3), pt) == Point2::Infinity).count();
1023 assert_eq!(e3, 9, "E[3] ≅ (ℤ/3)² is fully rational over 𝔽_{{p²}}");
1024 }
1025
1026 #[test]
1027 fn fp2_sqrt_round_trips() {
1028 let p = big("59");
1029 for (a, b) in [(3i64, 7), (1, 0), (0, 5), (11, 2), (23, 41)] {
1030 let x = (ib(a), ib(b));
1031 let sq = fp2_mul(&x, &x, &p);
1032 let r = fp2_sqrt(&sq, &p).expect("a square has a root");
1033 assert_eq!(fp2_mul(&r, &r, &p), sq, "(√α)² = α");
1034 }
1035 }
1036
1037 #[test]
1038 fn torsion_image_certificate_lifts_to_fp2_at_sike_scale() {
1039 let p = big("59");
1043 let c = Curve2::new(fp2_const(1, &p), fp2_const(0, &p), p.clone());
1044 let kernel = point_of_order2(&c, 3).expect("3-torsion over 𝔽_{p²}");
1045 let iso = Isogeny2::from_kernel(&c, &kernel, 3).expect("Vélu over 𝔽_{p²}");
1046 let (pp, qq) = torsion_basis2(&c, 5).expect("full 5-torsion basis over 𝔽_{p²}");
1047 let ep = weil_pairing2(&c, &pp, &qq, 5).expect("basis pairing");
1048 assert_ne!(ep, fp2_const(1, &p), "independent basis ⟹ a nontrivial (primitive 5th root) pairing");
1049 let (fp_, fq_) = (iso.eval(&pp), iso.eval(&qq));
1050 assert!(iso.codomain.is_on_curve(&fp_) && iso.codomain.is_on_curve(&fq_), "images land on the codomain");
1051 let eq = weil_pairing2(&iso.codomain, &fp_, &fq_, 5).expect("image pairing");
1052 assert_eq!(eq, fp2_pow(&ep, iso.degree, &p), "e_5(φP,φQ) = e_5(P,Q)^{{deg φ}} over 𝔽_{{p²}}");
1053 }
1054
1055 fn sidh_scale_setup() -> (BigInt, Curve2, (Point2, Point2), (Point2, Point2)) {
1058 let p = big("107");
1059 let e0 = Curve2::new(fp2_const(1, &p), fp2_const(0, &p), p.clone());
1060 let basis_a = full_order_basis2(&e0, 3, 3).expect("rank-2 E[3³] basis");
1061 let basis_b = full_order_basis2(&e0, 2, 2).expect("rank-2 E[2²] basis");
1062 (p, e0, basis_a, basis_b)
1063 }
1064
1065 #[test]
1066 fn images_reconstruct_the_secret_generator_at_sidh_scale() {
1067 let (_p, e0, (pa, qa), (pb, qb)) = sidh_scale_setup();
1068 let secret = ib(7);
1070 let gen = kernel_generator2(&e0, &pa, &qa, &secret);
1071 assert_eq!(point_order2(&e0, &gen, 28), Some(27), "the secret kernel generates order 3³");
1072 let path = derive_isogeny_path2(&e0, &gen, 3, 3).expect("the secret 3³-isogeny");
1073 let images =
1074 (push_through2(&path, 3, &pb).expect("push P_B"), push_through2(&path, 3, &qb).expect("push Q_B"));
1075 let (rs, rgen, rpath) =
1077 recover_secret2(&e0, (&pa, &qa), 3, 3, (&pb, &qb), (&images.0, &images.1)).expect("recovery");
1078 assert_eq!(push_through2(&rpath, 3, &pb).unwrap(), images.0, "recovered chain reproduces φ(P_B)");
1082 assert_eq!(push_through2(&rpath, 3, &qb).unwrap(), images.1, "recovered chain reproduces φ(Q_B)");
1083 assert_eq!(rpath.last().unwrap().codomain, path.last().unwrap().codomain, "and lands on the target E");
1084 assert_eq!(kernel_generator2(&e0, &pa, &qa, &rs), rgen, "the reported scalar yields the reported gen");
1085 let _ = gen; }
1087
1088 #[test]
1089 fn the_structured_tree_recovery_matches_the_flat_oracle() {
1090 let (_p, e0, (pa, qa), (pb, qb)) = sidh_scale_setup();
1091 let secret = ib(11);
1092 let gen = kernel_generator2(&e0, &pa, &qa, &secret);
1093 let path = derive_isogeny_path2(&e0, &gen, 3, 3).unwrap();
1094 let images = (push_through2(&path, 3, &pb).unwrap(), push_through2(&path, 3, &qb).unwrap());
1095 let (digits, rs, rgen) =
1097 recover_secret_recursive2(&e0, (&pa, &qa), 3, 3, (&pb, &qb), (&images.0, &images.1)).expect("tree");
1098 assert_eq!(digits.len(), 3, "one ℓ-adic digit per isogeny step");
1099 assert!(digits.iter().all(|&d| d < 3), "each digit lies in 0..ℓ");
1100 let s_check: i64 = digits.iter().enumerate().map(|(k, &d)| d as i64 * 3i64.pow(k as u32)).sum();
1102 assert_eq!(rs, ib(s_check), "the reported scalar is the ℓ-adic digit reconstruction");
1103 let rpath = derive_isogeny_path2(&e0, &rgen, 3, 3).unwrap();
1105 assert_eq!(push_through2(&rpath, 3, &pb).unwrap(), images.0, "the recovered generator reproduces φ(P_B)");
1106 assert_eq!(push_through2(&rpath, 3, &qb).unwrap(), images.1, "and φ(Q_B)");
1107 assert_eq!(rpath.last().unwrap().codomain, path.last().unwrap().codomain, "and lands on the target E");
1108 let _ = secret; }
1110
1111 #[test]
1112 fn the_keyspace_partitions_into_aut_orbits_so_one_recovery_yields_the_orbit() {
1113 let (p, e0, (pa, qa), _b) = sidh_scale_setup();
1114 let classes = keyspace_codomain_classes(&e0, (&pa, &qa), 3, 3);
1116 let total: usize = classes.iter().map(|(_, ss)| ss.len()).sum();
1117 assert_eq!(total, 27, "the classes partition the whole keyspace");
1118 assert!(classes.len() < 27, "the keyspace COLLAPSES — kernels share codomains ⟹ nontrivial symmetry");
1119 for s in 0..27u64 {
1123 let gen = kernel_generator2(&e0, &pa, &qa, &ib(s as i64));
1124 let j_gen = derive_isogeny_path2(&e0, &gen, 3, 3).unwrap().last().unwrap().codomain.j_invariant();
1125 let igen = aut_1728(&p, &gen);
1126 assert!(e0.is_on_curve(&igen), "ι(K) lands back on E₀");
1127 assert_eq!(point_order2(&e0, &igen, 28), Some(27), "ι preserves the order-27 kernel");
1128 let j_igen = derive_isogeny_path2(&e0, &igen, 3, 3).unwrap().last().unwrap().codomain.j_invariant();
1129 assert_eq!(j_gen, j_igen, "ι(K) shares K's codomain j — the orbit shares its target curve");
1130 }
1131 }
1132
1133 #[test]
1134 fn the_graph_reachability_oracle_prunes_the_tree_to_the_aut_orbit() {
1135 let (_p, e0, (pa, qa), (pb, qb)) = sidh_scale_setup();
1136 let secret = ib(11);
1137 let gen = kernel_generator2(&e0, &pa, &qa, &secret);
1138 let path = derive_isogeny_path2(&e0, &gen, 3, 3).unwrap();
1139 let target = path.last().unwrap().codomain.clone(); let images = (push_through2(&path, 3, &pb).unwrap(), push_through2(&path, 3, &qb).unwrap());
1141
1142 let dists = isogeny_graph_distances(&target, 3, 3);
1144 assert!(dists.len() > 1, "the ℓ-isogeny graph around E has genuine structure (a Ramanujan expander)");
1145 assert!(dists.iter().any(|(_, d)| *d == 0), "E sits at distance 0 from itself");
1146
1147 let (_rs, rgen, leaves) =
1150 recover_secret_pruned2(&e0, (&pa, &qa), 3, 3, (&pb, &qb), (&images.0, &images.1), &target)
1151 .expect("pruned recovery");
1152 let rpath = derive_isogeny_path2(&e0, &rgen, 3, 3).unwrap();
1153 assert_eq!(push_through2(&rpath, 3, &pb).unwrap(), images.0, "pruned recovery reproduces φ(P_B)");
1154 assert_eq!(push_through2(&rpath, 3, &qb).unwrap(), images.1, "and φ(Q_B)");
1155 assert_eq!(rpath.last().unwrap().codomain, target, "and lands on the target E");
1156
1157 let classes = keyspace_codomain_classes(&e0, (&pa, &qa), 3, 3);
1160 let tj = target.j_invariant().unwrap();
1161 let orbit_size = classes.iter().find(|(j, _)| *j == tj).map(|(_, ss)| ss.len()).unwrap();
1162 assert!(orbit_size < 27, "the keyspace collapses to a strictly smaller orbit");
1163 assert!(leaves <= orbit_size, "the graph oracle leaf-tests only the target's Aut-orbit, not all 27");
1164 }
1165}