Weil descent attack in Python
Reduces ECDLP over extension fields of small degree to a hyperelliptic DLP.
- File
weil_descent_attack.py, 515 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/weil-descent-attack/. The authors are listed in prov/83GQX3.jsonl.
import random
from math import isqrt
# Weil descent for n = 2 in odd characteristic. K = F_(q^2) = F_q[t]/(t^2 - nu)
# with q an odd prime and nu the least non-residue; an element u + v t of K is
# the pair (u, v), and its Frobenius conjugate is (u, -v). The curve
# E: y^2 = (x^2 - s x + t0)(x - beta) over K, with s, t0 in F_q and beta not in F_q,
# is covered by the genus 2 curve
# C0: Y^2 = H(u) = -(T - 2u) G(u) over F_q, T = beta + beta^q, N = beta^(q+1),
# G(u) = (N - u^2)^2 - s (N - u^2)(T - 2u) + t0 (T - 2u)^2,
# through phi(u, Y) = ((N - u^2)/(T - 2u), Y (u - beta)/(T - 2u)^2). A point R of
# E(K) goes to psi(R) = D + D^sigma in J(F_q), where D is the class of phi^*(R)
# minus 2 infinity, and the logarithm is then found by index calculus in J(F_q).
# Divisor classes are Mumford pairs (a, b) of coefficient lists, lowest first.
def _is_prime(m: int) -> bool:
return m >= 2 and all(m % d for d in range(2, isqrt(m) + 1))
def _factor(m: int) -> list[tuple[int, int]]:
out, d = [], 2
while d * d <= m:
if m % d == 0:
e = 0
while m % d == 0:
m //= d
e += 1
out.append((d, e))
d += 1
if m > 1:
out.append((m, 1))
return out
def sqrt_mod(c: int, q: int) -> int | None:
"""A square root of c mod the odd prime q (Tonelli-Shanks), or None."""
c %= q
if c == 0:
return 0
if pow(c, (q - 1) // 2, q) != 1:
return None
m, s = q - 1, 0
while m % 2 == 0:
m, s = m // 2, s + 1
z = next(z for z in range(2, q) if pow(z, (q - 1) // 2, q) == q - 1)
e, w, t, x = s, pow(z, m, q), pow(c, m, q), pow(c, (m + 1) // 2, q)
while t != 1:
i, t2 = 0, t
while t2 != 1:
t2, i = t2 * t2 % q, i + 1
bb = pow(w, 1 << (e - i - 1), q)
e, w, t, x = i, bb * bb % q, t * bb * bb % q, x * bb % q
return x
class Fq:
"""The prime field F_q; elements are ints in [0, q)."""
def __init__(self, q: int):
self.q, self.zero, self.one = q, 0, 1
def add(self, x, y):
return (x + y) % self.q
def sub(self, x, y):
return (x - y) % self.q
def mul(self, x, y):
return x * y % self.q
def inv(self, x):
return pow(x, -1, self.q)
def lift(self, c: int):
return c % self.q
class Fq2:
"""K = F_q[t]/(t^2 - nu); elements are pairs (u, v)."""
def __init__(self, q: int, nu: int):
self.q, self.nu, self.zero, self.one = q, nu, (0, 0), (1, 0)
def add(self, x, y):
return (x[0] + y[0]) % self.q, (x[1] + y[1]) % self.q
def sub(self, x, y):
return (x[0] - y[0]) % self.q, (x[1] - y[1]) % self.q
def mul(self, x, y):
q = self.q
return (x[0] * y[0] + self.nu * x[1] * y[1]) % q, (x[0] * y[1] + x[1] * y[0]) % q
def inv(self, x):
q = self.q
n = pow((x[0] * x[0] - self.nu * x[1] * x[1]) % q, -1, q)
return x[0] * n % q, (-x[1]) * n % q
def lift(self, c: int):
return c % self.q, 0
def conj(self, x):
return x[0], (-x[1]) % self.q
def norm(self, x) -> int:
return (x[0] * x[0] - self.nu * x[1] * x[1]) % self.q
def sqrt(self, x):
q = self.q
u, v = x
if v == 0:
s = sqrt_mod(u, q)
return (s, 0) if s is not None else (0, sqrt_mod(u * pow(self.nu, -1, q), q))
n = sqrt_mod(self.norm(x), q)
if n is None:
return None
for sg in (n, q - n):
s = sqrt_mod((u + sg) * pow(2, -1, q), q)
if s:
return s, v * pow(2 * s, -1, q) % q
return None
# polynomials over a field F: lists of elements, lowest degree first, trimmed
def ptrim(F, a):
a = list(a)
while a and a[-1] == F.zero:
a.pop()
return a
def padd(F, a, b):
n = max(len(a), len(b))
return ptrim(F, [F.add(a[i] if i < len(a) else F.zero, b[i] if i < len(b) else F.zero) for i in range(n)])
def psub(F, a, b):
n = max(len(a), len(b))
return ptrim(F, [F.sub(a[i] if i < len(a) else F.zero, b[i] if i < len(b) else F.zero) for i in range(n)])
def pmul(F, a, b):
if not a or not b:
return []
out = [F.zero] * (len(a) + len(b) - 1)
for i, x in enumerate(a):
for j, y in enumerate(b):
out[i + j] = F.add(out[i + j], F.mul(x, y))
return ptrim(F, out)
def pscale(F, a, c):
return ptrim(F, [F.mul(x, c) for x in a])
def pdivmod(F, a, m):
r = list(a)
inv = F.inv(m[-1])
quo = [F.zero] * max(0, len(a) - len(m) + 1)
for i in range(len(quo) - 1, -1, -1):
c = F.mul(r[i + len(m) - 1], inv)
quo[i] = c
for j, y in enumerate(m):
r[i + j] = F.sub(r[i + j], F.mul(c, y))
return ptrim(F, quo), ptrim(F, r[: len(m) - 1])
def pxgcd(F, a, b):
"""(g, x, y) with x a + y b = g and g monic, for a, b not both zero."""
r0, r1, x0, x1, y0, y1 = a, b, [F.one], [], [], [F.one]
while r1:
qt, r = pdivmod(F, r0, r1)
r0, r1 = r1, r
x0, x1 = x1, psub(F, x0, pmul(F, qt, x1))
y0, y1 = y1, psub(F, y0, pmul(F, qt, y1))
c = F.inv(r0[-1])
return pscale(F, r0, c), pscale(F, x0, c), pscale(F, y0, c)
def peval(F, a, x):
out = F.zero
for c in reversed(a):
out = F.add(F.mul(out, x), c)
return out
# the Jacobian of Y^2 = H(u), deg H = 5, by Cantor's algorithm
def jac_add(F, H, D1, D2):
(a1, b1), (a2, b2) = D1, D2
d1, e1, e2 = pxgcd(F, a1, a2)
if len(d1) == 1: # coprime supports: d = 1, s1 = e1, s2 = e2, s3 = 0
a = pmul(F, a1, a2)
b = pdivmod(F, padd(F, pmul(F, pmul(F, e1, a1), b2), pmul(F, pmul(F, e2, a2), b1)), a)[1]
else:
d, c1, c2 = pxgcd(F, d1, padd(F, b1, b2))
s1, s2 = pmul(F, c1, e1), pmul(F, c1, e2)
a = pdivmod(F, pmul(F, a1, a2), pmul(F, d, d))[0]
num = padd(F, padd(F, pmul(F, pmul(F, s1, a1), b2), pmul(F, pmul(F, s2, a2), b1)), pmul(F, c2, padd(F, pmul(F, b1, b2), H)))
b = pdivmod(F, pdivmod(F, num, d)[0], a)[1]
while len(a) > 3:
a = pdivmod(F, psub(F, H, pmul(F, b, b)), a)[0]
a = pscale(F, a, F.inv(a[-1]))
b = pdivmod(F, ptrim(F, [F.sub(F.zero, c) for c in b]), a)[1]
return a, b
def jac_neg(F, D):
return D[0], ptrim(F, [F.sub(F.zero, c) for c in D[1]])
def jac_mul(F, H, k: int, D):
R = ([F.one], [])
while k:
if k & 1:
R = jac_add(F, H, R, D)
D = jac_add(F, H, D, D)
k >>= 1
return R
def jac_doublings(F, H, D, m: int) -> list:
"""D, 2 D, 4 D, ... with enough terms for any multiplier below m."""
out = [D]
for _ in range(max(1, m.bit_length()) - 1):
out.append(jac_add(F, H, out[-1], out[-1]))
return out
def jac_mul_table(F, H, k: int, tab: list):
R, i = ([F.one], []), 0
while k:
if k & 1:
R = jac_add(F, H, R, tab[i])
k, i = k >> 1, i + 1
return R
class WeilCurve:
"""E: y^2 = (x^2 - s x + t0)(x - beta) over F_(q^2) and its genus 2 cover C0 over F_q."""
def __init__(self, q: int, s: int, t0: int, beta: tuple[int, int]):
if q < 3 or not _is_prime(q):
raise ValueError("q must be an odd prime")
nu = next(c for c in range(2, q) if pow(c, (q - 1) // 2, q) == q - 1) if q > 3 else 2
self.q, self.F, self.K = q, Fq(q), Fq2(q, nu)
K, F = self.K, self.F
self.s, self.t0 = s % q, t0 % q
self.beta = (beta[0] % q, beta[1] % q)
if self.beta[1] == 0:
raise ValueError("beta must lie outside F_q")
if (self.s * self.s - 4 * self.t0) % q == 0:
raise ValueError("x^2 - s x + t0 has a double root")
b = self.beta
gb = K.add(K.sub(K.mul(b, b), K.mul(K.lift(self.s), b)), K.lift(self.t0))
if gb == K.zero:
raise ValueError("beta is a root of x^2 - s x + t0")
# f(x) = x^3 + a2 x^2 + a4 x + a6 over K
self.a2 = K.sub(K.lift(-self.s), b)
self.a4 = K.add(K.lift(self.t0), K.mul(K.lift(self.s), b))
self.a6 = K.mul(K.lift(-self.t0), b)
self.T, self.N = 2 * b[0] % q, K.norm(b)
T, N = self.T, self.N
m = [N, 0, q - 1] # N - u^2
l = [T, q - 2] # T - 2u
G = padd(F, padd(F, pmul(F, m, m), pscale(F, pmul(F, m, l), (-self.s) % q)), pscale(F, pmul(F, l, l), self.t0))
self.H = pscale(F, pmul(F, l, G), q - 1) # @line L1
dH = ptrim(F, [i * c % q for i, c in enumerate(self.H)][1:])
if len(self.H) != 6 or len(pxgcd(F, self.H, dH)[0]) != 1:
raise ValueError("H is not squarefree of degree 5")
self.HK = [K.lift(c) for c in self.H]
# E(K)
def f(self, x):
K = self.K
return K.add(K.mul(K.add(K.mul(K.add(x, self.a2), x), self.a4), x), self.a6)
def on_curve(self, P) -> bool:
return P is None or self.K.mul(P[1], P[1]) == self.f(P[0])
def neg(self, P):
return None if P is None else (P[0], self.K.sub(self.K.zero, P[1]))
def add(self, P, Q):
K = self.K
if P is None:
return Q
if Q is None:
return P
(x1, y1), (x2, y2) = P, Q
if x1 == x2:
if K.add(y1, y2) == K.zero:
return None
num = K.add(K.add(K.mul(K.lift(3), K.mul(x1, x1)), K.mul(K.lift(2), K.mul(self.a2, x1))), self.a4)
lam = K.mul(num, K.inv(K.add(y1, y1)))
else:
lam = K.mul(K.sub(y2, y1), K.inv(K.sub(x2, x1)))
x3 = K.sub(K.sub(K.sub(K.mul(lam, lam), self.a2), x1), x2)
return x3, K.sub(K.mul(lam, K.sub(x1, x3)), y1)
def smul(self, k: int, P):
R = None
while k:
if k & 1:
R = self.add(R, P)
P = self.add(P, P)
k >>= 1
return R
def order_e(self) -> int:
"""#E(F_(q^2)) by counting, O(q^2) operations (a toy step)."""
K, q, n = self.K, self.q, 1
for u in range(q):
for v in range(q):
z = self.f((u, v))
n += 1 if z == K.zero else (2 if pow(K.norm(z), (q - 1) // 2, q) == 1 else 0)
return n
def order_j(self) -> int:
"""#J(F_q) from #C0(F_q) and #C0(F_(q^2)) by counting (a toy step)."""
F, K, q = self.F, self.K, self.q
n1 = 1
for u in range(q):
z = peval(F, self.H, u)
n1 += 1 if z == 0 else (2 if pow(z, (q - 1) // 2, q) == 1 else 0)
n2 = 1
for u in range(q):
for v in range(q):
z = peval(K, self.HK, (u, v))
n2 += 1 if z == K.zero else (2 if pow(K.norm(z), (q - 1) // 2, q) == 1 else 0)
a1 = n1 - q - 1
a2 = (a1 * a1 - (q * q + 1 - n2)) // 2
return 1 + a1 + a2 + q * a1 + q * q
def point_order(self, P, n: int) -> int:
for r, _ in _factor(n):
while n % r == 0 and self.smul(n // r, P) is None:
n //= r
return n
def random_point(self, rng: random.Random):
while True:
x = (rng.randrange(self.q), rng.randrange(self.q))
y = self.K.sqrt(self.f(x))
if y is not None:
return (x, y) if rng.randrange(2) else self.neg((x, y))
# the descent map psi: E(K) -> J(F_q)
def phi(self, u, Y):
"""The point of E over the point (u, Y) of C0 with u in K, T - 2u != 0."""
K = self.K
d = K.sub(K.lift(self.T), K.mul(K.lift(2), u))
x = K.mul(K.sub(K.lift(self.N), K.mul(u, u)), K.inv(d))
return x, K.mul(K.mul(Y, K.sub(u, self.beta)), K.inv(K.mul(d, d)))
def psi(self, R):
K, F = self.K, self.F
if R is None or R[0] == self.beta: # @line L10
return [1], []
xr, yr = R
A = [K.sub(K.mul(xr, K.lift(self.T)), K.lift(self.N)), K.mul(K.lift(-2), xr), K.one] # @line L11
_, inv, _ = pxgcd(K, [K.sub(K.zero, self.beta), K.one], A)
l = [K.lift(self.T), K.lift(-2)]
B = pdivmod(K, pmul(K, pscale(K, pmul(K, l, l), yr), inv), A)[1]
D = (ptrim(K, A), B)
Ds = ([K.conj(c) for c in D[0]], [K.conj(c) for c in D[1]])
a, b = jac_add(K, self.HK, D, Ds) # @line L12
if any(c[1] for c in a + b):
raise AssertionError("trace not defined over F_q")
return [c[0] for c in a], [c[0] for c in b]
def factor_base(W: WeilCurve) -> dict[int, int]:
"""u0 -> Y0 for every u0 in F_q with H(u0) a square, Y0 the smaller root."""
fb = {}
for u in range(W.q):
y = sqrt_mod(peval(W.F, W.H, u), W.q)
if y is not None:
fb[u] = min(y, (-y) % W.q)
return fb
def jac_decompose(W: WeilCurve, D, fb: dict[int, int]) -> dict[int, int] | None:
"""A row e with D = sum of e_u (u - u0, Y0) if the a-polynomial of D splits, or None."""
q, F = W.q, W.F
a, b = D
if len(a) == 1:
return {}
if len(a) == 2:
u0, Y = (-a[0]) % q, peval(F, b, (-a[0]) % q)
return {u0: 1 if Y == fb[u0] else -1}
w = sqrt_mod(a[1] * a[1] - 4 * a[0], q)
if w is None:
return None
half = pow(2, -1, q)
row: dict[int, int] = {}
for u0 in sorted({(-a[1] - w) * half % q, (-a[1] + w) * half % q}):
Y = peval(F, b, u0)
row[u0] = (1 if Y == fb[u0] else -1) * (2 if w == 0 else 1)
return row
def gaudry_log(W: WeilCurve, D1, D2, r: int, nj: int, fb: dict[int, int], rng: random.Random) -> int:
"""log_D1 D2 mod r in J(F_q) by index calculus on degree one divisors.
Precondition: D1 has prime order r, r divides nj = #J(F_q) exactly once, D2 in <D1>."""
F, H = W.F, W.H
cols = sorted(fb) # @line L13
idx = {u: i for i, u in enumerate(cols)}
m, us = nj // r, cols[0]
t1, t2 = jac_doublings(F, H, D1, r), jac_doublings(F, H, D2, r)
tu = jac_doublings(F, H, jac_mul(F, H, r, ([(-us) % W.q, 1], ptrim(F, [fb[us]]))), m)
piv: dict[int, tuple[list[int], dict[int, int]]] = {}
av: list[int] = []
bv: list[int] = []
while True: # @line L14
a, b = rng.randrange(r), rng.randrange(r) # @line L15
R = jac_add(F, H, jac_add(F, H, jac_mul_table(F, H, a, t1), jac_mul_table(F, H, b, t2)), jac_mul_table(F, H, rng.randrange(m), tu))
e = jac_decompose(W, R, fb) # @line L16
if e is None: # @line L17
continue
j = len(av)
av.append(a)
bv.append(b)
v = [0] * len(cols)
for u, c in e.items():
v[idx[u]] = c % r
comb = {j: 1}
for col in range(len(cols)): # @line L18
f = v[col]
if f and col in piv:
pv, pc = piv[col]
for k in range(col, len(cols)):
if pv[k]:
v[k] = (v[k] - f * pv[k]) % r
for k, c in pc.items():
comb[k] = (comb.get(k, 0) - f * c) % r
lead = next((k for k in range(len(cols)) if v[k]), None)
if lead is None:
A = sum(c * av[k] for k, c in comb.items()) % r
C = sum(c * bv[k] for k, c in comb.items()) % r
if C: # @line L19
return (-A) * pow(C, -1, r) % r
else:
inv = pow(v[lead], -1, r) # @line L20
piv[lead] = ([c * inv % r for c in v], {k: c * inv % r for k, c in comb.items() if c})
def bsgs(W: WeilCurve, P, Q, m: int) -> int | None:
"""x in [0, m) with x P = Q in E(K) for P of order m, or None."""
s = isqrt(m - 1) + 1
table: dict = {}
T = None
for i in range(s):
table.setdefault(T, i)
T = W.add(T, P)
step, T = W.neg(W.smul(s, P)), Q
for j in range(s + 1):
if T in table:
return (j * s + table[T]) % m
T = W.add(T, step)
return None
def weil_descent_log(W: WeilCurve, P, Q, seed: int = 0, ne: int | None = None, nj: int | None = None) -> int | None:
"""The x in [0, ord P) with x P = Q in E(F_(q^2)), or None if Q is not in <P>.
The group is E(F_(q^2)), of order about q^2; a group operation costs O(1)
operations in F_(q^2), O(log(q)^2) bit operations. psi costs O(1)
operations in F_(q^2). For the prime r > q with r || ord P, r^2 not
dividing #J(F_q) and psi(P_r) != 0 (there is at most one), the index
calculus in J(F_q) makes an expected O(q) draws (heuristic: about half of
the reduced divisors split), each O(log q) Jacobian operations of O(1)
operations in F_q, and the dense elimination costs O(q^3) operations mod
r. Other prime powers r^e go to baby-step giant-step, O(r^(e/2)) group
operations. Counting the two group orders is a toy O(q^2) step.
"""
if not (W.on_curve(P) and W.on_curve(Q)):
raise ValueError("points must lie on the curve")
rng = random.Random(seed)
if ne is None:
ne = W.order_e()
n = W.point_order(P, ne)
if W.smul(n, Q) is not None: # @line L2
return None
fb: dict[int, int] | None = None
x, mod = 0, 1
for r, e in _factor(n): # @line L3
re = r**e
Pr, Qr = W.smul(n // re, P), W.smul(n // re, Q) # @line L4
big = e == 1 and r > W.q
if big:
if nj is None:
nj = W.order_j()
DP = W.psi(Pr)
if big and nj % (r * r) != 0 and DP != ([1], []): # @line L5
if fb is None:
fb = factor_base(W)
y = gaudry_log(W, DP, W.psi(Qr), r, nj, fb, rng) # @line L6
if W.smul(y, Pr) != Qr: # @line L7
return None
xr = y
else:
xr = bsgs(W, Pr, Qr, re) # @line L8
if xr is None:
return None
x += mod * ((xr - x) * pow(mod, -1, re) % re) # @line L9
mod *= re
return x % n
test_weil_descent_attack.py 333 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/weil-descent-attack/. The authors are listed in prov/83GQX3.jsonl.
import itertools
import random
import pytest
from weil_descent_attack import (
Fq,
Fq2,
WeilCurve,
_factor,
bsgs,
factor_base,
gaudry_log,
jac_add,
jac_decompose,
jac_mul,
jac_neg,
pdivmod,
peval,
pmul,
psub,
sqrt_mod,
weil_descent_log,
)
ZERO = ([1], [])
def random_curve(rng: random.Random, primes) -> WeilCurve:
while True:
q = rng.choice(primes)
try:
return WeilCurve(q, rng.randrange(q), rng.randrange(q), (rng.randrange(q), rng.randrange(1, q)))
except ValueError:
pass
def all_points(W: WeilCurve) -> list:
q, K = W.q, W.K
out = [None]
for u in range(q):
for v in range(q):
z = W.f((u, v))
for a in range(q):
for b in range(q):
if K.mul((a, b), (a, b)) == z:
out.append(((u, v), (a, b)))
return out
def brute_log(W: WeilCurve, P, Q) -> int | None:
R, k = None, 0
while True:
if R == Q:
return k
R, k = W.add(R, P), k + 1
if R is None:
return None
def reduced_divisors(W: WeilCurve) -> list:
"""Every reduced Mumford pair over F_q by brute force."""
q, F, H = W.q, W.F, W.H
out = [ZERO]
for a0 in range(q):
a = [a0, 1]
for b0 in range(q):
b = [b0] if b0 else []
if not pdivmod(F, psub(F, H, pmul(F, b, b)), a)[1]:
out.append((a, b))
for a0, a1 in itertools.product(range(q), repeat=2):
a = [a0, a1, 1]
for b0, b1 in itertools.product(range(q), repeat=2):
b = [b0, b1] if b1 else ([b0] if b0 else [])
if not pdivmod(F, psub(F, H, pmul(F, b, b)), a)[1]:
out.append((a, b))
return out
def test_square_roots():
for q in (3, 5, 7, 11, 13, 101, 307):
for c in range(q):
s = sqrt_mod(c, q)
squares = {x * x % q for x in range(q)}
assert (s is not None) == (c in squares)
if s is not None:
assert s * s % q == c
if q in (3, 5, 7, 11):
K = Fq2(q, next(c for c in range(2, q) if pow(c, (q - 1) // 2, q) == q - 1))
sq = {K.mul((a, b), (a, b)) for a in range(q) for b in range(q)}
for u in range(q):
for v in range(q):
s = K.sqrt((u, v))
assert (s is not None) == ((u, v) in sq)
if s is not None:
assert K.mul(s, s) == (u, v)
def test_cantor_against_brute_force():
rng = random.Random(1)
for q in (3, 5, 7):
for _ in range(3):
W = random_curve(rng, [q])
divs = reduced_divisors(W)
assert len(divs) == W.order_j()
keys = {(tuple(a), tuple(b)) for a, b in divs}
for _ in range(60):
A, B, C = rng.choice(divs), rng.choice(divs), rng.choice(divs)
S = jac_add(W.F, W.H, A, B)
assert (tuple(S[0]), tuple(S[1])) in keys
assert S == jac_add(W.F, W.H, B, A)
assert jac_add(W.F, W.H, S, C) == jac_add(W.F, W.H, A, jac_add(W.F, W.H, B, C))
assert jac_add(W.F, W.H, A, jac_neg(W.F, A)) == ZERO
assert jac_add(W.F, W.H, A, ZERO) == A
assert jac_mul(W.F, W.H, len(divs), A) == ZERO
def test_group_orders_agree():
rng = random.Random(2)
for _ in range(25):
W = random_curve(rng, [3, 5, 7, 11, 13, 17, 19, 23])
ne = W.order_e()
assert ne == W.order_j()
if W.q <= 7:
assert ne == len(all_points(W))
for _ in range(5):
assert W.smul(ne, W.random_point(rng)) is None
def test_cover_map_lands_on_e():
rng = random.Random(3)
for _ in range(20):
W = random_curve(rng, [5, 7, 11, 13, 17])
for u in range(W.q):
if (W.T - 2 * u) % W.q == 0:
continue
Y = sqrt_mod(peval(W.F, W.H, u), W.q)
if Y is not None:
assert W.on_curve(W.phi((u, 0), (Y, 0)))
def test_psi_is_a_homomorphism():
rng = random.Random(4)
for _ in range(30):
W = random_curve(rng, [5, 7, 11, 13, 17, 19, 23, 29])
assert W.psi(None) == ZERO
T2 = (W.beta, W.K.zero)
assert W.on_curve(T2) and W.psi(T2) == ZERO
for _ in range(10):
A, B = W.random_point(rng), W.random_point(rng)
assert W.psi(W.add(A, B)) == jac_add(W.F, W.H, W.psi(A), W.psi(B))
assert W.psi(W.neg(A)) == jac_neg(W.F, W.psi(A))
assert W.psi(W.add(A, T2)) == W.psi(A)
def test_worked_example():
W = WeilCurve(7, 0, 1, (2, 1))
assert W.K.nu == 3 and (W.T, W.N) == (4, 1)
assert W.H == [2, 0, 2, 4, 3, 2]
assert W.order_e() == W.order_j() == 52
fb = factor_base(W)
assert fb == {0: 3, 2: 0, 5: 2, 6: 1}
P = ((0, 0), (3, 1))
Q = ((0, 1), (0, 4))
assert W.point_order(P, 52) == 13 and W.smul(6, P) == Q
D1, D2 = W.psi(P), W.psi(Q)
assert D1 == ([1, 2, 1], [3, 4]) and D2 == ([5, 6, 1], [3, 2])
rows = {}
for j in range(7):
R = jac_add(W.F, W.H, jac_mul(W.F, W.H, j, D1), D2)
rows[j] = jac_decompose(W, R, fb)
assert rows == {0: {2: 1, 6: 1}, 1: {2: 1, 6: -1}, 2: {5: 2}, 3: None, 4: None, 5: {0: -2}, 6: {6: 2}}
A = (0 - 1 - 6) % 13
C = (1 - 1 - 1) % 13
assert (A, C) == (6, 12) and (-A) * pow(C, -1, 13) % 13 == 6
assert weil_descent_log(W, P, Q) == 6
def test_decompositions_are_relations():
rng = random.Random(5)
for _ in range(20):
W = random_curve(rng, [5, 7, 11, 13, 17, 19, 23])
fb = factor_base(W)
for _ in range(20):
D = W.psi(W.random_point(rng))
e = jac_decompose(W, D, fb)
if e is None:
continue
S = ZERO
for u, c in e.items():
Fu = ([(-u) % W.q, 1], [fb[u]] if fb[u] else [])
S = jac_add(W.F, W.H, S, jac_mul(W.F, W.H, abs(c), Fu if c > 0 else jac_neg(W.F, Fu)))
assert S == D
def test_gaudry_log_against_brute_force():
rng = random.Random(6)
done = 0
while done < 25:
W = random_curve(rng, [7, 11, 13, 17, 19, 23, 29, 31, 37])
nj = W.order_j()
for r, e in _factor(nj):
if e != 1 or r == 2:
continue
P = W.smul(nj // r, W.random_point(rng))
if P is None:
continue
k = rng.randrange(r)
D1, D2 = W.psi(P), W.psi(W.smul(k, P))
assert gaudry_log(W, D1, D2, r, nj, factor_base(W), rng) == k
done += 1
def check(W: WeilCurve, P, Q, ne: int, nj: int, seed: int):
assert weil_descent_log(W, P, Q, seed=seed, ne=ne, nj=nj) == brute_log(W, P, Q)
def test_random_against_brute_force():
rng = random.Random(7)
for it in range(40):
W = random_curve(rng, [5, 7, 11, 13, 17, 19, 23, 29])
ne, nj = W.order_e(), W.order_j()
P = W.random_point(rng)
n = W.point_order(P, ne)
check(W, P, W.smul(rng.randrange(n), P), ne, nj, it)
check(W, P, W.random_point(rng), ne, nj, it)
def test_trivial_targets():
rng = random.Random(8)
for it in range(15):
W = random_curve(rng, [5, 7, 11, 13, 17, 19])
ne, nj = W.order_e(), W.order_j()
P = W.random_point(rng)
n = W.point_order(P, ne)
assert weil_descent_log(W, P, None, ne=ne, nj=nj) == 0
assert weil_descent_log(W, P, P, ne=ne, nj=nj) == (1 % n)
assert weil_descent_log(W, P, W.neg(P), ne=ne, nj=nj) == (n - 1) % n
assert weil_descent_log(W, None, None, ne=ne, nj=nj) == 0
assert weil_descent_log(W, None, P, ne=ne, nj=nj) is None
def test_generators_and_non_generators():
# E(F_(q^2)) holds the full 2-torsion, so it is never cyclic; a point of
# order #E/2 generates a cyclic subgroup of the largest possible order.
rng = random.Random(9)
gens = nongens = 0
while gens < 10 or nongens < 10:
W = random_curve(rng, [5, 7, 11, 13, 17, 19, 23])
ne, nj = W.order_e(), W.order_j()
assert len([P for P in [W.random_point(rng) for _ in range(4)] if W.point_order(P, ne) == ne]) == 0
P = W.random_point(rng)
if W.point_order(P, ne) == ne // 2:
gens += 1
for _ in range(3):
check(W, P, W.random_point(rng), ne, nj, gens)
check(W, P, W.smul(rng.randrange(ne), P), ne, nj, gens)
else:
nongens += 1
for _ in range(3):
check(W, P, W.random_point(rng), ne, nj, nongens)
def test_prime_and_composite_orders():
rng = random.Random(10)
prime = composite = 0
while prime < 10 or composite < 10:
W = random_curve(rng, [7, 11, 13, 17, 19, 23, 29, 31])
ne, nj = W.order_e(), W.order_j()
R = W.random_point(rng)
r = max(p for p, _ in _factor(ne))
if r > 2 and ne % (r * r) and prime < 10:
P = W.smul(ne // r, R)
if P is not None:
prime += 1
check(W, P, W.smul(rng.randrange(r), P), ne, nj, prime)
check(W, P, W.random_point(rng), ne, nj, prime)
n = W.point_order(R, ne)
if len(_factor(n)) >= 2:
composite += 1
check(W, R, W.smul(rng.randrange(n), R), ne, nj, composite)
def test_no_solution_is_reported():
rng = random.Random(11)
seen = 0
while seen < 20:
W = random_curve(rng, [5, 7, 11, 13, 17, 19, 23])
ne, nj = W.order_e(), W.order_j()
P, Q = W.random_point(rng), W.random_point(rng)
if brute_log(W, P, Q) is None:
assert weil_descent_log(W, P, Q, ne=ne, nj=nj) is None
seen += 1
def test_larger_fields():
rng = random.Random(12)
for q in (101, 211, 307):
W = random_curve(rng, [q])
ne, nj = W.order_e(), W.order_j()
assert ne == nj
P = W.random_point(rng)
n = W.point_order(P, ne)
k = rng.randrange(n)
Q = W.smul(k, P)
assert weil_descent_log(W, P, Q, seed=q, ne=ne, nj=nj) == k
if q == 101:
assert bsgs(W, P, Q, n) == k
def test_seed_independence():
rng = random.Random(13)
W = random_curve(rng, [23])
ne, nj = W.order_e(), W.order_j()
P = W.random_point(rng)
Q = W.smul(rng.randrange(W.point_order(P, ne)), P)
assert len({weil_descent_log(W, P, Q, seed=s, ne=ne, nj=nj) for s in range(6)}) == 1
def test_bad_input():
with pytest.raises(ValueError):
WeilCurve(9, 1, 1, (0, 1))
with pytest.raises(ValueError):
WeilCurve(7, 1, 1, (2, 0))
with pytest.raises(ValueError):
WeilCurve(7, 2, 1, (0, 1))
W = WeilCurve(7, 0, 1, (2, 1))
with pytest.raises(ValueError):
weil_descent_log(W, ((0, 0), (0, 0)), None)
assert Fq(7).inv(3) == 5
What the next level needs
- the tests did not run in this build