#!/usr/bin/sage # vim: syntax=python # vim: set ts=2 sw=2 et: # Constantine # Copyright (c) 2018-2019 Status Research & Development GmbH # Copyright (c) 2020-Present Mamy André-Ratsimbazafy # Licensed and distributed under either of # * MIT license (license terms in the root directory or at http://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at http://www.apache.org/licenses/LICENSE-2.0). # at your option. This file may not be copied, modified, or distributed except according to those terms. # ############################################################ # # Frobenius constants # # ############################################################ # Imports # --------------------------------------------------------- import os import inspect, textwrap # Working directory # --------------------------------------------------------- os.chdir(os.path.dirname(__file__)) # Sage imports # --------------------------------------------------------- # Accelerate arithmetic by accepting probabilistic proofs from sage.structure.proof.all import arithmetic arithmetic(False) load('curves.sage') # Utilities # --------------------------------------------------------- def fp2_to_hex(a): v = vector(a) return '0x' + Integer(v[0]).hex() + ' + β * ' + '0x' + Integer(v[1]).hex() def field_to_nim(value, field, curve, prefix = "", comment_above = "", comment_right = ""): result = '# ' + comment_above + '\n' if comment_above else '' comment_right = ' # ' + comment_right if comment_right else '' if field == 'Fp2': v = vector(value) result += inspect.cleandoc(f""" {prefix}Fp2[{curve}].fromHex( {comment_right} "0x{Integer(v[0]).hex()}", "0x{Integer(v[1]).hex()}" )""") elif field == 'Fp': result += inspect.cleandoc(f""" {prefix}Fp[{curve}].fromHex( {comment_right} "0x{Integer(value).hex()}") """) else: raise NotImplementedError() return result # Code generators # --------------------------------------------------------- def genFrobeniusMapConstants(curve_name, curve_config): embdeg = curve_config[curve_name]['tower']['embedding_degree'] twdeg = curve_config[curve_name]['tower']['twist_degree'] g2field = f'Fp{embdeg//twdeg}' if (embdeg//twdeg) > 1 else 'Fp' p = curve_config[curve_name]['field']['modulus'] Fp = GF(p) K. = PolynomialRing(Fp) if g2field == 'Fp2': QNR_Fp = curve_config[curve_name]['tower']['QNR_Fp'] Fp2. = Fp.extension(u^2 - QNR_Fp) else: SNR_Fp = curve_config[curve_name]['tower']['SNR_Fp'] Fp2. = Fp.extension(u^2 - SNR_Fp) if g2field == 'Fp2': SNR = curve_config[curve_name]['tower']['SNR_Fp2'] SNR = Fp2(SNR) else: # To build the Fp6 extension, since we use a SexticNonResidue # to build Fp2, we can reuse it as a cubic non-residue # It always has [0, 1] coordinates in Fp2 SNR = Fp2([0, 1]) halfK = embdeg//2 print('\n----> Frobenius extension field constants <----\n') buf = inspect.cleandoc(f""" # Frobenius map - on extension fields # ----------------------------------------------------------------- # We start from base frobenius constant for a {embdeg} embedding degree. # with # - a sextic twist, SNR being the Sextic Non-Residue. # - coef being the Frobenius coefficient "ID" # c = SNR^((p-1)/{halfK})^coef # # On Fp2 frobenius(c) = conj(c) so we have # For n=2, with n the number of Frobenius applications # c2 = c * (c^p) = c * frobenius(c) = c * conj(c) # c2 = (SNR * conj(SNR))^((p-1)/{halfK})^coef) # c2 = (norm(SNR))^((p-1)/{halfK})^coef) # For k=3 # c3 = c * c2^p = c * frobenius(c2) = c * conj(c2) # with conj(norm(SNR)) = norm(SNR) as a norm is strictly on the base field. # c3 = (SNR * norm(SNR))^((p-1)/{halfK})^coef) # # A more generic formula can be derived by observing that # c3 = c * c2^p = c * (c * c^p)^p # c3 = c * c^p * c^p² # with 4, we have # c4 = c * c3^p = c * (c * c^p * c^p²)^p # c4 = c * c^p * c^p² * c^p³ # with n we have # cn = c * c^p * c^p² ... * c^p^(n-1) # cn = c^(1+p+p² + ... + p^(n-1)) # This is the sum of first n terms of a geometric series # hence cn = c^((p^n-1)/(p-1)) # We now expand c # cn = SNR^((p-1)/{halfK})^coef^((p^n-1)/(p-1)) # cn = SNR^((p^n-1)/{halfK})^coef # cn = SNR^(coef * (p^n-1)/{halfK}) const {curve_name}_FrobeniusMapCoefficients* = [ """) arr = "" maxN = 3 # We only need up to f^(p^3) in final exponentiation for n in range(1, maxN + 1): for coef in range(halfK): if coef == 0: arr += f'\n# frobenius({n}) -----------------------\n' arr += '[' frobmapcoef = SNR^(coef*((p^n-1)/halfK)) hatN = '^' + str(n) if n>1 else '' arr += field_to_nim(frobmapcoef, 'Fp2', curve_name, comment_right = f'SNR^((p{hatN}-1)/{halfK})^{coef}') if coef != halfK - 1: arr += ',\n' arr += '],\n' buf += textwrap.indent(arr, ' ') buf += ']' return buf def genFrobeniusPsiConstants(curve_name, curve_config): embdeg = curve_config[curve_name]['tower']['embedding_degree'] twdeg = curve_config[curve_name]['tower']['twist_degree'] twkind = curve_config[curve_name]['tower']['twist'] g2field = f'Fp{embdeg//twdeg}' if (embdeg//twdeg) > 1 else 'Fp' p = curve_config[curve_name]['field']['modulus'] Fp = GF(p) K. = PolynomialRing(Fp) if g2field == 'Fp2': QNR_Fp = curve_config[curve_name]['tower']['QNR_Fp'] Fp2. = Fp.extension(u^2 - QNR_Fp) if g2field == 'Fp2': SNR = curve_config[curve_name]['tower']['SNR_Fp2'] SNR = Fp2(SNR) else: SNR = curve_config[curve_name]['tower']['SNR_Fp'] SNR = Fp(SNR) print('\n----> ψ (Psi) - Untwist-Frobenius-Twist Endomorphism constants <----\n') buf = inspect.cleandoc(f""" # ψ (Psi) - Untwist-Frobenius-Twist Endomorphisms on twisted curves # ----------------------------------------------------------------- """) buf += '\n' if twkind == 'D_Twist': buf += f'# {curve_name} is a D-Twist: psi1_coef1 = SNR^((p-1)/{twdeg})\n\n' xi = SNR snrUsed = 'SNR' else: buf += f'# {curve_name} is a M-Twist: psi1_coef1 = (1/SNR)^((p-1)/{twdeg})\n\n' xi = 1/SNR snrUsed = '(1/SNR)' maxPsi = CyclotomicField(embdeg).degree() for n in range(1, maxPsi+1): for coef in range(2, 3+1): # Same formula as FrobeniusMap constants # except that # - we only need 2 coefs for elliptic curve twists # - xi = SNR or 1/SNR depending on D-Twist or M-Twist respectively # - the divisor is the twist degree isntead of half the embedding degree frobpsicoef = xi^(coef*(p^n - 1)/twdeg) hatN = '^' + str(n) if n>1 else '' buf += field_to_nim( frobpsicoef, g2field, curve_name, prefix = f'const {curve_name}_FrobeniusPsi_psi{n}_coef{coef}* = ', comment_above = f'{snrUsed}^({coef}(p{hatN}-1)/{twdeg})' ) + '\n' buf += '\n' buf += inspect.cleandoc(f""" # For a sextic twist # - p ≡ 1 (mod 2) # - p ≡ 1 (mod 3) # # psi2_coef3 is always -1 (mod p^m) with m = embdeg/twdeg # Recap, with ξ (xi) the sextic non-residue for D-Twist or 1/SNR for M-Twist # psi_2 ≡ ξ^((p-1)/6)^2 ≡ ξ^((p-1)/3) # psi_3 ≡ psi_2 * ξ^((p-1)/6) ≡ ξ^((p-1)/3) * ξ^((p-1)/6) ≡ ξ^((p-1)/2) # # In Fp² (i.e. embedding degree of 12, G2 on Fp2) # - quadratic non-residues respect the equation a^((p²-1)/2) ≡ -1 (mod p²) by the Legendre symbol # - sextic non-residues are also quadratic non-residues so ξ^((p²-1)/2) ≡ -1 (mod p²) # - QRT(1/a) = QRT(a) with QRT the quadratic residuosity test # # We have psi2_3 ≡ psi_3 * psi_3^p ≡ psi_3^(p+1) # ≡ (ξ^(p-1)/2)^(p+1) (mod p²) # ≡ ξ^((p-1)(p+1)/2) (mod p²) # ≡ ξ^((p²-1)/2) (mod p²) # And ξ^((p²-1)/2) ≡ -1 (mod p²) since ξ is a quadratic non-residue # So psi2_3 ≡ -1 (mod p²) # # # In Fp (i.e. embedding degree of 6, G2 on Fp) # - Fermat's Little Theorem gives us a^(p-1) ≡ 1 (mod p) # # psi2_3 ≡ ξ^((p-1)(p+1)/2) (mod p) # ≡ ξ^((p+1)/2)^(p-1) (mod p) as we have 2|p+1 # ≡ 1 (mod p) by Fermat's Little Theorem """) return buf # CLI # --------------------------------------------------------- if __name__ == "__main__": # Usage # BLS12-381 # sage sage/derive_frobenius.sage BLS12_381 from argparse import ArgumentParser parser = ArgumentParser() parser.add_argument("curve",nargs="+") args = parser.parse_args() curve = args.curve[0] if curve not in Curves: raise ValueError( curve + ' is not one of the available curves: ' + str(Curves.keys()) ) else: trace = Curves[curve]['field']['trace'] print(f'trace of Frobenius ({int(trace).bit_length()}-bit): 0x{Integer(trace).hex()}') FrobMap = genFrobeniusMapConstants(curve, Curves) FrobPsi = genFrobeniusPsiConstants(curve, Curves) with open(f'{curve.lower()}_frobenius.nim', 'w') as f: f.write(copyright()) f.write('\n\n') embdeg = Curves[curve]['tower']['embedding_degree'] twdeg = Curves[curve]['tower']['twist_degree'] if embdeg//twdeg >= 2: f.write(inspect.cleandoc(""" import ../config/curves, ../towers, ../io/io_towers """)) else: f.write(inspect.cleandoc(""" import ../config/[curves, type_ff], ../towers, ../io/[io_fields, io_towers] """)) f.write('\n\n') f.write(FrobMap) f.write('\n\n') f.write(FrobPsi) print(f'Successfully created {curve}_frobenius.nim') print(inspect.cleandoc("""\n For testing you can verify the following invariants: Galbraith-Lin-Scott, 2008, Theorem 1 Fuentes-Castaneda et al, 2011, Equation (2) ψ(ψ(P)) - t*ψ(P) + p*P == Infinity Galbraith-Scott, 2008, Lemma 1 The cyclotomic polynomial with GΦ(ψ(P)) == Infinity Hence for embedding degree k=12 ψ⁴(P) - ψ²(P) + P == Infinity for embedding degree k=6 ψ²(P) - ψ(P) + P == Infinity """))