mirror of
https://github.com/luxfi/evm.git
synced 2026-09-24 00:27:44 +00:00
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:
@@ -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))
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user