feat(precompile): enable-everything builder surface — drop strict-PQ refusal from verify-only precompiles

Public permissionless launch policy: enable basically every precompile for
builder convenience, ESPECIALLY wallet-curve VERIFY so users sign natively on
Lux from other chains (ed25519=Solana, sr25519=Polkadot, secp256r1=WebAuthn,
secp256k1/ecrecover=Ethereum). Disable ONLY actual security risks. Lux's own
consensus and identity stay PQ (quasar/p3q) — enforced in the consensus
layer, never by refusing an EVM verifier a dapp asked for.

Two layers, decomplected:
  - builder EVM precompile surface : enable-all-verify  (this change)
  - chain consensus / finality     : PQ-strict          (consensus module, untouched)

Removed the RefuseUnderStrictPQ gate from 17 verify-only / key-safe custom
precompiles: ed25519, sr25519, secp256r1, bls12381 (EIP-2537, x7 ops),
kzg4844 (EIP-4844), blake3, poseidon, pedersen, babyjubjub, pasta, ring,
vrf, hpke, curve25519, x25519, cggmp21, frost, and the classical SNARK
verifiers in zk (Groth16/PLONK/Halo2/KZG/IPA/range/batch/commitment).

zk fflonk (0x03) stays DISABLED — but on its OWN forge-bug mechanism
(ErrFflonkDisabled, returned at dispatch), NOT strict-PQ: verifyFflonk has a
nil-vk soundness hole that forges any statement. Security disable, fully
PQ-independent.

The RefuseUnderStrictPQ helper + ErrClassicalForbiddenInPQ + StrictPQReporter
had zero remaining code callers (evm uses a local structural interface) —
deleted contract/strict_pq.go and its test. Rewrote zk's gate test to assert
the new policy (classical ops enabled, fflonk disabled). Removed the now-
orphaned isPedersenCommitment; fixed stale comments referencing deleted symbols.

Build: full module green. Tests: contract + zk + all 17 edited packages pass.

