params/extras: StrictPQTimestamp + per-chain PQ posture wiring (CR-2)

Closes CR-2 from the cross-stack adversarial audit: PQ posture on
multi-chain hosts moves from a process-global to a per-chain
ChainConfig setting. A strict-PQ subnet and a permissive L1 in the
same node process now refuse / admit classical primitives correctly,
independent of which subnet last initialised.

Surface:
  - NetworkUpgrades.StrictPQTimestamp *uint64 (json:"strictPQTimestamp")
    nil = chain is classical-permissive (default for non-Lux integrators)
    0   = strict-PQ from genesis (set by plugin/evm when config.PQ=true)
  - NetworkUpgrades.IsStrictPQ(time uint64) bool — fork rule
  - chainConfigAdapter.IsStrictPQ — bridges params/extras into the
    contract.StrictPQReporter contract that stateful precompiles call.
  - extChainConfigBridge.IsStrictPQ — type-asserts via a local
    interface so non-Lux chains integrating Lux precompiles remain
    classical-permissive (no contract widening forced on them).
  - plugin/evm: when config.PQ=true, vm.chainConfig.PQ is installed
    (covers 0x01..0x0a standard precompiles via runPrecompile gate) AND
    extras.StrictPQTimestamp is set to genesis (covers Lux stateful
    precompiles — KZG, Groth16, PLONK, fflonk, Halo2, BN254-Pedersen,
    BabyJubJub, Pallas/Vesta — via contract.RefuseUnderStrictPQ).

Module bumps:
  - github.com/luxfi/geth v1.16.91
    (bundles MLDSATxType (0x42) + per-chain PQ profile gate)
  - github.com/luxfi/pq v1.0.1-0.20260512064747-3d4c9414e15e
    (RefuseUnder promoted to method on *Profile)

