mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
vrf: an algorithm from one RFC 9381 suite, labelled with another's byte
The package documented ECVRF-EDWARDS25519-SHA512-ELL2 and sent suite byte 0x04, while the code ran try-and-increment — which is TAI, suite 0x03. The suite byte is hashed into both the encode-to-curve and challenge digests, so the combination matched no conforming prover and could not verify a proof from one. Aligned on TAI throughout, with the RFC's front/back domain separators. Verification also now runs with validate_key = TRUE (Section 5.4.5). The public key arrives in calldata, so it is attacker-chosen, and that check is what buys the collision resistance and unpredictability-under-malicious-key-generation an on-chain lottery is paying for (Sections 7.1.1, 7.1.3). Point decoding refuses non-canonical encodings. Gas follows the work: encode_to_curve hashes the whole alpha string once per counter, so the fee carries a per-word term, and a call that does no work is charged rather than free.
This commit is contained in:
+193
-128
@@ -1,17 +1,24 @@
|
||||
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Package vrf implements an EVM precompile for ECVRF-EDWARDS25519-SHA512-ELL2
|
||||
// Package vrf implements an EVM precompile for ECVRF-EDWARDS25519-SHA512-TAI
|
||||
// verification per RFC 9381. This enables on-chain verifiable randomness for
|
||||
// lotteries, leader election, NFT minting, and privacy-preserving protocols.
|
||||
//
|
||||
// Only verification and proof-to-hash extraction are exposed. Proving requires
|
||||
// the secret key and happens off-chain.
|
||||
//
|
||||
// The public key is supplied by the caller and is therefore never trusted:
|
||||
// ECVRF_verify runs with validate_key = TRUE (RFC 9381, Section 5.4.5), which
|
||||
// is what buys "full collision resistance" and "unpredictability under
|
||||
// malicious key generation" (RFC 9381, Sections 7.1.1 and 7.1.3) -- the two
|
||||
// properties an on-chain lottery is actually paying for.
|
||||
//
|
||||
// Precompile address: 0x0000000000000000000000000000000000003213 (LP-3213)
|
||||
package vrf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha512"
|
||||
"errors"
|
||||
|
||||
@@ -29,29 +36,56 @@ const (
|
||||
// OpProofToHash extracts beta_string from a proof without verification.
|
||||
OpProofToHash = 0x02
|
||||
|
||||
// Gas costs.
|
||||
GasVerify = 20_000 // One scalar mul + two hash-to-curve ops
|
||||
GasProofToHash = 1_000 // One SHA-512
|
||||
// MinCallGas is charged for any call that performs no work -- empty input
|
||||
// or an unrecognised selector -- so that no call is ever free.
|
||||
MinCallGas = 21_000
|
||||
|
||||
// Sizes per RFC 9381 section 5.5 (ECVRF-EDWARDS25519-SHA512-ELL2).
|
||||
// GasVerify covers the size-independent work: one double-scalar base
|
||||
// multiplication and two scalar multiplications.
|
||||
GasVerify = 20_000
|
||||
|
||||
// GasVerifyPerWord prices the size-dependent work. ECVRF_encode_to_curve
|
||||
// hashes the whole alpha string once per counter, and the counter loop
|
||||
// advances only when a candidate fails to decode -- probability 1/2 each --
|
||||
// so forcing n passes over alpha costs an offline search of 2^n. This is
|
||||
// eight times the EVM's own SHA3 word price (6), which covers a grind of
|
||||
// that scale given SHA-512's throughput advantage over Keccak.
|
||||
GasVerifyPerWord = 48
|
||||
|
||||
// GasProofToHash covers one point decompression and one SHA-512 of a
|
||||
// fixed-size input.
|
||||
GasProofToHash = 1_000
|
||||
|
||||
// Sizes per RFC 9381 Section 5.5 (ECVRF-EDWARDS25519-SHA512-TAI).
|
||||
PublicKeySize = 32 // Compressed Edwards Y coordinate
|
||||
ProofSize = 80 // Gamma(32) + c(16) + s(32)
|
||||
BetaSize = 64 // SHA-512 output
|
||||
|
||||
// RFC 9381 suite string for ECVRF-EDWARDS25519-SHA512-ELL2.
|
||||
suiteString = 0x04
|
||||
// AlphaLenSize is the width of the big-endian alpha_string length prefix.
|
||||
AlphaLenSize = 2
|
||||
|
||||
// Domain separation prefixes (RFC 9381 section 5.4.1.2).
|
||||
dsHashToTryCAndIncrement = 0x01
|
||||
dsHashPoints = 0x02
|
||||
dsProofToHash = 0x03
|
||||
// RFC 9381 suite string for ECVRF-EDWARDS25519-SHA512-TAI.
|
||||
suiteString = 0x03
|
||||
|
||||
// Domain separators (RFC 9381 Sections 5.4.1.1, 5.4.3 and 5.2). Each
|
||||
// hashed string opens with suite_string || front and closes with back.
|
||||
dsEncodeToCurve = 0x01
|
||||
dsChallenge = 0x02
|
||||
dsProofToHash = 0x03
|
||||
dsBack = 0x00
|
||||
|
||||
// maxCounter bounds the encode_to_curve counter to the one octet RFC 9381
|
||||
// gives it.
|
||||
maxCounter = 256
|
||||
)
|
||||
|
||||
// ─── Errors ───────────────────────────────────────────────────────────────────
|
||||
|
||||
var (
|
||||
ErrInvalidInput = contract.ErrInvalidInput
|
||||
ErrInvalidOperation = errors.New("vrf: invalid operation selector")
|
||||
ErrInvalidOperation = contract.ErrInvalidOp
|
||||
|
||||
errNonCanonicalPoint = errors.New("vrf: non-canonical point encoding")
|
||||
)
|
||||
|
||||
// ─── Addresses ────────────────────────────────────────────────────────────────
|
||||
@@ -70,18 +104,26 @@ var (
|
||||
|
||||
type vrfPrecompile struct{}
|
||||
|
||||
// words returns the number of 32-byte words spanned by n bytes.
|
||||
func words(n int) uint64 {
|
||||
return (uint64(n) + 31) / 32
|
||||
}
|
||||
|
||||
// RequiredGas returns the gas cost for the given input.
|
||||
func (p *vrfPrecompile) RequiredGas(input []byte) uint64 {
|
||||
if len(input) < 1 {
|
||||
return 0
|
||||
return MinCallGas
|
||||
}
|
||||
switch input[0] {
|
||||
case OpVerify:
|
||||
return GasVerify
|
||||
// alpha_string is variable length and is hashed, so the fee grows
|
||||
// with it. len(input) bounds alpha from above and is available here,
|
||||
// before the input has been parsed.
|
||||
return GasVerify + words(len(input)-1)*GasVerifyPerWord
|
||||
case OpProofToHash:
|
||||
return GasProofToHash
|
||||
default:
|
||||
return 0
|
||||
return MinCallGas
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,13 +131,16 @@ func (p *vrfPrecompile) RequiredGas(input []byte) uint64 {
|
||||
//
|
||||
// OpVerify (0x01):
|
||||
//
|
||||
// Input: opcode(1) || pk(32) || alpha_len(2 big-endian) || alpha(variable) || proof(80)
|
||||
// Output: beta_string(64) on success, empty on failure
|
||||
// Input: opcode(1) || pk(32) || alpha_len(2 big-endian) || alpha(alpha_len) || proof(80)
|
||||
// Output: beta_string(64) if the proof is valid, empty if it is not
|
||||
//
|
||||
// OpProofToHash (0x02):
|
||||
//
|
||||
// Input: opcode(1) || proof(80)
|
||||
// Output: beta_string(64)
|
||||
// Output: beta_string(64), empty if the proof does not decode
|
||||
//
|
||||
// Both operations require an exact-length input; an unrecognised selector or
|
||||
// an empty input is an error, never an empty success.
|
||||
func (p *vrfPrecompile) Run(
|
||||
accessibleState contract.AccessibleState,
|
||||
caller common.Address,
|
||||
@@ -111,37 +156,38 @@ func (p *vrfPrecompile) Run(
|
||||
}
|
||||
|
||||
if len(input) < 1 {
|
||||
return nil, remainingGas, nil
|
||||
return nil, remainingGas, ErrInvalidInput
|
||||
}
|
||||
|
||||
switch input[0] {
|
||||
case OpVerify:
|
||||
result := p.opVerify(input[1:])
|
||||
return result, remainingGas, nil
|
||||
return p.opVerify(input[1:]), remainingGas, nil
|
||||
|
||||
case OpProofToHash:
|
||||
result := p.opProofToHash(input[1:])
|
||||
return result, remainingGas, nil
|
||||
return p.opProofToHash(input[1:]), remainingGas, nil
|
||||
|
||||
default:
|
||||
return nil, remainingGas, nil
|
||||
return nil, remainingGas, ErrInvalidOperation
|
||||
}
|
||||
}
|
||||
|
||||
// opVerify implements ECVRF_verify per RFC 9381 section 5.3.
|
||||
// opVerify implements ECVRF_verify per RFC 9381 Section 5.3.
|
||||
//
|
||||
// Input: pk(32) || alpha_len(2) || alpha(variable) || proof(80)
|
||||
// Input: pk(32) || alpha_len(2) || alpha(alpha_len) || proof(80)
|
||||
func (p *vrfPrecompile) opVerify(data []byte) []byte {
|
||||
// Minimum: pk(32) + alpha_len(2) + alpha(0) + proof(80) = 114
|
||||
if len(data) < PublicKeySize+2+ProofSize {
|
||||
// The header carries the length that describes the rest.
|
||||
if len(data) < PublicKeySize+AlphaLenSize {
|
||||
return nil
|
||||
}
|
||||
|
||||
pk := data[:PublicKeySize]
|
||||
alphaLen := int(data[PublicKeySize])<<8 | int(data[PublicKeySize+1])
|
||||
offset := PublicKeySize + 2
|
||||
offset := PublicKeySize + AlphaLenSize
|
||||
|
||||
if len(data) < offset+alphaLen+ProofSize {
|
||||
// alpha_len must place the proof exactly at the end of the body: further
|
||||
// overruns the buffer, nearer leaves trailing bytes, and either way one
|
||||
// query would have more than one encoding.
|
||||
if len(data) != offset+alphaLen+ProofSize {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -155,15 +201,12 @@ func (p *vrfPrecompile) opVerify(data []byte) []byte {
|
||||
return beta
|
||||
}
|
||||
|
||||
// opProofToHash implements ECVRF_proof_to_hash per RFC 9381 section 5.2.
|
||||
// opProofToHash implements ECVRF_proof_to_hash per RFC 9381 Section 5.2.
|
||||
// decodeProof owns the length of pi_string, so there is nothing to check here.
|
||||
//
|
||||
// Input: proof(80)
|
||||
func (p *vrfPrecompile) opProofToHash(data []byte) []byte {
|
||||
if len(data) < ProofSize {
|
||||
return nil
|
||||
}
|
||||
|
||||
gamma, _, _, ok := decodeProof(data[:ProofSize])
|
||||
gamma, _, _, ok := decodeProof(data)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
@@ -171,164 +214,186 @@ func (p *vrfPrecompile) opProofToHash(data []byte) []byte {
|
||||
return proofToHash(gamma)
|
||||
}
|
||||
|
||||
// ─── RFC 9381 ECVRF-EDWARDS25519-SHA512-ELL2 ─────────────────────────────────
|
||||
// ─── RFC 9381 ECVRF-EDWARDS25519-SHA512-TAI ──────────────────────────────────
|
||||
|
||||
// ecvrfVerify implements ECVRF_verify (RFC 9381 section 5.3).
|
||||
// Returns (beta_string, true) on success.
|
||||
// ecvrfVerify implements ECVRF_verify (RFC 9381 Section 5.3) with
|
||||
// validate_key = TRUE. Returns (beta_string, true) on success.
|
||||
func ecvrfVerify(pkBytes, alpha, piBytes []byte) ([]byte, bool) {
|
||||
// Step 1: Decode public key Y
|
||||
Y, err := new(edwards25519.Point).SetBytes(pkBytes)
|
||||
// Step 1-2: Y = string_to_point(PK_string)
|
||||
Y, err := decodePoint(pkBytes)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Step 2: Decode proof (Gamma, c, s)
|
||||
// Step 3: ECVRF_validate_key(Y)
|
||||
if !validateKey(Y) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Step 4-6: (Gamma, c, s) = ECVRF_decode_proof(pi_string)
|
||||
Gamma, c, s, ok := decodeProof(piBytes)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Step 3: Hash to curve H = ECVRF_encode_to_curve(encode_to_curve_salt=pk, alpha)
|
||||
H := hashToCurveELL2(pkBytes, alpha)
|
||||
// Step 7: H = ECVRF_encode_to_curve(encode_to_curve_salt = PK_string, alpha)
|
||||
H := encodeToCurve(pkBytes, alpha)
|
||||
if H == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Step 4: U = s*B - c*Y
|
||||
// Step 8: U = s*B - c*Y
|
||||
cNeg := new(edwards25519.Scalar).Negate(c)
|
||||
U := new(edwards25519.Point).VarTimeDoubleScalarBaseMult(cNeg, Y, s)
|
||||
|
||||
// Step 5: V = s*H - c*Gamma
|
||||
// Step 9: V = s*H - c*Gamma
|
||||
sH := new(edwards25519.Point).ScalarMult(s, H)
|
||||
cGamma := new(edwards25519.Point).ScalarMult(c, Gamma)
|
||||
cGammaNeg := new(edwards25519.Point).Negate(cGamma)
|
||||
V := new(edwards25519.Point).Add(sH, cGammaNeg)
|
||||
V := new(edwards25519.Point).Subtract(sH, cGamma)
|
||||
|
||||
// Step 6: Derive c' = ECVRF_challenge_generation(Y, H, Gamma, U, V)
|
||||
// Step 10: c' = ECVRF_challenge_generation(Y, H, Gamma, U, V)
|
||||
cPrime := challengeGeneration(Y, H, Gamma, U, V)
|
||||
|
||||
// Step 7: Check c == c'
|
||||
// Step 11: c == c' ?
|
||||
if c.Equal(cPrime) != 1 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Step 8: beta = ECVRF_proof_to_hash(pi)
|
||||
beta := proofToHash(Gamma)
|
||||
return beta, true
|
||||
return proofToHash(Gamma), true
|
||||
}
|
||||
|
||||
// proofToHash implements ECVRF_proof_to_hash (RFC 9381 section 5.2).
|
||||
// beta_string = SHA-512(suite_string || proof_to_hash_domain_separator || cofactor*Gamma || zero)
|
||||
// decodePoint implements string_to_point (RFC 8032 Section 5.1.3), which
|
||||
// RFC 9381 Section 5.5 names for this suite and which MUST reject an octet
|
||||
// string that is not the encoding of a point.
|
||||
//
|
||||
// edwards25519.Point.SetBytes deliberately does not: it accepts an unreduced y
|
||||
// and accepts a set sign bit where x is zero, matching the wider ecosystem
|
||||
// rather than RFC 8032. Under those rules a Gamma has more than one pi_string
|
||||
// and a public key has more than one address. Re-encoding and comparing is the
|
||||
// canonicality test, and it covers both cases at once.
|
||||
func decodePoint(b []byte) (*edwards25519.Point, error) {
|
||||
P, err := new(edwards25519.Point).SetBytes(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !bytes.Equal(P.Bytes(), b) {
|
||||
return nil, errNonCanonicalPoint
|
||||
}
|
||||
return P, nil
|
||||
}
|
||||
|
||||
// validateKey implements ECVRF_validate_key (RFC 9381 Section 5.4.5): the key
|
||||
// is valid iff cofactor*Y is not the identity.
|
||||
//
|
||||
// Y arrives in calldata, so a caller is free to pick it. Without this check a
|
||||
// caller who picks the identity produces a proof that verifies for every
|
||||
// alpha_string and whose beta_string is a fixed value known in advance, which
|
||||
// is exactly the outcome a verifiable random function exists to prevent.
|
||||
func validateKey(Y *edwards25519.Point) bool {
|
||||
cofY := new(edwards25519.Point).MultByCofactor(Y)
|
||||
return cofY.Equal(edwards25519.NewIdentityPoint()) == 0
|
||||
}
|
||||
|
||||
// proofToHash implements ECVRF_proof_to_hash (RFC 9381 Section 5.2).
|
||||
//
|
||||
// beta_string = Hash(suite_string || 0x03 || point_to_string(cofactor*Gamma) || 0x00)
|
||||
func proofToHash(Gamma *edwards25519.Point) []byte {
|
||||
// Cofactor multiply: cofGamma = cofactor * Gamma
|
||||
// For Ed25519, cofactor = 8. We multiply by 8 via three doublings.
|
||||
cofGamma := new(edwards25519.Point).Add(Gamma, Gamma) // 2*Gamma
|
||||
cofGamma = new(edwards25519.Point).Add(cofGamma, cofGamma) // 4*Gamma
|
||||
cofGamma = new(edwards25519.Point).Add(cofGamma, cofGamma) // 8*Gamma
|
||||
cofGamma := new(edwards25519.Point).MultByCofactor(Gamma)
|
||||
|
||||
h := sha512.New()
|
||||
h.Write([]byte{suiteString, dsProofToHash})
|
||||
h.Write(cofGamma.Bytes())
|
||||
h.Write([]byte{0x00})
|
||||
h.Write([]byte{dsBack})
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
// hashToCurveELL2 implements ECVRF_encode_to_curve using the
|
||||
// Elligator2 method per RFC 9381 section 5.4.1.2.
|
||||
// encodeToCurve implements ECVRF_encode_to_curve_try_and_increment (RFC 9381
|
||||
// Section 5.4.1.1) with interpret_hash_value_as_a_point(s) =
|
||||
// string_to_point(s[0]...s[31]), as fixed for this suite by Section 5.5.
|
||||
//
|
||||
// This is the "try-and-increment" variant. It hashes the input to a
|
||||
// field element, applies Elligator2 to obtain a point, and multiplies
|
||||
// by the cofactor.
|
||||
func hashToCurveELL2(pkBytes, alpha []byte) *edwards25519.Point {
|
||||
// Hash: SHA-512(suite_string || one || pk || alpha || ctr || zero)
|
||||
// Try ctr = 0, 1, 2, ... until we get a valid point.
|
||||
for ctr := range byte(255) {
|
||||
// The counter advances while the candidate fails to decode to a point or
|
||||
// decodes to the identity; the hash input is the full string every time, which
|
||||
// is why the verify fee is charged per word of input.
|
||||
func encodeToCurve(salt, alpha []byte) *edwards25519.Point {
|
||||
for ctr := range maxCounter {
|
||||
h := sha512.New()
|
||||
h.Write([]byte{suiteString, dsHashToTryCAndIncrement})
|
||||
h.Write(pkBytes)
|
||||
h.Write([]byte{suiteString, dsEncodeToCurve})
|
||||
h.Write(salt)
|
||||
h.Write(alpha)
|
||||
h.Write([]byte{ctr})
|
||||
h.Write([]byte{0x00})
|
||||
hash := h.Sum(nil)
|
||||
h.Write([]byte{byte(ctr), dsBack})
|
||||
|
||||
// Take the first 32 bytes, clear the high bit (MSB of last byte
|
||||
// when using little-endian encoding), and try to decompress.
|
||||
var candidate [32]byte
|
||||
copy(candidate[:], hash[:32])
|
||||
candidate[31] &= 0x7F // Clear sign bit
|
||||
|
||||
point, err := new(edwards25519.Point).SetBytes(candidate[:])
|
||||
if err != nil {
|
||||
continue
|
||||
if H := interpretHashAsPoint(h.Sum(nil)); H != nil {
|
||||
return H
|
||||
}
|
||||
|
||||
// Multiply by cofactor (8) to ensure we're in the prime-order subgroup.
|
||||
cofPoint := new(edwards25519.Point).Add(point, point) // 2
|
||||
cofPoint = new(edwards25519.Point).Add(cofPoint, cofPoint) // 4
|
||||
cofPoint = new(edwards25519.Point).Add(cofPoint, cofPoint) // 8
|
||||
|
||||
// Reject identity.
|
||||
if cofPoint.Equal(edwards25519.NewIdentityPoint()) == 1 {
|
||||
continue
|
||||
}
|
||||
return cofPoint
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// challengeGeneration implements ECVRF_challenge_generation (RFC 9381 section 5.4.3).
|
||||
// c = SHA-512(suite_string || challenge_generation_domain_separator || encode(Y) || encode(H) || encode(Gamma) || encode(U) || encode(V) || zero)[0:16]
|
||||
func challengeGeneration(Y, H, Gamma, U, V *edwards25519.Point) *edwards25519.Scalar {
|
||||
h := sha512.New()
|
||||
h.Write([]byte{suiteString, dsHashPoints})
|
||||
h.Write(Y.Bytes())
|
||||
h.Write(H.Bytes())
|
||||
h.Write(Gamma.Bytes())
|
||||
h.Write(U.Bytes())
|
||||
h.Write(V.Bytes())
|
||||
h.Write([]byte{0x00})
|
||||
|
||||
hash := h.Sum(nil)
|
||||
|
||||
// c is the first 16 bytes, interpreted as a little-endian scalar.
|
||||
var truncated [32]byte
|
||||
copy(truncated[:16], hash[:16])
|
||||
|
||||
scalar, err := new(edwards25519.Scalar).SetCanonicalBytes(truncated[:])
|
||||
// interpretHashAsPoint is interpret_hash_value_as_a_point as RFC 9381
|
||||
// Section 5.5 fixes it for this suite: string_to_point over the first 32
|
||||
// octets, then multiplication by the cofactor.
|
||||
//
|
||||
// It returns nil where the counter must advance: the octets are not a point
|
||||
// encoding, or the cofactor kills them. The high bit of the last octet is the
|
||||
// sign of x (RFC 8032 Section 5.1.3), not padding, and is carried through.
|
||||
func interpretHashAsPoint(hash []byte) *edwards25519.Point {
|
||||
point, err := decodePoint(hash[:32])
|
||||
if err != nil {
|
||||
// Should not happen with a 128-bit value in a 256-bit field.
|
||||
return new(edwards25519.Scalar)
|
||||
return nil
|
||||
}
|
||||
return scalar
|
||||
|
||||
cofPoint := new(edwards25519.Point).MultByCofactor(point)
|
||||
if cofPoint.Equal(edwards25519.NewIdentityPoint()) == 1 {
|
||||
return nil
|
||||
}
|
||||
return cofPoint
|
||||
}
|
||||
|
||||
// decodeProof splits pi_string into (Gamma, c, s).
|
||||
// challengeGeneration implements ECVRF_challenge_generation (RFC 9381
|
||||
// Section 5.4.3) over the five points ECVRF_verify passes it.
|
||||
//
|
||||
// c = Hash(suite_string || 0x02 || point_to_string(P1) || ... || 0x00)[0..cLen-1]
|
||||
func challengeGeneration(Y, H, Gamma, U, V *edwards25519.Point) *edwards25519.Scalar {
|
||||
h := sha512.New()
|
||||
h.Write([]byte{suiteString, dsChallenge})
|
||||
for _, P := range []*edwards25519.Point{Y, H, Gamma, U, V} {
|
||||
h.Write(P.Bytes())
|
||||
}
|
||||
h.Write([]byte{dsBack})
|
||||
hash := h.Sum(nil)
|
||||
|
||||
// c = string_to_int(hash[0..cLen-1]), little-endian, cLen = 16. A 128-bit
|
||||
// value is below the group order for every input, so the canonical-encoding
|
||||
// check cannot reject it and there is nothing to branch on.
|
||||
var truncated [32]byte
|
||||
copy(truncated[:16], hash[:16])
|
||||
c, _ := new(edwards25519.Scalar).SetCanonicalBytes(truncated[:])
|
||||
return c
|
||||
}
|
||||
|
||||
// decodeProof implements ECVRF_decode_proof (RFC 9381 Section 5.4.4), splitting
|
||||
// pi_string into (Gamma, c, s).
|
||||
//
|
||||
// pi_string layout (80 bytes):
|
||||
//
|
||||
// Gamma: 32 bytes (compressed Edwards point)
|
||||
// c: 16 bytes (little-endian scalar, zero-padded to 32 for SetCanonicalBytes)
|
||||
// s: 32 bytes (little-endian scalar)
|
||||
// c: 16 bytes (little-endian integer)
|
||||
// s: 32 bytes (little-endian integer, rejected if >= the group order)
|
||||
func decodeProof(pi []byte) (*edwards25519.Point, *edwards25519.Scalar, *edwards25519.Scalar, bool) {
|
||||
if len(pi) != ProofSize {
|
||||
return nil, nil, nil, false
|
||||
}
|
||||
|
||||
// Decode Gamma
|
||||
Gamma, err := new(edwards25519.Point).SetBytes(pi[:32])
|
||||
Gamma, err := decodePoint(pi[:32])
|
||||
if err != nil {
|
||||
return nil, nil, nil, false
|
||||
}
|
||||
|
||||
// Decode c (16 bytes, little-endian, padded to 32)
|
||||
var cBytes [32]byte
|
||||
copy(cBytes[:16], pi[32:48])
|
||||
c, err := new(edwards25519.Scalar).SetCanonicalBytes(cBytes[:])
|
||||
if err != nil {
|
||||
return nil, nil, nil, false
|
||||
}
|
||||
c, _ := new(edwards25519.Scalar).SetCanonicalBytes(cBytes[:])
|
||||
|
||||
// Decode s (32 bytes, little-endian)
|
||||
// s >= q is rejected here; without it a proof would have a second encoding.
|
||||
s, err := new(edwards25519.Scalar).SetCanonicalBytes(pi[48:80])
|
||||
if err != nil {
|
||||
return nil, nil, nil, false
|
||||
|
||||
+520
-305
@@ -4,328 +4,569 @@
|
||||
package vrf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha512"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
edwards "filippo.io/edwards25519"
|
||||
"github.com/luxfi/precompile/contract"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const testGas = 100_000
|
||||
const testGas = 10_000_000
|
||||
|
||||
// ─── Gas ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestRequiredGas(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
want uint64
|
||||
}{
|
||||
{"empty", nil, 0},
|
||||
{"verify", []byte{OpVerify}, GasVerify},
|
||||
{"proof_to_hash", []byte{OpProofToHash}, GasProofToHash},
|
||||
{"unknown_op", []byte{0xFF}, 0},
|
||||
// TestGas_VerifyGrowsWithInput is the regression test for a flat verify fee.
|
||||
// ECVRF_encode_to_curve hashes the whole alpha_string, once per counter, so a
|
||||
// fee that ignores the length lets a caller buy unbounded hashing at a fixed
|
||||
// price.
|
||||
func TestGas_VerifyGrowsWithInput(t *testing.T) {
|
||||
alphaLens := []int{0, 32, 64, 1024, 65535}
|
||||
|
||||
prev := uint64(0)
|
||||
for _, n := range alphaLens {
|
||||
input := buildVerifyInput(make([]byte, PublicKeySize), make([]byte, n), make([]byte, ProofSize))
|
||||
got := VRFPrecompile.RequiredGas(input)
|
||||
|
||||
want := GasVerify + words(len(input)-1)*GasVerifyPerWord
|
||||
require.Equalf(t, want, got, "alpha_len=%d", n)
|
||||
require.Greaterf(t, got, prev, "fee must rise with alpha_len (%d)", n)
|
||||
prev = got
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := VRFPrecompile.RequiredGas(tt.input); got != tt.want {
|
||||
t.Errorf("RequiredGas = %d, want %d", got, tt.want)
|
||||
}
|
||||
|
||||
// The largest alpha the two-byte length prefix can describe must cost
|
||||
// materially more than the size-independent part alone.
|
||||
maxInput := buildVerifyInput(make([]byte, PublicKeySize), make([]byte, 65535), make([]byte, ProofSize))
|
||||
require.Greater(t, VRFPrecompile.RequiredGas(maxInput), 3*uint64(GasVerify))
|
||||
}
|
||||
|
||||
// TestGas_FlatVerifyFeeNoLongerBuysMaxAlpha states the same property through
|
||||
// Run: the old flat fee is not enough to verify a maximum-length alpha_string.
|
||||
func TestGas_FlatVerifyFeeNoLongerBuysMaxAlpha(t *testing.T) {
|
||||
input := buildVerifyInput(make([]byte, PublicKeySize), make([]byte, 65535), make([]byte, ProofSize))
|
||||
|
||||
_, remaining, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, GasVerify, true)
|
||||
require.ErrorIs(t, err, contract.ErrOutOfGas)
|
||||
require.Zero(t, remaining)
|
||||
|
||||
// It is affordable at the advertised price and not one gas below it.
|
||||
need := VRFPrecompile.RequiredGas(input)
|
||||
_, _, err = VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, need-1, true)
|
||||
require.ErrorIs(t, err, contract.ErrOutOfGas)
|
||||
_, remaining, err = VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, need, true)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, remaining)
|
||||
}
|
||||
|
||||
// TestGas_NoCallIsEverFree is the regression test for a successful zero-gas
|
||||
// call. Every selector, including the 254 that do nothing, must either charge
|
||||
// gas or fail; a nil error with the full gas returned is a free call.
|
||||
func TestGas_NoCallIsEverFree(t *testing.T) {
|
||||
for op := range 256 {
|
||||
input := []byte{byte(op)}
|
||||
charged := VRFPrecompile.RequiredGas(input)
|
||||
require.Positivef(t, charged, "selector 0x%02x is free", op)
|
||||
|
||||
if op != OpVerify && op != OpProofToHash {
|
||||
require.GreaterOrEqualf(t, charged, uint64(MinCallGas),
|
||||
"selector 0x%02x does no work and is priced below the floor", op)
|
||||
}
|
||||
|
||||
_, remaining, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
require.Lessf(t, remaining, uint64(testGas), "selector 0x%02x succeeded without charging gas", op)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGas_UnknownSelectorIsAnError(t *testing.T) {
|
||||
for _, op := range []byte{0x00, 0x03, 0x10, 0xFF} {
|
||||
out, remaining, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, []byte{op}, testGas, true)
|
||||
require.ErrorIsf(t, err, contract.ErrInvalidOp, "selector 0x%02x", op)
|
||||
require.Nil(t, out)
|
||||
require.Equal(t, uint64(testGas-MinCallGas), remaining)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGas_EmptyInputIsAnError(t *testing.T) {
|
||||
require.Equal(t, uint64(MinCallGas), VRFPrecompile.RequiredGas(nil))
|
||||
|
||||
out, remaining, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, nil, testGas, true)
|
||||
require.ErrorIs(t, err, contract.ErrInvalidInput)
|
||||
require.Nil(t, out)
|
||||
require.Equal(t, uint64(testGas-MinCallGas), remaining)
|
||||
}
|
||||
|
||||
// TestGas_RunChargesTheAdvertisedPrice ties Run's accounting to RequiredGas
|
||||
// for every operation, so the two can never drift apart.
|
||||
func TestGas_RunChargesTheAdvertisedPrice(t *testing.T) {
|
||||
v := rfc9381TAI[1]
|
||||
inputs := map[string][]byte{
|
||||
"verify": buildVerifyInput(mustHex(v.pk), mustHex(v.alpha), mustHex(v.pi)),
|
||||
"proof_to_hash": append([]byte{OpProofToHash}, mustHex(v.pi)...),
|
||||
"unknown": {0xFF},
|
||||
"empty": {},
|
||||
}
|
||||
for name, input := range inputs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
want := VRFPrecompile.RequiredGas(input)
|
||||
_, remaining, _ := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true)
|
||||
require.Equal(t, uint64(testGas)-want, remaining)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Out of Gas ───────────────────────────────────────────────────────────────
|
||||
|
||||
func TestOutOfGas(t *testing.T) {
|
||||
input := append([]byte{OpVerify}, make([]byte, 200)...)
|
||||
_, _, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, GasVerify-1, true)
|
||||
if err == nil {
|
||||
t.Error("expected out of gas error")
|
||||
func TestGas_ProofToHashIsFixedPrice(t *testing.T) {
|
||||
for _, n := range []int{0, ProofSize, 4096} {
|
||||
input := append([]byte{OpProofToHash}, make([]byte, n)...)
|
||||
require.Equal(t, uint64(GasProofToHash), VRFPrecompile.RequiredGas(input))
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Empty / Short Input ──────────────────────────────────────────────────────
|
||||
func TestGas_Words(t *testing.T) {
|
||||
require.Equal(t, uint64(0), words(0))
|
||||
require.Equal(t, uint64(1), words(1))
|
||||
require.Equal(t, uint64(1), words(32))
|
||||
require.Equal(t, uint64(2), words(33))
|
||||
}
|
||||
|
||||
func TestEmptyInput(t *testing.T) {
|
||||
result, gas, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, nil, testGas, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
// ─── Key validation (RFC 9381 Section 5.4.5) ─────────────────────────────────
|
||||
|
||||
// smallOrderKeys are the encodings of the Ed25519 points killed by the
|
||||
// cofactor: the identity, the point of order 2, the two of order 4 and the
|
||||
// four of order 8. Each is checked to be exactly that below rather than
|
||||
// trusted, so a wrong constant fails loudly instead of weakening the test.
|
||||
var smallOrderKeys = []string{
|
||||
"0100000000000000000000000000000000000000000000000000000000000000",
|
||||
"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
|
||||
"0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"0000000000000000000000000000000000000000000000000000000000000080",
|
||||
"26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05",
|
||||
"c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a",
|
||||
"26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85",
|
||||
"c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa",
|
||||
}
|
||||
|
||||
func TestValidateKey_RejectsExactlyTheSmallOrderPoints(t *testing.T) {
|
||||
for _, h := range smallOrderKeys {
|
||||
P, err := new(edwards.Point).SetBytes(mustHex(h))
|
||||
require.NoErrorf(t, err, "%s is not a point encoding", h)
|
||||
require.Equalf(t, 1, new(edwards.Point).MultByCofactor(P).Equal(edwards.NewIdentityPoint()),
|
||||
"%s is not killed by the cofactor", h)
|
||||
require.Falsef(t, validateKey(P), "%s must fail ECVRF_validate_key", h)
|
||||
}
|
||||
if result != nil {
|
||||
t.Error("expected nil result for empty input")
|
||||
}
|
||||
if gas != testGas {
|
||||
t.Errorf("expected full gas returned, got %d", gas)
|
||||
|
||||
// A real public key survives.
|
||||
for _, v := range rfc9381TAI {
|
||||
Y, err := new(edwards.Point).SetBytes(mustHex(v.pk))
|
||||
require.NoError(t, err)
|
||||
require.True(t, validateKey(Y))
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyShortInput(t *testing.T) {
|
||||
input := []byte{OpVerify, 0x01, 0x02}
|
||||
result, _, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result != nil {
|
||||
t.Error("expected nil for short verify input")
|
||||
// TestVerify_RejectsSmallOrderKey drives the same keys through the precompile.
|
||||
func TestVerify_RejectsSmallOrderKey(t *testing.T) {
|
||||
v := rfc9381TAI[0]
|
||||
for _, h := range smallOrderKeys {
|
||||
input := buildVerifyInput(mustHex(h), mustHex(v.alpha), mustHex(v.pi))
|
||||
out, _, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true)
|
||||
require.NoError(t, err)
|
||||
require.Nilf(t, out, "small-order key %s was accepted", h)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProofToHashShortInput(t *testing.T) {
|
||||
input := append([]byte{OpProofToHash}, make([]byte, 10)...)
|
||||
result, _, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
// TestVerify_IdentityKeyForgeryIsRefused builds the proof that an unvalidated
|
||||
// key buys and requires the precompile to refuse it.
|
||||
//
|
||||
// With Y and Gamma both the identity, U = s*B and V = s*H no longer depend on
|
||||
// c, so the challenge can simply be computed and written into the proof: the
|
||||
// c == c' comparison passes for any alpha_string with no secret key at all,
|
||||
// and beta_string is Hash(suite || 0x03 || point_to_string(identity) || 0x00),
|
||||
// the same constant every time. ECVRF_validate_key is the only step that
|
||||
// stands between a caller and a VRF output they picked in advance.
|
||||
func TestVerify_IdentityKeyForgeryIsRefused(t *testing.T) {
|
||||
identity := edwards.NewIdentityPoint()
|
||||
pk := identity.Bytes()
|
||||
s := edwards.NewScalar()
|
||||
_, err := s.SetCanonicalBytes(append([]byte{1}, make([]byte, 31)...))
|
||||
require.NoError(t, err)
|
||||
|
||||
forge := func(alpha []byte) []byte {
|
||||
H := encodeToCurve(pk, alpha)
|
||||
require.NotNil(t, H)
|
||||
U := new(edwards.Point).ScalarBaseMult(s) // s*B - c*identity
|
||||
V := new(edwards.Point).ScalarMult(s, H) // s*H - c*identity
|
||||
c := challengeGeneration(identity, H, identity, U, V)
|
||||
|
||||
pi := make([]byte, ProofSize)
|
||||
copy(pi[:32], identity.Bytes())
|
||||
cBytes := c.Bytes()
|
||||
copy(pi[32:48], cBytes[:16])
|
||||
copy(pi[48:80], s.Bytes())
|
||||
return pi
|
||||
}
|
||||
if result != nil {
|
||||
t.Error("expected nil for short proof_to_hash input")
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Unknown Operation ────────────────────────────────────────────────────────
|
||||
// The forgery is arithmetically sound: every step of ECVRF_verify after
|
||||
// key validation accepts it.
|
||||
alphaA, alphaB := []byte("lottery round 1"), []byte("lottery round 2")
|
||||
piA, piB := forge(alphaA), forge(alphaB)
|
||||
|
||||
func TestUnknownOp(t *testing.T) {
|
||||
input := []byte{0xFF}
|
||||
result, gas, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result != nil {
|
||||
t.Error("expected nil for unknown op")
|
||||
}
|
||||
if gas != testGas {
|
||||
t.Errorf("expected full gas on unknown op, got %d", gas)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Invalid Public Key ───────────────────────────────────────────────────────
|
||||
|
||||
func TestVerifyInvalidPublicKey(t *testing.T) {
|
||||
pk := make([]byte, 32)
|
||||
alpha := []byte("test")
|
||||
proof := make([]byte, ProofSize)
|
||||
|
||||
input := buildVerifyInput(pk, alpha, proof)
|
||||
result, _, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result != nil {
|
||||
t.Error("expected nil for invalid public key")
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Invalid Proof ────────────────────────────────────────────────────────────
|
||||
|
||||
func TestVerifyInvalidProof(t *testing.T) {
|
||||
// Generate a proof with one key, try to verify against a different key.
|
||||
sk1 := mustHex("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60")
|
||||
_, pk1 := expandSecretKey(sk1)
|
||||
|
||||
sk2 := mustHex("4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb")
|
||||
_, pk2 := expandSecretKey(sk2)
|
||||
|
||||
alpha := []byte("test message")
|
||||
|
||||
// Prove with sk1
|
||||
pi := testProve(sk1, pk1, alpha)
|
||||
|
||||
// Verify against pk2 -- must fail
|
||||
input := buildVerifyInput(pk2.Bytes(), alpha, pi)
|
||||
result, _, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result != nil {
|
||||
t.Error("expected nil when verifying proof against wrong public key")
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Self-Consistency Tests ───────────────────────────────────────────────────
|
||||
|
||||
func TestProveAndVerify(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
skHex string
|
||||
for _, tc := range []struct {
|
||||
alpha []byte
|
||||
}{
|
||||
{
|
||||
name: "empty_alpha",
|
||||
skHex: "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60",
|
||||
alpha: []byte{},
|
||||
},
|
||||
{
|
||||
name: "single_byte",
|
||||
skHex: "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb",
|
||||
alpha: []byte{0x72},
|
||||
},
|
||||
{
|
||||
name: "two_bytes",
|
||||
skHex: "c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7",
|
||||
alpha: []byte{0xaf, 0x82},
|
||||
},
|
||||
{
|
||||
name: "ascii_string",
|
||||
skHex: "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60",
|
||||
alpha: []byte("Example of ECVRF-EDWARDS25519-SHA512-ELL2"),
|
||||
},
|
||||
{
|
||||
name: "long_alpha",
|
||||
skHex: "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb",
|
||||
alpha: make([]byte, 256),
|
||||
},
|
||||
pi []byte
|
||||
}{{alphaA, piA}, {alphaB, piB}} {
|
||||
Gamma, c, sc, ok := decodeProof(tc.pi)
|
||||
require.True(t, ok)
|
||||
Y, err := new(edwards.Point).SetBytes(pk)
|
||||
require.NoError(t, err)
|
||||
H := encodeToCurve(pk, tc.alpha)
|
||||
cNeg := new(edwards.Scalar).Negate(c)
|
||||
U := new(edwards.Point).VarTimeDoubleScalarBaseMult(cNeg, Y, sc)
|
||||
V := new(edwards.Point).Subtract(new(edwards.Point).ScalarMult(sc, H), new(edwards.Point).ScalarMult(c, Gamma))
|
||||
require.Equal(t, 1, c.Equal(challengeGeneration(Y, H, Gamma, U, V)),
|
||||
"forgery does not satisfy c == c'; the test no longer models the attack")
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sk := mustHex(tt.skHex)
|
||||
x, Y := expandSecretKey(sk)
|
||||
// The output it would have produced is one constant, known before either
|
||||
// alpha_string was chosen.
|
||||
predicted := proofToHash(identity)
|
||||
require.Equal(t, predicted, proofToHash(identity))
|
||||
|
||||
// Prove
|
||||
pi := testProve(sk, Y, tt.alpha)
|
||||
if len(pi) != ProofSize {
|
||||
t.Fatalf("proof size = %d, want %d", len(pi), ProofSize)
|
||||
}
|
||||
// And the precompile refuses both.
|
||||
for _, tc := range []struct {
|
||||
alpha []byte
|
||||
pi []byte
|
||||
}{{alphaA, piA}, {alphaB, piB}} {
|
||||
input := buildVerifyInput(pk, tc.alpha, tc.pi)
|
||||
out, _, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, out, "identity public key produced a valid VRF output")
|
||||
require.NotEqual(t, predicted, out)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify via precompile
|
||||
input := buildVerifyInput(Y.Bytes(), tt.alpha, pi)
|
||||
result, gas, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true)
|
||||
if err != nil {
|
||||
t.Fatalf("Run error: %v", err)
|
||||
}
|
||||
if gas != testGas-GasVerify {
|
||||
t.Errorf("remaining gas = %d, want %d", gas, testGas-GasVerify)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("verification returned nil (failed)")
|
||||
}
|
||||
if len(result) != BetaSize {
|
||||
t.Fatalf("beta length = %d, want %d", len(result), BetaSize)
|
||||
}
|
||||
// ─── Encoding refusals ────────────────────────────────────────────────────────
|
||||
|
||||
// ProofToHash must return the same beta
|
||||
p2hInput := append([]byte{OpProofToHash}, pi...)
|
||||
p2hResult, _, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, p2hInput, testGas, true)
|
||||
if err != nil {
|
||||
t.Fatalf("ProofToHash error: %v", err)
|
||||
}
|
||||
if hex.EncodeToString(p2hResult) != hex.EncodeToString(result) {
|
||||
t.Errorf("ProofToHash beta differs from Verify beta")
|
||||
}
|
||||
func TestVerify_RejectsShortInput(t *testing.T) {
|
||||
minLen := PublicKeySize + AlphaLenSize + ProofSize
|
||||
for n := 0; n < minLen; n += 7 {
|
||||
input := append([]byte{OpVerify}, make([]byte, n)...)
|
||||
out, _, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true)
|
||||
require.NoError(t, err)
|
||||
require.Nilf(t, out, "accepted a %d-byte verify body", n)
|
||||
}
|
||||
}
|
||||
|
||||
// Determinism: same input, same output
|
||||
result2, _, _ := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true)
|
||||
if hex.EncodeToString(result) != hex.EncodeToString(result2) {
|
||||
t.Error("VRF verify is not deterministic")
|
||||
}
|
||||
// TestVerify_RejectsAlphaLenOverrun covers a declared alpha_len that runs past
|
||||
// the end of the supplied buffer.
|
||||
func TestVerify_RejectsAlphaLenOverrun(t *testing.T) {
|
||||
v := rfc9381TAI[2]
|
||||
base := buildVerifyInput(mustHex(v.pk), mustHex(v.alpha), mustHex(v.pi))
|
||||
|
||||
// Different alpha must produce different beta
|
||||
otherAlpha := append(tt.alpha, 0xFF)
|
||||
otherPi := testProve(sk, Y, otherAlpha)
|
||||
otherInput := buildVerifyInput(Y.Bytes(), otherAlpha, otherPi)
|
||||
otherResult, _, _ := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, otherInput, testGas, true)
|
||||
if otherResult != nil && hex.EncodeToString(result) == hex.EncodeToString(otherResult) {
|
||||
t.Error("different alpha produced same beta")
|
||||
}
|
||||
for _, declared := range []uint16{3, 100, 0xFFFF} {
|
||||
input := bytes.Clone(base)
|
||||
binary.BigEndian.PutUint16(input[1+PublicKeySize:], declared)
|
||||
out, _, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true)
|
||||
require.NoError(t, err)
|
||||
require.Nilf(t, out, "accepted alpha_len=%d over a %d-byte body", declared, len(base)-1)
|
||||
}
|
||||
}
|
||||
|
||||
_ = x // used in prove
|
||||
// TestVerify_RejectsTrailingBytes: a declared alpha_len shorter than the body,
|
||||
// or bytes appended after the proof, would give one query two encodings.
|
||||
func TestVerify_RejectsTrailingBytes(t *testing.T) {
|
||||
v := rfc9381TAI[2]
|
||||
base := buildVerifyInput(mustHex(v.pk), mustHex(v.alpha), mustHex(v.pi))
|
||||
|
||||
// Sanity: the exact encoding is accepted.
|
||||
out, _, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, base, testGas, true)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, out)
|
||||
|
||||
appended := append(bytes.Clone(base), 0x00)
|
||||
out, _, err = VRFPrecompile.Run(nil, ContractAddress, ContractAddress, appended, testGas, true)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, out, "accepted a trailing byte")
|
||||
|
||||
// alpha_len one short: the last alpha byte would be read as proof.
|
||||
short := bytes.Clone(base)
|
||||
binary.BigEndian.PutUint16(short[1+PublicKeySize:], 1)
|
||||
out, _, err = VRFPrecompile.Run(nil, ContractAddress, ContractAddress, short, testGas, true)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, out, "accepted a short alpha_len over a longer body")
|
||||
}
|
||||
|
||||
// notOnCurve is a y with no matching x: (y^2-1)/(dy^2+1) is not a square.
|
||||
const notOnCurve = "0200000000000000000000000000000000000000000000000000000000000000"
|
||||
|
||||
// nonCanonicalPoints are octet strings that edwards25519.Point.SetBytes accepts
|
||||
// but RFC 8032 string_to_point does not: an unreduced y, and a set sign bit
|
||||
// where x is zero. Each aliases a point that already has its own encoding, so
|
||||
// accepting them would give one Gamma two pi_strings and one key two addresses.
|
||||
var nonCanonicalPoints = map[string]string{
|
||||
"y_equal_to_p": "edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
|
||||
"y_above_p": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"identity_sign_set": "0100000000000000000000000000000000000000000000000000000000000080",
|
||||
"order_two_sign_set": "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
}
|
||||
|
||||
func TestDecodePoint_RejectsNonCanonicalEncodings(t *testing.T) {
|
||||
for name, h := range nonCanonicalPoints {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
raw := mustHex(h)
|
||||
|
||||
// The leniency being closed here is real: the library accepts it.
|
||||
P, err := new(edwards.Point).SetBytes(raw)
|
||||
require.NoError(t, err, "test constant is not accepted even leniently")
|
||||
require.NotEqual(t, h, hex.EncodeToString(P.Bytes()), "test constant is canonical after all")
|
||||
|
||||
_, err = decodePoint(raw)
|
||||
require.ErrorIs(t, err, errNonCanonicalPoint)
|
||||
|
||||
// The canonical encoding of the same point is accepted.
|
||||
canonical, err := decodePoint(P.Bytes())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, canonical.Equal(P))
|
||||
})
|
||||
}
|
||||
|
||||
_, err := decodePoint(mustHex(notOnCurve))
|
||||
require.Error(t, err)
|
||||
require.NotErrorIs(t, err, errNonCanonicalPoint)
|
||||
|
||||
_, err = decodePoint(make([]byte, 31))
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestVerify_RejectsUndecodableGamma(t *testing.T) {
|
||||
v := rfc9381TAI[0]
|
||||
pi := mustHex(v.pi)
|
||||
|
||||
mutants := map[string]string{"not_on_curve": notOnCurve}
|
||||
for name, h := range nonCanonicalPoints {
|
||||
mutants[name] = h
|
||||
}
|
||||
|
||||
for name, h := range mutants {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
mutant := bytes.Clone(pi)
|
||||
copy(mutant[:32], mustHex(h))
|
||||
|
||||
_, _, _, ok := decodeProof(mutant)
|
||||
require.False(t, ok)
|
||||
|
||||
input := buildVerifyInput(mustHex(v.pk), mustHex(v.alpha), mutant)
|
||||
out, _, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, out)
|
||||
|
||||
p2h, _, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, append([]byte{OpProofToHash}, mutant...), testGas, true)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, p2h)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Uniqueness: Different keys produce different outputs ─────────────────────
|
||||
// TestVerify_RejectsNonCanonicalS pins the s < q check in ECVRF_decode_proof.
|
||||
// Without it a proof has a second encoding, s + q, which verifies identically.
|
||||
func TestVerify_RejectsNonCanonicalS(t *testing.T) {
|
||||
v := rfc9381TAI[0]
|
||||
pi := bytes.Clone(mustHex(v.pi))
|
||||
for i := 48; i < 80; i++ {
|
||||
pi[i] = 0xFF
|
||||
}
|
||||
|
||||
_, _, _, ok := decodeProof(pi)
|
||||
require.False(t, ok, "s >= q must not decode")
|
||||
|
||||
input := buildVerifyInput(mustHex(v.pk), mustHex(v.alpha), pi)
|
||||
out, _, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, out)
|
||||
}
|
||||
|
||||
func TestVerify_RejectsUndecodablePublicKey(t *testing.T) {
|
||||
v := rfc9381TAI[0]
|
||||
|
||||
keys := map[string]string{"not_on_curve": notOnCurve}
|
||||
for name, h := range nonCanonicalPoints {
|
||||
keys[name] = h
|
||||
}
|
||||
|
||||
for name, h := range keys {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
input := buildVerifyInput(mustHex(h), mustHex(v.alpha), mustHex(v.pi))
|
||||
out, _, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, out)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProofToHash_RequiresExactProofLength(t *testing.T) {
|
||||
pi := mustHex(rfc9381TAI[0].pi)
|
||||
for _, n := range []int{0, 1, ProofSize - 1, ProofSize + 1, 2 * ProofSize} {
|
||||
body := make([]byte, n)
|
||||
copy(body, pi)
|
||||
out, _, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, append([]byte{OpProofToHash}, body...), testGas, true)
|
||||
require.NoError(t, err)
|
||||
require.Nilf(t, out, "accepted a %d-byte proof", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProofToHash_RejectsUndecodableGamma(t *testing.T) {
|
||||
pi := bytes.Clone(mustHex(rfc9381TAI[0].pi))
|
||||
copy(pi[:32], mustHex("0200000000000000000000000000000000000000000000000000000000000000"))
|
||||
|
||||
out, _, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, append([]byte{OpProofToHash}, pi...), testGas, true)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, out)
|
||||
}
|
||||
|
||||
// ─── Round trip ───────────────────────────────────────────────────────────────
|
||||
|
||||
// TestProveAndVerify covers alpha_string lengths the published vectors do not
|
||||
// reach. The published vectors are what pin the construction; this pins that
|
||||
// nothing breaks as alpha grows.
|
||||
func TestProveAndVerify(t *testing.T) {
|
||||
sk := mustHex(rfc9381TAI[1].sk)
|
||||
_, Y := expandSecretKey(sk)
|
||||
|
||||
for _, alpha := range [][]byte{
|
||||
[]byte("ECVRF-EDWARDS25519-SHA512-TAI"),
|
||||
make([]byte, 256),
|
||||
bytes.Repeat([]byte{0xA5}, 4096),
|
||||
} {
|
||||
pi := testProve(sk, Y, alpha)
|
||||
require.Len(t, pi, ProofSize)
|
||||
|
||||
input := buildVerifyInput(Y.Bytes(), alpha, pi)
|
||||
beta, _, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, beta, BetaSize)
|
||||
|
||||
// proof_to_hash agrees with verify, and both are deterministic.
|
||||
p2h, _, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, append([]byte{OpProofToHash}, pi...), testGas, true)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, beta, p2h)
|
||||
|
||||
again, _, _ := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true)
|
||||
require.Equal(t, beta, again)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUniqueness: distinct keys over one alpha_string give distinct outputs.
|
||||
func TestUniqueness(t *testing.T) {
|
||||
sk1 := mustHex("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60")
|
||||
sk2 := mustHex("4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb")
|
||||
alpha := []byte("uniqueness test")
|
||||
alpha := []byte("uniqueness")
|
||||
seen := map[string]bool{}
|
||||
|
||||
_, Y1 := expandSecretKey(sk1)
|
||||
_, Y2 := expandSecretKey(sk2)
|
||||
for _, v := range rfc9381TAI {
|
||||
sk := mustHex(v.sk)
|
||||
_, Y := expandSecretKey(sk)
|
||||
pi := testProve(sk, Y, alpha)
|
||||
|
||||
pi1 := testProve(sk1, Y1, alpha)
|
||||
pi2 := testProve(sk2, Y2, alpha)
|
||||
|
||||
// Both must verify
|
||||
input1 := buildVerifyInput(Y1.Bytes(), alpha, pi1)
|
||||
result1, _, _ := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input1, testGas, true)
|
||||
if result1 == nil {
|
||||
t.Fatal("sk1 proof failed to verify")
|
||||
}
|
||||
|
||||
input2 := buildVerifyInput(Y2.Bytes(), alpha, pi2)
|
||||
result2, _, _ := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input2, testGas, true)
|
||||
if result2 == nil {
|
||||
t.Fatal("sk2 proof failed to verify")
|
||||
}
|
||||
|
||||
// Different keys must produce different betas
|
||||
if hex.EncodeToString(result1) == hex.EncodeToString(result2) {
|
||||
t.Error("different secret keys produced the same beta")
|
||||
input := buildVerifyInput(Y.Bytes(), alpha, pi)
|
||||
beta, _, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, beta)
|
||||
require.False(t, seen[hex.EncodeToString(beta)], "two keys produced one beta")
|
||||
seen[hex.EncodeToString(beta)] = true
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Truncated / Corrupted Proofs ─────────────────────────────────────────────
|
||||
|
||||
func TestCorruptedProof(t *testing.T) {
|
||||
sk := mustHex("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60")
|
||||
_, Y := expandSecretKey(sk)
|
||||
alpha := []byte("corruption test")
|
||||
pi := testProve(sk, Y, alpha)
|
||||
|
||||
// Flip a byte in the proof
|
||||
corrupted := make([]byte, len(pi))
|
||||
copy(corrupted, pi)
|
||||
corrupted[40] ^= 0xFF // corrupt the c scalar
|
||||
|
||||
input := buildVerifyInput(Y.Bytes(), alpha, corrupted)
|
||||
result, _, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true)
|
||||
// inPrimeOrderSubgroup reports whether q*P is the identity. Scalars reduce mod
|
||||
// q, so q itself is not representable; (q-1)*P + P is.
|
||||
func inPrimeOrderSubgroup(P *edwards.Point) bool {
|
||||
one, err := new(edwards.Scalar).SetCanonicalBytes(append([]byte{1}, make([]byte, 31)...))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
panic(err)
|
||||
}
|
||||
if result != nil {
|
||||
t.Error("corrupted proof should not verify")
|
||||
qMinus1 := new(edwards.Scalar).Negate(one)
|
||||
qP := new(edwards.Point).Add(new(edwards.Point).ScalarMult(qMinus1, P), P)
|
||||
return qP.Equal(edwards.NewIdentityPoint()) == 1
|
||||
}
|
||||
|
||||
// TestEncodeToCurve_IsInThePrimeOrderSubgroup: encode_to_curve multiplies by
|
||||
// the cofactor and rejects the identity, so H always lies in the group the
|
||||
// proof is checked in.
|
||||
func TestEncodeToCurve_IsInThePrimeOrderSubgroup(t *testing.T) {
|
||||
identity := edwards.NewIdentityPoint()
|
||||
|
||||
// The predicate has teeth: a torsion point fails it.
|
||||
for _, h := range smallOrderKeys[1:] {
|
||||
P, err := new(edwards.Point).SetBytes(mustHex(h))
|
||||
require.NoError(t, err)
|
||||
require.Falsef(t, inPrimeOrderSubgroup(P), "%s should not be in the prime-order subgroup", h)
|
||||
}
|
||||
|
||||
for i := range 64 {
|
||||
H := encodeToCurve([]byte{byte(i)}, []byte("subgroup"))
|
||||
require.NotNil(t, H)
|
||||
require.Equal(t, 0, H.Equal(identity), "encode_to_curve returned the identity")
|
||||
require.True(t, inPrimeOrderSubgroup(H))
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Gas Accounting ───────────────────────────────────────────────────────────
|
||||
// TestInterpretHashAsPoint_AdvancesTheCounterOnATorsionPoint pins the identity
|
||||
// rejection inside encode_to_curve. If a counter's octets decode to a point the
|
||||
// cofactor kills, H would be the identity and V = s*H would carry no binding to
|
||||
// alpha_string at all, so the counter has to advance instead.
|
||||
func TestInterpretHashAsPoint_AdvancesTheCounterOnATorsionPoint(t *testing.T) {
|
||||
hash := make([]byte, 64)
|
||||
|
||||
func TestGasAccountingVerify(t *testing.T) {
|
||||
sk := mustHex("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60")
|
||||
_, Y := expandSecretKey(sk)
|
||||
alpha := []byte{}
|
||||
pi := testProve(sk, Y, alpha)
|
||||
for _, h := range smallOrderKeys {
|
||||
copy(hash, mustHex(h))
|
||||
require.Nilf(t, interpretHashAsPoint(hash), "torsion point %s was used as H", h)
|
||||
}
|
||||
|
||||
input := buildVerifyInput(Y.Bytes(), alpha, pi)
|
||||
_, gas, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gas != testGas-GasVerify {
|
||||
t.Errorf("remaining gas = %d, want %d", gas, testGas-GasVerify)
|
||||
}
|
||||
copy(hash, mustHex(notOnCurve))
|
||||
require.Nil(t, interpretHashAsPoint(hash))
|
||||
|
||||
copy(hash, mustHex(nonCanonicalPoints["y_above_p"]))
|
||||
require.Nil(t, interpretHashAsPoint(hash))
|
||||
|
||||
// A hash that does decode gives a point in the prime-order subgroup.
|
||||
H := encodeToCurve(mustHex(rfc9381TAI[0].pk), nil)
|
||||
require.NotNil(t, H)
|
||||
require.True(t, inPrimeOrderSubgroup(H))
|
||||
}
|
||||
|
||||
func TestGasAccountingProofToHash(t *testing.T) {
|
||||
sk := mustHex("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60")
|
||||
_, Y := expandSecretKey(sk)
|
||||
pi := testProve(sk, Y, []byte{})
|
||||
// TestDecodeProof_RequiresExactLength: ECVRF_decode_proof splits pi_string at
|
||||
// fixed offsets, so a short or long buffer is refused rather than padded.
|
||||
func TestDecodeProof_RequiresExactLength(t *testing.T) {
|
||||
pi := mustHex(rfc9381TAI[0].pi)
|
||||
for _, n := range []int{0, ProofSize - 1, ProofSize + 1} {
|
||||
buf := make([]byte, n)
|
||||
copy(buf, pi)
|
||||
_, _, _, ok := decodeProof(buf)
|
||||
require.Falsef(t, ok, "decoded a %d-byte pi_string", n)
|
||||
}
|
||||
|
||||
input := append([]byte{OpProofToHash}, pi...)
|
||||
_, gas, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gas != testGas-GasProofToHash {
|
||||
t.Errorf("remaining gas = %d, want %d", gas, testGas-GasProofToHash)
|
||||
_, _, _, ok := decodeProof(pi)
|
||||
require.True(t, ok)
|
||||
}
|
||||
|
||||
// TestChallengeGeneration_AcceptsEveryTruncation: the challenge is 16 bytes
|
||||
// widened to a 32-byte scalar, which is below the group order for every
|
||||
// possible value, so the encoding check it goes through cannot reject one.
|
||||
func TestChallengeGeneration_AcceptsEveryTruncation(t *testing.T) {
|
||||
var maxTruncation [32]byte
|
||||
for i := range 16 {
|
||||
maxTruncation[i] = 0xFF
|
||||
}
|
||||
c, err := new(edwards.Scalar).SetCanonicalBytes(maxTruncation[:])
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, c)
|
||||
}
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func buildVerifyInput(pk, alpha, proof []byte) []byte {
|
||||
var alphaLen [2]byte
|
||||
var alphaLen [AlphaLenSize]byte
|
||||
binary.BigEndian.PutUint16(alphaLen[:], uint16(len(alpha)))
|
||||
|
||||
input := []byte{OpVerify}
|
||||
@@ -344,26 +585,24 @@ func mustHex(s string) []byte {
|
||||
return b
|
||||
}
|
||||
|
||||
// ─── Test-only VRF Prove (RFC 9381 section 5.1) ──────────────────────────────
|
||||
// ─── Test-only prover (RFC 9381 Section 5.1) ─────────────────────────────────
|
||||
|
||||
// expandSecretKey expands a 32-byte Ed25519 secret key into the scalar x
|
||||
// and public point Y = x*B, using the same derivation as Ed25519.
|
||||
// expandSecretKey derives (x, Y) from an Ed25519 seed exactly as RFC 9381
|
||||
// Section 5.5 specifies for this suite.
|
||||
func expandSecretKey(sk []byte) (*edwards.Scalar, *edwards.Point) {
|
||||
h := sha512.Sum512(sk)
|
||||
|
||||
// SetBytesWithClamping applies RFC 8032 clamping and reduces mod L.
|
||||
x, err := new(edwards.Scalar).SetBytesWithClamping(h[:32])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
Y := new(edwards.Point).ScalarBaseMult(x)
|
||||
return x, Y
|
||||
return x, new(edwards.Point).ScalarBaseMult(x)
|
||||
}
|
||||
|
||||
// testProve implements ECVRF_prove (RFC 9381 section 5.1) for testing.
|
||||
// testProve implements ECVRF_prove (RFC 9381 Section 5.1) with the nonce
|
||||
// generation of Section 5.4.2.2. TestRFC9381_Prove requires it to reproduce
|
||||
// the published pi_string, so it is an implementation of the RFC rather than a
|
||||
// mirror of the verifier.
|
||||
func testProve(sk []byte, Y *edwards.Point, alpha []byte) []byte {
|
||||
// Expand SK using Ed25519 key derivation.
|
||||
expanded := sha512.Sum512(sk)
|
||||
|
||||
x, err := new(edwards.Scalar).SetBytesWithClamping(expanded[:32])
|
||||
@@ -371,77 +610,53 @@ func testProve(sk []byte, Y *edwards.Point, alpha []byte) []byte {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
pkBytes := Y.Bytes()
|
||||
|
||||
// H = hash_to_curve(pk, alpha)
|
||||
H := hashToCurveELL2(pkBytes, alpha)
|
||||
H := encodeToCurve(Y.Bytes(), alpha)
|
||||
if H == nil {
|
||||
panic("hash_to_curve returned nil")
|
||||
panic("encode_to_curve found no point")
|
||||
}
|
||||
|
||||
// Gamma = x * H
|
||||
Gamma := new(edwards.Point).ScalarMult(x, H)
|
||||
|
||||
// Nonce generation (RFC 9381 section 5.4.2.1):
|
||||
// k_string = SHA-512(truncated_hashed_sk || encode(H))
|
||||
// truncated_hashed_sk = expanded[32:64]
|
||||
nonceHash := sha512.New()
|
||||
nonceHash.Write(expanded[32:64])
|
||||
nonceHash.Write(H.Bytes())
|
||||
nonceDigest := nonceHash.Sum(nil)
|
||||
|
||||
// Reduce to scalar (mod L)
|
||||
k, err := edwards.NewScalar().SetUniformBytes(nonceDigest)
|
||||
// k_string = Hash(truncated_hashed_sk_string || point_to_string(H))
|
||||
nonce := sha512.New()
|
||||
nonce.Write(expanded[32:64])
|
||||
nonce.Write(H.Bytes())
|
||||
k, err := edwards.NewScalar().SetUniformBytes(nonce.Sum(nil))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// U = k * B
|
||||
U := new(edwards.Point).ScalarBaseMult(k)
|
||||
|
||||
// V = k * H
|
||||
V := new(edwards.Point).ScalarMult(k, H)
|
||||
|
||||
// c = challenge_generation(Y, H, Gamma, U, V)
|
||||
c := challengeGeneration(Y, H, Gamma, U, V)
|
||||
|
||||
// s = k + c * x (mod L)
|
||||
cx := new(edwards.Scalar).Multiply(c, x)
|
||||
s := new(edwards.Scalar).Add(k, cx)
|
||||
s := new(edwards.Scalar).Add(k, new(edwards.Scalar).Multiply(c, x))
|
||||
|
||||
// Encode proof: Gamma(32) || c(16) || s(32)
|
||||
pi := make([]byte, ProofSize)
|
||||
copy(pi[:32], Gamma.Bytes())
|
||||
cBytes := c.Bytes()
|
||||
copy(pi[32:48], cBytes[:16]) // First 16 bytes of c
|
||||
copy(pi[32:48], cBytes[:16])
|
||||
copy(pi[48:80], s.Bytes())
|
||||
|
||||
return pi
|
||||
}
|
||||
|
||||
// ─── Benchmark ────────────────────────────────────────────────────────────────
|
||||
// ─── Benchmarks ───────────────────────────────────────────────────────────────
|
||||
|
||||
func BenchmarkVerify(b *testing.B) {
|
||||
sk := mustHex("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60")
|
||||
_, Y := expandSecretKey(sk)
|
||||
alpha := []byte{}
|
||||
pi := testProve(sk, Y, alpha)
|
||||
input := buildVerifyInput(Y.Bytes(), alpha, pi)
|
||||
v := rfc9381TAI[1]
|
||||
input := buildVerifyInput(mustHex(v.pk), mustHex(v.alpha), mustHex(v.pi))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true) //nolint:errcheck
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkProofToHash(b *testing.B) {
|
||||
sk := mustHex("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60")
|
||||
_, Y := expandSecretKey(sk)
|
||||
pi := testProve(sk, Y, []byte{})
|
||||
input := append([]byte{OpProofToHash}, pi...)
|
||||
input := append([]byte{OpProofToHash}, mustHex(rfc9381TAI[1].pi)...)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true) //nolint:errcheck
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package vrf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/precompile/modules"
|
||||
"github.com/luxfi/precompile/precompileconfig"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestModule_RegisteredAtTheAdvertisedAddress(t *testing.T) {
|
||||
m, ok := modules.GetPrecompileModuleByAddress(ContractAddress)
|
||||
require.True(t, ok, "vrf is not registered at %s", ContractAddress)
|
||||
require.Equal(t, ConfigKey, m.ConfigKey)
|
||||
require.Equal(t, VRFPrecompile, m.Contract)
|
||||
|
||||
byKey, ok := modules.GetPrecompileModule(ConfigKey)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, ContractAddress, byKey.Address)
|
||||
}
|
||||
|
||||
// TestModule_RegisteringTwiceIsRejected pins the guard that stops a second
|
||||
// module claiming this address or config key.
|
||||
func TestModule_RegisteringTwiceIsRejected(t *testing.T) {
|
||||
require.Error(t, modules.RegisterModule(Module))
|
||||
}
|
||||
|
||||
func TestConfig_MakeConfigMatchesKey(t *testing.T) {
|
||||
cfg := Module.Configurator.MakeConfig()
|
||||
require.IsType(t, &Config{}, cfg)
|
||||
require.Equal(t, ConfigKey, cfg.Key())
|
||||
}
|
||||
|
||||
func TestConfig_Timestamp(t *testing.T) {
|
||||
var cfg Config
|
||||
require.Nil(t, cfg.Timestamp())
|
||||
|
||||
at := uint64(1700000000)
|
||||
cfg.Upgrade.BlockTimestamp = &at
|
||||
require.NotNil(t, cfg.Timestamp())
|
||||
require.Equal(t, at, *cfg.Timestamp())
|
||||
}
|
||||
|
||||
func TestConfig_IsDisabled(t *testing.T) {
|
||||
var cfg Config
|
||||
require.False(t, cfg.IsDisabled())
|
||||
|
||||
cfg.Upgrade.Disable = true
|
||||
require.True(t, cfg.IsDisabled())
|
||||
}
|
||||
|
||||
func TestConfig_Equal(t *testing.T) {
|
||||
at, other := uint64(10), uint64(11)
|
||||
|
||||
a := &Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &at}}
|
||||
same := &Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &at}}
|
||||
later := &Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &other}}
|
||||
disabled := &Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &at, Disable: true}}
|
||||
|
||||
require.True(t, a.Equal(same))
|
||||
require.False(t, a.Equal(later))
|
||||
require.False(t, a.Equal(disabled))
|
||||
require.False(t, a.Equal(nil))
|
||||
require.False(t, a.Equal(otherConfig{}))
|
||||
}
|
||||
|
||||
// otherConfig is a Config of a different concrete type, which Equal must
|
||||
// refuse rather than treat as a match.
|
||||
type otherConfig struct{ precompileconfig.Config }
|
||||
|
||||
func (otherConfig) Key() string { return "not-vrf" }
|
||||
|
||||
func TestConfig_VerifyAndConfigureAreTotal(t *testing.T) {
|
||||
cfg := &Config{}
|
||||
require.NoError(t, cfg.Verify(nil))
|
||||
require.NoError(t, Module.Configurator.Configure(nil, cfg, nil, nil))
|
||||
}
|
||||
|
||||
func TestConfig_JSONRoundTrip(t *testing.T) {
|
||||
at := uint64(42)
|
||||
in := &Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &at}}
|
||||
|
||||
raw, err := json.Marshal(in)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := Module.Configurator.MakeConfig()
|
||||
require.NoError(t, json.Unmarshal(raw, out))
|
||||
require.True(t, in.Equal(out))
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package vrf
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
edwards "filippo.io/edwards25519"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// RFC 9381 Appendix B.3 -- ECVRF-EDWARDS25519-SHA512-TAI.
|
||||
//
|
||||
// These are the only oracle in this package that does not come from the
|
||||
// implementation itself: a prove/verify round trip agrees with itself even
|
||||
// when every domain separator, the suite octet and the point encoding are all
|
||||
// wrong together. Each field below is transcribed from the RFC.
|
||||
var rfc9381TAI = []struct {
|
||||
name string
|
||||
sk string // secret key (Ed25519 seed)
|
||||
pk string // public key, point_to_string(Y)
|
||||
alpha string // alpha_string
|
||||
h string // ECVRF_encode_to_curve(PK, alpha)
|
||||
pi string // pi_string
|
||||
beta string // beta_string
|
||||
}{
|
||||
{
|
||||
name: "example16_empty_alpha",
|
||||
sk: "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60",
|
||||
pk: "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",
|
||||
alpha: "",
|
||||
h: "91bbed02a99461df1ad4c6564a5f5d829d0b90cfc7903e7a5797bd658abf3318",
|
||||
pi: "8657106690b5526245a92b003bb079ccd1a92130477671f6fc01ad16f26f723f26f8a57ccaed74ee1b190bed1f479d9727d2d0f9b005a6e456a35d4fb0daab1268a1b0db10836d9826a528ca76567805",
|
||||
beta: "90cf1df3b703cce59e2a35b925d411164068269d7b2d29f3301c03dd757876ff66b71dda49d2de59d03450451af026798e8f81cd2e333de5cdf4f3e140fdd8ae",
|
||||
},
|
||||
{
|
||||
name: "example17_one_byte_alpha",
|
||||
sk: "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb",
|
||||
pk: "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c",
|
||||
alpha: "72",
|
||||
h: "5b659fc3d4e9263fd9a4ed1d022d75eaacc20df5e09f9ea937502396598dc551",
|
||||
pi: "f3141cd382dc42909d19ec5110469e4feae18300e94f304590abdced48aed5933bf0864a62558b3ed7f2fea45c92a465301b3bbf5e3e54ddf2d935be3b67926da3ef39226bbc355bdc9850112c8f4b02",
|
||||
beta: "eb4440665d3891d668e7e0fcaf587f1b4bd7fbfe99d0eb2211ccec90496310eb5e33821bc613efb94db5e5b54c70a848a0bef4553a41befc57663b56373a5031",
|
||||
},
|
||||
{
|
||||
name: "example18_two_byte_alpha",
|
||||
sk: "c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7",
|
||||
pk: "fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025",
|
||||
alpha: "af82",
|
||||
h: "bf4339376f5542811de615e3313d2b36f6f53c0acfebb482159711201192576a",
|
||||
pi: "9bc0f79119cc5604bf02d23b4caede71393cedfbb191434dd016d30177ccbf8096bb474e53895c362d8628ee9f9ea3c0e52c7a5c691b6c18c9979866568add7a2d41b00b05081ed0f58ee5e31b3a970e",
|
||||
beta: "645427e5d00c62a23fb703732fa5d892940935942101e456ecca7bb217c61c452118fec1219202a0edcf038bb6373241578be7217ba85a2687f7a0310b2df19f",
|
||||
},
|
||||
}
|
||||
|
||||
// TestRFC9381_Verify runs each published proof through the precompile and
|
||||
// requires the published beta_string back.
|
||||
func TestRFC9381_Verify(t *testing.T) {
|
||||
for _, v := range rfc9381TAI {
|
||||
t.Run(v.name, func(t *testing.T) {
|
||||
input := buildVerifyInput(mustHex(v.pk), mustHex(v.alpha), mustHex(v.pi))
|
||||
got, _, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, v.beta, hex.EncodeToString(got))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRFC9381_ProofToHash pins ECVRF_proof_to_hash on its own, so a break in
|
||||
// the cofactor multiplication or the 0x03 domain separator is attributable.
|
||||
func TestRFC9381_ProofToHash(t *testing.T) {
|
||||
for _, v := range rfc9381TAI {
|
||||
t.Run(v.name, func(t *testing.T) {
|
||||
input := append([]byte{OpProofToHash}, mustHex(v.pi)...)
|
||||
got, _, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, v.beta, hex.EncodeToString(got))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRFC9381_EncodeToCurve pins ECVRF_encode_to_curve. This is the value the
|
||||
// suite octet, both domain separators, the sign bit of the candidate y and the
|
||||
// cofactor multiplication all feed into, so it localises a break that
|
||||
// TestRFC9381_Verify would only report as "invalid".
|
||||
func TestRFC9381_EncodeToCurve(t *testing.T) {
|
||||
for _, v := range rfc9381TAI {
|
||||
t.Run(v.name, func(t *testing.T) {
|
||||
H := encodeToCurve(mustHex(v.pk), mustHex(v.alpha))
|
||||
require.NotNil(t, H)
|
||||
require.Equal(t, v.h, hex.EncodeToString(H.Bytes()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRFC9381_Prove requires the test prover to reproduce the published
|
||||
// pi_string bit for bit. The prover shares encodeToCurve and
|
||||
// challengeGeneration with the verifier, so this pins the challenge
|
||||
// derivation and the proof encoding as well.
|
||||
func TestRFC9381_Prove(t *testing.T) {
|
||||
for _, v := range rfc9381TAI {
|
||||
t.Run(v.name, func(t *testing.T) {
|
||||
Y, err := new(edwards.Point).SetBytes(mustHex(v.pk))
|
||||
require.NoError(t, err)
|
||||
|
||||
_, derived := expandSecretKey(mustHex(v.sk))
|
||||
require.Equal(t, v.pk, hex.EncodeToString(derived.Bytes()),
|
||||
"public key derived from SK must match the published PK")
|
||||
|
||||
pi := testProve(mustHex(v.sk), Y, mustHex(v.alpha))
|
||||
require.Equal(t, v.pi, hex.EncodeToString(pi))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRFC9381_ProofIsBoundToAlpha flips one bit of alpha_string and requires
|
||||
// the published proof to stop verifying. A verifier that ignored alpha would
|
||||
// pass every test above.
|
||||
func TestRFC9381_ProofIsBoundToAlpha(t *testing.T) {
|
||||
for _, v := range rfc9381TAI {
|
||||
t.Run(v.name, func(t *testing.T) {
|
||||
alpha := append(mustHex(v.alpha), 0x00)
|
||||
input := buildVerifyInput(mustHex(v.pk), alpha, mustHex(v.pi))
|
||||
got, _, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRFC9381_ProofIsBoundToKey verifies each proof against the other two
|
||||
// published public keys and requires refusal.
|
||||
func TestRFC9381_ProofIsBoundToKey(t *testing.T) {
|
||||
for i, v := range rfc9381TAI {
|
||||
for j, other := range rfc9381TAI {
|
||||
if i == j {
|
||||
continue
|
||||
}
|
||||
t.Run(v.name+"_under_"+other.name, func(t *testing.T) {
|
||||
input := buildVerifyInput(mustHex(other.pk), mustHex(v.alpha), mustHex(v.pi))
|
||||
got, _, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRFC9381_EveryProofBitIsLoadBearing flips one bit in every byte of each
|
||||
// published proof and requires all 80 mutants to be refused. A verifier that
|
||||
// skipped the c == c' comparison, or that only compared a prefix of the
|
||||
// scalars, survives a single hand-picked corruption but not this.
|
||||
func TestRFC9381_EveryProofBitIsLoadBearing(t *testing.T) {
|
||||
for _, v := range rfc9381TAI {
|
||||
t.Run(v.name, func(t *testing.T) {
|
||||
pi := mustHex(v.pi)
|
||||
for i := range pi {
|
||||
mutant := make([]byte, len(pi))
|
||||
copy(mutant, pi)
|
||||
mutant[i] ^= 0x01
|
||||
|
||||
input := buildVerifyInput(mustHex(v.pk), mustHex(v.alpha), mutant)
|
||||
got, _, err := VRFPrecompile.Run(nil, ContractAddress, ContractAddress, input, testGas, true)
|
||||
require.NoError(t, err)
|
||||
require.Nilf(t, got, "flipping bit 0 of proof byte %d still verified", i)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user