Files
chains/zkvm/precompiles/zk_verifiers_test.go
zeekayandHanzo Dev f377c29746 chains: comments state the contract, not the history behind it
Across these files the comments explained a rule by recounting what happened
without it — which key was mistaken for which, which payer a full pool released,
what a verifier accepted, what a chain did with work it never announced. That
tells a reader what changed rather than what holds, and it dates: the account
stays in the file long after anyone needs it, while the rule it was meant to
convey has to be inferred back out.

They now state the rule. A circuit is judged on its own key. A witness is
exactly as long as the key's circuit says. A count describes the records it is
read from. A full pool releases what pays least. Custody membership costs stake
because an identifier can be aimed and stake cannot. Consensus builds when
WaitForEvent returns, so accepting work and reporting it are one step.

Behaviour is unchanged; the diff is comments, with one exception worth naming:
gofmt realigned a block of one-line methods in quantumvm/feegate_test.go that
was already unformatted before this commit.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-26 13:17:16 -07:00

670 lines
22 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package precompiles
import (
"crypto/rand"
"encoding/binary"
"errors"
"math/big"
"testing"
"github.com/consensys/gnark-crypto/ecc/bn254"
"github.com/consensys/gnark-crypto/ecc/bn254/fr"
"github.com/luxfi/ids"
"github.com/luxfi/precompile/starkfri"
)
func TestGroth16VerifierGas(t *testing.T) {
v := &Groth16Verifier{}
if g := v.RequiredGas(nil); g != groth16Gas {
t.Fatalf("expected %d gas, got %d", groth16Gas, g)
}
}
func TestGroth16VerifierRejectsTooShort(t *testing.T) {
v := &Groth16Verifier{}
result, err := v.Run([]byte{0x00})
if err == nil {
t.Fatal("expected error for short input")
}
if len(result) != 1 || result[0] != 0x00 {
t.Fatal("expected 0x00 result for invalid input")
}
}
// TestGroth16VerifierValidProof constructs a trivial Groth16 proof
// and verifying key using known discrete log relations, then verifies it.
func TestGroth16VerifierValidProof(t *testing.T) {
// Generate a valid Groth16 proof/VK pair with known trapdoor.
// Trapdoor: alpha, beta, gamma, delta, tau are random field elements.
var alpha, beta, gamma, delta fr.Element
alpha.SetRandom()
beta.SetRandom()
gamma.SetRandom()
delta.SetRandom()
_, _, g1, g2 := bn254.Generators()
var alphaBI, betaBI, gammaBI, deltaBI big.Int
// VK components
var vkAlpha bn254.G1Affine
vkAlpha.ScalarMultiplication(&g1, alpha.BigInt(&alphaBI))
var vkBeta bn254.G2Affine
vkBeta.ScalarMultiplication(&g2, beta.BigInt(&betaBI))
var vkGamma bn254.G2Affine
vkGamma.ScalarMultiplication(&g2, gamma.BigInt(&gammaBI))
var vkDelta bn254.G2Affine
vkDelta.ScalarMultiplication(&g2, delta.BigInt(&deltaBI))
// Single public input: witness w. K[0] = g1, K[1] = g1 (simple circuit).
vkK := []bn254.G1Affine{g1, g1}
// Witness
var w fr.Element
w.SetRandom()
// pubLC = K[0] + w * K[1] = g1 + w*g1 = (1+w)*g1
var onePlusW fr.Element
onePlusW.SetOne()
onePlusW.Add(&onePlusW, &w)
var pubLC bn254.G1Affine
var onePlusWBI big.Int
pubLC.ScalarMultiplication(&g1, onePlusW.BigInt(&onePlusWBI))
// For a valid Groth16 proof, we need:
// e(A, B) = e(alpha, beta) * e(pubLC, gamma) * e(C, delta)
//
// Pick random r, s. Set:
// A = alpha + r*delta (in G1)
// B = beta + s*delta (in G2)
// C = (alpha*beta + r*s*delta^2 + pubLC*gamma_inv*delta - alpha*beta) / delta
// = (r*beta + s*alpha + r*s*delta) + pubLC * gamma_inv ... simplified
//
// Simplest valid proof: use the identity that e(A,B) can be decomposed.
// For testing, we use the multi-pairing check directly.
//
// We construct A, B, C such that the pairing equation holds.
// Let A = alpha*g1, B = beta*g2. Then e(A,B) = e(alpha*g1, beta*g2).
// We need e(alpha*g1, beta*g2) = e(vkAlpha, vkBeta) * e(pubLC, vkGamma) * e(C, vkDelta)
// = e(alpha*g1, beta*g2) * e(pubLC, gamma*g2) * e(C, delta*g2)
//
// So we need e(pubLC, gamma*g2) * e(C, delta*g2) = 1
// i.e., e(pubLC, gamma*g2) = e(-C, delta*g2)
// i.e., C = pubLC * gamma / delta (scalar division in the exponent)
var gammaInvDelta fr.Element
gammaInvDelta.Div(&gamma, &delta)
var gammaInvDeltaBI big.Int
gammaInvDelta.BigInt(&gammaInvDeltaBI)
var proofC bn254.G1Affine
proofC.ScalarMultiplication(&pubLC, &gammaInvDeltaBI)
proofC.Neg(&proofC)
proofA := vkAlpha // alpha * g1
proofB := vkBeta // beta * g2
// Verify the pairing equation holds (sanity check before serializing)
{
var negA, negPubLC, negC bn254.G1Affine
negA.Neg(&vkAlpha)
negPubLC.Neg(&pubLC)
negC.Neg(&proofC)
ok, err := bn254.PairingCheck(
[]bn254.G1Affine{proofA, negA, negPubLC, negC},
[]bn254.G2Affine{proofB, vkBeta, vkGamma, vkDelta},
)
if err != nil {
t.Fatalf("sanity pairing error: %v", err)
}
if !ok {
t.Fatal("sanity pairing check failed — test setup is wrong")
}
}
// Serialize
input := serializeGroth16Input(
&groth16VK{Alpha: vkAlpha, Beta: vkBeta, Gamma: vkGamma, Delta: vkDelta, K: vkK},
&groth16Proof{Ar: proofA, Bs: proofB, Krs: proofC},
[]fr.Element{w},
)
v := &Groth16Verifier{}
result, err := v.Run(input)
if err != nil {
t.Fatalf("Run error: %v", err)
}
if len(result) != 1 || result[0] != 0x01 {
t.Fatalf("expected valid proof (0x01), got %x", result)
}
}
func TestGroth16VerifierRejectsInvalidProof(t *testing.T) {
_, _, g1, g2 := bn254.Generators()
// Construct a VK with generators
vk := &groth16VK{
Alpha: g1,
Beta: g2,
Gamma: g2,
Delta: g2,
K: []bn254.G1Affine{g1, g1},
}
// Random (invalid) proof — points are on curve but pairing won't check out
var r fr.Element
r.SetRandom()
var randG1 bn254.G1Affine
var rBI big.Int
randG1.ScalarMultiplication(&g1, r.BigInt(&rBI))
proof := &groth16Proof{Ar: randG1, Bs: g2, Krs: g1}
var w fr.Element
w.SetRandom()
input := serializeGroth16Input(vk, proof, []fr.Element{w})
v := &Groth16Verifier{}
result, err := v.Run(input)
if err != nil {
t.Fatalf("Run should not return error for invalid proof, got: %v", err)
}
if len(result) != 1 || result[0] != 0x00 {
t.Fatalf("expected invalid proof (0x00), got %x", result)
}
}
// 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 {
t.Fatalf("expected %d gas, got %d", plonkGas, g)
}
}
// buildPLONKInput serialises [vk_len(4)][vk][proof_len(4)][proof][num_inputs(4)][inputs]
// in the wire format the PLONKVerifier precompile expects.
func buildPLONKInput(vk, proof []byte, numInputs uint32, inputs []byte) []byte {
out := make([]byte, 0, 4+len(vk)+4+len(proof)+4+len(inputs))
var l [4]byte
binary.BigEndian.PutUint32(l[:], uint32(len(vk)))
out = append(out, l[:]...)
out = append(out, vk...)
binary.BigEndian.PutUint32(l[:], uint32(len(proof)))
out = append(out, l[:]...)
out = append(out, proof...)
binary.BigEndian.PutUint32(l[:], numInputs)
out = append(out, l[:]...)
out = append(out, inputs...)
return out
}
// TestPLONKVerifierNeverUniversalAccepts pins that this verifier accepts
// nothing while the verification equation is unimplemented. It builds a
// well-formed but arbitrary proof and key — 7 valid G1 commitments, 3 scalars,
// a valid SRS G2 — which is the shape a structural check alone would pass, and
// requires 0x00.
func TestPLONKVerifierNeverUniversalAccepts(t *testing.T) {
_, _, g1, g2 := bn254.Generators()
// Proof: 7 G1 commitments (use the generator — a valid prime-order
// point) + 3 scalar evals (32 bytes each) = 448 + 96 = 544 bytes.
g1m := g1.Marshal() // 64 bytes uncompressed
proof := make([]byte, 0, 544)
for i := 0; i < 7; i++ {
proof = append(proof, g1m...)
}
proof = append(proof, make([]byte, 96)...) // 3 zero scalars
// VK: SRS G2 = generator (valid prime-order G2 point), 128 bytes.
vk := g2.Marshal()
input := buildPLONKInput(vk, proof, 1, make([]byte, 32))
v := &PLONKVerifier{}
res, err := v.Run(input)
// The precompile treats an invalid proof as 0x00 with nil error
// (invalid proof is not an execution error). The load-bearing
// assertion: it must NOT be 0x01 (valid).
if err == nil && len(res) == 1 && res[0] == 0x01 {
t.Fatal("PLONKVerifier UNIVERSAL-ACCEPTED an arbitrary well-formed proof (H4 bypass)")
}
if len(res) != 1 || res[0] != 0x00 {
t.Fatalf("PLONKVerifier must fail closed (0x00) for an unverified proof, got res=%x err=%v", res, err)
}
}
// TestVerifyPLONKFailsClosed asserts the internal verifyPLONK never
// returns nil (valid) — it either reports a structural error or
// errPLONKVerifierIncomplete, but NEVER accepts.
func TestVerifyPLONKFailsClosed(t *testing.T) {
_, _, g1, g2 := bn254.Generators()
g1m := g1.Marshal()
proof := make([]byte, 0, 544)
for i := 0; i < 7; i++ {
proof = append(proof, g1m...)
}
proof = append(proof, make([]byte, 96)...)
vk := g2.Marshal()
if err := verifyPLONK(vk, proof, make([]byte, 32)); err == nil {
t.Fatal("verifyPLONK returned nil (valid) — must fail closed, never universal-accept")
} else if !errors.Is(err, errPLONKVerifierIncomplete) {
// A well-formed proof should reach the incomplete-verifier guard,
// not bail early on a structural error.
t.Fatalf("verifyPLONK: expected errPLONKVerifierIncomplete for a well-formed blob, got %v", err)
}
}
// buildSTARKInput serialises [proof_len(4)][proof][pub_len(4)][pub] in
// the wire format the STARKVerifier precompile expects.
func buildSTARKInput(proof, pub []byte) []byte {
out := make([]byte, 0, 4+len(proof)+4+len(pub))
var l [4]byte
binary.BigEndian.PutUint32(l[:], uint32(len(proof)))
out = append(out, l[:]...)
out = append(out, proof...)
binary.BigEndian.PutUint32(l[:], uint32(len(pub)))
out = append(out, l[:]...)
out = append(out, pub...)
return out
}
// TestSTARKVerifierFailsClosedWhenUnbound proves the Z-Chain STARK
// rollup verifier delegates to precompile/starkfri and FAILS CLOSED
// when no verifier binding is registered (the CGO_ENABLED=0 / no
// starkfri_p3q build). A structurally well-formed P3Q1 proof must NOT
// be accepted in the unbound configuration — there is no forgery
// oracle. This is the post-quantum-safe posture: absent the FRI
// verifier, the precompile refuses rather than rubber-stamps.
func TestSTARKVerifierFailsClosedWhenUnbound(t *testing.T) {
starkfri.RegisterVerifier(nil) // ensure no binding
v := &STARKVerifier{}
proof := append([]byte(starkfri.MagicHeader), []byte("strict-PQ-FRI-Goldilocks-payload")...)
pub := []byte{0x01, 0x02, 0x03, 0x04}
res, err := v.Run(buildSTARKInput(proof, pub))
if !errors.Is(err, errVerifierUnavailable) {
t.Fatalf("expected errVerifierUnavailable (fail-closed), got: %v", err)
}
if len(res) != 1 || res[0] != 0x00 {
t.Fatalf("unbound STARK verifier must return 0x00 (invalid), got: %x", res)
}
}
// TestSTARKVerifierDelegatesToStarkFRI proves the verifier-side switch:
// the Z-Chain rollup STARK path routes through starkfri.Verify (NOT the
// pairing-based Groth16 path). We register a fake FRI verifier and
// assert (a) it is invoked with the proof/pub bytes the precompile
// parsed, (b) an accept yields 0x01, (c) a reject yields 0x00, and
// (d) a proof without the "P3Q1" magic is rejected by starkfri's
// structural pre-filter BEFORE the verifier callback runs.
func TestSTARKVerifierDelegatesToStarkFRI(t *testing.T) {
defer starkfri.RegisterVerifier(nil)
v := &STARKVerifier{}
proof := append([]byte(starkfri.MagicHeader), []byte("trace-root|fri-root|queries")...)
pub := []byte{0xaa, 0xbb, 0xcc}
// (a)+(b): accept path, capture what starkfri.Verify received.
var sawProof, sawPub []byte
starkfri.RegisterVerifier(func(_ byte, p, pi []byte) (bool, error) {
sawProof = append([]byte(nil), p...)
sawPub = append([]byte(nil), pi...)
return true, nil
})
res, err := v.Run(buildSTARKInput(proof, pub))
if err != nil {
t.Fatalf("accept path returned error: %v", err)
}
if len(res) != 1 || res[0] != 0x01 {
t.Fatalf("accept path must return 0x01 (valid), got: %x", res)
}
if string(sawProof) != string(proof) {
t.Fatalf("starkfri verifier saw wrong proof bytes: %x != %x", sawProof, proof)
}
if string(sawPub) != string(pub) {
t.Fatalf("starkfri verifier saw wrong public-input bytes: %x != %x", sawPub, pub)
}
// (c): reject path.
starkfri.RegisterVerifier(func(byte, []byte, []byte) (bool, error) { return false, nil })
res, err = v.Run(buildSTARKInput(proof, pub))
if err != nil {
t.Fatalf("reject path must not surface an execution error, got: %v", err)
}
if len(res) != 1 || res[0] != 0x00 {
t.Fatalf("reject path must return 0x00 (invalid), got: %x", res)
}
// (d): bad-magic proof is rejected by starkfri's structural filter
// before the callback runs (the callback would say true otherwise).
called := false
starkfri.RegisterVerifier(func(byte, []byte, []byte) (bool, error) { called = true; return true, nil })
badProof := append([]byte("BAD!"), []byte("not-a-p3q1-proof")...)
res, _ = v.Run(buildSTARKInput(badProof, pub))
if len(res) != 1 || res[0] != 0x00 {
t.Fatalf("bad-magic proof must return 0x00 (invalid), got: %x", res)
}
if called {
t.Fatal("starkfri callback must not run on a proof missing the P3Q1 magic header")
}
}
func TestHalo2VerifierNotImplemented(t *testing.T) {
v := &Halo2Verifier{}
_, err := v.Run([]byte{0x00})
if err != errNotImplemented {
t.Fatalf("expected errNotImplemented, got: %v", err)
}
}
func TestNovaVerifierNotImplemented(t *testing.T) {
v := &NovaVerifier{}
_, err := v.Run([]byte{0x00})
if err != errNotImplemented {
t.Fatalf("expected errNotImplemented, got: %v", err)
}
}
// TestRegistryRegistersAll confirms a NON-strict chain registers every
// verifier, including the classical Groth16/PLONK kept as an optional
// building block.
func TestRegistryRegistersAll(t *testing.T) {
reg := NewMapRegistry()
RegisterZKPrecompiles(reg, false /* strictPQ */)
addrs := []byte{
Groth16VerifierAddr,
PLONKVerifierAddr,
STARKVerifierAddr,
Halo2VerifierAddr,
NovaVerifierAddr,
}
for _, addr := range addrs {
if _, err := reg.Get(addr); err != nil {
t.Fatalf("precompile 0x%02x not registered: %v", addr, err)
}
}
// Non-existent address
if _, err := reg.Get(0xFF); err == nil {
t.Fatal("expected error for unregistered address")
}
}
// TestRegistryStrictPQOmitsClassical is the H2 gating test: on a strict-PQ
// chain the classical Groth16 (0x80) and PLONK (0x81) verifiers are NOT
// registered (calls fail closed by absence), while the post-quantum STARK
// (0x82) verifier and the always-fail-closed Halo2/Nova stubs ARE.
func TestRegistryStrictPQOmitsClassical(t *testing.T) {
reg := NewMapRegistry()
RegisterZKPrecompiles(reg, true /* strictPQ */)
// Classical verifiers MUST be absent on a strict-PQ chain.
for _, addr := range []byte{Groth16VerifierAddr, PLONKVerifierAddr} {
if _, err := reg.Get(addr); err == nil {
t.Fatalf("classical precompile 0x%02x MUST NOT be registered on a strict-PQ chain", addr)
}
}
// Post-quantum STARK and the fail-closed stubs MUST be present.
for _, addr := range []byte{STARKVerifierAddr, Halo2VerifierAddr, NovaVerifierAddr} {
if _, err := reg.Get(addr); err != nil {
t.Fatalf("precompile 0x%02x must be registered on a strict-PQ chain: %v", addr, err)
}
}
}
// TestCrossChainVerifierStrictPQRefusesClassical is the H2 cross-chain
// gating test: a strict-PQ router refuses to route Groth16/PLONK proofs
// to Z-Chain, but still routes the quantum-safe STARK path.
func TestCrossChainVerifierStrictPQRefusesClassical(t *testing.T) {
v := &CrossChainZKVerifier{ZChainID: ids.ID{}, StrictPQ: true}
for _, vtype := range []byte{VerifierTypeGroth16, VerifierTypePLONK} {
res, err := v.Run([]byte{vtype, 0x00, 0x01, 0x02})
if !errors.Is(err, errClassicalForbiddenStrictPQ) {
t.Fatalf("type 0x%02x: expected errClassicalForbiddenStrictPQ, got %v", vtype, err)
}
if len(res) != 1 || res[0] != 0x00 {
t.Fatalf("type 0x%02x: expected 0x00 result, got %x", vtype, res)
}
}
// STARK still routes under strict-PQ.
msg, err := v.Run(append([]byte{VerifierTypeSTARK}, []byte("p3q-proof")...))
if err != nil {
t.Fatalf("strict-PQ router must still route STARK, got: %v", err)
}
if _, gotAddr, _, derr := DecodeWarpPayload(msg); derr != nil || gotAddr != STARKVerifierAddr {
t.Fatalf("strict-PQ STARK route: addr=0x%02x err=%v", gotAddr, derr)
}
}
func TestCrossChainVerifierGas(t *testing.T) {
v := &CrossChainZKVerifier{ZChainID: ids.ID{}}
tests := []struct {
input []byte
expected uint64
}{
{nil, 100_000},
{[]byte{VerifierTypeGroth16}, 100_000 + groth16Gas},
{[]byte{VerifierTypePLONK}, 100_000 + plonkGas},
{[]byte{VerifierTypeSTARK}, 100_000 + starkGas},
{[]byte{VerifierTypeHalo2}, 100_000 + halo2Gas},
{[]byte{VerifierTypeNova}, 100_000 + novaGas},
{[]byte{0xFF}, 100_000}, // unknown type
}
for _, tt := range tests {
if g := v.RequiredGas(tt.input); g != tt.expected {
t.Errorf("input=%x: expected %d gas, got %d", tt.input, tt.expected, g)
}
}
}
func TestCrossChainWarpPayloadRoundtrip(t *testing.T) {
var zID ids.ID
rand.Read(zID[:])
payload := []byte("test proof data")
encoded := encodeWarpPayload(zID, Groth16VerifierAddr, payload)
gotID, gotAddr, gotPayload, err := DecodeWarpPayload(encoded)
if err != nil {
t.Fatal(err)
}
if gotID != zID {
t.Fatal("chain ID mismatch")
}
if gotAddr != Groth16VerifierAddr {
t.Fatalf("addr mismatch: got 0x%02x", gotAddr)
}
if string(gotPayload) != string(payload) {
t.Fatal("payload mismatch")
}
}
func TestCrossChainVerifierRejectsTooShort(t *testing.T) {
v := &CrossChainZKVerifier{ZChainID: ids.ID{}}
result, err := v.Run([]byte{})
if err == nil {
t.Fatal("expected error for empty input")
}
if result[0] != 0x00 {
t.Fatal("expected 0x00 result")
}
}
func TestCrossChainVerifierRejectsStubs(t *testing.T) {
v := &CrossChainZKVerifier{ZChainID: ids.ID{}}
// STARK routes to the strict-PQ verifier on Z-Chain. Halo2 and Nova are
// the unimplemented ones.
for _, vtype := range []byte{VerifierTypeHalo2, VerifierTypeNova} {
result, err := v.Run([]byte{vtype, 0x00})
if err != errNotImplemented {
t.Fatalf("type 0x%02x: expected errNotImplemented, got %v", vtype, err)
}
if result[0] != 0x00 {
t.Fatalf("type 0x%02x: expected 0x00 result", vtype)
}
}
}
// TestCrossChainVerifierRoutesSTARK proves the cross-chain router now
// forwards strict-PQ STARK rollup proofs to the Z-Chain STARKVerifier
// (target address STARKVerifierAddr), instead of rejecting them as
// not-implemented. The quantum-safe rollup path is reachable from any
// EVM chain via Warp.
func TestCrossChainVerifierRoutesSTARK(t *testing.T) {
var zID ids.ID
rand.Read(zID[:])
v := &CrossChainZKVerifier{ZChainID: zID}
proofData := []byte("p3q-stark-proof-bytes")
msg, err := v.Run(append([]byte{VerifierTypeSTARK}, proofData...))
if err != nil {
t.Fatalf("STARK routing returned error: %v", err)
}
_, gotAddr, gotPayload, err := DecodeWarpPayload(msg)
if err != nil {
t.Fatalf("decode warp payload: %v", err)
}
if gotAddr != STARKVerifierAddr {
t.Fatalf("STARK must route to STARKVerifierAddr 0x%02x, got 0x%02x", STARKVerifierAddr, gotAddr)
}
if string(gotPayload) != string(proofData) {
t.Fatalf("STARK routed payload mismatch: %x != %x", gotPayload, proofData)
}
}
// --- helpers ---
func serializeGroth16Input(vk *groth16VK, proof *groth16Proof, witness []fr.Element) []byte {
// Serialize VK
vkBuf := make([]byte, 0, 64+128+128+128+4+len(vk.K)*64)
vkBuf = append(vkBuf, vk.Alpha.Marshal()...)
vkBuf = append(vkBuf, vk.Beta.Marshal()...)
vkBuf = append(vkBuf, vk.Gamma.Marshal()...)
vkBuf = append(vkBuf, vk.Delta.Marshal()...)
numK := make([]byte, 4)
binary.BigEndian.PutUint32(numK, uint32(len(vk.K)))
vkBuf = append(vkBuf, numK...)
for i := range vk.K {
vkBuf = append(vkBuf, vk.K[i].Marshal()...)
}
// Build input
buf := make([]byte, 0, 4+len(vkBuf)+256+4+len(witness)*32)
vkLen := make([]byte, 4)
binary.BigEndian.PutUint32(vkLen, uint32(len(vkBuf)))
buf = append(buf, vkLen...)
buf = append(buf, vkBuf...)
buf = append(buf, proof.Ar.Marshal()...)
buf = append(buf, proof.Bs.Marshal()...)
buf = append(buf, proof.Krs.Marshal()...)
numInputs := make([]byte, 4)
binary.BigEndian.PutUint32(numInputs, uint32(len(witness)))
buf = append(buf, numInputs...)
for _, w := range witness {
b := w.Bytes()
buf = append(buf, b[:]...)
}
return buf
}
// TestPublicInputCountMustMatchTheKeysCircuit. This precompile takes both the
// verifying key and the public-input count from its caller's calldata, so a
// caller picks both and they have to agree. K holds one point per public input
// plus a constant term, so the key states how many its circuit takes. Too many
// reads past the end of K; too few leaves the trailing K points out of the
// linear combination, judging the proof against a smaller statement than the
// key describes. Only the first was checked.
func TestPublicInputCountMustMatchTheKeysCircuit(t *testing.T) {
_, _, g1, g2 := bn254.Generators()
// Three K points is a circuit taking two public inputs.
vk := &groth16VK{Alpha: g1, Beta: g2, Gamma: g2, Delta: g2,
K: []bn254.G1Affine{g1, g1, g1}}
var r fr.Element
r.SetRandom()
var rBI big.Int
var randG1 bn254.G1Affine
randG1.ScalarMultiplication(&g1, r.BigInt(&rBI))
proof := &groth16Proof{Ar: randG1, Bs: g2, Krs: g1}
witness := func(n int) []fr.Element {
out := make([]fr.Element, n)
for i := range out {
out[i].SetUint64(uint64(i + 1))
}
return out
}
v := &Groth16Verifier{}
for _, n := range []int{0, 1, 3, 7} {
if _, err := v.Run(serializeGroth16Input(vk, proof, witness(n))); err == nil {
t.Fatalf("%d public inputs judged against a circuit taking 2", n)
}
}
// The count the key states reaches the arithmetic and is answered on its
// merits — without this control the refusals above would prove only that
// this rejects everything.
result, err := v.Run(serializeGroth16Input(vk, proof, witness(2)))
if err != nil {
t.Fatalf("the count the key states must reach the pairing, got: %v", err)
}
if len(result) != 1 || result[0] != 0x00 {
t.Fatalf("a proof satisfying no statement must be rejected, got %x", result)
}
}