No semantic change for chains that do not opt into PQ:
StrictPQTimestamp is nil, IsStrictPQ returns false, every classical
primitive admits, gas accounting unchanged.
This commit is contained in:
2026-05-11 23:54:35 -07:00
parent df26cf1a3a
commit e382bf3955
6 changed files with 74 additions and 14 deletions
+10
View File
@@ -234,3 +234,13 @@ func (c *chainConfigAdapter) IsDurango(time uint64) bool {
extra := params.GetExtra(c.config)
return extra.IsDurango(time)
}
// IsStrictPQ satisfies the contract.StrictPQReporter interface so that
// classical precompiles (KZG, Groth16, PLONK, fflonk, Halo2, BN254-Pedersen,
// BabyJubJub, Pallas/Vesta) refuse to run on chains that pin a strict
// post-quantum profile. Returns false when StrictPQTimestamp is unset
// (the default for non-Lux chains and Lux chains without strict-PQ pinned).
func (c *chainConfigAdapter) IsStrictPQ(time uint64) bool {
extra := params.GetExtra(c.config)
return extra.IsStrictPQ(time)
}
+2 -2
View File
@@ -22,7 +22,7 @@ require (
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.90
github.com/luxfi/geth v1.16.91
github.com/luxfi/go-bip39 v1.1.2
github.com/luxfi/gpu v1.0.1
github.com/luxfi/ids v1.2.9
@@ -92,7 +92,7 @@ require (
github.com/luxfi/fhe v1.8.0 // indirect
github.com/luxfi/lattice/v7 v7.0.0 // indirect
github.com/luxfi/node v1.23.4 // indirect
github.com/luxfi/pq v0.0.0-20260511232911-de15d6ce2f0f // indirect
github.com/luxfi/pq v1.0.1-0.20260512064747-3d4c9414e15e // indirect
github.com/luxfi/protocol v0.0.2 // indirect
github.com/luxfi/resource v0.0.1 // indirect
github.com/luxfi/threshold v1.5.5 // indirect
+4
View File
@@ -228,6 +228,8 @@ github.com/luxfi/formatting v1.0.1 h1:ZnE1rAdEUds9yAegdVdGDOBGN6hLMPOv6E03Fp8IEY
github.com/luxfi/formatting v1.0.1/go.mod h1:mYzNf5DJOiqSSKUPzNj5dKy4tstFbN3pZlkI5716eKc=
github.com/luxfi/geth v1.16.90 h1:LLvhUggNyK2bZSN1gtH3yknVB8YPXcS9sHrqTZwWpfg=
github.com/luxfi/geth v1.16.90/go.mod h1:ynzwEXqLboNr1R1wdht/SqYnGINtaU4lYajR779JxgE=
github.com/luxfi/geth v1.16.91 h1:1XjCX29Ry6+wrn1Jmu/Ri05QYfgrIRo50Xmn+BP1PSk=
github.com/luxfi/geth v1.16.91/go.mod h1:7qZotINLz5Zq3WD2CK2gm6mngscR9x/eSXtfzBzmDjA=
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 v1.0.1 h1:7cDJTwTZ8Wk5KmyzsiGPe1nekyTgsSG6LJ+4rgKim8E=
@@ -254,6 +256,8 @@ github.com/luxfi/p2p v1.19.2 h1:uqZq7ofmEDbXlTkv1QThtci01Q+dmDkNmAPeORIyP8E=
github.com/luxfi/p2p v1.19.2/go.mod h1:tI9Bt1R0ouvVtJvXG4e20GlGeV4AR230k4mFF9Vglzk=
github.com/luxfi/pq v0.0.0-20260511232911-de15d6ce2f0f h1:heVZJMlU6RtYZGjgmqFbtcO9RCMl+6Sfchz/n0U/6+U=
github.com/luxfi/pq v0.0.0-20260511232911-de15d6ce2f0f/go.mod h1:8bppZcRElfrVt0n3nYCZW3iX1TvhvzNbdjNdK1irgIE=
github.com/luxfi/pq v1.0.1-0.20260512064747-3d4c9414e15e h1:eXbRZiu08vO0f4JaJd/XVGnDn25x5W4TS4Jf2nHRTVM=
github.com/luxfi/pq v1.0.1-0.20260512064747-3d4c9414e15e/go.mod h1:8bppZcRElfrVt0n3nYCZW3iX1TvhvzNbdjNdK1irgIE=
github.com/luxfi/precompile v0.5.16 h1:tIY7/yK856lggOvv/5vVC0Cr7gRUs6Wi7MhibIszsng=
github.com/luxfi/precompile v0.5.16/go.mod h1:r2nImBshkpIfl3n0+Zas0uxfJ3c7JB0E0qpJkBtSV/s=
github.com/luxfi/protocol v0.0.2 h1:TAuXMm1K4VDpG3B3brq1EE9Lb7XlbhQmVBObskgNhmc=
+17
View File
@@ -54,6 +54,12 @@ type NetworkUpgrades struct {
FortunaTimestamp *uint64 `json:"fortunaTimestamp,omitempty"`
// Granite is a placeholder for the next upgrade.
GraniteTimestamp *uint64 `json:"graniteTimestamp,omitempty"`
// StrictPQTimestamp pins the chain to a strict post-quantum profile from
// the given timestamp onward. When active, classical pairing-based and
// discrete-log precompiles refuse to execute via contract.RefuseUnderStrictPQ.
// nil = never activates (classical-permissive, the default for non-Lux
// chains that integrate Lux precompiles). 0 = active from genesis.
StrictPQTimestamp *uint64 `json:"strictPQTimestamp,omitempty"`
}
func (n *NetworkUpgrades) Equal(other *NetworkUpgrades) bool {
@@ -201,6 +207,9 @@ func (n *NetworkUpgrades) Override(o *NetworkUpgrades) {
if o.GraniteTimestamp != nil {
n.GraniteTimestamp = o.GraniteTimestamp
}
if o.StrictPQTimestamp != nil {
n.StrictPQTimestamp = o.StrictPQTimestamp
}
}
// IsEVM returns whether [time] represents a block
@@ -233,6 +242,14 @@ func (n *NetworkUpgrades) IsGranite(time uint64) bool {
return isTimestampForked(n.GraniteTimestamp, time)
}
// IsStrictPQ returns whether [time] represents a block at or after the
// strict-PQ activation timestamp. nil StrictPQTimestamp means the chain
// is classical-permissive (the default for non-Lux chains that integrate
// Lux precompiles).
func (n *NetworkUpgrades) IsStrictPQ(time uint64) bool {
return isTimestampForked(n.StrictPQTimestamp, time)
}
func (n *NetworkUpgrades) Description() string {
var banner string
banner += fmt.Sprintf(" - EVM Timestamp: @%-10v (https://github.com/luxfi/node/releases/tag/v1.10.0)\n", ptrToString(n.EVMTimestamp))
+25 -12
View File
@@ -345,20 +345,22 @@ 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 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.
// F102 close-out — install the chain-scoped PQ posture on the chain
// config. 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.
// One concept, one way: ChainConfig.PQ holds the profile;
// (*EVM).runPrecompile reads it via chainConfig.PQ.RefuseUnder(op)
// before dispatching each precompile. nil profile (the default)
// preserves classical EVM semantics — every legacy chain runs
// unchanged. Multi-chain hosts (strict-PQ + permissive in one
// process) get the right gate per EVM instance because the profile
// lives on each chain's config, not on a process-global atomic.
if vm.config.PQ {
gethvm.SetPQProfile(gethvm.AllForbidden())
vm.chainConfig.PQ = gethvm.AllForbidden()
log.Info("EVM PQ mode active: classical precompiles (ecrecover, sha256/ripemd/blake2F, alt_bn128, BLS12-381, KZG) refuse")
}
@@ -403,6 +405,17 @@ func (vm *VM) Initialize(ctx context.Context, init block.Init) error {
}
debugLog("parseGenesis succeeded: chainID=%v alloc=%d accounts", g.Config.ChainID, len(g.Alloc))
// When config.PQ is set, pin strict-PQ from genesis on the extras config so
// stateful precompiles that call contract.RefuseUnderStrictPQ refuse
// classical primitives. The geth-layer gate above (SetPQProfile) covers
// 0x01..0x09 standard precompiles; this covers Lux stateful precompiles
// (KZG, Groth16, PLONK, fflonk, Halo2, BN254-Pedersen, BabyJubJub,
// Pallas/Vesta, BLS12-381 modules, etc.) via the same posture.
if vm.config.PQ {
zero := uint64(0)
params.GetExtra(g.Config).StrictPQTimestamp = &zero
}
vm.syntacticBlockValidator = NewBlockValidator()
debugLog("Creating ethConfig")
+16
View File
@@ -277,6 +277,22 @@ func (c *extChainConfigBridge) IsDurango(time uint64) bool {
return c.internal.IsDurango(time)
}
// IsStrictPQ satisfies the external contract.StrictPQReporter interface so
// classical precompiles in github.com/luxfi/precompile/* refuse to execute
// when the chain pins a strict post-quantum profile. The internal config
// (chainConfigAdapter) delegates to extras.IsStrictPQ. We type-assert
// rather than widen precompileconfig.ChainConfig so non-Lux chains that
// integrate Lux precompiles remain classical-permissive by default.
func (c *extChainConfigBridge) IsStrictPQ(time uint64) bool {
type strictPQ interface {
IsStrictPQ(time uint64) bool
}
if r, ok := c.internal.(strictPQ); ok {
return r.IsStrictPQ(time)
}
return false
}
// =============================================================================
// Configurator Bridge (external → internal)
// =============================================================================