Files
evm/core/precompile_alwayson_test.go
zeekayandHanzo Dev 7df2ba44c9 Each Rules carries the context its Lux rules follow from
GetRulesExtra derived a Rules' Lux rules from a process-wide last context:
the chain config and timestamp of whichever Rules any goroutine evaluated
most recently. Rules are evaluated concurrently, by block processing, the
miner, the tx pool and every eth_call, so a Rules could read another
evaluation's upgrades: its IsDurango, its predicaters, its enabled
precompiles, and the intrinsic gas that depends on them.

The context now travels with the Rules value. core's rules hook already put
the chain config and timestamp in Rules.Payload for its precompile
overrider; the overrider answers them, and GetRulesExtra reads them from
the Rules it is given. RulesAt attaches them where no hook did. The global
and SetRulesContext are gone. Every call site evaluates its Rules for the
block it reads them for, so a caller that no other goroutine races sees the
same rules as before. The blob pool's state checks passed a zero Rules and
read the head's context the check before them had left; they now pass the
head's Rules.

TestRulesExtraIsTheRulesOwn evaluates two rule sets and reads the first;
TestRulesExtraUnderConcurrentEvaluation reads four rule sets on four
goroutines at once, across two times and two chains, including the
intrinsic gas of a creation. TestActivePrecompiles, skipped for this bug,
runs again.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 11:44:42 -07:00

241 lines
11 KiB
Go

// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Excluded under -tags cevm: imports precompile/registry, which transitively
// pulls luxfi/precompile/dex -> luxfi/dex/pkg/dex whose cgo file #includes the
// (absent) dex_gpu.h header — breaking the CGO_ENABLED=1 build the cevm shadow
// verifier needs. Precompile test, not a block-execution test; still runs in
// every non-cevm build. Orthogonal blocker, not a cevm regression.
//go:build !cevm
package core
import (
"math/big"
"sync"
"testing"
"github.com/luxfi/evm/core/state"
"github.com/luxfi/evm/params"
"github.com/luxfi/evm/params/extras"
"github.com/luxfi/evm/precompile/modules"
"github.com/luxfi/evm/precompile/registry"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/rawdb"
"github.com/stretchr/testify/require"
)
// precompile_alwayson_test.go pins the activation contract of the DEX settlement money
// path 0x9999. It is a SYSTEM precompile activated by the Lux PROTOCOL on every chain
// with NO per-net config (no dexSettleConfig genesis precompile, no precompileUpgrades
// entry) — but NOT from genesis. It turns on at the protocol constant
// registry.DexSettleActivationTime (2025-12-25), exactly like a dated fork, so a chain
// whose genesis predates it reproduces its ORIGINAL genesis (see
// genesis_dexsettle_reproduce_test.go). The activation couples two effects, both pinned
// here and both keyed on the SAME timestamp so they can never disagree:
//
// - DISPATCH: 0x9999 enters the enabled precompile set at timestamps >=
// DexSettleActivationTime, and is ABSENT before it. This is the set
// PrecompileOverride consults; the decision is a pure function of the passed-in
// timestamp (no process-global read), so it is race-safe and identical on every
// replay of a given block.
// - MARKER: the EXTCODESIZE marker (nonce=1 + non-empty code) is written into 0x9999
// exactly ONCE, on the block transition that crosses DexSettleActivationTime (which
// is genesis iff the genesis timestamp already reaches it). So EXTCODESIZE>0 /
// eth_getCode!=0x / Solidity's contract-existence guard pass from the activation
// block forward, and NOT before.
var settleAddr9999 = common.HexToAddress("0x0000000000000000000000000000000000009999")
// addr9999 is the shared handle used by the genesis-reproduction regression in the
// sibling file genesis_dexsettle_reproduce_test.go.
var addr9999 = settleAddr9999
// activation is the protocol timestamp at which 0x9999 turns on (marker + dispatch).
var activation = registry.DexSettleActivationTime
// TestAlwaysOn_9999_Registered asserts the DEX settlement precompile bridges into the
// EVM registry as AlwaysOn, is enumerated by AlwaysOnModules(), and carries the protocol
// ActivationTime (Dec 2025) — the value the whole activation contract keys on.
func TestAlwaysOn_9999_Registered(t *testing.T) {
m, ok := modules.GetPrecompileModuleByAddress(settleAddr9999)
require.True(t, ok, "0x9999 must be registered in the EVM precompile registry")
require.True(t, m.AlwaysOn, "0x9999 (%s) must be AlwaysOn — it is the system money-path precompile", m.ConfigKey)
require.Equal(t, activation, m.ActivationTime,
"0x9999 must carry the DEX settlement protocol ActivationTime, not activate at genesis")
require.NotZero(t, activation, "the DEX settlement activation must be a real (post-genesis) timestamp")
found := false
for _, am := range modules.AlwaysOnModules() {
if am.Address == settleAddr9999 {
found = true
}
}
require.True(t, found, "0x9999 must be enumerated by modules.AlwaysOnModules()")
}
// TestDexSettle_DispatchGatedByActivation asserts 0x9999 is ABSENT from the enabled
// precompile set before its activation timestamp and PRESENT at/after it, on a chain
// whose config carries NO precompile activation whatsoever (the AlwaysOn mechanism, not
// a genesis config, drives this).
func TestDexSettle_DispatchGatedByActivation(t *testing.T) {
cfg := params.WithExtra(&params.ChainConfig{}, &extras.ChainConfig{})
absentAt := []uint64{0, 1, activation - 1}
for _, ts := range absentAt {
rules := params.GetExtrasRules(params.Rules{}, cfg, ts)
_, ok := rules.Precompiles[settleAddr9999]
require.Falsef(t, ok, "0x9999 must be ABSENT at ts=%d (< activation %d)", ts, activation)
}
presentAt := []uint64{activation, activation + 1, ^uint64(0)}
for _, ts := range presentAt {
rules := params.GetExtrasRules(params.Rules{}, cfg, ts)
_, ok := rules.Precompiles[settleAddr9999]
require.Truef(t, ok, "0x9999 must be PRESENT at ts=%d (>= activation %d)", ts, activation)
}
}
// TestDexSettle_MarkerAtActivationCrossing asserts the EXTCODESIZE marker is installed
// exactly on the transition that crosses the activation timestamp, and nowhere else.
func TestDexSettle_MarkerAtActivationCrossing(t *testing.T) {
newState := func() *state.StateDB {
db := rawdb.NewMemoryDatabase()
sdb, err := state.New(common.Hash{}, state.NewDatabase(db), nil)
require.NoError(t, err)
return sdb
}
cfg := &params.ChainConfig{}
hasMarker := func(sdb *state.StateDB) bool {
return sdb.GetNonce(settleAddr9999) == 1 && len(sdb.GetCode(settleAddr9999)) > 0
}
// Pre-activation genesis (parent==nil, ts < activation): NO marker — preserves the
// original genesis hash of every pre-2025 chain.
sdb := newState()
require.NoError(t, ApplyPrecompileActivations(cfg, nil, NewBlockContext(big.NewInt(0), activation-1), sdb))
require.False(t, hasMarker(sdb), "no marker at a pre-activation genesis")
// Post-activation genesis (parent==nil, ts >= activation): marker present (a fresh
// chain that wants 0x9999 at genesis).
sdb = newState()
require.NoError(t, ApplyPrecompileActivations(cfg, nil, NewBlockContext(big.NewInt(0), activation), sdb))
require.True(t, hasMarker(sdb), "marker at a post-activation genesis")
// Pre-activation block during replay (parent < block < activation): still no marker.
sdb = newState()
parent := activation - 100
require.NoError(t, ApplyPrecompileActivations(cfg, &parent, NewBlockContext(big.NewInt(10), activation-1), sdb))
require.False(t, hasMarker(sdb), "no marker before the crossing block")
// The crossing block (parent < activation <= block): marker installed here.
sdb = newState()
parent = activation - 1
require.NoError(t, ApplyPrecompileActivations(cfg, &parent, NewBlockContext(big.NewInt(11), activation), sdb))
require.True(t, hasMarker(sdb), "marker installed on the activation-crossing block")
// A later block wholly after activation (parent >= activation): this transition does
// NOT re-write the marker (in production it is already in committed state).
sdb = newState()
parent = activation
require.NoError(t, ApplyPrecompileActivations(cfg, &parent, NewBlockContext(big.NewInt(12), activation+100), sdb))
require.False(t, hasMarker(sdb), "the marker is written only on the crossing transition, not re-written after")
}
// TestDexSettle_GenesisBuilders_Skip9999 is the AUTHORITATIVE INFO1 regression: with the
// registry bridge imported (so 0x9999 is a live AlwaysOn module), the genesis-config
// builders must NOT emit a genesis config for 0x9999. Its activation is the AlwaysOn
// protocol mechanism (timestamp-gated dispatch + marker), never a genesis config — one
// and only one activation path.
func TestDexSettle_GenesisBuilders_Skip9999(t *testing.T) {
m, ok := modules.GetPrecompileModuleByAddress(settleAddr9999)
require.True(t, ok)
require.True(t, m.AlwaysOn)
fromFunc := extras.AllGenesisPrecompiles()
_, in := fromFunc[m.ConfigKey]
require.Falsef(t, in,
"AllGenesisPrecompiles must SKIP the AlwaysOn 0x9999 module (%s)", m.ConfigKey)
var cfg extras.ChainConfig
cfg.SetAllGenesisPrecompiles()
_, inMethod := cfg.GenesisPrecompiles[m.ConfigKey]
require.Falsef(t, inMethod,
"SetAllGenesisPrecompiles must SKIP the AlwaysOn 0x9999 module (%s)", m.ConfigKey)
}
// TestDexSettle_PrecompileOverride_GatedByActivation asserts the dispatch gate
// (LuxPrecompileOverrider.PrecompileOverride) returns the wrapped 0x9999 settlement
// contract at/after activation and ABSENT before it, bound to the overrider's OWN
// timestamp field. Rules evaluated at the other side of activation first do not move
// the answer.
func TestDexSettle_PrecompileOverride_GatedByActivation(t *testing.T) {
cfg := params.WithExtra(&params.ChainConfig{}, &extras.ChainConfig{})
// Evaluate post-activation Rules first — a pre-activation override must still
// answer ABSENT from its own field.
_ = cfg.Rules(new(big.Int), params.IsMergeTODO, activation+1_000_000)
for _, ts := range []uint64{0, 1, activation - 1} {
o := &LuxPrecompileOverrider{chainConfig: cfg, timestamp: ts}
_, ok := o.PrecompileOverride(settleAddr9999)
require.Falsef(t, ok, "0x9999 must NOT dispatch at ts=%d (< activation)", ts)
}
// Evaluate pre-activation Rules first — a post-activation override must still
// answer PRESENT from its own field.
_ = cfg.Rules(new(big.Int), params.IsMergeTODO, 0)
for _, ts := range []uint64{activation, activation + 1, ^uint64(0)} {
o := &LuxPrecompileOverrider{chainConfig: cfg, timestamp: ts}
c, ok := o.PrecompileOverride(settleAddr9999)
require.Truef(t, ok, "0x9999 must dispatch at ts=%d (>= activation)", ts)
require.NotNilf(t, c, "0x9999 override must return the wrapped settlement contract at ts=%d", ts)
}
}
// TestDexSettle_PrecompileOverride_NoGlobalRace is the -race regression for the
// activation model. It reproduces the relaunch scenario: a verify goroutine replaying a
// post-activation block concurrently with an eth_call/estimateGas/worker goroutine that
// evaluates Rules at other timestamps. Both overriders are pinned to their OWN
// post-activation timestamps, so both MUST deterministically see 0x9999 PRESENT on every
// iteration — presence cannot depend on what another goroutine evaluated last.
//
// Run with: go test -race -run NoGlobalRace ./core/
func TestDexSettle_PrecompileOverride_NoGlobalRace(t *testing.T) {
cfg := params.WithExtra(&params.ChainConfig{}, &extras.ChainConfig{})
a := &LuxPrecompileOverrider{chainConfig: cfg, timestamp: activation} // activation-block replay
b := &LuxPrecompileOverrider{chainConfig: cfg, timestamp: activation + 1<<30} // later block / eth_call
const iters = 2000
done := make(chan struct{})
go func() {
for {
select {
case <-done:
return
default:
_ = cfg.Rules(new(big.Int), params.IsMergeTODO, activation+1)
_ = cfg.Rules(new(big.Int), params.IsMergeTODO, 0) // pre-activation
}
}
}()
var wg sync.WaitGroup
wg.Add(2)
for _, o := range []*LuxPrecompileOverrider{a, b} {
o := o
go func() {
defer wg.Done()
for i := 0; i < iters; i++ {
if _, ok := o.PrecompileOverride(settleAddr9999); !ok {
t.Errorf("0x9999 override saw ABSENT at iter %d — a post-activation override must be "+
"present on every replay regardless of the Rules other goroutines evaluate", i)
return
}
}
}()
}
wg.Wait()
close(done)
}