Files
zeekay f8fba1ba39 chains(zkvm): hard-disable classical shielded/ZK paths on strict-PQ (Red H1/H2/H4)
H1 — shielded-tx ProofVerifier was classical-only (groth16/plonk) and
fail-closed only via all-zero dummy VKs; real bn254 VKs would re-enable a
CRQC-forgeable shield/unshield mint/steal path. Add ZConfig.StrictPQ and a
single gate refuseClassicalUnderStrictPQ(proofType) called at the top of
VerifyTransactionProof AND per-tx in the GPU batch path (which verifies
groth16 inline, bypassing VerifyTransactionProof). Under strict-PQ only
"stark" is accepted — wired to precompile/starkfri (cSHAKE256/Goldilocks
FRI, fail-closed until the prover binds), public inputs bound to the tx's
nullifiers+commitments. loadVerifyingKeys now ERRORS on a non-dummy bn254
VK under strict-PQ (explicit fail-closed, not implicit dummy-keys). The
remaining prover work (p3q_prove C ABI + shielded/ML-DSA AIR) is tracked
as a code comment; shielded value is effectively disabled on strict-PQ
chains until it lands — the correct no-classical-fallback posture.

H2 — Groth16/PLONK precompiles (0x80/0x81) were registered unconditionally
and Run([]byte) has no AccessibleState to call RefuseUnderStrictPQ.
RegisterZKPrecompiles(registry, strictPQ) now omits Groth16/PLONK on
strict-PQ chains (registering STARK 0x82 + fail-closed Halo2/Nova stubs
only) so 0x80/0x81 fail closed by absence; non-strict chains keep them as
an optional building block (kept, not deleted). CrossChainZKVerifier gains
StrictPQ that refuses routing Groth16/PLONK while still routing STARK.

H4 — verifyPLONK computed a self-cancelling pairing, discarded the result,
ignored public inputs, and returned valid for ANY >=544-byte blob (total
bypass). Now fails closed (errPLONKVerifierIncomplete) after structural
parsing — NEVER universal-accepts.

Groth16/bn254/PLONK remain present-but-gated; STARK/FRI (P3Q) is the only
accepted proof path on strict-PQ chains.

Tests: strict-PQ refuses groth16/plonk/bulletproofs (direct + GPU-batch);
real-VK-load errors; STARK fails-closed unbound and accepts bound with
nullifier+commitment-bound public inputs; non-strict path unchanged;
strict registry omits classical + keeps STARK; cross-chain strict refuses
classical; PLONK never universal-accepts. H-02 regression updated to the
new wired-STARK fail-closed behavior.
2026-06-19 12:48:26 -07:00

68 lines
2.5 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package precompiles
import "fmt"
// PrecompileRegistry allows registering precompiled contracts at fixed addresses.
type PrecompileRegistry interface {
Register(addr byte, contract PrecompiledContract)
}
// RegisterZKPrecompiles registers the Z-Chain ZK verifier precompiles.
//
// strictPQ gates the classical (quantum-breakable) verifiers. The
// PrecompiledContract.Run([]byte) interface carries no AccessibleState,
// so a strict-PQ chain cannot refuse at call time the way the
// AccessibleState-aware precompile/zk path does (RefuseUnderStrictPQ).
// Instead the decision is made HERE, at genesis registration: on a
// strict-PQ chain the classical Groth16 (0x80) and PLONK (0x81)
// verifiers are simply NOT REGISTERED, so any call to those addresses
// hits "no precompile at address" — fail-closed by absence. The
// post-quantum STARK/FRI verifier (0x82) is the only accepted proof
// system. Halo2 (0x83) and Nova (0x84) are stubs that already
// fail-closed (errNotImplemented) on every chain.
//
// On a non-strict chain every verifier is registered: Groth16/PLONK are
// kept as an OPTIONAL building block for cross-chain verification of
// classical proofs, never deleted.
func RegisterZKPrecompiles(registry PrecompileRegistry, strictPQ bool) {
// Post-quantum + always-fail-closed stubs: registered on every chain.
registry.Register(STARKVerifierAddr, &STARKVerifier{})
registry.Register(Halo2VerifierAddr, &Halo2Verifier{})
registry.Register(NovaVerifierAddr, &NovaVerifier{})
if strictPQ {
// Classical pairing-based verifiers are forbidden on strict-PQ
// chains: omit them so 0x80/0x81 resolve to "no precompile".
return
}
// Non-strict chain: classical verifiers available as an optional
// (quantum-breakable) building block.
registry.Register(Groth16VerifierAddr, &Groth16Verifier{})
registry.Register(PLONKVerifierAddr, &PLONKVerifier{})
}
// MapRegistry is a simple map-based precompile registry for testing.
type MapRegistry struct {
contracts map[byte]PrecompiledContract
}
func NewMapRegistry() *MapRegistry {
return &MapRegistry{contracts: make(map[byte]PrecompiledContract)}
}
func (r *MapRegistry) Register(addr byte, contract PrecompiledContract) {
r.contracts[addr] = contract
}
func (r *MapRegistry) Get(addr byte) (PrecompiledContract, error) {
c, ok := r.contracts[addr]
if !ok {
return nil, fmt.Errorf("no precompile at address 0x%02x", addr)
}
return c, nil
}