evm: install bn256-carve-out strict-PQ profile; prove StrictPQ gate active (Red H2 + guardrail)

Task 2 (Red H2): StrictPQTimestamp was wired from config.PQ (vm.go) but the
gate was unproven inert. The canonical lux-mainnet config already sets
pq:true and upgrade.json sets strictPQTimestamp=Quasar-TS; added behavioral
tests proving NetworkUpgrades.IsStrictPQ activates (genesis when pq:true ->
StrictPQTimestamp=0; at the timestamp on running chains; Override carries it).

GUARDRAIL fix: vm.Initialize installed gethvm.AllForbidden() (=pq.Strict),
which forbids the standard bn256 precompiles 0x06-0x08 — breaking
Ethereum-compat dapps. Now installs the EVM-owned LuxStrictPQ() projection
(pq_profile.go): every classical family forbidden EXCEPT bn256. Lux's
security-critical pairing/DLOG is in CUSTOM precompiles (0x0900, 0x22
Pedersen, zkvm verifiers) + the consensus cert, each gated by the SAME
profile bit (config.PQ also pins StrictPQTimestamp=0 -> RefuseUnderStrictPQ).

Tests (b): IsStrictPQ true on a prod strict chain (genesis + timestamp +
Override paths), false when nil. (f): LuxStrictPQ admits bn256 0x06-0x08 and
still refuses every other family; differs from AllForbidden only by bn256.
This commit is contained in:
2026-06-19 13:37:37 -07:00
parent ab10a99880
commit 7e079e9ac1
4 changed files with 196 additions and 3 deletions
@@ -0,0 +1,80 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package extras
import (
"testing"
"github.com/luxfi/evm/utils"
"github.com/stretchr/testify/require"
)
// strict_pq_activation_test.go proves Red wiring task 2 deliverable (b):
// the StrictPQ gate is no longer inert. NetworkUpgrades.IsStrictPQ is the
// single predicate contract.RefuseUnderStrictPQ consults (via the chain
// config's StrictPQReporter); these tests prove it returns true on a chain
// whose StrictPQTimestamp is set, and that the production wiring (vm.config.
// PQ -> StrictPQTimestamp=0) makes it active from genesis.
const quasarStrictPQTS uint64 = 1766708400 // Dec 25 2025 16:20 PST — mainnet
// TestIsStrictPQ_NilTimestamp_NeverActive confirms the default (nil) is
// classical-permissive: IsStrictPQ is false at every timestamp. This is
// exactly the inert state Red found on chains that never set the field.
func TestIsStrictPQ_NilTimestamp_NeverActive(t *testing.T) {
n := NetworkUpgrades{} // StrictPQTimestamp nil
require.False(t, n.IsStrictPQ(0))
require.False(t, n.IsStrictPQ(quasarStrictPQTS))
require.False(t, n.IsStrictPQ(^uint64(0)))
}
// TestIsStrictPQ_GenesisActivation proves a prod strict chain wired via
// vm.config.PQ (which sets StrictPQTimestamp = &0) reports strict-PQ at
// every timestamp ≥ 0 — i.e. from genesis. This is the posture a node
// rebuilding state from scratch gets.
func TestIsStrictPQ_GenesisActivation(t *testing.T) {
zero := uint64(0)
n := NetworkUpgrades{StrictPQTimestamp: &zero}
require.True(t, n.IsStrictPQ(0), "StrictPQTimestamp=0 ⇒ active from genesis")
require.True(t, n.IsStrictPQ(quasarStrictPQTS))
}
// TestIsStrictPQ_TimestampActivation proves the running-chain rollout: the
// canonical mainnet upgrade.json sets strictPQTimestamp = the Quasar
// timestamp. IsStrictPQ must be false strictly before it and true at/after.
func TestIsStrictPQ_TimestampActivation(t *testing.T) {
ts := quasarStrictPQTS
n := NetworkUpgrades{StrictPQTimestamp: &ts}
require.False(t, n.IsStrictPQ(ts-1), "must be classical-permissive strictly before activation")
require.True(t, n.IsStrictPQ(ts), "must be strict-PQ at the activation timestamp")
require.True(t, n.IsStrictPQ(ts+1))
}
// TestIsStrictPQ_OverridePropagates proves NetworkUpgrades.Override carries
// StrictPQTimestamp through — the path upgrade.json's networkUpgradeOverrides
// takes to reach the live config. Without this the upgrade.json activation
// would be silently dropped (the inert failure mode).
func TestIsStrictPQ_OverridePropagates(t *testing.T) {
base := NetworkUpgrades{} // nil — inert
require.False(t, base.IsStrictPQ(quasarStrictPQTS))
ts := quasarStrictPQTS
base.Override(&NetworkUpgrades{StrictPQTimestamp: &ts})
require.True(t, base.IsStrictPQ(quasarStrictPQTS),
"Override must carry StrictPQTimestamp so upgrade.json's override actually activates the gate")
}
// TestSetDefaults_PreservesExplicitStrictPQ proves SetDefaults does not
// clobber an explicitly-set StrictPQTimestamp (it has no default, so a set
// value must survive default-filling).
func TestSetDefaults_PreservesExplicitStrictPQ(t *testing.T) {
n := NetworkUpgrades{
EVMTimestamp: utils.NewUint64(0),
StrictPQTimestamp: utils.NewUint64(0),
}
n.SetDefaults(getTestMainnetConfig())
require.NotNil(t, n.StrictPQTimestamp)
require.Equal(t, uint64(0), *n.StrictPQTimestamp)
require.True(t, n.IsStrictPQ(0))
}
+39
View File
@@ -0,0 +1,39 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import "github.com/luxfi/pq"
// pq_profile.go — the strict-PQ precompile profile a Lux-derived EVM chain
// installs on its ChainConfig. The EVM plugin OWNS the profile it installs
// (vm.Initialize sets vm.chainConfig.PQ), so the projection lives here, in
// one place, constructed from the shared luxfi/pq primitives.
//
// THE GUARDRAIL. LuxStrictPQ refuses every quantum-breakable precompile
// family EXCEPT the standard alt_bn128 (BN254) precompiles at 0x06/0x07/
// 0x08 (EIP-196 / EIP-197). Those are Ethereum-compatibility precompiles
// general-purpose dapps depend on; they are NOT a Lux settlement-security
// surface. Lux's security-critical pairing / DLOG usage lives entirely in
// the CUSTOM precompiles (precompile/zk @ 0x0900, the 0x22 Pedersen
// sub-path, the Z-Chain zkvm verifiers) and the consensus cert — each
// gated by its own strict-PQ switch (contract.RefuseUnderStrictPQ /
// RegisterZKPrecompiles / VerifyUnderPolicy), all driven by the SAME
// profile bit (config.PQ, which also pins extras.StrictPQTimestamp=0).
//
// pq.Strict() (the library maximal profile, re-exported as
// gethvm.AllForbidden) forbids bn256 too; a Lux chain must NOT install
// that — it would break dapp bn256. We construct the carve-out here as a
// pq.Profile literal (every field of pq.Strict() except ForbidBn256).
func LuxStrictPQ() *pq.Profile {
return &pq.Profile{
ForbidEcrecover: true,
ForbidP256Verify: true,
ForbidSHA256: true,
ForbidRIPEMD160: true,
ForbidBlake2F: true,
ForbidBn256: false, // Ethereum-compat dapp precompiles — see doc
ForbidBLS12381: true,
ForbidKZG: true,
}
}
+66
View File
@@ -95,3 +95,69 @@ func TestPQ_NilProfileAdmits(t *testing.T) {
}
}
}
// TestPQ_EVMInstallsLuxStrictPQ pins the GUARDRAIL at the EVM-plugin
// layer: vm.Initialize installs LuxStrictPQ() (NOT AllForbidden())
// when config.PQ is set. LuxStrictPQ admits the standard alt_bn128 (BN254)
// precompiles at 0x06–0x08 (Ethereum-compat for dapps) while still
// refusing every other classical family.
//
// Deliverable (f): standard EVM bn256 still works under strict-PQ. We
// assert the RefuseUnder gate (the chokepoint runPrecompile calls) does
// NOT refuse the bn256 ops under the profile the chain actually installs.
func TestPQ_EVMInstallsLuxStrictPQ(t *testing.T) {
p := LuxStrictPQ()
if p == nil {
t.Fatal("LuxStrictPQ returned nil")
}
// bn256 family (0x06-0x08) MUST be admitted (deliverable f).
for _, op := range []pq.Op{pq.OpBn256Add, pq.OpBn256ScalarMul, pq.OpBn256Pairing} {
if err := p.RefuseUnder(op); err != nil {
t.Errorf("Lux strict-PQ must admit standard bn256 op %v (EVM-compat); got %v", op, err)
}
}
// Every OTHER classical family stays refused under the chain profile.
refused := []struct {
op pq.Op
wantErr error
}{
{pq.OpEcrecover, pq.ErrEcrecoverForbidden},
{pq.OpP256Verify, pq.ErrP256VerifyForbidden},
{pq.OpSHA256, pq.ErrSHA256Forbidden},
{pq.OpRIPEMD160, pq.ErrRIPEMD160Forbidden},
{pq.OpBlake2F, pq.ErrBlake2FForbidden},
{pq.OpBLS12381Pairing, pq.ErrBLS12381Forbidden},
{pq.OpKZGPointEval, pq.ErrKZGForbidden},
}
for _, c := range refused {
if err := p.RefuseUnder(c.op); !errors.Is(err, c.wantErr) {
t.Errorf("Lux strict-PQ must refuse %v with %v; got %v", c.op, c.wantErr, err)
}
}
}
// TestPQ_LuxStrictPQDiffersFromAllForbiddenOnlyByBn256 proves the carve-out
// is exactly one family: LuxStrictPQ and AllForbidden agree on every flag
// except ForbidBn256. This keeps the two projections from silently drifting
// apart on any other family.
func TestPQ_LuxStrictPQDiffersFromAllForbiddenOnlyByBn256(t *testing.T) {
lux := LuxStrictPQ()
all := gethvm.AllForbidden()
if lux.ForbidBn256 {
t.Error("LuxStrictPQ().ForbidBn256 must be false")
}
if !all.ForbidBn256 {
t.Error("AllForbidden().ForbidBn256 must be true (the library maximal profile)")
}
// Force the only known difference to equal, then the encodings must match.
luxAlignedBn := *lux
luxAlignedBn.ForbidBn256 = true
if luxAlignedBn.Encode() != all.Encode() {
t.Fatalf("LuxStrictPQ differs from AllForbidden on more than bn256: %x vs %x",
luxAlignedBn.Encode(), all.Encode())
}
}
+11 -3
View File
@@ -47,7 +47,6 @@ 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"
@@ -524,9 +523,18 @@ func (vm *VM) Initialize(ctx context.Context, init block.Init) error {
// F102 close-out — PQ gate. chainConfig is now non-nil.
// One concept, one place: ChainConfig.PQ holds the profile;
// (*EVM).runPrecompile reads it via chainConfig.PQ.RefuseUnder(op).
//
// Install the LUX strict-PQ profile, NOT AllForbidden(): the standard
// alt_bn128 (BN254) precompiles at 0x06–0x08 stay available for
// Ethereum-compat dapps. Lux's security-critical pairing/DLOG usage is
// in the CUSTOM precompiles (precompile/zk @ 0x0900, 0x22 Pedersen, the
// Z-Chain verifiers) and the consensus cert — each gated by its own
// strict-PQ switch driven by the SAME profile (vm.config.PQ also pins
// StrictPQTimestamp=0 above, which activates contract.RefuseUnderStrictPQ
// for those custom precompiles). See LuxStrictPQ (pq_profile.go).
if vm.config.PQ {
vm.chainConfig.PQ = gethvm.AllForbidden()
log.Info("EVM PQ mode active: classical precompiles refuse")
vm.chainConfig.PQ = LuxStrictPQ()
log.Info("EVM PQ mode active: classical precompiles refuse (standard bn256 0x06-0x08 kept for EVM-compat)")
}
// create genesisHash after applying upgradeBytes in case