Files
evm/params/extras/precompile_upgrade_rollout_test.go
zeekay 5460f8370e feat(evm): enable standard precompiles — remove vestigial strict-PQ refusal layer
Enable-everything builder surface (public permissionless launch). The geth-layer
strict-PQ profile (LuxStrictPQ) refused ecrecover / p256Verify / sha256 /
ripemd160 / blake2f / bls12381 / kzg under vm.config.PQ — but ecrecover
(secp256k1) is THE Ethereum signature path every dapp depends on, and the rest
are verify-only / hash, key-safe. Refusing them breaks the builder surface.

The chain now installs NO forbidding pq.Profile: chainConfig.PQ stays nil,
which pq.RefuseUnder(nil) treats as permissive. The generic geth mechanism
(chainConfig.PQ + runPrecompile.RefuseUnder + legacypool nil-check) is kept for
any chain that wants it; Lux installs nothing.

This completes the decomplect started in luxfi/precompile: consensus PQ and the
EVM precompile profile were complected through config.PQ / StrictPQTimestamp.
After precompile dropped contract.RefuseUnderStrictPQ from every verify-only
precompile, the whole strict-PQ projection here was dead. Removed:
  - plugin/evm/pq_profile.go (LuxStrictPQ)
  - plugin/evm/vm.go: the `if vm.config.PQ { chainConfig.PQ = LuxStrictPQ() }`
    install + the unconditional StrictPQTimestamp=0 genesis pin
  - plugin/evm/config: the config.PQ bool field (no code writer; was JSON-opt-in)
  - params/extras/network_upgrades.go: StrictPQTimestamp field + apply + IsStrictPQ
  - precompile/registry/bridge.go + core/precompile_overrider.go: the dead
    IsStrictPQ adapters (only existed to feed the deleted StrictPQReporter)
  - tests: pq_test.go, strict_pq_activation_test.go, and the two StrictPQ
    assertions (+ orphaned helper) in precompile_upgrade_rollout_test.go

Lux's PQ security is enforced at consensus / identity (quasar/p3q), not by
refusing an EVM verifier a dapp asked for. The live lux-mainnet upgrade.json
never actually set strictPQTimestamp, so this matches production.

Build: full module green. Tests: params/extras, plugin/evm/config,
precompile/registry pass.
2026-06-27 21:14:05 -07:00

259 lines
11 KiB
Go

// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// External test package so the rollout regression can depend on the
// luxfi/evm/params/extras public API without dragging in
// luxfi/evm/precompile/registry (which would create an import cycle
// via the modules.Register side-effects).
//
// The tests parse raw JSON only — they intentionally avoid the typed
// PrecompileUpgrade UnmarshalJSON path so they do NOT need every
// precompile module's init() to have run. The contract these tests
// enforce is the textual JSON shape (key + blockTimestamp) which is
// what luxd's checkPrecompileCompatible compares; the per-key Config
// struct identity is verified inside the existing module-level tests.
package extras_test
import (
"encoding/json"
"os"
"testing"
"github.com/stretchr/testify/require"
)
// luxMainnetLiveUpgradeJSON is the upgrade.json currently inlined in the
// lux-mainnet StatefulSet startup script
// (~/work/lux/universe/k8s/lux-mainnet/luxd-startup.yaml line 182). It is
// the source-of-truth list of precompile activations active on every C-
// Chain pod today. Every entry here MUST stay present (at the same
// blockTimestamp) in the canonical upgrade.json shipped by luxfi/genesis,
// otherwise luxd's checkPrecompileCompatible refuses to boot.
//
// If this constant changes, the lux-mainnet StatefulSet manifest changed
// out-of-band — sync it back to luxfi/genesis BEFORE shipping the new
// luxd image, never the other way around.
const luxMainnetLiveUpgradeJSON = `{
"networkUpgradeOverrides": {
"durangoTimestamp": 0,
"quasarTimestamp": 0,
"fortunaTimestamp": 0,
"graniteTimestamp": 0
},
"precompileUpgrades": [
{"aiMiningConfig": {"blockTimestamp": 0}},
{"blake3Config": {"blockTimestamp": 0}},
{"cggmp21Verify": {"blockTimestamp": 0}},
{"deadZeroConfig": {"blockTimestamp": 0}},
{"deadConfig": {"blockTimestamp": 0}},
{"deadFullConfig": {"blockTimestamp": 0}},
{"routerConfig": {"blockTimestamp": 0}},
{"fheConfig": {"blockTimestamp": 0}},
{"frostVerify": {"blockTimestamp": 0}},
{"graphConfig": {"blockTimestamp": 0}},
{"hpkeConfig": {"blockTimestamp": 0}},
{"mldsaVerify": {"blockTimestamp": 0}},
{"mlkemConfig": {"blockTimestamp": 0}},
{"ringConfig": {"blockTimestamp": 0}},
{"slhdsaVerify": {"blockTimestamp": 0}},
{"zkConfig": {"blockTimestamp": 0}}
]
}`
// TestMainnetUpgradeJSON_IsForwardCompatibleWithLiveActivations is the
// regression gate from red-review finding #7. It asserts that every
// precompile activation currently live on lux-mainnet is preserved at the
// same blockTimestamp in the canonical upgrade.json so that
// checkPrecompileCompatible returns nil at boot.
//
// The failure mode this prevents:
//
// luxd boots, reads the new upgrade.json, and runs
// checkPrecompileCompatible against the active configs (the live
// activations that already shipped). For each live entry it walks the
// new list looking for the same key at the same timestamp. If it's
// missing -> "missing PrecompileUpgrade[i]". If it's at a different
// timestamp -> "mismatching PrecompileUpgrade[i]". Either way, BOOT
// FAILS. This is the exact wedge that bricked the cluster on the
// original rollout attempt.
func TestMainnetUpgradeJSON_IsForwardCompatibleWithLiveActivations(t *testing.T) {
canonical := readPrecompileUpgradeTimestamps(t, readCanonicalMainnetUpgradeJSONRaw(t))
live := readPrecompileUpgradeTimestamps(t, []byte(luxMainnetLiveUpgradeJSON))
for key, liveTs := range live {
canonicalTs, ok := canonical[key]
require.Truef(t, ok,
"precompile %q is active on lux-mainnet (blockTimestamp=%d) but is missing from canonical upgrade.json — luxd will refuse to boot (checkPrecompileCompatible: missing PrecompileUpgrade)",
key, liveTs,
)
require.Equalf(t, liveTs, canonicalTs,
"precompile %q is active on lux-mainnet at blockTimestamp=%d but canonical upgrade.json schedules it at blockTimestamp=%d — luxd will refuse to boot (checkPrecompileCompatible: mismatching PrecompileUpgrade). RESCHEDULING AN ALREADY-LIVE PRECOMPILE IS NEVER VALID.",
key, liveTs, canonicalTs,
)
}
}
// TestMainnetUpgradeJSON_PrecompileTimestampsAreMonotonic enforces the
// validation rule from extras.ChainConfig.verifyPrecompileUpgrades —
// precompile timestamps must be monotonically increasing across the
// list (the verify call refuses a config that decreases). If we get this
// wrong in the rollout config, every luxd boot rejects the JSON before
// it even runs checkPrecompileCompatible.
func TestMainnetUpgradeJSON_PrecompileTimestampsAreMonotonic(t *testing.T) {
raw := readCanonicalMainnetUpgradeJSONRaw(t)
var doc struct {
PrecompileUpgrades []map[string]json.RawMessage `json:"precompileUpgrades"`
}
require.NoError(t, json.Unmarshal(raw, &doc))
var prev uint64
for i, entry := range doc.PrecompileUpgrades {
require.Lenf(t, entry, 1, "precompileUpgrades[%d] must have exactly one key", i)
for key, rawVal := range entry {
var v struct {
BlockTimestamp uint64 `json:"blockTimestamp"`
}
require.NoErrorf(t, json.Unmarshal(rawVal, &v), "precompileUpgrades[%d][%q] must have blockTimestamp", i, key)
require.GreaterOrEqualf(t, v.BlockTimestamp, prev,
"precompileUpgrades[%d][%q] blockTimestamp=%d is < previous %d — verifyPrecompileUpgrades refuses non-monotonic timestamps",
i, key, v.BlockTimestamp, prev,
)
prev = v.BlockTimestamp
}
}
}
// TestMainnetUpgradeJSON_WarpRequiresPrimaryNetworkSigners enforces the
// Warp policy from red-review finding #8: the canonical upgrade.json must
// schedule warp so that, once the strict-PQ fork lands, every cross-chain
// warp message is signed by primary-network validators (not just a subnet
// quorum).
//
// Warp lives in the genesis chainConfig (cchain.json config.warpConfig at
// the genesis timestamp, with requirePrimaryNetworkSigners=false during the
// classical era). The upgrade schedule then carries a two-step toggle at the
// strict-PQ fork: disable@strictPQ-1, then re-enable@strictPQ with the
// PQ-hardened policy. Both toggles are strictly AFTER genesis time, so this
// is a genuine future upgrade — NOT a reschedule of the genesis warp (that
// would be the relaunch fork hazard; see relaunch_safety_test.go). The
// policy assertion therefore targets the RE-ENABLE entry (the one that
// carries quorumNumerator + requirePrimaryNetworkSigners); the disable entry
// carries only {blockTimestamp, disable:true}.
func TestMainnetUpgradeJSON_WarpRequiresPrimaryNetworkSigners(t *testing.T) {
const strictPQ uint64 = 1766708400
raw := readCanonicalMainnetUpgradeJSONRaw(t)
var doc struct {
PrecompileUpgrades []map[string]json.RawMessage `json:"precompileUpgrades"`
}
require.NoError(t, json.Unmarshal(raw, &doc))
var (
foundDisable bool
foundReEnable bool
prevWarpTs *uint64
)
for i, entry := range doc.PrecompileUpgrades {
warpRaw, ok := entry["warpConfig"]
if !ok {
continue
}
var warp struct {
BlockTimestamp uint64 `json:"blockTimestamp"`
Disable bool `json:"disable"`
QuorumNumerator uint64 `json:"quorumNumerator"`
RequirePrimaryNetworkSigners bool `json:"requirePrimaryNetworkSigners"`
}
require.NoError(t, json.Unmarshal(warpRaw, &warp))
// Every warp upgrade entry must be strictly after genesis time —
// rescheduling the genesis warp into the (0, genesisTime] window is
// the relaunch fork hazard.
require.Greaterf(t, warp.BlockTimestamp, uint64(0),
"warpConfig upgrade entry at index %d has blockTimestamp 0 — warp is declared in the genesis chainConfig; an upgrade entry pinned to 0 would collide with the genesis warp and fail verifyPrecompileUpgrades",
i,
)
// Same-key strict increase (mirrors verifyPrecompileUpgrades).
if prevWarpTs != nil {
require.Greaterf(t, warp.BlockTimestamp, *prevWarpTs,
"warpConfig entries must strictly increase in blockTimestamp (got %d after %d)",
warp.BlockTimestamp, *prevWarpTs,
)
}
ts := warp.BlockTimestamp
prevWarpTs = &ts
if warp.Disable {
foundDisable = true
require.Equalf(t, strictPQ-1, warp.BlockTimestamp,
"warpConfig disable must fire one second before the strict-PQ fork (%d), got %d",
strictPQ-1, warp.BlockTimestamp,
)
continue
}
// Re-enable entry: carries the PQ-hardened policy.
foundReEnable = true
require.Equalf(t, strictPQ, warp.BlockTimestamp,
"warpConfig re-enable must fire at the strict-PQ fork (%d), got %d", strictPQ, warp.BlockTimestamp,
)
require.Equal(t, uint64(67), warp.QuorumNumerator, "warpConfig quorumNumerator must be 67")
require.Truef(t, warp.RequirePrimaryNetworkSigners,
"warpConfig.requirePrimaryNetworkSigners must be true on lux-mainnet — every cross-chain warp message MUST be signed by primary-network validators (red-review finding #8)",
)
}
require.True(t, foundDisable, "warpConfig disable entry must be present in canonical upgrade.json")
require.True(t, foundReEnable, "warpConfig re-enable entry (with PQ signer policy) must be present in canonical upgrade.json")
}
// readPrecompileUpgradeTimestamps decodes the precompileUpgrades array
// into a {key: blockTimestamp} map, skipping warpConfig (covered by its
// own test) and feeManagerConfig (not a precompile activation per se).
func readPrecompileUpgradeTimestamps(t *testing.T, raw []byte) map[string]uint64 {
t.Helper()
var doc struct {
PrecompileUpgrades []map[string]json.RawMessage `json:"precompileUpgrades"`
}
require.NoError(t, json.Unmarshal(raw, &doc))
out := make(map[string]uint64, len(doc.PrecompileUpgrades))
for i, entry := range doc.PrecompileUpgrades {
require.Lenf(t, entry, 1, "precompileUpgrades[%d] must have exactly one key", i)
for key, rawVal := range entry {
if key == "warpConfig" || key == "feeManagerConfig" {
continue
}
var v struct {
BlockTimestamp uint64 `json:"blockTimestamp"`
}
require.NoErrorf(t, json.Unmarshal(rawVal, &v), "precompileUpgrades[%d][%q] must have blockTimestamp", i, key)
_, dup := out[key]
require.Falsef(t, dup, "precompileUpgrades has duplicate key %q at index %d", key, i)
out[key] = v.BlockTimestamp
}
}
return out
}
// readCanonicalMainnetUpgradeJSONRaw returns the canonical upgrade.json
// bytes. The test resolves the file via the relative path from
// evm/params/extras up to genesis/configs/mainnet — both luxfi/evm and
// luxfi/genesis live in the same `~/work/lux` worktree on CI runners.
func readCanonicalMainnetUpgradeJSONRaw(t *testing.T) []byte {
t.Helper()
candidates := []string{
// luxfi/evm running standalone, sibling luxfi/genesis checkout.
"../../../../genesis/configs/mainnet/upgrade.json",
// monorepo layout (e.g. when both repos are inside the same root).
"../../../genesis/configs/mainnet/upgrade.json",
}
for _, candidate := range candidates {
if data, err := os.ReadFile(candidate); err == nil {
return data
}
}
t.Skipf("canonical mainnet upgrade.json not reachable from cwd — looked in %v; run from a worktree that contains luxfi/genesis alongside luxfi/evm", candidates)
return nil
}