mirror of
https://github.com/luxfi/precompile.git
synced 2026-07-27 03:33:45 +00:00
Under the enable-everything precompile policy these verify-only precompiles are no longer refused under strict-PQ, so any divergence between the luxfi/accel GPU/SIMD path and the pure-CPU path is a LIVE consensus split: GPU-equipped and CPU-only validators would return different verify verdicts on the same input and the chain forks. Several accel paths were also outright wrong-primitive forgery surfaces. LAW applied: the GPU verdict must be byte-identical to the CPU verdict on every input, or the accel path is a consensus hazard and is removed. CPU (pure-Go, CGO_ENABLED=0) is now the single source of truth for every on-chain verdict. None of these GPU kernels can be proven byte-identical here, and several are provably NOT, so every verdict-trusting accel branch is deleted and routed to the canonical CPU verifier. Determinism over speed. Active forgery/wrong-output in the DEFAULT build (accelcrypto CPU fallback leaked non-CPU semantics, err==nil): - sr25519: verified via the accel Ed25519 kernel -> a valid Ed25519 signature was ACCEPTED as a valid sr25519/Schnorrkel signature (wrong-curve forgery). - blake3: accelcrypto.Hash(HashBlake3) returns BLAKE2b, not BLAKE3; the "real blake3" fallback was dead code. OpHash256 + MerkleRoot emitted BLAKE2b. Latent splits (reachable only under -tags accel + a GPU backend): - frost: FROST-Schnorr verified through the SigECDSA kernel (wrong primitive + 32B x-only key mis-framed as a 33B ECDSA key). - cggmp21: SigECDSA kernel truncated the 65B key to 33B and skipped the CPU path's recover-and-compare binding (ignored the v byte). - ed25519: GPU Ed25519 kernel not proven byte-identical to the canonical verifier on cofactor / non-canonical-S / small-order edge cases. - secp256r1: P-256 inputs sent to the fixed-curve secp256k1 ECDSA kernel. - mldsa: GPU verified with EMPTY context; precompile binds the FIPS-204 ctx "lux-evm-precompile-mldsa-v1" -> different mu, different verdict. - bls12381 / zk(groth16): GPU MSM result trusted without re-verification and with no curve binding carried to the kernel. - kzg4844: GPU commitment used the backend's OWN embedded trusted setup (empty SRS) instead of the canonical EIP-4844 SRS -> breaks KZG binding. - ai_mining (BatchVerifyMLDSA/BatchAccumulator): accel batch kernel truncates messages to 32 bytes and is mode-fixed -> diverges from VerifyMLDSA. - fhe: GPU computed BFV arithmetic while CPU computes TFHE (different scheme, incompatible ciphertext, nil relin key) -> corrupt + divergent ciphertext. Cleanups (no split, vestigial): removed dead mlkem encapsulateGPU; removed the hpke GPU pre-warm that discarded its result and returned the CPU output anyway. slhdsa was already CPU-only (prior identical fix). Tests (differential / forgery proofs, all green CGO_ENABLED=0 and CGO -race): - sr25519: a valid Ed25519 sig is now REJECTED; real sr25519 still verifies. - blake3: OpHash256/MerkleRoot output == BLAKE3 and != BLAKE2b. - cggmp21: flipped recovery byte is rejected (recover-compare authoritative). - frost: a valid ECDSA sig is rejected as FROST-Schnorr. - ed25519: verdict matches the canonical ed25519.Verify oracle over 64x4 vectors. - mldsa: empty-ctx sig (the accel framing) rejected; precompileCtx sig accepted. Net -593 LOC. No consensus code touched.
1090 lines
28 KiB
Go
1090 lines
28 KiB
Go
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||
// See the file LICENSE for licensing terms.
|
||
|
||
package zk
|
||
|
||
import (
|
||
"crypto/sha256"
|
||
"errors"
|
||
"math/big"
|
||
"sync"
|
||
"time"
|
||
|
||
bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381"
|
||
"github.com/luxfi/crypto/bn256"
|
||
"github.com/luxfi/crypto/kzg4844"
|
||
"github.com/luxfi/geth/common"
|
||
)
|
||
|
||
// ZKVerifier provides zero-knowledge proof verification
|
||
// This is the main precompile at 0x0900 for ZK operations
|
||
type ZKVerifier struct {
|
||
// Verification keys
|
||
VerifyingKeys map[[32]byte]*VerifyingKey
|
||
|
||
// Nullifier tracking (for privacy)
|
||
Nullifiers map[[32]byte]*Nullifier
|
||
|
||
// Commitment tracking
|
||
Commitments map[[32]byte]*Commitment
|
||
|
||
// Rollup configurations
|
||
Rollups map[[32]byte]*RollupConfig
|
||
|
||
// Rollup state
|
||
RollupStates map[[32]byte]*RollupState
|
||
|
||
// Confidential pools
|
||
Pools map[[32]byte]*ConfidentialPool
|
||
|
||
// KZG trusted setup
|
||
KZGSetup *KZGSetup
|
||
|
||
// Statistics
|
||
TotalVerifications uint64
|
||
TotalProofsValid uint64
|
||
TotalProofsFailed uint64
|
||
|
||
mu sync.RWMutex
|
||
}
|
||
|
||
// RollupState tracks the state of a rollup
|
||
type RollupState struct {
|
||
LastBatchID [32]byte
|
||
LastStateRoot [32]byte
|
||
LastL1Block uint64
|
||
TotalBatches uint64
|
||
TotalTxs uint64
|
||
}
|
||
|
||
// NewZKVerifier creates a new ZK verifier
|
||
func NewZKVerifier() *ZKVerifier {
|
||
return &ZKVerifier{
|
||
VerifyingKeys: make(map[[32]byte]*VerifyingKey),
|
||
Nullifiers: make(map[[32]byte]*Nullifier),
|
||
Commitments: make(map[[32]byte]*Commitment),
|
||
Rollups: make(map[[32]byte]*RollupConfig),
|
||
RollupStates: make(map[[32]byte]*RollupState),
|
||
Pools: make(map[[32]byte]*ConfidentialPool),
|
||
}
|
||
}
|
||
|
||
// RegisterVerifyingKey registers a new verification key
|
||
func (zv *ZKVerifier) RegisterVerifyingKey(
|
||
owner common.Address,
|
||
proofSystem ProofSystem,
|
||
circuitType CircuitType,
|
||
alpha, beta, gamma, delta []byte,
|
||
ic [][]byte,
|
||
) ([32]byte, error) {
|
||
zv.mu.Lock()
|
||
defer zv.mu.Unlock()
|
||
|
||
// Generate key ID
|
||
keyData := append(alpha, beta...)
|
||
keyData = append(keyData, gamma...)
|
||
keyData = append(keyData, delta...)
|
||
keyID := sha256.Sum256(keyData)
|
||
|
||
vk := &VerifyingKey{
|
||
KeyID: keyID,
|
||
ProofSystem: proofSystem,
|
||
CircuitType: circuitType,
|
||
Alpha: alpha,
|
||
Beta: beta,
|
||
Gamma: gamma,
|
||
Delta: delta,
|
||
IC: ic,
|
||
Hash: sha256.Sum256(keyData),
|
||
Owner: owner,
|
||
CreatedAt: uint64(time.Now().Unix()),
|
||
}
|
||
|
||
zv.VerifyingKeys[keyID] = vk
|
||
return keyID, nil
|
||
}
|
||
|
||
// VerifyGroth16 verifies a Groth16 proof
|
||
func (zv *ZKVerifier) VerifyGroth16(
|
||
vkID [32]byte,
|
||
proofA, proofB, proofC []byte,
|
||
publicInputs []*big.Int,
|
||
) (*VerificationResult, error) {
|
||
zv.mu.Lock()
|
||
defer zv.mu.Unlock()
|
||
|
||
vk := zv.VerifyingKeys[vkID]
|
||
if vk == nil {
|
||
return nil, ErrInvalidVerifyingKey
|
||
}
|
||
|
||
if vk.ProofSystem != ProofSystemGroth16 {
|
||
return nil, ErrProofSystemMismatch
|
||
}
|
||
|
||
// Validate public inputs count
|
||
if len(publicInputs) != len(vk.IC)-1 {
|
||
return nil, ErrInvalidPublicInputs
|
||
}
|
||
|
||
// Verify the proof using pairing check
|
||
// In production, this would use BN254 pairing operations
|
||
valid := zv.groth16PairingCheck(vk, proofA, proofB, proofC, publicInputs)
|
||
|
||
zv.TotalVerifications++
|
||
if valid {
|
||
zv.TotalProofsValid++
|
||
} else {
|
||
zv.TotalProofsFailed++
|
||
}
|
||
|
||
return &VerificationResult{
|
||
Valid: valid,
|
||
ProofSystem: ProofSystemGroth16,
|
||
CircuitType: vk.CircuitType,
|
||
PublicInputs: publicInputs,
|
||
GasUsed: GasGroth16Verify,
|
||
}, nil
|
||
}
|
||
|
||
// VerifyPlonk verifies a PLONK proof
|
||
func (zv *ZKVerifier) VerifyPlonk(
|
||
vkID [32]byte,
|
||
proof []byte,
|
||
publicInputs []*big.Int,
|
||
) (*VerificationResult, error) {
|
||
zv.mu.Lock()
|
||
defer zv.mu.Unlock()
|
||
|
||
vk := zv.VerifyingKeys[vkID]
|
||
if vk == nil {
|
||
return nil, ErrInvalidVerifyingKey
|
||
}
|
||
|
||
if vk.ProofSystem != ProofSystemPlonk {
|
||
return nil, ErrProofSystemMismatch
|
||
}
|
||
|
||
// PLONK verification
|
||
valid := zv.plonkVerify(vk, proof, publicInputs)
|
||
|
||
zv.TotalVerifications++
|
||
if valid {
|
||
zv.TotalProofsValid++
|
||
} else {
|
||
zv.TotalProofsFailed++
|
||
}
|
||
|
||
return &VerificationResult{
|
||
Valid: valid,
|
||
ProofSystem: ProofSystemPlonk,
|
||
CircuitType: vk.CircuitType,
|
||
PublicInputs: publicInputs,
|
||
GasUsed: GasPlonkVerify,
|
||
}, nil
|
||
}
|
||
|
||
// VerifyFflonk verifies an fflonk proof with a registered verification key.
|
||
//
|
||
// fflonk is an optimized PLONK variant that uses batched KZG openings
|
||
// to reduce the number of pairings required for verification.
|
||
func (zv *ZKVerifier) VerifyFflonk(
|
||
vkID [32]byte,
|
||
proof []byte,
|
||
publicInputs []*big.Int,
|
||
) (*VerificationResult, error) {
|
||
zv.mu.Lock()
|
||
defer zv.mu.Unlock()
|
||
|
||
vk := zv.VerifyingKeys[vkID]
|
||
if vk == nil {
|
||
return nil, ErrInvalidVerifyingKey
|
||
}
|
||
|
||
if vk.ProofSystem != ProofSystemFflonk {
|
||
return nil, ErrProofSystemMismatch
|
||
}
|
||
|
||
// fflonk verification using the registered verification key
|
||
valid := zv.fflonkVerify(vk, proof, publicInputs)
|
||
|
||
zv.TotalVerifications++
|
||
if valid {
|
||
zv.TotalProofsValid++
|
||
} else {
|
||
zv.TotalProofsFailed++
|
||
}
|
||
|
||
// Use a dedicated gas constant for fflonk (between Groth16 and PLONK)
|
||
const GasFflonkVerify = uint64(180000)
|
||
|
||
return &VerificationResult{
|
||
Valid: valid,
|
||
ProofSystem: ProofSystemFflonk,
|
||
CircuitType: vk.CircuitType,
|
||
PublicInputs: publicInputs,
|
||
GasUsed: GasFflonkVerify,
|
||
}, nil
|
||
}
|
||
|
||
// fflonkVerify performs fflonk proof verification with a verification key.
|
||
//
|
||
// The fflonk proof format (448+ bytes):
|
||
// - C1: batched commitment (48 bytes, BLS12-381 G1 compressed)
|
||
// - C2: batched commitment (48 bytes, BLS12-381 G1 compressed)
|
||
// - W1: opening proof (48 bytes, BLS12-381 G1 compressed)
|
||
// - W2: opening proof (48 bytes, BLS12-381 G1 compressed)
|
||
// - Evaluations: field elements (8+ x 32 bytes)
|
||
//
|
||
// Verification equation:
|
||
// e(F - [r]G1, G2) = e(W, [τ - ξ]G2)
|
||
// fflonkVerify verifies an fflonk proof's KZG opening layer with real
|
||
// BLS12-381 pairing checks against the KZG trusted setup.
|
||
//
|
||
// Layout: C1‖C2‖W1‖W2 (four compressed G1 points, 48 bytes each) followed by
|
||
// >= 8 scalar-field evaluations (32 bytes each, big-endian). Evaluations[0]
|
||
// and [1] are the values C1 and C2 open to at the Fiat-Shamir point z;
|
||
// evaluations[2:] are auxiliary openings bound into z.
|
||
//
|
||
// Soundness: every commitment and opening proof must be an on-curve,
|
||
// in-subgroup G1 point and every evaluation a canonical scalar (else reject).
|
||
// z is derived by Fiat-Shamir from the verifying key, both commitments, the
|
||
// public inputs and every non-claim evaluation — so tampering any of those
|
||
// changes z and breaks both openings. The two openings are then checked with
|
||
// kzg4844.VerifyProof, a real pairing equation e(C-[y]G1,G2)=e(W,[τ-z]G2) over
|
||
// the trusted setup, so tampering a commitment, an opening proof, or a claim
|
||
// value fails a pairing. No proof element is unbound; there is no return-true.
|
||
//
|
||
// Boundary: this verifies the KZG opening layer that all fflonk proofs share.
|
||
// Binding the openings to a specific PLONK constraint system additionally
|
||
// requires a fflonk-native verifying key carrying the circuit's selector and
|
||
// permutation commitments; the VerifyingKey type here (Groth16-shaped: Alpha,
|
||
// Beta, Gamma, Delta, IC) does not carry them, so circuit binding is out of
|
||
// scope for this representation and intentionally not faked.
|
||
func (zv *ZKVerifier) fflonkVerify(
|
||
vk *VerifyingKey,
|
||
proof []byte,
|
||
publicInputs []*big.Int,
|
||
) bool {
|
||
// Minimum proof size: 4 G1 points (48 bytes each) + 8 evaluations (32 bytes each)
|
||
const minProofSize = 4*48 + 8*32
|
||
if len(proof) < minProofSize {
|
||
return false
|
||
}
|
||
|
||
c1 := proof[0:48]
|
||
c2 := proof[48:96]
|
||
w1 := proof[96:144]
|
||
w2 := proof[144:192]
|
||
|
||
// Reject any off-curve or wrong-subgroup commitment / opening proof.
|
||
for _, pt := range [][]byte{c1, c2, w1, w2} {
|
||
if !validG1Point(pt) {
|
||
return false
|
||
}
|
||
}
|
||
|
||
evalOffset := 192
|
||
numEvals := (len(proof) - evalOffset) / 32
|
||
if numEvals < 8 {
|
||
return false
|
||
}
|
||
r := blsScalarField()
|
||
evaluations := make([]*big.Int, numEvals)
|
||
for i := range numEvals {
|
||
e := new(big.Int).SetBytes(proof[evalOffset+i*32 : evalOffset+(i+1)*32])
|
||
if e.Cmp(r) >= 0 { // non-canonical scalar
|
||
return false
|
||
}
|
||
evaluations[i] = e
|
||
}
|
||
|
||
// Fiat-Shamir opening point, bound to everything except the two claim
|
||
// values (which are determined BY z, so they must not feed it).
|
||
z := computeFflonkOpeningPoint(vk, c1, c2, publicInputs, evaluations[2:])
|
||
|
||
// Real BLS12-381 pairing checks via the KZG trusted setup.
|
||
if !verifyKZGOpening(c1, z, evaluations[0], w1) {
|
||
return false
|
||
}
|
||
if !verifyKZGOpening(c2, z, evaluations[1], w2) {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
// blsScalarField returns the BLS12-381 scalar field order r.
|
||
func blsScalarField() *big.Int {
|
||
r := new(big.Int)
|
||
r.SetString("73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001", 16)
|
||
return r
|
||
}
|
||
|
||
// validG1Point reports whether b is a valid, non-infinity, in-subgroup
|
||
// compressed BLS12-381 G1 point. SetBytes performs the on-curve and prime
|
||
// subgroup checks; off-curve, wrong-subgroup, or malformed encodings fail.
|
||
func validG1Point(b []byte) bool {
|
||
if len(b) != 48 {
|
||
return false
|
||
}
|
||
var g bls12381.G1Affine
|
||
if _, err := g.SetBytes(b); err != nil {
|
||
return false
|
||
}
|
||
return !g.IsInfinity()
|
||
}
|
||
|
||
// padScalar32 left-pads x to a 32-byte big-endian buffer.
|
||
func padScalar32(x *big.Int) []byte {
|
||
out := make([]byte, 32)
|
||
if x == nil {
|
||
return out
|
||
}
|
||
b := x.Bytes()
|
||
if len(b) >= 32 {
|
||
copy(out, b[len(b)-32:])
|
||
} else {
|
||
copy(out[32-len(b):], b)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// computeFflonkOpeningPoint derives the Fiat-Shamir opening point z, bound to
|
||
// the verifying key, both commitments, the public inputs, and every non-claim
|
||
// evaluation.
|
||
func computeFflonkOpeningPoint(vk *VerifyingKey, c1, c2 []byte, publicInputs, boundEvals []*big.Int) *big.Int {
|
||
h := sha256.New()
|
||
h.Write([]byte("lux/zk/fflonk/opening-point/v1"))
|
||
if vk != nil {
|
||
h.Write(vk.Hash[:])
|
||
}
|
||
h.Write(c1)
|
||
h.Write(c2)
|
||
for _, pi := range publicInputs {
|
||
h.Write(padScalar32(pi))
|
||
}
|
||
for _, e := range boundEvals {
|
||
h.Write(padScalar32(e))
|
||
}
|
||
z := new(big.Int).SetBytes(h.Sum(nil))
|
||
return z.Mod(z, blsScalarField())
|
||
}
|
||
|
||
// verifyKZGOpening checks the pairing equation e(C-[claim]G1,G2)=e(proof,[τ-z]G2)
|
||
// via kzg4844.VerifyProof over the trusted setup. commitment and proof must be
|
||
// exactly 48 bytes (validated by the caller).
|
||
func verifyKZGOpening(commitment []byte, z, claim *big.Int, proof []byte) bool {
|
||
r := blsScalarField()
|
||
|
||
var c kzg4844.Commitment
|
||
copy(c[:], commitment)
|
||
var pf kzg4844.Proof
|
||
copy(pf[:], proof)
|
||
|
||
var pt kzg4844.Point
|
||
copy(pt[:], padScalar32(new(big.Int).Mod(z, r)))
|
||
var cl kzg4844.Claim
|
||
copy(cl[:], padScalar32(new(big.Int).Mod(claim, r)))
|
||
|
||
return kzg4844.VerifyProof(c, pt, cl, pf) == nil
|
||
}
|
||
|
||
// VerifyKZG verifies a KZG point evaluation
|
||
func (zv *ZKVerifier) VerifyKZG(
|
||
commitment []byte,
|
||
point *big.Int,
|
||
value *big.Int,
|
||
proof []byte,
|
||
) (bool, error) {
|
||
zv.mu.RLock()
|
||
defer zv.mu.RUnlock()
|
||
|
||
if zv.KZGSetup == nil {
|
||
return false, errors.New("KZG setup not initialized")
|
||
}
|
||
|
||
// KZG point evaluation verification
|
||
// Uses pairing: e(C - [y]G1, G2) = e(proof, [τ - z]G2)
|
||
valid := zv.kzgPointEvaluation(commitment, point, value, proof)
|
||
|
||
return valid, nil
|
||
}
|
||
|
||
// VerifyRangeProof verifies that a committed value is within a range
|
||
func (zv *ZKVerifier) VerifyRangeProof(
|
||
commitment []byte,
|
||
rangeProof []byte,
|
||
bitLength uint32,
|
||
) (bool, error) {
|
||
zv.mu.RLock()
|
||
defer zv.mu.RUnlock()
|
||
|
||
// Bulletproofs-style range proof verification
|
||
valid := zv.bulletproofRangeVerify(commitment, rangeProof, bitLength)
|
||
|
||
return valid, nil
|
||
}
|
||
|
||
// CheckNullifier checks if a nullifier has been spent
|
||
func (zv *ZKVerifier) CheckNullifier(nullifierHash [32]byte) (bool, error) {
|
||
zv.mu.RLock()
|
||
defer zv.mu.RUnlock()
|
||
|
||
_, spent := zv.Nullifiers[nullifierHash]
|
||
return spent, nil
|
||
}
|
||
|
||
// SpendNullifier marks a nullifier as spent
|
||
func (zv *ZKVerifier) SpendNullifier(
|
||
nullifierHash [32]byte,
|
||
txHash common.Hash,
|
||
blockHeight uint64,
|
||
) error {
|
||
zv.mu.Lock()
|
||
defer zv.mu.Unlock()
|
||
|
||
if _, exists := zv.Nullifiers[nullifierHash]; exists {
|
||
return ErrNullifierSpent
|
||
}
|
||
|
||
zv.Nullifiers[nullifierHash] = &Nullifier{
|
||
Hash: nullifierHash,
|
||
SpentAt: blockHeight,
|
||
SpentTx: txHash,
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// AddCommitment adds a new commitment to the pool
|
||
func (zv *ZKVerifier) AddCommitment(
|
||
poolID [32]byte,
|
||
commitment *Commitment,
|
||
) ([32]byte, error) {
|
||
zv.mu.Lock()
|
||
defer zv.mu.Unlock()
|
||
|
||
pool := zv.Pools[poolID]
|
||
if pool == nil {
|
||
return [32]byte{}, ErrPoolNotFound
|
||
}
|
||
|
||
if !pool.Enabled {
|
||
return [32]byte{}, ErrPoolDisabled
|
||
}
|
||
|
||
// Generate commitment ID
|
||
commitID := sha256.Sum256(commitment.Value)
|
||
|
||
pool.Commitments[commitID] = commitment
|
||
pool.TotalDeposits.Add(pool.TotalDeposits, commitment.Amount)
|
||
|
||
// Update merkle root
|
||
zv.updatePoolMerkleRoot(pool)
|
||
|
||
return commitID, nil
|
||
}
|
||
|
||
// VerifyCommitmentInclusion verifies a commitment is in the pool
|
||
func (zv *ZKVerifier) VerifyCommitmentInclusion(
|
||
poolID [32]byte,
|
||
commitmentID [32]byte,
|
||
merkleProof [][]byte,
|
||
leafIndex uint64,
|
||
) (bool, error) {
|
||
zv.mu.RLock()
|
||
defer zv.mu.RUnlock()
|
||
|
||
pool := zv.Pools[poolID]
|
||
if pool == nil {
|
||
return false, ErrPoolNotFound
|
||
}
|
||
|
||
// Verify merkle proof
|
||
valid := zv.verifyMerkleProof(pool.MerkleRoot, commitmentID[:], merkleProof, leafIndex)
|
||
|
||
return valid, nil
|
||
}
|
||
|
||
// RegisterRollup registers a new ZK rollup
|
||
func (zv *ZKVerifier) RegisterRollup(
|
||
owner common.Address,
|
||
verifyingKeyID [32]byte,
|
||
proofSystem ProofSystem,
|
||
maxTxPerBatch uint64,
|
||
batchInterval uint64,
|
||
sequencer common.Address,
|
||
) ([32]byte, error) {
|
||
zv.mu.Lock()
|
||
defer zv.mu.Unlock()
|
||
|
||
vk := zv.VerifyingKeys[verifyingKeyID]
|
||
if vk == nil {
|
||
return [32]byte{}, ErrInvalidVerifyingKey
|
||
}
|
||
|
||
// Generate rollup ID
|
||
rollupData := append(owner.Bytes(), verifyingKeyID[:]...)
|
||
rollupID := sha256.Sum256(rollupData)
|
||
|
||
config := &RollupConfig{
|
||
RollupID: rollupID,
|
||
Owner: owner,
|
||
VerifyingKey: vk,
|
||
ProofSystem: proofSystem,
|
||
MaxTxPerBatch: maxTxPerBatch,
|
||
BatchInterval: batchInterval,
|
||
ChallengeWindow: 7 * 24 * 60 * 60, // 7 days default
|
||
Sequencer: sequencer,
|
||
Enabled: true,
|
||
}
|
||
|
||
state := &RollupState{
|
||
LastStateRoot: [32]byte{}, // Genesis state
|
||
}
|
||
|
||
zv.Rollups[rollupID] = config
|
||
zv.RollupStates[rollupID] = state
|
||
|
||
return rollupID, nil
|
||
}
|
||
|
||
// VerifyRollupBatch verifies a ZK rollup batch
|
||
func (zv *ZKVerifier) VerifyRollupBatch(
|
||
rollupID [32]byte,
|
||
batch *RollupBatch,
|
||
) error {
|
||
zv.mu.Lock()
|
||
defer zv.mu.Unlock()
|
||
|
||
config := zv.Rollups[rollupID]
|
||
if config == nil {
|
||
return ErrRollupNotFound
|
||
}
|
||
|
||
if !config.Enabled {
|
||
return errors.New("rollup disabled")
|
||
}
|
||
|
||
state := zv.RollupStates[rollupID]
|
||
|
||
// Verify proposer authorization
|
||
if batch.Proposer != config.Sequencer && batch.Proposer != config.Owner {
|
||
return ErrUnauthorizedProposer
|
||
}
|
||
|
||
// Verify batch size
|
||
if batch.Transactions > config.MaxTxPerBatch {
|
||
return ErrBatchTooLarge
|
||
}
|
||
|
||
// Verify state transition
|
||
if batch.PrevStateRoot != state.LastStateRoot {
|
||
return ErrInvalidStateRoot
|
||
}
|
||
|
||
// Verify the validity proof
|
||
var valid bool
|
||
switch config.ProofSystem {
|
||
case ProofSystemGroth16:
|
||
result, err := zv.verifyGroth16Batch(config.VerifyingKey, batch)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
valid = result
|
||
case ProofSystemPlonk:
|
||
result, err := zv.verifyPlonkBatch(config.VerifyingKey, batch)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
valid = result
|
||
default:
|
||
return ErrProofSystemMismatch
|
||
}
|
||
|
||
if !valid {
|
||
return ErrInvalidProof
|
||
}
|
||
|
||
// Update state
|
||
state.LastBatchID = batch.BatchID
|
||
state.LastStateRoot = batch.NewStateRoot
|
||
state.LastL1Block = batch.L1BatchNum
|
||
state.TotalBatches++
|
||
state.TotalTxs += batch.Transactions
|
||
|
||
return nil
|
||
}
|
||
|
||
// GetRollupState returns the current state of a rollup
|
||
func (zv *ZKVerifier) GetRollupState(rollupID [32]byte) (*RollupState, error) {
|
||
zv.mu.RLock()
|
||
defer zv.mu.RUnlock()
|
||
|
||
state := zv.RollupStates[rollupID]
|
||
if state == nil {
|
||
return nil, ErrRollupNotFound
|
||
}
|
||
|
||
return state, nil
|
||
}
|
||
|
||
// CreateConfidentialPool creates a new confidential transaction pool
|
||
func (zv *ZKVerifier) CreateConfidentialPool(
|
||
owner common.Address,
|
||
token common.Address,
|
||
merkleDepth uint32,
|
||
) ([32]byte, error) {
|
||
zv.mu.Lock()
|
||
defer zv.mu.Unlock()
|
||
|
||
poolData := append(owner.Bytes(), token.Bytes()...)
|
||
poolID := sha256.Sum256(poolData)
|
||
|
||
pool := &ConfidentialPool{
|
||
PoolID: poolID,
|
||
Token: token,
|
||
Commitments: make(map[[32]byte]*Commitment),
|
||
Nullifiers: make(map[[32]byte]*Nullifier),
|
||
MerkleRoot: [32]byte{},
|
||
MerkleDepth: merkleDepth,
|
||
TotalDeposits: big.NewInt(0),
|
||
TotalWithdraws: big.NewInt(0),
|
||
Enabled: true,
|
||
}
|
||
|
||
zv.Pools[poolID] = pool
|
||
return poolID, nil
|
||
}
|
||
|
||
// Helper functions
|
||
|
||
// groth16PairingCheck implements the Groth16 pairing verification equation:
|
||
// e(A, B) = e(α, β) · e(∑ᵢ wᵢ · ICᵢ, γ) · e(C, δ)
|
||
//
|
||
// Equivalently, we verify:
|
||
// e(A, B) · e(-α, β) · e(-vk_x, γ) · e(-C, δ) = 1
|
||
//
|
||
// Where vk_x = ∑ᵢ wᵢ · ICᵢ (linear combination of public inputs with IC points)
|
||
func (zv *ZKVerifier) groth16PairingCheck(
|
||
vk *VerifyingKey,
|
||
proofA, proofB, proofC []byte,
|
||
publicInputs []*big.Int,
|
||
) bool {
|
||
// Parse proof elements
|
||
var a bn256.G1
|
||
if _, err := a.Unmarshal(proofA); err != nil {
|
||
return false
|
||
}
|
||
|
||
var b bn256.G2
|
||
if _, err := b.Unmarshal(proofB); err != nil {
|
||
return false
|
||
}
|
||
|
||
var c bn256.G1
|
||
if _, err := c.Unmarshal(proofC); err != nil {
|
||
return false
|
||
}
|
||
|
||
// Parse verification key elements
|
||
var alpha bn256.G1
|
||
if _, err := alpha.Unmarshal(vk.Alpha); err != nil {
|
||
return false
|
||
}
|
||
|
||
var beta bn256.G2
|
||
if _, err := beta.Unmarshal(vk.Beta); err != nil {
|
||
return false
|
||
}
|
||
|
||
var gamma bn256.G2
|
||
if _, err := gamma.Unmarshal(vk.Gamma); err != nil {
|
||
return false
|
||
}
|
||
|
||
var delta bn256.G2
|
||
if _, err := delta.Unmarshal(vk.Delta); err != nil {
|
||
return false
|
||
}
|
||
|
||
// Parse IC points (input constraints)
|
||
if len(vk.IC) < 1 {
|
||
return false
|
||
}
|
||
|
||
ic := make([]*bn256.G1, len(vk.IC))
|
||
for i, icBytes := range vk.IC {
|
||
ic[i] = new(bn256.G1)
|
||
if _, err := ic[i].Unmarshal(icBytes); err != nil {
|
||
return false
|
||
}
|
||
}
|
||
|
||
// Compute vk_x = IC[0] + ∑ᵢ (publicInputs[i] * IC[i+1])
|
||
// This is the linear combination of public inputs with IC points (MSM)
|
||
vkX := new(bn256.G1)
|
||
|
||
// CPU is the single source of truth: sequential bn256 MSM. The removed
|
||
// accel path called sess.ZK().MSM with NO curve binding (the kernel cannot
|
||
// know it must operate over BN254), and its result was folded into vk_x
|
||
// without independent verification -- a wrong or wrong-curve MSM silently
|
||
// flipped the Groth16 pairing verdict on accel-equipped validators, a
|
||
// consensus split on proof verification. Re-introducing accel requires a
|
||
// BN254 MSM kernel proven bit-identical to this loop.
|
||
vkX.ScalarMult(ic[0], big.NewInt(1)) // Start with IC[0]
|
||
|
||
for i, input := range publicInputs {
|
||
if i+1 >= len(ic) {
|
||
return false
|
||
}
|
||
tmp := new(bn256.G1)
|
||
tmp.ScalarMult(ic[i+1], input)
|
||
vkX.Add(vkX, tmp)
|
||
}
|
||
|
||
// Negate points for the pairing check
|
||
// We check: e(A, B) · e(-α, β) · e(-vk_x, γ) · e(-C, δ) = 1
|
||
negAlpha := new(bn256.G1)
|
||
negAlpha.ScalarMult(&alpha, big.NewInt(-1))
|
||
|
||
negVkX := new(bn256.G1)
|
||
negVkX.ScalarMult(vkX, big.NewInt(-1))
|
||
|
||
negC := new(bn256.G1)
|
||
negC.ScalarMult(&c, big.NewInt(-1))
|
||
|
||
// Perform pairing check
|
||
// PairingCheck returns true if ∏ᵢ e(Pᵢ, Qᵢ) = 1
|
||
g1Points := []*bn256.G1{&a, negAlpha, negVkX, negC}
|
||
g2Points := []*bn256.G2{&b, &beta, &gamma, &delta}
|
||
|
||
return bn256.PairingCheck(g1Points, g2Points)
|
||
}
|
||
|
||
// plonkVerify verifies a PLONK proof using KZG polynomial commitments.
|
||
//
|
||
// PLONK verification involves:
|
||
// 1. Parse proof elements (wire commitments, quotient commitments, opening proofs)
|
||
// 2. Compute public input polynomial evaluation at challenge point
|
||
// 3. Verify batched KZG opening proofs using pairing
|
||
//
|
||
// The proof format expected:
|
||
// - [0:64] - a commitment (G1)
|
||
// - [64:128] - b commitment (G1)
|
||
// - [128:192] - c commitment (G1)
|
||
// - [192:256] - z commitment (permutation polynomial, G1)
|
||
// - [256:320] - t_lo commitment (quotient polynomial low, G1)
|
||
// - [320:384] - t_mid commitment (quotient polynomial mid, G1)
|
||
// - [384:448] - t_hi commitment (quotient polynomial high, G1)
|
||
// - [448:512] - W_zeta proof (opening at zeta, G1)
|
||
// - [512:576] - W_zeta_omega proof (opening at zeta*omega, G1)
|
||
// - [576:608] - a_eval (scalar)
|
||
// - [608:640] - b_eval (scalar)
|
||
// - [640:672] - c_eval (scalar)
|
||
// - [672:704] - s1_eval (scalar)
|
||
// - [704:736] - s2_eval (scalar)
|
||
// - [736:768] - z_omega_eval (scalar)
|
||
func (zv *ZKVerifier) plonkVerify(
|
||
vk *VerifyingKey,
|
||
proof []byte,
|
||
publicInputs []*big.Int,
|
||
) bool {
|
||
// Minimum proof size: 9 G1 points (576 bytes) + 6 scalars (192 bytes) = 768 bytes
|
||
const minProofSize = 768
|
||
if len(proof) < minProofSize {
|
||
return false
|
||
}
|
||
|
||
// Parse G1 commitments from proof
|
||
commitments := make([]*bn256.G1, 9)
|
||
for i := range 9 {
|
||
commitments[i] = new(bn256.G1)
|
||
if _, err := commitments[i].Unmarshal(proof[i*64 : (i+1)*64]); err != nil {
|
||
return false
|
||
}
|
||
}
|
||
|
||
// Parse evaluations
|
||
evalOffset := 576
|
||
evaluations := make([]*big.Int, 6)
|
||
for i := range 6 {
|
||
evaluations[i] = new(big.Int).SetBytes(proof[evalOffset+i*32 : evalOffset+(i+1)*32])
|
||
}
|
||
|
||
// Parse verification key selectors and permutation commitments
|
||
// VK contains: Qm, Ql, Qr, Qo, Qc, S1, S2, S3 (8 G1 points)
|
||
// Plus: X2 (G2 generator scaled by tau)
|
||
if len(vk.IC) < 9 {
|
||
return false
|
||
}
|
||
|
||
vkPoints := make([]*bn256.G1, 8)
|
||
for i := range 8 {
|
||
vkPoints[i] = new(bn256.G1)
|
||
if _, err := vkPoints[i].Unmarshal(vk.IC[i]); err != nil {
|
||
return false
|
||
}
|
||
}
|
||
|
||
// Parse X2 from verification key (G2 element)
|
||
var x2 bn256.G2
|
||
if _, err := x2.Unmarshal(vk.IC[8]); err != nil {
|
||
return false
|
||
}
|
||
|
||
// Compute the public input polynomial evaluation
|
||
// PI(x) = -∑ᵢ wᵢ · Lᵢ(x) where Lᵢ are Lagrange basis polynomials
|
||
// For simplicity, we compute the hash-based challenge point
|
||
piEval := computePlonkPIEvaluation(publicInputs)
|
||
|
||
// Compute the linearization commitment
|
||
// F = Qm·a·b + Ql·a + Qr·b + Qo·c + Qc + PI +
|
||
// α·[(a+β·z+γ)(b+β·k1·z+γ)(c+β·k2·z+γ)·Z - (a+β·S1+γ)(b+β·S2+γ)(c+β·S3+γ)·Z_ω]
|
||
// + α²·L1(z)·(Z-1)
|
||
// Then verify the opening: e(F - r, G2) = e(W_zeta, X2)
|
||
|
||
// For the final pairing check, we verify the batched opening:
|
||
// e(W_zeta + u·W_zeta_omega, [x]₂) = e(zeta·W_zeta + u·zeta·ω·W_zeta_omega + F - r, G2)
|
||
//
|
||
// This is equivalent to checking:
|
||
// e(W_zeta + u·W_zeta_omega, [x]₂) · e(-(zeta·W_zeta + u·zeta·ω·W_zeta_omega + F - r), G2) = 1
|
||
|
||
// Compute challenge scalars using Fiat-Shamir
|
||
// In a real implementation, these would be derived from transcript
|
||
alpha := computeChallenge(proof, []byte("alpha"))
|
||
beta := computeChallenge(proof, []byte("beta"))
|
||
gamma := computeChallenge(proof, []byte("gamma"))
|
||
zeta := computeChallenge(proof, []byte("zeta"))
|
||
u := computeChallenge(proof, []byte("u"))
|
||
|
||
// Build the linearization polynomial commitment F
|
||
F := computeLinearizationCommitment(
|
||
commitments, vkPoints, evaluations,
|
||
alpha, beta, gamma, zeta, piEval,
|
||
)
|
||
|
||
// Compute the batched opening check
|
||
// Left side: W_zeta + u·W_zeta_omega
|
||
wZeta := commitments[7]
|
||
wZetaOmega := commitments[8]
|
||
|
||
leftG1 := new(bn256.G1)
|
||
uWZetaOmega := new(bn256.G1)
|
||
uWZetaOmega.ScalarMult(wZetaOmega, u)
|
||
leftG1.Add(wZeta, uWZetaOmega)
|
||
|
||
// Right side involves F and the evaluation point
|
||
// For simplicity, we verify: e(leftG1, X2) = e(F, G2)
|
||
// This is a simplified version; full PLONK has more terms
|
||
|
||
// Parse G2 generator from verification key
|
||
var g2Gen bn256.G2
|
||
if _, err := g2Gen.Unmarshal(vk.Beta); err != nil {
|
||
return false
|
||
}
|
||
|
||
// Negate F for the pairing check
|
||
negF := new(bn256.G1)
|
||
negF.ScalarMult(F, big.NewInt(-1))
|
||
|
||
// Pairing check: e(leftG1, X2) · e(-F, G2) = 1
|
||
g1Points := []*bn256.G1{leftG1, negF}
|
||
g2Points := []*bn256.G2{&x2, &g2Gen}
|
||
|
||
return bn256.PairingCheck(g1Points, g2Points)
|
||
}
|
||
|
||
// computePlonkPIEvaluation computes the public input contribution.
|
||
func computePlonkPIEvaluation(publicInputs []*big.Int) *big.Int {
|
||
if len(publicInputs) == 0 {
|
||
return big.NewInt(0)
|
||
}
|
||
|
||
// Simple aggregation for demonstration
|
||
// In full PLONK, this evaluates the PI polynomial at challenge point
|
||
result := new(big.Int)
|
||
for _, input := range publicInputs {
|
||
result.Add(result, input)
|
||
}
|
||
return result
|
||
}
|
||
|
||
// computeChallenge computes a Fiat-Shamir challenge from proof and domain separator.
|
||
func computeChallenge(proof, domain []byte) *big.Int {
|
||
h := sha256.New()
|
||
h.Write(domain)
|
||
h.Write(proof)
|
||
hash := h.Sum(nil)
|
||
return new(big.Int).SetBytes(hash)
|
||
}
|
||
|
||
// computeLinearizationCommitment computes the PLONK linearization polynomial commitment.
|
||
func computeLinearizationCommitment(
|
||
commitments []*bn256.G1,
|
||
vkPoints []*bn256.G1,
|
||
evaluations []*big.Int,
|
||
alpha, beta, gamma, zeta, piEval *big.Int,
|
||
) *bn256.G1 {
|
||
// Extract evaluations
|
||
aEval := evaluations[0]
|
||
bEval := evaluations[1]
|
||
cEval := evaluations[2]
|
||
|
||
// Compute gate constraint contribution
|
||
// gate = Qm·a·b + Ql·a + Qr·b + Qo·c + Qc + PI
|
||
F := new(bn256.G1)
|
||
|
||
// Qm contribution: a·b·Qm
|
||
ab := new(big.Int).Mul(aEval, bEval)
|
||
qmContrib := new(bn256.G1)
|
||
qmContrib.ScalarMult(vkPoints[0], ab)
|
||
F.Add(F, qmContrib)
|
||
|
||
// Ql contribution: a·Ql
|
||
qlContrib := new(bn256.G1)
|
||
qlContrib.ScalarMult(vkPoints[1], aEval)
|
||
F.Add(F, qlContrib)
|
||
|
||
// Qr contribution: b·Qr
|
||
qrContrib := new(bn256.G1)
|
||
qrContrib.ScalarMult(vkPoints[2], bEval)
|
||
F.Add(F, qrContrib)
|
||
|
||
// Qo contribution: c·Qo
|
||
qoContrib := new(bn256.G1)
|
||
qoContrib.ScalarMult(vkPoints[3], cEval)
|
||
F.Add(F, qoContrib)
|
||
|
||
// Qc contribution (constant)
|
||
F.Add(F, vkPoints[4])
|
||
|
||
// Permutation argument contribution (simplified)
|
||
// In full PLONK, this includes the Z polynomial contribution
|
||
zCommit := commitments[3]
|
||
alphaZ := new(bn256.G1)
|
||
alphaZ.ScalarMult(zCommit, alpha)
|
||
F.Add(F, alphaZ)
|
||
|
||
return F
|
||
}
|
||
|
||
// kzgPointEvaluation verifies a KZG polynomial commitment opening.
|
||
// Given commitment C = [P(τ)]₁, it verifies that P(z) = y.
|
||
//
|
||
// The verification uses the pairing equation:
|
||
// e(C - [y]G1, G2) = e(proof, [τ - z]G2)
|
||
//
|
||
// For EIP-4844 style verification, we use the kzg4844 package.
|
||
func (zv *ZKVerifier) kzgPointEvaluation(
|
||
commitment []byte,
|
||
point *big.Int,
|
||
value *big.Int,
|
||
proof []byte,
|
||
) bool {
|
||
// Validate input sizes
|
||
if len(commitment) != 48 || len(proof) != 48 {
|
||
return false
|
||
}
|
||
|
||
// Convert to kzg4844 types
|
||
var kzgCommitment kzg4844.Commitment
|
||
copy(kzgCommitment[:], commitment)
|
||
|
||
var kzgProof kzg4844.Proof
|
||
copy(kzgProof[:], proof)
|
||
|
||
// Convert point to 32-byte field element
|
||
var kzgPoint kzg4844.Point
|
||
pointBytes := point.Bytes()
|
||
if len(pointBytes) > 32 {
|
||
return false
|
||
}
|
||
// Right-align the bytes in the 32-byte array (big-endian)
|
||
copy(kzgPoint[32-len(pointBytes):], pointBytes)
|
||
|
||
// Convert value to 32-byte claim
|
||
var kzgClaim kzg4844.Claim
|
||
valueBytes := value.Bytes()
|
||
if len(valueBytes) > 32 {
|
||
return false
|
||
}
|
||
// Right-align the bytes in the 32-byte array (big-endian)
|
||
copy(kzgClaim[32-len(valueBytes):], valueBytes)
|
||
|
||
// Verify the proof using the kzg4844 package
|
||
err := kzg4844.VerifyProof(kzgCommitment, kzgPoint, kzgClaim, kzgProof)
|
||
return err == nil
|
||
}
|
||
|
||
func (zv *ZKVerifier) bulletproofRangeVerify(
|
||
commitment []byte,
|
||
rangeProof []byte,
|
||
bitLength uint32,
|
||
) bool {
|
||
// Bulletproofs range proof verification
|
||
// Proves that committed value v satisfies 0 ≤ v < 2^n
|
||
|
||
return len(commitment) > 0 && len(rangeProof) > 0 && bitLength > 0
|
||
}
|
||
|
||
func (zv *ZKVerifier) verifyMerkleProof(
|
||
root [32]byte,
|
||
leaf []byte,
|
||
proof [][]byte,
|
||
index uint64,
|
||
) bool {
|
||
// Standard merkle proof verification
|
||
current := sha256.Sum256(leaf)
|
||
|
||
for i, sibling := range proof {
|
||
var combined []byte
|
||
if (index>>uint(i))&1 == 0 {
|
||
combined = append(current[:], sibling...)
|
||
} else {
|
||
combined = append(sibling, current[:]...)
|
||
}
|
||
current = sha256.Sum256(combined)
|
||
}
|
||
|
||
return current == root
|
||
}
|
||
|
||
func (zv *ZKVerifier) updatePoolMerkleRoot(pool *ConfidentialPool) {
|
||
// Rebuild merkle tree from commitments
|
||
// In production, use incremental merkle tree for efficiency
|
||
}
|
||
|
||
func (zv *ZKVerifier) verifyGroth16Batch(vk *VerifyingKey, batch *RollupBatch) (bool, error) {
|
||
if batch.Proof == nil {
|
||
return false, ErrInvalidProof
|
||
}
|
||
|
||
// Public inputs for rollup: [prevRoot, newRoot, txRoot, batchNum]
|
||
publicInputs := []*big.Int{
|
||
new(big.Int).SetBytes(batch.PrevStateRoot[:]),
|
||
new(big.Int).SetBytes(batch.NewStateRoot[:]),
|
||
big.NewInt(int64(batch.Transactions)),
|
||
big.NewInt(int64(batch.L1BatchNum)),
|
||
}
|
||
|
||
return zv.groth16PairingCheck(vk, batch.Proof.A, batch.Proof.B, batch.Proof.C, publicInputs), nil
|
||
}
|
||
|
||
func (zv *ZKVerifier) verifyPlonkBatch(vk *VerifyingKey, batch *RollupBatch) (bool, error) {
|
||
if batch.Proof == nil {
|
||
return false, ErrInvalidProof
|
||
}
|
||
|
||
publicInputs := []*big.Int{
|
||
new(big.Int).SetBytes(batch.PrevStateRoot[:]),
|
||
new(big.Int).SetBytes(batch.NewStateRoot[:]),
|
||
}
|
||
|
||
// Encode proof for PLONK
|
||
proofData := append(batch.Proof.A, batch.Proof.B...)
|
||
proofData = append(proofData, batch.Proof.C...)
|
||
|
||
return zv.plonkVerify(vk, proofData, publicInputs), nil
|
||
}
|