Ryan Rueger

ryan@rueg.re / picture / key / home
aboutsummaryrefslogtreecommitdiffhomepage
path: root/pegasis.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 /pegasis.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 'pegasis.py')
-rw-r--r--pegasis.py704
1 files changed, 704 insertions, 0 deletions
diff --git a/pegasis.py b/pegasis.py
new file mode 100644
index 0000000..6664e0d
--- /dev/null
+++ b/pegasis.py
@@ -0,0 +1,704 @@
+from time import time
+import logging
+
+from sage.all import *
+
+from xonly import xPoint, random_xPoint, MontgomeryA, isWeierstrass, translate_by_T
+from lcsidh import UVSolver
+from uv_params import UV_params
+from ideals import ideal_to_sage, PrincipalGenerator, Conjugate, RandomDegreeOnePrimeIdeal
+from elkies import Elkies
+
+from theta_lib.isogenies.Kani_clapoti import KaniClapotiIsog
+
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.WARNING)
+logger_sh = logging.StreamHandler()
+formatter = logging.Formatter('%(name)s [%(levelname)s] %(message)s')
+logger_sh.setFormatter(formatter)
+logger.addHandler(logger_sh)
+
+class PEGASIS:
+ def __init__(self, level, params=None):
+ r"""
+ Main Clapoti class. Takes as input the security level
+ (500|1000|1500|2000|4000) and generates the necessary parameters and
+ structures to solve the norm equation and run the Clapoti algorithm.
+ Additional parameters can be supplied as a dictionary.
+ """
+ logger.debug("Initialising")
+ uv_params = UV_params(level, params)
+ self.uv_params = uv_params
+
+ self.p = uv_params.p
+ self.Fp = uv_params.Fp
+ self.Fp2 = uv_params.Fp2
+ self.E_start = uv_params.E_start
+
+ self.K = uv_params.K
+ self.w = uv_params.w
+ self.max_order = uv_params.max_order
+ self.order = uv_params.order
+
+ def eval_frob(P):
+ E = P.curve()
+ frob = E.base_field().frobenius_endomorphism()
+ return E([frob(c) for c in P.xy()])
+
+ def eval_x_frob(xP):
+ frob = xP.curve.base_field().frobenius_endomorphism()
+ return xPoint(frob(xP.X), xP.curve)
+
+ def eval_x_frob2(xP):
+ frob = xP.curve.frobenius_endomorphism()
+ return xPoint(frob(frob(xP.X)), xP.curve)
+
+ self.pi = eval_frob
+ self.pi_x = eval_x_frob
+ self.pi2_x = eval_x_frob2
+
+ for ell in self.uv_params.allowed_primes:
+ if kronecker_symbol(-self.p, ell) == 1:
+ self.lam = Integer(GF(ell)(-self.p).sqrt())
+ self.small_ell = ell
+ self.frak_ell = self.order*ell + self.order*(self.w-self.lam)
+ assert self.frak_ell.norm() == ell
+ break
+
+ def action(self, E, frak_a, verb_return = False):
+ r"""
+ Function for computes the class action of any ideal.
+ Input:
+ - E: Elliptic curve E over Fp
+ - frak_a: An ideal of End(E)
+ Output:
+ - frak_a * E
+ - Optional: Extra data for measuring performance
+ """
+ compensate = 0
+ logger.info("")
+ logger.info("="*50)
+ logger.info("Starting UV Solver....")
+ logger.info("="*50)
+
+ # Step 1: find u and v
+ _t0 = time()
+ _rerand_cnt = 0
+
+ uv_output = None
+ while not uv_output:
+ uv_output = UVSolver(
+ [list(beta) for beta in frak_a.gens()], frak_a.norm(),
+ self.uv_params)
+ if not uv_output:
+ _rerand_cnt += 1
+ frak_a = frak_a*self.frak_ell
+ compensate += 1
+ logger.debug("Rerandomising!")
+ continue
+
+ _t1 = time()
+ logger.info(f"Used {_t1 - _t0} seconds")
+
+ # Step 2: compute the kernel
+ u, v, b_list, c_list, sig_1, sig_2, t = uv_output
+ _n1, _cof1, _v21, _gens1 = b_list
+ _n2, _cof2, _v22, _gens2 = c_list
+
+ if compensate > 0:
+ E = E.short_weierstrass_model()
+ prev_j = None
+ for _ in range(compensate):
+ logger.debug("Compensation step")
+ E_prev = E
+ E = self.small_prime_ideal_action(E, self.small_ell, self.small_ell - self.lam, prev_j).codomain()
+ prev_j = E_prev.j_invariant()
+ E = E.montgomery_model()
+
+ logger.info("")
+ logger.info("="*50)
+ logger.info("Starting Ideal Action....")
+ logger.info("="*50)
+ N1, N2, x_u, y_u, g_u, x_v, y_v, g_v, Pu, Qu, Pv, Qv = self.ideal_to_kernel(E, [u,v], [b_list, c_list], t)
+
+ _t2 = time()
+ logger.info(f"Used {_t2 - _t1} seconds")
+
+ # Sanity checks
+ assert all([not R*2**(t+2) for R in [Pu, Qu, Pv, Qv]])
+ assert all([bool(R*2**(t+1)) for R in [Pu, Qu, Pv, Qv]])
+ assert Pu.weil_pairing(Qu, 2**(t+2))**(g_v*N1*N2) == Pv.weil_pairing(Qv, 2**(t+2))**(g_u)
+ assert N1*g_u*(x_u**2 + y_u**2) + N2*g_v*(x_v**2 + y_v**2) == 2**t
+
+ # Step 3 : 4d iso
+ logger.info("")
+ logger.info("="*50)
+ logger.info(f"Starting 4D isog")
+ logger.info("="*50)
+ logger.debug("Starting product: ")
+ logger.debug(Pu.curve())
+ logger.debug(Qv.curve())
+
+ F = KaniClapotiIsog([Pu,Qu,Pv,Qv],[g_u,x_u,y_u,g_v,x_v,y_v,N1,N2,t])
+ logger.debug(f"Change of coordinates: {F.iso_type} Twist={F.twist}")
+ _t3 = time()
+
+ logger.info(f"Used {_t3 - _t2} seconds")
+ logger.info(f"Total time: {_t3 - _t0} seconds")
+
+ stats = {
+ "time_step1": _t1 - _t0,
+ "time_step2": _t2 - _t1,
+ "time_step3": _t3 - _t2,
+ "time_total": _t3 - _t0,
+ "rerandomizations": _rerand_cnt,
+ "u": u,
+ "v": v,
+ "g_u": g_u,
+ "x_u": x_u,
+ "y_u": y_u,
+ "g_v": g_v,
+ "x_v": x_v,
+ "y_v": y_v,
+ "N1": N1,
+ "N2": N2,
+ "_cof1": _cof1,
+ "_cof2": _cof2,
+ "_v21": _v21,
+ "_v22": _v22,
+ "t": t
+ }
+
+ if verb_return:
+ return F.Ea.change_ring(self.Fp), stats
+
+ return F.Ea.change_ring(self.Fp)
+
+
+ def ideal_to_kernel(self, E, uv, frak_bc, e):
+ r"""
+ Following sec. 5.4, prepare the data necessary for the 4D isogeny.
+ Input:
+ - E: starting curve
+ - uv: the decomposition of u and v as cof + sum of squares
+ - frak_bc: the ideals b and c as output of UVSolver
+ - e: the torsion we want to use
+
+ Output:
+ - N1, N2, x_u, y_u, g_u, x_v, y_u, g_v e s.t.
+ N1*g_u*(x_u^2 + y_u^2) + N2*g_v*(x_v^2 + y_v^2) == 2^e
+ - P_u, Q_u, P_v, Q_v: image points on E_u, Ev as described
+ in sec 5.4
+ """
+ # First recover the numbers
+ tstart = time()
+ if type(uv[0]) == tuple:
+ (x_u, y_u, g_u) = uv[0]
+ else:
+ x_u = None
+ y_u = None
+ g_u = 1
+
+ if type(uv[1]) == tuple:
+ (x_v, y_v, g_v) = uv[1]
+ else:
+ x_v = None
+ y_v = None
+ g_v = 1
+
+ # (x_u, y_u, g_u), (x_v, y_v, g_v) = uv
+ b_list, c_list = frak_bc
+
+ N1, b_cof, b_twopower, frak_b_list = b_list
+ N2, c_cof, c_twopower, frak_c_list = c_list
+
+ # assert N1 * g_u * (x_u**2 + y_u**2) + N2 * g_v * (x_v**2 + y_v**2) == 2**e
+
+ def div_by_n(ideal, n):
+ generators = [gi / n for gi in ideal.gens()]
+ return sum([self.max_order * gi for gi in generators])
+
+ frak_b = ideal_to_sage(frak_b_list, self.max_order)
+ frak_c = ideal_to_sage(frak_c_list, self.max_order)
+
+ if b_twopower > 1: # factors through two
+ frak_b = div_by_n(frak_b, 2)
+ if c_twopower > 1:
+ frak_c = div_by_n(frak_c, 2)
+
+ #The part thats the same for b and c
+ frak_bc_same = frak_b + frak_c
+
+ #We only want the "unique" part of b and c
+ frak_b = div_by_n(frak_b * Conjugate(self.max_order, frak_bc_same), frak_bc_same.norm())
+ frak_c = div_by_n(frak_c * Conjugate(self.max_order, frak_bc_same), frak_bc_same.norm())
+
+ frak_b_2 = frak_b + self.max_order*((2**frak_b.norm().valuation(2)))
+ frak_c_2 = frak_c + self.max_order*((2**frak_c.norm().valuation(2)))
+
+ odd_part = Integer(frak_bc_same.norm()).prime_to_m_part(2)
+ pow2_part = frak_bc_same.norm()/odd_part
+ frak_bc_same_2 = frak_bc_same + self.max_order*pow2_part
+ frak_bc_same_odd = frak_bc_same + self.max_order*odd_part
+
+ b_cof = b_cof/odd_part
+ c_cof = c_cof/odd_part
+
+ #First step to the starting curve given by same direction steps
+ ee = valuation(self.p+1, 2) - 1
+ logger.debug("Walking to starting point (i.e. steps that are same for b and c)...")
+ if frak_bc_same.norm() > 1:
+ if pow2_part > 1:
+ P_temp, Q_temp = self.TwoTorsBasis(E, ee)
+ #P_temp, Q_temp = TwoTorsBasis(E, two_pow)
+ _, E = self.ideal_above_2_action(E, P_temp, Q_temp, ee, frak_bc_same_2)
+ if odd_part > 1:
+ _, E = self.smooth_ideal_action(E, odd_part, frak_bc_same_odd)
+ logger.debug(f"Done! Have used {time() - tstart}")
+ P, Q = self.TwoTorsBasis(E, ee)
+ #P, Q = TwoTorsBasis(E, ee)
+
+ # Doing the Elkies steps for b
+ logger.debug("odd Elkies steps for b and conjugate of c")
+ # These are really different directions, so we can
+ isogs_cof, E1 = self.smooth_ideal_action(E, b_cof*c_cof, frak_b*Conjugate(self.max_order, frak_c))
+ P1, Q1 = P, Q
+ for phi in isogs_cof:
+ P1 = P1.push(phi)
+ Q1 = Q1.push(phi)
+ logger.debug(f"Done! Have used {time() - tstart}")
+
+ #Adjust the order
+ P1, Q1 = P1.xMUL(2**(ee - (e+2))), Q1.xMUL(2**(ee - (e+2)))
+
+ assert P1.xMUL(2**(e+1))
+ assert not P1.xMUL(2**(e+2))
+ assert Q1.xMUL(2**(e+1))
+ assert not Q1.xMUL(2**(e+2))
+ #At this point, P1, Q1 are the "starting points"
+
+ logger.debug("Doing g_u isogeny")
+ g_u_isogs, Eu = self.random_smooth_isogeny(E1, g_u)
+ Pu, Qu = P1, Q1
+ for phi_gu in g_u_isogs:
+ Pu = Pu.push(phi_gu)
+ Qu = Qu.push(phi_gu)
+ logger.debug(f"Done! Have used {time() - tstart}")
+
+ # Now the second part
+ # Evaluating the endomorphism
+ logger.debug("Evaluating the endomorphism frak_b*frac_c_bar")
+ rho_bc = PrincipalGenerator(frak_b*Conjugate(self.max_order, frak_c))
+ rho_bc_val_2 = rho_bc.norm().valuation(2)
+
+ #assert (frak_b*frak_c).norm() == rho_bc.norm()
+ #assert rho_bc in frak_b
+ #assert rho_bc.conjugate() in frak_c
+
+ #adjust_2 = 0
+ #if rho_bc_val_2 > 0: #Here we really need to clear denominators to evaluate the endomorphism
+ # rho_bc *= 2
+ # adjust_2 = 1
+
+ #print("!!!!!!!!!")
+ #print(ee-(rho_bc_val_2))
+ #print(e+2)
+ #assert ee-(rho_bc_val_2 + adjust_2) >= e+2, "NotImplemented: We can drop adjust_2 if we allow extension-fields"
+ #print(f"Evaluating {rho_bc}")
+ max = (ee - rho_bc_val_2 == e+2)
+ Pc = self.eval_endomorphism(rho_bc, P, False, max)
+ Qc = self.eval_endomorphism(rho_bc, Q, True, max)
+
+ logger.debug(f"Done! Have used {time() - tstart}")
+
+ # Act with 2-ideals right away.
+ logger.debug("Acting with 2-part of frak_b and frak_c")
+ #print(f"!!!! {frak_c_2.norm(), frak_b_2.norm()}")
+ #print(f"!!!? {rho_bc_val_2}")
+ if frak_c_2.norm() > 1 or frak_b_2.norm() > 1:
+ frak_bc_2 = frak_c_2*Conjugate(self.max_order, frak_b_2)
+ phi_2, E = self.ideal_above_2_action(E, P, Q, ee, frak_bc_2)
+ Pc = Pc.push(phi_2)
+ Qc = Qc.push(phi_2)
+
+ logger.debug(f"Done! Have used {time() - tstart}")
+ P2 = Pc.xMUL(2**(ee - (e+2) - (rho_bc_val_2)))
+ Q2 = Qc.xMUL(2**(ee - (e+2) - (rho_bc_val_2)))
+
+ assert P2.xMUL(2**(e+1))
+ assert not P2.xMUL(2**(e+2))
+ assert Q2.xMUL(2**(e+1))
+ assert not Q2.xMUL(2**(e+2))
+ E2 = E
+
+ logger.debug("Doing g_v isogeny")
+ Pv, Qv = P2, Q2
+
+ g_v_isogs, Ev = self.random_smooth_isogeny(E2, g_v)
+
+ for phi_gv in g_v_isogs:
+ Pv = Pv.push(phi_gv)
+ Qv = Qv.push(phi_gv)
+ logger.debug(f"Done! Have used {time() - tstart}")
+
+ Ev = Pv.curve.change_ring(self.Fp2)
+ Eu = Pu.curve.change_ring(self.Fp2)
+
+ Pv = Ev.lift_x(Pv.X)
+ Qv = Ev.lift_x(Qv.X)
+
+ Pu = Eu.lift_x(Pu.X)
+ Qu = Eu.lift_x(Qu.X)
+
+ #Make sure to lift correctly
+ if Pu.weil_pairing(Qu, 2**(e+2))**(g_v*N1*N2) != Pv.weil_pairing(Qv, 2**(e+2))**(g_u):
+ Qv = -Qv
+
+ # Double check full order
+ assert Pu.weil_pairing(Qu, 2**(e+2))**(2**(e+2)) == 1
+ assert Pu.weil_pairing(Qu, 2**(e+2))**(2**(e+1)) != 1
+ assert Pv.weil_pairing(Qv, 2**(e+2))**(2**(e+2)) == 1
+ assert Pv.weil_pairing(Qv, 2**(e+2))**(2**(e+1)) != 1
+ # Weil pairing check
+ assert Pu.weil_pairing(Qu, 2**(e+2))**(g_v*N1*N2) == Pv.weil_pairing(Qv, 2**(e+2))**(g_u)
+
+ return N1, N2, x_u, y_u, g_u, x_v, y_v, g_v, Pu, Qu, Pv, Qv
+
+
+ def sample_ideal(self):
+ r"""
+ Samples a random ideal of End(E)
+ """
+ N_a, frak_a = RandomDegreeOnePrimeIdeal(self.order, self.w, self.p)
+ return frak_a
+
+ def ideal_above_2_action(self, E, P, Q, basis_ord, frak_a):
+ r"""
+ Computes the action of a power of an ideal above 2 using Velu.
+ Input:
+ - E: Elliptic curve over Fp
+ - P, Q: Basis of E[2^basis_ord], where P is in E(Fp) and Q is in Et(Fp) for a quadratic twist Et
+ - basis_ord: The order of P, Q (log_2)
+ - frak_a: A power of an ideal above 2
+ Output:
+ - phi : The isogeny corresponding to frak_a
+ - frak_a * E
+ """
+ _, val_2_frak_a = factor(frak_a.norm())[0]
+
+ if (self.w - 1)/2 in frak_a:
+ K = P
+ else:
+ assert (self.w + 1)/2 in frak_a
+ K = Q
+
+ K = K.xMUL(2**(basis_ord - val_2_frak_a))
+ assert K.xMUL(2**(val_2_frak_a - 1))
+ assert not K.xMUL(2**val_2_frak_a)
+
+ phi_2 = K.xMUL(2**(val_2_frak_a-1)).xISOG(E, 2)
+ phi = phi_2
+ for steps in range(1, val_2_frak_a):
+ E = phi.codomain()
+ K = K.push(phi_2)
+ assert K.xMUL(2**(val_2_frak_a-1-steps))
+ assert not K.xMUL(2**(val_2_frak_a-steps))
+ phi_2 = K.xMUL(2**(val_2_frak_a-1-steps)).xISOG(E, 2)
+ phi = phi_2 * phi
+
+ E_out = phi.codomain().montgomery_model()
+
+ return phi.codomain().isomorphism_to(E_out) * phi, E_out
+
+
+ def small_prime_ideal_action(self, E, ell, lam=None, prev_j=None):
+ r"""
+ Computes the action of a an ideal above ell using Elkies algorithm.
+ Input:
+ - E: Elliptic curve over Fp IN WEIERSTRASS FORM
+ - ell: A small prime, split in End(E)
+ - lam (optional): The eigenvalue determining the ideal frak_l = (ell, pi - lam)
+ - prev_j (optional): The j-invariant of the WRONG ell-isogenous curve
+ Output:
+ - phi : The isogeny corresponding to an ideal above ell
+ """
+ # Fix later, but Elkies uses short Weierstrass
+ # Change ring stuff is messy, but needed to actually compute the rational model
+ assert isWeierstrass(E)
+ h = Elkies(E, ell, self.Fp, lam=lam, prev_j=prev_j)
+ phi = E.isogeny(h)
+ return phi
+
+
+ def small_prime_degree_isogeny(self, E, ell):
+ r"""
+ Computes an (unspecified) isogeny of degree ell
+ Input:
+ - E: Elliptic curve over Fp
+ - ell: A small prime, not necessarily split in End(E)
+ Output:
+ - phi : An isogeny from E of degree ell
+ """
+ # Just a random isogeny of degree ell
+ # Not (necessarily) oriented (if prime is Elkies, we should use the other)
+ Fbig, k, ontwist = self.ell_to_Fpk[ell]
+ Ebig = E.change_ring(Fbig)
+ if ontwist:
+ Ebig_ord = self.p**k + (-1)**k
+ else:
+ Ebig_ord = self.p**k - (-1)**k
+ assert not Ebig_ord % ell, f"ehh {(self.p**k + (-1)**k) % ell}, {(self.p**k - (-1)**k) % ell}"
+
+ cof = Ebig_ord//ell
+
+ # Picking point smarter makes this easier
+ K = None
+ while not K:
+ K = random_xPoint(Ebig, Fbig, ontwist)
+ # TODO: Clear some cofactors by working in trace 0 part
+ K = K.xMUL(cof)
+
+ phi = K.xISOG(E, ell)
+ return phi
+
+
+ def _adjust_2_tors_order(self, P, Q, e, return_scalars = False):
+ r"""
+ Multiplies P, Q by suitable cofactors so they form a basis of E[2**e]
+ Input:
+ - P, Q, points of order at least 2**e, so that E[2**e] \subseteq <P, Q>
+ - e: Exponent of 2.
+ Output:
+ - Pm, Qm: Basis of E[2**e]
+ - Optional: The scalars a, b so that Pm = a*P, Qm = b*Q
+ """
+ assert P.xMUL((2**(e-1)))
+ assert Q.xMUL((2**(e-1)))
+ P_small = P.xMUL(2**e)
+ scal_1 = 1
+ scal_2 = 1
+ while P_small:
+ P = P.xMUL(2)
+ P_small = P_small.xMUL(2)
+ scal_1 *= 2
+ Q_small = Q.xMUL(2**e)
+ while Q_small:
+ Q = Q.xMUL(2)
+ Q_small = Q_small.xMUL(2)
+ scal_2 *= 2
+ if return_scalars:
+ return P, Q, scal_1, scal_2
+ return P, Q
+
+
+ def smooth_ideal_action(self, E, cof, frak):
+ r"""
+ Computes the action of a an ideal of odd, smooth norm using Elkies algorithm repeatedly.
+ Input:
+ - E: Elliptic curve over Fp
+ - cof: norm of frak
+ - frak: An ideal of End(E)
+ Output:
+ - phi : The isogeny corresponding to frak
+ - frak * E
+ """
+ # Evaluate a smooth, odd normed ideal
+ isogs = []
+ E1 = E.short_weierstrass_model()
+ pre_isom = E.isomorphism_to(E1)
+ isogs.append(pre_isom)
+ if cof > 1:
+ for ell_cof, e_cof in factor(cof):
+ lam = Integer(GF(ell_cof)(-self.p).sqrt())
+ if not self.w - lam in frak + ell_cof*self.max_order:
+ # In this case, we want the other one
+ lam = ell_cof - lam
+ assert self.w - lam in frak + ell_cof*self.max_order
+ prev_j = None
+ for _ in range(e_cof):
+ logger.debug(f'Starting Elkies step of degree {ell_cof}')
+ phi_part = self.small_prime_ideal_action(E1, ell_cof, lam, prev_j)
+ prev_j = E1.j_invariant()
+ E1 = phi_part.codomain()
+ isogs.append(phi_part)
+
+ E_out = isogs[-1].codomain().montgomery_model()
+ post_isom = isogs[-1].codomain().isomorphism_to(E_out)
+ isogs.append(post_isom)
+ return isogs, E_out
+
+
+ def random_smooth_isogeny(self, E, g):
+ r"""
+ Returns a random isogeny of smooth degree g from E
+ Input:
+ - E: Elliptic curve over Fp
+ - g: Smooth number
+ Output:
+ - phi : An isogeny from E of degree g
+ - E_out : phi(E)
+ """
+ # Random smooth odd norm isogeny
+ isogs = []
+ if g > 1:
+ E1 = E.short_weierstrass_model()
+ pre_isom = E.isomorphism_to(E1)
+ isogs.append(pre_isom)
+ logger.debug(f"Extra isogeny for g = {factor(g)}")
+ # Do elkies first, to stay over Fp
+ g_elkies = []
+ g_non_elkies = []
+ for ell, e in factor(g):
+ if kronecker_symbol(-self.p, ell) == 1:
+ g_elkies.append((ell, e))
+ else:
+ g_elkies.append((ell, e))
+ for ell, e in g_elkies:
+ prev_j = None
+ for _ in range(e):
+ phi_ell = self.small_prime_ideal_action(E1, ell, prev_j = prev_j)
+ prev_j = E1.j_invariant()
+ E1 = phi_ell.codomain()
+ isogs.append(phi_ell)
+
+ E_out = isogs[-1].codomain().montgomery_model()
+ post_isom = isogs[-1].codomain().isomorphism_to(E_out)
+ isogs.append(post_isom)
+
+ for ell, e in g_non_elkies:
+ for _ in range(e):
+ phi_ell = self.small_prime_degree_isogeny(E_out, ell)
+ E_out = phi_ell.codomain()
+ isogs.append(phi_ell)
+
+ return isogs, E_out
+ else:
+ return [], E
+
+
+ def eval_endomorphism(self, rho, P, twist, max):
+ r"""
+ Evaluates an element of End(E) on a point P.
+ P is assumed to be Fp rational on either E or on a twist
+ """
+ a, b = list(rho) #write as a + b*pi
+ n = rho.denominator()
+ if twist:
+ #a + b*pi = a - b, since pi(P) = -P
+ m = Integer(a - b)
+ else:
+ #a + b*pi = a + b, since pi(P) = P
+ m = Integer(a + b)
+ if max and (n == 2): #See Appendix D.2
+ mP = P.xMUL(m)
+ E = P.curve
+
+ T0 = self.find_Ts(E, only_T0 = True)
+
+ #Already know which point to choose, Remark D.2
+ assert (P.X - T0.x()).is_square() != twist
+
+ T = xPoint(T0.x(), E)
+
+ return translate_by_T(mP, T)
+
+ else:
+ return P.xMUL(m)
+
+
+ def TwoTorsBasis(self, E, e):
+ r"""
+ Fast sampling of a basis P, Q of E[2**e], such that x(P) and x(Q) are both defined over Fp
+ Input:
+ - E: Elliptic curve over Fp
+ - e: Exponent
+ Output:
+ - P, Q: Basis of E[2**e] so that P is in E(Fp), and Q is in E^t(Fp) for an Fp-twist of E.
+ """
+ logger.debug(" > Finding TwoTorsionBasis")
+ tstart = time()
+
+ T0, Tm1, T1 = self.find_Ts(E)
+
+ A = MontgomeryA(E)
+ F = E.base_field()
+ R = F["X"]
+ X = R.gens()[0]
+ f = X**2 + A*X + 1
+
+ logger.debug(f" > Done, have used {time()-tstart} sec")
+
+ logger.debug(" > sample xP")
+ xT0 = T0.x()
+ xP = xT0 + F.random_element()**2
+ while not (f(xP)*xP).is_square() or (xP-Tm1.x()).is_square():
+ xP = xT0 + F.random_element()**2
+
+ logger.debug(" > sample xQ")
+ xQ = xT0 - F.random_element()**2
+ while (f(xQ)*xQ).is_square() or not ((xQ-T1.x()).is_square()):
+ xQ = xT0 - F.random_element()**2
+
+ logger.debug(f" > Done, have used {time()-tstart} sec")
+ #print(T0.tate_pairing(E.lift_x(xP), 2, 1))
+
+ P = xPoint(xP, E)
+ Q = xPoint(xQ, E)
+
+ assert (self.p+1) % 2**(e+1) == 0
+ cofac = (self.p+1)/2**(e+1)
+ P = P.xMUL(cofac)
+ Q = Q.xMUL(cofac)
+
+ assert P.xMUL(2**(e-1))
+ assert not P.xMUL(2**e)
+ assert Q.xMUL(2**(e-1))
+ assert not Q.xMUL(2**e)
+ assert Q.xMUL(2**(e-1)) != P.xMUL(2**(e-1))
+
+ logger.debug(f" > Total time for 2-tors basis finding: {time()-tstart}")
+
+ return P, Q
+
+ def find_Ts(self, E, only_T0 = False):
+ r"""
+ Given a curve E, finds and marks the non-trivial
+ 2-torsion points according to Lemma D.1
+ """
+ A = MontgomeryA(E)
+ F = E.base_field()
+ R = F["X"]
+ X = R.gens()[0]
+ f = X**2 + A*X + 1
+ logger.debug(" > root finding")
+ lam1, lam2 = f.roots(multiplicities=False)
+
+ R1 = E(lam1, 0)
+ R2 = E(lam2, 0)
+ R3 = E(0, 0)
+
+ #Find T0
+ Rs = [R1, R2]
+ for T in Rs:
+ if T.tate_pairing(T, 2, 1) != 1:
+ T0 = T
+ Rs.remove(T)
+ break
+
+ assert T0
+ if only_T0:
+ return T0
+
+ assert T0.tate_pairing(T0, 2, 1) == -1
+ Rs.append(R3)
+ for T in Rs:
+ if T.tate_pairing(T0, 2, 1) == 1:
+ Tm1 = T
+ Rs.remove(T)
+ break
+
+ assert Tm1
+ T1 = Rs[0]
+ assert T1.tate_pairing(T0, 2, 1) != 1
+
+ return T0, Tm1, T1