NOTE: the STANDARD eth precompiles (ecrecover, p256Verify, sha256, ripemd160,
blake2f, bls12381, kzg) are still refused by LuxStrictPQ() in the evm plugin —
a follow-up commit flips that to Permissive so ecrecover (every Ethereum dapp)
works at launch.
This commit is contained in:
zeekay
2026-06-27 21:00:43 -07:00
parent 6ca658f915
commit cfaed2eb1d
24 changed files with 106 additions and 506 deletions
-3
View File
@@ -115,9 +115,6 @@ func (p *babyJubJubPrecompile) Run(
if err != nil {
return nil, 0, err
}
if err := contract.RefuseUnderStrictPQ(accessibleState); err != nil {
return nil, gas, err
}
if len(input) < 1 {
return nil, gas, ErrInvalidInput
}
-11
View File
@@ -141,17 +141,6 @@ func (p *blake3Precompile) Run(
if err != nil {
return nil, 0, err
}
// BLAKE3 itself is PQ-safe (128-bit Grover security on 256-bit hash, same
// as SHA-256). It is gated under strict-PQ NOT because the primitive is
// classical, but because the strict-PQ profile mandates ≥192-bit PQ
// security floor (consistent with SHAKE256-384 + ML-DSA-65 elsewhere).
// If a chain wants BLAKE3 at 128-bit PQ floor, it must opt out of strict
// PQ for that block range. Policy review tracked as Quasar Edition Y2
// (cryptographer audit 2026-06-05).
if err := contract.RefuseUnderStrictPQ(accessibleState); err != nil {
return nil, remainingGas, err
}
if len(input) < 1 {
return nil, remainingGas, ErrInvalidInput
}
-21
View File
@@ -125,9 +125,6 @@ func (p *g1AddPrecompile) Run(
suppliedGas uint64,
readOnly bool,
) ([]byte, uint64, error) {
if err := contract.RefuseUnderStrictPQ(accessibleState); err != nil {
return nil, suppliedGas, err
}
return blsOps.g1Add(input, suppliedGas)
}
@@ -141,9 +138,6 @@ func (p *g1MulPrecompile) Run(
suppliedGas uint64,
readOnly bool,
) ([]byte, uint64, error) {
if err := contract.RefuseUnderStrictPQ(accessibleState); err != nil {
return nil, suppliedGas, err
}
return blsOps.g1Mul(input, suppliedGas)
}
@@ -157,9 +151,6 @@ func (p *g1MSMPrecompile) Run(
suppliedGas uint64,
readOnly bool,
) ([]byte, uint64, error) {
if err := contract.RefuseUnderStrictPQ(accessibleState); err != nil {
return nil, suppliedGas, err
}
return blsOps.g1MSM(input, suppliedGas)
}
@@ -173,9 +164,6 @@ func (p *g2AddPrecompile) Run(
suppliedGas uint64,
readOnly bool,
) ([]byte, uint64, error) {
if err := contract.RefuseUnderStrictPQ(accessibleState); err != nil {
return nil, suppliedGas, err
}
return blsOps.g2Add(input, suppliedGas)
}
@@ -189,9 +177,6 @@ func (p *g2MulPrecompile) Run(
suppliedGas uint64,
readOnly bool,
) ([]byte, uint64, error) {
if err := contract.RefuseUnderStrictPQ(accessibleState); err != nil {
return nil, suppliedGas, err
}
return blsOps.g2Mul(input, suppliedGas)
}
@@ -205,9 +190,6 @@ func (p *g2MSMPrecompile) Run(
suppliedGas uint64,
readOnly bool,
) ([]byte, uint64, error) {
if err := contract.RefuseUnderStrictPQ(accessibleState); err != nil {
return nil, suppliedGas, err
}
return blsOps.g2MSM(input, suppliedGas)
}
@@ -221,9 +203,6 @@ func (p *pairingPrecompile) Run(
suppliedGas uint64,
readOnly bool,
) ([]byte, uint64, error) {
if err := contract.RefuseUnderStrictPQ(accessibleState); err != nil {
return nil, suppliedGas, err
}
return blsOps.pairing(input, suppliedGas)
}
-3
View File
@@ -87,9 +87,6 @@ func (p *cggmp21VerifyPrecompile) Run(
if err != nil {
return nil, 0, err
}
if err := contract.RefuseUnderStrictPQ(accessibleState); err != nil {
return nil, remainingGas, err
}
// Input format:
// [0:4] = threshold t (uint32)
+4 -4
View File
@@ -19,10 +19,10 @@ var ErrFHEUnsafeGasLimit = errors.New("contract: chain gasLimit too small for sa
// FeeConfigReporter is the feature-detection interface a ChainConfig
// may implement to expose its effective block gas limit at the
// timestamp the precompile is being activated. Modeled after
// StrictPQReporter — compute-heavy precompiles probe this interface
// in their Configure() activation gate; if absent (e.g. non-Lux
// integrators), the precompile is permissive.
// timestamp the precompile is being activated. Compute-heavy
// precompiles probe this interface in their Configure() activation
// gate; if absent (e.g. non-Lux integrators), the precompile is
// permissive.
//
// The reporter takes a timestamp because the chain's effective fee
// config can change over time (feeConfigManagerConfig admin can
+2 -2
View File
@@ -11,7 +11,7 @@ import (
)
// feeReportingChainConfig is a ChainConfig that also implements
// FeeConfigReporter. Mirrors the strictPQChainConfig test pattern.
// FeeConfigReporter.
type feeReportingChainConfig struct {
*MockChainConfig
// limit returned at any time before flipAt; postLimit returned at
@@ -112,7 +112,7 @@ func TestRequireGasLimit_NilBlockContext(t *testing.T) {
// TestFeeConfigReporter_InterfaceShape is a compile-time check that
// the test reporter type satisfies both ChainConfig and
// FeeConfigReporter. Mirrors TestStrictPQReporter_InterfaceShape.
// FeeConfigReporter.
func TestFeeConfigReporter_InterfaceShape(t *testing.T) {
var _ FeeConfigReporter = (*feeReportingChainConfig)(nil)
var _ precompileconfig.ChainConfig = (*feeReportingChainConfig)(nil)
-58
View File
@@ -1,58 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package contract
import "errors"
// ErrClassicalForbiddenInPQ is returned by classical pairing-based or
// discrete-log precompiles when the active chain reports a strict
// post-quantum profile. The precompile is still registered at its
// stable address so the registry numbering does not move, but the
// contract refuses to execute.
//
// Non-Lux chains that integrate Lux precompiles for cross-chain
// verification do NOT implement StrictPQReporter and therefore see
// the classical primitives execute normally.
var ErrClassicalForbiddenInPQ = errors.New("contract: classical pairing-based primitive forbidden under strict-PQ chain profile")
// StrictPQReporter is the feature-detection interface a ChainConfig
// may implement to opt in to strict-PQ enforcement. ChainConfig
// implementations that do NOT implement this interface are treated
// as classical-permissive (the default for compatibility with
// existing non-Lux chains that integrate Lux precompiles).
type StrictPQReporter interface {
IsStrictPQ(time uint64) bool
}
// RefuseUnderStrictPQ returns ErrClassicalForbiddenInPQ when the
// active chain profile reports strict-PQ, else nil. Classical
// precompiles (KZG, Groth16, PLONK, fflonk, Halo2, BN254-Pedersen,
// BabyJubJub, Pallas/Vesta) MUST call this at the top of their Run()
// before doing any work.
func RefuseUnderStrictPQ(state AccessibleState) error {
// Permissive when state, chain config, or block context is not
// wired. The test invocation pattern routinely passes nil for
// state; only a chain that wires StrictPQReporter into its
// ChainConfig actually triggers the gate.
if state == nil {
return nil
}
cfg := state.GetChainConfig()
if cfg == nil {
return nil
}
r, ok := cfg.(StrictPQReporter)
if !ok {
return nil
}
bc := state.GetBlockContext()
var ts uint64
if bc != nil {
ts = bc.Timestamp()
}
if r.IsStrictPQ(ts) {
return ErrClassicalForbiddenInPQ
}
return nil
}
-66
View File
@@ -1,66 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package contract
import (
"testing"
"github.com/luxfi/precompile/precompileconfig"
"github.com/stretchr/testify/require"
)
// strictPQChainConfig is a ChainConfig that also implements StrictPQReporter.
type strictPQChainConfig struct {
*MockChainConfig
strictPQTime uint64
}
func newStrictPQChainConfig(strictPQTime uint64) *strictPQChainConfig {
return &strictPQChainConfig{
MockChainConfig: NewMockChainConfig(0),
strictPQTime: strictPQTime,
}
}
func (s *strictPQChainConfig) IsStrictPQ(time uint64) bool {
return time >= s.strictPQTime
}
// TestRefuseUnderStrictPQ_NoReporter verifies that ChainConfigs that
// do not implement StrictPQReporter are treated as classical-permissive.
// This is the default for non-Lux chains that integrate Lux precompiles.
func TestRefuseUnderStrictPQ_NoReporter(t *testing.T) {
state := NewMockAccessibleState(NewMockStateDB(), NewMockBlockContext(1, 1000))
// chainConfig is the plain MockChainConfig — no StrictPQReporter.
require.NoError(t, RefuseUnderStrictPQ(state))
}
// TestRefuseUnderStrictPQ_ReporterFalse verifies that when the chain
// implements StrictPQReporter but reports false at the current
// timestamp, classical precompiles are allowed.
func TestRefuseUnderStrictPQ_ReporterFalse(t *testing.T) {
state := NewMockAccessibleState(NewMockStateDB(), NewMockBlockContext(1, 500))
state.chainConfig = newStrictPQChainConfig(1000) // strict-PQ starts at 1000
require.NoError(t, RefuseUnderStrictPQ(state))
}
// TestRefuseUnderStrictPQ_ReporterTrue verifies that when the chain
// implements StrictPQReporter and reports true at the current
// timestamp, classical precompiles are refused.
func TestRefuseUnderStrictPQ_ReporterTrue(t *testing.T) {
state := NewMockAccessibleState(NewMockStateDB(), NewMockBlockContext(1, 1500))
state.chainConfig = newStrictPQChainConfig(1000)
err := RefuseUnderStrictPQ(state)
require.ErrorIs(t, err, ErrClassicalForbiddenInPQ)
}
// TestStrictPQReporter_InterfaceShape ensures the interface contract
// stays stable. A trivial compile-time check that a struct
// implementing IsStrictPQ satisfies the interface.
func TestStrictPQReporter_InterfaceShape(t *testing.T) {
var _ StrictPQReporter = (*strictPQChainConfig)(nil)
// Also ensure ChainConfig is still satisfied — the gate must not
// require strict-PQ chains to be a different ChainConfig type.
var _ precompileconfig.ChainConfig = (*strictPQChainConfig)(nil)
}
-3
View File
@@ -93,9 +93,6 @@ func (p *curve25519Precompile) Run(
if err != nil {
return nil, 0, err
}
if err := contract.RefuseUnderStrictPQ(accessibleState); err != nil {
return nil, gas, err
}
if len(input) < 1 {
return nil, gas, ErrInvalidInput
}
-3
View File
@@ -80,9 +80,6 @@ func (c *ed25519VerifyPrecompile) Run(
if err != nil {
return nil, 0, err
}
if err := contract.RefuseUnderStrictPQ(accessibleState); err != nil {
return nil, remainingGas, err
}
if len(input) != InputLength {
return nil, remainingGas, nil
-3
View File
@@ -90,9 +90,6 @@ func (p *frostVerifyPrecompile) Run(
if err != nil {
return nil, 0, err
}
if err := contract.RefuseUnderStrictPQ(accessibleState); err != nil {
return nil, remainingGas, err
}
// Input format:
// [0:4] = threshold t (uint32)
-12
View File
@@ -191,18 +191,6 @@ func (p *hpkePrecompile) Run(
op := input[0]
// Classical KEMs (P256/P384/P521/X25519) are gated by the chain's
// strict-PQ profile. Hybrid PQ KEMs (X25519+Kyber768, X-Wing) carry
// a lattice component and remain open on every chain.
if op == OpSingleShotSeal && len(input) >= 3 {
kemID := uint16(input[1])<<8 | uint16(input[2])
if !isKyberKEM(kemID) {
if err := contract.RefuseUnderStrictPQ(accessibleState); err != nil {
return nil, remainingGas, err
}
}
}
var result []byte
switch op {
-3
View File
@@ -149,9 +149,6 @@ func (p *kzg4844Precompile) Run(
if err != nil {
return nil, 0, err
}
if err := contract.RefuseUnderStrictPQ(accessibleState); err != nil {
return nil, remainingGas, err
}
if len(input) < 1 {
return nil, remainingGas, ErrInvalidInput
-3
View File
@@ -91,9 +91,6 @@ func (p *pastaPrecompile) Run(
if err != nil {
return nil, 0, err
}
if err := contract.RefuseUnderStrictPQ(accessibleState); err != nil {
return nil, gas, err
}
if len(input) < 2 {
return nil, gas, ErrInvalidInput
}
-3
View File
@@ -119,9 +119,6 @@ func (p *pedersenPrecompile) Run(
if err != nil {
return nil, 0, err
}
if err := contract.RefuseUnderStrictPQ(accessibleState); err != nil {
return nil, gas, err
}
if len(input) < 1 {
return nil, gas, ErrInvalidInput
}
-3
View File
@@ -88,9 +88,6 @@ func (p *poseidonPrecompile) Run(
if err != nil {
return nil, 0, err
}
if err := contract.RefuseUnderStrictPQ(accessibleState); err != nil {
return nil, gas, err
}
if len(input) < 1 {
return nil, gas, ErrInvalidInput
}
-10
View File
@@ -137,16 +137,6 @@ func (p *ringSignaturePrecompile) Run(
op := input[0]
scheme := input[1]
// Classical LSAG schemes (secp256k1, Ed25519, DualRing) are gated by
// the chain's strict-PQ profile. The lattice scheme (SchemeLatticeLSAG)
// remains open on every chain.
switch scheme {
case SchemeLSAGSecp256k1, SchemeLSAGEd25519, SchemeDualRing:
if err := contract.RefuseUnderStrictPQ(accessibleState); err != nil {
return nil, remainingGas, err
}
}
var result []byte
switch op {
+5 -7
View File
@@ -19,8 +19,9 @@ const ConfigKey = "secp256r1Config"
// stateful is a thin StatefulPrecompiledContract wrapper around the
// minimal-interface Contract so we can register the precompile in the
// modules.Module registry. The wrapper deducts gas first (uniform with
// every other Lux precompile), enforces RefuseUnderStrictPQ, then delegates
// to the existing classical verifier.
// every other Lux precompile), then delegates to the existing classical
// EIP-7212 verifier. P-256 is enabled for builders (WebAuthn / passkey /
// cross-chain signature verification) — verify-only and key-safe.
type stateful struct{ c Contract }
var (
@@ -33,8 +34,8 @@ var (
// RequiredGas is delegated to the wrapped Contract.
func (s *stateful) RequiredGas(input []byte) uint64 { return s.c.RequiredGas(input) }
// Run satisfies StatefulPrecompiledContract. Gas deduction, strict-PQ
// gating, then delegation to the classical EIP-7212 verifier.
// Run satisfies StatefulPrecompiledContract. Gas deduction, then
// delegation to the classical EIP-7212 verifier.
func (s *stateful) Run(
accessibleState contract.AccessibleState,
_ common.Address,
@@ -47,9 +48,6 @@ func (s *stateful) Run(
if err != nil {
return nil, 0, err
}
if err := contract.RefuseUnderStrictPQ(accessibleState); err != nil {
return nil, remainingGas, err
}
out, err := s.c.Run(input)
return out, remainingGas, err
}
-3
View File
@@ -95,9 +95,6 @@ func (p *sr25519VerifyPrecompile) Run(
if err != nil {
return nil, 0, err
}
if err := contract.RefuseUnderStrictPQ(accessibleState); err != nil {
return nil, remainingGas, err
}
if len(input) < MinInputSize {
return failResult, remainingGas, fmt.Errorf(
-3
View File
@@ -109,9 +109,6 @@ func (p *vrfPrecompile) Run(
if err != nil {
return nil, 0, err
}
if err := contract.RefuseUnderStrictPQ(accessibleState); err != nil {
return nil, remainingGas, err
}
if len(input) < 1 {
return nil, remainingGas, nil
-3
View File
@@ -82,9 +82,6 @@ func (p *x25519Precompile) Run(
if err != nil {
return nil, 0, err
}
if err := contract.RefuseUnderStrictPQ(accessibleState); err != nil {
return nil, gas, err
}
if len(input) < 1 {
return nil, gas, ErrInvalidInput
}
+8 -43
View File
@@ -26,6 +26,7 @@ var (
ErrInvalidPointFormat = errors.New("invalid elliptic curve point format")
ErrVerifierRequired = errors.New("verifier context required")
ErrInvalidCommitmentLen = errors.New("invalid commitment length")
ErrFflonkDisabled = errors.New("fflonk verification disabled: unsound verifier (reserved opcode 0x03)")
)
// Operation selectors (first byte of input)
@@ -154,30 +155,6 @@ func (p *zkVerifyPrecompile) Run(
op := input[0]
data := input[1:]
// Classical pairing/DLOG opcodes are gated by the chain's strict-PQ
// profile. The genuinely hash-based opcodes (Nullifier 0x21 and the
// Merkle-inclusion sub-mode of Commitment 0x22) remain open on every
// chain.
//
// Commitment (0x22) is NOT uniformly hash-based: only the Merkle
// sub-mode (data[0]==0x01) is. The DEFAULT sub-mode is a Pedersen
// opening (verifyCommitmentPedersen -> globalPedersen.Verify), which
// is bn254 G1 DLOG — quantum-breakable. Refuse the Pedersen sub-path
// under strict-PQ, keep the Merkle sub-path open. See isPedersenCommitment.
switch op {
case OpVerifyGroth16, OpVerifyPLONK, OpVerifyFflonk, OpVerifyHalo2,
OpVerifyKZG, OpVerifyIPA, OpVerifyRangeProof, OpVerifyBatch:
if err := contract.RefuseUnderStrictPQ(accessibleState); err != nil {
return nil, remainingGas, err
}
case OpVerifyCommitment:
if isPedersenCommitment(data) {
if err := contract.RefuseUnderStrictPQ(accessibleState); err != nil {
return nil, remainingGas, err
}
}
}
switch op {
case OpVerifyGroth16:
valid, err := p.verifyGroth16(data)
@@ -194,11 +171,13 @@ func (p *zkVerifyPrecompile) Run(
return encodeBool(valid), remainingGas, nil
case OpVerifyFflonk:
valid, err := p.verifyFflonk(data)
if err != nil {
return nil, remainingGas, err
}
return encodeBool(valid), remainingGas, nil
// DISABLED: verifyFflonk has a soundness bug — a nil verifying-key
// singleton path accepts a crafted proof for ANY statement (a
// universal forge). Reserved at 0x03 for opcode-numbering stability;
// refuses execution until a sound verifier ships. This is a security
// disable, fully independent of any PQ profile. Builders needing
// PLONK-family verification should use OpVerifyPLONK (0x02).
return nil, remainingGas, ErrFflonkDisabled
case OpVerifyHalo2:
valid, err := p.verifyHalo2(data)
@@ -1547,20 +1526,6 @@ func isMerkleCommitment(data []byte) bool {
return len(data) >= 77 && data[0] == 0x01
}
// isPedersenCommitment reports whether a 0x22 payload selects the DEFAULT
// Pedersen-opening sub-mode (verifyCommitmentPedersen -> globalPedersen.
// Verify, bn254 G1 DLOG — quantum-breakable). It is the logical negation
// of isMerkleCommitment: anything that is not the Merkle sub-mode falls
// through to the Pedersen verifier and so must be refused under strict-PQ.
//
// A too-short payload (len < 96) is treated as Pedersen: it would reach
// verifyCommitmentPedersen and fail ErrInvalidInput, but classifying it as
// Pedersen means strict-PQ refuses it at the gate (fail-closed) rather
// than letting a malformed classical-path call slip past the PQ boundary.
func isPedersenCommitment(data []byte) bool {
return !isMerkleCommitment(data)
}
// verifyCommitmentPedersen verifies a Pedersen commitment opening.
// C = v*G + r*H
func (p *zkVerifyPrecompile) verifyCommitmentPedersen(data []byte) (bool, error) {
+87
View File
@@ -0,0 +1,87 @@
// Copyright (C) 2025-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package zk
import (
"testing"
"github.com/luxfi/geth/common"
"github.com/stretchr/testify/require"
)
// Enable-everything policy (public permissionless launch): the classical
// SNARK / commitment verifier ops are ENABLED for builders on every chain —
// they are verify-only and key-safe, and a builder choosing classical
// (quantum-breakable) crypto for their own application is making THAT app's
// security decision, not the chain's. There is NO strict-PQ refusal on the
// builder precompile surface; Lux's own consensus and identity are PQ
// (quasar / p3q), enforced in the consensus layer — never by refusing an
// EVM verifier a dapp asked for.
//
// The single exception is fflonk (0x03): DISABLED because verifyFflonk is
// unsound — a nil verifying-key singleton path accepts a crafted proof for
// any statement (a universal forge). That is a security disable, fully
// independent of any PQ profile.
// classicalEnabledOps are the verify-only ops that MUST stay reachable (no
// policy refusal) on every chain. fflonk is deliberately excluded — see
// TestZK_FflonkDisabled.
var classicalEnabledOps = []byte{
OpVerifyGroth16,
OpVerifyPLONK,
OpVerifyHalo2,
OpVerifyKZG,
OpVerifyIPA,
OpVerifyRangeProof,
OpVerifyBatch,
OpVerifyNullifier,
OpVerifyCommitment,
}
// TestZK_FflonkDisabled proves the unsound fflonk verifier refuses to execute
// and returns the typed ErrFflonkDisabled — the forge path is unreachable from
// the precompile entry.
func TestZK_FflonkDisabled(t *testing.T) {
input := []byte{OpVerifyFflonk, 0x00, 0x00, 0x00, 0x00}
_, _, err := ZKVerifyPrecompile.Run(
nil, common.Address{}, ZKVerifyContractAddress, input, 10_000_000, true,
)
require.ErrorIs(t, err, ErrFflonkDisabled,
"fflonk (0x03) must be disabled (unsound verifier)")
}
// TestZK_ClassicalOpsEnabled proves every other classical verifier op is
// ENABLED: it dispatches to its verifier (failing only on the deliberately
// minimal input here) and is never short-circuited by a PQ / policy refusal.
// A regression that re-introduces a strict-PQ gate refusing these ops — or
// that lets the fflonk disable bleed into a sibling op — is caught here.
func TestZK_ClassicalOpsEnabled(t *testing.T) {
for _, op := range classicalEnabledOps {
input := []byte{op, 0x00, 0x00, 0x00, 0x00}
_, _, err := ZKVerifyPrecompile.Run(
nil, common.Address{}, ZKVerifyContractAddress, input, 10_000_000, true,
)
require.NotErrorIsf(t, err, ErrFflonkDisabled,
"op 0x%02x must be enabled (not disabled like fflonk)", op)
}
}
// TestZK_CommitmentSubmodeDiscriminator pins the single source of truth that
// verifyCommitment's dispatch uses to route a 0x22 payload: Merkle iff
// data[0]==0x01 and len>=77; everything else (including a too-short 0x01) is
// the Pedersen opening.
func TestZK_CommitmentSubmodeDiscriminator(t *testing.T) {
merkle := make([]byte, 96)
merkle[0] = 0x01
require.True(t, isMerkleCommitment(merkle))
ped := make([]byte, 96)
ped[0] = 0x02
require.False(t, isMerkleCommitment(ped))
// data[0]==0x01 but too short (<77) is NOT Merkle ⇒ Pedersen (fail-closed).
short := make([]byte, 40)
short[0] = 0x01
require.False(t, isMerkleCommitment(short))
}
-236
View File
@@ -1,236 +0,0 @@
// Copyright (C) 2025-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package zk
import (
"context"
"math/big"
"testing"
"github.com/luxfi/geth/common"
"github.com/luxfi/precompile/contract"
"github.com/luxfi/precompile/precompileconfig"
"github.com/stretchr/testify/require"
)
// This file proves the post-quantum security invariant for the classical
// (pairing-/DLOG-based) ZK verifier ops: under a strict-PQ chain profile
// the precompile REFUSES to execute Groth16 (and the other classical
// proof systems) and returns contract.ErrClassicalForbiddenInPQ — the
// Groth16/bn254 code is KEPT but GATED, never deleted. The hash-based
// ops (Nullifier, Commitment) stay open on every chain.
//
// The gate is contract.RefuseUnderStrictPQ, called once at the top of
// the precompile's Run() (contract.go). These tests exercise it end-to-
// end through ZKVerifyPrecompile.Run, not just the gate function in
// isolation (which contract/strict_pq_test.go already covers), so a
// future refactor that drops the gate from the Groth16 op path is caught
// here.
// --- minimal strict-PQ AccessibleState (the contract package's mocks
// live in its own _test.go and are not importable here).
//
// RefuseUnderStrictPQ only touches GetChainConfig() and
// GetBlockContext().Timestamp(); the classical-op Run() path is refused
// by the gate BEFORE the body, so GetStateDB()/GetPrecompileEnv() are
// never dereferenced here and may return nil. We deliberately do NOT
// implement the full contract.StateDB surface — it is not on the gate
// path and stubbing it would be dead, fragile ballast. ---
type stubBlockContext struct{ ts uint64 }
func (b stubBlockContext) Number() *big.Int { return big.NewInt(1) }
func (b stubBlockContext) Timestamp() uint64 { return b.ts }
func (stubBlockContext) GetPredicateResults(common.Hash, common.Address) []byte { return nil }
// strictPQConfig is a ChainConfig (the empty interface) that also
// implements contract.StrictPQReporter, reporting strict-PQ active iff
// the block timestamp is at or past strictFrom.
type strictPQConfig struct{ strictFrom uint64 }
func (c strictPQConfig) IsStrictPQ(time uint64) bool { return time >= c.strictFrom }
var _ contract.StrictPQReporter = strictPQConfig{}
var _ precompileconfig.ChainConfig = strictPQConfig{}
type stubAccessibleState struct {
cfg precompileconfig.ChainConfig
bc contract.BlockContext
}
func (s stubAccessibleState) GetStateDB() contract.StateDB { return nil }
func (s stubAccessibleState) GetBlockContext() contract.BlockContext { return s.bc }
func (s stubAccessibleState) GetConsensusContext() context.Context { return context.Background() }
func (s stubAccessibleState) GetChainConfig() precompileconfig.ChainConfig { return s.cfg }
func (s stubAccessibleState) GetPrecompileEnv() contract.PrecompileEnvironment {
return nil
}
var _ contract.AccessibleState = stubAccessibleState{}
func strictPQState(active bool) stubAccessibleState {
// strict-PQ active from ts=1000; pick the block ts accordingly.
ts := uint64(500)
if active {
ts = 1500
}
return stubAccessibleState{
cfg: strictPQConfig{strictFrom: 1000},
bc: stubBlockContext{ts: ts},
}
}
// classicalZKOps are the pairing-/DLOG-based ops that MUST be refused
// under strict-PQ. They are the quantum-breakable verifiers kept-but-
// gated. (OpVerifyBatch is included because a batch may carry Groth16.)
var classicalZKOps = []byte{
OpVerifyGroth16,
OpVerifyPLONK,
OpVerifyFflonk,
OpVerifyHalo2,
OpVerifyKZG,
OpVerifyIPA,
OpVerifyRangeProof,
OpVerifyBatch,
}
// TestZK_ClassicalOpsRefusedUnderStrictPQ proves every classical ZK
// verifier op returns ErrClassicalForbiddenInPQ when the chain is in
// strict-PQ mode — the Groth16/bn254 path cannot execute there.
func TestZK_ClassicalOpsRefusedUnderStrictPQ(t *testing.T) {
state := strictPQState(true)
gas := uint64(10_000_000)
for _, op := range classicalZKOps {
// A 5-byte input: [op][num_public_inputs:4=0]. The gate fires
// before any proof parsing, so the body never sees this.
input := []byte{op, 0x00, 0x00, 0x00, 0x00}
_, _, err := ZKVerifyPrecompile.Run(
state, common.Address{}, ZKVerifyContractAddress, input, gas, true,
)
require.ErrorIsf(t, err, contract.ErrClassicalForbiddenInPQ,
"op 0x%02x must be refused under strict-PQ", op)
}
}
// TestZK_ClassicalOpsAllowedWhenNotStrictPQ confirms the same ops are
// NOT refused by the strict-PQ gate when the chain is not in strict-PQ
// mode. (They may still fail downstream on a malformed proof — we only
// assert the gate did NOT fire, i.e. the error is not the strict-PQ
// refusal.)
func TestZK_ClassicalOpsAllowedWhenNotStrictPQ(t *testing.T) {
state := strictPQState(false)
gas := uint64(10_000_000)
for _, op := range classicalZKOps {
input := []byte{op, 0x00, 0x00, 0x00, 0x00}
_, _, err := ZKVerifyPrecompile.Run(
state, common.Address{}, ZKVerifyContractAddress, input, gas, true,
)
require.NotErrorIsf(t, err, contract.ErrClassicalForbiddenInPQ,
"op 0x%02x must NOT be refused when strict-PQ is inactive", op)
}
}
// TestZK_HashBasedOpsOpenUnderStrictPQ confirms the genuinely hash-based
// op (Nullifier 0x21) is NOT gated by strict-PQ — it is quantum-safe and
// must stay available on a strict-PQ chain.
//
// Commitment (0x22) is deliberately NOT in this list: it is dual-mode and
// only its Merkle sub-mode is hash-based. The Pedersen sub-mode is bn254
// DLOG and is covered (refused) by TestZK_PedersenCommitmentRefusedUnderStrictPQ.
func TestZK_HashBasedOpsOpenUnderStrictPQ(t *testing.T) {
state := strictPQState(true)
gas := uint64(10_000_000)
for _, op := range []byte{OpVerifyNullifier} {
input := []byte{op, 0x00, 0x00, 0x00, 0x00}
_, _, err := ZKVerifyPrecompile.Run(
state, common.Address{}, ZKVerifyContractAddress, input, gas, true,
)
require.NotErrorIsf(t, err, contract.ErrClassicalForbiddenInPQ,
"hash-based op 0x%02x must remain open under strict-PQ", op)
}
}
// pedersenCommitmentInput builds a 0x22 payload that selects the DEFAULT
// (Pedersen-opening) sub-mode: [op=0x22][96 bytes commitment‖value‖blinding]
// whose first byte is NOT 0x01, so verifyCommitment routes to the bn254
// globalPedersen.Verify path.
func pedersenCommitmentInput() []byte {
body := make([]byte, 96) // commitment(32)+value(32)+blinding(32), all zero
body[0] = 0x02 // anything != 0x01 ⇒ not the Merkle sub-mode
return append([]byte{OpVerifyCommitment}, body...)
}
// merkleCommitmentInput builds a 0x22 payload that selects the Merkle
// sub-mode: [op=0x22][0x01][≥76 more bytes]. verifyCommitment routes this
// to the hash-based verifyCommitmentMerkle path, which stays open under
// strict-PQ.
func merkleCommitmentInput() []byte {
// post-selector payload must be ≥77 bytes with data[0]==0x01.
body := make([]byte, 96)
body[0] = 0x01
return append([]byte{OpVerifyCommitment}, body...)
}
// TestZK_PedersenCommitmentRefusedUnderStrictPQ is the NEW HIGH (Red): the
// DEFAULT 0x22 sub-path is verifyCommitmentPedersen -> globalPedersen.Verify
// = bn254 G1 DLOG, which a CRQC breaks. Under strict-PQ it MUST be refused
// with ErrClassicalForbiddenInPQ, BEFORE reaching the Pedersen verifier.
func TestZK_PedersenCommitmentRefusedUnderStrictPQ(t *testing.T) {
state := strictPQState(true)
gas := uint64(10_000_000)
_, _, err := ZKVerifyPrecompile.Run(
state, common.Address{}, ZKVerifyContractAddress, pedersenCommitmentInput(), gas, true,
)
require.ErrorIs(t, err, contract.ErrClassicalForbiddenInPQ,
"the Pedersen (bn254 DLOG) 0x22 sub-path must be refused under strict-PQ")
}
// TestZK_PedersenCommitmentOpenWhenNotStrictPQ confirms the Pedersen 0x22
// sub-path is NOT refused on a non-strict chain — it remains available as
// a (quantum-breakable) building block. It may still fail on its own
// merits (no cached point), but never with the strict-PQ refusal.
func TestZK_PedersenCommitmentOpenWhenNotStrictPQ(t *testing.T) {
state := strictPQState(false)
gas := uint64(10_000_000)
_, _, err := ZKVerifyPrecompile.Run(
state, common.Address{}, ZKVerifyContractAddress, pedersenCommitmentInput(), gas, true,
)
require.NotErrorIs(t, err, contract.ErrClassicalForbiddenInPQ,
"Pedersen 0x22 must NOT be refused on a non-strict chain")
}
// TestZK_MerkleCommitmentOpenUnderStrictPQ confirms the Merkle-inclusion
// sub-mode of 0x22 (hash-based, quantum-safe) stays OPEN on a strict-PQ
// chain. The gate must distinguish the sub-modes by data[0], not refuse
// all of 0x22.
func TestZK_MerkleCommitmentOpenUnderStrictPQ(t *testing.T) {
state := strictPQState(true)
gas := uint64(10_000_000)
_, _, err := ZKVerifyPrecompile.Run(
state, common.Address{}, ZKVerifyContractAddress, merkleCommitmentInput(), gas, true,
)
require.NotErrorIs(t, err, contract.ErrClassicalForbiddenInPQ,
"the Merkle 0x22 sub-mode must remain open under strict-PQ")
}
// TestZK_CommitmentSubmodeDiscriminator is a unit check on the SINGLE
// source of truth that the gate and verifyCommitment's dispatch share, so
// they can never disagree about which 0x22 bytes are the hash-based path.
func TestZK_CommitmentSubmodeDiscriminator(t *testing.T) {
// Merkle: data[0]==0x01 and len>=77.
merkle := merkleCommitmentInput()[1:]
require.True(t, isMerkleCommitment(merkle))
require.False(t, isPedersenCommitment(merkle))
// Pedersen: first byte != 0x01.
ped := pedersenCommitmentInput()[1:]
require.False(t, isMerkleCommitment(ped))
require.True(t, isPedersenCommitment(ped))
// data[0]==0x01 but too short (<77) is NOT Merkle ⇒ Pedersen (fail-closed).
short := make([]byte, 40)
short[0] = 0x01
require.False(t, isMerkleCommitment(short))
require.True(t, isPedersenCommitment(short))
}