Files
geth/core/vm/pq_gate_test.go
zeekay 4d73303479 core/vm: per-chain PQ profile gate at runPrecompile (CR-1 closure)
Closes CR-1 from the cross-stack adversarial audit: the precompile gate
no longer reads a process-global pq.Active() — it reads the per-chain
profile from ChainConfig.PQ. Multi-chain hosts now get the right
posture per EVM instance instead of last-writer-wins.

Decomplecting per Rich Hickey:
  - One gate in one place: (*EVM).runPrecompile is the single chokepoint
    for every non-stateful precompile call (Call/CallCode/DelegateCall/
    StaticCall all funnel through it).
  - Per-precompile Run() bodies are PQ-agnostic again: removed 19 ad-hoc
    `refuse(OpX)` calls scattered across the body of ecrecover, sha256,
    ripemd160, blake2F, bn256*, bls12381*, kzg, p256Verify.
  - Profile lives on the value (*pq.Profile field on params.ChainConfig)
    not the place (global pq.Active()). Multi-chain hosts host strict-PQ
    and permissive chains side-by-side without race.
  - Gas is still charged on refusal: PQ refusal must not be detectable
    via gas timing — test pinned (TestStrictPQ_GasIsStillCharged).

Surface:
  - params.ChainConfig.PQ *pq.Profile (json:"-", not persisted)
  - (*EVM).runPrecompile(p, input, gas) — the chokepoint
  - opForPrecompile(p PrecompiledContract) Op — type-switch dispatch,
    OpUnknown fail-open for stateful / overrider precompiles
  - AllForbidden() returns pq.Strict() (alias for ergonomic call sites)
  - SetPQProfile / ActivePQProfile preserved as deprecated shims

Module bumps:
  - github.com/luxfi/pq v1.0.1-0.20260512064747-3d4c9414e15e
    (promotes RefuseUnder from function to method on *Profile)

Tests: core/vm/pq_gate_test.go pins the per-chain semantics —
strict-PQ on chain A refuses ecrecover; permissive chain B in the
same process admits it; gas is still charged in both cases.
2026-05-11 23:50:32 -07:00

258 lines
9.1 KiB
Go

// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package vm
import (
"errors"
"math/big"
"testing"
"github.com/luxfi/geth/params"
)
// pq_gate_test.go — coverage for the cross-family PQ gate.
//
// Each gate case names a precompile, the Op it reports, and the
// family-specific error (*EVM).runPrecompile must return when
// AllForbidden() is the chain-config profile. Each test constructs a
// test EVM whose ChainConfig.PQ holds the per-case profile.
// newGateEVM constructs a minimal EVM whose chain config has the
// supplied profile installed. State is nil; runPrecompile does not
// touch state.
func newGateEVM(t *testing.T, p *PQProfile) *EVM {
t.Helper()
cc := *params.TestChainConfig
cc.PQ = p
return NewEVM(BlockContext{BlockNumber: big.NewInt(0)}, nil, &cc, Config{})
}
// gateCase is one row of the gate matrix.
type gateCase struct {
name string
p PrecompiledContract
input []byte
op Op
wantErr error
}
// gateCases enumerates every gated precompile and the error
// runPrecompile must return for that op under AllForbidden().
func gateCases() []gateCase {
// Inputs are size-conforming but otherwise zero. The gate fires
// before the precompile interprets them; cryptographic output is
// irrelevant.
zero64 := make([]byte, 64)
zero96 := make([]byte, 96)
zero128 := make([]byte, 128)
zero160 := make([]byte, 160)
zero192 := make([]byte, 192)
zero213 := make([]byte, 213)
zero256 := make([]byte, 256)
zero288 := make([]byte, 288)
zero384 := make([]byte, 384)
return []gateCase{
{"ecrecover", &ecrecover{}, zero128, OpEcrecover, ErrEcrecoverForbidden},
{"sha256", &sha256hash{}, zero64, OpSHA256, ErrSHA256Forbidden},
{"ripemd160", &ripemd160hash{}, zero64, OpRIPEMD160, ErrRIPEMD160Forbidden},
{"blake2F", &blake2F{}, zero213, OpBlake2F, ErrBlake2FForbidden},
{"bn256AddIstanbul", &bn256AddIstanbul{}, zero128, OpBn256Add, ErrBn256Forbidden},
{"bn256AddByzantium", &bn256AddByzantium{}, zero128, OpBn256Add, ErrBn256Forbidden},
{"bn256ScalarMulIstanbul", &bn256ScalarMulIstanbul{}, zero96, OpBn256ScalarMul, ErrBn256Forbidden},
{"bn256ScalarMulByzantium", &bn256ScalarMulByzantium{}, zero96, OpBn256ScalarMul, ErrBn256Forbidden},
{"bn256PairingIstanbul", &bn256PairingIstanbul{}, zero192, OpBn256Pairing, ErrBn256Forbidden},
{"bn256PairingByzantium", &bn256PairingByzantium{}, zero192, OpBn256Pairing, ErrBn256Forbidden},
{"bls12381G1Add", &bls12381G1Add{}, zero256, OpBLS12381G1Add, ErrBLS12381Forbidden},
{"bls12381G1MultiExp", &bls12381G1MultiExp{}, zero160, OpBLS12381G1MSM, ErrBLS12381Forbidden},
{"bls12381G2Add", &bls12381G2Add{}, zero384, OpBLS12381G2Add, ErrBLS12381Forbidden},
{"bls12381G2MultiExp", &bls12381G2MultiExp{}, zero288, OpBLS12381G2MSM, ErrBLS12381Forbidden},
{"bls12381Pairing", &bls12381Pairing{}, zero384, OpBLS12381Pairing, ErrBLS12381Forbidden},
{"bls12381MapG1", &bls12381MapG1{}, zero64, OpBLS12381MapG1, ErrBLS12381Forbidden},
{"bls12381MapG2", &bls12381MapG2{}, zero128, OpBLS12381MapG2, ErrBLS12381Forbidden},
{"p256Verify", &p256Verify{}, zero160, OpP256Verify, ErrP256VerifyForbidden},
{"kzgPointEvaluation", &kzgPointEvaluation{}, zero192, OpKZGPointEval, ErrKZGForbidden},
}
}
// runGate executes the precompile through runPrecompile with the
// supplied profile, supplying enough gas that the gate is the only
// possible failure mode.
func runGate(t *testing.T, profile *PQProfile, c gateCase) error {
t.Helper()
evm := newGateEVM(t, profile)
gas := c.p.RequiredGas(c.input)
_, _, err := evm.runPrecompile(c.p, c.input, gas*2)
return err
}
// TestStrictPQRefusesEveryClassicalPrecompile asserts AllForbidden()
// refuses every gated precompile with the family-specific error.
func TestStrictPQRefusesEveryClassicalPrecompile(t *testing.T) {
for _, tc := range gateCases() {
t.Run(tc.name, func(t *testing.T) {
err := runGate(t, AllForbidden(), tc)
if err == nil {
t.Fatalf("strict-PQ profile must refuse, got nil")
}
if !errors.Is(err, tc.wantErr) {
t.Fatalf("want %v, got %v", tc.wantErr, err)
}
})
}
}
// TestNilProfileAdmitsEveryPrecompile asserts the default state (no
// profile installed) lets every precompile reach its math.
func TestNilProfileAdmitsEveryPrecompile(t *testing.T) {
gateErrors := []error{
ErrEcrecoverForbidden, ErrP256VerifyForbidden,
ErrSHA256Forbidden, ErrRIPEMD160Forbidden, ErrBlake2FForbidden,
ErrBn256Forbidden, ErrBLS12381Forbidden, ErrKZGForbidden,
}
for _, tc := range gateCases() {
t.Run(tc.name, func(t *testing.T) {
err := runGate(t, nil, tc)
for _, ge := range gateErrors {
if errors.Is(err, ge) {
t.Fatalf("nil profile must not refuse; got %v", err)
}
}
})
}
}
// TestEmptyProfileAdmitsEveryPrecompile asserts a profile with every
// Forbid* false (the zero value) admits every precompile, just like
// the nil case.
func TestEmptyProfileAdmitsEveryPrecompile(t *testing.T) {
gateErrors := []error{
ErrEcrecoverForbidden, ErrP256VerifyForbidden,
ErrSHA256Forbidden, ErrRIPEMD160Forbidden, ErrBlake2FForbidden,
ErrBn256Forbidden, ErrBLS12381Forbidden, ErrKZGForbidden,
}
for _, tc := range gateCases() {
t.Run(tc.name, func(t *testing.T) {
err := runGate(t, &PQProfile{}, tc)
for _, ge := range gateErrors {
if errors.Is(err, ge) {
t.Fatalf("empty profile must not refuse; got %v", err)
}
}
})
}
}
// TestPerFamilyIsolation asserts each Forbid* flag refuses only its
// own family. Setting ForbidBn256 alone must not refuse BLS12-381,
// SHA-256, or any other family.
func TestPerFamilyIsolation(t *testing.T) {
cases := []struct {
name string
profile *PQProfile
op Op
wantErr error
}{
{"only-ecrecover", &PQProfile{ForbidEcrecover: true}, OpEcrecover, ErrEcrecoverForbidden},
{"only-p256", &PQProfile{ForbidP256Verify: true}, OpP256Verify, ErrP256VerifyForbidden},
{"only-sha256", &PQProfile{ForbidSHA256: true}, OpSHA256, ErrSHA256Forbidden},
{"only-ripemd", &PQProfile{ForbidRIPEMD160: true}, OpRIPEMD160, ErrRIPEMD160Forbidden},
{"only-blake2", &PQProfile{ForbidBlake2F: true}, OpBlake2F, ErrBlake2FForbidden},
{"only-bn256", &PQProfile{ForbidBn256: true}, OpBn256Pairing, ErrBn256Forbidden},
{"only-bls", &PQProfile{ForbidBLS12381: true}, OpBLS12381Pairing, ErrBLS12381Forbidden},
{"only-kzg", &PQProfile{ForbidKZG: true}, OpKZGPointEval, ErrKZGForbidden},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if err := c.profile.RefuseUnder(c.op); !errors.Is(err, c.wantErr) {
t.Fatalf("want %v, got %v", c.wantErr, err)
}
// Every other op must pass under this single-flag profile.
otherOps := []Op{
OpEcrecover, OpP256Verify, OpSHA256, OpRIPEMD160, OpBlake2F,
OpBn256Add, OpBn256ScalarMul, OpBn256Pairing,
OpBLS12381G1Add, OpBLS12381G1MSM,
OpBLS12381G2Add, OpBLS12381G2MSM,
OpBLS12381Pairing, OpBLS12381MapG1, OpBLS12381MapG2,
OpKZGPointEval,
}
for _, op := range otherOps {
if op == c.op {
continue
}
if sameFamily(op, c.op) {
continue
}
if err := c.profile.RefuseUnder(op); err != nil {
t.Errorf("single-flag profile %s also refused %v: %v", c.name, op, err)
}
}
})
}
}
// sameFamily reports whether two ops share a Forbid* flag.
func sameFamily(a, b Op) bool {
bn := func(op Op) bool {
return op == OpBn256Add || op == OpBn256ScalarMul || op == OpBn256Pairing
}
bls := func(op Op) bool {
switch op {
case OpBLS12381G1Add, OpBLS12381G1MSM,
OpBLS12381G2Add, OpBLS12381G2MSM,
OpBLS12381Pairing, OpBLS12381MapG1, OpBLS12381MapG2:
return true
}
return false
}
switch {
case bn(a) && bn(b):
return true
case bls(a) && bls(b):
return true
}
return false
}
// TestAllForbiddenSetsAllFlags asserts AllForbidden() sets every
// Forbid* flag. The canonical strict-PQ projection.
func TestAllForbiddenSetsAllFlags(t *testing.T) {
p := AllForbidden()
cases := map[string]bool{
"Ecrecover": p.ForbidEcrecover,
"P256Verify": p.ForbidP256Verify,
"SHA256": p.ForbidSHA256,
"RIPEMD160": p.ForbidRIPEMD160,
"Blake2F": p.ForbidBlake2F,
"Bn256": p.ForbidBn256,
"BLS12381": p.ForbidBLS12381,
"KZG": p.ForbidKZG,
}
for name, v := range cases {
if !v {
t.Errorf("AllForbidden().Forbid%s must be true", name)
}
}
}
// TestRefuseUnknownOpIsPermissive documents that
// (*PQProfile).RefuseUnder(OpUnknown) returns nil even under strict-PQ.
// Callers MUST use a recognized Op; the gate cannot fail-closed on
// unknown ops without breaking callers who have not been ported yet.
// opForPrecompile returns OpUnknown for unrecognized precompile types
// (e.g. dataCopy, bigModExp, stateful custom precompiles).
func TestRefuseUnknownOpIsPermissive(t *testing.T) {
if err := AllForbidden().RefuseUnder(OpUnknown); err != nil {
t.Errorf("AllForbidden.RefuseUnder(OpUnknown) must be nil; got %v", err)
}
// And via runPrecompile with a non-classical precompile (dataCopy
// is not in the strict-PQ matrix).
evm := newGateEVM(t, AllForbidden())
_, _, err := evm.runPrecompile(&dataCopy{}, []byte("anything"), (&dataCopy{}).RequiredGas([]byte("anything")))
if err != nil {
t.Errorf("runPrecompile(dataCopy, AllForbidden) must be nil; got %v", err)
}
}