Ryan Rueger

ryan@rueg.re / picture / key / home
aboutsummaryrefslogtreecommitdiffhomepage
path: root/lcsidh.py
diff options
context:
space:
mode:
authorRyan Rueger <git@rueg.re>2025-03-01 20:25:41 +0100
committerRyan Rueger <git@rueg.re>2025-03-01 22:11:11 +0100
commitd40de259097c5e8d8fd35539560ca7c3d47523e7 (patch)
tree18e0f94350a2329060c2a19b56b0e3e2fdae56f1 /lcsidh.py
downloadpegasis-d40de259097c5e8d8fd35539560ca7c3d47523e7.tar.gz
pegasis-d40de259097c5e8d8fd35539560ca7c3d47523e7.tar.bz2
pegasis-d40de259097c5e8d8fd35539560ca7c3d47523e7.zip
Initial Commit
Co-Authored-By: Damien Robert <Damien.Olivier.Robert+git@gmail.com> Co-Authored-By: Frederik Vercauteren <frederik.vercauteren@gmail.com> Co-Authored-By: Jonathan Komada Eriksen <jonathan.eriksen97@gmail.com> Co-Authored-By: Pierrick Dartois <pierrickdartois@icloud.com> Co-Authored-By: Riccardo Invernizzi <nidadoni@gmail.com> Co-Authored-By: Ryan Rueger <git@rueg.re> [0.01s] Co-Authored-By: Benjamin Wesolowski <benjamin@pasch.umpa.ens-lyon.fr> Co-Authored-By: Arthur Herlédan Le Merdy <ahlm@riseup.net> Co-Authored-By: Boris Fouotsa <tako.fouotsa@epfl.ch>
Diffstat (limited to 'lcsidh.py')
-rw-r--r--lcsidh.py195
1 files changed, 195 insertions, 0 deletions
diff --git a/lcsidh.py b/lcsidh.py
new file mode 100644
index 0000000..30ff6ce
--- /dev/null
+++ b/lcsidh.py
@@ -0,0 +1,195 @@
+from sage.all import(
+ isqrt, sqrt, floor, prod,
+ randint,
+ log,
+ proof,
+ next_prime,
+ kronecker,
+ GF, ZZ, QQ,
+ Matrix,
+ gcd, xgcd, valuation,
+)
+
+from coin import sos_pair_generator, xsos_pair_generator
+from ideals import ideal_to_sage, ShortIdeals
+import time
+
+import logging
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.WARNING)
+logger_sh = logging.StreamHandler()
+logger_sh.setLevel(logging.WARNING)
+formatter = logging.Formatter('%(name)s [%(levelname)s] %(message)s')
+logger_sh.setFormatter(formatter)
+logger.addHandler(logger_sh)
+
+proof.all(False)
+
+
+def generate_combs(n):
+ """
+ Generate pairs (i, j) sorted by L1-norm.
+ """
+ i_start = 1
+ while i_start < n:
+ i = i_start
+ j = 0
+ while i > j:
+ yield i, j
+ j += 1
+ i -= 1
+ i_start += 1
+
+
+def eval_sol(vals, allowed_primes):
+ """
+ Given a list vals of Elkies isogenies of degrees contained in
+ allowed_primes, estimate the total cost of such isogeny. The cost of a
+ single step is assumed to be linear in the degree.
+ """
+ costs = {}
+ for li in allowed_primes:
+ cost_li = 0
+ for vi in vals:
+ cost_li += valuation(vi, li)
+ costs[li] = cost_li
+
+ return sum(li * costs[li] for li in allowed_primes)
+
+
+def UVSolver(R, N_R, uv_params):
+ """
+ Given an ideal R try to find two equivalent ideals I_1 and I_2 such that,
+ after writing n(I_1) = N1*cof1 and n(I_2) = N2*cof2 we can solve the
+ equation
+ u*N1 + v*N2 = 2^e
+ for u and v sums of two squares.
+
+ Input:
+ - R: the input ideal as a list of generators, where a generator is a pair
+ of numbers [a, b] representing the element a + ib;
+ - N_R: the (ideal) norm of R;
+ - uv_params: an instance of uv_params.UV_params containing the parameters
+ for the algorithm, as described in the UV_params class.
+
+ Output:
+ - u, v, ids[i], ids[j], sig_1, sig_2, t
+ where
+ - u, v are written as (x_u, y_u, g_u) such that g_u*(x_u^2 + y_2^2)=u
+ - ids[i], ids[j] are ideals represented as (norm, cofactor, 2-valuation,
+ gens); norm * cofactor * 2**(2-valuation) is the norm if the ideal
+ generated by gens;
+ - setting n1=ids[i][0], n2 = ids[j][0] (the cofactor-free norms) it holds
+ 2**t == u*n1 + v*n2; in general t <= uv_params.e, but they do not need to
+ be equal;
+ - sig_1, sig_2 are the ideals over 2 that are left in ids[i], ids[j]
+ respectively after multiplications by two are removed, as a pair (left,
+ right); one between left and right must be zero; if they point in the
+ same direction (e. g. (e_1, 0) and (e_2, 0)) the algorithm assumes that
+ the common part is computed in advance and removed by the ideals.
+ - if uv_params.n_squares=1, only one among u and v is decomposed as sum of
+ squares
+ """
+
+ # Look for short vectors
+ # Elements of L are of the form (norm, [elements])
+ p, spr = uv_params.p, uv_params.spr
+ norm_bound = uv_params.norm_bound
+ L = ShortIdeals(R, N_R, norm_bound*spr, p, spr, uv_params.norm_cff_bound)
+ logger.info(f'{len(L) = }')
+
+ # Remove allowed primes
+ ids = {}
+ for nI, I in L:
+ # Even exponents
+ v2 = nI.valuation(2)
+ nI //= 2**v2
+
+ k = nI / gcd(nI, uv_params.allowed_primes_prod)
+ while gcd(k, uv_params.allowed_primes_prod) != 1:
+ k /= gcd(k, uv_params.allowed_primes_prod)
+
+ cof = nI/k
+ if k in ids and cof*2**v2 >= ids[k][0]*2**ids[k][1]:
+ # Multiple of a previous vector
+ continue
+ ids[k] = (cof, v2, I)
+
+ # Short vectors sorted by cofactor-free norm
+ ids = [(j,) + ids[j] for j in ids]
+ ids.sort()
+
+ two_parts = {}
+ two_left = ideal_to_sage([[2, 0], [-1/2, 1/2]], uv_params.max_order)
+ two_right = ideal_to_sage([[2, 0], [1/2, 1/2]], uv_params.max_order)
+
+ # Keep track of the best solution found so far
+ best_sol = None
+ best_cost = None
+ sol_cnt = 0
+
+ def compute_two_sig(id_i, v2_i):
+ id_sage = ideal_to_sage(id_i, uv_params.max_order)
+ id_l = id_sage + two_left**v2_i
+ id_r = id_sage + two_right**v2_i
+ v2_l = valuation(id_l.norm(), 2)
+ v2_r = valuation(id_r.norm(), 2)
+ mul2 = min(v2_l, v2_r)
+ sig_i = (v2_l - mul2, v2_r - mul2)
+ return sig_i
+
+ # Try pairs to solve the coin equation
+ for i, j in generate_combs(min(len(ids), uv_params.comb_bound)):
+
+ if uv_params.sol_bound and sol_cnt >= uv_params.sol_bound:
+ break
+ n1, cof1, v2_1, id1 = ids[i]
+ n2, cof2, v2_2, id2 = ids[j]
+ if gcd(n1, n2) != 1:
+ continue
+
+ # Compute the part above two of the ideals
+ if i in two_parts:
+ sig_1 = two_parts[i]
+ else:
+ sig_1 = compute_two_sig(id1, v2_1)
+ two_parts[i] = sig_1
+
+ if j in two_parts:
+ sig_2 = two_parts[j]
+ else:
+ sig_2 = compute_two_sig(id2, v2_2)
+ two_parts[j] = sig_2
+
+ # Compute the remeaning torsion
+ used_torsion = abs(sig_1[0] - sig_2[0]) + abs(sig_1[1] - sig_2[1])
+ av_torsion = uv_params.uv_e - used_torsion
+
+ # Avoid generating too many combinations for a single pair
+ local_sol_ncnt = 0
+ for u, v, t in xsos_pair_generator(n1, n2, av_torsion,
+ uv_params.sos_allowed_primes,
+ n_squares=uv_params.n_squares):
+ sol_cnt += 1
+ local_sol_ncnt += 1
+
+ if uv_params.sol_bound == 1:
+ return u, v, ids[i], ids[j], sig_1, sig_2, t
+
+ # Cost estimate for best solution
+ elkies_vals = [ids[i][1], ids[j][1]]
+ if type(u) in [tuple, list]:
+ elkies_vals.append(u[2])
+ if type(v) in [tuple, list]:
+ elkies_vals.append(v[2])
+ elkies_cost = eval_sol(elkies_vals, uv_params.allowed_primes)
+ if not best_sol or elkies_cost < best_cost:
+ best_sol = u, v, ids[i], ids[j], sig_1, sig_2, t
+ best_cost = elkies_cost
+ if sol_cnt >= uv_params.sol_bound:
+ break
+ if local_sol_ncnt > 3: # TODO: this is completely arbitrary
+ break
+
+ return best_sol
+