Repository: sagemath/sage

Issue:
sparse strategies for composite elliptic-curve isogenies
<div id="comment:0"></div>

Some cryptographic applications (most prominently, https://sike.org/) rely on computing isogenies of huge prime-power degrees quickly. The current algorithm used for this in Sage (since #32744) is quadratic in the number of prime-degree steps. It is known from the SIKE literature how to do better, replacing quadratic by quasilinear scaling.

This patch implements a simple version of the quasilinear algorithm. I haven't found any example where the change incurs a noticeable slowdown, while (as shown below) there can be quite significant speedups.

Example:

```sage
sage: F = GF((2^216*3^137-1, 2))
sage: E.<P,Q> = EllipticCurve(F, [1,0])
sage: %timeit E.isogeny(P, algorithm="factored")
```

Sage 9.7.beta6:

```
2.19 s ± 341 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
```

This branch:

```
1.04 s ± 13.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
```

CC:  @defeo @JohnCremona @categorie

Component: **elliptic curves**

Author: **Lorenz Panny**

_Issue created by migration from https://trac.sagemath.org/ticket/34239_




Discussion:

Resolving patch:
diff --git a/src/sage/schemes/elliptic_curves/hom_composite.py b/src/sage/schemes/elliptic_curves/hom_composite.py
--- a/src/sage/schemes/elliptic_curves/hom_composite.py
+++ b/src/sage/schemes/elliptic_curves/hom_composite.py
@@ -92,8 +92,6 @@
 from sage.schemes.elliptic_curves.ell_curve_isogeny import EllipticCurveIsogeny
 from sage.schemes.elliptic_curves.weierstrass_morphism import WeierstrassIsomorphism, identity_morphism
 
-# TODO: Implement sparse strategies? (cf. the SIKE cryptosystem)
-
 
 def _eval_factored_isogeny(phis, P):
     """
@@ -120,33 +118,100 @@ def _eval_factored_isogeny(phis, P):
     return P
 
 
-def _compute_factored_isogeny_prime_power(P, l, e):
-    """
-    This method takes a point `P` of order `l^e` and returns
-    a sequence of degree-`l` isogenies whose composition has
+def _compute_factored_isogeny_prime_power(P, l, n, split=.8):
+    r"""
+    This method takes a point `P` of order `\ell^n` and returns
+    a sequence of degree-`\ell` isogenies whose composition has
     the subgroup generated by `P` as its kernel.
 
+    The optional argument ``split``, a real number between
+    `0` and `1`, controls the *strategy* used to compute the
+    isogeny: In general, the algorithm performs a number of
+    scalar multiplications `[\ell]` and a number of
+    `\ell`-isogeny evaluations, and there exist tradeoffs
+    between them.
+
+    - Setting ``split`` to `0` skews the algorithm towards
+      isogenies, minimizing multiplications.
+      The asymptotic complexity is `O(n \log(\ell) + n^2 \ell)`.
+
+    - Setting ``split`` to `1` skews the algorithm towards
+      multiplications, minimizing isogenies.
+      The asymptotic complexity is `O(n^2 \log(\ell) + n \ell)`.
+
+    - Values strictly between `0` and `1` define *sparse*
+      strategies, which balance the number of isogenies and
+      multiplications according to the parameter.
+      The asymptotic complexity is `O(n \log(n) \ell)`.
+
+    .. NOTE::
+
+        As of July 2022, good values for ``split`` range somewhere
+        between roughly `0.6` and `0.9`, depending on the size of
+        `\ell` and the cost of base-field arithmetic.
+
+    REFERENCES:
+
+    Sparse strategies were introduced in [DJP2014]_, §4.2.2.
+
+    ALGORITHM:
+
+    The splitting rule implemented here is a simple heuristic which
+    is usually not optimal. The advantage is that it does not rely
+    on prior knowledge of degrees and exact costs of elliptic-curve
+    arithmetic, while still working reasonably well for a fairly
+    wide range of situations.
+
     EXAMPLES::
 
         sage: from sage.schemes.elliptic_curves import hom_composite
         sage: E = EllipticCurve(GF(8191), [1,0])                                        # optional - sage.rings.finite_rings
         sage: P = E.random_point()                                                      # optional - sage.rings.finite_rings
-        sage: (l,e), = P.order().factor()                                               # optional - sage.rings.finite_rings
-        sage: phis = hom_composite._compute_factored_isogeny_prime_power(P, l, e)       # optional - sage.rings.finite_rings
+        sage: (l,n), = P.order().factor()                                               # optional - sage.rings.finite_rings
+        sage: phis = hom_composite._compute_factored_isogeny_prime_power(P, l, n)       # optional - sage.rings.finite_rings
         sage: hom_composite._eval_factored_isogeny(phis, P)                             # optional - sage.rings.finite_rings
         (0 : 1 : 0)
-        sage: [phi.degree() for phi in phis] == [l]*e                                   # optional - sage.rings.finite_rings
+        sage: [phi.degree() for phi in phis] == [l]*n                                   # optional - sage.rings.finite_rings
+        True
+
+    All choices of ``split`` produce the same result, albeit
+    not equally fast::
+
+        sage: E = EllipticCurve(GF(2^127 - 1), [1,0])                                           # optional - sage.rings.finite_rings
+        sage: P, = E.gens()                                                                     # optional - sage.rings.finite_rings
+        sage: (l,n), = P.order().factor()                                                       # optional - sage.rings.finite_rings
+        sage: phis = hom_composite._compute_factored_isogeny_prime_power(P,l,n)                 # optional - sage.rings.finite_rings
+        sage: phis == hom_composite._compute_factored_isogeny_prime_power(P,l,n, split=0)       # optional - sage.rings.finite_rings
+        True
+        sage: phis == hom_composite._compute_factored_isogeny_prime_power(P,l,n, split=0.1)     # optional - sage.rings.finite_rings
+        True
+        sage: phis == hom_composite._compute_factored_isogeny_prime_power(P,l,n, split=0.5)     # optional - sage.rings.finite_rings
+        True
+        sage: phis == hom_composite._compute_factored_isogeny_prime_power(P,l,n, split=0.9)     # optional - sage.rings.finite_rings
+        True
+        sage: phis == hom_composite._compute_factored_isogeny_prime_power(P,l,n, split=1)       # optional - sage.rings.finite_rings
         True
     """
-    E = P.curve()
-    phis = []
-    for i in range(e):
-        K = l**(e-1-i) * P
-        phi = EllipticCurveIsogeny(E, K)
-        E = phi.codomain()
-        P = phi(P)
-        phis.append(phi)
-    return phis
+    def rec(Q, k):
+
+        if k == 1:
+            # base case: Q has order l
+            return [EllipticCurveIsogeny(Q.curve(), Q)]
+
+        # recursive case: k > 1 and Q has order l^k
+
+        k1 = int(k * split + .5)
+        k1 = max(1, min(k - 1, k1))  # clamp to [1, k - 1]
+
+        Q1 = l**k1 * Q
+        L = rec(Q1, k - k1)
+
+        Q2 = _eval_factored_isogeny(L, Q)
+        R = rec(Q2, k1)
+
+        return L + R
+
+    return rec(P, n)
 
 
 def _compute_factored_isogeny_single_generator(P):
@@ -196,7 +261,7 @@ def _compute_factored_isogeny(kernel):
     phis = []
     ker = list(kernel)
     while ker:
-        K, ker = ker[0], ker[1:]
+        K = ker.pop(0)
         psis = _compute_factored_isogeny_single_generator(K)
         ker = [_eval_factored_isogeny(psis, P) for P in ker]
         phis += psis


Test patch: