precompile: decomplect strict-PQ gate across every classical Run() (#2)

* precompile: pulsar threshold ML-DSA at LP-4200 0x012204

Adds the Pulsar precompile slot for threshold ML-DSA verification.
Pulsar's wire format is byte-equal to FIPS 204 ML-DSA (Class N1
manifesto), so the verification op dispatches to luxfi/crypto/mldsa
under a Pulsar-specific domain-separation context.

- Address: 0x0000000000000000000000000000000000012204 (LP-4200 unified
  PQCrypto block; companion to ML-KEM 0x012201, ML-DSA 0x012202,
  SLH-DSA 0x012203).
- Three modes: Pulsar-44 / Pulsar-65 / Pulsar-87 mirroring ML-DSA
  Categories 2 / 3 / 5. Production target is Pulsar-65.
- Gas: per-mode base + 10/byte (matches ML-DSA precompile tier).
- Context: lux-evm-precompile-pulsar-v1 to keep on-chain transcripts
  unambiguous vs. single-party ML-DSA at 0x012202.

Tests: 5 passing (address pin, gas table, round-trip across 3 modes,
tampered-signature rejection, invalid-mode rejection). GOWORK=off
go test -count=1 ./pulsar/... is green.

Imports github.com/luxfi/pulsar-m (pre-rename); will be retargeted to
github.com/luxfi/pulsar when the cross-repo move lands.

* precompile/LLM.md: mark Pulsar 0x012204 as IMPLEMENTED

Pulsar precompile now lives in precompile/pulsar/ and dispatches to
luxfi/crypto/mldsa under a Pulsar-specific context. Update the
LP-4200 canonical note to reflect deployment status alongside the
other three PQCrypto slots.

* precompile/zk: rip classical pairing-based opcodes (PQ-only)

Shor breaks Groth16/PLONK/fflonk/Halo2/KZG/IPA/Bulletproofs.
The 0x0900 EVM precompile now exposes only the PQ-safe ops:
hash-based nullifiers (0x21) and Merkle commitment inclusion (0x22).
PQ STARK verification is dispatched via the Z-Chain envelope path
(consensus/protocol/zchain/verify.go), not from this precompile.

* precompile/kzg4844: retire BLS12-381 KZG blob-opening precompile

KZG point-evaluation verifies via the BLS12-381 pairing
e(C - [y]G1, G2) == e(W, [tau - z]G2). The discrete-log assumption
that anchors BLS12-381's security is broken by Shor's algorithm, so
EIP-4844-style blob commitments give zero soundness against a future
quantum adversary. The Lux EVM is PQ-only.

Strip the implementation (gokzg4844, accel GPU MSM, all CPU opcodes).
The module still registers the address 0xB002 so the precompile
registry numbering stays stable, but every call to the precompile
returns the new contract.ErrClassicalForbiddenInPQ sentinel. The
minimum 21000 gas is deducted on every call to prevent the retired
address being used as a zero-cost probe.

Blob availability on the Lux EVM is now provided by hash-based
commitments (Blake3 / Poseidon2) and STARK-validated state roots.

* precompile/zk: strip Go-level classical pairing SNARK verifiers

The Run() path of the 0x0900 ZK precompile only exposes hash-based
nullifier checks and Merkle commitment inclusion, both PQ-safe. The
remaining Go-level methods on ZKVerifier (VerifyGroth16, VerifyPlonk,
VerifyFflonk, VerifyKZG, VerifyRangeProof, VerifyRollupBatch and
their helpers) all dispatched through a BN254 pairing check or a
BLS12-381 KZG opening, both broken by Shor's algorithm.

Strip those methods, their bn256 / kzg4844 / accel-MSM dependencies,
and the rollup-batch types (RollupBatch, RollupConfig, StateTransition,
KZGSetup, BN254P) that only existed to feed the pairing verifier.
Update verifier_test.go to cover only the PQ-safe surface that
remains: NewZKVerifier, RegisterVerifyingKey (Go-side bookkeeping),
nullifier check/spend, confidential pool create/add-commitment,
hash-based Merkle inclusion.

PQ STARK verification continues to be handled by the P3Q backend on
the Z-chain envelope path (consensus/protocol/zchain/verify.go); it
was never wired through this Go struct.

* precompile/babyjubjub: retire BN254-companion twisted Edwards curve

Baby Jubjub is the ZK-friendly twisted Edwards curve aligned to BN254's
scalar field (used by circom / iden3 / Polygon zkEVM / Semaphore /
RLN / EdDSA-on-BN254). Its security reduces to discrete-log on a curve
of BN254 scalar order; Shor breaks that in poly time. Every consumer
of Baby Jubjub is a classical pairing-based SNARK gadget with no PQ
analogue, so the Lux EVM retires the precompile rather than offer a
quantum-broken primitive.

Strip the gnark-crypto twisted-edwards implementation. The module
still registers address 0x0500..0007 so the precompile registry
numbering stays stable, but every call returns
contract.ErrClassicalForbiddenInPQ with 21000 minimum gas deducted.

* precompile/pedersen: retire BN254 Pedersen commitment precompile

The implementation built homomorphic Pedersen commitments over BN254 G1
(C = v*G + r*H, plus a 32-slot vector commitment). Both binding and
hiding reduce to the discrete-log assumption on BN254, which Shor's
algorithm breaks. The Lux EVM is PQ-only and retires the precompile
rather than offer a quantum-broken commitment.

Strip the gnark-crypto BN254 implementation and the 34 deterministic
generators. The module still registers address 0x0500..0006 so the
precompile registry numbering stays stable, but every call returns
contract.ErrClassicalForbiddenInPQ with the 21000 minimum gas
deducted.

PQ-safe commitment schemes on the Lux EVM use hash-based primitives
(Blake3 0x3202, Poseidon2 0x3201) and STARK gadgets on Z-chain.

* Revert "precompile/pedersen: retire BN254 Pedersen commitment precompile"

This reverts commit cc0db71edf.

* Revert "precompile/babyjubjub: retire BN254-companion twisted Edwards curve"

This reverts commit e666729d6b.

* Revert "precompile/zk: strip Go-level classical pairing SNARK verifiers"

This reverts commit 92e5ac182b.

* Revert "precompile/kzg4844: retire BLS12-381 KZG blob-opening precompile"

This reverts commit 5786c8f57e.

* Revert "precompile/zk: rip classical pairing-based opcodes (PQ-only)"

This reverts commit 555a9b68ff.

* precompile: profile gate + P3Q + classical-primitive integration

Strict-PQ gate (contract.RefuseUnderStrictPQ) at the top of every
classical pairing/DLOG precompile's Run() refuses the call when the
chain's ChainConfig implements StrictPQReporter and reports true at
the current timestamp. Non-Lux chains that integrate Lux precompiles
without wiring StrictPQReporter see the classical primitives execute
normally -- forward perfection on Lux, full compatibility elsewhere.

Wired into kzg4844, zk (classical opcodes only -- Nullifier and
Merkle Commitment stay open), babyjubjub, pedersen, pasta.

P3Q strict-PQ STARK verifier at LP-4200 0x012205. Plonky3 fork with
cSHAKE256 Merkle / Goldilocks field / FRI low-degree test. Verifier
callback registered at node startup; default returns
ErrVerifierNotRegistered. 8 tests pass.

Gate semantics:
  RefuseUnderStrictPQ(nil) → nil  (test-permissive)
  ChainConfig without StrictPQReporter → nil  (non-Lux chains)
  StrictPQReporter false → nil  (pre-strict-PQ activation timestamp)
  StrictPQReporter true → ErrClassicalForbiddenInPQ

Composable, orthogonal, complete. One and only one way.

* precompile: gate every classical Run() via contract.RefuseUnderStrictPQ

Decomplect: profile-gating logic stays in one place (contract.RefuseUnderStrictPQ);
each classical precompile calls it once at the top of Run() and otherwise stays in
its lane. Quasar/P3Q/Pulsar/ML-KEM/ML-DSA/SLH-DSA/Corona/X-Wing/FHE precompiles
are post-quantum and skip the gate.

Wired into:
  bls12381 (7 EIP-2537 ops: G1/G2 add/mul/MSM + pairing)
  ed25519 (signature verify)
  cggmp21 (ECDSA threshold)
  frost (Schnorr threshold)
  ring (LSAG classical schemes; lattice scheme stays open)
  vrf (ECVRF EDWARDS25519)
  sr25519 (Schnorrkel/Ristretto255)
  x25519 (DH)
  curve25519 (raw Edwards25519 ops)
  hpke (classical KEMs; Kyber/X-Wing hybrid stays open)
  blake3 (classical hash)
  poseidon (BN254 algebraic hash)

Already gated upstream: kzg4844, zk (classical opcodes), babyjubjub, pedersen, pasta.

secp256r1 uses the non-stateful Run([]byte) interface (EIP-7212 standard).
Gating that precompile requires hooking the geth EVM precompile overrider, not
this package -- left for the LuxPrecompileOverrider follow-up.

