Files
precompile/zk/verifier_test.go
zeekay 18622afd6d zk,ai: real fflonk pairing verify, Pallas point validation, TEE attestation
D1: verifyTEESignature now parses the receipt's X.509 chain, verifies it to an
embedded Intel SGX Root CA at the report's timestamp, and verifies the report
signature by the leaf key (ECDSA raw/ASN.1, Ed25519, RSA). Fails closed; never
returns true by default.

B2: fflonkVerify now performs two real BLS12-381 KZG pairing openings bound by
Fiat-Shamir so every commitment, opening proof, claim and bound evaluation is
load-bearing; off-curve/wrong-subgroup G1 points rejected via gnark-crypto.
isValidCompressedPoint now decodes the sign bit and checks x<p and that x^3+5
is a QR mod the Pallas field (on-curve == in-subgroup for cofactor 1).

Tests: KAT happy paths + tampered/wrong-signer/truncated/expired/empty-root and
soundness negatives; halo2 fixtures emit real on-curve points.
2026-06-26 18:15:00 -07:00

1801 lines
50 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package zk
import (
"encoding/hex"
"math/big"
"testing"
"github.com/luxfi/crypto/bn256"
"github.com/luxfi/geth/common"
)
// TestNewZKVerifier tests verifier creation
func TestNewZKVerifier(t *testing.T) {
zv := NewZKVerifier()
if zv == nil {
t.Fatal("Expected non-nil ZKVerifier")
}
if zv.VerifyingKeys == nil {
t.Error("Expected VerifyingKeys map to be initialized")
}
if zv.Nullifiers == nil {
t.Error("Expected Nullifiers map to be initialized")
}
if zv.Commitments == nil {
t.Error("Expected Commitments map to be initialized")
}
if zv.Rollups == nil {
t.Error("Expected Rollups map to be initialized")
}
if zv.Pools == nil {
t.Error("Expected Pools map to be initialized")
}
}
// TestRegisterVerifyingKey tests verifying key registration
func TestRegisterVerifyingKey(t *testing.T) {
zv := NewZKVerifier()
owner := common.HexToAddress("0x1234567890123456789012345678901234567890")
alpha := []byte("alpha_point")
beta := []byte("beta_point")
gamma := []byte("gamma_point")
delta := []byte("delta_point")
ic := [][]byte{[]byte("ic0"), []byte("ic1"), []byte("ic2")}
keyID, err := zv.RegisterVerifyingKey(
owner,
ProofSystemGroth16,
CircuitTransfer,
alpha, beta, gamma, delta, ic,
)
if err != nil {
t.Fatalf("RegisterVerifyingKey failed: %v", err)
}
if keyID == [32]byte{} {
t.Error("Expected non-zero key ID")
}
// Verify key was stored
vk := zv.VerifyingKeys[keyID]
if vk == nil {
t.Fatal("Verifying key not stored")
}
if vk.ProofSystem != ProofSystemGroth16 {
t.Errorf("Expected Groth16, got %v", vk.ProofSystem)
}
if vk.CircuitType != CircuitTransfer {
t.Errorf("Expected Transfer circuit, got %v", vk.CircuitType)
}
if vk.Owner != owner {
t.Error("Owner mismatch")
}
if len(vk.IC) != 3 {
t.Errorf("Expected 3 IC points, got %d", len(vk.IC))
}
}
// TestVerifyGroth16 tests Groth16 proof verification
func TestVerifyGroth16(t *testing.T) {
zv := NewZKVerifier()
owner := common.HexToAddress("0x1234567890123456789012345678901234567890")
// Register verifying key with 3 IC points (2 public inputs + 1)
ic := [][]byte{[]byte("ic0"), []byte("ic1"), []byte("ic2")}
keyID, _ := zv.RegisterVerifyingKey(
owner,
ProofSystemGroth16,
CircuitTransfer,
[]byte("alpha"), []byte("beta"), []byte("gamma"), []byte("delta"),
ic,
)
// Create proof
proofA := make([]byte, 64)
proofB := make([]byte, 128)
proofC := make([]byte, 64)
publicInputs := []*big.Int{big.NewInt(100), big.NewInt(200)}
result, err := zv.VerifyGroth16(keyID, proofA, proofB, proofC, publicInputs)
if err != nil {
t.Fatalf("VerifyGroth16 failed: %v", err)
}
if result == nil {
t.Fatal("Expected non-nil result")
}
if result.ProofSystem != ProofSystemGroth16 {
t.Errorf("Expected Groth16, got %v", result.ProofSystem)
}
if result.GasUsed != GasGroth16Verify {
t.Errorf("Expected gas %d, got %d", GasGroth16Verify, result.GasUsed)
}
if len(result.PublicInputs) != 2 {
t.Errorf("Expected 2 public inputs, got %d", len(result.PublicInputs))
}
// Verify stats updated
if zv.TotalVerifications != 1 {
t.Errorf("Expected 1 verification, got %d", zv.TotalVerifications)
}
}
// TestVerifyGroth16InvalidKey tests error for non-existent key
func TestVerifyGroth16InvalidKey(t *testing.T) {
zv := NewZKVerifier()
nonExistent := [32]byte{0xFF}
_, err := zv.VerifyGroth16(nonExistent, nil, nil, nil, nil)
if err != ErrInvalidVerifyingKey {
t.Errorf("Expected ErrInvalidVerifyingKey, got %v", err)
}
}
// TestVerifyGroth16WrongProofSystem tests proof system validation
func TestVerifyGroth16WrongProofSystem(t *testing.T) {
zv := NewZKVerifier()
owner := common.HexToAddress("0x1234567890123456789012345678901234567890")
// Register as PLONK
keyID, _ := zv.RegisterVerifyingKey(
owner,
ProofSystemPlonk,
CircuitTransfer,
[]byte("alpha"), []byte("beta"), []byte("gamma"), []byte("delta"),
[][]byte{[]byte("ic0")},
)
// Try to verify as Groth16
_, err := zv.VerifyGroth16(keyID, nil, nil, nil, nil)
if err != ErrProofSystemMismatch {
t.Errorf("Expected ErrProofSystemMismatch, got %v", err)
}
}
// TestVerifyGroth16InvalidPublicInputs tests public input count validation
func TestVerifyGroth16InvalidPublicInputs(t *testing.T) {
zv := NewZKVerifier()
owner := common.HexToAddress("0x1234567890123456789012345678901234567890")
// Register with 3 IC points (expects 2 public inputs)
keyID, _ := zv.RegisterVerifyingKey(
owner,
ProofSystemGroth16,
CircuitTransfer,
[]byte("alpha"), []byte("beta"), []byte("gamma"), []byte("delta"),
[][]byte{[]byte("ic0"), []byte("ic1"), []byte("ic2")},
)
// Provide wrong number of public inputs
publicInputs := []*big.Int{big.NewInt(1)} // Only 1, should be 2
_, err := zv.VerifyGroth16(keyID, []byte("a"), []byte("b"), []byte("c"), publicInputs)
if err != ErrInvalidPublicInputs {
t.Errorf("Expected ErrInvalidPublicInputs, got %v", err)
}
}
// TestVerifyPlonk tests PLONK proof verification
func TestVerifyPlonk(t *testing.T) {
zv := NewZKVerifier()
owner := common.HexToAddress("0x1234567890123456789012345678901234567890")
keyID, _ := zv.RegisterVerifyingKey(
owner,
ProofSystemPlonk,
CircuitRollupBatch,
[]byte("alpha"), []byte("beta"), []byte("gamma"), []byte("delta"),
[][]byte{[]byte("ic0")},
)
proof := make([]byte, 512)
publicInputs := []*big.Int{big.NewInt(100)}
result, err := zv.VerifyPlonk(keyID, proof, publicInputs)
if err != nil {
t.Fatalf("VerifyPlonk failed: %v", err)
}
if result == nil {
t.Fatal("Expected non-nil result")
}
if result.ProofSystem != ProofSystemPlonk {
t.Errorf("Expected PLONK, got %v", result.ProofSystem)
}
if result.GasUsed != GasPlonkVerify {
t.Errorf("Expected gas %d, got %d", GasPlonkVerify, result.GasUsed)
}
}
// TestCheckNullifier tests nullifier checking
func TestCheckNullifier(t *testing.T) {
zv := NewZKVerifier()
nullifierHash := [32]byte{0x01, 0x02, 0x03}
// Initially not spent
spent, err := zv.CheckNullifier(nullifierHash)
if err != nil {
t.Fatalf("CheckNullifier failed: %v", err)
}
if spent {
t.Error("Expected nullifier to be unspent")
}
// Spend it
txHash := common.HexToHash("0xABCDEF")
err = zv.SpendNullifier(nullifierHash, txHash, 100)
if err != nil {
t.Fatalf("SpendNullifier failed: %v", err)
}
// Now should be spent
spent, err = zv.CheckNullifier(nullifierHash)
if err != nil {
t.Fatalf("CheckNullifier failed: %v", err)
}
if !spent {
t.Error("Expected nullifier to be spent")
}
}
// TestSpendNullifierAlreadySpent tests double-spend prevention
func TestSpendNullifierAlreadySpent(t *testing.T) {
zv := NewZKVerifier()
nullifierHash := [32]byte{0x01, 0x02, 0x03}
txHash := common.HexToHash("0xABCDEF")
// First spend
err := zv.SpendNullifier(nullifierHash, txHash, 100)
if err != nil {
t.Fatalf("First spend failed: %v", err)
}
// Second spend should fail
err = zv.SpendNullifier(nullifierHash, txHash, 101)
if err != ErrNullifierSpent {
t.Errorf("Expected ErrNullifierSpent, got %v", err)
}
}
// TestCreateConfidentialPool tests pool creation
func TestCreateConfidentialPool(t *testing.T) {
zv := NewZKVerifier()
owner := common.HexToAddress("0x1234567890123456789012345678901234567890")
token := common.HexToAddress("0xABCDABCDABCDABCDABCDABCDABCDABCDABCDABCD")
poolID, err := zv.CreateConfidentialPool(owner, token, 32)
if err != nil {
t.Fatalf("CreateConfidentialPool failed: %v", err)
}
if poolID == [32]byte{} {
t.Error("Expected non-zero pool ID")
}
// Verify pool was created
pool := zv.Pools[poolID]
if pool == nil {
t.Fatal("Pool not stored")
}
if pool.Token != token {
t.Error("Token mismatch")
}
if pool.MerkleDepth != 32 {
t.Errorf("Expected depth 32, got %d", pool.MerkleDepth)
}
if !pool.Enabled {
t.Error("Expected pool to be enabled")
}
}
// TestAddCommitment tests adding commitment to pool
func TestAddCommitment(t *testing.T) {
zv := NewZKVerifier()
owner := common.HexToAddress("0x1234567890123456789012345678901234567890")
token := common.HexToAddress("0xABCDABCDABCDABCDABCDABCDABCDABCDABCDABCD")
poolID, _ := zv.CreateConfidentialPool(owner, token, 32)
commitment := &Commitment{
Value: []byte("commitment_hash"),
Amount: big.NewInt(1e18),
Blinding: []byte("blinding_factor"),
}
commitID, err := zv.AddCommitment(poolID, commitment)
if err != nil {
t.Fatalf("AddCommitment failed: %v", err)
}
if commitID == [32]byte{} {
t.Error("Expected non-zero commitment ID")
}
// Verify pool state updated
pool := zv.Pools[poolID]
if pool.TotalDeposits.Cmp(big.NewInt(1e18)) != 0 {
t.Error("Total deposits not updated")
}
}
// TestAddCommitmentPoolNotFound tests error for non-existent pool
func TestAddCommitmentPoolNotFound(t *testing.T) {
zv := NewZKVerifier()
nonExistent := [32]byte{0xFF}
_, err := zv.AddCommitment(nonExistent, &Commitment{})
if err != ErrPoolNotFound {
t.Errorf("Expected ErrPoolNotFound, got %v", err)
}
}
// TestAddCommitmentPoolDisabled tests error for disabled pool
func TestAddCommitmentPoolDisabled(t *testing.T) {
zv := NewZKVerifier()
owner := common.HexToAddress("0x1234567890123456789012345678901234567890")
token := common.HexToAddress("0xABCDABCDABCDABCDABCDABCDABCDABCDABCDABCD")
poolID, _ := zv.CreateConfidentialPool(owner, token, 32)
// Disable pool
zv.Pools[poolID].Enabled = false
_, err := zv.AddCommitment(poolID, &Commitment{Amount: big.NewInt(1)})
if err != ErrPoolDisabled {
t.Errorf("Expected ErrPoolDisabled, got %v", err)
}
}
// TestVerifyCommitmentInclusion tests merkle proof verification
func TestVerifyCommitmentInclusion(t *testing.T) {
zv := NewZKVerifier()
owner := common.HexToAddress("0x1234567890123456789012345678901234567890")
token := common.HexToAddress("0xABCDABCDABCDABCDABCDABCDABCDABCDABCDABCD")
poolID, _ := zv.CreateConfidentialPool(owner, token, 32)
// Test with empty proof (function still works for basic check)
valid, err := zv.VerifyCommitmentInclusion(poolID, [32]byte{0x01}, [][]byte{}, 0)
if err != nil {
t.Fatalf("VerifyCommitmentInclusion failed: %v", err)
}
_ = valid // Returns true for empty root (valid by convention)
}
// TestRegisterRollup tests rollup registration
func TestRegisterRollup(t *testing.T) {
zv := NewZKVerifier()
owner := common.HexToAddress("0x1234567890123456789012345678901234567890")
sequencer := common.HexToAddress("0xABCDABCDABCDABCDABCDABCDABCDABCDABCDABCD")
// First register a verifying key
vkID, _ := zv.RegisterVerifyingKey(
owner,
ProofSystemGroth16,
CircuitRollupBatch,
[]byte("alpha"), []byte("beta"), []byte("gamma"), []byte("delta"),
[][]byte{[]byte("ic0")},
)
rollupID, err := zv.RegisterRollup(
owner,
vkID,
ProofSystemGroth16,
1000, // max tx per batch
60, // batch interval
sequencer,
)
if err != nil {
t.Fatalf("RegisterRollup failed: %v", err)
}
if rollupID == [32]byte{} {
t.Error("Expected non-zero rollup ID")
}
// Verify rollup config
config := zv.Rollups[rollupID]
if config == nil {
t.Fatal("Rollup config not stored")
}
if config.Owner != owner {
t.Error("Owner mismatch")
}
if config.Sequencer != sequencer {
t.Error("Sequencer mismatch")
}
if config.MaxTxPerBatch != 1000 {
t.Errorf("Expected max tx 1000, got %d", config.MaxTxPerBatch)
}
if !config.Enabled {
t.Error("Expected rollup to be enabled")
}
// Verify state initialized
state := zv.RollupStates[rollupID]
if state == nil {
t.Fatal("Rollup state not initialized")
}
}
// TestRegisterRollupInvalidKey tests error for non-existent verifying key
func TestRegisterRollupInvalidKey(t *testing.T) {
zv := NewZKVerifier()
owner := common.HexToAddress("0x1234567890123456789012345678901234567890")
nonExistent := [32]byte{0xFF}
_, err := zv.RegisterRollup(owner, nonExistent, ProofSystemGroth16, 1000, 60, owner)
if err != ErrInvalidVerifyingKey {
t.Errorf("Expected ErrInvalidVerifyingKey, got %v", err)
}
}
// TestVerifyRollupBatch tests batch verification
// Note: With real cryptographic verification, invalid proof data is rejected.
// This test verifies the error handling for invalid proofs.
func TestVerifyRollupBatch(t *testing.T) {
zv := NewZKVerifier()
owner := common.HexToAddress("0x1234567890123456789012345678901234567890")
// Setup rollup with test VK (valid format but won't verify real proofs)
vkID, _ := zv.RegisterVerifyingKey(
owner,
ProofSystemGroth16,
CircuitRollupBatch,
[]byte("alpha"), []byte("beta"), []byte("gamma"), []byte("delta"),
[][]byte{[]byte("ic0"), []byte("ic1"), []byte("ic2"), []byte("ic3"), []byte("ic4")},
)
rollupID, _ := zv.RegisterRollup(owner, vkID, ProofSystemGroth16, 1000, 60, owner)
// Create batch with test proof data (invalid curve points)
batch := &RollupBatch{
BatchID: [32]byte{0x01},
RollupID: rollupID,
PrevStateRoot: [32]byte{}, // Matches initial state
NewStateRoot: [32]byte{0x02},
Transactions: 500,
L1BatchNum: 1,
Proposer: owner,
Proof: &Proof{
A: make([]byte, 64),
B: make([]byte, 128),
C: make([]byte, 64),
},
}
// With real crypto verification, invalid proof data should fail
err := zv.VerifyRollupBatch(rollupID, batch)
if err != ErrInvalidProof {
t.Errorf("Expected ErrInvalidProof for invalid proof data, got %v", err)
}
}
// TestVerifyRollupBatchStateUpdate tests that valid state updates work
// when cryptographic verification is bypassed (integration test pattern)
func TestVerifyRollupBatchStateUpdate(t *testing.T) {
zv := NewZKVerifier()
owner := common.HexToAddress("0x1234567890123456789012345678901234567890")
// Setup rollup
vkID, _ := zv.RegisterVerifyingKey(
owner,
ProofSystemGroth16,
CircuitRollupBatch,
[]byte("alpha"), []byte("beta"), []byte("gamma"), []byte("delta"),
[][]byte{[]byte("ic0"), []byte("ic1"), []byte("ic2"), []byte("ic3"), []byte("ic4")},
)
rollupID, _ := zv.RegisterRollup(owner, vkID, ProofSystemGroth16, 1000, 60, owner)
// Verify initial state
state, err := zv.GetRollupState(rollupID)
if err != nil {
t.Fatalf("GetRollupState failed: %v", err)
}
if state.TotalBatches != 0 {
t.Errorf("Expected 0 initial batches, got %d", state.TotalBatches)
}
if state.LastStateRoot != ([32]byte{}) {
t.Error("Expected empty initial state root")
}
}
// TestVerifyRollupBatchNotFound tests error for non-existent rollup
func TestVerifyRollupBatchNotFound(t *testing.T) {
zv := NewZKVerifier()
nonExistent := [32]byte{0xFF}
err := zv.VerifyRollupBatch(nonExistent, &RollupBatch{})
if err != ErrRollupNotFound {
t.Errorf("Expected ErrRollupNotFound, got %v", err)
}
}
// TestVerifyRollupBatchUnauthorized tests proposer authorization
func TestVerifyRollupBatchUnauthorized(t *testing.T) {
zv := NewZKVerifier()
owner := common.HexToAddress("0x1234567890123456789012345678901234567890")
sequencer := common.HexToAddress("0xABCDABCDABCDABCDABCDABCDABCDABCDABCDABCD")
unauthorized := common.HexToAddress("0x1111111111111111111111111111111111111111")
vkID, _ := zv.RegisterVerifyingKey(
owner,
ProofSystemGroth16,
CircuitRollupBatch,
[]byte("alpha"), []byte("beta"), []byte("gamma"), []byte("delta"),
[][]byte{[]byte("ic0")},
)
rollupID, _ := zv.RegisterRollup(owner, vkID, ProofSystemGroth16, 1000, 60, sequencer)
batch := &RollupBatch{
RollupID: rollupID,
Proposer: unauthorized,
}
err := zv.VerifyRollupBatch(rollupID, batch)
if err != ErrUnauthorizedProposer {
t.Errorf("Expected ErrUnauthorizedProposer, got %v", err)
}
}
// TestVerifyRollupBatchTooLarge tests batch size validation
func TestVerifyRollupBatchTooLarge(t *testing.T) {
zv := NewZKVerifier()
owner := common.HexToAddress("0x1234567890123456789012345678901234567890")
vkID, _ := zv.RegisterVerifyingKey(
owner,
ProofSystemGroth16,
CircuitRollupBatch,
[]byte("alpha"), []byte("beta"), []byte("gamma"), []byte("delta"),
[][]byte{[]byte("ic0")},
)
rollupID, _ := zv.RegisterRollup(owner, vkID, ProofSystemGroth16, 100, 60, owner)
batch := &RollupBatch{
RollupID: rollupID,
Transactions: 500, // Exceeds max 100
Proposer: owner,
}
err := zv.VerifyRollupBatch(rollupID, batch)
if err != ErrBatchTooLarge {
t.Errorf("Expected ErrBatchTooLarge, got %v", err)
}
}
// TestVerifyRollupBatchInvalidStateRoot tests state root validation
func TestVerifyRollupBatchInvalidStateRoot(t *testing.T) {
zv := NewZKVerifier()
owner := common.HexToAddress("0x1234567890123456789012345678901234567890")
vkID, _ := zv.RegisterVerifyingKey(
owner,
ProofSystemGroth16,
CircuitRollupBatch,
[]byte("alpha"), []byte("beta"), []byte("gamma"), []byte("delta"),
[][]byte{[]byte("ic0")},
)
rollupID, _ := zv.RegisterRollup(owner, vkID, ProofSystemGroth16, 1000, 60, owner)
batch := &RollupBatch{
RollupID: rollupID,
PrevStateRoot: [32]byte{0xFF}, // Doesn't match current state (empty)
Transactions: 10,
Proposer: owner,
}
err := zv.VerifyRollupBatch(rollupID, batch)
if err != ErrInvalidStateRoot {
t.Errorf("Expected ErrInvalidStateRoot, got %v", err)
}
}
// TestGetRollupState tests state retrieval
func TestGetRollupState(t *testing.T) {
zv := NewZKVerifier()
owner := common.HexToAddress("0x1234567890123456789012345678901234567890")
vkID, _ := zv.RegisterVerifyingKey(
owner, ProofSystemGroth16, CircuitRollupBatch,
[]byte("alpha"), []byte("beta"), []byte("gamma"), []byte("delta"),
[][]byte{[]byte("ic0")},
)
rollupID, _ := zv.RegisterRollup(owner, vkID, ProofSystemGroth16, 1000, 60, owner)
state, err := zv.GetRollupState(rollupID)
if err != nil {
t.Fatalf("GetRollupState failed: %v", err)
}
if state == nil {
t.Fatal("Expected non-nil state")
}
if state.TotalBatches != 0 {
t.Errorf("Expected 0 batches, got %d", state.TotalBatches)
}
}
// TestGetRollupStateNotFound tests error for non-existent rollup
func TestGetRollupStateNotFound(t *testing.T) {
zv := NewZKVerifier()
nonExistent := [32]byte{0xFF}
_, err := zv.GetRollupState(nonExistent)
if err != ErrRollupNotFound {
t.Errorf("Expected ErrRollupNotFound, got %v", err)
}
}
// TestVerifyKZGNotInitialized tests KZG verification without setup
func TestVerifyKZGNotInitialized(t *testing.T) {
zv := NewZKVerifier()
_, err := zv.VerifyKZG([]byte("commit"), big.NewInt(1), big.NewInt(2), []byte("proof"))
if err == nil {
t.Error("Expected error for uninitialized KZG setup")
}
}
// TestVerifyRangeProof tests range proof verification
func TestVerifyRangeProof(t *testing.T) {
zv := NewZKVerifier()
commitment := make([]byte, 32)
rangeProof := make([]byte, 128)
valid, err := zv.VerifyRangeProof(commitment, rangeProof, 64)
if err != nil {
t.Fatalf("VerifyRangeProof failed: %v", err)
}
_ = valid // Returns true for range proof structural validation
}
// TestVerificationStatistics tests stats tracking
func TestVerificationStatistics(t *testing.T) {
zv := NewZKVerifier()
owner := common.HexToAddress("0x1234567890123456789012345678901234567890")
// Register keys (use different values to avoid keyID collision)
grothKeyID, err := zv.RegisterVerifyingKey(
owner, ProofSystemGroth16, CircuitTransfer,
[]byte("groth16_alpha"), []byte("groth16_beta"), []byte("groth16_gamma"), []byte("groth16_delta"),
[][]byte{[]byte("ic0"), []byte("ic1")},
)
if err != nil {
t.Fatalf("Failed to register Groth16 key: %v", err)
}
plonkKeyID, err := zv.RegisterVerifyingKey(
owner, ProofSystemPlonk, CircuitTransfer,
[]byte("plonk_alpha"), []byte("plonk_beta"), []byte("plonk_gamma"), []byte("plonk_delta"),
[][]byte{[]byte("ic0")},
)
if err != nil {
t.Fatalf("Failed to register PLONK key: %v", err)
}
// Perform verifications - each should increment TotalVerifications
_, err = zv.VerifyGroth16(grothKeyID, []byte("a"), []byte("b"), []byte("c"), []*big.Int{big.NewInt(1)})
if err != nil {
t.Logf("VerifyGroth16 #1 error: %v", err)
}
_, err = zv.VerifyGroth16(grothKeyID, []byte("a"), []byte("b"), []byte("c"), []*big.Int{big.NewInt(2)})
if err != nil {
t.Logf("VerifyGroth16 #2 error: %v", err)
}
_, err = zv.VerifyPlonk(plonkKeyID, []byte("proof"), []*big.Int{})
if err != nil {
t.Logf("VerifyPlonk error: %v", err)
}
if zv.TotalVerifications != 3 {
t.Errorf("Expected 3 verifications, got %d", zv.TotalVerifications)
}
}
// Benchmark tests
func BenchmarkVerifyGroth16(b *testing.B) {
zv := NewZKVerifier()
owner := common.HexToAddress("0x1234567890123456789012345678901234567890")
keyID, _ := zv.RegisterVerifyingKey(
owner, ProofSystemGroth16, CircuitTransfer,
[]byte("alpha"), []byte("beta"), []byte("gamma"), []byte("delta"),
[][]byte{[]byte("ic0"), []byte("ic1")},
)
proofA := make([]byte, 64)
proofB := make([]byte, 128)
proofC := make([]byte, 64)
publicInputs := []*big.Int{big.NewInt(100)}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = zv.VerifyGroth16(keyID, proofA, proofB, proofC, publicInputs)
}
}
func BenchmarkVerifyPlonk(b *testing.B) {
zv := NewZKVerifier()
owner := common.HexToAddress("0x1234567890123456789012345678901234567890")
keyID, _ := zv.RegisterVerifyingKey(
owner, ProofSystemPlonk, CircuitTransfer,
[]byte("alpha"), []byte("beta"), []byte("gamma"), []byte("delta"),
[][]byte{[]byte("ic0")},
)
proof := make([]byte, 512)
publicInputs := []*big.Int{big.NewInt(100)}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = zv.VerifyPlonk(keyID, proof, publicInputs)
}
}
func BenchmarkSpendNullifier(b *testing.B) {
zv := NewZKVerifier()
txHash := common.HexToHash("0xABCDEF")
b.ResetTimer()
for i := 0; i < b.N; i++ {
nullifier := [32]byte{byte(i >> 24), byte(i >> 16), byte(i >> 8), byte(i)}
_ = zv.SpendNullifier(nullifier, txHash, uint64(i))
}
}
func BenchmarkCheckNullifier(b *testing.B) {
zv := NewZKVerifier()
// Pre-populate some nullifiers
for i := range 10000 {
nullifier := [32]byte{byte(i >> 24), byte(i >> 16), byte(i >> 8), byte(i)}
zv.Nullifiers[nullifier] = &Nullifier{}
}
nullifier := [32]byte{0x00, 0x00, 0x27, 0x10} // 10000
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = zv.CheckNullifier(nullifier)
}
}
// TestBN256PairingCheck tests that the BN256 pairing operations work correctly
// using real curve points from the BN254 curve.
func TestBN256PairingCheck(t *testing.T) {
// Test that identity pairing check passes
// e(0, 0) = 1 (empty product is identity)
emptyG1 := []*bn256.G1{}
emptyG2 := []*bn256.G2{}
if !bn256.PairingCheck(emptyG1, emptyG2) {
t.Error("Empty pairing check should return true")
}
}
// TestGroth16WithRealCurvePoints tests Groth16 verification with properly
// formatted curve points to verify parsing and verification logic.
func TestGroth16WithRealCurvePoints(t *testing.T) {
zv := NewZKVerifier()
owner := common.HexToAddress("0x1234567890123456789012345678901234567890")
// Create G1 point at infinity (all zeros in x,y)
g1Infinity := make([]byte, 64)
// Create G2 point at infinity (all zeros in x,y components)
g2Infinity := make([]byte, 128)
// Register a key with points at infinity (valid curve points)
keyID, err := zv.RegisterVerifyingKey(
owner,
ProofSystemGroth16,
CircuitTransfer,
g1Infinity, // alpha (G1)
g2Infinity, // beta (G2)
g2Infinity, // gamma (G2)
g2Infinity, // delta (G2)
[][]byte{g1Infinity, g1Infinity}, // IC points (G1)
)
if err != nil {
t.Fatalf("RegisterVerifyingKey failed: %v", err)
}
// Try to verify with infinity points
proofA := g1Infinity
proofB := g2Infinity
proofC := g1Infinity
publicInputs := []*big.Int{big.NewInt(0)}
result, err := zv.VerifyGroth16(keyID, proofA, proofB, proofC, publicInputs)
if err != nil {
t.Fatalf("VerifyGroth16 returned error: %v", err)
}
// Note: Mathematically, identity points (infinity) satisfy the pairing equation
// because e(O, Q) = 1 for the identity O in G1, and ∏ 1 = 1.
// This is correct crypto behavior - the test verifies that parsing works
// and the verification completes without errors.
// Real proofs with proper curve points would need to satisfy e(A,B) = e(α,β)·e(vk_x,γ)·e(C,δ)
if result.ProofSystem != ProofSystemGroth16 {
t.Errorf("Expected ProofSystemGroth16, got %v", result.ProofSystem)
}
}
// TestKZGPointEvaluationInvalidSizes tests that KZG verification rejects invalid input sizes
func TestKZGPointEvaluationInvalidSizes(t *testing.T) {
zv := NewZKVerifier()
// Setup with a KZGSetup to pass the nil check
zv.KZGSetup = &KZGSetup{
MaxDegree: 4096,
}
testCases := []struct {
name string
commitment []byte
proof []byte
wantValid bool
}{
{"commitment too short", make([]byte, 32), make([]byte, 48), false},
{"proof too short", make([]byte, 48), make([]byte, 32), false},
{"both too short", make([]byte, 32), make([]byte, 32), false},
{"correct sizes but invalid data", make([]byte, 48), make([]byte, 48), false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
valid, _ := zv.VerifyKZG(tc.commitment, big.NewInt(1), big.NewInt(2), tc.proof)
if valid != tc.wantValid {
t.Errorf("Expected valid=%v, got %v", tc.wantValid, valid)
}
})
}
}
// TestCurvePointParsing tests that curve points can be correctly parsed
func TestCurvePointParsing(t *testing.T) {
// BN254 G1 generator point coordinates (from Ethereum precompile test vectors)
// G1 = (1, 2)
g1GenX := "0000000000000000000000000000000000000000000000000000000000000001"
g1GenY := "0000000000000000000000000000000000000000000000000000000000000002"
g1Bytes, _ := hex.DecodeString(g1GenX + g1GenY)
var g1 bn256.G1
n, err := g1.Unmarshal(g1Bytes)
if err != nil {
t.Fatalf("Failed to unmarshal G1 generator: %v", err)
}
if n != 64 {
t.Errorf("Expected to consume 64 bytes, consumed %d", n)
}
// Verify serialization roundtrip
serialized := g1.Marshal()
if len(serialized) != 64 {
t.Errorf("Expected 64 bytes, got %d", len(serialized))
}
}
// TestPLONKProofSizeValidation tests that PLONK verification rejects undersized proofs
func TestPLONKProofSizeValidation(t *testing.T) {
zv := NewZKVerifier()
owner := common.HexToAddress("0x1234567890123456789012345678901234567890")
// Create minimal valid-format VK
g1Point := make([]byte, 64)
g2Point := make([]byte, 128)
// PLONK VK needs 9 IC entries (8 G1 selector points + 1 G2 X2 point)
ic := make([][]byte, 9)
for i := range 8 {
ic[i] = g1Point
}
ic[8] = g2Point
keyID, _ := zv.RegisterVerifyingKey(
owner,
ProofSystemPlonk,
CircuitTransfer,
g1Point,
g2Point,
g2Point,
g2Point,
ic,
)
// Test with undersized proof
shortProof := make([]byte, 100) // Less than required 768 bytes
result, err := zv.VerifyPlonk(keyID, shortProof, []*big.Int{})
if err != nil {
t.Fatalf("VerifyPlonk returned error: %v", err)
}
if result.Valid {
t.Error("Expected undersized proof to fail verification")
}
}
// =============================================================================
// Halo2 Verifier Tests
// =============================================================================
// TestParseHalo2Proof tests Halo2 proof parsing
func TestParseHalo2Proof(t *testing.T) {
// Build a valid Halo2 proof structure
proof := buildTestHalo2Proof(2, 3, 2, 8) // 2 inputs, 3 advice, 2 instance, 8 rounds
parsed, err := parseHalo2Proof(proof)
if err != nil {
t.Fatalf("parseHalo2Proof failed: %v", err)
}
if len(parsed.publicInputs) != 2 {
t.Errorf("Expected 2 public inputs, got %d", len(parsed.publicInputs))
}
if len(parsed.adviceCommitments) != 3 {
t.Errorf("Expected 3 advice commitments, got %d", len(parsed.adviceCommitments))
}
if len(parsed.instanceCommitments) != 2 {
t.Errorf("Expected 2 instance commitments, got %d", len(parsed.instanceCommitments))
}
if parsed.numRounds != 8 {
t.Errorf("Expected 8 rounds, got %d", parsed.numRounds)
}
if len(parsed.lPoints) != 8 {
t.Errorf("Expected 8 L points, got %d", len(parsed.lPoints))
}
if len(parsed.rPoints) != 8 {
t.Errorf("Expected 8 R points, got %d", len(parsed.rPoints))
}
}
// TestParseHalo2ProofTooShort tests error handling for truncated proofs
func TestParseHalo2ProofTooShort(t *testing.T) {
// Test minimum length check
shortData := make([]byte, 35) // Less than 36 bytes minimum
_, err := parseHalo2Proof(shortData)
if err == nil {
t.Error("Expected error for data shorter than 36 bytes")
}
// Test truncated public inputs
truncated := make([]byte, 40)
// vkID: 32 bytes, numInputs: 4 (value = 10), then only 4 bytes remaining
copy(truncated[32:36], []byte{0, 0, 0, 10}) // 10 inputs would need 320 bytes
_, err = parseHalo2Proof(truncated)
if err == nil {
t.Error("Expected error for truncated public inputs")
}
}
// TestParseHalo2ProofInvalidRounds tests error for invalid round count
func TestParseHalo2ProofInvalidRounds(t *testing.T) {
// Build proof with 0 rounds (invalid)
proof := buildTestHalo2ProofWithRounds(0)
_, err := parseHalo2Proof(proof)
if err == nil {
t.Error("Expected error for 0 rounds")
}
// Build proof with 33 rounds (invalid - max is 32)
proof = buildTestHalo2ProofWithRounds(33)
_, err = parseHalo2Proof(proof)
if err == nil {
t.Error("Expected error for 33 rounds")
}
}
// TestVerifyHalo2Structural tests structural validation
func TestVerifyHalo2Structural(t *testing.T) {
// Build valid proof structure
proofData := buildTestHalo2Proof(1, 2, 1, 8)
proof, err := parseHalo2Proof(proofData)
if err != nil {
t.Fatalf("parseHalo2Proof failed: %v", err)
}
// Structural validation should pass but return error (no VK)
valid, err := verifyHalo2Structural(proof)
if valid {
t.Error("Expected structural validation to return false without VK")
}
if err == nil {
t.Error("Expected error message about needing verification key")
}
if err != nil && err.Error() != "halo2: structural validation passed - full verification requires registered verification key" {
t.Errorf("Unexpected error: %v", err)
}
}
// TestVerifyHalo2StructuralInvalidCommitment tests rejection of invalid commitments
func TestVerifyHalo2StructuralInvalidCommitment(t *testing.T) {
// Build proof with zero commitment (invalid)
proofData := buildTestHalo2Proof(1, 1, 1, 8)
proof, _ := parseHalo2Proof(proofData)
// Zero out an advice commitment
proof.adviceCommitments[0] = make([]byte, 32) // All zeros = invalid
valid, err := verifyHalo2Structural(proof)
if valid {
t.Error("Expected invalid commitment to fail")
}
if err == nil || err.Error() != "halo2: invalid advice commitment at index 0" {
t.Errorf("Expected specific error about invalid advice commitment, got: %v", err)
}
}
// TestVerifyHalo2StructuralInvalidLPoint tests rejection of invalid L points
func TestVerifyHalo2StructuralInvalidLPoint(t *testing.T) {
proofData := buildTestHalo2Proof(1, 1, 1, 8)
proof, _ := parseHalo2Proof(proofData)
// Zero out an L point
proof.lPoints[3] = make([]byte, 32) // All zeros = invalid
valid, err := verifyHalo2Structural(proof)
if valid {
t.Error("Expected invalid L point to fail")
}
if err == nil || err.Error() != "halo2: invalid L point at round 3" {
t.Errorf("Expected specific error about invalid L point, got: %v", err)
}
}
// TestVerifyHalo2Full tests full verification with registered key
func TestVerifyHalo2Full(t *testing.T) {
proofData := buildTestHalo2Proof(1, 2, 1, 8)
proof, err := parseHalo2Proof(proofData)
if err != nil {
t.Fatalf("parseHalo2Proof failed: %v", err)
}
// Create a verification key for Halo2
vk := &VerifyingKey{
KeyID: proof.vkID,
ProofSystem: ProofSystemHalo2,
CircuitType: CircuitTransfer,
IC: make([][]byte, 3), // Enough for 1 public input + 2
}
for i := range vk.IC {
vk.IC[i] = make([]byte, 32)
vk.IC[i][0] = byte(i + 1) // Non-zero
}
// Full verification should complete
valid, err := verifyHalo2Full(proof, vk)
if err != nil {
t.Fatalf("verifyHalo2Full failed: %v", err)
}
// Structural validation passes; actual curve point verification
// is performed by the on-chain BN254/Pallas precompile calls.
_ = valid
}
// TestComputeHalo2Challenges tests Fiat-Shamir challenge generation
func TestComputeHalo2Challenges(t *testing.T) {
proofData := buildTestHalo2Proof(2, 3, 2, 8)
proof, _ := parseHalo2Proof(proofData)
challenges, err := computeHalo2Challenges(proof)
if err != nil {
t.Fatalf("computeHalo2Challenges failed: %v", err)
}
if len(challenges) != 8 {
t.Errorf("Expected 8 challenges, got %d", len(challenges))
}
// Verify challenges are non-zero and 32 bytes
for i, ch := range challenges {
if len(ch) != 32 {
t.Errorf("Challenge %d is %d bytes, expected 32", i, len(ch))
}
if isZeroBytes(ch) {
t.Errorf("Challenge %d is zero", i)
}
}
// Verify challenges are deterministic
challenges2, _ := computeHalo2Challenges(proof)
for i := range challenges {
for j := range challenges[i] {
if challenges[i][j] != challenges2[i][j] {
t.Errorf("Challenges are not deterministic at round %d", i)
break
}
}
}
}
// TestModInverse tests modular inverse computation
func TestModInverse(t *testing.T) {
// Test with a simple value
a := make([]byte, 32)
a[31] = 7 // a = 7
inv, err := modInverse(a)
if err != nil {
t.Fatalf("modInverse failed: %v", err)
}
if len(inv) != 32 {
t.Errorf("Expected 32 bytes, got %d", len(inv))
}
// Test zero returns error
zero := make([]byte, 32)
_, err = modInverse(zero)
if err == nil {
t.Error("Expected error for zero inverse")
}
}
// TestIsValidCompressedPoint tests real Pallas compressed-point validation.
func TestIsValidCompressedPoint(t *testing.T) {
// A genuine on-curve Pallas point: the standard generator (x = -1 ≡ p-1,
// y = 2), since (-1)^3 + 5 = 4 = 2^2. Encoded as 32-byte big-endian x with
// the sign bit clear (p-1 < 2^255).
x := new(big.Int).Sub(pallasFieldModulus, big.NewInt(1))
valid := make([]byte, 32)
copy(valid[32-len(x.Bytes()):], x.Bytes())
if !isValidCompressedPoint(valid) {
t.Error("Expected on-curve Pallas point to pass")
}
// Invalid: wrong size
if isValidCompressedPoint(make([]byte, 31)) {
t.Error("Expected wrong size to fail")
}
// Invalid: all zeros (identity)
if isValidCompressedPoint(make([]byte, 32)) {
t.Error("Expected zero point to fail")
}
// Invalid: non-canonical x >= field modulus (all 0xFF, sign bit cleared
// => x = 2^255 - 1 > p).
overflow := make([]byte, 32)
for i := range overflow {
overflow[i] = 0xFF
}
overflow[0] &^= 0x80
if isValidCompressedPoint(overflow) {
t.Error("Expected non-canonical x >= p to fail")
}
// Invalid: off-curve x (x^3 + 5 is a quadratic non-residue mod p).
offX := big.NewInt(2)
for {
rhs := new(big.Int).Mul(offX, offX)
rhs.Mul(rhs, offX)
rhs.Add(rhs, big.NewInt(5))
rhs.Mod(rhs, pallasFieldModulus)
if !isQuadraticResidue(rhs, pallasFieldModulus) {
break // found an x with no curve point
}
offX.Add(offX, big.NewInt(1))
}
off := make([]byte, 32)
copy(off[32-len(offX.Bytes()):], offX.Bytes())
if isValidCompressedPoint(off) {
t.Errorf("Expected off-curve point (x=%s) to fail", offX)
}
}
// TestHalo2Transcript tests transcript operations
func TestHalo2Transcript(t *testing.T) {
tr := newHalo2Transcript()
// Add some data
tr.appendScalar([]byte{1, 2, 3})
tr.appendPoint([]byte{4, 5, 6})
// Get challenge
ch1 := tr.challenge()
if len(ch1) != 32 {
t.Errorf("Expected 32-byte challenge, got %d", len(ch1))
}
// Get another challenge (should be different)
ch2 := tr.challenge()
if len(ch2) != 32 {
t.Errorf("Expected 32-byte challenge, got %d", len(ch2))
}
// Challenges should differ
same := true
for i := range ch1 {
if ch1[i] != ch2[i] {
same = false
break
}
}
if same {
t.Error("Consecutive challenges should be different")
}
}
// TestVerifyHalo2Precompile tests the precompile interface
func TestVerifyHalo2Precompile(t *testing.T) {
p := &zkVerifyPrecompile{
verifier: NewZKVerifier(),
}
// Build valid proof data
proofData := buildTestHalo2Proof(1, 2, 1, 8)
// Should parse successfully but fail verification (no VK registered)
valid, err := p.verifyHalo2(proofData)
if valid {
t.Error("Expected false without registered VK")
}
if err == nil {
t.Error("Expected error without registered VK")
}
// Test with invalid (too short) data
_, err = p.verifyHalo2(make([]byte, 30))
if err == nil {
t.Error("Expected error for short data")
}
}
// Helper function to build test Halo2 proof data
// realCompressedPallasPoint returns a valid on-curve compressed Pallas point,
// deterministically derived from seed (distinct seeds give distinct points).
// Used so test proof fixtures present points that pass isValidCompressedPoint.
func realCompressedPallasPoint(seed byte) []byte {
x := new(big.Int).SetBytes([]byte{seed, 0xA5, 0x5A, 0x01})
for {
rhs := new(big.Int).Mul(x, x)
rhs.Mul(rhs, x)
rhs.Add(rhs, big.NewInt(5))
rhs.Mod(rhs, pallasFieldModulus)
if isQuadraticResidue(rhs, pallasFieldModulus) {
break
}
x.Add(x, big.NewInt(1))
}
out := make([]byte, 32)
xb := x.Bytes()
copy(out[32-len(xb):], xb)
return out
}
func buildTestHalo2Proof(numInputs, numAdvice, numInstance int, numRounds uint32) []byte {
// Calculate total size
size := 32 + // vkID
4 + numInputs*32 + // public inputs
4 + numAdvice*32 + // advice commitments
4 + numInstance*32 + // instance commitments
4 + int(numRounds)*32 + // L points
int(numRounds)*32 + // R points
32 + // a_scalar
32 + // evalPoint
32 // claimedEval
data := make([]byte, size)
offset := 0
// vkID (32 bytes with non-zero content)
for i := range 32 {
data[offset+i] = byte(i + 1)
}
offset += 32
// numInputs (4 bytes)
data[offset] = byte(numInputs >> 24)
data[offset+1] = byte(numInputs >> 16)
data[offset+2] = byte(numInputs >> 8)
data[offset+3] = byte(numInputs)
offset += 4
// public inputs (numInputs * 32 bytes)
for i := range numInputs {
data[offset+31] = byte(i + 1) // Non-zero value
offset += 32
}
// numAdvice (4 bytes)
data[offset] = byte(numAdvice >> 24)
data[offset+1] = byte(numAdvice >> 16)
data[offset+2] = byte(numAdvice >> 8)
data[offset+3] = byte(numAdvice)
offset += 4
// advice commitments (numAdvice * 32 bytes) - real on-curve points
for i := range numAdvice {
copy(data[offset:offset+32], realCompressedPallasPoint(byte(i+0x10)))
offset += 32
}
// numInstance (4 bytes)
data[offset] = byte(numInstance >> 24)
data[offset+1] = byte(numInstance >> 16)
data[offset+2] = byte(numInstance >> 8)
data[offset+3] = byte(numInstance)
offset += 4
// instance commitments (numInstance * 32 bytes) - real on-curve points
for i := range numInstance {
copy(data[offset:offset+32], realCompressedPallasPoint(byte(i+0x20)))
offset += 32
}
// numRounds (4 bytes)
data[offset] = byte(numRounds >> 24)
data[offset+1] = byte(numRounds >> 16)
data[offset+2] = byte(numRounds >> 8)
data[offset+3] = byte(numRounds)
offset += 4
// L points (numRounds * 32 bytes) - real on-curve points
for i := range numRounds {
copy(data[offset:offset+32], realCompressedPallasPoint(byte(i+0x30)))
offset += 32
}
// R points (numRounds * 32 bytes) - real on-curve points
for i := range numRounds {
copy(data[offset:offset+32], realCompressedPallasPoint(byte(i+0x40)))
offset += 32
}
// a_scalar (32 bytes) - non-zero
data[offset] = 0x50
offset += 32
// evalPoint (32 bytes) - non-zero
data[offset] = 0x60
offset += 32
// claimedEval (32 bytes) - non-zero
data[offset] = 0x70
return data
}
// Helper to build proof with specific round count for edge case testing
func buildTestHalo2ProofWithRounds(numRounds uint32) []byte {
// Minimal proof: 0 inputs, 0 advice, 0 instance
size := 32 + 4 + 4 + 4 + 4 // vkID + numInputs + numAdvice + numInstance + numRounds
if numRounds > 0 && numRounds <= 32 {
size += int(numRounds)*32 + int(numRounds)*32 + 32 + 32 + 32
}
data := make([]byte, size)
offset := 0
// vkID
data[0] = 1 // Non-zero
offset = 32
// numInputs = 0
offset += 4
// numAdvice = 0
offset += 4
// numInstance = 0
offset += 4
// numRounds
data[offset] = byte(numRounds >> 24)
data[offset+1] = byte(numRounds >> 16)
data[offset+2] = byte(numRounds >> 8)
data[offset+3] = byte(numRounds)
return data
}
// Benchmark for Halo2 proof parsing
func BenchmarkParseHalo2Proof(b *testing.B) {
proof := buildTestHalo2Proof(4, 10, 2, 8)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = parseHalo2Proof(proof)
}
}
// Benchmark for Halo2 challenge computation
func BenchmarkComputeHalo2Challenges(b *testing.B) {
proofData := buildTestHalo2Proof(4, 10, 2, 8)
proof, _ := parseHalo2Proof(proofData)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = computeHalo2Challenges(proof)
}
}
// Benchmark for Halo2 structural validation
func BenchmarkVerifyHalo2Structural(b *testing.B) {
proofData := buildTestHalo2Proof(4, 10, 2, 8)
proof, _ := parseHalo2Proof(proofData)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = verifyHalo2Structural(proof)
}
}
// ============================================================================
// fflonk verification tests
// ============================================================================
// TestParseFflonkProof tests fflonk proof parsing
func TestParseFflonkProof(t *testing.T) {
// Build valid fflonk proof: 4*48 + 8*32 = 448 bytes minimum
proof := buildTestFflonkProof(8)
fp, err := parseFflonkProof(proof)
if err != nil {
t.Fatalf("parseFflonkProof failed: %v", err)
}
if len(fp.C1) != 48 {
t.Errorf("Expected C1 length 48, got %d", len(fp.C1))
}
if len(fp.C2) != 48 {
t.Errorf("Expected C2 length 48, got %d", len(fp.C2))
}
if len(fp.W1) != 48 {
t.Errorf("Expected W1 length 48, got %d", len(fp.W1))
}
if len(fp.W2) != 48 {
t.Errorf("Expected W2 length 48, got %d", len(fp.W2))
}
if len(fp.Evaluations) != 8 {
t.Errorf("Expected 8 evaluations, got %d", len(fp.Evaluations))
}
}
// TestParseFflonkProofTooShort tests error for short proof
func TestParseFflonkProofTooShort(t *testing.T) {
proof := make([]byte, 100) // Too short
_, err := parseFflonkProof(proof)
if err == nil {
t.Error("Expected error for short proof")
}
}
// TestValidateBLS12381G1Point tests BLS12-381 G1 point validation
func TestValidateBLS12381G1Point(t *testing.T) {
// Test point at infinity (all zeros)
infinity := make([]byte, 48)
err := validateBLS12381G1Point(infinity)
if err != nil {
t.Errorf("Expected point at infinity to be valid: %v", err)
}
// Test valid compressed point format (bit 7 set)
valid := make([]byte, 48)
valid[0] = 0x80 // Compression flag set
valid[1] = 0x01 // Some x-coordinate
err = validateBLS12381G1Point(valid)
if err != nil {
t.Errorf("Expected valid compressed point: %v", err)
}
// Test uncompressed point format (error: bit 7 not set)
uncompressed := make([]byte, 48)
uncompressed[0] = 0x00
uncompressed[1] = 0x01
err = validateBLS12381G1Point(uncompressed)
if err == nil {
t.Error("Expected error for uncompressed point")
}
// Test wrong length
wrongLen := make([]byte, 32)
err = validateBLS12381G1Point(wrongLen)
if err == nil {
t.Error("Expected error for wrong length")
}
// Test infinity flag set but non-zero bytes
badInfinity := make([]byte, 48)
badInfinity[0] = 0xC0 // Compression + infinity flags
badInfinity[1] = 0x01 // But has data
err = validateBLS12381G1Point(badInfinity)
if err == nil {
t.Error("Expected error for invalid infinity point")
}
}
// TestValidateBLS12381Scalar tests scalar field validation
func TestValidateBLS12381Scalar(t *testing.T) {
// Test valid scalar (small value)
valid := big.NewInt(12345)
err := validateBLS12381Scalar(valid)
if err != nil {
t.Errorf("Expected valid scalar: %v", err)
}
// Test zero (valid)
zero := big.NewInt(0)
err = validateBLS12381Scalar(zero)
if err != nil {
t.Errorf("Expected zero to be valid: %v", err)
}
// Test negative (invalid)
negative := big.NewInt(-1)
err = validateBLS12381Scalar(negative)
if err == nil {
t.Error("Expected error for negative scalar")
}
// Test nil (invalid)
err = validateBLS12381Scalar(nil)
if err == nil {
t.Error("Expected error for nil scalar")
}
// Test value >= field order (invalid)
tooLarge := new(big.Int)
tooLarge.SetString("73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001", 16)
err = validateBLS12381Scalar(tooLarge)
if err == nil {
t.Error("Expected error for scalar >= field order")
}
}
// TestComputeFflonkChallenge tests Fiat-Shamir challenge computation
func TestComputeFflonkChallenge(t *testing.T) {
proof := buildTestFflonkProof(8)
inputs := []*big.Int{big.NewInt(100), big.NewInt(200)}
// Compute challenge
ch1 := computeFflonkChallenge(proof, inputs, []byte("test"))
// Verify it's non-zero
if ch1.Sign() == 0 {
t.Error("Expected non-zero challenge")
}
// Verify determinism
ch2 := computeFflonkChallenge(proof, inputs, []byte("test"))
if ch1.Cmp(ch2) != 0 {
t.Error("Challenge computation not deterministic")
}
// Different domain should produce different challenge
ch3 := computeFflonkChallenge(proof, inputs, []byte("other"))
if ch1.Cmp(ch3) == 0 {
t.Error("Different domains should produce different challenges")
}
}
// TestVerifyFflonkProof tests fflonk proof verification
func TestVerifyFflonkProof(t *testing.T) {
proof := buildTestFflonkProof(8)
inputs := []*big.Int{big.NewInt(100)}
// Verify proof structure (will fail KZG check with test data)
valid, err := verifyFflonkProof(proof, inputs)
// Test data won't pass KZG verification, but parsing should succeed
// The function should return false (invalid proof) not an error
if err != nil {
// If error, it should be about invalid point format, not parsing
t.Logf("verifyFflonkProof returned error: %v", err)
}
// With random test data, proof should be invalid
if valid {
t.Error("Random test data should not verify as valid")
}
}
// TestVerifyFflonkPrecompile tests the precompile interface
func TestVerifyFflonkPrecompile(t *testing.T) {
p := &zkVerifyPrecompile{
verifier: NewZKVerifier(),
}
// Build valid proof data: vkID(32) + numInputs(4) + inputs(n*32) + proof(448+)
proof := buildTestFflonkProof(8)
data := make([]byte, 36+32+len(proof))
// vkID (32 bytes)
for i := range 32 {
data[i] = byte(i + 1)
}
// numInputs = 1 (4 bytes big-endian)
data[35] = 1
// public input (32 bytes)
data[67] = 0x42
// proof data
copy(data[68:], proof)
// Should parse successfully but fail verification (test data)
valid, err := p.verifyFflonk(data)
if err != nil {
t.Logf("verifyFflonk returned error: %v", err)
}
if valid {
t.Error("Random test data should not verify as valid")
}
// Test with too short data
_, err = p.verifyFflonk(make([]byte, 30))
if err == nil {
t.Error("Expected error for short data")
}
}
// TestVerifyFflonkWithVerifier tests fflonk verification with registered VK
func TestVerifyFflonkWithVerifier(t *testing.T) {
zv := NewZKVerifier()
owner := common.HexToAddress("0x1234567890123456789012345678901234567890")
// Register fflonk verification key
keyID, err := zv.RegisterVerifyingKey(
owner,
ProofSystemFflonk,
CircuitTransfer,
[]byte("fflonk_alpha"),
[]byte("fflonk_beta"),
[]byte("fflonk_gamma"),
[]byte("fflonk_delta"),
[][]byte{[]byte("ic0")},
)
if err != nil {
t.Fatalf("Failed to register fflonk key: %v", err)
}
// Create proof data
proof := buildTestFflonkProof(8)
inputs := []*big.Int{big.NewInt(100)}
// Verify - should process but fail with test data
result, err := zv.VerifyFflonk(keyID, proof, inputs)
if err != nil {
t.Logf("VerifyFflonk returned error: %v", err)
}
if result != nil && result.Valid {
t.Error("Random test data should not verify as valid")
}
}
// TestVerifyFflonkInvalidKey tests error for non-existent key
func TestVerifyFflonkInvalidKey(t *testing.T) {
zv := NewZKVerifier()
nonExistent := [32]byte{0xFF}
proof := buildTestFflonkProof(8)
inputs := []*big.Int{big.NewInt(100)}
_, err := zv.VerifyFflonk(nonExistent, proof, inputs)
if err != ErrInvalidVerifyingKey {
t.Errorf("Expected ErrInvalidVerifyingKey, got %v", err)
}
}
// TestVerifyFflonkWrongProofSystem tests error for wrong proof system
func TestVerifyFflonkWrongProofSystem(t *testing.T) {
zv := NewZKVerifier()
owner := common.HexToAddress("0x1234567890123456789012345678901234567890")
// Register key as Groth16, not fflonk
keyID, _ := zv.RegisterVerifyingKey(
owner,
ProofSystemGroth16, // Wrong proof system
CircuitTransfer,
[]byte("alpha"),
[]byte("beta"),
[]byte("gamma"),
[]byte("delta"),
[][]byte{[]byte("ic0")},
)
proof := buildTestFflonkProof(8)
inputs := []*big.Int{big.NewInt(100)}
_, err := zv.VerifyFflonk(keyID, proof, inputs)
if err != ErrProofSystemMismatch {
t.Errorf("Expected ErrProofSystemMismatch, got %v", err)
}
}
// buildTestFflonkProof builds a test fflonk proof with the specified number of evaluations
func buildTestFflonkProof(numEvals int) []byte {
// Proof format: C1(48) + C2(48) + W1(48) + W2(48) + evals(n*32)
size := 4*48 + numEvals*32
proof := make([]byte, size)
offset := 0
// C1: valid compressed BLS12-381 G1 point format
proof[offset] = 0x80 // Compression flag
proof[offset+1] = 0x01
offset += 48
// C2: valid compressed BLS12-381 G1 point format
proof[offset] = 0x80
proof[offset+1] = 0x02
offset += 48
// W1: valid compressed BLS12-381 G1 point format
proof[offset] = 0x80
proof[offset+1] = 0x03
offset += 48
// W2: valid compressed BLS12-381 G1 point format
proof[offset] = 0x80
proof[offset+1] = 0x04
offset += 48
// Evaluations: field elements (small values that pass validation)
for i := range numEvals {
proof[offset+31] = byte(i + 1) // Small non-zero value
offset += 32
}
return proof
}
// BenchmarkParseFflonkProof benchmarks fflonk proof parsing
func BenchmarkParseFflonkProof(b *testing.B) {
proof := buildTestFflonkProof(8)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = parseFflonkProof(proof)
}
}
// BenchmarkComputeFflonkChallenge benchmarks challenge computation
func BenchmarkComputeFflonkChallenge(b *testing.B) {
proof := buildTestFflonkProof(8)
inputs := []*big.Int{big.NewInt(100), big.NewInt(200)}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = computeFflonkChallenge(proof, inputs, []byte("xi"))
}
}
// BenchmarkVerifyFflonk benchmarks full fflonk verification
func BenchmarkVerifyFflonk(b *testing.B) {
zv := NewZKVerifier()
owner := common.HexToAddress("0x1234567890123456789012345678901234567890")
keyID, _ := zv.RegisterVerifyingKey(
owner, ProofSystemFflonk, CircuitTransfer,
[]byte("alpha"), []byte("beta"), []byte("gamma"), []byte("delta"),
[][]byte{[]byte("ic0")},
)
proof := buildTestFflonkProof(8)
inputs := []*big.Int{big.NewInt(100)}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = zv.VerifyFflonk(keyID, proof, inputs)
}
}