mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
zkvm: a point at infinity is not a point this verifier will read
Three of the four verifier failures were one mistake: BigInt(nil) assigns through a nil receiver. msmCPU panicked on every call, the Groth16 linear combination on every proof carrying a public input, and PLONK unconditionally. Removing the PLONK panic uncovered what it had been hiding. gnark decodes all-zero bytes to the point at infinity without error, reports it as in the prime-order subgroup, and a pairing check drops every term whose argument is infinity — so an element at infinity removes itself from the equation, and a check with every term removed is a product of nothing, which is one, which is true. loadVerifyingKeys installs an all-zero placeholder for each circuit an operator did not key. So on a chain keyed for transfers, a shield proof was judged against zeros and every one of them verified. Mint from nothing. checkG1 and checkG2 are now the one place that decides what a usable point is — in the subgroup and not the identity — and every decoder routes through them, so a decoded proof carries the invariant instead of each caller re-deriving it. The precompile had the same hole from the other direction: it takes its verifying key from calldata, so the caller chose it, and a degenerate key made Run answer 0x01. The witness bound was off by one. K holds a point per public input plus the constant term, so it covers len(K)-1 inputs, and a peer-chosen count of exactly len(K) read one past the end — a remote kill from a well-formed message. It was off by one in two places; the second was a duplicate of the whole pairing check on a GPU path that also overwrote the CPU's linear combination with an unverified GPU result. A GPU returning a wrong point flips a verdict on the nodes that have one, which is a fork rather than a slow node, so that copy is gone and one deterministic check serves both the single and batch paths. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
+7
-79
@@ -6,6 +6,7 @@ package zkvm
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/consensys/gnark-crypto/ecc/bn254"
|
||||
"github.com/consensys/gnark-crypto/ecc/bn254/fr"
|
||||
@@ -108,9 +109,10 @@ func msmWithSession(session *accel.Session, scalars []fr.Element, bases []bn254.
|
||||
// msmCPU computes MSM sequentially on CPU.
|
||||
func msmCPU(scalars []fr.Element, bases []bn254.G1Affine) bn254.G1Affine {
|
||||
var result bn254.G1Affine
|
||||
for i, s := range scalars {
|
||||
var scalar big.Int
|
||||
for i := range scalars {
|
||||
var term bn254.G1Affine
|
||||
term.ScalarMultiplication(&bases[i], s.BigInt(nil))
|
||||
term.ScalarMultiplication(&bases[i], scalars[i].BigInt(&scalar))
|
||||
result.Add(&result, &term)
|
||||
}
|
||||
return result
|
||||
@@ -175,16 +177,6 @@ func batchVerifyProofsGPU(pv *ProofVerifier, txs []*Transaction) []error {
|
||||
continue
|
||||
}
|
||||
|
||||
// Subgroup checks on proof points
|
||||
if !grothProof.Ar.IsInSubGroup() || !grothProof.Krs.IsInSubGroup() {
|
||||
results[i] = errors.New("zkvm: Groth16 proof G1 point not in prime-order subgroup")
|
||||
continue
|
||||
}
|
||||
if !grothProof.Bs.IsInSubGroup() {
|
||||
results[i] = errors.New("zkvm: Groth16 proof G2 point not in prime-order subgroup")
|
||||
continue
|
||||
}
|
||||
|
||||
vk, err := deserializeVerifyingKey(vkBytes)
|
||||
if err != nil {
|
||||
results[i] = fmt.Errorf("deserialize vk: %w", err)
|
||||
@@ -206,78 +198,14 @@ func batchVerifyProofsGPU(pv *ProofVerifier, txs []*Transaction) []error {
|
||||
batch = append(batch, batchEntry{index: i, proof: grothProof, vk: vk, wit: witness})
|
||||
}
|
||||
|
||||
// If no GPU or only 1 proof, verify sequentially
|
||||
if len(batch) <= 1 || !accel.Available() {
|
||||
for _, e := range batch {
|
||||
results[e.index] = verifyGroth16Pairing(e.proof, e.vk, e.wit)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// GPU batch path: accelerate MSM per proof, verify pairings
|
||||
// One pairing check serves both doors: a block of one transaction and a
|
||||
// block of many must reach the same verdict on the same proof.
|
||||
for _, e := range batch {
|
||||
results[e.index] = verifyGroth16PairingGPU(e.proof, e.vk, e.wit, pv.log)
|
||||
results[e.index] = verifyGroth16Pairing(e.proof, e.vk, e.wit)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// verifyGroth16PairingGPU is identical to verifyGroth16Pairing but uses GPU MSM
|
||||
// for the public input linear combination step.
|
||||
func verifyGroth16PairingGPU(proof *Groth16Proof, vk *Groth16VerifyingKey, witness []fr.Element, logger log.Logger) error {
|
||||
if len(witness) > len(vk.K) {
|
||||
return errors.New("too many public inputs")
|
||||
}
|
||||
|
||||
// GPU-accelerated MSM for public input LC: K[0] + sum(witness_i * K[i+1])
|
||||
// Build scalars=[1, w0, w1, ...] and bases=[K[0], K[1], K[2], ...]
|
||||
scalars := make([]fr.Element, len(witness)+1)
|
||||
bases := make([]bn254.G1Affine, len(witness)+1)
|
||||
|
||||
scalars[0].SetOne()
|
||||
bases[0].Set(&vk.K[0])
|
||||
for i, w := range witness {
|
||||
scalars[i+1].Set(&w)
|
||||
bases[i+1].Set(&vk.K[i+1])
|
||||
}
|
||||
|
||||
publicInputLC, err := msmGPU(scalars, bases, logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GPU MSM failed: %w", err)
|
||||
}
|
||||
|
||||
// Pairing check (same as CPU path)
|
||||
leftSide, err := bn254.Pair([]bn254.G1Affine{proof.Ar}, []bn254.G2Affine{proof.Bs})
|
||||
if err != nil {
|
||||
return fmt.Errorf("pairing A*B failed: %w", err)
|
||||
}
|
||||
|
||||
alphaBeta, err := bn254.Pair([]bn254.G1Affine{vk.Alpha}, []bn254.G2Affine{vk.Beta})
|
||||
if err != nil {
|
||||
return fmt.Errorf("pairing alpha*beta failed: %w", err)
|
||||
}
|
||||
|
||||
pubGamma, err := bn254.Pair([]bn254.G1Affine{publicInputLC}, []bn254.G2Affine{vk.Gamma})
|
||||
if err != nil {
|
||||
return fmt.Errorf("pairing pubInput*gamma failed: %w", err)
|
||||
}
|
||||
|
||||
cDelta, err := bn254.Pair([]bn254.G1Affine{proof.Krs}, []bn254.G2Affine{vk.Delta})
|
||||
if err != nil {
|
||||
return fmt.Errorf("pairing C*delta failed: %w", err)
|
||||
}
|
||||
|
||||
var rightSide bn254.GT
|
||||
rightSide.Set(&alphaBeta)
|
||||
rightSide.Mul(&rightSide, &pubGamma)
|
||||
rightSide.Mul(&rightSide, &cDelta)
|
||||
|
||||
if !leftSide.Equal(&rightSide) {
|
||||
return errors.New("pairing check failed: proof is invalid")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// poseidonHashGPU computes Poseidon hash of inputs using GPU acceleration.
|
||||
// Falls back to SHA-256 if GPU is unavailable.
|
||||
func poseidonHashGPU(inputs [][]byte) ([]byte, error) {
|
||||
|
||||
@@ -40,6 +40,7 @@ var (
|
||||
errInputTooShort = errors.New("input too short")
|
||||
errInvalidPoint = errors.New("invalid elliptic curve point")
|
||||
errSubgroupCheck = errors.New("point not in correct subgroup")
|
||||
errPointAtInfinity = errors.New("point at infinity")
|
||||
errPairingFailed = errors.New("pairing computation failed")
|
||||
errProofInvalid = errors.New("proof verification failed")
|
||||
errNotImplemented = errors.New("verifier not yet available")
|
||||
@@ -321,6 +322,32 @@ type groth16VK struct {
|
||||
K []bn254.G1Affine
|
||||
}
|
||||
|
||||
// A pairing drops every term whose argument is the point at infinity, so a
|
||||
// key or proof element at infinity removes itself from the equation: a key of
|
||||
// all-infinity points turns the check into 1 == 1 and reports any proof valid.
|
||||
// gnark decodes all-zero bytes to infinity and calls it in-subgroup, so the
|
||||
// subgroup test alone does not catch it. checkG1 and checkG2 are the one place
|
||||
// that decides what a usable point is.
|
||||
func checkG1(p *bn254.G1Affine) error {
|
||||
if !p.IsInSubGroup() {
|
||||
return errSubgroupCheck
|
||||
}
|
||||
if p.IsInfinity() {
|
||||
return errPointAtInfinity
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkG2(p *bn254.G2Affine) error {
|
||||
if !p.IsInSubGroup() {
|
||||
return errSubgroupCheck
|
||||
}
|
||||
if p.IsInfinity() {
|
||||
return errPointAtInfinity
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseGroth16Proof(data []byte) (*groth16Proof, error) {
|
||||
if len(data) < 256 {
|
||||
return nil, errInputTooShort
|
||||
@@ -329,20 +356,20 @@ func parseGroth16Proof(data []byte) (*groth16Proof, error) {
|
||||
if err := p.Ar.Unmarshal(data[0:64]); err != nil {
|
||||
return nil, fmt.Errorf("Ar: %w", err)
|
||||
}
|
||||
if !p.Ar.IsInSubGroup() {
|
||||
return nil, errSubgroupCheck
|
||||
if err := checkG1(&p.Ar); err != nil {
|
||||
return nil, fmt.Errorf("Ar: %w", err)
|
||||
}
|
||||
if err := p.Bs.Unmarshal(data[64:192]); err != nil {
|
||||
return nil, fmt.Errorf("Bs: %w", err)
|
||||
}
|
||||
if !p.Bs.IsInSubGroup() {
|
||||
return nil, errSubgroupCheck
|
||||
if err := checkG2(&p.Bs); err != nil {
|
||||
return nil, fmt.Errorf("Bs: %w", err)
|
||||
}
|
||||
if err := p.Krs.Unmarshal(data[192:256]); err != nil {
|
||||
return nil, fmt.Errorf("Krs: %w", err)
|
||||
}
|
||||
if !p.Krs.IsInSubGroup() {
|
||||
return nil, errSubgroupCheck
|
||||
if err := checkG1(&p.Krs); err != nil {
|
||||
return nil, fmt.Errorf("Krs: %w", err)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
@@ -359,32 +386,32 @@ func parseVerifyingKey(data []byte) (*groth16VK, error) {
|
||||
if err := vk.Alpha.Unmarshal(data[off : off+64]); err != nil {
|
||||
return nil, fmt.Errorf("Alpha: %w", err)
|
||||
}
|
||||
if !vk.Alpha.IsInSubGroup() {
|
||||
return nil, fmt.Errorf("Alpha: %w", errSubgroupCheck)
|
||||
if err := checkG1(&vk.Alpha); err != nil {
|
||||
return nil, fmt.Errorf("Alpha: %w", err)
|
||||
}
|
||||
off += 64
|
||||
|
||||
if err := vk.Beta.Unmarshal(data[off : off+128]); err != nil {
|
||||
return nil, fmt.Errorf("Beta: %w", err)
|
||||
}
|
||||
if !vk.Beta.IsInSubGroup() {
|
||||
return nil, fmt.Errorf("Beta: %w", errSubgroupCheck)
|
||||
if err := checkG2(&vk.Beta); err != nil {
|
||||
return nil, fmt.Errorf("Beta: %w", err)
|
||||
}
|
||||
off += 128
|
||||
|
||||
if err := vk.Gamma.Unmarshal(data[off : off+128]); err != nil {
|
||||
return nil, fmt.Errorf("Gamma: %w", err)
|
||||
}
|
||||
if !vk.Gamma.IsInSubGroup() {
|
||||
return nil, fmt.Errorf("Gamma: %w", errSubgroupCheck)
|
||||
if err := checkG2(&vk.Gamma); err != nil {
|
||||
return nil, fmt.Errorf("Gamma: %w", err)
|
||||
}
|
||||
off += 128
|
||||
|
||||
if err := vk.Delta.Unmarshal(data[off : off+128]); err != nil {
|
||||
return nil, fmt.Errorf("Delta: %w", err)
|
||||
}
|
||||
if !vk.Delta.IsInSubGroup() {
|
||||
return nil, fmt.Errorf("Delta: %w", errSubgroupCheck)
|
||||
if err := checkG2(&vk.Delta); err != nil {
|
||||
return nil, fmt.Errorf("Delta: %w", err)
|
||||
}
|
||||
off += 128
|
||||
|
||||
@@ -400,8 +427,8 @@ func parseVerifyingKey(data []byte) (*groth16VK, error) {
|
||||
if err := vk.K[i].Unmarshal(data[off : off+64]); err != nil {
|
||||
return nil, fmt.Errorf("K[%d]: %w", i, err)
|
||||
}
|
||||
if !vk.K[i].IsInSubGroup() {
|
||||
return nil, fmt.Errorf("K[%d]: %w", i, errSubgroupCheck)
|
||||
if err := checkG1(&vk.K[i]); err != nil {
|
||||
return nil, fmt.Errorf("K[%d]: %w", i, err)
|
||||
}
|
||||
off += 64
|
||||
}
|
||||
|
||||
@@ -182,6 +182,44 @@ func TestGroth16VerifierRejectsInvalidProof(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroth16VerifierRefusesADegenerateKey. The verifying key arrives in the
|
||||
// precompile's own input, so a caller picks it. A pairing skips every term
|
||||
// whose argument is the point at infinity, and gnark decodes all-zero bytes to
|
||||
// infinity and calls it in-subgroup — so a key of infinity points leaves the
|
||||
// multi-pairing empty, which equals one, which reads as "valid" for a proof
|
||||
// nobody checked. Such a key must be refused at parse.
|
||||
func TestGroth16VerifierRefusesADegenerateKey(t *testing.T) {
|
||||
_, _, g1, g2 := bn254.Generators()
|
||||
|
||||
// Alpha, Beta, Gamma, Delta at infinity; K holds real points so the
|
||||
// public-input bound does not fire first.
|
||||
vk := &groth16VK{K: []bn254.G1Affine{g1, g1}}
|
||||
|
||||
var w fr.Element
|
||||
w.SetRandom()
|
||||
|
||||
// Ar at infinity makes the one surviving term vanish as well.
|
||||
proof := &groth16Proof{Bs: g2, Krs: g1}
|
||||
|
||||
input := serializeGroth16Input(vk, proof, []fr.Element{w})
|
||||
|
||||
v := &Groth16Verifier{}
|
||||
res, err := v.Run(input)
|
||||
if err == nil && len(res) == 1 && res[0] == 0x01 {
|
||||
t.Fatal("Groth16Verifier accepted a proof against a key of infinity points")
|
||||
}
|
||||
if len(res) != 1 || res[0] != 0x00 {
|
||||
t.Fatalf("expected 0x00 for a degenerate key, got res=%x err=%v", res, err)
|
||||
}
|
||||
|
||||
if _, err := parseVerifyingKey(input[4 : 4+binary.BigEndian.Uint32(input[:4])]); err == nil {
|
||||
t.Fatal("parseVerifyingKey accepted a key of infinity points")
|
||||
}
|
||||
if _, err := parseGroth16Proof(make([]byte, 256)); err == nil {
|
||||
t.Fatal("parseGroth16Proof accepted a proof of infinity points")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPLONKVerifierGas(t *testing.T) {
|
||||
v := &PLONKVerifier{}
|
||||
if g := v.RequiredGas(nil); g != plonkGas {
|
||||
|
||||
+94
-74
@@ -10,6 +10,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"sync"
|
||||
|
||||
"github.com/luxfi/accel"
|
||||
@@ -476,14 +477,6 @@ func (pv *ProofVerifier) verifyGroth16WithGnark(proof *ZKProof, vkBytes []byte)
|
||||
return fmt.Errorf("failed to deserialize proof: %w", err)
|
||||
}
|
||||
|
||||
// Subgroup checks on proof points — prevents small-subgroup attacks
|
||||
if !grothProof.Ar.IsInSubGroup() || !grothProof.Krs.IsInSubGroup() {
|
||||
return errors.New("zkvm: Groth16 proof G1 point not in prime-order subgroup")
|
||||
}
|
||||
if !grothProof.Bs.IsInSubGroup() {
|
||||
return errors.New("zkvm: Groth16 proof G2 point not in prime-order subgroup")
|
||||
}
|
||||
|
||||
// Deserialize public witness (public inputs)
|
||||
witness := make([]fr.Element, 0, len(proof.PublicInputs))
|
||||
for _, inputBytes := range proof.PublicInputs {
|
||||
@@ -502,38 +495,28 @@ func (pv *ProofVerifier) verifyGroth16WithGnark(proof *ZKProof, vkBytes []byte)
|
||||
|
||||
// verifyGroth16Pairing performs the Groth16 pairing check
|
||||
// Verifies: e(A, B) = e(alpha, beta) * e(sum(pubInput_i * K_i), gamma) * e(C, delta)
|
||||
// Uses GPU MSM for the public input linear combination when available.
|
||||
//
|
||||
// K carries one point per public input plus the constant term K[0], so a key
|
||||
// of n points speaks about n-1 public inputs. The witness length arrives with
|
||||
// the transaction and a peer chooses it, so it is bounded against the key
|
||||
// before any K point is read.
|
||||
func verifyGroth16Pairing(proof *Groth16Proof, vk *Groth16VerifyingKey, witness []fr.Element) error {
|
||||
if len(witness) > len(vk.K) {
|
||||
return errors.New("too many public inputs")
|
||||
if len(witness)+1 > len(vk.K) {
|
||||
return fmt.Errorf("public inputs: %d supplied, verifying key holds %d K points",
|
||||
len(witness), len(vk.K))
|
||||
}
|
||||
|
||||
// Compute public input linear combination: K[0] + sum(witness_i * K[i+1])
|
||||
// GPU MSM path when available and enough inputs to justify overhead
|
||||
// Public input linear combination: K[0] + sum(witness_i * K[i+1]).
|
||||
// Every node has to reach the same point from the same key and witness,
|
||||
// so the sum stays on the CPU rather than being handed to an accelerator
|
||||
// whose answer is never compared against it.
|
||||
var publicInputLC bn254.G1Affine
|
||||
if accel.Available() && len(witness) > 2 {
|
||||
scalars := make([]fr.Element, len(witness)+1)
|
||||
bases := make([]bn254.G1Affine, len(witness)+1)
|
||||
scalars[0].SetOne()
|
||||
bases[0].Set(&vk.K[0])
|
||||
for i, w := range witness {
|
||||
scalars[i+1].Set(&w)
|
||||
bases[i+1].Set(&vk.K[i+1])
|
||||
}
|
||||
publicInputLC = msmCPU(scalars, bases) // msmGPU needs logger; use CPU MSM helper
|
||||
// For inline GPU without logger, try session directly
|
||||
if session, err := accel.DefaultSession(); err == nil {
|
||||
if r, err := msmWithSession(session, scalars, bases); err == nil {
|
||||
publicInputLC = r
|
||||
}
|
||||
}
|
||||
} else {
|
||||
publicInputLC.Set(&vk.K[0])
|
||||
for i, w := range witness {
|
||||
var term bn254.G1Affine
|
||||
term.ScalarMultiplication(&vk.K[i+1], w.BigInt(nil))
|
||||
publicInputLC.Add(&publicInputLC, &term)
|
||||
}
|
||||
publicInputLC.Set(&vk.K[0])
|
||||
var scalar big.Int
|
||||
for i := range witness {
|
||||
var term bn254.G1Affine
|
||||
term.ScalarMultiplication(&vk.K[i+1], witness[i].BigInt(&scalar))
|
||||
publicInputLC.Add(&publicInputLC, &term)
|
||||
}
|
||||
|
||||
// Pairing check: e(A, B) == e(alpha, beta) * e(publicInputLC, gamma) * e(C, delta)
|
||||
@@ -569,40 +552,67 @@ func verifyGroth16Pairing(proof *Groth16Proof, vk *Groth16VerifyingKey, witness
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateVerifyingKey performs subgroup checks on verifying key elliptic curve points
|
||||
// This is CRITICAL for trusted setup validation - ensures points are in correct subgroup
|
||||
// A point this verifier reads has to be in the prime-order subgroup and must
|
||||
// not be the point at infinity. gnark encodes infinity as all-zero bytes and
|
||||
// reports it as in-subgroup, and a pairing drops any term whose argument is
|
||||
// infinity — so an element at infinity silently removes itself from the
|
||||
// equation and leaves a weaker check than the one written down. checkG1 and
|
||||
// checkG2 are the one place that decides what a usable point is; every
|
||||
// decoder below routes through them.
|
||||
var (
|
||||
errOffSubgroup = errors.New("point not in the prime-order subgroup")
|
||||
errAtInfinity = errors.New("point at infinity")
|
||||
)
|
||||
|
||||
func checkG1(p *bn254.G1Affine) error {
|
||||
if !p.IsInSubGroup() {
|
||||
return errOffSubgroup
|
||||
}
|
||||
if p.IsInfinity() {
|
||||
return errAtInfinity
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkG2(p *bn254.G2Affine) error {
|
||||
if !p.IsInSubGroup() {
|
||||
return errOffSubgroup
|
||||
}
|
||||
if p.IsInfinity() {
|
||||
return errAtInfinity
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateVerifyingKey checks every point of a Groth16 verifying key.
|
||||
// A trusted setup never produces infinity for alpha, beta, gamma, delta or K,
|
||||
// so a key that carries one is not a setup output and its pairing equation
|
||||
// would collapse to something weaker.
|
||||
func validateVerifyingKey(vk *Groth16VerifyingKey) error {
|
||||
// Validate Alpha is in G1 subgroup
|
||||
if !vk.Alpha.IsInSubGroup() {
|
||||
return errors.New("Alpha point not in G1 subgroup")
|
||||
if err := checkG1(&vk.Alpha); err != nil {
|
||||
return fmt.Errorf("Alpha: %w", err)
|
||||
}
|
||||
|
||||
// Validate Beta is in G2 subgroup
|
||||
if !vk.Beta.IsInSubGroup() {
|
||||
return errors.New("Beta point not in G2 subgroup")
|
||||
if err := checkG2(&vk.Beta); err != nil {
|
||||
return fmt.Errorf("Beta: %w", err)
|
||||
}
|
||||
|
||||
// Validate Gamma is in G2 subgroup
|
||||
if !vk.Gamma.IsInSubGroup() {
|
||||
return errors.New("Gamma point not in G2 subgroup")
|
||||
if err := checkG2(&vk.Gamma); err != nil {
|
||||
return fmt.Errorf("Gamma: %w", err)
|
||||
}
|
||||
|
||||
// Validate Delta is in G2 subgroup
|
||||
if !vk.Delta.IsInSubGroup() {
|
||||
return errors.New("Delta point not in G2 subgroup")
|
||||
if err := checkG2(&vk.Delta); err != nil {
|
||||
return fmt.Errorf("Delta: %w", err)
|
||||
}
|
||||
|
||||
// Validate all K points are in G1 subgroup
|
||||
for i := range vk.K {
|
||||
if !vk.K[i].IsInSubGroup() {
|
||||
return fmt.Errorf("K[%d] point not in G1 subgroup", i)
|
||||
if err := checkG1(&vk.K[i]); err != nil {
|
||||
return fmt.Errorf("K[%d]: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deserializeGroth16Proof deserializes a Groth16 proof from bytes
|
||||
// deserializeGroth16Proof deserializes a Groth16 proof from bytes.
|
||||
// A proof it returns has already passed the point checks, so callers never
|
||||
// repeat them.
|
||||
func deserializeGroth16Proof(data []byte) (*Groth16Proof, error) {
|
||||
// Expected format: Ar (64 bytes) | Bs (128 bytes) | Krs (64 bytes) = 256 bytes
|
||||
if len(data) < 256 {
|
||||
@@ -616,18 +626,27 @@ func deserializeGroth16Proof(data []byte) (*Groth16Proof, error) {
|
||||
if err := proof.Ar.Unmarshal(data[offset : offset+64]); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal Ar: %w", err)
|
||||
}
|
||||
if err := checkG1(&proof.Ar); err != nil {
|
||||
return nil, fmt.Errorf("Ar: %w", err)
|
||||
}
|
||||
offset += 64
|
||||
|
||||
// Deserialize Bs (G2 point, 128 bytes compressed)
|
||||
if err := proof.Bs.Unmarshal(data[offset : offset+128]); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal Bs: %w", err)
|
||||
}
|
||||
if err := checkG2(&proof.Bs); err != nil {
|
||||
return nil, fmt.Errorf("Bs: %w", err)
|
||||
}
|
||||
offset += 128
|
||||
|
||||
// Deserialize Krs (G1 point, 64 bytes compressed)
|
||||
if err := proof.Krs.Unmarshal(data[offset : offset+64]); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal Krs: %w", err)
|
||||
}
|
||||
if err := checkG1(&proof.Krs); err != nil {
|
||||
return nil, fmt.Errorf("Krs: %w", err)
|
||||
}
|
||||
|
||||
return proof, nil
|
||||
}
|
||||
@@ -836,9 +855,10 @@ func verifyPLONKPairing(proof *PLONKProof, vk *PLONKVerifyingKey, publicInputs [
|
||||
u.SetBytes(uBytes[:32])
|
||||
|
||||
// Compute: [W_z]_1 + u·[W_{zw}]_1
|
||||
var scalar big.Int
|
||||
var leftG1 bn254.G1Affine
|
||||
var uWzw bn254.G1Affine
|
||||
uWzw.ScalarMultiplication(&proof.WzwOpening, u.BigInt(nil))
|
||||
uWzw.ScalarMultiplication(&proof.WzwOpening, u.BigInt(&scalar))
|
||||
leftG1.Add(&proof.WzOpening, &uWzw)
|
||||
|
||||
// Compute: z·[W_z]_1 + u·(zω)·[W_{zw}]_1
|
||||
@@ -846,9 +866,9 @@ func verifyPLONKPairing(proof *PLONKProof, vk *PLONKVerifyingKey, publicInputs [
|
||||
zOmega.Mul(&z, &vk.Omega)
|
||||
|
||||
var zWz, uzwWzw bn254.G1Affine
|
||||
zWz.ScalarMultiplication(&proof.WzOpening, z.BigInt(nil))
|
||||
uzwWzw.ScalarMultiplication(&proof.WzwOpening, zOmega.BigInt(nil))
|
||||
uzwWzw.ScalarMultiplication(&uzwWzw, u.BigInt(nil))
|
||||
zWz.ScalarMultiplication(&proof.WzOpening, z.BigInt(&scalar))
|
||||
uzwWzw.ScalarMultiplication(&proof.WzwOpening, zOmega.BigInt(&scalar))
|
||||
uzwWzw.ScalarMultiplication(&uzwWzw, u.BigInt(&scalar))
|
||||
|
||||
var rightG1 bn254.G1Affine
|
||||
rightG1.Add(&zWz, &uzwWzw)
|
||||
@@ -898,8 +918,8 @@ func deserializePLONKProof(data []byte) (*PLONKProof, error) {
|
||||
if err := pt.Unmarshal(data[offset : offset+64]); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal G1 point %d: %w", i, err)
|
||||
}
|
||||
if !pt.IsInSubGroup() {
|
||||
return nil, fmt.Errorf("PLONK proof G1 point %d not in prime-order subgroup", i)
|
||||
if err := checkG1(pt); err != nil {
|
||||
return nil, fmt.Errorf("PLONK proof G1 point %d: %w", i, err)
|
||||
}
|
||||
offset += 64
|
||||
}
|
||||
@@ -934,8 +954,8 @@ func deserializePLONKVerifyingKey(data []byte) (*PLONKVerifyingKey, error) {
|
||||
if err := vk.G1.Unmarshal(data[offset : offset+64]); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal G1: %w", err)
|
||||
}
|
||||
if !vk.G1.IsInSubGroup() {
|
||||
return nil, errors.New("PLONK VK G1 generator not in prime-order subgroup")
|
||||
if err := checkG1(&vk.G1); err != nil {
|
||||
return nil, fmt.Errorf("PLONK VK G1 generator: %w", err)
|
||||
}
|
||||
offset += 64
|
||||
|
||||
@@ -943,8 +963,8 @@ func deserializePLONKVerifyingKey(data []byte) (*PLONKVerifyingKey, error) {
|
||||
if err := vk.G2.Unmarshal(data[offset : offset+128]); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal G2: %w", err)
|
||||
}
|
||||
if !vk.G2.IsInSubGroup() {
|
||||
return nil, errors.New("PLONK VK G2 generator not in prime-order subgroup")
|
||||
if err := checkG2(&vk.G2); err != nil {
|
||||
return nil, fmt.Errorf("PLONK VK G2 generator: %w", err)
|
||||
}
|
||||
offset += 128
|
||||
|
||||
@@ -952,8 +972,8 @@ func deserializePLONKVerifyingKey(data []byte) (*PLONKVerifyingKey, error) {
|
||||
if err := vk.G2Alpha.Unmarshal(data[offset : offset+128]); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal G2Alpha: %w", err)
|
||||
}
|
||||
if !vk.G2Alpha.IsInSubGroup() {
|
||||
return nil, errors.New("PLONK VK G2Alpha not in prime-order subgroup")
|
||||
if err := checkG2(&vk.G2Alpha); err != nil {
|
||||
return nil, fmt.Errorf("PLONK VK G2Alpha: %w", err)
|
||||
}
|
||||
offset += 128
|
||||
|
||||
@@ -968,8 +988,8 @@ func deserializePLONKVerifyingKey(data []byte) (*PLONKVerifyingKey, error) {
|
||||
if err := pt.Unmarshal(data[offset : offset+64]); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal selector %d: %w", i, err)
|
||||
}
|
||||
if !pt.IsInSubGroup() {
|
||||
return nil, fmt.Errorf("PLONK VK selector %d not in prime-order subgroup", i)
|
||||
if err := checkG1(pt); err != nil {
|
||||
return nil, fmt.Errorf("PLONK VK selector %d: %w", i, err)
|
||||
}
|
||||
offset += 64
|
||||
}
|
||||
@@ -983,8 +1003,8 @@ func deserializePLONKVerifyingKey(data []byte) (*PLONKVerifyingKey, error) {
|
||||
if err := pt.Unmarshal(data[offset : offset+64]); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal permutation %d: %w", i, err)
|
||||
}
|
||||
if !pt.IsInSubGroup() {
|
||||
return nil, fmt.Errorf("PLONK VK permutation %d not in prime-order subgroup", i)
|
||||
if err := checkG1(pt); err != nil {
|
||||
return nil, fmt.Errorf("PLONK VK permutation %d: %w", i, err)
|
||||
}
|
||||
offset += 64
|
||||
}
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package zkvm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math/big"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/consensys/gnark-crypto/ecc/bn254"
|
||||
"github.com/consensys/gnark-crypto/ecc/bn254/fr"
|
||||
"github.com/luxfi/log"
|
||||
)
|
||||
|
||||
// Groth16 and PLONK verification are the only things standing between a peer's
|
||||
// bytes and shielded value. What follows pins the three ways that guard has
|
||||
// been walked past: arithmetic that dies on a nil scalar, a public-input count
|
||||
// the key cannot cover, and a curve point at infinity that deletes its own
|
||||
// term from the pairing.
|
||||
|
||||
// groth16Key serializes a Groth16 verifying key of numK generator points:
|
||||
// Alpha(G1) | Beta(G2) | Gamma(G2) | Delta(G2) | numK(4) | K[numK](G1).
|
||||
func groth16Key(numK uint32) []byte {
|
||||
_, _, g1, g2 := bn254.Generators()
|
||||
out := make([]byte, 0, 452+int(numK)*64)
|
||||
out = append(out, g1.Marshal()...)
|
||||
out = append(out, g2.Marshal()...)
|
||||
out = append(out, g2.Marshal()...)
|
||||
out = append(out, g2.Marshal()...)
|
||||
n := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(n, numK)
|
||||
out = append(out, n...)
|
||||
for i := uint32(0); i < numK; i++ {
|
||||
out = append(out, g1.Marshal()...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// groth16Frame is Ar‖Bs‖Krs of curve generators: 256 bytes that decode and
|
||||
// pass every point check, so verification reaches the pairing.
|
||||
func groth16Frame() []byte {
|
||||
_, _, g1, g2 := bn254.Generators()
|
||||
out := make([]byte, 0, 256)
|
||||
out = append(out, g1.Marshal()...)
|
||||
out = append(out, g2.Marshal()...)
|
||||
out = append(out, g1.Marshal()...)
|
||||
return out
|
||||
}
|
||||
|
||||
// plonkKey is G1 | G2 | [alpha]_2 | 5 selectors | 3 permutations, all
|
||||
// generators, padded to the 1024 bytes the decoder requires.
|
||||
func plonkKey() []byte {
|
||||
_, _, g1, g2 := bn254.Generators()
|
||||
out := make([]byte, 0, 1024)
|
||||
out = append(out, g1.Marshal()...)
|
||||
out = append(out, g2.Marshal()...)
|
||||
out = append(out, g2.Marshal()...)
|
||||
for i := 0; i < 8; i++ {
|
||||
out = append(out, g1.Marshal()...)
|
||||
}
|
||||
return append(out, make([]byte, 1024-len(out))...)
|
||||
}
|
||||
|
||||
// plonkFrame is 9 G1 generators followed by 5 zero evaluations: 736 bytes.
|
||||
func plonkFrame() []byte {
|
||||
_, _, g1, _ := bn254.Generators()
|
||||
out := make([]byte, 0, 736)
|
||||
for i := 0; i < 9; i++ {
|
||||
out = append(out, g1.Marshal()...)
|
||||
}
|
||||
return append(out, make([]byte, 160)...)
|
||||
}
|
||||
|
||||
func keyedVerifier(t *testing.T, system string, keys map[string][]byte) *ProofVerifier {
|
||||
t.Helper()
|
||||
pv, err := NewProofVerifier(ZConfig{
|
||||
ProofSystem: system,
|
||||
ProofCacheSize: 16,
|
||||
VerifyingKeys: keys,
|
||||
}, log.NoLog{})
|
||||
if err != nil {
|
||||
t.Fatalf("NewProofVerifier: %v", err)
|
||||
}
|
||||
return pv
|
||||
}
|
||||
|
||||
// TestMSMAgreesWithRepeatedAddition. The multi-scalar multiplication carries
|
||||
// the public-input linear combination, so everything a classical proof is
|
||||
// judged on rests on it equalling the sum it stands for.
|
||||
func TestMSMAgreesWithRepeatedAddition(t *testing.T) {
|
||||
_, _, g1, _ := bn254.Generators()
|
||||
|
||||
scalars := make([]fr.Element, 4)
|
||||
bases := make([]bn254.G1Affine, 4)
|
||||
for i := range scalars {
|
||||
scalars[i].SetUint64(uint64(i + 3))
|
||||
bases[i].Set(&g1)
|
||||
}
|
||||
|
||||
var want bn254.G1Affine
|
||||
for i := range scalars {
|
||||
var term bn254.G1Affine
|
||||
term.ScalarMultiplication(&bases[i], scalars[i].BigInt(new(big.Int)))
|
||||
want.Add(&want, &term)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("computing an MSM must not panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
if got := msmCPU(scalars, bases); !got.Equal(&want) {
|
||||
t.Fatalf("msmCPU = %v, want %v", got, want)
|
||||
}
|
||||
|
||||
got, err := msmGPU(scalars, bases, log.NoLog{})
|
||||
if err != nil {
|
||||
t.Fatalf("msmGPU: %v", err)
|
||||
}
|
||||
if !got.Equal(&want) {
|
||||
t.Fatalf("msmGPU = %v, want %v — the accelerated path disagrees with the plain one", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWitnessMustFitTheVerifyingKey. A peer chooses how many public inputs its
|
||||
// transaction carries; the key fixes how many the circuit has. K holds one
|
||||
// point per input plus the constant term, so a witness of len(K) inputs reads
|
||||
// K[len(K)] — one past the end. That must be an error, never a panic.
|
||||
func TestWitnessMustFitTheVerifyingKey(t *testing.T) {
|
||||
_, _, g1, _ := bn254.Generators()
|
||||
|
||||
key := func(n int) *Groth16VerifyingKey {
|
||||
vk := &Groth16VerifyingKey{K: make([]bn254.G1Affine, n)}
|
||||
for i := range vk.K {
|
||||
vk.K[i].Set(&g1)
|
||||
}
|
||||
return vk
|
||||
}
|
||||
|
||||
for _, n := range []int{0, 1, 2, 4} {
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("a peer-chosen public-input count must be an error, not a panic: %v", r)
|
||||
}
|
||||
}()
|
||||
if err := verifyGroth16Pairing(&Groth16Proof{}, key(n), make([]fr.Element, n)); err == nil {
|
||||
t.Fatalf("%d public inputs against %d K points must be refused", n, n)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// A witness the key does cover reaches the pairing and is judged there.
|
||||
// Without this the refusals above could be a verifier that refuses
|
||||
// everything.
|
||||
proof, err := deserializeGroth16Proof(groth16Frame())
|
||||
if err != nil {
|
||||
t.Fatalf("deserializeGroth16Proof: %v", err)
|
||||
}
|
||||
vk, err := deserializeVerifyingKey(groth16Key(3))
|
||||
if err != nil {
|
||||
t.Fatalf("deserializeVerifyingKey: %v", err)
|
||||
}
|
||||
err = verifyGroth16Pairing(proof, vk, make([]fr.Element, 2))
|
||||
if err == nil || !strings.Contains(err.Error(), "pairing check failed") {
|
||||
t.Fatalf("a covered witness must be judged by the pairing, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroth16ReachesItsPairing. A non-strict chain holding real keys is a
|
||||
// supported deployment and its whole point is that a groth16 proof is accepted
|
||||
// or rejected on the arithmetic. Any proof that decodes must produce a verdict.
|
||||
func TestGroth16ReachesItsPairing(t *testing.T) {
|
||||
pv := keyedVerifier(t, "groth16", map[string][]byte{
|
||||
string(TransactionTypeTransfer): groth16Key(4),
|
||||
})
|
||||
if !pv.VerifyingKeysLoaded() {
|
||||
t.Fatal("precondition: the chain holds a real verifying key")
|
||||
}
|
||||
|
||||
nullifier := make([]byte, 32)
|
||||
nullifier[0] = 0xA1
|
||||
commitment := make([]byte, 32)
|
||||
commitment[0] = 0xC1
|
||||
|
||||
tx := &Transaction{
|
||||
Type: TransactionTypeTransfer,
|
||||
Version: 1,
|
||||
Nullifiers: [][]byte{nullifier},
|
||||
Outputs: []*ShieldedOutput{{Commitment: commitment}},
|
||||
Proof: &ZKProof{
|
||||
ProofType: "groth16",
|
||||
ProofData: groth16Frame(),
|
||||
PublicInputs: [][]byte{nullifier, commitment},
|
||||
},
|
||||
}
|
||||
tx.ID = tx.ComputeID()
|
||||
|
||||
var err error
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("verifying a well-formed groth16 proof must not panic: %v", r)
|
||||
}
|
||||
}()
|
||||
err = pv.VerifyTransactionProof(tx)
|
||||
}()
|
||||
|
||||
// The proof satisfies no statement, so the verdict is "rejected" — but a
|
||||
// verdict is what has to come back.
|
||||
if err == nil || !strings.Contains(err.Error(), "verification failed") {
|
||||
t.Fatalf("expected a pairing verdict, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPLONKReachesItsPairing is the same property for the other classical
|
||||
// system a non-strict chain may be configured with.
|
||||
func TestPLONKReachesItsPairing(t *testing.T) {
|
||||
pv := keyedVerifier(t, "plonk", map[string][]byte{
|
||||
string(TransactionTypeTransfer): plonkKey(),
|
||||
})
|
||||
|
||||
nullifier := make([]byte, 32)
|
||||
nullifier[0] = 0xA1
|
||||
commitment := make([]byte, 32)
|
||||
commitment[0] = 0xC1
|
||||
|
||||
tx := &Transaction{
|
||||
Type: TransactionTypeTransfer,
|
||||
Version: 1,
|
||||
Nullifiers: [][]byte{nullifier},
|
||||
Outputs: []*ShieldedOutput{{Commitment: commitment}},
|
||||
Proof: &ZKProof{
|
||||
ProofType: "plonk",
|
||||
ProofData: plonkFrame(),
|
||||
PublicInputs: [][]byte{nullifier, commitment},
|
||||
},
|
||||
}
|
||||
tx.ID = tx.ComputeID()
|
||||
|
||||
var err error
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("verifying a well-formed PLONK proof must not panic: %v", r)
|
||||
}
|
||||
}()
|
||||
err = pv.VerifyTransactionProof(tx)
|
||||
}()
|
||||
|
||||
if err == nil || !strings.Contains(err.Error(), "PLONK") {
|
||||
t.Fatalf("expected a PLONK verdict, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestInfinityIsNotAUsablePoint. gnark decodes all-zero bytes to the point at
|
||||
// infinity and reports it in-subgroup, and a pairing skips every term whose
|
||||
// argument is infinity. A proof or key element at infinity therefore deletes
|
||||
// itself from the equation, so no decoder may hand one on.
|
||||
func TestInfinityIsNotAUsablePoint(t *testing.T) {
|
||||
if _, err := deserializeGroth16Proof(make([]byte, 256)); err == nil {
|
||||
t.Fatal("a Groth16 proof of infinity points must not decode")
|
||||
}
|
||||
|
||||
zeroKey, err := deserializeVerifyingKey(make([]byte, 1024))
|
||||
if err == nil {
|
||||
if err := validateVerifyingKey(zeroKey); err == nil {
|
||||
t.Fatal("a Groth16 key of infinity points must not validate")
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := deserializePLONKProof(make([]byte, 736)); err == nil {
|
||||
t.Fatal("a PLONK proof of infinity points must not decode")
|
||||
}
|
||||
|
||||
if _, err := deserializePLONKVerifyingKey(make([]byte, 1024)); err == nil {
|
||||
t.Fatal("a PLONK key of infinity points must not decode")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCircuitWithoutAKeyIsRefused. A chain is keyed per circuit; the circuits
|
||||
// the operator did not key get an all-zero placeholder. Those zeros decode to
|
||||
// infinity, and a PLONK key of infinity points makes both sides of the pairing
|
||||
// empty — every proof valid, value minted from nothing. A circuit with no key
|
||||
// must be refused instead.
|
||||
func TestCircuitWithoutAKeyIsRefused(t *testing.T) {
|
||||
pv := keyedVerifier(t, "plonk", map[string][]byte{
|
||||
string(TransactionTypeTransfer): plonkKey(), // shield and unshield keyed with zeros
|
||||
})
|
||||
|
||||
commitment := make([]byte, 32)
|
||||
commitment[0] = 0xC1
|
||||
|
||||
tx := &Transaction{
|
||||
Type: TransactionTypeShield,
|
||||
Version: 1,
|
||||
Outputs: []*ShieldedOutput{{Commitment: commitment}},
|
||||
Proof: &ZKProof{
|
||||
ProofType: "plonk",
|
||||
ProofData: plonkFrame(),
|
||||
PublicInputs: [][]byte{commitment},
|
||||
},
|
||||
}
|
||||
tx.ID = tx.ComputeID()
|
||||
|
||||
if err := pv.VerifyTransactionProof(tx); err == nil {
|
||||
t.Fatal("a shield proof judged against an unkeyed circuit must be refused, not accepted")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user