KZG followup - Batch verification (#272)

* KZG: add batch verification

* workaround Clang and empty {.goto.} branches

* Apply suggestions from code review
This commit is contained in:
Mamy Ratsimbazafy 2023-09-17 11:05:09 +02:00 committed by GitHub
parent 153b37b77f
commit 7b64f85a29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 424 additions and 109 deletions

View File

@ -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()

View File

@ -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)
let poly = allocHeapAligned(PolynomialEval[FIELD_ELEMENTS_PER_BLOB, matchingOrderBigInt(BLS12_381)], 64)
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)]₁
block HappyPath:
# Blob -> Polynomial
check HappyPath, poly.blob_to_field_polynomial(blob)
kzg_prove(
proof, y,
poly[], ctx.domain,
z, 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
y_bytes.marshal(y, bigEndian) # cannot fail
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
var challengeFr {.noInit.}: Fr[BLS12_381]
challengeFr.fiatShamirChallenge(blob[], commitment_bytes)
var challenge, eval_at_challenge {.noInit.}: matchingOrderBigInt(BLS12_381)
challenge.fromField(challengeFr)
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)
block HappyPath:
# Blob -> Polynomial
check HappyPath, poly.blob_to_field_polynomial(blob)
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])
# 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
freeHeapAligned(invRootsMinusZ)
freeHeapAligned(poly)
return result
let verif = kzg_verify(commitment.raw, challenge, eval_at_challenge, proof.raw, ctx.srs_monomial_g2.coefs[1])
if verif:
return cttEthKZG_Success
else:
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)
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)
return result
# Ethereum Trusted Setup
# ------------------------------------------------------------

View File

@ -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

View File

@ -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()