diff --git a/constantine/commitments/kzg_polynomial_commitments.nim b/constantine/commitments/kzg_polynomial_commitments.nim index af8dfbff..dfb6f536 100644 --- a/constantine/commitments/kzg_polynomial_commitments.nim +++ b/constantine/commitments/kzg_polynomial_commitments.nim @@ -9,7 +9,7 @@ import ../math/config/curves, ../math/[ec_shortweierstrass, arithmetic, extension_fields], - ../math/elliptic/[ec_scalar_mul, ec_multi_scalar_mul], + ../math/elliptic/[ec_multi_scalar_mul, ec_shortweierstrass_batch_ops], ../math/pairings/pairings_generic, ../math/constants/zoo_generators, ../math/polynomials/polynomials, @@ -306,4 +306,117 @@ func kzg_verify*[F2; C: static Curve]( var gt {.noInit.}: C.getGT() gt.pairing([proof, cmyG1], [tmzG2, negG2]) + return gt.isOne().bool() + +func kzg_verify_batch*[bits: static int, F2; C: static Curve]( + commitments: ptr UncheckedArray[ECP_ShortW_Aff[Fp[C], G1]], + challenges: ptr UncheckedArray[Fr[C]], + evals_at_challenges: ptr UncheckedArray[BigInt[bits]], + proofs: ptr UncheckedArray[ECP_ShortW_Aff[Fp[C], G1]], + linearIndepRandNumbers: ptr UncheckedArray[Fr[C]], + n: int, + tauG2: ECP_ShortW_Aff[F2, G2]): bool {.tags:[HeapAlloc, Alloca, Vartime].} = + ## Verify multiple KZG proofs efficiently + ## + ## Parameters + ## + ## `n` verification sets + ## A verification set i (commitmentᵢ, challengeᵢ, eval_at_challengeᵢ, proofᵢ) + ## is passed in a "struct-of-arrays" fashion. + ## + ## Notation: + ## i ∈ [0, n), a verification set with ID i + ## [a]₁ corresponds to the scalar multiplication [a]G by the generator G of the group 𝔾1 + ## + ## - `commitments`: `n` commitments [commitmentᵢ]₁ + ## - `challenges`: `n` challenges zᵢ + ## - `evals_at_challenges`: `n` evaluation yᵢ = pᵢ(zᵢ) + ## - `proofs`: `n` [proof]₁ + ## - `linearIndepRandNumbers`: `n` linearly independant numbers that are not in control + ## of a prover (potentially malicious). + ## - `n`: the number of verification sets + ## + ## For all (commitmentᵢ, challengeᵢ, eval_at_challengeᵢ, proofᵢ), + ## we verify the relation + ## proofᵢ.(τ - zᵢ) = pᵢ(τ)-pᵢ(zᵢ) + ## + ## As τ is the secret from the trusted setup, boxed in [τ]₁ and [τ]₂, + ## we rewrite the equality check using pairings + ## + ## e([proofᵢ]₁, [τ]₂ - [challengeᵢ]₂) . e([commitmentᵢ]₁ - [eval_at_challengeᵢ]₁, [-1]₂) = 1 + ## + ## Or batched using Feist-Khovratovich method + ## + ## e(∑ [rᵢ][proofᵢ]₁, [τ]₂) . e(∑[rᵢ]([commitmentᵢ]₁ - [eval_at_challengeᵢ]₁) + ∑[rᵢ][zᵢ][proofᵢ]₁, [-1]₂) = 1 + # + # Described in: + # - https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.1/specs/deneb/polynomial-commitments.md#verify_kzg_proof_batch + # - https://dankradfeist.de/ethereum/2021/06/18/pcs-multiproofs.html]\ + # - Fast amortized KZG proofs + # Feist, Khovratovich + # https://eprint.iacr.org/2023/033 + # - https://alinush.github.io/2021/06/17/Feist-Khovratovich-technique-for-computing-KZG-proofs-fast.html + + static: doAssert BigInt[bits] is matchingOrderBigInt(C) + + var sums_jac {.noInit.}: array[2, ECP_ShortW_Jac[Fp[C], G1]] + template sum_rand_proofs: untyped = sums_jac[0] + template sum_commit_minus_evals_G1: untyped = sums_jac[1] + var sum_rand_challenge_proofs {.noInit.}: ECP_ShortW_Jac[Fp[C], G1] + + # ∑ [rᵢ][proofᵢ]₁ + # --------------- + let coefs = allocHeapArrayAligned(matchingOrderBigInt(C), n, alignment = 64) + for i in 0 ..< n: + coefs[i].fromField(linearIndepRandNumbers[i]) + + sum_rand_proofs.multiScalarMul_vartime(coefs, proofs, n) + + # ∑[rᵢ]([commitmentᵢ]₁ - [eval_at_challengeᵢ]₁) + # --------------------------------------------- + # + # We interleave allocation and deallocation, which hurts cache reuse + # i.e. when alloc is being done, it's better to do all allocs as the metadata will already be in cache + # + # but it's more important to minimize memory usage especially if we want to commit with 2^26+ points + # + # We dealloc in reverse alloc order, to avoid leaving holes in the allocator pages. + let commits_min_evals = allocHeapArrayAligned(ECP_ShortW_Aff[Fp[C], G1], n, alignment = 64) + let commits_min_evals_jac = allocHeapArrayAligned(ECP_ShortW_Jac[Fp[C], G1], n, alignment = 64) + + for i in 0 ..< n: + commits_min_evals_jac[i].fromAffine(commitments[i]) + var boxed_eval {.noInit.}: ECP_ShortW_Jac[Fp[C], G1] + boxed_eval.fromAffine(C.getGenerator("G1")) + boxed_eval.scalarMul_vartime(evals_at_challenges[i]) + commits_min_evals_jac[i].diff_vartime(commits_min_evals_jac[i], boxed_eval) + + commits_min_evals.batchAffine(commits_min_evals_jac, n) + freeHeapAligned(commits_min_evals_jac) + sum_commit_minus_evals_G1.multiScalarMul_vartime(coefs, commits_min_evals, n) + freeHeapAligned(commits_min_evals) + + # ∑[rᵢ][zᵢ][proofᵢ]₁ + var tmp {.noInit.}: Fr[C] + for i in 0 ..< n: + tmp.prod(linearIndepRandNumbers[i], challenges[i]) + coefs[i].fromField(tmp) + + sum_rand_challenge_proofs.multiScalarMul_vartime(coefs, proofs, n) + freeHeapAligned(coefs) + + # e(∑ [rᵢ][proofᵢ]₁, [τ]₂) . e(∑[rᵢ]([commitmentᵢ]₁ - [eval_at_challengeᵢ]₁) + ∑[rᵢ][zᵢ][proofᵢ]₁, [-1]₂) = 1 + template sum_of_sums: untyped = sums_jac[1] + + sum_of_sums.sum_vartime(sum_commit_minus_evals_G1, sum_rand_challenge_proofs) + + var sums {.noInit.}: array[2, ECP_ShortW_Aff[Fp[C], G1]] + sums.batchAffine(sums_jac) + + var negG2 {.noInit.}: ECP_ShortW_Aff[F2, G2] + negG2.neg(C.getGenerator("G2")) + + var gt {.noInit.}: C.getGT() + gt.pairing(sums, [tauG2, negG2]) + return gt.isOne().bool() \ No newline at end of file diff --git a/constantine/ethereum_eip4844_kzg_polynomial_commitments.nim b/constantine/ethereum_eip4844_kzg_polynomial_commitments.nim index 0e7f0733..831f5c65 100644 --- a/constantine/ethereum_eip4844_kzg_polynomial_commitments.nim +++ b/constantine/ethereum_eip4844_kzg_polynomial_commitments.nim @@ -7,6 +7,8 @@ # at your option. This file may not be copied, modified, or distributed except according to those terms. import + std/typetraits, + ./math/config/curves, ./math/io/[io_bigints, io_fields], ./math/[ec_shortweierstrass, arithmetic, extension_fields], @@ -15,7 +17,7 @@ import ./math/polynomials/polynomials, ./commitments/kzg_polynomial_commitments, ./hashes, - ./platforms/[abstractions, views, allocs], + ./platforms/[abstractions, allocs], ./serialization/[codecs_bls12_381, endians], ./trusted_setups/ethereum_kzg_srs @@ -67,11 +69,9 @@ const BYTES_PER_BLOB = BYTES_PER_FIELD_ELEMENT*FIELD_ELEMENTS_PER_BLOB type Blob* = array[BYTES_PER_BLOB, byte] - KZGCommitment* = object - raw: ECP_ShortW_Aff[Fp[BLS12_381], G1] + KZGCommitment* = distinct ECP_ShortW_Aff[Fp[BLS12_381], G1] - KZGProof* = object - raw: ECP_ShortW_Aff[Fp[BLS12_381], G1] + KZGProof* = distinct ECP_ShortW_Aff[Fp[BLS12_381], G1] CttEthKzgStatus* = enum cttEthKZG_Success @@ -121,21 +121,20 @@ func fiatShamirChallenge(dst: var Fr[BLS12_381], blob: Blob, commitmentBytes: ar transcript.finish(challenge) dst.fromDigest(challenge) -func computePowers(dst: MutableView[Fr[BLS12_381]], base: Fr[BLS12_381]) = +func computePowers(dst: ptr UncheckedArray[Fr[BLS12_381]], len: int, base: Fr[BLS12_381]) = ## We need linearly independent random numbers ## for batch proof sampling. ## Powers are linearly independent. ## It's also likely faster than calling a fast RNG + modular reduction ## to be in 0 < number < curve_order ## since modular reduction needs modular multiplication anyway. - let N = dst.len + let N = len if N >= 1: dst[0].setOne() if N >= 2: dst[1] = base - if N >= 3: - for i in 2 ..< N: - dst[i].prod(dst[i-1], base) + for i in 2 ..< N: + dst[i].prod(dst[i-1], base) # Conversion # ------------------------------------------------------------ @@ -160,7 +159,7 @@ func bytes_to_bls_field(dst: var Fr[BLS12_381], src: array[32, byte]): CttCodecS func bytes_to_kzg_commitment(dst: var KZGCommitment, src: array[48, byte]): CttCodecEccStatus = ## Convert untrusted bytes into a trusted and validated KZGCommitment. - let status = dst.raw.deserialize_g1_compressed(src) + let status = dst.distinctBase().deserialize_g1_compressed(src) if status == cttCodecEcc_PointAtInfinity: # Point at infinity is allowed return cttCodecEcc_Success @@ -168,7 +167,7 @@ func bytes_to_kzg_commitment(dst: var KZGCommitment, src: array[48, byte]): CttC func bytes_to_kzg_proof(dst: var KZGProof, src: array[48, byte]): CttCodecEccStatus = ## Convert untrusted bytes into a trusted and validated KZGProof. - let status = dst.raw.deserialize_g1_compressed(src) + let status = dst.distinctBase().deserialize_g1_compressed(src) if status == cttCodecEcc_PointAtInfinity: # Point at infinity is allowed return cttCodecEcc_Success @@ -212,8 +211,13 @@ func blob_to_field_polynomial( # Ethereum KZG public API # ------------------------------------------------------------ +# +# We use a simple goto state machine to handle errors and cleanup (if allocs were done) +# and have 2 different checks: +# - Either we are in "HappyPath" section that shortcuts to resource cleanup on error +# - or there are no resources to clean and we can early return from a function. -template check(evalExpr: CttCodecScalarStatus): untyped = +template check(evalExpr: CttCodecScalarStatus): untyped {.dirty.} = # Translate codec status code to KZG status code # Beware of resource cleanup like heap allocation, this can early exit the caller. block: @@ -223,7 +227,7 @@ template check(evalExpr: CttCodecScalarStatus): untyped = of cttCodecScalar_Zero: discard of cttCodecScalar_ScalarLargerThanCurveOrder: return cttEthKZG_ScalarLargerThanCurveOrder -template check(evalExpr: CttCodecEccStatus): untyped = +template check(evalExpr: CttCodecEccStatus): untyped {.dirty.} = # Translate codec status code to KZG status code # Beware of resource cleanup like heap allocation, this can early exit the caller. block: @@ -236,6 +240,29 @@ template check(evalExpr: CttCodecEccStatus): untyped = of cttCodecEcc_PointNotInSubgroup: return cttEthKZG_EccPointNotInSubGroup of cttCodecEcc_PointAtInfinity: discard +template check(Section: untyped, evalExpr: CttCodecScalarStatus): untyped {.dirty.} = + # Translate codec status code to KZG status code + # Exit current code block + block: + let status = evalExpr # Ensure single evaluation + case status + of cttCodecScalar_Success: discard + of cttCodecScalar_Zero: discard + of cttCodecScalar_ScalarLargerThanCurveOrder: result = cttEthKZG_EccPointNotInSubGroup; break Section + +template check(Section: untyped, evalExpr: CttCodecEccStatus): untyped {.dirty.} = + # Translate codec status code to KZG status code + # Exit current code block + block: + let status = evalExpr # Ensure single evaluation + case status + of cttCodecEcc_Success: discard + of cttCodecEcc_InvalidEncoding: result = cttEthKZG_EccInvalidEncoding; break Section + of cttCodecEcc_CoordinateGreaterThanOrEqualModulus: result = cttEthKZG_EccCoordinateGreaterThanOrEqualModulus; break Section + of cttCodecEcc_PointNotOnCurve: result = cttEthKZG_EccPointNotOnCurve; break Section + of cttCodecEcc_PointNotInSubgroup: result = cttEthKZG_EccPointNotInSubGroup; break Section + of cttCodecEcc_PointAtInfinity: discard + func blob_to_kzg_commitment*( ctx: ptr EthereumKZGContext, dst: var array[48, byte], @@ -256,22 +283,20 @@ func blob_to_kzg_commitment*( ## - and at the verification challenge z. ## ## with proof = [(p(τ) - p(z)) / (τ-z)]₁ + let poly = allocHeapAligned(PolynomialEval[FIELD_ELEMENTS_PER_BLOB, matchingOrderBigInt(BLS12_381)], 64) - let status = poly.blob_to_bigint_polynomial(blob) - if status == cttCodecScalar_ScalarLargerThanCurveOrder: - freeHeapAligned(poly) - return cttEthKZG_ScalarLargerThanCurveOrder - elif status != cttCodecScalar_Success: - debugEcho "Unreachable status in blob_to_kzg_commitment: ", status - debugEcho "Panicking ..." - quit 1 - - var r {.noinit.}: ECP_ShortW_Aff[Fp[BLS12_381], G1] - kzg_commit(r, poly.evals, ctx.srs_lagrange_g1) # symbol resolution need explicit generics - discard dst.serialize_g1_compressed(r) + + block HappyPath: + check HappyPath, poly.blob_to_bigint_polynomial(blob) + + var r {.noinit.}: ECP_ShortW_Aff[Fp[BLS12_381], G1] + kzg_commit(r, poly.evals, ctx.srs_lagrange_g1) + discard dst.serialize_g1_compressed(r) + + result = cttEthKZG_Success freeHeapAligned(poly) - return cttEthKZG_Success + return result func compute_kzg_proof*( ctx: ptr EthereumKZGContext, @@ -295,36 +320,30 @@ func compute_kzg_proof*( # Random or Fiat-Shamir challenge var z {.noInit.}: Fr[BLS12_381] - var status = bytes_to_bls_field(z, z_bytes) - if status != cttCodecScalar_Success: - # cttCodecScalar_Zero is not possible - return cttEthKZG_ScalarLargerThanCurveOrder + check z.bytes_to_bls_field(z_bytes) - # Blob -> Polynomial let poly = allocHeapAligned(PolynomialEval[FIELD_ELEMENTS_PER_BLOB, Fr[BLS12_381]], 64) - status = poly.blob_to_field_polynomial(blob) - if status == cttCodecScalar_ScalarLargerThanCurveOrder: - freeHeapAligned(poly) - return cttEthKZG_ScalarLargerThanCurveOrder - elif status != cttCodecScalar_Success: - debugEcho "Unreachable status in compute_kzg_proof: ", status - debugEcho "Panicking ..." - quit 1 - - var y {.noInit.}: Fr[BLS12_381] # y = p(z), eval at challenge z - var proof {.noInit.}: ECP_ShortW_Aff[Fp[BLS12_381], G1] # [proof]₁ = [(p(τ) - p(z)) / (τ-z)]₁ - - kzg_prove( - proof, y, - poly[], ctx.domain, - z, ctx.srs_lagrange_g1, - isBitReversedDomain = true) - - discard proof_bytes.serialize_g1_compressed(proof) # cannot fail - y_bytes.marshal(y, bigEndian) # cannot fail + + block HappyPath: + # Blob -> Polynomial + check HappyPath, poly.blob_to_field_polynomial(blob) + + # KZG Prove + var y {.noInit.}: Fr[BLS12_381] # y = p(z), eval at challenge z + var proof {.noInit.}: ECP_ShortW_Aff[Fp[BLS12_381], G1] # [proof]₁ = [(p(τ) - p(z)) / (τ-z)]₁ + + kzg_prove( + proof, y, + poly[], ctx.domain, + z, ctx.srs_lagrange_g1, + isBitReversedDomain = true) + + discard proof_bytes.serialize_g1_compressed(proof) # cannot fail + y_bytes.marshal(y, bigEndian) # cannot fail + result = cttEthKZG_Success freeHeapAligned(poly) - return cttEthKZG_Success + return result func verify_kzg_proof*( ctx: ptr EthereumKZGContext, @@ -346,7 +365,10 @@ func verify_kzg_proof*( var proof {.noInit.}: KZGProof check proof.bytes_to_kzg_proof(proof_bytes) - let verif = kzg_verify(commitment.raw, challenge, eval_at_challenge, proof.raw, ctx.srs_monomial_g2.coefs[1]) + let verif = kzg_verify(ECP_ShortW_Aff[Fp[BLS12_381], G1](commitment), + challenge, eval_at_challenge, + ECP_ShortW_Aff[Fp[BLS12_381], G1](proof), + ctx.srs_monomial_g2.coefs[1]) if verif: return cttEthKZG_Success else: @@ -365,31 +387,31 @@ func compute_blob_kzg_proof*( # Blob -> Polynomial let poly = allocHeapAligned(PolynomialEval[FIELD_ELEMENTS_PER_BLOB, Fr[BLS12_381]], 64) - var status = poly.blob_to_field_polynomial(blob) - if status == cttCodecScalar_ScalarLargerThanCurveOrder: - freeHeapAligned(poly) - return cttEthKZG_ScalarLargerThanCurveOrder - elif status != cttCodecScalar_Success: - debugEcho "Unreachable status in compute_kzg_proof: ", status - debugEcho "Panicking ..." - quit 1 - var challenge {.noInit.}: Fr[BLS12_381] - challenge.fiatShamirChallenge(blob[], commitment_bytes) + block HappyPath: + # Blob -> Polynomial + check HappyPath, poly.blob_to_field_polynomial(blob) - var y {.noInit.}: Fr[BLS12_381] # y = p(z), eval at challenge z - var proof {.noInit.}: ECP_ShortW_Aff[Fp[BLS12_381], G1] # [proof]₁ = [(p(τ) - p(z)) / (τ-z)]₁ + # Fiat-Shamir challenge + var challenge {.noInit.}: Fr[BLS12_381] + challenge.fiatShamirChallenge(blob[], commitment_bytes) - kzg_prove( - proof, y, - poly[], ctx.domain, - challenge, ctx.srs_lagrange_g1, - isBitReversedDomain = true) + # KZG Prove + var y {.noInit.}: Fr[BLS12_381] # y = p(z), eval at challenge z + var proof {.noInit.}: ECP_ShortW_Aff[Fp[BLS12_381], G1] # [proof]₁ = [(p(τ) - p(z)) / (τ-z)]₁ - discard proof_bytes.serialize_g1_compressed(proof) # cannot fail + kzg_prove( + proof, y, + poly[], ctx.domain, + challenge, ctx.srs_lagrange_g1, + isBitReversedDomain = true) + + discard proof_bytes.serialize_g1_compressed(proof) # cannot fail + + result = cttEthKZG_Success freeHeapAligned(poly) - return cttEthKZG_Success + return result func verify_blob_kzg_proof*( ctx: ptr EthereumKZGContext, @@ -404,49 +426,156 @@ func verify_blob_kzg_proof*( var proof {.noInit.}: KZGProof check proof.bytes_to_kzg_proof(proof_bytes) - # Blob -> Polynomial let poly = allocHeapAligned(PolynomialEval[FIELD_ELEMENTS_PER_BLOB, Fr[BLS12_381]], 64) - var status = poly.blob_to_field_polynomial(blob) - if status == cttCodecScalar_ScalarLargerThanCurveOrder: - freeHeapAligned(poly) - return cttEthKZG_ScalarLargerThanCurveOrder - elif status != cttCodecScalar_Success: - debugEcho "Unreachable status in compute_kzg_proof: ", status - debugEcho "Panicking ..." - quit 1 + let invRootsMinusZ = allocHeapAligned(array[FIELD_ELEMENTS_PER_BLOB, Fr[BLS12_381]], alignment = 64) - var challengeFr {.noInit.}: Fr[BLS12_381] - challengeFr.fiatShamirChallenge(blob[], commitment_bytes) + block HappyPath: + # Blob -> Polynomial + check HappyPath, poly.blob_to_field_polynomial(blob) + + # Fiat-Shamir challenge + var challengeFr {.noInit.}: Fr[BLS12_381] + challengeFr.fiatShamirChallenge(blob[], commitment_bytes) + + var challenge, eval_at_challenge {.noInit.}: matchingOrderBigInt(BLS12_381) + challenge.fromField(challengeFr) + + # Lagrange Polynomial evaluation + # ------------------------------ + # 1. Compute 1/(ωⁱ - z) with ω a root of unity, i in [0, N). + # zIndex = i if ωⁱ - z == 0 (it is the i-th root of unity) and -1 otherwise. + let zIndex = invRootsMinusZ[].inverseRootsMinusZ_vartime( + ctx.domain, challengeFr, + earlyReturnOnZero = true) + + # 2. Actual evaluation + if zIndex == -1: + var eval_at_challenge_fr{.noInit.}: Fr[BLS12_381] + eval_at_challenge_fr.evalPolyAt( + poly[], challengeFr, + invRootsMinusZ[], + ctx.domain) + eval_at_challenge.fromField(eval_at_challenge_fr) + else: + eval_at_challenge.fromField(poly.evals[zIndex]) + + # KZG verification + let verif = kzg_verify(ECP_ShortW_Aff[Fp[BLS12_381], G1](commitment), + challenge, eval_at_challenge, + ECP_ShortW_Aff[Fp[BLS12_381], G1](proof), + ctx.srs_monomial_g2.coefs[1]) + if verif: + result = cttEthKZG_Success + else: + result = cttEthKZG_VerificationFailure - var challenge, eval_at_challenge {.noInit.}: matchingOrderBigInt(BLS12_381) - challenge.fromField(challengeFr) + freeHeapAligned(invRootsMinusZ) + freeHeapAligned(poly) + return result +func verify_blob_kzg_proof_batch*( + ctx: ptr EthereumKZGContext, + blobs: ptr UncheckedArray[Blob], + commitments_bytes: ptr UncheckedArray[array[48, byte]], + proof_bytes: ptr UncheckedArray[array[48, byte]], + n: int, + secureRandomBytes: array[32, byte]): CttEthKzgStatus {.tags:[Alloca, HeapAlloc, Vartime].} = + ## Verify `n` (blob, commitment, proof) sets efficiently + ## + ## `n` is the number of verifications set + ## - if n is negative, this procedure returns verification failure + ## - if n is zero, this procedure returns verification success + ## + ## `secureRandomBytes` random byte must come from a cryptographically secure RNG + ## or computed through the Fiat-Shamir heuristic. + ## It serves as a random number + ## that is not in the control of a potential attacker to prevent potential + ## rogue commitments attacks due to homomorphic properties of pairings, + ## i.e. commitments that are linear combination of others and sum would be zero. + + if n < 0: + return cttEthKZG_VerificationFailure + if n == 0: + return cttEthKZG_Success + + let commitments = allocHeapArrayAligned(KZGCommitment, n, alignment = 64) + let challenges = allocHeapArrayAligned(Fr[BLS12_381], n, alignment = 64) + let evals_at_challenges = allocHeapArrayAligned(matchingOrderBigInt(BLS12_381), n, alignment = 64) + let proofs = allocHeapArrayAligned(KZGProof, n, alignment = 64) + + let poly = allocHeapAligned(PolynomialEval[FIELD_ELEMENTS_PER_BLOB, Fr[BLS12_381]], alignment = 64) let invRootsMinusZ = allocHeapAligned(array[FIELD_ELEMENTS_PER_BLOB, Fr[BLS12_381]], alignment = 64) - # Compute 1/(ωⁱ - z) with ω a root of unity, i in [0, N). - # zIndex = i if ωⁱ - z == 0 (it is the i-th root of unity) and -1 otherwise. - let zIndex = invRootsMinusZ[].inverseRootsMinusZ_vartime( - ctx.domain, challengeFr, - earlyReturnOnZero = true) - - if zIndex == -1: - var eval_at_challenge_fr{.noInit.}: Fr[BLS12_381] - eval_at_challenge_fr.evalPolyAt( - poly[], challengeFr, - invRootsMinusZ[], - ctx.domain) - eval_at_challenge.fromField(eval_at_challenge_fr) - else: - eval_at_challenge.fromField(poly.evals[zIndex]) + block HappyPath: + for i in 0 ..< n: + check HappyPath, commitments[i].bytes_to_kzg_commitment(commitments_bytes[i]) + check HappyPath, poly.blob_to_field_polynomial(blobs[i].addr) + challenges[i].fiatShamirChallenge(blobs[i], commitments_bytes[i]) + + # Lagrange Polynomial evaluation + # ------------------------------ + # 1. Compute 1/(ωⁱ - z) with ω a root of unity, i in [0, N). + # zIndex = i if ωⁱ - z == 0 (it is the i-th root of unity) and -1 otherwise. + let zIndex = invRootsMinusZ[].inverseRootsMinusZ_vartime( + ctx.domain, challenges[i], + earlyReturnOnZero = true) + # 2. Actual evaluation + if zIndex == -1: + var eval_at_challenge_fr{.noInit.}: Fr[BLS12_381] + eval_at_challenge_fr.evalPolyAt( + poly[], challenges[i], + invRootsMinusZ[], + ctx.domain) + evals_at_challenges[i].fromField(eval_at_challenge_fr) + else: + evals_at_challenges[i].fromField(poly.evals[zIndex]) + + check HappyPath, proofs[i].bytes_to_kzg_proof(proof_bytes[i]) + + var randomBlindingFr {.noInit.}: Fr[BLS12_381] + block blinding: # Ensure we don't multiply by 0 for blinding + # 1. Try with the random number supplied + for i in 0 ..< secureRandomBytes.len: + if secureRandomBytes[i] != byte 0: + randomBlindingFr.fromDigest(secureRandomBytes) + break blinding + # 2. If it's 0 (how?!), we just hash all the Fiat-Shamir challenges + var transcript: sha256 + transcript.init() + transcript.update(RANDOM_CHALLENGE_KZG_BATCH_DOMAIN) + transcript.update(cast[ptr UncheckedArray[byte]](challenges).toOpenArray(0, n*sizeof(Fr[BLS12_381])-1)) + + var blindingBytes {.noInit.}: array[32, byte] + transcript.finish(blindingBytes) + randomBlindingFr.fromDigest(blindingBytes) + + let linearIndepRandNumbers = allocHeapArrayAligned(Fr[BLS12_381], n, alignment = 64) + linearIndepRandNumbers.computePowers(n, randomBlindingFr) + + type EcAffArray = ptr UncheckedArray[ECP_ShortW_Aff[Fp[BLS12_381], G1]] + let verif = kzg_verify_batch( + cast[EcAffArray](commitments), + challenges, + evals_at_challenges, + cast[EcAffArray](proofs), + linearIndepRandNumbers, + n, + ctx.srs_monomial_g2.coefs[1]) + if verif: + result = cttEthKZG_Success + else: + result = cttEthKZG_VerificationFailure + + freeHeapAligned(linearIndepRandNumbers) freeHeapAligned(invRootsMinusZ) freeHeapAligned(poly) + freeHeapAligned(proofs) + freeHeapAligned(evals_at_challenges) + freeHeapAligned(challenges) + freeHeapAligned(commitments) - let verif = kzg_verify(commitment.raw, challenge, eval_at_challenge, proof.raw, ctx.srs_monomial_g2.coefs[1]) - if verif: - return cttEthKZG_Success - else: - return cttEthKZG_VerificationFailure + return result # Ethereum Trusted Setup # ------------------------------------------------------------ diff --git a/constantine/math/elliptic/ec_multi_scalar_mul.nim b/constantine/math/elliptic/ec_multi_scalar_mul.nim index ddda79a3..a9a5a58b 100644 --- a/constantine/math/elliptic/ec_multi_scalar_mul.nim +++ b/constantine/math/elliptic/ec_multi_scalar_mul.nim @@ -446,9 +446,9 @@ func multiScalarMul_vartime*[bits: static int, F, G]( r: var ECP_ShortW[F, G], coefs: ptr UncheckedArray[BigInt[bits]], points: ptr UncheckedArray[ECP_ShortW_Aff[F, G]], - N: int) {.tags:[VarTime, Alloca, HeapAlloc], meter.} = + len: int) {.tags:[VarTime, Alloca, HeapAlloc], meter.} = ## Multiscalar multiplication: - ## r <- [a₀]P₀ + [a₁]P₁ + ... + [aₙ]Pₙ + ## r <- [a₀]P₀ + [a₁]P₁ + ... + [aₙ₋₁]Pₙ₋₁ multiScalarMul_dispatch_vartime(r, coefs, points, len) @@ -457,7 +457,7 @@ func multiScalarMul_vartime*[bits: static int, F, G]( coefs: openArray[BigInt[bits]], points: openArray[ECP_ShortW_Aff[F, G]]) {.tags:[VarTime, Alloca, HeapAlloc], meter.} = ## Multiscalar multiplication: - ## r <- [a₀]P₀ + [a₁]P₁ + ... + [aₙ]Pₙ + ## r <- [a₀]P₀ + [a₁]P₁ + ... + [aₙ₋₁]Pₙ₋₁ debug: doAssert coefs.len == points.len let N = points.len diff --git a/tests/t_ethereum_eip4844_deneb_kzg.nim b/tests/t_ethereum_eip4844_deneb_kzg.nim index 72a7561f..49dd0198 100644 --- a/tests/t_ethereum_eip4844_deneb_kzg.nim +++ b/tests/t_ethereum_eip4844_deneb_kzg.nim @@ -12,6 +12,7 @@ import # 3rd party pkg/yaml, # Internals + ../constantine/hashes, ../constantine/serialization/codecs, ../constantine/ethereum_eip4844_kzg_polynomial_commitments @@ -87,6 +88,35 @@ template parseAssign(dstVariable: untyped, size: static int, hexInput: string) = var dstVariable{.inject.} = new(array[size, byte]) dstVariable[].fromHex(hexInput) +template parseAssignList(dstVariable: untyped, elemSize: static int, hexListInput: YamlNode) = + + var dstVariable{.inject.} = newSeq[array[elemSize, byte]]() + + block exitHappyPath: + block exitException: + for elem in hexListInput: + let hexInput = elem.content + + let prefixBytes = 2*int(hexInput.startsWith("0x")) + let expectedLength = elemSize*2 + prefixBytes + if hexInput.len != expectedLength: + let encodedBytes = (hexInput.len - prefixBytes) div 2 + stdout.write "[ Incorrect input length for '" & + astToStr(dstVariable) & + "': encoding " & $encodedBytes & " bytes" & + " instead of expected " & $elemSize & " ]\n" + + doAssert testVector["output"].content == "null" + break exitException + else: + dstVariable.setLen(dstVariable.len + 1) + dstVariable[^1].fromHex(hexInput) + + break exitHappyPath + + # We're in a template, this shortcuts the caller `walkTests` + continue + testGen(blob_to_kzg_commitment, testVector): parseAssign(blob, 32*4096, testVector["input"]["blob"].content) @@ -175,6 +205,46 @@ testGen(verify_blob_kzg_proof, testVector): else: doAssert testVector["output"].content == "null" +testGen(verify_blob_kzg_proof_batch, testVector): + parseAssignList(blobs, 32*4096, testVector["input"]["blobs"]) + parseAssignList(commitments, 48, testVector["input"]["commitments"]) + parseAssignList(proofs, 48, testVector["input"]["proofs"]) + + if blobs.len != commitments.len: + stdout.write "[ Length mismatch between blobs and commitments ]\n" + doAssert testVector["output"].content == "null" + continue + if blobs.len != proofs.len: + stdout.write "[ Length mismatch between blobs and proofs ]\n" + doAssert testVector["output"].content == "null" + continue + + # For reproducibility/debugging we don't use the CSPRNG here + var randomBlinding {.noInit.}: array[32, byte] + sha256.hash(randomBlinding, "The wizard quickly jinxed the gnomes before they vaporized.") + + template asUnchecked[T](a: openArray[T]): ptr UncheckedArray[T] = + if a.len > 0: + cast[ptr UncheckedArray[T]](a[0].unsafeAddr) + else: + nil + + let status = verify_blob_kzg_proof_batch( + ctx, + blobs.asUnchecked(), + commitments.asUnchecked(), + proofs.asUnchecked(), + blobs.len, + randomBlinding) + stdout.write "[" & $status & "]\n" + + if status == cttEthKZG_Success: + doAssert testVector["output"].content == "true" + elif status == cttEthKZG_VerificationFailure: + doAssert testVector["output"].content == "false" + else: + doAssert testVector["output"].content == "null" + block: suite "Ethereum Deneb Hardfork / EIP-4844 / Proto-Danksharding / KZG Polynomial Commitments": let ctx = load_ethereum_kzg_test_trusted_setup_mainnet() @@ -191,7 +261,10 @@ block: test "compute_blob_kzg_proof(proof: var array[48, byte], blob: ptr array[4096, byte], commitment: array[48, byte])": ctx.test_compute_blob_kzg_proof() - test "verify_blob_kzg_proof(blob: ptr array[4096, byte], commitment: array[48, byte], proof: var array[48, byte])": + test "verify_blob_kzg_proof(blob: ptr array[4096, byte], commitment, proof: array[48, byte])": ctx.test_verify_blob_kzg_proof() + test "verify_blob_kzg_proof_batch(blobs: ptr UncheckedArray[array[4096, byte]], commitments, proofs: ptr UncheckedArray[array[48, byte]], n: int, secureRandomBytes: array[32, byte])": + ctx.test_verify_blob_kzg_proof_batch() + ctx.delete()