mirror of
https://github.com/luxfi/precompile.git
synced 2026-07-27 03:33:45 +00:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
Reference in New Issue
Block a user