Skip to content

Instantly share code, notes, and snippets.

@HarryR
Created May 13, 2025 00:29
Show Gist options
  • Save HarryR/3d8a90d04fe64ad5c378b1013fb8d02e to your computer and use it in GitHub Desktop.
Save HarryR/3d8a90d04fe64ad5c378b1013fb8d02e to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
import json
from math import log2
from random import randint
from time import perf_counter
from collections import defaultdict
from contextlib import contextmanager
from sage.all import random_prime, is_prime, GF, EllipticCurve, sqrt as sage_sqrt, primitive_root
STATS = defaultdict(lambda: defaultdict(int))
COUNT = 25
BITLENS = range(30,150)
@contextmanager
def measure(key, action):
start = perf_counter()
try:
yield
finally:
STATS[key][action] += perf_counter() - start
def check_glv_endomorphism(curve, generator=None):
"""
Check if an elliptic curve supports the specific endomorphism needed for GLV method.
Parameters:
curve: An elliptic curve defined over a finite field
generator: Optional generator point. If None, will use the default generator
Returns:
A dictionary containing the test results and relevant values
"""
# Get the curve parameters
F = curve.base_field()
p = F.order()
n = curve.order()
# Check if curve has the correct form for j-invariant 0
if curve.a_invariants() != (0, 0, 0, 0, curve.a_invariants()[4]):
return {"supports_glv": False, "reason": "Curve is not in the form y^2 = x^3 + b"}
# Check field characteristics
if p % 3 != 1:
return {"supports_glv": False, "reason": "Field characteristic p must satisfy p ≡ 1 (mod 3)"}
# Check if n is prime
if not n.is_prime():
return {"supports_glv": False, "reason": "Curve order must be prime"}
# Use provided generator or get default
if generator is None:
try:
generator = curve.gens()[0]
except:
return {"supports_glv": False, "reason": "No generator specified and unable to find default generator"}
# Find a cube root of unity in the base field
try:
# primitive_root() is usually faster than via F.unit_group(), e.g. F_star = F.unit_group(), g_base = F(F_star.gen(0))
g_base = F(primitive_root(F.order()))
beta = g_base**((p-1)//3)
# Verify beta is a non-trivial cube root of unity
if beta == 1 or beta**3 != 1:
return {"supports_glv": False, "reason": "Failed to find valid cube root of unity in base field"}
# Verify beta satisfies the minimal polynomial
if beta**2 + beta + 1 != 0:
return {"supports_glv": False, "reason": "Cube root of unity does not satisfy minimal polynomial"}
except Exception as e:
return {"supports_glv": False, "reason": f"Error finding cube root of unity: {e}"}
# Find primitive 6th root of unity in the scalar field
try:
Fq = GF(n)
g_scalar = Fq(primitive_root(n))
# Try both (n-1)/6 and (n-1)/3 for lambda
lambda_val_1 = g_scalar**((n-1)//6)
lambda_val_2 = g_scalar**((n-1)//3)
# Test both values to see if either works
lambda_vals = [lambda_val_1, lambda_val_2]
working_lambda = None
for lambda_val in lambda_vals:
# Verify lambda^6 = 1
if lambda_val**6 != 1:
continue
# Check if lambda satisfies the minimal polynomial
if lambda_val**2 + lambda_val + 1 != 0:
continue
# The critical test: check if lambda*G = (beta*G_x, G_y)
try:
endo_point = curve(beta * generator[0], generator[1])
scalar_point = lambda_val * generator
if endo_point == scalar_point:
working_lambda = lambda_val
break
except Exception:
continue
if working_lambda is None:
return {
"supports_glv": False,
"reason": "No suitable lambda value found that satisfies the endomorphism relation",
"beta": beta,
"lambda_candidates": lambda_vals,
"test_points": {
"endomorphism": endo_point,
"scalar_mult": scalar_point
}
}
except Exception as e:
return {"supports_glv": False, "reason": f"Error finding lambda value: {e}"}
# If we get here, the curve supports GLV
return {
"supports_glv": True,
"beta": beta,
"lambda": working_lambda,
"verification": "lambda*G == E(beta*G[0], G[1])"
}
def find_generator(E:EllipticCurve):
x = E.base_field()(1)
while True:
try:
G = E.lift_x(x)
except ValueError:
x += 1
continue
if G.order() == E.order():
# Generator must be even?
if int(G[1]) & 1:
G = -G
return G
x += 1
def find_split_constants_explicit_tof(p:int, E:EllipticCurve):
# from libsecp256k1/sage
assert p % 3 == 1
assert E.j_invariant() == 0
t = int(E.trace_of_frobenius())
c = int(sage_sqrt((4*p - t**2)//3))
b1, b2 = c, (1 - (t - c)//2) % E.order()
return b1, int(b2)
def decompose_scalar(k, n, g1, g2, b1, b2, lambda_val, bitsize2=384):
"""Decompose scalar k into k1 and k2 using GLV method."""
k = k % n
# Compute c1 and c2 using precomputed constants and bit operations
# This replaces division with multiplication and right shift
c1 = (k * g1) >> bitsize2
c2 = (k * g2) >> bitsize2
# Handle final bit for rounding as per the paper
if (k * g1) & (1 << (bitsize2-1)):
c1 += 1
if (k * g2) & (1 << (bitsize2-1)):
c2 += 1
neg_b1 = -b1 % n
neg_b2 = -b2 % n
# Compute k2 = -c1·(-b1) - c2·(-b2)
k2 = (c1 * neg_b1 + c2 * neg_b2) % n
# Compute k1 = k - k2·λ mod n
k1 = (k - (k2 * lambda_val) % n) % n
# Ensure k1 and k2 are properly minimized
if k1 > (n >> 1):
k1 = k1 - n
if k2 > (n >> 1):
k2 = k2 - n
return k1, k2
def find_g1_g2(b1, b2, n, bitsize=256, bitsize2=384):
# Python's round() is off by 1
quotient_g1 = (2**bitsize2)*(-b2)//n
remainder_g1 = (2**bitsize2)*(-b2)%n
g1 = (quotient_g1 + (1 if remainder_g1 >= n//2 else 0)) % (2**bitsize)
# Python's round() is off by one
quotient_g2 = (2**bitsize2)*(b1)//n
remainder_g2 = (2**bitsize2)*(b1)%n
g2 = (quotient_g2 + (1 if remainder_g2 >= n//2 else 0)) % (2**bitsize)
return g1, g2
def sample_primes(bitlen):
while True:
with measure(bitlen, '1_time_random_prime'):
p = random_prime(2**bitlen, lbound=2**(bitlen-1))
if p % 12 == 7:
yield p
STATS[bitlen]['1_prime_congruent'] += 1
continue
STATS[bitlen]['1_prime_not_congruent'] += 1
def sample_curves(bitlen):
for p in sample_primes(bitlen):
with measure(bitlen, '2_time_primitive_root'):
F = GF(p)
g = F(primitive_root(p))
n_found = 0
# 6th primitive roots g^1 and g^-1 are only ones which can be prime
for b in [int(F(g)), int(F(g**5))]:
with measure(bitlen, '2_time_check_curve'):
E = EllipticCurve(F, [0, b])
E_q = E.order()
E_q_prime = is_prime(E_q)
if E_q_prime:
n_found += 1
yield p, b, E
if n_found == 0:
STATS[bitlen]['2_no_prime_curve'] += 1
else:
STATS[bitlen]['2_prime_curve'] += 1
if n_found == 2:
STATS[bitlen]['2_1_prime_curve_both'] += 1
else:
STATS[bitlen]['2_1_prime_curve_one'] += 1
def try_decompose(E_q, bitlen, b1, b2, lambda_val):
g1, g2 = find_g1_g2(b1, b2, E_q, bitlen, bitlen//2 + bitlen)
count = 10
k1_log2_total = 0
k2_log2_total = 0
for _ in range(count):
k = randint(2**510, 2**512) % E_q
k1, k2 = decompose_scalar(k, E_q, g1, g2, b1, b2, lambda_val, (bitlen//2)+bitlen)
recomposed = (k1 + (k2 * lambda_val) % E_q) % E_q
if k != recomposed:
return None
k1_log2 = log2(abs(int(k1))) if k1 != 0 else 0
k2_log2 = log2(abs(int(k2))) if k2 != 0 else 0
k1_log2_total += k1_log2
k2_log2_total += k2_log2
k1_log2 = round(k1_log2_total / count,2)
k2_log2 = round(k2_log2_total / count,2)
return k1_log2, k2_log2, k1_log2 <= bitlen//2 and k2_log2 <= bitlen//2, int(lambda_val) & 1
with open('graphs/3-glv-decomp.jsonl', 'w') as handle:
for bitlen in BITLENS:
FOUND = 0
bitlen_start = perf_counter()
with measure(bitlen, 'time_total'):
for p, b, E in sample_curves(bitlen):
if FOUND >= COUNT:
break
with measure(bitlen, '2_time_basic_field_checks'):
b_log2 = log2(b)
E_q = E.order()
E_q_prime = is_prime(E_q)
STATS[bitlen]['3_found_curves'] += 1
with measure(bitlen, '3_time_find_generator'):
G = find_generator(E)
if log2(G[1]) <= 8:
STATS[bitlen]['3_1_G_y_is_under_8bit'] += 1
else:
STATS[bitlen]['3_1_G_y_is_over_8bit'] += 1
with measure(bitlen, '3_time_check_glv_endomorphism'):
glv_info = check_glv_endomorphism(E, G)
supports_glv = glv_info['supports_glv']
if not supports_glv:
STATS[bitlen]['3_not_glv'] += 1
continue
STATS[bitlen]['3_glv'] += 1
lambda_val = glv_info['lambda']
neg_lambda = -int(lambda_val) % E_q
with measure(bitlen, '4_time_find_split_constants_explicit_tof'):
b1, b2 = find_split_constants_explicit_tof(p, E)
ok = False
ok_i = None
with measure(bitlen, '4_time_try_decompose'):
checks = [
try_decompose(E_q, bitlen, b1, b2, lambda_val),
try_decompose(E_q, bitlen, -b1 % E_q, b2, lambda_val),
try_decompose(E_q, bitlen, b1, -b2 % E_q, lambda_val),
try_decompose(E_q, bitlen, -b1 % E_q, -b2 % E_q, lambda_val),
try_decompose(E_q, bitlen, b1, b2, neg_lambda),
try_decompose(E_q, bitlen, -b1 % E_q, b2, neg_lambda),
try_decompose(E_q, bitlen, b1, -b2 % E_q, neg_lambda),
try_decompose(E_q, bitlen, -b1 % E_q, -b2 % E_q, neg_lambda)
]
for i, x in enumerate(checks):
if x[2]:
ok = True
ok_i = i
if ok:
STATS[bitlen]['4_glv_efficient_decompose'] += 1
print()
print(f"{bitlen}-bit Curve with b = {b} (log2={round(b_log2,3)},{round(b_log2/log2(p),3)}), j-invariant = {E.j_invariant()}, order = {E_q} (prime? {E_q_prime})")
print(' - G', G)
print(' - GLV', glv_info)
for i, x in enumerate(checks):
print(i, x)
print(ok)
print()
print()
FOUND += 1
else:
STATS[bitlen]['4_glv_inefficient_decompose'] += 1
for b, x in STATS.items():
for y in sorted(x.keys()):
print(b, y, x[y])
print()
handle.write(json.dumps((bitlen,STATS[bitlen])) + "\n")
handle.flush()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment