diff --git a/README-PERFORMANCE.md b/README-PERFORMANCE.md index 2e6eb9d7..85f3c443 100644 --- a/README-PERFORMANCE.md +++ b/README-PERFORMANCE.md @@ -58,7 +58,7 @@ The full list of benchmarks is available in the [`benchmarks`](./benchmarks) fol As mentioned in the [Compiler caveats](#compiler-caveats) section, GCC is up to 2x slower than Clang due to mishandling of carries and register usage. -#### Ethereum BLS signatures (over BLS12-381 G2) +#### Ethereum BLS signatures (over BLS12-381 𝔾₂) ![Bench Ethereum BLS signature](./media/ethereum_bls_signatures.png) @@ -192,4 +192,4 @@ Constantine does not use heap allocation. At the moment Constantine is optimized for 32-bit and 64-bit CPUs. When performance and code size conflicts, a careful and informed default is chosen. -In the future, a compile-time flag that goes beyond the compiler `-Os` might be provided. \ No newline at end of file +In the future, a compile-time flag that goes beyond the compiler `-Os` might be provided. diff --git a/README.md b/README.md index d5ea9dcf..8b731eb3 100644 --- a/README.md +++ b/README.md @@ -90,8 +90,8 @@ For all elliptic curves, the following arithmetic is supported - on Fr (i.e. modulo the 255-bit curve order) - on Fp (i.e. modulo the 381-bit prime modulus) - elliptic curve arithmetic: - - on elliptic curve over Fp (EC G1) with affine, jacobian and homogenous projective coordinates - - on elliptic curve over Fp2 (EC G2) with affine, jacobian and homogenous projective coordinates + - on elliptic curve over Fp (EC 𝔾₁) with affine, jacobian and homogenous projective coordinates + - on elliptic curve over Fp2 (EC 𝔾₂) with affine, jacobian and homogenous projective coordinates - including scalar multiplication, multi-scalar-multiplication (MSM) and parallel MSM _All operations are constant-time unless explicitly mentioned_ vartime. @@ -222,7 +222,7 @@ and modify Constantine's [`build.rs`](https://github.com/mratsim/constantine/blo ``` > [!IMPORTANT] > Constantine uses a separate modfile for tests.
It has no dependencies (key to avoid supply chain attacks) except for testing. - + ### From C 1. Install a C compiler, `clang` is recommended, for example: diff --git a/constantine.nimble b/constantine.nimble index dbdccb3f..a6cb8a4a 100644 --- a/constantine.nimble +++ b/constantine.nimble @@ -430,7 +430,7 @@ const testDesc: seq[tuple[path: string, useGMP: bool]] = @[ # ---------------------------------------------------------- ("tests/math_elliptic_curves/t_ec_conversion.nim", false), - # Elliptic curve arithmetic G1 + # Elliptic curve arithmetic 𝔾₁ # ---------------------------------------------------------- ("tests/math_elliptic_curves/t_ec_shortw_prj_g1_add_double.nim", false), # ("tests/math_elliptic_curves/t_ec_shortw_prj_g1_mul_sanity.nim", false), @@ -458,7 +458,7 @@ const testDesc: seq[tuple[path: string, useGMP: bool]] = @[ ("tests/math_elliptic_curves/t_ec_twedwards_mul_endomorphism_bandersnatch", false), - # Elliptic curve arithmetic G2 + # Elliptic curve arithmetic 𝔾₂ # ---------------------------------------------------------- # ("tests/math_elliptic_curves/t_ec_shortw_prj_g2_add_double_bn254_snarks.nim", false), # ("tests/math_elliptic_curves/t_ec_shortw_prj_g2_mul_sanity_bn254_snarks.nim", false), @@ -551,7 +551,7 @@ const testDesc: seq[tuple[path: string, useGMP: bool]] = @[ # ("tests/math_pairings/t_pairing_bls12_381_gt_subgroup.nim", false), # ("tests/math_pairings/t_pairing_bw6_761_gt_subgroup.nim", false), - # Pairing + # Pairing & # ---------------------------------------------------------- # ("tests/math_pairings/t_pairing_bls12_377_line_functions.nim", false), # ("tests/math_pairings/t_pairing_bls12_381_line_functions.nim", false), @@ -562,6 +562,8 @@ const testDesc: seq[tuple[path: string, useGMP: bool]] = @[ # ("tests/math_pairings/t_pairing_bls12_377_optate.nim", false), # ("tests/math_pairings/t_pairing_bls12_381_optate.nim", false), + ("tests/math_pairings/t_pairing_bn254_snarks_gt_exp.nim", false), + # Multi-Pairing # ---------------------------------------------------------- ("tests/math_pairings/t_pairing_bn254_nogami_multi.nim", false), @@ -977,25 +979,25 @@ task bench_fp6, "Run benchmark 𝔽p6 with your CC compiler": task bench_fp12, "Run benchmark 𝔽p12 with your CC compiler": runBench("bench_fp12") -# Elliptic curve G1 +# Elliptic curve 𝔾₁ # ------------------------------------------ task bench_ec_g1, "Run benchmark on Elliptic Curve group 𝔾1 - CC compiler": runBench("bench_ec_g1") -# Elliptic curve G1 - batch operations +# Elliptic curve 𝔾₁ - batch operations # ------------------------------------------ task bench_ec_g1_batch, "Run benchmark on Elliptic Curve group 𝔾1 (batch ops) - CC compiler": runBench("bench_ec_g1_batch") -# Elliptic curve G1 - scalar multiplication +# Elliptic curve 𝔾₁ - scalar multiplication # ------------------------------------------ task bench_ec_g1_scalar_mul, "Run benchmark on Elliptic Curve group 𝔾1 (Scalar Multiplication) - CC compiler": runBench("bench_ec_g1_scalar_mul") -# Elliptic curve G1 - Multi-scalar-mul +# Elliptic curve 𝔾₁ - Multi-scalar-mul # ------------------------------------------ task bench_ec_msm_pasta, "Run benchmark: Multi-Scalar-Mul for Pasta curves - CC compiler": @@ -1014,13 +1016,13 @@ task bench_ec_msm_bandersnatch, "Run benchmark: Multi-Scalar-Mul for Bandersnatc runBench("bench_ec_msm_bandersnatch") -# Elliptic curve G2 +# Elliptic curve 𝔾₂ # ------------------------------------------ task bench_ec_g2, "Run benchmark on Elliptic Curve group 𝔾2 - CC compiler": runBench("bench_ec_g2") -# Elliptic curve G2 - scalar multiplication +# Elliptic curve 𝔾₂ - scalar multiplication # ------------------------------------------ task bench_ec_g2_scalar_mul, "Run benchmark on Elliptic Curve group 𝔾2 (Multi-Scalar-Mul) - CC compiler": diff --git a/constantine/math/elliptic/ec_scalar_mul_vartime.nim b/constantine/math/elliptic/ec_scalar_mul_vartime.nim index 825a4394..b2727daa 100644 --- a/constantine/math/elliptic/ec_scalar_mul_vartime.nim +++ b/constantine/math/elliptic/ec_scalar_mul_vartime.nim @@ -180,7 +180,7 @@ func initNAF[precompSize, NafMax: static int, EC, ECaff]( P.fromAffine(tab[digit shr 1]) return true elif digit < 0: - P.fromAffine(tab[digit shr 1]) + P.fromAffine(tab[-digit shr 1]) P.neg() return true else: diff --git a/constantine/math/extension_fields/towers.nim b/constantine/math/extension_fields/towers.nim index b70fa255..1d76c379 100644 --- a/constantine/math/extension_fields/towers.nim +++ b/constantine/math/extension_fields/towers.nim @@ -329,9 +329,6 @@ func has2extraBits*(E: type ExtensionField): bool = ## We construct extensions only on Fp (and not Fr) getSpareBits(Fp[E.F.Name]) >= 2 -template A(E: type ExtensionField2x): Algebra = - E.F.Name - template c0*(a: ExtensionField2x): auto = a.coords[0] template c1*(a: ExtensionField2x): auto = diff --git a/constantine/math/pairings/gt_exponentiations_vartime.nim b/constantine/math/pairings/gt_exponentiations_vartime.nim new file mode 100644 index 00000000..f41cedb8 --- /dev/null +++ b/constantine/math/pairings/gt_exponentiations_vartime.nim @@ -0,0 +1,233 @@ +# 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. + +import + # Internals + constantine/math/elliptic/ec_endomorphism_accel, + constantine/math/arithmetic, + constantine/math/extension_fields, + constantine/math/io/io_bigints, + constantine/platforms/abstractions, + constantine/math_arbitrary_precision/arithmetic/limbs_views, + constantine/named/zoo_endomorphisms, + constantine/named/algebras, + ./cyclotomic_subgroups + +{.push raises: [].} # No exceptions allowed in core cryptographic operations +{.push checks: off.} # No defects due to array bound checking or signed integer overflow allowed + +iterator unpackBE(scalarByte: byte): bool = + for i in countdown(7, 0): + yield bool((scalarByte shr i) and 1) + +func gtExp_sqrmul_vartime*[Gt: ExtensionField](r: var Gt, a: Gt, scalar: BigInt) {.tags:[VarTime], meter.} = + ## **Variable-time** Exponentiation in Gt + ## + ## r <- aᵏ + ## + ## This uses the square-and-multiply algorithm + ## This MUST NOT be used with secret data. + ## + ## This is highly VULNERABLE to timing attacks and power analysis attacks. + var scalarCanonical: array[scalar.bits.ceilDiv_vartime(8), byte] + scalarCanonical.marshal(scalar, bigEndian) + + let a {.noInit.} = a # Avoid aliasing issues + + r.setOne() + var isNeutral = true + + for scalarByte in scalarCanonical: + for bit in unpackBE(scalarByte): + if not isNeutral: + r.square() + if bit: + if isNeutral: + r = a + isNeutral = false + else: + r *= a + +func gtExp_addchain_4bit_vartime[Gt: ExtensionField](r: var Gt, a: Gt, scalar: BigInt) {.tags:[VarTime], meter.} = + ## **Variable-time** Exponentiation in Gt + ## This can only handle for small scalars up to 2⁴ = 16 excluded + let s = uint scalar.limbs[0] + + case s + of 0: + r.setNeutral() + of 1: + discard + of 2: + r.square(a) + of 3: + var t {.noInit.}: Gt + t.square(a) + r.prod(a, t) + of 4: + r.square(a) + r.square() + of 5: + var t {.noInit.}: Gt + t.square(a) + t.square() + r.prod(a, t) + of 6: + var t {.noInit.}: Gt + t.square(a) + r.prod(a, t) + r.square() + of 7: + var t {.noInit.}: Gt + t.square(a) + t.square() + t.square() + r.cyclotomic_inv(a) + r *= t + of 8: + r.square(a) + r.square() + r.square() + of 9: + var t {.noInit.}: Gt + t.square(a) + t.square() + t.square() + r.prod(a, t) + of 10: + var t {.noInit.}: Gt + t.square(a) + t.square() + r.prod(a, t) + r.square() + of 11: + var t1 {.noInit.}, t2 {.noInit.}: Gt + t1.square(a) # [2]P + t2.square(t1) + t2.square() # [8]P + t1 *= t2 + r.prod(a, t1) + of 12: + var t1 {.noInit.}, t2 {.noInit.}: Gt + t1.square(a) + t1.square() # [4]P + t2.square(t1) # [8]P + r.prod(t1, t2) + of 13: + var t1 {.noInit.}, t2 {.noInit.}: Gt + t1.square(a) + t1.square() # [4]P + t2.square(t1) # [8]P + t1 *= t2 + r.prod(a, t1) + of 14: + var t {.noInit.}: Gt + t.square(a) + t.square() + t.square() + r.cyclotomic_inv(a) + t *= r # [7]P + r.square(t) + of 15: + var t {.noInit.}: Gt + t.square(a) + t.square() + t.square() + t.square() + r.cyclotomic_inv(a) + r *= t + else: + unreachable() + +func gtExp_minHammingWeight_vartime*[Gt: ExtensionField](r: var Gt, a: Gt, scalar: BigInt) {.tags:[VarTime].} = + ## **Variable-time** Exponentiation in Gt + ## + ## r <- aᵏ + ## + ## This uses an online recoding with minimum Hamming Weight + ## (which is not NAF, NAF is least-significant bit to most) + ## This MUST NOT be used with secret data. + ## + ## This is highly VULNERABLE to timing attacks and power analysis attacks + let a {.noInit.} = a # Avoid aliasing issues + var na {.noInit.}: Gt + na.cyclotomic_inv(a) + + r.setOne() + for bit in recoding_l2r_signed_vartime(scalar): + r.square() + if bit == 1: + r *= a + elif bit == -1: + r *= na + +func initNAF[precompSize, NafMax: static int, Gt: ExtensionField]( + acc: var Gt, + tab: array[precompSize, Gt], + naf: array[NafMax, int8], nafLen: int, + nafIteratorIdx: int): bool {.inline.} = + + let digit = naf[nafLen-1-nafIteratorIdx] + if digit > 0: + acc = tab[digit shr 1] + return true + elif digit < 0: + acc.cyclotomic_inv(tab[digit shr 1]) + return true + else: + acc.setOne() + return false + +func accumNAF[precompSize, NafMax: static int, Gt: ExtensionField]( + acc: var Gt, + tab: array[precompSize, Gt], + naf: array[NafMax, int8], nafLen: int, + nafIteratorIdx: int) {.inline.} = + + let digit = naf[nafLen-1-nafIteratorIdx] + if digit > 0: + acc *= tab[digit shr 1] + elif digit < 0: + var neg {.noInit.}: Gt + neg.cyclotomic_inv(tab[-digit shr 1]) + acc *= neg + +func gtExp_minHammingWeight_windowed_vartime*[Gt: ExtensionField]( + r: var Gt, a: Gt, scalar: BigInt, window: static int) {.tags:[VarTime], meter.} = + ## **Variable-time** Exponentiation in Gt + ## + ## r <- aᵏ + ## + ## This uses windowed-NAF (wNAF) + ## This MUST NOT be used with secret data. + ## + ## This is highly VULNERABLE to timing attacks and power analysis attacks + + # Signed digits divides precomputation table size by 2 + # Odd-only divides precomputation table size by another 2 + + const precompSize = 1 shl (window - 2) + static: doAssert window < 8, "Window is too large and precomputation would use " & $(precompSize * sizeof(Gt)) & " stack space." + + var tab {.noinit.}: array[precompSize, Gt] + var a2{.noInit.}: Gt + tab[0] = a + a2.square(a) + for i in 1 ..< tab.len: + tab[i].prod(tab[i-1], a2) + + var naf {.noInit.}: array[BigInt.bits+1, int8] + let nafLen = naf.recode_r2l_signed_window_vartime(scalar, window) + + var isInit = false + for i in 0 ..< nafLen: + if isInit: + r.square() + r.accumNAF(tab, naf, nafLen, i) + else: + isInit = r.initNAF(tab, naf, nafLen, i) diff --git a/constantine/named/zoo_endomorphisms.nim b/constantine/named/zoo_endomorphisms.nim index 0561b985..429f949c 100644 --- a/constantine/named/zoo_endomorphisms.nim +++ b/constantine/named/zoo_endomorphisms.nim @@ -8,6 +8,7 @@ import std/macros, + constantine/platforms/abstractions, constantine/math/extension_fields, constantine/math/isogenies/frobenius, constantine/math/elliptic/[ @@ -103,6 +104,10 @@ func computeEndomorphisms*[EC; M: static int](endos: var array[M-1, EC], P: EC) else: {.error: "Unconfigured".} +func computeEndomorphisms*[Gt: ExtensionField; M: static int](endos: var array[M-1, Gt], a: Gt) = + staticFor i, 0, M-1: + endos[i].frobenius_map(a, i+1) + func hasEndomorphismAcceleration*(Name: static Algebra): bool {.compileTime.} = Name in { Bandersnatch, diff --git a/sage/derive_endomorphisms.sage b/sage/derive_endomorphisms.sage index 0f17413c..c17ecace 100644 --- a/sage/derive_endomorphisms.sage +++ b/sage/derive_endomorphisms.sage @@ -171,7 +171,7 @@ def genCubicRootEndo(curve_name, curve_config): return phi1, lattice, babai -# G2 Endomorphism +# 𝔾₂ Endomorphism # --------------------------------------------------------- def genPsiEndo(curve_name, curve_config): @@ -258,7 +258,7 @@ if __name__ == "__main__": print('\nPrecomputing G1 - 𝜑 (phi) cubic root endomorphism') print('----------------------------------------------------\n') cubeRootModP, g1lat, g1babai = genCubicRootEndo(curve, Curves) - print('\n\nPrecomputing G2 - ψ (Psi) - untwist-Frobenius-twist endomorphism') + print('\n\nPrecomputing 𝔾₂ - ψ (Psi) - untwist-Frobenius-twist endomorphism') print('----------------------------------------------------\n') g2lat, g2babai = genPsiEndo(curve, Curves) @@ -290,7 +290,7 @@ if __name__ == "__main__": )) f.write('\n\n') f.write(inspect.cleandoc(f""" - # {curve} G2 + # {curve} 𝔾₂ # ------------------------------------------------------------ """)) f.write('\n\n') diff --git a/sage/derive_frobenius.sage b/sage/derive_frobenius.sage index 12932644..8746437c 100644 --- a/sage/derive_frobenius.sage +++ b/sage/derive_frobenius.sage @@ -216,7 +216,7 @@ def genFrobeniusPsiConstants(curve_name, curve_config): # 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) + # In Fp² (i.e. embedding degree of 12, 𝔾₂ 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 @@ -229,7 +229,7 @@ def genFrobeniusPsiConstants(curve_name, curve_config): # So psi2_3 ≡ -1 (mod p²) # # - # In Fp (i.e. embedding degree of 6, G2 on Fp) + # In Fp (i.e. embedding degree of 6, 𝔾₂ on Fp) # - Fermat's Little Theorem gives us a^(p-1) ≡ 1 (mod p) # # psi2_3 ≡ ξ^((p-1)(p+1)/2) (mod p) diff --git a/tests/math_pairings/t_pairing_bn254_snarks_gt_exp.nim b/tests/math_pairings/t_pairing_bn254_snarks_gt_exp.nim new file mode 100644 index 00000000..dea4efd8 --- /dev/null +++ b/tests/math_pairings/t_pairing_bn254_snarks_gt_exp.nim @@ -0,0 +1,15 @@ +# 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. + +import + # Test utilities + ./t_pairing_template + +runGTexponentiationTests( + Iters = 4, + GT = Fp12[BN254_Nogami]) diff --git a/tests/math_pairings/t_pairing_template.nim b/tests/math_pairings/t_pairing_template.nim index c7378d1f..1d88c1f8 100644 --- a/tests/math_pairings/t_pairing_template.nim +++ b/tests/math_pairings/t_pairing_template.nim @@ -16,7 +16,7 @@ import constantine/named/algebras, constantine/named/zoo_subgroups, constantine/math/elliptic/[ec_shortweierstrass_affine, ec_shortweierstrass_projective], - constantine/math/pairings/cyclotomic_subgroups, + constantine/math/pairings/[cyclotomic_subgroups, gt_exponentiations_vartime, pairings_generic], constantine/math/io/io_extfields, # Test utilities @@ -27,7 +27,7 @@ export ec_shortweierstrass_affine, ec_shortweierstrass_projective, arithmetic, extension_fields, io_extfields, - cyclotomic_subgroups, + cyclotomic_subgroups, gt_exponentiations_vartime, abstractions, algebras type @@ -105,7 +105,7 @@ template runPairingTests*(Iters: static int, Name: static Algebra, G1, G2, GT: t test_bilinearity_double_impl(randZ = false, gen = HighHammingWeight) test_bilinearity_double_impl(randZ = false, gen = Long01Sequence) -func random_elem*(rng: var RngState, F: typedesc, gen: RandomGen): F {.inline, noInit.} = +func random_elem(rng: var RngState, F: typedesc, gen: RandomGen): F {.inline, noInit.} = if gen == Uniform: result = rng.random_unsafe(F) elif gen == HighHammingWeight: @@ -114,7 +114,7 @@ func random_elem*(rng: var RngState, F: typedesc, gen: RandomGen): F {.inline, n result = rng.random_long01Seq(F) template runGTsubgroupTests*(Iters: static int, GT: typedesc, finalExpHard_fn: untyped): untyped {.dirty.}= - bind affineType + bind affineType, random_elem var rng: RngState let timeseed = uint32(toUnix(getTime()) and (1'i64 shl 32 - 1)) # unixTime mod 2^32 @@ -139,8 +139,67 @@ template runGTsubgroupTests*(Iters: static int, GT: typedesc, finalExpHard_fn: u stdout.write '\n' - suite "Pairing - GT subgroup " & $GT.Name & " [" & $WordBitWidth & "-bit words]": - test "Final Exponentiation and GT-subgroup membership": + suite "Pairing - 𝔾ₜ subgroup " & $GT.Name & " [" & $WordBitWidth & "-bit words]": + test "Final Exponentiation and 𝔾ₜ-subgroup membership": test_gt_impl(gen = Uniform) test_gt_impl(gen = HighHammingWeight) test_gt_impl(gen = Long01Sequence) + +func random_gt*(rng: var RngState, F: typedesc, gen: RandomGen): F {.inline, noInit.} = + if gen == Uniform: + result = rng.random_unsafe(F) + elif gen == HighHammingWeight: + result = rng.random_highHammingWeight(F) + else: + result = rng.random_long01Seq(F) + + result.finalExp() + +template runGTexponentiationTests*(Iters: static int, GT: typedesc): untyped {.dirty.} = + var rng: RngState + let timeseed = uint32(toUnix(getTime()) and (1'i64 shl 32 - 1)) # unixTime mod 2^32 + seed(rng, timeseed) + echo "\n------------------------------------------------------\n" + echo "test_pairing_",$GT.Name,"_gt_exponentiation xoshiro512** seed: ", timeseed + + proc test_gt_exponentiation_impl(gen: RandomGen) = + stdout.write " " + for _ in 0 ..< Iters: + let a = rng.random_gt(GT, gen) + let kUnred = rng.random_long01seq(GT.Name.getBigInt(kScalarField)) + var k {.noInit.}: GT.Name.getBigInt(kScalarField) + discard k.reduce_vartime(kUnred, GT.Name.scalarFieldModulus()) + + # Reference impl using exponentiation with tables on any field/extension field + var r_ref = a + r_ref.pow_vartime(k, window = 3) + + # Square-and-multiply + var r_sqrmul {.noInit.}: GT + r_sqrmul.gtExp_sqrmul_vartime(a, k) + doAssert bool(r_ref == r_sqrmul) + + # MSB->LSB min Hamming Weight signed recoding + var r_l2r_recoding {.noInit.}: GT + r_l2r_recoding.gtExp_minHammingWeight_vartime(a, k) + doAssert bool(r_ref == r_l2r_recoding) + + # Windowed NAF + var r_wNAF {.noInit.}: GT + r_wNAF.gtExp_minHammingWeight_windowed_vartime(a, k, window = 2) + doAssert bool(r_ref == r_wNAF) + r_wNAF.gtExp_minHammingWeight_windowed_vartime(a, k, window = 3) + doAssert bool(r_ref == r_wNAF) + r_wNAF.gtExp_minHammingWeight_windowed_vartime(a, k, window = 4) + doAssert bool(r_ref == r_wNAF) + + stdout.write '.' + + stdout.write '\n' + + + suite "Pairing - Exponentiation for 𝔾ₜ " & $GT.Name & " [" & $WordBitWidth & "-bit words]": + test "𝔾ₜ exponentiation consistency": + test_gt_exponentiation_impl(gen = Uniform) + test_gt_exponentiation_impl(gen = HighHammingWeight) + test_gt_exponentiation_impl(gen = Long01Sequence)