diff --git a/constantine.nimble b/constantine.nimble index 7827eb5d9..e6dd7a15a 100644 --- a/constantine.nimble +++ b/constantine.nimble @@ -497,6 +497,7 @@ const testDesc: seq[tuple[path: string, useGMP: bool]] = @[ ("tests/t_ethereum_bls_signatures.nim", false), ("tests/t_ethereum_eip2333_bls12381_key_derivation.nim", false), ("tests/t_ethereum_eip4844_deneb_kzg.nim", false), + ("tests/t_ethereum_verkle_primitives.nim", false), ("tests/t_ethereum_eip4844_deneb_kzg_parallel.nim", false), ] diff --git a/constantine/ethereum_verkle_primitives.nim b/constantine/ethereum_verkle_primitives.nim new file mode 100644 index 000000000..043210353 --- /dev/null +++ b/constantine/ethereum_verkle_primitives.nim @@ -0,0 +1,94 @@ +# 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. + +## ############################################################ +## +## Verkle Trie primitives for Ethereum +## +## ############################################################ + +import + ./math/config/[type_ff, curves], + ./math/arithmetic, + ./math/elliptic/[ + ec_twistededwards_projective, + ec_twistededwards_batch_ops + ], + ./math/io/[io_bigints, io_fields], + ./curves_primitives + + +func mapToBaseField*(dst: var Fp[Banderwagon],p: ECP_TwEdwards_Prj[Fp[Banderwagon]]) = + ## The mapping chosen for the Banderwagon Curve is x/y + ## + ## This function takes a Banderwagon element & then + ## computes the x/y value and returns as an Fp element + ## + ## Spec : https://hackmd.io/@6iQDuIePQjyYBqDChYw_jg/BJBNcv9fq#Map-To-Field + + var invY: Fp[Banderwagon] + invY.inv(p.y) # invY = 1/Y + dst.prod(p.x, invY) # dst = (X) * (1/Y) + +func mapToScalarField*(res: var Fr[Banderwagon], p: ECP_TwEdwards_Prj[Fp[Banderwagon]]): bool {.discardable.} = + ## This function takes the x/y value from the above function as Fp element + ## and convert that to bytes in Big Endian, + ## and then load that to a Fr element + ## + ## Spec : https://hackmd.io/wliPP_RMT4emsucVuCqfHA?view#MapToFieldElement + + var baseField: Fp[Banderwagon] + var baseFieldBytes: array[32, byte] + + baseField.mapToBaseField(p) # compute the defined mapping + + let check1 = baseFieldBytes.marshalBE(baseField) # Fp -> bytes + let check2 = res.unmarshalBE(baseFieldBytes) # bytes -> Fr + + return check1 and check2 + +## ############################################################ +## +## Batch Operations +## +## ############################################################ +func batchMapToScalarField*( + res: var openArray[Fr[Banderwagon]], + points: openArray[ECP_TwEdwards_Prj[Fp[Banderwagon]]]): bool {.discardable.} = + ## This function performs the `mapToScalarField` operation + ## on a batch of points + ## + ## The batch inversion used in this using + ## the montogomenry trick, makes is faster than + ## just iterating of over the array of points and + ## converting the curve points to field elements + ## + ## Spec : https://hackmd.io/wliPP_RMT4emsucVuCqfHA?view#MapToFieldElement + + var check: bool = true + check = check and (res.len == points.len) + + let N = res.len + var ys = allocStackArray(Fp[Banderwagon], N) + var ys_inv = allocStackArray(Fp[Banderwagon], N) + + + for i in 0 ..< N: + ys[i] = points[i].y + + ys_inv.batchInvert(ys, N) + + for i in 0 ..< N: + var mappedElement: Fp[Banderwagon] + var bytes: array[32, byte] + + mappedElement.prod(points[i].x, ys_inv[i]) + check = bytes.marshalBE(mappedElement) + check = check and res[i].unmarshalBE(bytes) + + return check \ No newline at end of file diff --git a/constantine/math/elliptic/ec_twistededwards_batch_ops.nim b/constantine/math/elliptic/ec_twistededwards_batch_ops.nim new file mode 100644 index 000000000..cdcf684a3 --- /dev/null +++ b/constantine/math/elliptic/ec_twistededwards_batch_ops.nim @@ -0,0 +1,123 @@ +# 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 + ../../platforms/abstractions, + ../arithmetic, + ./ec_twistededwards_affine, + ./ec_twistededwards_projective + +# No exceptions allowed, or array bound checks or integer overflow +{.push raises: [], checks:off.} + +# ############################################################ +# +# Elliptic Curve in Twisted Edwards form +# Batch conversion +# +# ############################################################ + +func batchAffine*[F]( + affs: ptr UncheckedArray[ECP_TwEdwards_Aff[F]], + projs: ptr UncheckedArray[ECP_TwEdwards_Prj[F]], + N: int) {.noInline, tags:[Alloca].} = + # Algorithm: Montgomery's batch inversion + # - Speeding the Pollard and Elliptic Curve Methods of Factorization + # Section 10.3.1 + # Peter L. Montgomery + # https://www.ams.org/journals/mcom/1987-48-177/S0025-5718-1987-0866113-7/S0025-5718-1987-0866113-7.pdf + # - Modern Computer Arithmetic + # Section 2.5.1 Several inversions at once + # Richard P. Brent and Paul Zimmermann + # https://members.loria.fr/PZimmermann/mca/mca-cup-0.5.9.pdf + + # To avoid temporaries, we store partial accumulations + # in affs[i].x + let zeroes = allocStackArray(SecretBool, N) + affs[0].x = projs[0].z + zeroes[0] = affs[0].x.isZero() + affs[0].x.csetOne(zeroes[0]) + + for i in 1 ..< N: + # Skip zero z-coordinates (infinity points) + var z = projs[i].z + zeroes[i] = z.isZero() + z.csetOne(zeroes[i]) + + if i != N-1: + affs[i].x.prod(affs[i-1].x, z, skipFinalSub = true) + else: + affs[i].x.prod(affs[i-1].x, z, skipFinalSub = false) + + var accInv {.noInit.}: F + accInv.inv(affs[N-1].x) + + for i in countdown(N-1, 1): + # Extract 1/Pᵢ + var invi {.noInit.}: F + invi.prod(accInv, affs[i-1].x, skipFinalSub = true) + invi.csetZero(zeroes[i]) + + # Now convert Pᵢ to affine + affs[i].x.prod(projs[i].x, invi) + affs[i].y.prod(projs[i].y, invi) + + # next iteration + invi = projs[i].z + invi.csetOne(zeroes[i]) + accInv.prod(accInv, invi, skipFinalSub = true) + + block: # tail + accInv.csetZero(zeroes[0]) + affs[0].x.prod(projs[0].x, accInv) + affs[0].y.prod(projs[0].y, accInv) + +func batchAffine*[N: static int, F]( + affs: var array[N, ECP_TwEdwards_Aff[F]], + projs: array[N, ECP_TwEdwards_Prj[F]]) {.inline.} = + batchAffine(affs.asUnchecked(), projs.asUnchecked(), N) + +func batchAffine*[M, N: static int, F]( + affs: var array[M, array[N, ECP_TwEdwards_Aff[F]]], + projs: array[M, array[N, ECP_TwEdwards_Prj[F]]]) {.inline.} = + batchAffine(affs[0].asUnchecked(), projs[0].asUnchecked(), M*N) + +func batchInvert*[F]( + dst: ptr UncheckedArray[F], + elements: ptr UncheckedArray[F], + N: int + ) {.noInline.} = + ## Montgomery's batch inversion + var zeros = allocStackArray(bool, N) + zeroMem(zeros, N) + + var accumulator: F + accumulator.setOne() # sets the accumulator to 1 + + for i in 0 ..< N: + if elements[i].isZero().bool(): + zeros[i] = true + continue + + dst[i] = accumulator + accumulator *= elements[i] + + accumulator.inv() # inversion of the accumulator + + for i in countdown(N-1, 0): + if zeros[i] == true: + continue + dst[i] *= accumulator + accumulator *= elements[i] + +func batchInvert*[F](dst: openArray[F], source: openArray[F]): bool {.inline.} = + if dst.len != source.len: + return false + let N = dst.len + batchInvert(dst.asUnchecked(), source.asUnchecked(), N) + return true diff --git a/tests/t_banderwagon.nim b/tests/t_ethereum_verkle_primitives.nim similarity index 64% rename from tests/t_banderwagon.nim rename to tests/t_ethereum_verkle_primitives.nim index 9c48d07c8..2cb73f6fa 100644 --- a/tests/t_banderwagon.nim +++ b/tests/t_ethereum_verkle_primitives.nim @@ -6,12 +6,19 @@ # * 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. +# ############################################################ +# +# Ethereum Verkle Primitves Tests +# +# ############################################################ + import std/unittest, ../constantine/math/config/[type_ff, curves], ../constantine/math/elliptic/[ ec_twistededwards_affine, - ec_twistededwards_projective + ec_twistededwards_projective, + ec_twistededwards_batch_ops ], ../constantine/math/io/io_fields, ../constantine/serialization/[ @@ -20,7 +27,8 @@ import codecs ], ../constantine/math/arithmetic, - ../constantine/math/constants/zoo_generators + ../constantine/math/constants/zoo_generators, + ../constantine/ethereum_verkle_primitives type EC* = ECP_TwEdwards_Prj[Fp[Banderwagon]] @@ -70,6 +78,11 @@ const bad_bit_string: array[16, string] = [ "0x120faa1df94d5d831bbb69fc44816e25afd27288a333299ac3c94518fd0e016f", ] +const expected_scalar_field_elements: array[2, string] = [ + "0x0e0c604381ef3cd11bdc84e8faa59b542fbbc92f800ed5767f21e5dbc59840ce", + "0x0a21f7dfa8ddaf6ef6f2044f13feec50cbb963996112fa1de4e3f52dbf6b7b6d" +] # test data generated from go-ipa implementation + # ############################################################ # # Banderwagon Serialization Tests @@ -207,3 +220,124 @@ suite "Banderwagon Points Tests": testTwoTorsion() +# ############################################################ +# +# Banderwagon Points Mapped to Scalar Field ( Fp -> Fr ) +# +# ############################################################ +suite "Banderwagon Elements Mapping": + + ## Tests if the mapping from Fp to Fr + ## is working as expected or not + test "Testing Map To Base Field": + proc testMultiMapToBaseField() = + var A, B, genPoint {.noInit.}: EC + genPoint.fromAffine(generator) + + A.sum(genPoint, genPoint) # A = g+g = 2g + B.double(genPoint) # B = 2g + B.double() # B = 2B = 4g + + var expected_a, expected_b: Fr[Banderwagon] + + # conver the points A & B which are in Fp + # to the their mapped Fr points + expected_a.mapToScalarField(A) + expected_b.mapToScalarField(B) + + doAssert expected_a.toHex() == expected_scalar_field_elements[0], "Mapping to Scalar Field Incorrect" + doAssert expected_b.toHex() == expected_scalar_field_elements[1], "Mapping to Scalar Field Incorrect" + + testMultiMapToBaseField() + +# ############################################################ +# +# Banderwagon Batch Operations +# +# ############################################################ +suite "Batch Operations on Banderwagon": + + ## Tests if the Batch Affine operations are + ## consistent with the signular affine operation + ## Using the concept of point double from generator point + ## we try to achive this + test "BatchAffine and fromAffine Consistency": + proc testbatch(n: static int) = + var g, temp {.noInit.}: EC + g.fromAffine(generator) # setting the generator point + + var aff{.noInit.}: ECP_TwEdwards_Aff[Fp[Banderwagon]] + aff = generator + + var points_prj: array[n, EC] + var points_aff: array[n, ECP_TwEdwards_Aff[Fp[Banderwagon]]] + + for i in 0 ..< n: + points_prj[i] = g + g.double() # doubling the point + + points_aff.batchAffine(points_prj) # performs the batch operation + + # checking correspondence with singular affine conversion + for i in 0 ..< n: + doAssert (points_aff[i] == aff).bool(), "batch inconsistent with singular ops" + temp.fromAffine(aff) + temp.double() + aff.affine(temp) + + testbatch(1000) + + ## Tests to check if the Motgomery Batch Inversion + ## Check if the Batch Inversion is consistent with + ## it's respective sigular inversion operation of field elements + test "Batch Inversion": + proc batchInvert(n: static int) = + var one, two: EC + var arr_fp: array[n, Fp[Banderwagon]] # array for Fp field elements + + one.fromAffine(generator) # setting the 1st generator point + two.fromAffine(generator) # setting the 2nd generator point + + for i in 0 ..< n: + arr_fp[i] = one.x + one.double() + + var arr_fp_inv: array[n, Fp[Banderwagon]] + doAssert arr_fp_inv.batchInvert(arr_fp) == true + + # Checking the correspondence with singular element inversion + for i in 0 ..< n: + var temp: Fp[Banderwagon] + temp.inv(two.x) + doAssert (arr_fp_inv[i] == temp).bool(), "Batch Inversion in consistent" + two.double() + + batchInvert(10) + + ## Tests to check if the Batch Map to Scalar Field + ## is consistent with it's respective singular operation + ## of mapping from Fp to Fr + ## Using the concept of point double from generator point + ## we try to achive this + test "Testing Batch Map to Base Field": + proc testBatchMapToBaseField() = + var A, B, g: EC + g.fromAffine(generator) + + A.sum(g, g) + B.double(g) + B.double() + + var expected_a, expected_b: Fr[Banderwagon] + expected_a.mapToScalarField(A) + expected_b.mapToScalarField(B) + + var ARes, BRes: Fr[Banderwagon] + var scalars: array[2, Fr[Banderwagon]] = [ARes, BRes] + var fps: array[2, EC] = [A, B] + + doAssert scalars.batchMapToScalarField(fps), "Batch Map to Scalar Failed" + doAssert (expected_a == scalars[0]).bool(), "expected scalar for point `A` is incorrect" + doAssert (expected_b == scalars[1]).bool(), "expected scalar for point `B` is incorrect" + + testBatchMapToBaseField() \ No newline at end of file