Elliptic curve factorization method in Python
Lenstra's method computes [k]P on random elliptic curves mod N and finds a prime factor p when a point order mod p is smooth. Its cost depends on p, about L_p[1/2, 2^(1/2)] heuristically.
- File
elliptic_curve_factorization.py, 214 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/elliptic-curve-factorization/. The authors are listed in prov/FMGN8V.jsonl.
import random
from math import gcd, isqrt
from typing import Optional
def primes_up_to(b: int) -> list[int]:
"""The primes p <= b, by the sieve of Eratosthenes in O(b log log b) time."""
if b < 2:
return []
sieve = bytearray([1]) * (b + 1)
sieve[0] = sieve[1] = 0
for i in range(2, isqrt(b) + 1):
if sieve[i]:
sieve[i * i :: i] = bytearray(len(range(i * i, b + 1, i)))
return [i for i in range(b + 1) if sieve[i]]
Point = Optional[tuple[int, int]] # None is the point at infinity O
class Found(Exception):
"""Raised when an inverse mod n does not exist; g = gcd(d, n) > 1."""
def __init__(self, g: int) -> None:
super().__init__(g)
self.g = g
def inverse(d: int, n: int) -> int:
"""d^(-1) mod n, or raise Found(gcd(d, n)) when d is not invertible."""
g = gcd(d, n)
if g != 1:
raise Found(g) # @line L6
return pow(d, -1, n)
def add(p: Point, q: Point, a: int, n: int) -> Point:
"""The sum of two points of y^2 = x^3 + a x + b over Z/nZ, by the chord and
tangent rules. The cases are told apart modulo n; a denominator that is not
invertible raises Found. Cost: O(1) multiplications and one inverse mod n."""
if p is None:
return q
if q is None:
return p
x1, y1 = p
x2, y2 = q
if (x1 - x2) % n:
lam = (y2 - y1) * inverse(x2 - x1, n) % n
elif (y1 + y2) % n == 0:
return None
else:
lam = (3 * x1 * x1 + a) * inverse(y1 + y2, n) % n
x3 = (lam * lam - x1 - x2) % n
return x3, (lam * (x1 - x3) - y1) % n
def multiply(p: Point, m: int, a: int, n: int) -> Point:
"""[m]p for m >= 1, by left to right double and add with O(log m) additions."""
r = p
for bit in bin(m)[3:]:
r = add(r, r, a, n)
if bit == "1":
r = add(r, p, a, n)
return r
def ecm(n: int, b1: int, b2: int, curves: int, seed: int = 0) -> int:
"""Lenstra's elliptic curve method with a stage two up to b2.
Precondition: n > 1 odd, gcd(n, 6) = 1, b2 >= b1 >= 2. Returns a proper factor
of n, or 1 if no curve finds one. Each curve costs O(b1 + pi(b2) + G) point
additions, where G is the largest gap between primes in (b1, b2], and each
addition costs O(1) multiplications and one inverse mod n."""
rng = random.Random(seed)
small = primes_up_to(b1)
large = [q for q in primes_up_to(b2) if q > b1]
gap = max([v - u for u, v in zip(large, large[1:])], default=2)
for _ in range(curves): # @line L1
x, y, a = rng.randrange(n), rng.randrange(n), rng.randrange(n) # @line L2
b = (y * y - x * x * x - a * x) % n
g = gcd(4 * a * a * a + 27 * b * b, n)
if g == n: # @line L3
continue
if g > 1:
return g
try:
p: Point = (x, y)
for q in small: # @line L4
qe = q
while qe <= b1:
p = multiply(p, q, a, n)
qe *= q
if p is None:
break
if p is None or not large:
continue
r = multiply(p, large[0], a, n) # @line L5
table: list[Point] = [None, add(p, p, a, n)] # table[j] = [2j]p
while len(table) <= gap // 2:
table.append(add(table[-1], table[1], a, n))
for i in range(1, len(large)):
if r is None:
break
r = add(r, table[(large[i] - large[i - 1]) // 2], a, n)
except Found as f: # @line L6
if f.g < n:
return f.g
return 1 # @line L7
_BASES = (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41)
def is_probable_prime(n: int) -> bool:
"""Strong probable-prime test to the 13 prime bases up to 41.
It is a proof of primality for n < 3317044064679887385961981 (about 3.3 * 10^24)."""
if n < 2:
return False
for p in _BASES:
if n % p == 0:
return n == p
d, s = n - 1, 0
while d % 2 == 0:
d, s = d // 2, s + 1
for a in _BASES:
x = pow(a, d, n)
if x in (1, n - 1):
continue
for _ in range(s - 1):
x = x * x % n
if x == n - 1:
break
else:
return False
return True
_PRIMES: list[int] = primes_up_to(1 << 12)
_PRIME_CAP = 1 << 22
def _primes_to(b: int) -> list[int]:
"""A cached list of primes that covers every prime up to min(b, 2^22)."""
global _PRIMES
b = min(b, _PRIME_CAP)
if _PRIMES[-1] < b:
_PRIMES = primes_up_to(max(b, 2 * _PRIMES[-1]))
return _PRIMES
def iroot(m: int, k: int) -> int:
"""The integer k-th root floor(m^(1/k)) of m >= 0, by Newton's method."""
if m < 2:
return m
x = 1 << ((m.bit_length() + k - 1) // k)
while True:
y = ((k - 1) * x + m // x ** (k - 1)) // k
if y >= x:
return x
x = y
def _split(m: int) -> int:
"""A proper factor of a composite m with no prime factor below 1000.
A perfect power r^k gives r. Otherwise ECM runs with B1 doubling from 100 and
B2 = 50 B1, 20 curves per round. Trial division is the last resort."""
for k in range(2, m.bit_length() + 1):
r = iroot(m, k)
if r ** k == m:
return r
b1 = 100
for rnd in range(14):
g = ecm(m, b1, 50 * b1, 20, seed=rnd)
if g > 1:
return g
b1 *= 2
d = 1001
while m % d:
d += 2
return d
_SMALL = primes_up_to(1000)
def factorize(n: int) -> list[int]:
"""Prime factors of n >= 1 in non-decreasing order, with multiplicity.
Trial division removes the primes below 1000, then each composite part is split
by ECM. A loop with an explicit stack replaces recursion."""
if n < 1:
raise ValueError("n must be a positive integer")
out: list[int] = []
for d in _SMALL:
if d * d > n:
break
while n % d == 0:
out.append(d)
n //= d
stack = [n]
while stack:
m = stack.pop()
if m == 1:
continue
if is_probable_prime(m):
out.append(m)
continue
g = _split(m)
stack.extend((g, m // g))
return sorted(out)
test_elliptic_curve_factorization.py 200 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/elliptic-curve-factorization/. The authors are listed in prov/FMGN8V.jsonl.
import math
import random
from elliptic_curve_factorization import Found, add, ecm, factorize, iroot, multiply, primes_up_to
import pytest
CARMICHAEL = [561, 1105, 1729, 2465, 2821, 6601, 8911, 10585, 15841, 29341, 41041, 46657,
52633, 62745, 63973, 75361, 101101, 115921, 126217, 162401, 172081, 188461,
252601, 278545, 294409, 314821, 334153, 340561, 399001, 410041, 449065,
488881, 512461, 3215031751]
def is_prime(n: int) -> bool:
"""Deterministic Miller-Rabin for n < 3.3 * 10^24, written here independently."""
assert n < 3317044064679887385961981
if n < 2:
return False
small = (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41)
for p in small:
if n % p == 0:
return n == p
d, s = n - 1, 0
while d % 2 == 0:
d, s = d // 2, s + 1
for a in small:
x = pow(a, d, n)
if x in (1, n - 1):
continue
for _ in range(s - 1):
x = x * x % n
if x == n - 1:
break
else:
return False
return True
def random_prime(rng: random.Random, bits: int) -> int:
while True:
p = rng.getrandbits(bits) | (1 << (bits - 1)) | 1
if is_prime(p):
return p
def check(n: int) -> list[int]:
f = factorize(n)
assert math.prod(f) == n
assert all(is_prime(p) for p in f)
assert f == sorted(f)
return f
def brute_count(a: int, b: int, p: int) -> int:
"""The number of points of y^2 = x^3 + a x + b over F_p, O included."""
squares = [0] * p
for y in range(p):
squares[y * y % p] += 1
return 1 + sum(squares[(x * x * x + a * x + b) % p] for x in range(p))
def test_example() -> None:
p, q = 1129, 2029
n = p * q
assert n == 2290741
x, y, a = 2, 3, 1
b = (y * y - x**3 - a * x) % n
assert b == n - 1 and math.gcd(4 * a**3 + 27 * b * b, n) == 1
assert brute_count(a, b % p, p) == 1120 and brute_count(a, b % q, q) == 2083
pt = (x, y)
for r in (2, 2, 2, 2, 3, 3, 5):
pt = multiply(pt, r, a, n)
assert pt == (1115156, 449422)
two = add(pt, pt, a, n)
three = add(two, pt, a, n)
six = add(three, three, a, n)
assert six == (2200125, 2229695)
assert (pt[0] - six[0]) % n == 1205772 and math.gcd(1205772, n) == p
with pytest.raises(Found) as e:
add(six, pt, a, n)
assert e.value.g == p
# Over F_p the point P has order 560 = 2^4 5 7; over F_q it has the prime order 2083.
mod_p = (x, y)
for _ in range(558):
mod_p = add(mod_p, (x, y), a, p)
assert mod_p is not None
assert add(mod_p, (x, y), a, p) is None
assert multiply((x, y), 2083, a, q) is None
def test_group_law_mod_prime() -> None:
rng = random.Random(7)
for p in (101, 307, 1009):
for _ in range(5):
a, x, y = rng.randrange(p), rng.randrange(p), rng.randrange(p)
b = (y * y - x**3 - a * x) % p
if (4 * a**3 + 27 * b * b) % p == 0:
continue
order = brute_count(a, b, p)
assert abs(order - (p + 1)) <= 2 * math.isqrt(p) + 1
assert multiply((x, y), order, a, p) is None
u = multiply((x, y), 5, a, p)
v = multiply((x, y), 7, a, p)
w = multiply((x, y), 11, a, p)
assert add(add(u, v, a, p), w, a, p) == add(u, add(v, w, a, p), a, p)
assert add(u, v, a, p) == multiply((x, y), 12, a, p)
def test_returns_divisor() -> None:
rng = random.Random(11)
for _ in range(200):
n = rng.randrange(10**6, 10**9) | 1
if n % 3 == 0:
continue
g = ecm(n, 50, 500, 3, seed=rng.randrange(1000))
assert g == 1 or (1 < g < n and n % g == 0)
for p in (1000003, 2**31 - 1, 2**61 - 1):
assert ecm(p, 200, 2000, 5) == 1
def test_ecm_splits_small() -> None:
# Every composite n < 40000 with no prime factor below 100 and not a perfect power.
count = 0
for n in range(10201, 40001, 2):
if any(n % p == 0 for p in range(3, 101, 2)) or is_prime(n):
continue
if math.isqrt(n) ** 2 == n:
continue
g = ecm(n, 30, 300, 40, seed=n)
assert 1 < g < n and n % g == 0, n
count += 1
assert count > 400
def test_edges() -> None:
with pytest.raises(ValueError):
factorize(0)
assert factorize(1) == []
assert factorize(2) == [2]
assert factorize(3) == [3]
assert factorize(2**61 - 1) == [2**61 - 1]
assert factorize(2**40) == [2] * 40
assert factorize(101**2) == [101, 101]
assert factorize(101**3) == [101] * 3
assert factorize(1000003**2) == [1000003, 1000003]
assert factorize(10007**3 * 10009) == [10007] * 3 + [10009]
assert factorize(2**67 - 1) == [193707721, 761838257287]
assert iroot(10**30, 3) == 10**10 and iroot(10**30 - 1, 3) == 10**10 - 1
def test_all_small() -> None:
for n in range(1, 100001):
f = factorize(n)
assert math.prod(f) == n
assert all(is_prime(p) for p in f)
def test_carmichael() -> None:
for n in CARMICHAEL + [2199733160881]:
check(n)
def test_random_semiprimes() -> None:
rng = random.Random(1403)
for bits in range(4, 31):
for _ in range(2):
p, q = random_prime(rng, bits), random_prime(rng, bits)
assert check(p * q) == sorted([p, q])
def test_small_factor_of_large_number() -> None:
rng = random.Random(77)
for bits in (16, 20, 24):
p = random_prime(rng, bits)
big = random_prime(rng, 80)
assert check(p * big) == sorted([p, big])
def test_close_primes() -> None:
p = 1000003
for _ in range(6):
q = p + 2
while not is_prime(q):
q += 2
assert check(p * q) == [p, q]
p = q
def test_random_smooth() -> None:
rng = random.Random(2047)
primes = [p for p in range(101, 3000) if is_prime(p)]
for k in range(1, 9):
f = sorted(rng.choice(primes) for _ in range(k))
assert check(math.prod(f)) == f
assert primes_up_to(30) == [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
What the next level needs
- the tests did not run in this build
Read the full page about Elliptic curve factorization method