Tests pass across all twelve gated dirs plus contract/.
This commit is contained in:
2026-05-11 15:10:04 -07:00
committed by GitHub
parent 471f1a3d5d
commit f69b524211
24 changed files with 903 additions and 4 deletions
+7 -4
View File
@@ -182,10 +182,13 @@ Example hook implementations:
> **Canonical (LP-4200)**: the unified PQCrypto precompile block at
> `0x012201..0x012204` is authoritative — ML-KEM `0x012201`, ML-DSA
> `0x012202`, SLH-DSA `0x012203`, Pulsar `0x012204`. The `0x0600`
> (Post-Quantum) and `0x0800` (Threshold) ranges below are the legacy
> per-VM mapping; new code MUST target the unified block. See
> `~/work/lux/evm/LLM.md` for the canonical per-feature address table.
> `0x012202`, SLH-DSA `0x012203`, Pulsar `0x012204` (threshold ML-DSA,
> FIPS 204 byte-equal). All four are IMPLEMENTED; Pulsar lives in
> `precompile/pulsar/` and dispatches to `luxfi/crypto/mldsa` under
> the hood. The `0x0600` (Post-Quantum) and `0x0800` (Threshold) ranges
> below are the legacy per-VM mapping; new code MUST target the unified
> block. See `~/work/lux/evm/LLM.md` for the canonical per-feature
> address table.
| Range | Category | Description |
|-------|----------|-------------|
+3
View File
@@ -114,6 +114,9 @@ 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
}
+3
View File
@@ -142,6 +142,9 @@ func (p *blake3Precompile) 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
+21
View File
@@ -98,6 +98,9 @@ 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)
}
@@ -111,6 +114,9 @@ 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)
}
@@ -124,6 +130,9 @@ 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)
}
@@ -137,6 +146,9 @@ 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)
}
@@ -150,6 +162,9 @@ 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)
}
@@ -163,6 +178,9 @@ 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)
}
@@ -176,6 +194,9 @@ 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
@@ -88,6 +88,9 @@ 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)
+58
View File
@@ -0,0 +1,58 @@
// 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
@@ -0,0 +1,66 @@
// 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,6 +93,9 @@ 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
@@ -81,6 +81,9 @@ 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
@@ -91,6 +91,9 @@ 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,6 +191,18 @@ 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
@@ -151,6 +151,9 @@ 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
+189
View File
@@ -0,0 +1,189 @@
// Copyright (C) 2025-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package p3q implements the EVM precompile entry for the P3Q strict
// post-quantum STARK verifier, the Lux family canonical proof system.
//
// P3Q is a fork of Plonky3 with the classical-curve surface stripped
// out: cSHAKE256 (FIPS 202 / SP 800-185) Merkle hashes over the
// Goldilocks 64-bit prime field, no KZG, no pairings. The wire-byte
// proof-backend ID is `ProofBackendP3QSTARKFRISHA3 = 0x22`, pinned in
// `consensus/config/pq_mode.go`; this precompile is the on-chain
// dispatch point for that backend.
//
// LP-4200 unified PQCrypto block:
//
// 0x012201 = ML-KEM
// 0x012202 = ML-DSA (single-party FIPS 204)
// 0x012203 = SLH-DSA (FIPS 205)
// 0x012204 = Pulsar (threshold FIPS 204, byte-equal)
// 0x012205 = P3Q ← this precompile (strict-PQ STARK)
//
// Wire format (single dispatch, version byte differentiates future
// proof formats; first deployment is v0x01):
//
// [1 byte version]
// [4 bytes proof_len (big-endian)]
// [proof_len bytes proof] // must begin with MagicHeader "P3Q1"
// [4 bytes pub_len (big-endian)]
// [pub_len bytes public_inputs]
//
// The Rust prover / verifier in `~/work/lux/p3q` runs out-of-band; at
// startup the node calls `p3q.RegisterVerifier` to wire the Go-side
// callback that actually invokes the FFI verifier. Without a
// registered verifier the precompile returns ErrVerifierNotRegistered.
// Structural validation (length checks + MagicHeader) runs in the
// precompile itself and is constant-time-friendly: no secret-dependent
// branches, public inputs and the proof are non-secret by construction.
package p3q
import (
"encoding/binary"
"errors"
"sync/atomic"
"github.com/luxfi/geth/common"
"github.com/luxfi/precompile/contract"
)
// ContractP3QVerifyAddress is the canonical LP-4200 slot for P3Q.
var ContractP3QVerifyAddress = common.HexToAddress("0x0000000000000000000000000000000000012205")
// P3QVerifyPrecompile is the singleton instance.
var P3QVerifyPrecompile = &p3qVerifyPrecompile{}
var _ contract.StatefulPrecompiledContract = &p3qVerifyPrecompile{}
// MagicHeader is the 4-byte prefix every P3Q proof MUST begin with.
// Ties the wire to the Plonky3 fork's cSHAKE256/Goldilocks profile.
const MagicHeader = "P3Q1"
// Wire layout constants.
const (
versionByte = 1
proofLenByte = 4
pubLenByte = 4
// MinInputLength is the smallest valid input: 1 version + 4 proof_len
// + 4 magic header + 4 pub_len. Proof must contain at least the
// MagicHeader to clear structural validation.
MinInputLength = versionByte + proofLenByte + len(MagicHeader) + pubLenByte
// VersionV1 is the first wire-format version.
VersionV1 byte = 0x01
)
// Gas schedule. STARK verification scales roughly with log²(circuit
// size), but on-chain we only see the serialized proof; charge a flat
// base plus a per-byte cost so callers pay for bandwidth and decode.
const (
BaseVerifyGas uint64 = 200_000
PerByteGas uint64 = 10
)
// Error sentinels returned by this precompile.
var (
ErrInvalidInputLength = errors.New("p3q: invalid input length")
ErrInvalidVersion = errors.New("p3q: invalid version byte")
ErrVerifierNotRegistered = errors.New("p3q: verifier not registered")
ErrInvalidProof = errors.New("p3q: proof verification failed")
)
// VerifierFn is the Go-side callback that bridges to the out-of-band
// P3Q (Plonky3 fork) Rust verifier. Returns (ok, err). A non-nil err
// indicates an internal verifier failure (FFI, decode); ok=false with
// nil err indicates a well-formed proof that did not verify.
type VerifierFn func(version byte, proof, pubInputs []byte) (bool, error)
// verifier is the registered callback. Atomic.Value so RegisterVerifier
// is safe to call once at node startup without locking the hot path.
var verifier atomic.Value // VerifierFn
// RegisterVerifier wires the actual P3Q verifier. Called once at node
// startup. Passing nil clears the registration (useful for tests).
func RegisterVerifier(fn VerifierFn) {
if fn == nil {
verifier.Store(VerifierFn(nil))
return
}
verifier.Store(fn)
}
// loadVerifier returns the currently-registered verifier, or nil.
func loadVerifier() VerifierFn {
v, _ := verifier.Load().(VerifierFn)
return v
}
type p3qVerifyPrecompile struct{}
// RequiredGas returns BaseVerifyGas + PerByteGas * len(input). STARK
// verifier cost is dominated by FRI query rounds (logarithmic in
// circuit size); per-byte covers serialization / hashing overhead.
func (p *p3qVerifyPrecompile) RequiredGas(input []byte) uint64 {
return BaseVerifyGas + uint64(len(input))*PerByteGas
}
// Run verifies a P3Q STARK proof.
//
// Returns:
// - empty bytes, nil error on successful verification.
// - empty bytes, typed error on any failure.
func (p *p3qVerifyPrecompile) Run(
_ contract.AccessibleState,
_ common.Address,
_ common.Address,
input []byte,
suppliedGas uint64,
_ bool,
) ([]byte, uint64, error) {
gas := p.RequiredGas(input)
if suppliedGas < gas {
return nil, 0, contract.ErrOutOfGas
}
remaining := suppliedGas - gas
if len(input) < MinInputLength {
return nil, remaining, ErrInvalidInputLength
}
version := input[0]
if version != VersionV1 {
return nil, remaining, ErrInvalidVersion
}
off := versionByte
proofLen := binary.BigEndian.Uint32(input[off : off+proofLenByte])
off += proofLenByte
if uint64(len(input))-uint64(off) < uint64(proofLen)+uint64(pubLenByte) {
return nil, remaining, ErrInvalidInputLength
}
proof := input[off : off+int(proofLen)]
off += int(proofLen)
pubLen := binary.BigEndian.Uint32(input[off : off+pubLenByte])
off += pubLenByte
if uint64(len(input))-uint64(off) < uint64(pubLen) {
return nil, remaining, ErrInvalidInputLength
}
pub := input[off : off+int(pubLen)]
// Structural check: proof must begin with the MagicHeader. P3Q
// proofs are non-secret, so byte-equality is fine.
if len(proof) < len(MagicHeader) || string(proof[:len(MagicHeader)]) != MagicHeader {
return nil, remaining, ErrInvalidProof
}
fn := loadVerifier()
if fn == nil {
return nil, remaining, ErrVerifierNotRegistered
}
ok, err := fn(version, proof, pub)
if err != nil {
return nil, remaining, err
}
if !ok {
return nil, remaining, ErrInvalidProof
}
return nil, remaining, nil
}
+152
View File
@@ -0,0 +1,152 @@
// Copyright (C) 2025-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package p3q
import (
"encoding/binary"
"testing"
"github.com/luxfi/geth/common"
"github.com/stretchr/testify/require"
)
// TestP3Q_Address pins the canonical LP-4200 slot 0x012205.
func TestP3Q_Address(t *testing.T) {
want := common.HexToAddress("0x0000000000000000000000000000000000012205")
require.Equal(t, want, ContractP3QVerifyAddress)
}
// TestP3Q_RequiredGas verifies the gas formula: 200_000 + 10 * len(input).
func TestP3Q_RequiredGas(t *testing.T) {
for _, n := range []int{0, 1, 64, 256, 4096, 131072} {
input := make([]byte, n)
got := P3QVerifyPrecompile.RequiredGas(input)
want := BaseVerifyGas + uint64(n)*PerByteGas
require.Equal(t, want, got, "len=%d", n)
}
}
// TestP3Q_RejectsBadMagic ensures proofs without the "P3Q1" header are
// rejected with ErrInvalidProof (even when a verifier is registered).
func TestP3Q_RejectsBadMagic(t *testing.T) {
// Register a verifier that would say yes if asked; the structural
// check must reject before the verifier is invoked.
called := false
RegisterVerifier(func(byte, []byte, []byte) (bool, error) {
called = true
return true, nil
})
defer RegisterVerifier(nil)
proof := []byte("BOGUS-not-a-p3q-proof-but-long-enough")
pub := []byte{0xde, 0xad, 0xbe, 0xef}
input := buildInput(VersionV1, proof, pub)
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
_, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, gas, true)
require.ErrorIs(t, err, ErrInvalidProof)
require.False(t, called, "verifier callback must not run on bad magic")
}
// TestP3Q_RejectsShortInput ensures inputs below MinInputLength fail with
// ErrInvalidInputLength.
func TestP3Q_RejectsShortInput(t *testing.T) {
for _, sz := range []int{0, 1, 5, MinInputLength - 1} {
input := make([]byte, sz)
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
_, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, gas, true)
require.ErrorIs(t, err, ErrInvalidInputLength, "len=%d", sz)
}
}
// TestP3Q_RoundTrip_WithRegisteredVerifier registers a fake verifier
// that accepts and asserts Run() succeeds end-to-end.
func TestP3Q_RoundTrip_WithRegisteredVerifier(t *testing.T) {
saw := struct {
version byte
proof []byte
pub []byte
}{}
RegisterVerifier(func(v byte, p, pi []byte) (bool, error) {
saw.version = v
saw.proof = append([]byte(nil), p...)
saw.pub = append([]byte(nil), pi...)
return true, nil
})
defer RegisterVerifier(nil)
proof := append([]byte(MagicHeader), []byte("STARK-FRI-cSHAKE256-Goldilocks-payload")...)
pub := []byte{0x01, 0x02, 0x03, 0x04, 0x05}
input := buildInput(VersionV1, proof, pub)
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
out, gasLeft, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, gas, true)
require.NoError(t, err)
require.Empty(t, out)
require.Less(t, gasLeft, gas)
require.Equal(t, VersionV1, saw.version)
require.Equal(t, proof, saw.proof)
require.Equal(t, pub, saw.pub)
}
// TestP3Q_NoVerifierRegistered confirms that without a registered
// verifier callback, structurally-valid input yields
// ErrVerifierNotRegistered.
func TestP3Q_NoVerifierRegistered(t *testing.T) {
RegisterVerifier(nil)
proof := append([]byte(MagicHeader), []byte("payload")...)
pub := []byte{0xaa}
input := buildInput(VersionV1, proof, pub)
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
_, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, gas, true)
require.ErrorIs(t, err, ErrVerifierNotRegistered)
}
// TestP3Q_RejectsBadVersion confirms versions other than 0x01 are
// rejected with ErrInvalidVersion.
func TestP3Q_RejectsBadVersion(t *testing.T) {
RegisterVerifier(func(byte, []byte, []byte) (bool, error) { return true, nil })
defer RegisterVerifier(nil)
proof := append([]byte(MagicHeader), []byte("payload")...)
pub := []byte{0xaa}
input := buildInput(0x00, proof, pub)
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
_, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, gas, true)
require.ErrorIs(t, err, ErrInvalidVersion)
}
// TestP3Q_VerifierReturnsFalse confirms that when the registered
// verifier reports the proof as invalid we surface ErrInvalidProof.
func TestP3Q_VerifierReturnsFalse(t *testing.T) {
RegisterVerifier(func(byte, []byte, []byte) (bool, error) { return false, nil })
defer RegisterVerifier(nil)
proof := append([]byte(MagicHeader), []byte("payload")...)
pub := []byte{0xaa}
input := buildInput(VersionV1, proof, pub)
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
_, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, gas, true)
require.ErrorIs(t, err, ErrInvalidProof)
}
// buildInput serialises [version][proof_len][proof][pub_len][pub] in
// the wire format the precompile expects.
func buildInput(version byte, proof, pub []byte) []byte {
out := make([]byte, 0, 1+4+len(proof)+4+len(pub))
out = append(out, version)
var pl [4]byte
binary.BigEndian.PutUint32(pl[:], uint32(len(proof)))
out = append(out, pl[:]...)
out = append(out, proof...)
var ul [4]byte
binary.BigEndian.PutUint32(ul[:], uint32(len(pub)))
out = append(out, ul[:]...)
out = append(out, pub...)
return out
}
+3
View File
@@ -91,6 +91,9 @@ 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,6 +119,9 @@ 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,6 +88,9 @@ 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
}
+211
View File
@@ -0,0 +1,211 @@
// Copyright (C) 2025-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package pulsar implements the EVM precompile entry for Pulsar
// threshold ML-DSA signature verification, the Module-LWE
// FIPS 204-compatible threshold scheme (github.com/luxfi/pulsar,
// formerly github.com/luxfi/pulsar-m).
//
// The cryptographic verification operation is **literally identical**
// to FIPS 204 ML-DSA.Verify -- that's the Class N1 manifesto. Any
// signature produced by a Pulsar threshold ceremony (DKG + Round1 +
// Round2 + Combine) is byte-equal to a single-party FIPS 204
// signature on the same message and group public key. Therefore this
// precompile dispatches to the underlying ML-DSA verifier in
// github.com/luxfi/crypto/mldsa under the hood.
//
// Why have a separate precompile address at all (vs. directing all
// callers to the existing ML-DSA precompile at 0x012202)?
//
// 1. **Gas-tier separation** -- contracts that consume threshold
// signatures may want different accounting / metering policies
// than single-party signature verifications.
// 2. **Telemetry** -- on-chain accounting can attribute Pulsar
// verifies separately from generic ML-DSA verifies for analytics
// and validator-set lifecycle dashboards.
// 3. **Forward-compatibility** -- if a future Pulsar version adds a
// non-FIPS-204 verification path (e.g. an identifiable-abort
// hint, an aggregated-cert envelope), the address slot is
// reserved.
//
// At present (Pulsar v0.1.x), the precompile is a thin pass-through
// to the ML-DSA verifier with the same wire format. Gas cost matches
// the ML-DSA-65 base tier (Pulsar's production target).
package pulsar
import (
"errors"
"github.com/luxfi/crypto/mldsa"
"github.com/luxfi/geth/common"
"github.com/luxfi/precompile/contract"
)
// Pulsar threshold-ML-DSA precompile address. The canonical LP-4200
// unified PQCrypto block reserves this slot for Pulsar.
//
// 0x012201 = ML-KEM
// 0x012202 = ML-DSA (single-party FIPS 204)
// 0x012203 = SLH-DSA
// 0x012204 = Pulsar ← this precompile (threshold FIPS 204)
var ContractPulsarVerifyAddress = common.HexToAddress("0x0000000000000000000000000000000000012204")
// PulsarVerifyPrecompile is the singleton instance.
var PulsarVerifyPrecompile = &pulsarVerifyPrecompile{}
var (
_ contract.StatefulPrecompiledContract = &pulsarVerifyPrecompile{}
)
// Errors returned by the precompile.
var (
ErrInvalidInputLength = contract.ErrInvalidInput
ErrInvalidMode = errors.New("invalid Pulsar mode (must be 0x44, 0x65, or 0x87)")
ErrUnsupportedMode = errors.New("unsupported Pulsar mode")
ErrInvalidSignature = errors.New("Pulsar signature verification failed")
)
// Pulsar parameter sets mirror FIPS 204 ML-DSA's three security
// levels. Wire-format and size constants are identical to ML-DSA.
const (
ModePulsar44 uint8 = 0x44 // NIST PQ Category 2
ModePulsar65 uint8 = 0x65 // NIST PQ Category 3 (Lux production target)
ModePulsar87 uint8 = 0x87 // NIST PQ Category 5
)
const (
// Pulsar-44 sizes (same as ML-DSA-44 per FIPS 204).
Pulsar44PublicKeySize = 1312
Pulsar44SignatureSize = 2420
// Pulsar-65 sizes.
Pulsar65PublicKeySize = 1952
Pulsar65SignatureSize = 3309
// Pulsar-87 sizes.
Pulsar87PublicKeySize = 2592
Pulsar87SignatureSize = 4627
ModeByte = 1
)
// Gas costs per Pulsar mode. Matches the ML-DSA precompile's
// per-mode costs (same work; same cost).
const (
Pulsar44VerifyBaseGas uint64 = 75_000
Pulsar65VerifyBaseGas uint64 = 100_000
Pulsar87VerifyBaseGas uint64 = 150_000
PulsarVerifyPerByteGas uint64 = 10
)
// precompileCtx is the domain-separation context for the Pulsar
// threshold precompile. Different from the single-party ML-DSA
// precompile context to keep on-chain transcripts unambiguous about
// which scheme produced the signature.
var precompileCtx = []byte("lux-evm-precompile-pulsar-v1")
type pulsarVerifyPrecompile struct{}
// RequiredGas returns the gas cost for verifying a Pulsar signature.
// Layout matches the ML-DSA precompile: first byte is the mode, then
// pubkey, then message, then signature.
func (p *pulsarVerifyPrecompile) RequiredGas(input []byte) uint64 {
if len(input) < ModeByte {
return Pulsar65VerifyBaseGas // default to mid mode for malformed input
}
mode := input[0]
switch mode {
case ModePulsar44:
return Pulsar44VerifyBaseGas + uint64(len(input))*PulsarVerifyPerByteGas
case ModePulsar65:
return Pulsar65VerifyBaseGas + uint64(len(input))*PulsarVerifyPerByteGas
case ModePulsar87:
return Pulsar87VerifyBaseGas + uint64(len(input))*PulsarVerifyPerByteGas
default:
return Pulsar65VerifyBaseGas
}
}
// Run verifies a Pulsar threshold signature.
//
// Input layout:
//
// [mode][pubkey][message_len_uint256][message][signature]
//
// The verification operation is `mldsa.VerifyCtx(mode, pubkey, msg,
// precompileCtx, sig)`. By construction this also accepts any
// single-party ML-DSA signature -- Pulsar's wire format is FIPS 204
// byte-equal.
//
// Returns:
// - empty bytes and nil error on successful verification.
// - empty bytes and a typed error on any failure.
func (p *pulsarVerifyPrecompile) Run(
_ contract.AccessibleState,
_ common.Address,
_ common.Address,
input []byte,
suppliedGas uint64,
_ bool,
) ([]byte, uint64, error) {
if suppliedGas < p.RequiredGas(input) {
return nil, 0, contract.ErrOutOfGas
}
if len(input) < ModeByte {
return nil, suppliedGas, ErrInvalidInputLength
}
mode := input[0]
body := input[1:]
var pubSize, sigSize int
var mldsaMode mldsa.Mode
switch mode {
case ModePulsar44:
pubSize = Pulsar44PublicKeySize
sigSize = Pulsar44SignatureSize
mldsaMode = mldsa.MLDSA44
case ModePulsar65:
pubSize = Pulsar65PublicKeySize
sigSize = Pulsar65SignatureSize
mldsaMode = mldsa.MLDSA65
case ModePulsar87:
pubSize = Pulsar87PublicKeySize
sigSize = Pulsar87SignatureSize
mldsaMode = mldsa.MLDSA87
default:
return nil, suppliedGas, ErrInvalidMode
}
if len(body) < pubSize+32 {
return nil, suppliedGas, ErrInvalidInputLength
}
pubkey := body[:pubSize]
body = body[pubSize:]
if len(body) < 32 {
return nil, suppliedGas, ErrInvalidInputLength
}
// Message length is encoded as a big-endian uint256; we accept
// the low 8 bytes as the actual length.
mlen := uint64(0)
for i := 24; i < 32; i++ {
mlen = (mlen << 8) | uint64(body[i])
}
body = body[32:]
if uint64(len(body)) < mlen+uint64(sigSize) {
return nil, suppliedGas, ErrInvalidInputLength
}
msg := body[:mlen]
body = body[mlen:]
sig := body[:sigSize]
pk, err := mldsa.PublicKeyFromBytes(pubkey, mldsaMode)
if err != nil {
return nil, suppliedGas - p.RequiredGas(input), ErrInvalidSignature
}
if !pk.VerifySignatureCtx(msg, sig, precompileCtx) {
return nil, suppliedGas - p.RequiredGas(input), ErrInvalidSignature
}
return nil, suppliedGas - p.RequiredGas(input), nil
}
+127
View File
@@ -0,0 +1,127 @@
// Copyright (C) 2025-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package pulsar
import (
"crypto/rand"
"encoding/binary"
"testing"
"github.com/luxfi/crypto/mldsa"
"github.com/luxfi/geth/common"
"github.com/stretchr/testify/require"
)
// TestPulsarPrecompile_Address pins the canonical LP-4200 address.
func TestPulsarPrecompile_Address(t *testing.T) {
want := common.HexToAddress("0x0000000000000000000000000000000000012204")
require.Equal(t, want, ContractPulsarVerifyAddress)
}
// TestPulsarPrecompile_RequiredGas exercises the per-mode gas table.
func TestPulsarPrecompile_RequiredGas(t *testing.T) {
for _, tc := range []struct {
mode uint8
want uint64
}{
{ModePulsar44, Pulsar44VerifyBaseGas},
{ModePulsar65, Pulsar65VerifyBaseGas},
{ModePulsar87, Pulsar87VerifyBaseGas},
} {
input := []byte{tc.mode}
got := PulsarVerifyPrecompile.RequiredGas(input)
// One-byte input → base + 1 * PerByteGas.
want := tc.want + 1*PulsarVerifyPerByteGas
require.Equal(t, want, got, "mode=0x%x", tc.mode)
}
}
// TestPulsarPrecompile_RoundTrip_VerifyAcceptsValid generates a real
// ML-DSA signature with the precompile context, hands the bytes to
// the Pulsar precompile, and asserts it accepts. This is the Class
// N1 manifesto test: a single-party FIPS 204 signature on the
// Pulsar context verifies through the Pulsar precompile because the
// verification operation IS FIPS 204 ML-DSA.Verify.
func TestPulsarPrecompile_RoundTrip_VerifyAcceptsValid(t *testing.T) {
for _, mode := range []mldsa.Mode{mldsa.MLDSA44, mldsa.MLDSA65, mldsa.MLDSA87} {
mode := mode
t.Run(modeString(mode), func(t *testing.T) {
priv, err := mldsa.GenerateKey(rand.Reader, mode)
require.NoError(t, err)
pub := priv.Public().(*mldsa.PublicKey)
pubBytes := pub.Bytes()
message := []byte("pulsar threshold sign roundtrip via precompile")
sig, err := priv.SignCtx(rand.Reader, message, precompileCtx)
require.NoError(t, err)
// Build precompile input.
var modeByte uint8
switch mode {
case mldsa.MLDSA44:
modeByte = ModePulsar44
case mldsa.MLDSA65:
modeByte = ModePulsar65
case mldsa.MLDSA87:
modeByte = ModePulsar87
}
input := buildInput(modeByte, pubBytes, message, sig)
gas := PulsarVerifyPrecompile.RequiredGas(input) * 2 // give plenty
out, gasLeft, err := PulsarVerifyPrecompile.Run(nil, common.Address{}, ContractPulsarVerifyAddress, input, gas, true)
require.NoError(t, err)
require.Empty(t, out)
require.Less(t, gasLeft, gas)
})
}
}
// TestPulsarPrecompile_RejectsTamperedSignature confirms that flipping
// a byte in the signature causes verification to fail.
func TestPulsarPrecompile_RejectsTamperedSignature(t *testing.T) {
priv, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
require.NoError(t, err)
pub := priv.Public().(*mldsa.PublicKey)
pubBytes := pub.Bytes()
message := []byte("tamper detection")
sig, err := priv.SignCtx(rand.Reader, message, precompileCtx)
require.NoError(t, err)
sig[0] ^= 0xff
input := buildInput(ModePulsar65, pubBytes, message, sig)
gas := PulsarVerifyPrecompile.RequiredGas(input) * 2
_, _, err = PulsarVerifyPrecompile.Run(nil, common.Address{}, ContractPulsarVerifyAddress, input, gas, true)
require.ErrorIs(t, err, ErrInvalidSignature)
}
// TestPulsarPrecompile_RejectsInvalidMode confirms that mode bytes
// outside {0x44, 0x65, 0x87} are rejected.
func TestPulsarPrecompile_RejectsInvalidMode(t *testing.T) {
input := []byte{0x99} // bogus mode
_, _, err := PulsarVerifyPrecompile.Run(nil, common.Address{}, ContractPulsarVerifyAddress, input, 1_000_000, true)
require.ErrorIs(t, err, ErrInvalidMode)
}
func buildInput(mode uint8, pub, msg, sig []byte) []byte {
out := make([]byte, 0, 1+len(pub)+32+len(msg)+len(sig))
out = append(out, mode)
out = append(out, pub...)
var mlen [32]byte
binary.BigEndian.PutUint64(mlen[24:], uint64(len(msg)))
out = append(out, mlen[:]...)
out = append(out, msg...)
out = append(out, sig...)
return out
}
func modeString(m mldsa.Mode) string {
switch m {
case mldsa.MLDSA44:
return "Pulsar-44"
case mldsa.MLDSA65:
return "Pulsar-65"
case mldsa.MLDSA87:
return "Pulsar-87"
default:
return "unknown"
}
}
+10
View File
@@ -137,6 +137,16 @@ 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 {
+3
View File
@@ -96,6 +96,9 @@ 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,6 +109,9 @@ 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,6 +82,9 @@ 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
}
+11
View File
@@ -154,6 +154,17 @@ func (p *zkVerifyPrecompile) Run(
op := input[0]
data := input[1:]
// Classical pairing/DLOG opcodes are gated by the chain's strict-PQ
// profile. The hash-based opcodes (Nullifier 0x21, Commitment 0x22)
// remain open on every chain.
switch op {
case OpVerifyGroth16, OpVerifyPLONK, OpVerifyFflonk, OpVerifyHalo2,
OpVerifyKZG, OpVerifyIPA, OpVerifyRangeProof, OpVerifyBatch:
if err := contract.RefuseUnderStrictPQ(accessibleState); err != nil {
return nil, remainingGas, err
}
}
switch op {
case OpVerifyGroth16:
valid, err := p.verifyGroth16(data)