evm: ecrecover strict-PQ refusal via profile gate

VM.Initialize installs the chain-wide strict-PQ posture into the
go-ethereum precompile layer when Config.LuxStrictPQ is true. The
ecrecover precompile at 0x01 then returns ErrClassicalAuthForbidden
(gas is still charged per EIP-150) and never produces a recovered
address.

Default (LuxStrictPQ=false) preserves classical-compat semantics
required by legacy chains and the Lux-Permissive profile.

Mirrors the coreth wire-up; the gate itself lives in luxfi/geth
(commit 738ecc775, tag v1.16.88) and is single-implementation. Bumps
luxfi/geth pre-release v1.16.86-0.20260413… → v1.16.88 to pull in
the gate; transitive deps (luxfi/accel v1.0.7→v1.0.8, luxfi/consensus
v1.22.70→v1.22.84, luxfi/precompile v0.5.12→v0.5.14) update as
required by geth v1.16.88's own go.mod.

Tests:
  TestEcrecoverStrictPQ_Forbidden    — strict-PQ returns the error
  TestEcrecoverClassicalCompat_Works — nil profile keeps upstream path
This commit is contained in:
2026-05-11 09:26:44 -07:00
parent 53cc611dd3
commit 2201580901
5 changed files with 169 additions and 12 deletions
+4 -4
View File
@@ -17,12 +17,12 @@ require (
github.com/holiman/uint256 v1.3.2
github.com/luxfi/cache v1.2.1
github.com/luxfi/codec v1.1.4
github.com/luxfi/consensus v1.22.70
github.com/luxfi/consensus v1.22.84
github.com/luxfi/constants v1.4.7
github.com/luxfi/crypto v1.18.1
github.com/luxfi/database v1.18.1
github.com/luxfi/filesystem v0.0.1
github.com/luxfi/geth v1.16.86-0.20260413014255-3e903c3d2e06
github.com/luxfi/geth v1.16.88
github.com/luxfi/go-bip39 v1.1.2
github.com/luxfi/gpu v0.30.0
github.com/luxfi/ids v1.2.9
@@ -32,7 +32,7 @@ require (
github.com/luxfi/math/safe v0.0.1
github.com/luxfi/metric v1.5.1
github.com/luxfi/p2p v1.19.2
github.com/luxfi/precompile v0.5.12
github.com/luxfi/precompile v0.5.14
github.com/luxfi/runtime v1.0.1
github.com/luxfi/sys v0.0.0-20260110090042-50187ec5ffd8
github.com/luxfi/timer v1.0.2
@@ -82,7 +82,7 @@ require (
github.com/klauspost/crc32 v1.3.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/luxfi/accel v1.0.7 // indirect
github.com/luxfi/accel v1.0.8 // indirect
github.com/luxfi/age v1.4.0 // indirect
github.com/luxfi/api v1.0.4 // indirect
github.com/luxfi/atomic v1.0.0 // indirect
+8 -8
View File
@@ -194,8 +194,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4=
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
github.com/luxfi/accel v1.0.7 h1:ksHieAp50umwqxqgyHk9WiOmXM54kia3IEDb5H7FsM8=
github.com/luxfi/accel v1.0.7/go.mod h1:iZD3oxffiMEIT/KvzD8bgwC/cBn4AYlMW3QJpbRa4RE=
github.com/luxfi/accel v1.0.8 h1:NYvCa31W9/ay3RPyfG1mUjUPRTdp0O0Qrx0zIRl78Xk=
github.com/luxfi/accel v1.0.8/go.mod h1:XvgcJLYsLObbKlrOgd3E2OJx5Oy5c/HnDlIewjr+Y+c=
github.com/luxfi/age v1.4.0 h1:tU5q65RQSQdaVq64Z/DVUhiPQMoMPQT6pr41LsvG0AQ=
github.com/luxfi/age v1.4.0/go.mod h1:iAYAxgvrXxcy746+Ovh/eWWDuF9teJLNcCSSOX9RYW0=
github.com/luxfi/api v1.0.4 h1:5altGkSSk3zIsGzK/NPhRQe/It5QMHrAPgBVg2TMkK4=
@@ -210,8 +210,8 @@ github.com/luxfi/compress v0.0.5 h1:4tEUHw5MK1bu5UOjfYCt4OKMiH7yykIgmGPRA/BfJTM=
github.com/luxfi/compress v0.0.5/go.mod h1:Cc1yxD2pfzrvpO32W2GDwLKff+CylHEvzZh2Ko8RSIU=
github.com/luxfi/concurrent v0.0.3 h1:eJyv1fhaC0jMLMw6+QS774cUmp7GK+ouMgvLCqnC7cc=
github.com/luxfi/concurrent v0.0.3/go.mod h1:Aj/FR5NpM0cB2P4Nt3+tz9+dV6V+LUW4HuMgSjwq5hw=
github.com/luxfi/consensus v1.22.70 h1:IQlDxkNY2j6W55nYh11WenvdIsZ9g619qo+xS69GA00=
github.com/luxfi/consensus v1.22.70/go.mod h1:y6y+bVjvpDLkkTBCCKLcch6R4PRW8EA+lTfr+6sB6F8=
github.com/luxfi/consensus v1.22.84 h1:fKw4n7fY+lCWW5HSx/ciGeRSxv4knODQGWh6lBWAveQ=
github.com/luxfi/consensus v1.22.84/go.mod h1:y6y+bVjvpDLkkTBCCKLcch6R4PRW8EA+lTfr+6sB6F8=
github.com/luxfi/constants v1.4.7 h1:e/Qs+DQP3pugle3Zncq6fZCxKgqqtbyD/z7Gm4ZjsYg=
github.com/luxfi/constants v1.4.7/go.mod h1:hOszZ2NDQ8gMZKncfcZ67PXkb5OIbnwAzXC3oFbQwW0=
github.com/luxfi/container v0.0.4 h1:BXhF82WyfqVP5mjlNcr7tP0Fcnvl0Ap1rkiu+rq5XuM=
@@ -226,8 +226,8 @@ github.com/luxfi/filesystem v0.0.1 h1:VZ6xMFKaAPBW/ddlMsDnI2G0VU1lV5rYaVcW5d+KwE
github.com/luxfi/filesystem v0.0.1/go.mod h1:OQVSU6XNwqrr1AI+MqkID2taHUclx7NYmmr3svgttec=
github.com/luxfi/formatting v1.0.1 h1:ZnE1rAdEUds9yAegdVdGDOBGN6hLMPOv6E03Fp8IEYo=
github.com/luxfi/formatting v1.0.1/go.mod h1:mYzNf5DJOiqSSKUPzNj5dKy4tstFbN3pZlkI5716eKc=
github.com/luxfi/geth v1.16.86-0.20260413014255-3e903c3d2e06 h1:NAIcfesKGSITw/mYTQpq62igXqUiDR7gekx8rQTKBmQ=
github.com/luxfi/geth v1.16.86-0.20260413014255-3e903c3d2e06/go.mod h1:OdpR6OnAsYROywc8wm7W0lPn1z06DjgVmRL/F8dz5Kw=
github.com/luxfi/geth v1.16.88 h1:I46mMJPbLh2qfqjFDtP91RTSoyj1KdWiTmZX6rjHqaA=
github.com/luxfi/geth v1.16.88/go.mod h1:E5YcGOW9jB6Rgo4LB7NlTZxvkPzIoh1w7q56O63Zf1g=
github.com/luxfi/go-bip39 v1.1.2 h1:p+wLMPGs6MLQh7q0YIsmy2EhHL7LHiELEGTJko6t/Jg=
github.com/luxfi/go-bip39 v1.1.2/go.mod h1:96de9VkR2kY/ASAnhMtvt3TSh+PZkAFAngNj0GjRGDo=
github.com/luxfi/gpu v0.30.0 h1:k+7EDp/WHTa0176zXet1UXTl8IcZcp1ZkS8GUJ5l2iU=
@@ -252,8 +252,8 @@ github.com/luxfi/node v1.23.4 h1:NzARkjbFUtlHyMEqSFQ0VeQRKF5eGWWwfQA7PO4f9VQ=
github.com/luxfi/node v1.23.4/go.mod h1:eSzDGb6N60yHIANLmU5l8uRLlrTzVGq9yvwgITGeWkU=
github.com/luxfi/p2p v1.19.2 h1:uqZq7ofmEDbXlTkv1QThtci01Q+dmDkNmAPeORIyP8E=
github.com/luxfi/p2p v1.19.2/go.mod h1:tI9Bt1R0ouvVtJvXG4e20GlGeV4AR230k4mFF9Vglzk=
github.com/luxfi/precompile v0.5.12 h1:YiF8gaPVFjXESHC+44XVM1RkAyZFMb8iU/60Tfq3Kxs=
github.com/luxfi/precompile v0.5.12/go.mod h1:YiTU6xMf2wmKM2LBzKctn82JhaesFDz4ysC+S864qCA=
github.com/luxfi/precompile v0.5.14 h1:z8mlWv7yWqWczWp59qoxTp0B7l/a0HMtMBPk27MEKyg=
github.com/luxfi/precompile v0.5.14/go.mod h1:r2nImBshkpIfl3n0+Zas0uxfJ3c7JB0E0qpJkBtSV/s=
github.com/luxfi/protocol v0.0.2 h1:TAuXMm1K4VDpG3B3brq1EE9Lb7XlbhQmVBObskgNhmc=
github.com/luxfi/protocol v0.0.2/go.mod h1:Yjn2VRwn5PXim0+JTalAyrVG6YbmuSnYOfeUXSbAsqA=
github.com/luxfi/resource v0.0.1 h1:mTh+ICWSy548GTUSSyx7V/X5dV18oEwxZeQEYGJQhD4=
+19
View File
@@ -98,6 +98,11 @@ type Config struct {
// Airdrop
AirdropFile string `json:"airdrop"`
// ImportChainData is the path to an RLP-encoded blockchain export to import after
// the chain is initialized. If empty, no import is performed. Mirrors the
// admin_importChain RPC but runs at startup. Used by luxd's --import-chain-data flag.
ImportChainData string `json:"import-chain-data"`
// GenesisAllocFile is a path to a JSON file containing additional genesis allocations.
// This allows bypassing P-chain transaction size limits for large genesis states.
// The file should contain a JSON object with address-to-account mappings.
@@ -265,6 +270,20 @@ 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
// 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"`
}
// TxPoolConfig contains the transaction pool config to be passed
+94
View File
@@ -0,0 +1,94 @@
// 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)
}
}
+44
View File
@@ -46,6 +46,7 @@ import (
"github.com/luxfi/evm/plugin/evm/validators/interfaces"
"github.com/luxfi/geth/core/rawdb"
"github.com/luxfi/geth/core/types"
gethvm "github.com/luxfi/geth/core/vm"
"github.com/luxfi/geth/metrics"
"github.com/luxfi/geth/triedb"
"github.com/luxfi/geth/triedb/hashdb"
@@ -344,6 +345,19 @@ 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")
}
if deprecateMsg != "" {
log.Warn("Deprecation Warning", "msg", deprecateMsg)
}
@@ -374,6 +388,7 @@ func (vm *VM) Initialize(ctx context.Context, init block.Init) error {
}
debugLog("Calling parseGenesis with genesisAllocFile=%q, upgradeBytes len=%d", vm.config.GenesisAllocFile, len(upgradeBytes))
fmt.Fprintf(os.Stderr, "[GENESIS-BYTES-DEBUG] len=%d first500=%s\n", len(genesisBytes), string(genesisBytes[:min(500, len(genesisBytes))]))
if len(upgradeBytes) > 0 {
debugLog("upgradeBytes content: %s", string(upgradeBytes))
}
@@ -580,6 +595,30 @@ func (vm *VM) Initialize(ctx context.Context, init block.Init) error {
}
debugLog("Chain initialized successfully")
// Auto-import RLP chain data if configured. Runs the same logic as admin_importChain
// but at startup, allowing luxd's --import-chain-data flag to drive bulk historical
// imports without requiring the RPC server to be up first.
if vm.config.ImportChainData != "" {
log.Info("Auto-importing chain data at startup", "path", vm.config.ImportChainData)
chain := vm.eth.BlockChain()
persistAccepted := func(hash common.Hash, height uint64) error {
var blkID ids.ID
copy(blkID[:], hash[:])
if err := vm.acceptedBlockDB.Put(lastAcceptedKey, blkID[:]); err != nil {
return fmt.Errorf("failed to update acceptedBlockDB: %w", err)
}
if err := vm.versiondb.Commit(); err != nil {
return fmt.Errorf("failed to commit versiondb: %w", err)
}
return nil
}
imported, lastHash, lastHeight, err := importBlocksFromFile(chain, vm.config.ImportChainData, persistAccepted)
if err != nil {
return fmt.Errorf("startup import failed: %w", err)
}
log.Info("Startup import complete", "blocks", imported, "lastHash", lastHash.Hex(), "height", lastHeight)
}
// Start continuous profiler in a goroutine with recovery
go func() {
defer func() {
@@ -660,6 +699,9 @@ func parseGenesis(ctx context.Context, genesisBytes []byte, upgradeBytes []byte,
debugLog("CancunTime was nil, forced to 0")
}
debugLog("EVM forks: shanghaiTime=%v cancunTime=%v", *g.Config.ShanghaiTime, *g.Config.CancunTime)
fmt.Fprintf(os.Stderr, "[EVM-FORK-DEBUG] genesisTimestamp=%v shanghaiTime=%v cancunTime=%v isShanghaiAtGenesis=%v\n",
g.Timestamp, *g.Config.ShanghaiTime, *g.Config.CancunTime,
g.Config.IsShanghai(big.NewInt(0), g.Timestamp))
}
// Populate the Lux config extras.
@@ -737,8 +779,10 @@ func parseGenesis(ctx context.Context, genesisBytes []byte, upgradeBytes []byte,
}
if val, ok := configData["durangoTimestamp"]; ok {
log.Info("DEBUG: Found durangoTimestamp in genesis", "val", val, "type", fmt.Sprintf("%T", val))
fmt.Fprintf(os.Stderr, "[DURANGO-DEBUG] val=%v type=%T\n", val, val)
if ts, ok := val.(float64); ok {
log.Info("DEBUG: Setting DurangoTimestamp from float64", "ts", ts, "uint64", uint64(ts))
fmt.Fprintf(os.Stderr, "[DURANGO-DEBUG] setting durango=%d\n", uint64(ts))
configExtra.DurangoTimestamp = utils.NewUint64(uint64(ts))
} else {
log.Info("DEBUG: durangoTimestamp is not float64")