Special number field sieve in Python
The number field sieve for integers of a special form such as 2^k - 1 or 2^k + 1, where a polynomial with tiny coefficients exists. Heuristically L_N[1/3, (32/9)^(1/3)].
- File
special_number_field_sieve.py, 528 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/special-number-field-sieve/. The authors are listed in prov/89BD3K.jsonl.
from math import gcd, isqrt
# Polynomials are lists of integers, lowest degree first. An element of
# Z[alpha] = Z[x]/(f) for a monic f of degree d is a list of d integers.
# The sieve is the same as in the general number field sieve; what is special
# is the polynomial, which comes from the form of n.
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]]
def iroot(m: int, e: int) -> int:
"""The integer e-th root floor(m^(1/e)) of m >= 0, by Newton's method."""
if m < 2:
return m
x = 1 << ((m.bit_length() + e - 1) // e)
while True:
y = ((e - 1) * x + m // x ** (e - 1)) // e
if y >= x:
return x
x = y
def evaluate(f: list[int], x: int, mod: int = 0) -> int:
"""f(x), or f(x) mod `mod` if mod > 0, by Horner's rule."""
v = 0
for c in reversed(f):
v = v * x + c
if mod:
v %= mod
return v
def homogeneous(f: list[int], a: int, b: int) -> int:
"""F(a, b) = b^d f(a/b), the norm of a - b alpha for a monic f."""
d = len(f) - 1
return sum(c * a**i * b ** (d - i) for i, c in enumerate(f))
def derivative(f: list[int]) -> list[int]:
return [i * c for i, c in enumerate(f)][1:]
def mulmod(u: list[int], v: list[int], f: list[int], mod: int = 0) -> list[int]:
"""u v in Z[x]/(f), with coefficients reduced modulo `mod` if mod > 0.
Precondition: f is monic of degree d, and u, v have length d."""
d = len(f) - 1
w = [0] * (2 * d - 1)
for i, x in enumerate(u):
if x:
for j, y in enumerate(v):
w[i + j] += x * y
for k in range(2 * d - 2, d - 1, -1):
c = w[k]
if c:
for j in range(d):
w[k - d + j] -= c * f[j]
w = w[:d]
return [x % mod for x in w] if mod else w
def powmod(u: list[int], e: int, f: list[int], mod: int) -> list[int]:
"""u^e in (Z/mod)[x]/(f), by binary exponentiation."""
d = len(f) - 1
r = [1] + [0] * (d - 1)
while e:
if e & 1:
r = mulmod(r, u, f, mod)
u = mulmod(u, u, f, mod)
e >>= 1
return r
def is_inert(f: list[int], p: int) -> bool:
"""True if the monic f of prime degree d is irreducible modulo the prime p.
For a prime d, f is irreducible modulo p if and only if it has no root
modulo p and x^(p^d) = x in F_p[x]/(f)."""
d = len(f) - 1
if any(evaluate(f, r, p) == 0 for r in range(p)):
return False
x = [0, 1] + [0] * (d - 2)
return powmod(x, p**d, f, p) == x
def dependencies(vectors: list[int]) -> list[int]:
"""Subsets (as bit masks over the rows) whose vectors add to zero over GF(2),
by Gaussian elimination on bit masks. One subset per linearly dependent row."""
pivots: dict[int, tuple[int, int]] = {}
found = []
for j, v in enumerate(vectors):
h = 1 << j
while v:
top = v.bit_length() - 1
if top not in pivots:
pivots[top] = (v, h)
break
pv, ph = pivots[top]
v, h = v ^ pv, h ^ ph
if v == 0:
found.append(h)
return found
def factor_bases(f: list[int], bound: int, chars: int):
"""Rational primes p <= bound, algebraic pairs (p, r) with f(r) = 0 mod p,
and `chars` quadratic characters (q, s): primes q > bound with a simple
root s of f modulo q."""
fp = derivative(f)
primes = primes_up_to(bound)
pairs = [(p, r) for p in primes for r in range(p) if evaluate(f, r, p) == 0]
qchars: list[tuple[int, int]] = []
q = bound
while len(qchars) < chars:
q += 1
if all(q % p for p in primes_up_to(isqrt(q))):
for s in range(q):
if evaluate(f, s, q) == 0 and evaluate(fp, s, q) != 0:
qchars.append((q, s))
if len(qchars) == chars:
break
return primes, pairs, qchars
def smooth_exponents(v: int, primes: list[int]) -> list[int] | None:
"""Exponents of |v| over primes, or None if |v| is not smooth over them."""
v = abs(v)
if v == 0:
return None
e = [0] * len(primes)
for i, p in enumerate(primes):
while v % p == 0:
v //= p
e[i] += 1
if v == 1:
return e
return e if v == 1 else None
def find_relations(n, f, m, primes, pairs, need, width, max_b):
"""Pairs (a, b) with gcd(a, b) = 1, b >= 1, |a| <= width, such that a - b m
and F(a, b) are both smooth over primes. Line sieving on both sides, one b
at a time, with logarithms; candidates are confirmed by trial division."""
rels = []
size = 2 * width + 1
for b in range(1, max_b + 1): # @line L3
rat = [0] * size
alg = [0] * size
for p in primes:
lg = p.bit_length()
for i in range((b * m + width) % p, size, p):
rat[i] += lg
for p, r in pairs:
lg = p.bit_length()
for i in range((b * r + width) % p, size, p):
alg[i] += lg
slack = primes[-1].bit_length() + 4
# |a - b m| >= b m - width on the whole line, so this threshold
# passes every point that the exact test below can pass.
low = max(0, b * m - width).bit_length() - slack
for i in [i for i, s in enumerate(rat) if s >= low]:
a = i - width
u = a - b * m
v = homogeneous(f, a, b)
if u == 0 or v == 0 or gcd(a, b) != 1:
continue
if rat[i] < abs(u).bit_length() - slack or alg[i] < abs(v).bit_length() - slack:
continue
eu = smooth_exponents(u, primes) # @line L4
if eu is None:
continue
ev = smooth_exponents(v, primes)
if ev is None:
continue
rels.append((a, b, u, eu, v, ev))
if len(rels) >= need:
return rels
return rels
def relation_vector(rel, primes, pairs, index, qchars) -> int:
"""The GF(2) vector of a relation: the sign of a - b m, the rational
exponents, the sign of F(a, b), the exponents of the first degree prime ideals (p, r) with
a = b r mod p, and the quadratic characters (a - b s / q)."""
a, b, u, eu, v, ev = rel
bits = [1 if u < 0 else 0, 1 if v < 0 else 0] + [e & 1 for e in eu]
alg = [0] * len(pairs)
for p, e in zip(primes, ev):
if e:
r = a * pow(b, -1, p) % p
alg[index[(p, r)]] = e & 1
bits += alg
for q, s in qchars:
bits.append(1 if pow((a - b * s) % q, (q - 1) // 2, q) == q - 1 else 0)
return sum(c << k for k, c in enumerate(bits))
def product_in_order(elements: list[list[int]], f: list[int]) -> list[int]:
"""The product of elements of Z[alpha], by a balanced product tree."""
level = elements
while len(level) > 1:
level = [mulmod(level[i], level[i + 1], f) if i + 1 < len(level) else level[i]
for i in range(0, len(level), 2)]
return level[0]
def inert_prime(f: list[int], start: int, tries: int = 2000) -> int:
"""The least prime p > start with p = 3 mod 4 and f irreducible modulo p,
or 0 if none is found among `tries` candidates. Then F_p[x]/(f) is the
field with q = p^d elements, and q = 3 mod 4 when d is odd."""
p = start + 1
for _ in range(tries):
if p % 4 == 3 and all(p % t for t in range(2, isqrt(p) + 1)) and is_inert(f, p):
return p
p += 1
return 0
def algebraic_sqrt(gamma: list[int], f: list[int], p: int) -> list[int] | None:
"""delta in Z[alpha] with delta^2 = gamma, or None.
Precondition: f monic of odd prime degree d, p an inert prime with
p = 3 mod 4 not dividing the norm of gamma. Computes r = gamma^(-1/2) in
F_q with q = p^d by one power, lifts it to Z/p^k by Newton's iteration
r <- r (3 - gamma r^2) / 2, and checks delta = gamma r with symmetric
coefficients exactly. Gives up when p^k passes a bound on the size."""
d = len(f) - 1
q = p**d
g0 = [c % p for c in gamma]
r = powmod(g0, (3 * q - 5) // 4, f, p)
if mulmod(mulmod(r, r, f, p), g0, f, p) != [1] + [0] * (d - 1):
return None
limit = 4 * max(abs(c) for c in gamma) + 4
mod = p
while mod < limit:
mod = mod * mod
g = [c % mod for c in gamma]
t = mulmod(mulmod(r, r, f, mod), g, f, mod)
t = [(-c) % mod for c in t]
t[0] = (t[0] + 3) % mod
half = pow(2, -1, mod)
r = [c * half % mod for c in mulmod(r, t, f, mod)]
delta = mulmod(g, r, f, mod)
delta = [c - mod if c > mod // 2 else c for c in delta]
if mulmod(delta, delta, f) == gamma:
return delta
return None
def base_m(n: int, d: int, m: int = 0) -> tuple[list[int], int]:
"""The base-m expansion f of n, with f(m) = n, and m.
Precondition: d >= 2 and n > 2^(2 d^2) or so, so that the leading digit is
1. By default m = floor(n^(1/d)); then m^d <= n < (m + 1)^d < 2 m^d."""
if m <= 0:
m = iroot(n, d)
f, x = [], n
for _ in range(d):
f.append(x % m)
x //= m
f.append(x)
return f, m
def balanced_base_m(n: int, d: int, m: int) -> list[int]:
"""The base-m expansion of n with digits in (-m/2, m/2], so f(m) = n."""
f, x = [], n
for _ in range(d):
c = x % m
if c > m // 2:
c -= m
f.append(c)
x = (x - c) // m
f.append(x)
return f
def select_polynomial(n: int, d: int, tries: int = 64) -> tuple[list[int], int]:
"""A monic f of degree d and an m with f(m) = n, chosen among the balanced
base-m expansions for m = floor(n^(1/d)), m - 1, ..., with the smallest
largest coefficient. Smaller coefficients give smaller norms F(a, b)."""
m0 = iroot(n, d)
best: tuple[int, list[int], int] | None = None
for m in range(m0, max(m0 - tries, 1), -1):
f = balanced_base_m(n, d, m)
if f[-1] != 1:
continue
size = max(abs(c) for c in f[:-1])
if best is None or size < best[0]:
best = (size, f, m)
if best is None:
return base_m(n, d, m0)
return best[1], best[2]
def special_polynomial(r: int, e: int, s: int, d: int) -> tuple[list[int], int]:
"""f = x^d + s r^t and m = r^l with l = ceil(e / d), t = d l - e, so that
f(m) = r^(d l) + s r^t = r^t (r^e + s). Every divisor of r^e + s divides f(m).
Precondition: r >= 2, e >= 1, s != 0, d >= 2."""
l = -(-e // d)
t = d * l - e
return [s * r**t] + [0] * (d - 1) + [1], r**l
def integer_roots(f: list[int]) -> list[int]:
"""The integer roots of the monic f with f(0) != 0. A root divides f(0)."""
c = abs(f[0])
out = []
for t in range(1, isqrt(c) + 1):
if c % t == 0:
for u in {t, c // t}:
for z in (u, -u):
if evaluate(f, z) == 0:
out.append(z)
return out
def default_bound(n: int) -> int:
"""A smoothness bound for this toy sieve, tuned by experiment. The special
polynomial has tiny coefficients, so the bound grows slowly with n."""
return max(150, int(150 * 2 ** ((n.bit_length() - 20) / 20)))
def general_bound(n: int) -> int:
"""The bound for a base-m polynomial, whose coefficients are about
n^(1/3): as in the general number field sieve page, it grows faster."""
return max(150, int(200 * 2 ** ((n.bit_length() - 20) / 9)))
def snfs(n: int, f: list[int], m: int, bound: int = 0, width: int = 0, chars: int = 0,
extra: int = 10, max_b: int = 0) -> int:
"""A proper factor of the odd composite n by the number field sieve with a
given polynomial: f monic of odd prime degree d with f(m) = 0 mod n.
Precondition: n odd, not a prime power, f(m) = 0 mod n. Returns a proper
factor of n, or 1 on failure. Cost: heuristically L_n[1/3, (32/9)^(1/3)]
when f has coefficients of size n^(o(1/d)) and d grows as it should; this toy
version uses a fixed small degree."""
if evaluate(f, m, n) != 0: # @line L1
raise ValueError("f(m) must be 0 modulo n")
m %= n
for z in integer_roots(f):
# x - z divides f: the special number has an algebraic factor.
g = gcd(m - z, n)
if 1 < g < n:
return g
if bound <= 0:
bound = default_bound(n)
if width <= 0:
width = 3 * bound
if chars <= 0:
chars = 2 * n.bit_length() // 3 + 10
if max_b <= 0:
max_b = 5 * bound
for p in primes_up_to(max(bound, 2 * chars)):
if n % p == 0:
return p if p < n else 1
g = gcd(evaluate(derivative(f), m), n)
if 1 < g < n:
return g
p_inert = inert_prime(f, max(bound, 2 * chars) + 200)
if p_inert == 0:
return 1 # no inert prime: f is probably reducible over Q
primes, pairs, qchars = factor_bases(f, bound, chars) # @line L2
index = {pr: k for k, pr in enumerate(pairs)}
cols = 2 + len(primes) + len(pairs) + len(qchars)
rels = find_relations(n, f, m, primes, pairs, cols + extra, width, max_b)
vectors = [relation_vector(r, primes, pairs, index, qchars) for r in rels] # @line L5
fp = derivative(f)
fpm = evaluate(fp, m, n)
for mask in dependencies(vectors): # @line L6
chosen = [r for k, r in enumerate(rels) if mask >> k & 1]
total = [0] * len(primes)
for _, _, _, eu, _, _ in chosen:
total = [x + y for x, y in zip(total, eu)]
x = fpm # @line L7
for p, e in zip(primes, total):
x = x * pow(p, e // 2, n) % n
elems = [[a, -b] + [0] * (len(f) - 3) for a, b, _, _, _, _ in chosen]
gamma = product_in_order(elems + [fp, fp], f)
delta = algebraic_sqrt(gamma, f, p_inert) # @line L8
if delta is None:
continue
y = evaluate(delta, m, n)
g = gcd(x - y, n) # @line L9
if 1 < g < n:
return g
return 1 # @line L10
_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
def special_form(n: int) -> tuple[int, int, int] | None:
"""(r, e, s) with n = r^e + s for 2 <= r <= 12, |s| <= 12, s != 0 and
r^e >= 2^20, or None. Cunningham numbers such as 2^k - 1 have this form."""
best = None
for r in range(2, 13):
e = 1
while r ** (e + 1) <= n + 12:
e += 1
for k in (e, e + 1):
s = n - r**k
if s != 0 and abs(s) <= 12 and r**k >= 2**20:
if best is None or abs(s) < abs(best[2]):
best = (r, k, s)
return best
_SMALL = primes_up_to(1000)
def value_size(f: list[int], root: int, width: int) -> int:
"""About log2 of the largest product (a - b root) F(a, b) for |a|, b <= width:
the smaller it is, the more often both sides are smooth."""
rat = (abs(root) + 1) * width
alg = sum(abs(c) for c in f) * width ** (len(f) - 1)
return (rat * alg).bit_length()
def _split(m: int, polys: list[tuple[list[int], int]]) -> int:
"""A proper factor of a composite m with no prime factor below 1000.
A perfect power r^e is split by r. An algebraic factor x - z of a special
polynomial gives gcd(root - z, m). Then the sieve runs with the
polynomials that fit m, the special ones and a base-m polynomial of m,
in order of the size of their values, with growing bounds. Odd trial
division is the last resort."""
for e in range(2, m.bit_length() // 9 + 2):
r = iroot(m, e)
if r**e == m:
return r
general = select_polynomial(m, 3)
width = 3 * general_bound(m)
usable = []
for f, root in polys:
root %= m
if evaluate(f, root, m) != 0:
continue
roots = integer_roots(f)
for z in roots:
g = gcd(root - z, m)
if 1 < g < m:
return g
if not roots and value_size(f, root, width) < value_size(*general, width):
usable.append((value_size(f, root, width), f, root))
usable.sort()
for _, f, root in usable + [(0, *general)]:
bound = general_bound(m) if f is general[0] else default_bound(m)
for k in range(3):
g = snfs(m, f, root, bound, extra=10 + 20 * k)
if 1 < g < m:
return g
bound += bound // 2
d = 1001
while m % d:
d += 2
return d
def factorize(n: int) -> list[int]:
"""Prime factors of n >= 1 in non-decreasing order, with multiplicity.
If n = r^e + s has a special form, the polynomials x^3 + s r^t and
x^5 + s r^t serve n and every divisor of n. Other numbers use a base-m
polynomial, which is the general number field sieve. Trial division removes
the primes below 1000 first, and a stack replaces recursion."""
if n < 1:
raise ValueError("n must be a positive integer")
form = special_form(n)
polys = [special_polynomial(*form, d) for d in (3, 5)] if form else []
out: list[int] = []
for p in _SMALL:
if p * p > n:
break
while n % p == 0:
out.append(p)
n //= p
stack = [n]
while stack:
m = stack.pop()
if m == 1:
continue
if m < 1000 * 1000 or is_probable_prime(m):
out.append(m)
continue
g = _split(m, polys)
stack.extend((g, m // g))
return sorted(out)
test_special_number_field_sieve.py 287 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/special-number-field-sieve/. The authors are listed in prov/89BD3K.jsonl.
import math
import random
import pytest
from special_number_field_sieve import (
algebraic_sqrt,
dependencies,
derivative,
evaluate,
factor_bases,
factorize,
find_relations,
general_bound,
homogeneous,
inert_prime,
integer_roots,
is_inert,
mulmod,
product_in_order,
relation_vector,
select_polynomial,
snfs,
special_form,
special_polynomial,
value_size,
)
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 test_example() -> None:
# n = 2^41 - 1 = 13367 * 164511353 with f = x^3 - 2 and m = 2^14.
n = 2**41 - 1
f, m = special_polynomial(2, 41, -1, 3)
assert (f, m) == ([-2, 0, 0, 1], 16384)
assert evaluate(f, m) == 2 * n
primes, pairs, chars = factor_bases(f, 200, 10)
assert (len(primes), len(pairs), len(chars)) == (46, 40, 10)
assert pairs[:6] == [(2, 0), (3, 2), (5, 3), (11, 7), (17, 8), (23, 16)]
assert chars[:2] == [(223, 68), (223, 179)]
assert inert_prime(f, 400) == 463
rels = find_relations(n, f, m, primes, pairs, 108, 600, 1000)
assert len(rels) == 108 and max(r[1] for r in rels) == 59
assert [(r[0], r[1], r[2], r[4]) for r in rels[:3]] == [
(-224, 1, -16608, -11239426), (-208, 1, -16592, -8998914), (-176, 1, -16560, -5451778)]
index = {pr: k for k, pr in enumerate(pairs)}
vectors = [relation_vector(r, primes, pairs, index, chars) for r in rels]
deps = dependencies(vectors)
assert len(deps) == 21
fp = derivative(f)
assert evaluate(fp, m) == 805306368
outcomes = []
for mask in deps:
chosen = [r for k, r in enumerate(rels) if mask >> k & 1]
v = math.isqrt(math.prod(r[2] for r in chosen))
assert v * v == math.prod(r[2] for r in chosen)
x = 805306368 * v % n
gamma = product_in_order([[a, -b, 0] for a, b, *_ in chosen] + [fp, fp], f)
delta = algebraic_sqrt(gamma, f, 463)
assert delta is not None and mulmod(delta, delta, f) == gamma
y = evaluate(delta, m, n)
assert (x * x - y * y) % n == 0
outcomes.append((len(chosen), math.gcd(x - y, n)))
assert outcomes[1] == (52, 164511353)
assert sum(1 < g < n for _, g in outcomes) == 8
assert snfs(n, f, m, 200, 600, 10) == 164511353
def test_cole_m67() -> None:
# Cole's 1903 factorization of 2^67 - 1, found again with x^3 - 4 and m = 2^23.
n = 2**67 - 1
f, m = special_polynomial(2, 67, -1, 3)
assert (f, m) == ([-4, 0, 0, 1], 2**23) and evaluate(f, m) == 4 * n
assert snfs(n, f, m) == 193707721
general = select_polynomial(n, 3)
assert value_size(f, m, 3 * general_bound(n)) < value_size(*general, 3 * general_bound(n))
def test_special_polynomial() -> None:
rng = random.Random(4)
for _ in range(500):
r, e = rng.randrange(2, 13), rng.randrange(1, 200)
s = rng.choice([-1, 1]) * rng.randrange(1, 13)
d = rng.choice((3, 5))
f, m = special_polynomial(r, e, s, d)
t = d * (-(-e // d)) - e
assert 0 <= t < d and f[1:] == [0] * (d - 1) + [1]
assert evaluate(f, m) == r**t * (r**e + s)
def test_special_form() -> None:
for r, e, s in ((2, 41, -1), (2, 64, 1), (10, 17, -1), (3, 41, -1), (6, 23, 1), (7, 30, 2)):
form = special_form(r**e + s)
assert form is not None
a, b, c = form
assert a**b + c == r**e + s and abs(c) <= 12
assert special_form(1000003 * 1000033) is None
assert special_form(12345) is None
def test_algebraic_factor() -> None:
# 2^33 - 1 with d = 3 gives f = x^3 - 1 = (x - 1)(x^2 + x + 1).
n = 2**33 - 1
f, m = special_polynomial(2, 33, -1, 3)
assert f == [-1, 0, 0, 1] and integer_roots(f) == [1]
assert snfs(n, f, m) == 2**11 - 1
assert integer_roots([-2, 0, 0, 1]) == [] and integer_roots([8, 0, 0, 1]) == [-2]
assert sorted(integer_roots([-32, 0, 0, 0, 0, 1])) == [2]
def test_is_inert() -> None:
for c in (2, 3, 4, 5, 10):
for p in (2, 3, 5, 7, 11, 13, 19, 31, 37, 43, 61, 67, 73, 79, 97, 103, 109, 127):
if c % p == 0:
continue
expected = p % 3 == 1 and pow(c, (p - 1) // 3, p) != 1
assert is_inert([-c, 0, 0, 1], p) == expected
# x^5 - 2 modulo 11: 11 = 1 mod 5 and 2 is not a fifth power modulo 11.
assert is_inert([-2, 0, 0, 0, 0, 1], 11)
assert not is_inert([-2, 0, 0, 0, 0, 1], 7)
def test_algebraic_sqrt() -> None:
rng = random.Random(9)
for c in (2, 3, 5, 6, 7):
f = [-c, 0, 0, 1]
p = inert_prime(f, 300)
assert p % 4 == 3 and is_inert(f, p)
for _ in range(10):
delta = [rng.randrange(-10**8, 10**8) for _ in range(3)]
gamma = mulmod(delta, delta, f)
if all(x % p == 0 for x in gamma):
continue
r = algebraic_sqrt(gamma, f, p)
assert r in (delta, [-x for x in delta])
# alpha has norm c, which is not a square here.
assert algebraic_sqrt(mulmod(gamma, [0, 1, 0], f), f, p) is None
def test_relations() -> None:
n = 2**59 - 1
f, m = special_polynomial(2, 59, -1, 3)
primes, pairs, chars = factor_bases(f, 300, 10)
rels = find_relations(n, f, m, primes, pairs, 60, 900, 300)
assert len(rels) == 60
for a, b, u, eu, v, ev in rels:
assert math.gcd(a, b) == 1 and u == a - b * m and v == homogeneous(f, a, b)
assert abs(u) == math.prod(p**e for p, e in zip(primes, eu))
assert abs(v) == math.prod(p**e for p, e in zip(primes, ev))
def test_dependencies() -> None:
rng = random.Random(7)
for _ in range(200):
rows = [rng.getrandbits(12) for _ in range(rng.randrange(1, 20))]
for mask in dependencies(rows):
acc = 0
for j, v in enumerate(rows):
if mask >> j & 1:
acc ^= v
assert mask and acc == 0
assert len(dependencies(rows)) >= len(rows) - 12
def test_returns_divisor() -> None:
with pytest.raises(ValueError):
snfs(2**41 - 3, [-2, 0, 0, 1], 2**14)
for e in (37, 41, 43, 47):
n = 2**e - 1
f, m = special_polynomial(2, e, -1, 3)
g = snfs(n, f, m)
assert 1 < g < n and n % g == 0
def test_cunningham() -> None:
assert check(2**59 - 1) == [179951, 3203431780337]
assert check(2**64 + 1) == [274177, 67280421310721]
assert check(10**17 - 1) == [3, 3, 2071723, 5363222357]
assert check(3**41 - 1) == [2, 83, 2526913, 86950696619]
assert check(2**63 - 1) == [7, 7, 73, 127, 337, 92737, 649657]
assert check(2**66 - 1) == [3, 3, 7, 23, 67, 89, 683, 20857, 599479]
assert check(6**23 + 1) == [7, 113958101, 990000731]
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(1009**2) == [1009, 1009]
assert factorize(1009**3) == [1009] * 3
assert factorize(1000003**2) == [1000003, 1000003]
assert factorize(10007**3) == [10007] * 3
assert factorize(1009 * 1013) == [1009, 1013]
assert factorize(1000003 * 1000033) == [1000003, 1000033]
def test_carmichael() -> None:
for n in CARMICHAEL:
check(n)
assert factorize(3215031751) == [151, 751, 28351]
def test_close_primes() -> None:
rng = random.Random(17)
for bits in (16, 19, 22):
p = random_prime(rng, bits)
q = p + 2
while not is_prime(q):
q += 2
assert check(p * q) == [p, q]
def test_random_semiprimes() -> None:
rng = random.Random(2024)
for bits in (11, 14, 17, 20, 22):
for _ in range(2):
p, q = random_prime(rng, bits), random_prime(rng, bits)
assert check(p * q) == sorted((p, q))
def test_smooth_numbers() -> None:
rng = random.Random(31)
for size in (8, 16, 32, 64, 128):
for _ in range(3):
n = 1
while n.bit_length() < size:
n *= rng.choice((2, 3, 5, 7, 11, 13, 97, 997, 1009, 1013, 2003, 4001))
check(n)
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)
What the next level needs
- the tests did not run in this build