Example #1
0
        def __pow__(self, e):
            """
			Exponentiation

			...
			Parameters
			----------
				- self, which is an element of a Prime Field
				- an integer e
			Returns
			-------
				- self raised to e
			-----
			Usage:
				- self.pow(e)
				- self ** e
			Notes
			-----
				- This is a constant-time implementation by using the left-to-right method
				- It allows negative exponents, but any exponent is expected to belong to |[ 0 .. p - 1 ]|
			"""
            if e == 0:
                return FiniteField(1)

            elif e < 0:
                return self.inverse()**(-e)

            else:
                self.field.fpsqr += (bitlength(e) - 1)
                self.field.fpmul += (hamming_weight(e) - 1)
                return FiniteField(pow(self.x, e, self.field.p))
Example #2
0
 def cISOG(self, l):
     numpy.array(
         [
             (
                 3.0 * l
                 + 2.0 * hamming_weight(l)
                 - 9.0
                 + isequal[l == 3] * 4.0
             ),
             (l + 2.0 * bitlength(l) + 1.0 + isequal[l == 3] * 2.0),
             (3.0 * l - 7.0 + isequal[l == 3] * 6.0),
         ]
     )
Example #3
0
def Svelu(curve, tuned, multievaluation):
    fp = curve.fp
    poly_mul = Poly_mul(curve)
    poly_redc = Poly_redc(poly_mul)
    global_L = curve.L
    prime = curve.prime
    SCALED_REMAINDER_TREE = multievaluation
    n = parameters["csidh"][prime]["n"]

    cEVAL = lambda l: numpy.array([2.0 * (l - 1.0), 2.0, (l + 1.0)])
    cISOG = lambda l: numpy.array([
        (3.0 * l + 2.0 * hamming_weight(l) - 9.0 + isequal[l == 3] * 4.0),
        (l + 2.0 * bitlength(l) + 1.0 + isequal[l == 3] * 2.0),
        (3.0 * l - 7.0 + isequal[l == 3] * 6.0),
    ])

    C_xEVAL = list(
        map(cEVAL,
            global_L))  # list of the costs of each degree-l isogeny evaluation
    C_xISOG = list(map(
        cISOG,
        global_L))  # list of the costs of each degree-l isogeny construction

    # Global variables to be used in KPs, xISOG, and xEVAL

    # Here, J is a set of cardinality sJ
    J = None
    sJ = None

    # Here, ptree_I corresponds with the product tree determined by I, and I is a set of cardinality sJ
    ptree_hI = None
    sI = (None, )

    # Here, K is a set of cardinality sK
    K = None
    sK = None

    # An extra nonlocal variable which is used in xISOG and xEVAL
    XZJ4 = None

    # Next functions is used for setting the cardinalities sI, sJ, and sK
    def set_parameters_velu(b, c, i):

        nonlocal sJ
        nonlocal sI
        nonlocal sK

        assert b <= c

        # At this step, everythin is correct
        sJ = b
        sI = c
        d = ((global_L[i] - 2 - 4 * b * c - 1) // 2) + 1
        assert d >= 0
        sK = d
        return None

    def print_parameters_velu():

        print("| sI: %3d, sJ: %3d, sK: %3d |" % (sI, sJ, sK), end="")
        return None

    # KPs computes x([i]P), x([j]P), and x([k]P) for each i in I, j in J, and j in J.
    # I, J, and K are defined according to Examples 4.7 and 4.12 of https://eprint.iacr.org/2020/341
    def KPs(P, A, i):

        # Global variables to be used
        nonlocal J
        nonlocal sJ
        nonlocal ptree_hI
        nonlocal sI
        nonlocal K
        nonlocal sK

        # This functions computes all the independent data from the input of algorithm 2 of https://eprint.iacr.org/2020/341
        if sI == 0:

            # Case global_L[i] = 3 is super very special case (nothing to do)
            J = []
            ptree_hI = None
            K = [list(P)]
            # J, b, ptree_hI, c, K, d
            assert sJ == 0 and sI == 0 and sK == 1
            return None

        # We need to ensure sI is greater than or equal sJ
        assert sI >= sJ

        # Additionally, sK should be greater than or equal to zero. If that is not the case, then sJ and sI were badly chosen
        assert sK >= 0

        if sI == 1:
            # Branch corresponds with global_L[i] = 5 and global_L[i] = 7
            # Recall sJ > 0, then sJ = 1
            assert sJ == 1
            P2 = curve.xDBL(P, A)

            J = [list(P)]

            I = [list(P2)]
            hI = [list([fp.fp_sub(0, P2[0]), P2[1]])
                  ]  # we only need to negate x-coordinate of each point
            ptree_hI = poly_mul.product_tree(hI, sI)  # product tree of hI

            if not SCALED_REMAINDER_TREE:
                # Using remainder trees
                ptree_hI = poly_redc.reciprocal_tree(
                    {
                        'rpoly': [1],
                        'rdeg': 0,
                        'fpoly': [1],
                        'fdeg': 0,
                        'a': 1
                    },
                    2 * sJ + 1,
                    ptree_hI,
                    sI,
                )  # reciprocal of the root is used to compute sons' reciprocals

            else:
                # Using scaled remainder trees
                assert (2 * sJ - sI + 1) > sI
                ptree_hI['reciprocal'], ptree_hI['a'] = poly_redc.reciprocal(
                    ptree_hI['poly'][::-1], sI + 1, 2 * sJ - sI + 1)
                ptree_hI['scaled'], ptree_hI['as'] = (
                    list(ptree_hI['reciprocal'][:sI]),
                    ptree_hI['a'],
                )

            # Notice, 0 <= sK <= 1
            assert sK <= 1
            if sK == 1:
                K = [list(P2)]
            else:
                K = []

            return None

        # At this step, sI > 1
        assert sI > 1
        if sJ == 1:
            # This branch corresponds with global_L[i] = 11 and global_L[i] = 13
            Q = curve.xDBL(P, A)  # x([2]P)
            Q2 = curve.xDBL(Q, A)  # x([2]Q)

            J = [list(P)]

            I = [[0, 0]] * sI
            I[0] = list(Q)  # x(   Q)
            I[1] = curve.xADD(Q2, I[0], I[0])  # x([3]Q)
            for ii in range(2, sI, 1):
                I[ii] = curve.xADD(I[ii - 1], Q2, I[ii - 2])  # x([2**i + 1]Q)

            hI = [[fp.fp_sub(0, iP[0]), iP[1]] for iP in I
                  ]  # we only need to negate x-coordinate of each point
            ptree_hI = poly_mul.product_tree(hI, sI)  # product tree of hI

            if not SCALED_REMAINDER_TREE:
                # Using remainder trees
                ptree_hI = poly_redc.reciprocal_tree(
                    {
                        'rpoly': [1],
                        'rdeg': 0,
                        'fpoly': [1],
                        'fdeg': 0,
                        'a': 1
                    },
                    2 * sJ + 1,
                    ptree_hI,
                    sI,
                )  # reciprocal of the root is used to compute sons' reciprocals

            else:
                # Using scaled remainder trees
                assert (2 * sJ - sI + 1) <= sI
                ptree_hI['scaled'], ptree_hI['as'] = poly_redc.reciprocal(
                    ptree_hI['poly'][::-1], sI + 1, sI)
                ptree_hI['reciprocal'], ptree_hI['a'] = (
                    list(ptree_hI['scaled'][:(2 * sJ - sI + 1)]),
                    ptree_hI['as'],
                )

            # Notice, 0 <= sK <= 1
            assert sK <= 1
            if sK == 1:
                K = [list(Q)]
            else:
                K = []

            return None

        # Now, we ensure sI >= sJ > 1
        assert sJ > 1

        # In other words, we can proceed by the general case

        # ------------------------------------------------
        # Computing [j]P for each j in {1, 3, ..., 2*sJ - 1}
        J = [[0, 0]] * sJ
        J[0] = list(P)  # x(   P)
        P2 = curve.xDBL(P, A)  # x([2]P)
        J[1] = curve.xADD(P2, J[0], J[0])  # x([3]P)
        for jj in range(2, sJ, 1):
            J[jj] = curve.xADD(J[jj - 1], P2, J[jj - 2])  # x([2*jj + 1]P)

        # -------------------------------------------------------
        # Computing [i]P for i in { (2*sJ) * (2i + 1) : 0 <= i < sI}
        bhalf_floor = sJ // 2
        bhalf_ceil = sJ - bhalf_floor
        P4 = curve.xDBL(P2, A)  # x([4]P)
        P2[0], P4[0] = fp.fp_cswap(P2[0], P4[0], sJ % 2)  # Constant-time swap
        P2[1], P4[1] = fp.fp_cswap(
            P2[1], P4[1], sJ % 2)  # x([4]P) <--- coditional swap ---> x([2]P)
        Q = curve.xADD(J[bhalf_ceil], J[bhalf_floor - 1], P2)  # Q := [2b]P
        P2[0], P4[0] = fp.fp_cswap(P2[0], P4[0], sJ % 2)  # Constant-time swap
        P2[1], P4[1] = fp.fp_cswap(
            P2[1], P4[1], sJ % 2)  # x([4]P) <--- coditional swap ---> x([2]P)

        I = [[0, 0]] * sI
        I[0] = list(Q)  # x(   Q)
        Q2 = curve.xDBL(Q, A)  # x([2]Q)
        I[1] = curve.xADD(Q2, I[0], I[0])  # x([3]Q)
        for ii in range(2, sI, 1):
            I[ii] = curve.xADD(I[ii - 1], Q2, I[ii - 2])  # x([2**i + 1]Q)

        # --------------------------------------------------------------
        # Computing [k]P for k in { 4*sJ*sI + 1, ..., l - 6, l - 4, l - 2}
        K = [[0, 0]] * sK

        if sK >= 1:
            K[0] = list(P2)  # x([l - 2]P) = x([-2]P) = x([2]P)

        if sK >= 2:
            K[1] = list(P4)  # x([l - 4]P) = x([-4]P) = x([4]P)

        for k in range(2, sK, 1):
            K[k] = curve.xADD(K[k - 1], P2, K[k - 2])

        # ------------------------------------------------------------------------------------------------------
        #                   ~~~~~~~~               ~~~~~~~~
        #                    |    |                 |    |
        # Computing h_I(W) = |    | (W - x([i]P)) = |    | (Zi * W - Xi) / Zi where x([i]P) = Xi/Zi
        #                    i in I                 i in I
        # In order to avoid costly inverse computations in fp, we are gonna work with projective coordinates

        hI = [[fp.fp_sub(0, iP[0]), iP[1]]
              for iP in I]  # we only need to negate x-coordinate of each point
        ptree_hI = poly_mul.product_tree(hI, sI)  # product tree of hI

        if not SCALED_REMAINDER_TREE:
            # Using scaled remainder trees
            ptree_hI = poly_redc.reciprocal_tree(
                {
                    'rpoly': [1],
                    'rdeg': 0,
                    'fpoly': [1],
                    'fdeg': 0,
                    'a': 1
                },
                2 * sJ + 1,
                ptree_hI,
                sI,
            )  # reciprocal of the root is used to compute sons' reciprocals

        else:
            # Using scaled remainder trees
            if sI < (2 * sJ - sI + 1):
                ptree_hI['reciprocal'], ptree_hI['a'] = poly_redc.reciprocal(
                    ptree_hI['poly'][::-1], sI + 1, 2 * sJ - sI + 1)
                ptree_hI['scaled'], ptree_hI['as'] = (
                    list(ptree_hI['reciprocal'][:sI]),
                    ptree_hI['a'],
                )

            else:
                ptree_hI['scaled'], ptree_hI['as'] = poly_redc.reciprocal(
                    ptree_hI['poly'][::-1], sI + 1, sI)
                ptree_hI['reciprocal'], ptree_hI['a'] = (
                    list(ptree_hI['scaled'][:(2 * sJ - sI + 1)]),
                    ptree_hI['as'],
                )

        # Polynomial D_j of algorithm 2 from https://eprint.iacr.org/2020/341 is not required,
        # but we need some some squares and products determined by list J
        # In order to avoid costly inverse computations in fp, we are gonna work with projective coordinates

        # Ensuring the cardinality of each ser coincide with the expected one
        assert len(I) == sI
        assert len(J) == sJ
        assert len(K) == sK

        return None

    # Next function perform algorithm 2 of https://eprint.iacr.org/2020/341 with input \alpha = 1 and \alpha = -1, and
    # then it computes the isogenous Montgomery curve coefficient
    def xISOG(A, i):

        nonlocal J
        nonlocal sJ
        nonlocal ptree_hI
        nonlocal sI
        nonlocal K
        nonlocal sK
        nonlocal XZJ4

        AA = fp.fp_add(A[0], A[0])  # 2A' + 4C
        AA = fp.fp_sub(AA, A[1])  # 2A'
        AA = fp.fp_add(AA, AA)  # 4A'

        # Polynomial D_j of algorithm 2 from https://eprint.iacr.org/2020/341 is not required,
        # but we need some some squares and products determined by list J
        # In order to avoid costly inverse computations in fp, we are gonna work with projective coordinates

        SUB_SQUARED = [0 for j in range(0, sJ, 1)]  #
        ADD_SQUARED = [0 for j in range(0, sJ, 1)]  #

        # List XZJ4 is required for degree-l isogeny evaluations...
        XZJ4 = [0 for j in range(0, sJ, 1)]  # 2*Xj*Zj
        for j in range(0, sJ, 1):

            SUB_SQUARED[j] = fp.fp_sub(J[j][0], J[j][1])  # (Xj - Zj)
            SUB_SQUARED[j] = fp.fp_sqr(SUB_SQUARED[j])  # (Xj - Zj)^2

            ADD_SQUARED[j] = fp.fp_add(J[j][0], J[j][1])  # (Xj + Zj)
            ADD_SQUARED[j] = fp.fp_sqr(ADD_SQUARED[j])  # (Xj + Zj)^2

            XZJ4[j] = fp.fp_sub(SUB_SQUARED[j], ADD_SQUARED[j])  # -4*Xj*Zj

        # --------------------------------------------------------------------------------------------------
        #                   ~~~~~~~~
        #                    |    |
        # Computing E_J(W) = |    | [ F0(W, x([j]P)) * alpha^2 + F1(W, x([j]P)) * alpha + F2(W, x([j]P)) ]
        #                    j in J
        # In order to avoid costly inverse computations in fp, we are gonna work with projective coordinates
        # In particular, for a degree-l isogeny construction, we need alpha = 1 and alpha = -1

        # EJ_0 is the one determined by alpha = 1
        EJ_0 = [[0, 0, 0] for j in range(0, sJ, 1)]
        # EJ_1 is the one determined by alpha = -1
        EJ_1 = [[0, 0, 0] for j in range(0, sJ, 1)]

        for j in range(0, sJ, 1):

            # However, each SUB_SQUARED[j] and ADD_SQUARED[j] should be multiplied by C
            tadd = fp.fp_mul(ADD_SQUARED[j], A[1])
            tsub = fp.fp_mul(SUB_SQUARED[j], A[1])

            # We require the double of tadd and tsub
            tadd2 = fp.fp_add(tadd, tadd)
            tsub2 = fp.fp_add(tsub, tsub)

            t1 = fp.fp_mul(XZJ4[j], AA)  #       A *(-4*Xj*Zj)

            # Case alpha = 1
            linear = fp.fp_sub(
                t1, tadd2)  #       A *(-4*Xj*Zj)  - C * (2 * (Xj + Zj)^2)
            EJ_0[j] = [tsub, linear, tsub]

            # Case alpha = -1
            linear = fp.fp_sub(
                tsub2, t1)  #       C * (2 * (Xj - Zj)^2) - A *(-4*Xj*Zj)
            EJ_1[j] = [tadd, linear, tadd]

        # The faster way for multiplying is using a divide-and-conquer approach
        poly_EJ_0 = poly_mul.product_selfreciprocal_tree(
            EJ_0,
            sJ)['poly']  # product tree of EJ_0 (we only require the root)
        poly_EJ_1 = poly_mul.product_selfreciprocal_tree(
            EJ_1,
            sJ)['poly']  # product tree of EJ_1 (we only require the root)

        if not SCALED_REMAINDER_TREE:
            # Remainder tree computation
            remainders_EJ_0 = poly_redc.multieval_unscaled(
                poly_EJ_0, 2 * sJ + 1, ptree_hI, sI)
            remainders_EJ_1 = poly_redc.multieval_unscaled(
                poly_EJ_1, 2 * sJ + 1, ptree_hI, sI)

        else:
            # Approach using scaled remainder trees
            if ptree_hI != None:
                poly_EJ_0 = poly_redc.poly_redc(poly_EJ_0, 2 * sJ + 1,
                                                ptree_hI)
                fg_0 = poly_mul.poly_mul_middle(ptree_hI['scaled'], sI,
                                                poly_EJ_0[::-1], sI)
                remainders_EJ_0 = poly_redc.multieval_scaled(
                    fg_0[::-1], sI, [1] + [0] * (sI - 1), sI, ptree_hI, sI)

                poly_EJ_1 = poly_redc.poly_redc(poly_EJ_1, 2 * sJ + 1,
                                                ptree_hI)
                fg_1 = poly_mul.poly_mul_middle(ptree_hI['scaled'], sI,
                                                poly_EJ_1[::-1], sI)
                remainders_EJ_1 = poly_redc.multieval_scaled(
                    fg_1[::-1], sI, [1] + [0] * (sI - 1), sI, ptree_hI, sI)
            else:
                remainders_EJ_0 = []
                remainders_EJ_1 = []

        # Multipying all the remainders
        r0 = poly_mul.product(remainders_EJ_0, sI)
        r1 = poly_mul.product(remainders_EJ_1, sI)

        # ---------------------------------------------------------------------------------
        # Now, we proceed by computing the missing part which is determined by K
        # Notice, the denominators are the same and then they annulled between them
        # In other words, it is not required to compute the product of all Zk's with k In K

        # Case alpha = 1
        hK_0 = [[fp.fp_sub(K[k][1], K[k][0])] for k in range(0, sK, 1)]
        hK_0 = poly_mul.product(hK_0,
                                sK)  # product of (Zk - Xk) for each k in K
        # Case alpha = -1
        hK_1 = [[fp.fp_add(K[k][1], K[k][0])] for k in range(0, sK, 1)]
        hK_1 = poly_mul.product(hK_1,
                                sK)  # product of (Zk + Xk) for each k in K

        # --------------------------------------------------------------
        # Now, we have all the ingredients for computing the image curve
        A24m = fp.fp_sub(A[0], A[1])  # A' - 2C

        A24 = fp.fp_exp(A[0], global_L[i])  # (A' + 2C)^l
        A24m = fp.fp_exp(A24m, global_L[i])  # (A' - 2C)^l

        t24m = fp.fp_mul(
            hK_1, r1
        )  # output of algorithm 2 with alpha =-1 and without the demoninator
        t24m = fp.fp_sqr(t24m)  # raised at 2
        t24m = fp.fp_sqr(t24m)  # raised at 4
        t24m = fp.fp_sqr(t24m)  # raised at 8

        t24 = fp.fp_mul(
            hK_0, r0
        )  # output of algorithm 2 with alpha = 1 and without the demoninator
        t24 = fp.fp_sqr(t24)  # raised at 2
        t24 = fp.fp_sqr(t24)  # raised at 4
        t24 = fp.fp_sqr(t24)  # raised at 8

        A24 = fp.fp_mul(A24, t24m)
        A24m = fp.fp_mul(A24m, t24)

        # Now, we have d = (A24m / A24) where the image Montgomery cuve coefficient is
        #      B'   2*(1 + d)   2*(A24 + A24m)
        # B = ---- = --------- = --------------
        #      C      (1 - d)     (A24 - A24m)
        # However, we required B' + 2C = 4*A24 and 4C = 4 * (A24 - A24m)

        t24m = fp.fp_sub(A24, A24m)  #   (A24 - A24m)
        t24m = fp.fp_add(t24m, t24m)  # 2*(A24 - A24m)
        t24m = fp.fp_add(t24m, t24m)  # 4*(A24 - A24m)

        t24 = fp.fp_add(A24, A24)  # 2 * A24
        t24 = fp.fp_add(t24, t24)  # 4 * A24

        # return [t24, t24m], ptree_hI, XZJ4
        return [t24, t24m]

    def xEVAL(P, A):

        AA = fp.fp_add(A[0], A[0])  # 2A' + 4C
        AA = fp.fp_sub(AA, A[1])  # 2A'
        AA = fp.fp_add(AA, AA)  # 4A'

        # --------------------------------------------------------------------------------------------------
        #                   ~~~~~~~~
        #                    |    |
        # Computing E_J(W) = |    | [ F0(W, x([j]P)) * alpha^2 + F1(W, x([j]P)) * alpha + F2(W, x([j]P)) ]
        #                    j in J
        # In order to avoid costly inverse computations in fp, we are gonna work with projective coordinates
        # In particular, for a degree-l isogeny construction, we need alpha = 1 and alpha = -1

        # EJ_0 is the one determined by alpha = x
        EJ_0 = [[0, 0, 0] for j in range(0, sJ, 1)]
        # Notice, the corresponding EJ_1 that is determined by alpha = 1/x can be computed by using EJ_0

        XZ_add = fp.fp_add(P[0], P[1])  # X + Z
        XZ_sub = fp.fp_sub(P[0], P[1])  # X - Z

        AXZ2 = fp.fp_mul(P[0], P[1])  # X * Z
        t1 = fp.fp_sqr(P[0])  # X^2
        t2 = fp.fp_sqr(P[1])  # Z^2

        CX2Z2 = fp.fp_add(t1, t2)  #      X^2 + Z^2
        CX2Z2 = fp.fp_mul(CX2Z2, A[1])  # C * (X^2 + Z^2)

        AXZ2 = fp.fp_add(AXZ2, AXZ2)  #       2 * (X * Z)
        CXZ2 = fp.fp_mul(AXZ2, A[1])  # C  * [2 * (X * Z)]
        AXZ2 = fp.fp_mul(AXZ2, AA)  # A' * [2 * (X * Z)]

        for j in range(0, sJ, 1):

            XZj_add = fp.fp_add(J[j][0], J[j][1])  # Xj + Zj
            XZj_sub = fp.fp_sub(J[j][0], J[j][1])  # Xj - Zj

            t1 = fp.fp_mul(XZ_sub, XZj_add)  # (X - Z) * (Xj + Zj)
            t2 = fp.fp_mul(XZ_add, XZj_sub)  # (X + Z) * (Xj - Zj)

            # Computing the quadratic coefficient
            quadratic = fp.fp_sub(t1, t2)  #   2 * [(X*Zj) - (Z*Xj)]
            quadratic = fp.fp_sqr(quadratic)  # ( 2 * [(X*Zj) - (Z*Xj)] )^2
            quadratic = fp.fp_mul(A[1],
                                  quadratic)  # C * ( 2 * [(X*Zj) - (Z*Xj)] )^2

            # Computing the constant coefficient
            constant = fp.fp_add(t1, t2)  #   2 * [(X*Xj) - (Z*Zj)]
            constant = fp.fp_sqr(constant)  # ( 2 * [(X*Xj) - (Z*Zj)] )^2
            constant = fp.fp_mul(A[1],
                                 constant)  # C * ( 2 * [(X*Xj) - (Z*Zj)] )^2

            # Computing the linear coefficient
            # ----------------------------------------------------------------------------------------------------------
            # C * [ (-2*Xj*Zj)*(alpha^2 + 1) + (-2*alpha)*(Xj^2 + Zj^2)] + [A' * (-2*Xj*Zj) * (2*X*Z)] where alpha = X/Z
            t1 = fp.fp_add(J[j][0], J[j][1])  #     (Xj + Zj)
            t1 = fp.fp_sqr(t1)  #     (Xj + Zj)^2
            t1 = fp.fp_add(t1, t1)  # 2 * (Xj + Zj)^2
            t1 = fp.fp_add(
                t1,
                XZJ4[j])  # 2 * (Xj + Zj)^2 - (4*Xj*Zj) := 2 * (Xj^2 + Zj^2)
            t1 = fp.fp_mul(t1,
                           CXZ2)  # [2 * (Xj^2 + Zj^2)] * (2 * [ C * (X * Z)])

            t2 = fp.fp_mul(CX2Z2,
                           XZJ4[j])  # [C * (X^2 + Z^2)] * (-4 * Xj * Zj)
            t1 = fp.fp_sub(
                t2, t1
            )  # [C * (X^2 + Z^2)] * (-4 * Xj * Zj) - [2 * (Xj^2 + Zj^2)] * (2 * [ C * (X * Z)])

            t2 = fp.fp_mul(AXZ2,
                           XZJ4[j])  # (2 * [A' * (X * Z)]) * (-4 * Xj * Zj)
            linear = fp.fp_add(
                t1, t2)  # This is our desired equation but multiplied by 2
            linear = fp.fp_add(
                linear,
                linear)  # This is our desired equation but multiplied by 4
            # ----------------------------------------------------------------------------------------------------------

            # Case alpha = X / Z
            EJ_0[j] = [constant, linear, quadratic]

        # The faster way for multiplying is using a divide-and-conquer approach
        poly_EJ_0 = poly_mul.product_tree(
            EJ_0,
            sJ)['poly']  # product tree of EJ_0 (we only require the root)
        poly_EJ_1 = list(
            poly_EJ_0[::-1])  # product tree of EJ_1(x) = x^{2b + 1} EJ_0(1/X)

        if not SCALED_REMAINDER_TREE:
            # Remainder tree computation
            remainders_EJ_0 = poly_redc.multieval_unscaled(
                poly_EJ_0, 2 * sJ + 1, ptree_hI, sI)
            remainders_EJ_1 = poly_redc.multieval_unscaled(
                poly_EJ_1, 2 * sJ + 1, ptree_hI, sI)

        else:
            # Approach using scaled remainder trees
            if ptree_hI != None:
                poly_EJ_0 = poly_redc.poly_redc(poly_EJ_0, 2 * sJ + 1,
                                                ptree_hI)
                fg_0 = poly_mul.poly_mul_middle(ptree_hI['scaled'], sI,
                                                poly_EJ_0[::-1], sI)
                remainders_EJ_0 = poly_redc.multieval_scaled(
                    fg_0[::-1], sI, [1] + [0] * (sI - 1), sI, ptree_hI, sI)

                poly_EJ_1 = poly_redc.poly_redc(poly_EJ_1, 2 * sJ + 1,
                                                ptree_hI)
                fg_1 = poly_mul.poly_mul_middle(ptree_hI['scaled'], sI,
                                                poly_EJ_1[::-1], sI)
                remainders_EJ_1 = poly_redc.multieval_scaled(
                    fg_1[::-1], sI, [1] + [0] * (sI - 1), sI, ptree_hI, sI)
            else:
                remainders_EJ_0 = []
                remainders_EJ_1 = []

        # Multipying all the remainders
        r0 = poly_mul.product(remainders_EJ_0, sI)
        r1 = poly_mul.product(remainders_EJ_1, sI)

        # ---------------------------------------------------------------------------------
        # Now, we proceed by computing the missing part which is determined by K
        # Notice, the denominators are the same and then they annulled between them
        # In other words, it is not required to compute the product of all Zk's with k In K

        hK_0 = [[0]] * sK
        hK_1 = [[0]] * sK
        for k in range(0, sK, 1):

            XZk_add = fp.fp_add(K[k][0], K[k][1])  # Xk + Zk
            XZk_sub = fp.fp_sub(K[k][0], K[k][1])  # Xk - Zk
            t1 = fp.fp_mul(XZ_sub, XZk_add)  # (X - Z) * (Xk + Zk)
            t2 = fp.fp_mul(XZ_add, XZk_sub)  # (X + Z) * (Xk - Zk)

            # Case alpha = X/Z
            hK_0[k] = [fp.fp_sub(t1, t2)]  # 2 * [(X*Zk) - (Z*Xk)]

            # Case 1/alpha = Z/X
            hK_1[k] = [fp.fp_add(t1, t2)]  # 2 * [(X*Xk) - (Z*Zk)]

        hK_0 = poly_mul.product(hK_0,
                                sK)  # product of (XZk - ZXk) for each k in K
        hK_1 = poly_mul.product(hK_1,
                                sK)  # product of (XXk - ZZk) for each k in K

        # ---------------------------------------------------------------------------------
        # Now, unifying all the computations
        XX = fp.fp_mul(
            hK_1, r1
        )  # output of algorithm 2 with 1/alpha = Z/X and without the demoninator
        XX = fp.fp_sqr(XX)
        XX = fp.fp_mul(XX, P[0])

        ZZ = fp.fp_mul(
            hK_0, r0
        )  # output of algorithm 2 with alpha = X/Z and without the demoninator
        ZZ = fp.fp_sqr(ZZ)
        ZZ = fp.fp_mul(ZZ, P[1])

        return [XX, ZZ]

    # Get cost of the isogeny constructions and evaluations
    sI_list = None
    sJ_list = None

    def cISOG_and_cEVAL():

        nonlocal C_xISOG
        nonlocal C_xEVAL

        nonlocal sI_list
        nonlocal sJ_list

        if tuned:

            sI_list = []
            sJ_list = []
            try:
                f = open(resource_filename('sidh', 'data/ijk/' + prime))
            except Exception as ex:
                raise Exception(
                    "ijk data required for tuned mode not found: %r", ex)

            for i in range(0, n, 1):

                bc = f.readline()
                bc = [int(bci) for bci in bc.split()]
                sJ_list.append(bc[0])
                sI_list.append(bc[1])

            f.close()
        # First, we look for a full torsion point
        A = [2, 4]
        T_p, T_m = curve.full_torsion_points(A)

        for i in range(0, n, 1):

            if tuned:
                set_parameters_velu(sJ_list[i], sI_list[i], i)
            else:
                # Parameters sJ and sI correspond with the parameters b and b' from example 4.12 of https://eprint.iacr.org/2020/341
                # These paramters are required in KPs, xISOG, and xEVAL
                if global_L[i] == 3:
                    b = 0
                    c = 0
                else:
                    b = int(floor(sqrt(global_L[i] - 1) / 2.0))
                    c = int(floor((global_L[i] - 1.0) / (4.0 * b)))

                set_parameters_velu(b, c, i)

            # Getting an orderl-l point
            Tp = list(T_p)
            for j in range(i + 1, n, 1):
                Tp = curve.xMUL(Tp, A, j)

            # Cost of xISOG() and KPs()
            fp.set_zero_ops()
            KPs(Tp, A, i)
            t = fp.get_ops()
            C_xISOG[i] = numpy.array([t[0] * 1.0, t[1] * 1.0, t[2] * 1.0])

            fp.set_zero_ops()
            B = xISOG(A, i)
            t = fp.get_ops()
            C_xISOG[i] += numpy.array([t[0] * 1.0, t[1] * 1.0, t[2] * 1.0])

            # xEVAL: kernel point determined by the next isogeny evaluation
            fp.set_zero_ops()
            T_p = xEVAL(T_p, A)

            # Cost of xEVAL
            fp.set_zero_ops()
            T_m = xEVAL(T_m, A)
            t = fp.get_ops()
            C_xEVAL[i] = numpy.array([t[0] * 1.0, t[1] * 1.0, t[2] * 1.0])

            # Updating the new next curve
            A = list(B)

        fp.set_zero_ops()
        assert curve.coeff(A) == 0x6
        return None

    # Now, we proceed to store all the correct costs
    cISOG_and_cEVAL()

    return attrdict(name='svelu', **locals())
Example #4
0
def Tvelu(curve):

    global_L = curve.global_L
    prime = curve.prime
    fp = curve.fp
    p = curve.p
    np = curve.np
    nm = curve.nm
    Em = curve.Em
    Ep = curve.Ep
    random = SystemRandom()
    poly_mul = Poly_mul(curve)
    poly_redc = Poly_redc(poly_mul)

    cEVAL = lambda l: numpy.array([2.0 * (l - 1.0), 2.0, (l + 1.0)])
    cISOG = lambda l: numpy.array([
        (3.0 * l + 2.0 * hamming_weight(l) - 9.0 + isequal[l == 3] * 4.0),
        (l + 2.0 * bitlength(l) + 1.0 + isequal[l == 3] * 2.0),
        (3.0 * l - 7.0 + isequal[l == 3] * 6.0),
    ])

    C_xEVAL = list(
        map(cEVAL,
            global_L))  # list of the costs of each degree-l isogeny evaluation
    C_xISOG = list(map(
        cISOG,
        global_L))  # list of the costs of each degree-l isogeny construction

    K = None

    def yDBL(P, A):
        '''
        -------------------------------------------------------------------------
        yDBL()
        input : a projective Twisted Edwards y-coordinate point y(P) := YP/WP,
                and the projective Montgomery constants A24:= A + 2C and C24:=4C
                where E : y^2 = x^3 + (A/C)*x^2 + x
        output: the projective Twisted Edwards y-coordinate point y([2]P)
        -------------------------------------------------------------------------
        '''
        t_0 = fp.fp2_sqr(P[0])
        t_1 = fp.fp2_sqr(P[1])
        Z = fp.fp2_mul(A[1], t_0)
        X = fp.fp2_mul(Z, t_1)
        t_1 = fp.fp2_sub(t_1, t_0)
        t_0 = fp.fp2_mul(A[0], t_1)
        Z = fp.fp2_add(Z, t_0)
        Z = fp.fp2_mul(Z, t_1)
        return [fp.fp2_sub(X, Z), fp.fp2_add(X, Z)]

    def yADD(P, Q, PQ):
        '''
        -------------------------------------------------------------------------
        yADD()
        input : the projective Twisted Edwards y-coordinate points y(P) := YP/WP,
                y(Q) := YQ/WQ, and y(P-Q) := YPQ/QPQ
        output: the projective Twisted Edwards y-coordinate point y(P+Q)
        -------------------------------------------------------------------------
        '''
        a = fp.fp2_mul(P[1], Q[0])
        b = fp.fp2_mul(P[0], Q[1])
        c = fp.fp2_add(a, b)
        d = fp.fp2_sub(a, b)
        c = fp.fp2_sqr(c)
        d = fp.fp2_sqr(d)

        xD = fp.fp2_add(PQ[1], PQ[0])
        zD = fp.fp2_sub(PQ[1], PQ[0])
        X = fp.fp2_mul(zD, c)
        Z = fp.fp2_mul(xD, d)
        return [fp.fp2_sub(X, Z), fp.fp2_add(X, Z)]

    def KPs_t(P, A, i):
        '''
        -------------------------------------------------------------------------
        KPs()
        input : the projective Twisted Edwards y-coordinate points y(P) := YP/WP,
                the projective Montgomery constants A24:= A + 2C and C24:=4C where
                E : y^2 = x^3 + (A/C)*x^2 + x, and a positive integer 0 <= i < n
        output: the list of projective Twisted Edwards y-coordinate points y(P),
                y([2]P), y([3]P), ..., and y([d_i]P) where l_i = 2 * d_i + 1
        -------------------------------------------------------------------------
        '''
        assert curve.isinfinity(P) == False
        nonlocal K
        d = (global_L[i] - 1) // 2

        K = [[[0, 0], [0, 0]] for j in range(d + 1)]
        K[0] = list([fp.fp2_sub(P[0], P[1]), fp.fp2_add(P[0], P[1])])  # 2a
        if global_L[i] == 3:
            K[1] = list([list(K[0]), list(K[1])])
        else:
            K[1] = yDBL(K[0], A)  # 4M + 2S + 4a

        for j in range(2, d, 1):
            K[j] = yADD(K[j - 1], K[0], K[j - 2])  # 4M + 2S + 6a

        return K  # 2(l - 3)M + (l - 3)S + 3(l - 3)a

    def xISOG_t(A, i):
        '''
        ------------------------------------------------------------------------------
        xISOG()
        input : the projective Montgomery constants A24:= A + 2C and C24:=4C where
                E : y^2 = x^3 + (A/C)*x^2 + x, the list of projective Twisted
                Edwards y-coordinate points y(P), y([2]P), y([3]P), ..., and y([d_i]P)
                where l_i = 2 * d_i + 1, and a positive integer 0 <= i < n
        output: the projective Montgomery constants a24:= a + 2c and c24:=4c where
                E': y^2 = x^3 + (a/c)*x^2 + x is a degree-(l_i) isogenous curve to E
        ------------------------------------------------------------------------------
        '''
        nonlocal K
        l = global_L[i]  # l
        bits_of_l = bitlength(l)  # Number of bits of L[i]
        d = (l - 1) // 2  # Here, l = 2d + 1

        By = K[0][0]
        Bz = K[0][1]
        for j in range(1, d, 1):

            By = fp.fp2_mul(By, K[j][0])
            Bz = fp.fp2_mul(Bz, K[j][1])

        bits_of_l -= 1
        constant_d_edwards = fp.fp2_sub(A[0], A[1])  # 1a

        tmp_a = A[0]
        tmp_d = constant_d_edwards
        # left-to-right method for computing a^l and d^l
        for j in range(1, bits_of_l + 1):

            tmp_a = fp.fp2_sqr(tmp_a)
            tmp_d = fp.fp2_sqr(tmp_d)
            if ((l >> (bits_of_l - j)) & 1) != 0:

                tmp_a = fp.fp2_mul(tmp_a, A[0])
                tmp_d = fp.fp2_mul(tmp_d, constant_d_edwards)

        for j in range(3):

            By = fp.fp2_sqr(By)
            Bz = fp.fp2_sqr(Bz)

        C0 = fp.fp2_mul(tmp_a, Bz)
        C1 = fp.fp2_mul(tmp_d, By)
        C1 = fp.fp2_sub(C0, C1)

        return [C0, C1]  # (l - 1 + 2*HW(l) - 2)M + 2(|l|_2 + 1)S + 2a

    def xEVAL_t(P, i):
        '''
        ------------------------------------------------------------------------------
        xEVAL()
        input : the projective Montgomery constants A24:= A + 2C and C24:=4C where
                E : y^2 = x^3 + (A/C)*x^2 + x, the list of projective Twisted
                Edwards y-coordinate points y(P), y([2]P), y([3]P), ..., and y([d_i]P)
                where l_i = 2 * d_i + 1, and a positive integer 0 <= i < n
        output: the projective Montgomery constants a24:= a + 2c and c24:=4c where
                E': y^2 = x^3 + (a/c)*x^2 + x is a degree-(l_i) isogenous curve to E
        ------------------------------------------------------------------------------
        '''
        nonlocal K
        d = (global_L[i] - 1) // 2  # Here, l = 2d + 1

        Q0 = fp.fp2_add(P[0], P[1])
        Q1 = fp.fp2_sub(P[0], P[1])
        R0, R1 = curve.CrissCross(K[0][1], K[0][0], Q0, Q1)
        for j in range(1, d, 1):

            T0, T1 = curve.CrissCross(K[j][1], K[j][0], Q0, Q1)
            R0 = fp.fp2_mul(T0, R0)
            R1 = fp.fp2_mul(T1, R1)

        R0 = fp.fp2_sqr(R0)
        R1 = fp.fp2_sqr(R1)
        X = fp.fp2_mul(P[0], R0)
        Z = fp.fp2_mul(P[1], R1)

        return [X, Z]  # 2(l - 1)M + 2S + (l + 1)a

    def xISOG_4(P):
        """
        Degree-4 isogeny construction
        """
        nonlocal K
        K = [[0, 0], [0, 0], [0, 0]]

        K[1] = fp.fp2_sub(P[0], P[1])
        K[2] = fp.fp2_add(P[0], P[1])
        K[0] = fp.fp2_sqr(P[1])
        K[0] = fp.fp2_add(K[0], K[0])

        C24 = fp.fp2_sqr(K[0])
        K[0] = fp.fp2_add(K[0], K[0])
        A24 = fp.fp2_sqr(P[0])
        A24 = fp.fp2_add(A24, A24)
        A24 = fp.fp2_sqr(A24)
        return [A24, C24]

    def xEVAL_4(Q):
        """
        Degree-4 isogeny evaluation
        """
        t0 = fp.fp2_add(Q[0], Q[1])
        t1 = fp.fp2_sub(Q[0], Q[1])
        XQ = fp.fp2_mul(t0, K[1])
        ZQ = fp.fp2_mul(t1, K[2])
        t0 = fp.fp2_mul(t0, t1)
        t0 = fp.fp2_mul(t0, K[0])
        t1 = fp.fp2_add(XQ, ZQ)
        ZQ = fp.fp2_sub(XQ, ZQ)
        t1 = fp.fp2_sqr(t1)
        ZQ = fp.fp2_sqr(ZQ)
        XQ = fp.fp2_add(t0, t1)
        t0 = fp.fp2_sub(ZQ, t0)
        XQ = fp.fp2_mul(XQ, t1)
        ZQ = fp.fp2_mul(ZQ, t0)

        return [XQ, ZQ]

    def KPs(P, A, i):
        if global_L[i] != 4:
            return KPs_t(P, A, i)

    def xISOG(A, i):
        if global_L[i] != 4:
            return xISOG_t(A, i)
        else:
            # A should corresponds with an order-4 point
            return xISOG_4(A)

    def xEVAL(P, i):
        if global_L[i] != 4:
            return xEVAL_t(P, i)
        else:
            return xEVAL_4(P)

    def cISOG_and_cEVAL():
        """
        Get cost of the isogeny constructions and evaluations
        """
        nonlocal C_xISOG
        nonlocal C_xEVAL

        # E[p + 1]
        # First, we look for a full torsion point
        A = [[0x8, 0x0], [0x4, 0x0]]

        # Reading public generators points
        f = open(resource_filename('sidh', 'data/gen/' + prime))
        # x(PA), x(QA) and x(PA - QA)
        PQA = f.readline()
        PQA = [int(x, 16) for x in PQA.split()]
        PA = [list(PQA[0:2]), [0x1, 0x0]]
        QA = [list(PQA[2:4]), [0x1, 0x0]]
        PQA = [list(PQA[4:6]), [0x1, 0x0]]

        # x(PB), x(QB) and x(PB - QB)
        PQB = f.readline()
        PQB = [int(x, 16) for x in PQB.split()]
        PB = [list(PQB[0:2]), [0x1, 0x0]]
        QB = [list(PQB[2:4]), [0x1, 0x0]]
        PQB = [list(PQB[4:6]), [0x1, 0x0]]

        f.close()

        for i in range(0, Ep[0] - 1, 1):

            PA = curve.xMUL(PA, A, 0)
            QA = curve.xMUL(QA, A, 0)
            PQA = curve.xMUL(PQA, A, 0)

        # Random kernels for counting the
        T_p = curve.Ladder3pt(random.randint(0, p - 1), PA, QA, PQA, A)
        T_m = curve.Ladder3pt(random.randint(0, p - 1), PB, QB, PQB, A)

        for i in range(0, np, 1):
            for j in range(0, Ep[i] - 1, 1):
                T_p = curve.xMUL(T_p, A, i)

        for i in range(np, np + nm, 1):
            for j in range(0, Em[i - np] - 1, 1):
                T_m = curve.xMUL(T_m, A, i)

        for i in range(0, np, 1):

            # Getting an orderl-l point
            Tp = list(T_p)
            for j in range(i + 1, np, 1):
                Tp = curve.xMUL(Tp, A, j)

            # Cost of xISOG() and KPs()
            fp.fp.set_zero_ops()
            KPs(Tp, A, i)
            t = fp.fp.get_ops()
            C_xISOG[i] = numpy.array([t[0] * 1.0, t[1] * 1.0, t[2] * 1.0])

            fp.fp.set_zero_ops()
            Tp[0], A[0] = fp.fp2_cswap(Tp[0], A[0], global_L[i] == 4)
            Tp[1], A[1] = fp.fp2_cswap(Tp[1], A[1], global_L[i] == 4)
            B = xISOG(A, i)
            Tp[0], A[0] = fp.fp2_cswap(Tp[0], A[0], global_L[i] == 4)
            Tp[1], A[1] = fp.fp2_cswap(Tp[1], A[1], global_L[i] == 4)
            t = fp.fp.get_ops()
            C_xISOG[i] += numpy.array([t[0] * 1.0, t[1] * 1.0, t[2] * 1.0])

            # xEVAL: kernel point determined by the next isogeny evaluation
            fp.fp.set_zero_ops()
            T_p = xEVAL(T_p, i)

            # Cost of xEVAL
            fp.fp.set_zero_ops()
            T_m = xEVAL(T_m, i)
            t = fp.fp.get_ops()
            C_xEVAL[i] = numpy.array([t[0] * 1.0, t[1] * 1.0, t[2] * 1.0])

            # Updating the new next curve
            A = list(B)

        # E[p - 1]
        # First, we look for a full torsion point
        A = [[0x8, 0x0], [0x4, 0x0]]
        T_m = curve.Ladder3pt(random.randint(0, p - 1), PA, QA, PQA, A)
        T_p = curve.Ladder3pt(random.randint(0, p - 1), PB, QB, PQB, A)

        for i in range(np, np + nm, 1):

            # Getting an orderl-l point
            Tp = list(T_p)
            for j in range(i + 1, np + nm, 1):
                Tp = curve.xMUL(Tp, A, j)

            # Cost of xISOG() and KPs()
            fp.fp.set_zero_ops()
            KPs(Tp, A, i)
            t = fp.fp.get_ops()
            C_xISOG[i] = numpy.array([t[0] * 1.0, t[1] * 1.0, t[2] * 1.0])

            fp.fp.set_zero_ops()
            B = xISOG(A, i)
            t = fp.fp.get_ops()
            C_xISOG[i] += numpy.array([t[0] * 1.0, t[1] * 1.0, t[2] * 1.0])

            # xEVAL: kernel point determined by the next isogeny evaluation
            fp.fp.set_zero_ops()
            T_p = xEVAL(T_p, i)

            # Cost of xEVAL
            fp.fp.set_zero_ops()
            T_m = xEVAL(T_m, i)
            t = fp.fp.get_ops()
            C_xEVAL[i] = numpy.array([t[0] * 1.0, t[1] * 1.0, t[2] * 1.0])

            # Updating the new next curve
            A = list(B)
        fp.fp.set_zero_ops()
        return None

    # Now, we proceed to store all the correct costs
    cISOG_and_cEVAL()
    return attrdict(name='tvelu', **locals())
Example #5
0
def Tvelu(curve):

    fp = curve.fp
    global_L = curve.L

    cEVAL = lambda l: numpy.array([2.0 * (l - 1.0), 2.0, (l + 1.0)])
    cISOG = lambda l: numpy.array([
        (3.0 * l + 2.0 * hamming_weight(l) - 9.0 + isequal[l == 3] * 4.0),
        (l + 2.0 * bitlength(l) + 1.0 + isequal[l == 3] * 2.0),
        (3.0 * l - 7.0 + isequal[l == 3] * 6.0),
    ])

    C_xEVAL = list(
        map(cEVAL,
            global_L))  # list of the costs of each degree-l isogeny evaluation
    C_xISOG = list(map(
        cISOG,
        global_L))  # list of the costs of each degree-l isogeny construction

    K = None
    ''' -------------------------------------------------------------------------
        yDBL()
        input : a projective Twisted Edwards y-coordinate point y(P) := YP/WP,
                and the projective Montgomery constants A24:= A + 2C and C24:=4C
                where E : y^2 = x^3 + (A/C)*x^2 + x
        output: the projective Twisted Edwards y-coordinate point y([2]P)
        ------------------------------------------------------------------------- '''
    def yDBL(P, A):

        t_0 = fp.fp_sqr(P[0])
        t_1 = fp.fp_sqr(P[1])
        Z = fp.fp_mul(A[1], t_0)
        X = fp.fp_mul(Z, t_1)
        t_1 = fp.fp_sub(t_1, t_0)
        t_0 = fp.fp_mul(A[0], t_1)
        Z = fp.fp_add(Z, t_0)
        Z = fp.fp_mul(Z, t_1)
        return [fp.fp_sub(X, Z), fp.fp_add(X, Z)]

    ''' -------------------------------------------------------------------------
        yADD()
        input : the projective Twisted Edwards y-coordinate points y(P) := YP/WP,
                y(Q) := YQ/WQ, and y(P-Q) := YPQ/QPQ
        output: the projective Twisted Edwards y-coordinate point y(P+Q)
        ------------------------------------------------------------------------- '''

    def yADD(P, Q, PQ):

        a = fp.fp_mul(P[1], Q[0])
        b = fp.fp_mul(P[0], Q[1])
        c = fp.fp_add(a, b)
        d = fp.fp_sub(a, b)
        c = fp.fp_sqr(c)
        d = fp.fp_sqr(d)

        xD = fp.fp_add(PQ[1], PQ[0])
        zD = fp.fp_sub(PQ[1], PQ[0])
        X = fp.fp_mul(zD, c)
        Z = fp.fp_mul(xD, d)
        return [fp.fp_sub(X, Z), fp.fp_add(X, Z)]

    ''' -------------------------------------------------------------------------
        KPs()
        input : the projective Twisted Edwards y-coordinate points y(P) := YP/WP,
                the projective Montgomery constants A24:= A + 2C and C24:=4C where 
                E : y^2 = x^3 + (A/C)*x^2 + x, and a positive integer 0 <= i < n
        output: the list of projective Twisted Edwards y-coordinate points y(P),
                y([2]P), y([3]P), ..., and y([d_i]P) where l_i = 2 * d_i + 1
        ------------------------------------------------------------------------- '''

    def KPs(P, A, i):

        nonlocal K
        d = (global_L[i] - 1) // 2

        K = [[0, 0] for j in range(d + 1)]
        K[0] = list([fp.fp_sub(P[0], P[1]), fp.fp_add(P[0], P[1])])  # 2a
        K[1] = yDBL(K[0], A)  # 4M + 2S + 4a

        for j in range(2, d, 1):
            K[j] = yADD(K[j - 1], K[0], K[j - 2])  # 4M + 2S + 6a

        return K  # 2(l - 3)M + (l - 3)S + 3(l - 3)a

    ''' ------------------------------------------------------------------------------
        xISOG()
        input : the projective Montgomery constants A24:= A + 2C and C24:=4C where
                E : y^2 = x^3 + (A/C)*x^2 + x, the list of projective Twisted 
                Edwards y-coordinate points y(P), y([2]P), y([3]P), ..., and y([d_i]P)
                where l_i = 2 * d_i + 1, and a positive integer 0 <= i < n
        output: the projective Montgomery constants a24:= a + 2c and c24:=4c where
                E': y^2 = x^3 + (a/c)*x^2 + x is a degree-(l_i) isogenous curve to E
        ------------------------------------------------------------------------------ '''

    def xISOG(A, i):

        nonlocal K
        l = global_L[i]  # l
        bits_of_l = bitlength(l)  # Number of bits of L[i]
        d = (l - 1) // 2  # Here, l = 2d + 1

        By = K[0][0]
        Bz = K[0][1]
        for j in range(1, d, 1):

            By = fp.fp_mul(By, K[j][0])
            Bz = fp.fp_mul(Bz, K[j][1])

        bits_of_l -= 1
        constant_d_edwards = fp.fp_sub(A[0], A[1])  # 1a

        tmp_a = A[0]
        tmp_d = constant_d_edwards
        # left-to-right method for computing a^l and d^l
        for j in range(1, bits_of_l + 1):

            tmp_a = fp.fp_sqr(tmp_a)
            tmp_d = fp.fp_sqr(tmp_d)
            if ((l >> (bits_of_l - j)) & 1) != 0:

                tmp_a = fp.fp_mul(tmp_a, A[0])
                tmp_d = fp.fp_mul(tmp_d, constant_d_edwards)

        for j in range(3):

            By = fp.fp_sqr(By)
            Bz = fp.fp_sqr(Bz)

        C0 = fp.fp_mul(tmp_a, Bz)
        C1 = fp.fp_mul(tmp_d, By)
        C1 = fp.fp_sub(C0, C1)

        return [C0, C1]  # (l - 1 + 2*HW(l) - 2)M + 2(|l|_2 + 1)S + 2a

    ''' ------------------------------------------------------------------------------
        xEVAL()
        input : the projective Montgomery constants A24:= A + 2C and C24:=4C where
                E : y^2 = x^3 + (A/C)*x^2 + x, the list of projective Twisted 
                Edwards y-coordinate points y(P), y([2]P), y([3]P), ..., and y([d_i]P)
                where l_i = 2 * d_i + 1, and a positive integer 0 <= i < n
        output: the projective Montgomery constants a24:= a + 2c and c24:=4c where
                E': y^2 = x^3 + (a/c)*x^2 + x is a degree-(l_i) isogenous curve to E
        ------------------------------------------------------------------------------ '''

    def xEVAL(P, i):

        nonlocal K
        d = (global_L[i] - 1) // 2  # Here, l = 2d + 1

        Q0 = fp.fp_add(P[0], P[1])
        Q1 = fp.fp_sub(P[0], P[1])
        R0, R1 = curve.CrissCross(K[0][1], K[0][0], Q0, Q1)
        for j in range(1, d, 1):

            T0, T1 = curve.CrissCross(K[j][1], K[j][0], Q0, Q1)
            R0 = fp.fp_mul(T0, R0)
            R1 = fp.fp_mul(T1, R1)

        R0 = fp.fp_sqr(R0)
        R1 = fp.fp_sqr(R1)
        X = fp.fp_mul(P[0], R0)
        Z = fp.fp_mul(P[1], R1)

        return [X, Z]  # 2(l - 1)M + 2S + (l + 1)a

    return attrdict(name='tvelu', **locals())