Files
zeekayandHanzo Dev 34226894ca zkvm: say why the PLONK precompile accepts nothing
The reason is the property, not a version that had it wrong: a verifier that
does not evaluate the statement is not a verifier, since it would accept any
well-formed blob. The equation is unimplemented here, so this accepts nothing.

Comment only.

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

540 lines
16 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) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package precompiles
import (
"encoding/binary"
"errors"
"fmt"
"math/big"
"github.com/consensys/gnark-crypto/ecc/bn254"
"github.com/consensys/gnark-crypto/ecc/bn254/fr"
"github.com/luxfi/precompile/starkfri"
)
// Precompile addresses for Z-Chain ZK verifiers.
// These live in the Z-Chain EVM precompile space.
const (
Groth16VerifierAddr = 0x80
PLONKVerifierAddr = 0x81
STARKVerifierAddr = 0x82
Halo2VerifierAddr = 0x83
NovaVerifierAddr = 0x84
)
// Gas costs calibrated to verification complexity.
// Groth16: 3 pairings + 1 MSM. PLONK: 2 pairings + polynomial evaluation.
// STARK: hash-chain verification. Halo2: IPA + polynomial commitment.
const (
groth16Gas = 50_000
plonkGas = 80_000
starkGas = 200_000
halo2Gas = 100_000
novaGas = 100_000
)
var (
errInputTooShort = errors.New("input too short")
errInvalidPoint = errors.New("invalid elliptic curve point")
errSubgroupCheck = errors.New("point not in correct subgroup")
errPointAtInfinity = errors.New("point at infinity")
errPairingFailed = errors.New("pairing computation failed")
errProofInvalid = errors.New("proof verification failed")
errNotImplemented = errors.New("verifier not yet available")
errVerifierUnavailable = errors.New("strict-PQ STARK verifier binding not registered (build with -tags starkfri_p3q); failing closed")
// errClassicalForbiddenStrictPQ is returned by the cross-chain router
// when a strict-PQ chain attempts to route a classical (Groth16/PLONK)
// verification. On a strict-PQ chain only the STARK/FRI path is allowed.
errClassicalForbiddenStrictPQ = errors.New("strict-PQ chain: classical Groth16/PLONK verification forbidden, use STARK/FRI (0x82)")
)
// result bytes returned by precompiles
var (
resultValid = []byte{0x01}
resultInvalid = []byte{0x00}
)
// PrecompiledContract is the interface that EVM precompiles must satisfy.
type PrecompiledContract interface {
RequiredGas(input []byte) uint64
Run(input []byte) ([]byte, error)
}
// --- Groth16 Verifier ---
// Groth16Verifier verifies Groth16 proofs on Z-Chain using bn254 pairings.
//
// Input format (all points uncompressed):
//
// vk_len (4 bytes, big-endian)
// vk (vk_len bytes): Alpha(64) | Beta(128) | Gamma(128) | Delta(128) | numK(4) | K[](64*numK)
// proof (256 bytes): Ar(64) | Bs(128) | Krs(64)
// num_inputs (4 bytes, big-endian)
// inputs (32 * num_inputs bytes): field elements
//
// Output: 0x01 if valid, 0x00 if invalid.
type Groth16Verifier struct{}
func (v *Groth16Verifier) RequiredGas(input []byte) uint64 {
return groth16Gas
}
func (v *Groth16Verifier) Run(input []byte) ([]byte, error) {
// Parse verifying key length
if len(input) < 4 {
return resultInvalid, errInputTooShort
}
vkLen := binary.BigEndian.Uint32(input[:4])
off := uint32(4)
if uint32(len(input)) < off+vkLen {
return resultInvalid, errInputTooShort
}
// Parse verifying key
vk, err := parseVerifyingKey(input[off : off+vkLen])
if err != nil {
return resultInvalid, fmt.Errorf("parse vk: %w", err)
}
off += vkLen
// Parse proof (256 bytes)
if uint32(len(input)) < off+256 {
return resultInvalid, errInputTooShort
}
proof, err := parseGroth16Proof(input[off : off+256])
if err != nil {
return resultInvalid, fmt.Errorf("parse proof: %w", err)
}
off += 256
// Parse public inputs
if uint32(len(input)) < off+4 {
return resultInvalid, errInputTooShort
}
numInputs := binary.BigEndian.Uint32(input[off : off+4])
off += 4
if uint32(len(input)) < off+numInputs*32 {
return resultInvalid, errInputTooShort
}
// K holds one point per public input plus the constant term K[0], so the key
// states how many inputs its circuit takes. The caller supplies both the key
// and the count, and they have to agree: too many reads past the end of K,
// and too few leaves the trailing K points out of the linear combination
// below, judging the proof against a smaller statement than the key
// describes.
if want := len(vk.K) - 1; int(numInputs) != want {
return resultInvalid, fmt.Errorf("public inputs: %d supplied, the verifying key's circuit takes %d", numInputs, want)
}
witness := make([]fr.Element, numInputs)
for i := uint32(0); i < numInputs; i++ {
witness[i].SetBytes(input[off : off+32])
off += 32
}
// Verify
if err := verifyGroth16(proof, vk, witness); err != nil {
return resultInvalid, nil // invalid proof is not an execution error
}
return resultValid, nil
}
// --- PLONK Verifier ---
// PLONKVerifier verifies PLONK proofs using KZG polynomial commitments on bn254.
//
// Input format:
//
// vk_len (4 bytes)
// vk (vk_len bytes)
// proof_len (4 bytes)
// proof (proof_len bytes)
// num_inputs (4 bytes)
// inputs (32 * num_inputs bytes)
//
// Output: 0x01 if valid, 0x00 if invalid.
type PLONKVerifier struct{}
func (v *PLONKVerifier) RequiredGas(input []byte) uint64 {
return plonkGas
}
func (v *PLONKVerifier) Run(input []byte) ([]byte, error) {
if len(input) < 4 {
return resultInvalid, errInputTooShort
}
vkLen := binary.BigEndian.Uint32(input[:4])
off := uint32(4)
if uint32(len(input)) < off+vkLen {
return resultInvalid, errInputTooShort
}
vkBytes := input[off : off+vkLen]
off += vkLen
if uint32(len(input)) < off+4 {
return resultInvalid, errInputTooShort
}
proofLen := binary.BigEndian.Uint32(input[off : off+4])
off += 4
if uint32(len(input)) < off+proofLen {
return resultInvalid, errInputTooShort
}
proofBytes := input[off : off+proofLen]
off += proofLen
if uint32(len(input)) < off+4 {
return resultInvalid, errInputTooShort
}
numInputs := binary.BigEndian.Uint32(input[off : off+4])
off += 4
if uint32(len(input)) < off+numInputs*32 {
return resultInvalid, errInputTooShort
}
inputBytes := input[off : off+numInputs*32]
if err := verifyPLONK(vkBytes, proofBytes, inputBytes); err != nil {
return resultInvalid, nil
}
return resultValid, nil
}
// --- STARK Verifier (strict-PQ STARK / FRI via P3Q) ---
// STARKVerifier verifies post-quantum STARK / FRI proofs on Z-Chain by
// delegating to the strict-PQ STARK verifier in precompile/starkfri
// (a Plonky3 fork: cSHAKE256 Merkle commitments over the Goldilocks
// 64-bit prime field, FRI low-degree test — NO KZG, NO pairings, NO
// trusted setup). This is the quantum-safe replacement for the
// pairing-based Groth16Verifier on the Z-Chain MLDSA-rollup path: a
// quantum adversary that breaks bn254 cannot forge a STARK/FRI proof,
// whose soundness rests only on the collision resistance of cSHAKE256
// and the Reed–Solomon proximity gap (no algebraic-group assumption).
//
// Input format:
//
// proof_len (4 bytes, big-endian)
// proof (proof_len bytes) — must begin with MagicHeader "P3Q1"
// pub_len (4 bytes, big-endian)
// inputs (pub_len bytes) — serialized public inputs
//
// Output: 0x01 if valid, 0x00 if invalid.
//
// Verifier binding. The actual FRI verifier runs out-of-band (Rust,
// behind the `starkfri_p3q` cgo build tag) and self-registers via
// starkfri.RegisterVerifier. When no verifier is bound (CGO_ENABLED=0
// or the tag is absent) starkfri.Verify returns ErrVerifierNotRegistered
// and this precompile FAILS CLOSED — it returns errVerifierUnavailable
// and NEVER accepts an unverified proof. There is no forgery oracle in
// the unbound configuration.
type STARKVerifier struct{}
func (v *STARKVerifier) RequiredGas(input []byte) uint64 {
return starkGas
}
func (v *STARKVerifier) Run(input []byte) ([]byte, error) {
// Parse [proof_len(4)][proof][pub_len(4)][pub].
if len(input) < 8 {
return resultInvalid, errInputTooShort
}
proofLen := binary.BigEndian.Uint32(input[:4])
off := uint32(4)
if uint32(len(input)) < off+proofLen+4 {
return resultInvalid, errInputTooShort
}
proof := input[off : off+proofLen]
off += proofLen
pubLen := binary.BigEndian.Uint32(input[off : off+4])
off += 4
if uint32(len(input)) < off+pubLen {
return resultInvalid, errInputTooShort
}
pub := input[off : off+pubLen]
ok, err := starkfri.Verify(proof, pub)
if err != nil {
// ErrVerifierNotRegistered (no cgo binding) or an FFI/decode
// failure. Either way the proof is NOT verified: fail closed.
// We distinguish "binding pending" from a malformed proof so an
// operator can tell a deployment-config gap from a bad proof,
// but in neither case do we return resultValid.
if errors.Is(err, starkfri.ErrVerifierNotRegistered) {
return resultInvalid, errVerifierUnavailable
}
return resultInvalid, nil // malformed / non-verifying proof is not an execution error
}
if !ok {
return resultInvalid, nil
}
return resultValid, nil
}
// --- Halo2 Verifier (stub) ---
// Halo2Verifier will verify Halo2 proofs with IPA commitments.
// Currently returns an error indicating the verifier is not yet available.
type Halo2Verifier struct{}
func (v *Halo2Verifier) RequiredGas(input []byte) uint64 {
return halo2Gas
}
func (v *Halo2Verifier) Run(input []byte) ([]byte, error) {
return resultInvalid, errNotImplemented
}
// --- Nova Verifier (stub) ---
// NovaVerifier will verify Nova IVC proofs.
// Currently returns an error indicating the verifier is not yet available.
type NovaVerifier struct{}
func (v *NovaVerifier) RequiredGas(input []byte) uint64 {
return novaGas
}
func (v *NovaVerifier) Run(input []byte) ([]byte, error) {
return resultInvalid, errNotImplemented
}
// --- Groth16 internals ---
type groth16Proof struct {
Ar bn254.G1Affine
Bs bn254.G2Affine
Krs bn254.G1Affine
}
type groth16VK struct {
Alpha bn254.G1Affine
Beta bn254.G2Affine
Gamma bn254.G2Affine
Delta bn254.G2Affine
K []bn254.G1Affine
}
// A pairing drops every term whose argument is the point at infinity, so a
// key or proof element at infinity removes itself from the equation: a key of
// all-infinity points turns the check into 1 == 1 and reports any proof valid.
// gnark decodes all-zero bytes to infinity and calls it in-subgroup, so the
// subgroup test alone does not catch it. checkG1 and checkG2 are the one place
// that decides what a usable point is.
func checkG1(p *bn254.G1Affine) error {
if !p.IsInSubGroup() {
return errSubgroupCheck
}
if p.IsInfinity() {
return errPointAtInfinity
}
return nil
}
func checkG2(p *bn254.G2Affine) error {
if !p.IsInSubGroup() {
return errSubgroupCheck
}
if p.IsInfinity() {
return errPointAtInfinity
}
return nil
}
func parseGroth16Proof(data []byte) (*groth16Proof, error) {
if len(data) < 256 {
return nil, errInputTooShort
}
p := &groth16Proof{}
if err := p.Ar.Unmarshal(data[0:64]); err != nil {
return nil, fmt.Errorf("Ar: %w", err)
}
if err := checkG1(&p.Ar); err != nil {
return nil, fmt.Errorf("Ar: %w", err)
}
if err := p.Bs.Unmarshal(data[64:192]); err != nil {
return nil, fmt.Errorf("Bs: %w", err)
}
if err := checkG2(&p.Bs); err != nil {
return nil, fmt.Errorf("Bs: %w", err)
}
if err := p.Krs.Unmarshal(data[192:256]); err != nil {
return nil, fmt.Errorf("Krs: %w", err)
}
if err := checkG1(&p.Krs); err != nil {
return nil, fmt.Errorf("Krs: %w", err)
}
return p, nil
}
func parseVerifyingKey(data []byte) (*groth16VK, error) {
// Alpha(64) | Beta(128) | Gamma(128) | Delta(128) | numK(4) | K[](64*numK)
minLen := 64 + 128 + 128 + 128 + 4
if len(data) < minLen {
return nil, errInputTooShort
}
vk := &groth16VK{}
off := 0
if err := vk.Alpha.Unmarshal(data[off : off+64]); err != nil {
return nil, fmt.Errorf("Alpha: %w", err)
}
if err := checkG1(&vk.Alpha); err != nil {
return nil, fmt.Errorf("Alpha: %w", err)
}
off += 64
if err := vk.Beta.Unmarshal(data[off : off+128]); err != nil {
return nil, fmt.Errorf("Beta: %w", err)
}
if err := checkG2(&vk.Beta); err != nil {
return nil, fmt.Errorf("Beta: %w", err)
}
off += 128
if err := vk.Gamma.Unmarshal(data[off : off+128]); err != nil {
return nil, fmt.Errorf("Gamma: %w", err)
}
if err := checkG2(&vk.Gamma); err != nil {
return nil, fmt.Errorf("Gamma: %w", err)
}
off += 128
if err := vk.Delta.Unmarshal(data[off : off+128]); err != nil {
return nil, fmt.Errorf("Delta: %w", err)
}
if err := checkG2(&vk.Delta); err != nil {
return nil, fmt.Errorf("Delta: %w", err)
}
off += 128
numK := binary.BigEndian.Uint32(data[off : off+4])
off += 4
if len(data) < off+int(numK)*64 {
return nil, errInputTooShort
}
vk.K = make([]bn254.G1Affine, numK)
for i := uint32(0); i < numK; i++ {
if err := vk.K[i].Unmarshal(data[off : off+64]); err != nil {
return nil, fmt.Errorf("K[%d]: %w", i, err)
}
if err := checkG1(&vk.K[i]); err != nil {
return nil, fmt.Errorf("K[%d]: %w", i, err)
}
off += 64
}
return vk, nil
}
// verifyGroth16 performs the Groth16 pairing check:
//
// e(A, B) == e(alpha, beta) * e(sum(w_i * K[i]), gamma) * e(C, delta)
//
// Equivalent to checking e(A, B) * e(-alpha, beta) * e(-pubLC, gamma) * e(-C, delta) == 1
// which is a single multi-pairing check (more efficient).
func verifyGroth16(proof *groth16Proof, vk *groth16VK, witness []fr.Element) error {
// Compute public input linear combination: K[0] + sum(witness[i] * K[i+1])
var pubLC bn254.G1Affine
pubLC.Set(&vk.K[0])
for i, w := range witness {
var wBI big.Int
var term bn254.G1Affine
term.ScalarMultiplication(&vk.K[i+1], w.BigInt(&wBI))
pubLC.Add(&pubLC, &term)
}
// Negate for multi-pairing check
var negAlpha, negPubLC, negKrs bn254.G1Affine
negAlpha.Neg(&vk.Alpha)
negPubLC.Neg(&pubLC)
negKrs.Neg(&proof.Krs)
// Multi-pairing: e(A, B) * e(-alpha, beta) * e(-pubLC, gamma) * e(-C, delta) == 1
ok, err := bn254.PairingCheck(
[]bn254.G1Affine{proof.Ar, negAlpha, negPubLC, negKrs},
[]bn254.G2Affine{proof.Bs, vk.Beta, vk.Gamma, vk.Delta},
)
if err != nil {
return errPairingFailed
}
if !ok {
return errProofInvalid
}
return nil
}
// errPLONKVerifierIncomplete is returned by verifyPLONK because the full
// PLONK verification equation (linearization-polynomial reconstruction +
// Fiat-Shamir challenge derivation + public-input binding) is not
// implemented here, and a verifier that does not evaluate the statement is
// not a verifier: it would accept any well-formed blob. So it FAILS CLOSED
// and accepts nothing. PLONK is a classical (quantum-breakable) system that is
// gated off on strict-PQ Lux chains (it is not registered there — see
// RegisterZKPrecompiles), so failing-closed here is the safe posture: the
// PLONKVerifier precompile now NEVER accepts a proof until a real,
// public-input-bound PLONK verifier is wired in.
var errPLONKVerifierIncomplete = errors.New(
"plonk: full verifier not implemented — failing closed (no universal-accept); " +
"PLONK is gated off strict-PQ chains, use the STARK/FRI verifier (0x82)")
// verifyPLONK fails closed. It performs structural parsing (lengths +
// on-curve / subgroup checks, which are cheap and let callers distinguish
// a malformed proof from a non-verifying one) and then returns
// errPLONKVerifierIncomplete WITHOUT ever returning nil — there is no
// path through this function that accepts a proof.
//
// Proof format: 7 G1 commitments (7*64=448 bytes) + opening evaluations.
// VK format: KZG SRS G2 point (128 bytes) + circuit commitments.
func verifyPLONK(vkBytes, proofBytes, inputBytes []byte) error {
// Minimum: 7 G1 points (448 bytes) + 3 scalars (96 bytes) = 544 bytes
if len(proofBytes) < 544 {
return errInputTooShort
}
// Structural parse: 7 G1 commitments must be well-formed on-curve
// prime-order points.
var commitments [7]bn254.G1Affine
for i := range commitments {
off := i * 64
if err := commitments[i].Unmarshal(proofBytes[off : off+64]); err != nil {
return fmt.Errorf("commitment %d: %w", i, err)
}
if !commitments[i].IsInSubGroup() {
return fmt.Errorf("commitment %d: %w", i, errSubgroupCheck)
}
}
// VK must contain at least the SRS G2 point (128 bytes), well-formed.
if len(vkBytes) < 128 {
return errInputTooShort
}
var srsG2 bn254.G2Affine
if err := srsG2.Unmarshal(vkBytes[:128]); err != nil {
return fmt.Errorf("SRS G2: %w", err)
}
if !srsG2.IsInSubGroup() {
return fmt.Errorf("SRS G2: %w", errSubgroupCheck)
}
_ = inputBytes
// Structure is well-formed, but the full PLONK check is not
// implemented. Fail closed — NEVER return nil for an unverified proof.
return errPLONKVerifierIncomplete
}