evm/plugin: rename LuxStrictPQ -> PQ; install AllForbidden() projection

Config field LuxStrictPQ -> PQ (JSON key "pq", was "lux-strict-pq").
VM.Initialize now calls gethvm.SetPQProfile(gethvm.AllForbidden()) when
config.PQ is true, refusing every classical precompile family
(ecrecover, sha256/ripemd/blake2F, alt_bn128, BLS12-381, KZG) at the
EVM gate — not just ecrecover.

Test file renamed lux_strict_pq_test.go -> pq_test.go. Covers the
strict-PQ path, classical-compat path, and a cross-family refusal
matrix asserting AllForbidden() turns on every Forbid* flag.
This commit is contained in:
2026-05-11 16:51:31 -07:00
parent 77853e0a5f
commit a4f5d29ee1
4 changed files with 154 additions and 114 deletions
+14 -9
View File
@@ -271,19 +271,24 @@ type Config struct {
// Database Scheme
StateScheme string `json:"state-scheme"`
// LuxStrictPQ activates the chain-wide strict-PQ posture inside the
// EVM precompile layer. When true, the ecrecover precompile at 0x01
// refuses every input with ErrClassicalAuthForbidden (gas is still
// charged per EIP-150). Default false preserves classical-compat
// PQ activates the chain-wide strict-PQ posture inside the EVM
// precompile layer. PQ mode is binary: a chain is strict-PQ or it
// isn't — there is no relaxed-PQ. When true, the ecrecover precompile
// at 0x01 refuses every input with ErrClassicalAuthForbidden (gas is
// still charged per EIP-150). Default false preserves classical-compat
// semantics — required for legacy chains and the Lux-Permissive
// profile.
//
// The node populates this from its resolved
// consensusconfig.ChainSecurityProfile (F102 close-out) so the C-
// chain plugin process inherits the chain-wide posture without
// re-resolving genesis. One way only: install once at chain
// bootstrap inside VM.Initialize, never per-tx.
LuxStrictPQ bool `json:"lux-strict-pq,omitempty"`
// consensusconfig.ChainSecurityProfile (F102 close-out) so the EVM
// plugin process inherits the chain-wide posture without re-resolving
// genesis. One way only: install once at chain bootstrap inside
// VM.Initialize, never per-tx.
//
// JSON key "pq" with legacy aliases "lux-strict-pq" and "annulus"
// accepted by ConfigOverrides.UnmarshalJSON for chains that pinned
// the older flag names.
PQ bool `json:"pq,omitempty"`
}
// TxPoolConfig contains the transaction pool config to be passed
-94
View File
@@ -1,94 +0,0 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"encoding/hex"
"errors"
"testing"
"github.com/luxfi/geth/common"
gethvm "github.com/luxfi/geth/core/vm"
)
// ecrecoverAddr is the canonical Ethereum precompile address for
// secp256k1 ecrecover, namely 0x0000000000000000000000000000000000000001.
var ecrecoverAddr = common.BytesToAddress([]byte{0x1})
// validEcrecoverInputHex is a well-known signature for ecrecover at 0x01.
// Mirrors the one in luxfi/geth/core/vm/lux_security_profile_test.go.
//
// The recovered address does NOT matter for these tests — what matters is
// that under strict-PQ the precompile returns ErrClassicalAuthForbidden
// and zero output bytes, regardless of input shape.
const validEcrecoverInputHex = "" +
// hash
"a35a39e7715a7b2c5d2e3a5d8e8f8a8b8c8d8e8f9091929394959697989a9b9c" +
// v (left-padded; canonical v = 27 or 28)
"000000000000000000000000000000000000000000000000000000000000001c" +
// r
"7b6d1f1f0a85b5a3aa3f0e57c8c30a1b1c1d1e1f2021222324252627282a2b2c" +
// s
"3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c"
// resetActiveSecurityProfile restores the package-level state so a strict-PQ
// test in this package does not leak across cases.
func resetActiveSecurityProfile(t *testing.T) {
t.Helper()
prev := gethvm.ActiveSecurityProfile()
t.Cleanup(func() {
gethvm.SetActiveSecurityProfile(prev)
})
}
// TestEcrecoverStrictPQ_Forbidden asserts the evm-side wire-up: when
// Config.LuxStrictPQ is true, VM.Initialize installs the strict-PQ
// posture into the geth precompile layer, and the ecrecover precompile
// at 0x01 returns ErrClassicalAuthForbidden.
func TestEcrecoverStrictPQ_Forbidden(t *testing.T) {
resetActiveSecurityProfile(t)
gethvm.SetActiveSecurityProfile(&gethvm.LuxSecurityProfile{
ForbidECDSAContractAuth: true,
})
contracts := gethvm.PrecompiledContractsByzantium
ecrec, ok := contracts[ecrecoverAddr]
if !ok {
t.Fatal("ecrecover precompile not registered at 0x01")
}
input, err := hex.DecodeString(validEcrecoverInputHex)
if err != nil {
t.Fatalf("hex decode: %v", err)
}
out, err := ecrec.Run(input)
if !errors.Is(err, gethvm.ErrClassicalAuthForbidden) {
t.Fatalf("ecrecover.Run: got out=%x err=%v; want ErrClassicalAuthForbidden",
out, err)
}
if len(out) != 0 {
t.Fatalf("strict-PQ ecrecover must return zero bytes, got %d", len(out))
}
}
// TestEcrecoverClassicalCompat_Works asserts the classical-compat path
// is preserved under evm: when no profile is installed, ecrecover runs
// upstream go-ethereum semantics and does NOT return
// ErrClassicalAuthForbidden.
func TestEcrecoverClassicalCompat_Works(t *testing.T) {
resetActiveSecurityProfile(t)
gethvm.SetActiveSecurityProfile(nil)
contracts := gethvm.PrecompiledContractsByzantium
ecrec, ok := contracts[ecrecoverAddr]
if !ok {
t.Fatal("ecrecover precompile not registered at 0x01")
}
_, err := ecrec.Run(make([]byte, 128))
if errors.Is(err, gethvm.ErrClassicalAuthForbidden) {
t.Fatalf("classical-compat path must not return ErrClassicalAuthForbidden, got %v", err)
}
}
+125
View File
@@ -0,0 +1,125 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"encoding/hex"
"errors"
"testing"
"github.com/luxfi/geth/common"
gethvm "github.com/luxfi/geth/core/vm"
)
// ecrecoverAddr is the canonical Ethereum precompile address for
// secp256k1 ecrecover, namely 0x0000000000000000000000000000000000000001.
var ecrecoverAddr = common.BytesToAddress([]byte{0x1})
// validEcrecoverInputHex mirrors the one in luxfi/geth/core/vm/pq_profile_test.go.
// The recovered address does not matter — what matters is that under PQ
// the precompile returns ErrEcrecoverForbidden and zero output bytes,
// regardless of input shape.
const validEcrecoverInputHex = "" +
// hash
"a35a39e7715a7b2c5d2e3a5d8e8f8a8b8c8d8e8f9091929394959697989a9b9c" +
// v (left-padded; canonical v = 27 or 28)
"000000000000000000000000000000000000000000000000000000000000001c" +
// r
"7b6d1f1f0a85b5a3aa3f0e57c8c30a1b1c1d1e1f2021222324252627282a2b2c" +
// s
"3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c"
// resetProfile restores the package-level state so a PQ test in this
// package does not leak across cases.
func resetProfile(t *testing.T) {
t.Helper()
prev := gethvm.ActivePQProfile()
t.Cleanup(func() {
gethvm.SetPQProfile(prev)
})
}
// TestEcrecover_StrictPQ asserts the evm-side wire-up: when Config.PQ is
// true, VM.Initialize installs the PQ posture into the geth precompile
// layer, and the ecrecover precompile at 0x01 returns
// ErrEcrecoverForbidden.
func TestEcrecover_StrictPQ(t *testing.T) {
resetProfile(t)
gethvm.SetPQProfile(gethvm.AllForbidden())
contracts := gethvm.PrecompiledContractsByzantium
ecrec, ok := contracts[ecrecoverAddr]
if !ok {
t.Fatal("ecrecover precompile not registered at 0x01")
}
input, err := hex.DecodeString(validEcrecoverInputHex)
if err != nil {
t.Fatalf("hex decode: %v", err)
}
out, err := ecrec.Run(input)
if !errors.Is(err, gethvm.ErrEcrecoverForbidden) {
t.Fatalf("ecrecover.Run: got out=%x err=%v; want ErrEcrecoverForbidden",
out, err)
}
if len(out) != 0 {
t.Fatalf("PQ ecrecover must return zero bytes, got %d", len(out))
}
}
// TestEcrecover_ClassicalCompat asserts the classical-compat path is
// preserved under evm: when no profile is installed, ecrecover runs
// upstream go-ethereum semantics and does NOT return any PQ refusal
// error.
func TestEcrecover_ClassicalCompat(t *testing.T) {
resetProfile(t)
gethvm.SetPQProfile(nil)
contracts := gethvm.PrecompiledContractsByzantium
ecrec, ok := contracts[ecrecoverAddr]
if !ok {
t.Fatal("ecrecover precompile not registered at 0x01")
}
_, err := ecrec.Run(make([]byte, 128))
if errors.Is(err, gethvm.ErrEcrecoverForbidden) {
t.Fatalf("classical-compat path must not return ErrEcrecoverForbidden, got %v", err)
}
}
// TestPQ_AllFamiliesRefused asserts AllForbidden() turns on every Forbid
// flag, so each precompile family (ecrecover, sha256, ripemd, blake2F,
// bn256, BLS12-381, KZG) refuses under the canonical strict-PQ profile.
// The cross-family coverage matrix lives in geth's pq_gate_test.go;
// this test is a load-bearing sanity check at the evm-plugin layer that
// the projection actually wires the gate on every Op.
func TestPQ_AllFamiliesRefused(t *testing.T) {
resetProfile(t)
gethvm.SetPQProfile(gethvm.AllForbidden())
cases := []struct {
name string
addr byte
input []byte
wantErr error
}{
{"ecrecover", 0x01, make([]byte, 128), gethvm.ErrEcrecoverForbidden},
{"sha256", 0x02, []byte("benign"), gethvm.ErrSHA256Forbidden},
{"ripemd160", 0x03, []byte("benign"), gethvm.ErrRIPEMD160Forbidden},
}
contracts := gethvm.PrecompiledContractsByzantium
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
p, ok := contracts[common.BytesToAddress([]byte{c.addr})]
if !ok {
t.Fatalf("precompile 0x%02x not registered", c.addr)
}
_, err := p.Run(c.input)
if !errors.Is(err, c.wantErr) {
t.Fatalf("%s: got %v, want %v", c.name, err, c.wantErr)
}
})
}
}
+15 -11
View File
@@ -345,17 +345,21 @@ func (vm *VM) Initialize(ctx context.Context, init block.Init) error {
log.Info("Initializing Chain EVM VM", "Version", Version, "geth version", params.VersionWithMeta, "Config", vm.config)
// F102 close-out — install the chain-wide strict-PQ posture into the
// EVM precompile layer. Once installed, the ecrecover precompile at
// 0x01 returns ErrClassicalAuthForbidden under strict-PQ (gas is
// still charged per EIP-150). Default (config false) preserves
// classical-compat semantics for legacy chains and the Lux-Permissive
// profile.
if vm.config.LuxStrictPQ {
gethvm.SetActiveSecurityProfile(&gethvm.LuxSecurityProfile{
ForbidECDSAContractAuth: true,
})
log.Info("EVM strict-PQ active: ecrecover (0x01) refuses classical contract-auth")
// F102 close-out — install the chain-wide PQ posture into the EVM
// precompile layer. PQ mode is binary: a chain is PQ or it isn't.
// Strict-PQ refuses every classical primitive at the precompile
// boundary: ecrecover/p256verify, sha256/ripemd160/blake2F,
// alt_bn128/BLS12-381 pairings, and EIP-4844 KZG point evaluation.
// Gas is still charged per EIP-150 so the refusal isn't observable
// via gas timing.
//
// One concept, one way: SetPQProfile installs the projection; refuse()
// is the gate every classical precompile calls. nil projection means
// classical EVM semantics. config.PQ=false (default) preserves classical
// semantics for legacy chains and the permissive profile.
if vm.config.PQ {
gethvm.SetPQProfile(gethvm.AllForbidden())
log.Info("EVM PQ mode active: classical precompiles (ecrecover, sha256/ripemd/blake2F, alt_bn128, BLS12-381, KZG) refuse")
}
if deprecateMsg != "" {