mirror of
https://github.com/luxfi/evm.git
synced 2026-09-24 00:27:44 +00:00
evm: fix 0x9999 dispatch consensus-divergence on replay path + harden value/genesis edges (v1.99.34)
Red review of the 0x9999 dated-fork activation (v1.99.33) found ONE HIGH that
blocked the production deploy, plus a money-path footgun and two guards. The
money path (V4 swap ABI, EXTCODESIZE marker, two-phase atomic settle) is
byte-for-byte unchanged — only the dispatch-path timestamp source, the SubBalance
fail-mode, the genesis-builder guard, and stale comments change. Bumps
precompile v0.5.53 -> v0.5.54 (Red L1 comment scrub).
H1 (HIGH, blocked deploy) — consensus-divergence on the relaunch/replay path:
core.LuxPrecompileOverrider.PrecompileOverride read the PROCESS-GLOBAL
params.lastRulesContext via GetRulesExtra(Rules{}). That global is rewritten
last-writer-wins by SetRulesContext on every Rules()/RulesAt() — including
concurrent eth_call/estimateGas/miner goroutines. The geth EVM invokes
PrecompileOverride LAZILY during opcode execution, a different instant than when
precompileHook set the global. On a live, RPC-serving post-fork node replaying a
PRE-fork RLP block (admin.importChain), a concurrent post-fork eth_call could
overwrite the global timestamp between the verify goroutine's NewEVM(pre-fork
block) and its tx dispatch -> the pre-fork block sees 0x9999 ENABLED and
dispatches SettleContract.Run during plain-account execution -> STATE DIVERGENCE.
Fix: the dispatch gate now decides from the overrider's OWN per-EVM fields
(o.chainConfig + o.timestamp) via the pure params.GetExtrasRules — which performs
the SAME dated-fork (extras.IsDexSettleActive) computation GetRulesExtra did,
minus the racy global read. Audited every GetRulesExtra reader: only the overrider
was on the precompile-dispatch path; no other dispatch/consensus-replay call site
consults the 0x9999 gate. Every replay of a given block now yields the same
enabled set on every validator, independent of concurrent activity.
L2 (money-path footgun) — registry stateDBBridge.SubBalance fail-open silent mint:
when the subBalancer type-assertion failed it logged + returned a zero 'previous
balance' WITHOUT subtracting, while the caller treats the debit as successful ->
native value minted. Now FAILS CLOSED: it panics, which the EVM recovers into a
reverted call, so the enclosing value move rolls back rather than committing
half-done. Structurally unreachable in prod (the concrete adapter implements
SubBalance) but no longer mints if a future StateDB omits it.
INFO1 (latent footgun) — genesis-config builders bypassed the dated fork:
SetAllGenesisPrecompiles/AllGenesisPrecompiles iterated ALL RegisteredModules()
incl the AlwaysOn 0x9999 and wrote timestamp-0 genesis configs, which would enable
0x9999 from block 0 on a fresh chain — bypassing the dated fork and re-introducing
the genesis-marker mutation. Both builders now skip AlwaysOn modules.
INFO2 (tests):
- core: -race regression TestDexSettle_PrecompileOverride_NoGlobalRace reproduces
the exact relaunch race (pre-fork verify vs concurrent post-fork eth_call
clobbering the global) and asserts the pre-fork side ALWAYS sees 0x9999 ABSENT,
post-fork ALWAYS PRESENT. PerEVMTimestamp pins the gate reads o.timestamp not the
global. Replay e2e asserts a pre-fork value transfer to 0x9999 credits a PLAIN
account WITHOUT invoking the precompile. All proven to FAIL under the old
global-read code and PASS under the fix; -race clean.
- extras + core: TestGenesisPrecompileBuilders_SkipAlwaysOn / _Skip9999 pin INFO1.
- subbalance test comment refreshed to 0x9999 + fail-closed semantics.
This commit is contained in:
@@ -5,8 +5,10 @@ package core
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/holiman/uint256"
|
||||
"github.com/luxfi/evm/core/state"
|
||||
"github.com/luxfi/evm/params"
|
||||
"github.com/luxfi/evm/params/extras"
|
||||
@@ -16,6 +18,7 @@ import (
|
||||
_ "github.com/luxfi/evm/precompile/registry"
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/geth/core/rawdb"
|
||||
"github.com/luxfi/geth/core/tracing"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -147,4 +150,176 @@ func TestDexSettle_ForkTransitionFiresOnce(t *testing.T) {
|
||||
"an existing net whose genesis ts < the fork must NOT install the marker at genesis")
|
||||
}
|
||||
|
||||
// TestDexSettle_GenesisBuilders_Skip9999 is the AUTHORITATIVE INFO1 regression: with the
|
||||
// registry bridge imported (so 0x9999 is a live AlwaysOn module in the EVM registry), the
|
||||
// genesis-config builders must NOT emit a timestamp-0 genesis config for 0x9999. If they
|
||||
// did, a fresh chain built via SetAllGenesisPrecompiles/AllGenesisPrecompiles would enable
|
||||
// 0x9999 from block 0 — bypassing the Dec 25 2025 dated fork and re-introducing the
|
||||
// genesis-marker mutation the fork exists to avoid. (The extras-package guard test runs
|
||||
// without the registry import, so 0x9999 is invisible there; THIS test sees it.)
|
||||
func TestDexSettle_GenesisBuilders_Skip9999(t *testing.T) {
|
||||
// Precondition: 0x9999 really is registered as AlwaysOn in this package's module view.
|
||||
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) — its activation is the "+
|
||||
"dated fork, not a genesis config; a genesis entry would bypass the gate", 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_PerEVMTimestamp pins the H1 fix: the dispatch gate
|
||||
// (LuxPrecompileOverrider.PrecompileOverride) must decide 0x9999's enabled-ness from the
|
||||
// overrider's OWN per-EVM timestamp (o.timestamp), NOT from the process-global
|
||||
// lastRulesContext. We POISON the global with the opposite timestamp first, then assert
|
||||
// the override still answers from its own field. Under the pre-fix code (which read
|
||||
// params.GetRulesExtra(Rules{}) → the global), a pre-fork EVM would wrongly see 0x9999
|
||||
// ENABLED whenever any concurrent caller had stamped a post-fork timestamp into the
|
||||
// global — a consensus divergence on the relaunch/replay path.
|
||||
func TestDexSettle_PrecompileOverride_PerEVMTimestamp(t *testing.T) {
|
||||
cfg := params.WithExtra(¶ms.ChainConfig{}, &extras.ChainConfig{})
|
||||
|
||||
// Poison the global with a POST-fork timestamp; a correct pre-fork override ignores it.
|
||||
params.SetRulesContext(¶ms.Rules{}, cfg, tsPostActivation)
|
||||
pre := &LuxPrecompileOverrider{chainConfig: cfg, timestamp: tsPreActivation}
|
||||
_, okPre := pre.PrecompileOverride(settleAddr9999)
|
||||
require.False(t, okPre,
|
||||
"pre-fork EVM must see 0x9999 ABSENT even when the global holds a post-fork timestamp "+
|
||||
"(dispatch gate must read o.timestamp, not lastRulesContext)")
|
||||
|
||||
// Poison the global with a PRE-fork timestamp; a correct post-fork override ignores it.
|
||||
params.SetRulesContext(¶ms.Rules{}, cfg, tsPreActivation)
|
||||
post := &LuxPrecompileOverrider{chainConfig: cfg, timestamp: tsPostActivation}
|
||||
c, okPost := post.PrecompileOverride(settleAddr9999)
|
||||
require.True(t, okPost,
|
||||
"post-fork EVM must see 0x9999 PRESENT even when the global holds a pre-fork timestamp")
|
||||
require.NotNil(t, c, "post-fork override must return the wrapped 0x9999 contract")
|
||||
}
|
||||
|
||||
// TestDexSettle_PrecompileOverride_NoGlobalRace is the -race regression test for H1. It
|
||||
// reproduces the exact relaunch scenario: a verify goroutine replaying a PRE-fork block
|
||||
// concurrently with a post-fork eth_call/estimateGas/worker goroutine that rewrites the
|
||||
// last-writer-wins global timestamp. The pre-fork side MUST deterministically see 0x9999
|
||||
// ABSENT (plain account, no SettleContract.Run) on every iteration, and the post-fork
|
||||
// side MUST deterministically see it PRESENT — regardless of who wrote the global last.
|
||||
//
|
||||
// Run with: go test -race -run NoGlobalRace ./core/
|
||||
// Under the pre-fix code this fails the assertion (and/or -race flags the unsynchronised
|
||||
// last-writer-wins read driving a consensus-relevant branch). Under the fix it is
|
||||
// deterministic because each override reads only its own immutable fields.
|
||||
func TestDexSettle_PrecompileOverride_NoGlobalRace(t *testing.T) {
|
||||
cfg := params.WithExtra(¶ms.ChainConfig{}, &extras.ChainConfig{})
|
||||
|
||||
pre := &LuxPrecompileOverrider{chainConfig: cfg, timestamp: tsPreActivation}
|
||||
post := &LuxPrecompileOverrider{chainConfig: cfg, timestamp: tsPostActivation}
|
||||
|
||||
const iters = 2000
|
||||
done := make(chan struct{})
|
||||
|
||||
// Adversary: continuously clobber the global with alternating timestamps, mimicking
|
||||
// concurrent eth_call (wall-clock, post-fork) and any pre-fork rules construction. This
|
||||
// is precisely what SetRulesContext does on every Rules()/RulesAt() across goroutines.
|
||||
go func() {
|
||||
r := ¶ms.Rules{}
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
params.SetRulesContext(r, cfg, tsPostActivation)
|
||||
params.SetRulesContext(r, cfg, tsPreActivation)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
// Pre-fork replay goroutine: 0x9999 must be ABSENT every single time.
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < iters; i++ {
|
||||
if _, ok := pre.PrecompileOverride(settleAddr9999); ok {
|
||||
t.Errorf("pre-fork override saw 0x9999 ENABLED at iter %d — consensus divergence: "+
|
||||
"a concurrent post-fork timestamp leaked through the global", i)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Post-fork goroutine: 0x9999 must be PRESENT every single time.
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < iters; i++ {
|
||||
if _, ok := post.PrecompileOverride(settleAddr9999); !ok {
|
||||
t.Errorf("post-fork override saw 0x9999 ABSENT at iter %d — a concurrent pre-fork "+
|
||||
"timestamp leaked through the global", i)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
close(done)
|
||||
}
|
||||
|
||||
// TestDexSettle_Replay_PreForkValueTransferCreditsPlainAccount is the replay e2e for
|
||||
// H1/INFO2: on the relaunch path, replaying a PRE-fork value transfer to 0x9999 must
|
||||
// credit a PLAIN account and must NOT invoke SettleContract.Run. geth's vm.Call routes
|
||||
// to the precompile iff PrecompileOverride returns (contract,true); when it returns
|
||||
// (nil,false), 0x9999 is an ordinary account and the EVM performs the balance transfer.
|
||||
// We drive that exact branch at the dispatch boundary (CGO-free — no full vm.NewEVM):
|
||||
//
|
||||
// - PRE-fork override ⇒ (nil,false) ⇒ we apply the plain-account transfer and assert the
|
||||
// 0x9999 account balance rose by the sent amount, while 0x9999 carries no activation
|
||||
// marker (nonce/code stay zero — it is a plain recipient, the precompile never ran).
|
||||
// - POST-fork override ⇒ (contract,true) ⇒ the EVM would dispatch the precompile instead.
|
||||
//
|
||||
// This is the canonical-history guarantee: a historical tx that paid native value into
|
||||
// 0x9999 before the fork replays as a credit, byte-identical to the committed state.
|
||||
func TestDexSettle_Replay_PreForkValueTransferCreditsPlainAccount(t *testing.T) {
|
||||
cfg := params.WithExtra(¶ms.ChainConfig{}, &extras.ChainConfig{})
|
||||
const sent uint64 = 7_500_000_000
|
||||
|
||||
// Poison the global to a post-fork wall-clock value — the live, RPC-serving post-fork
|
||||
// node's reality while it replays the imported pre-fork block. The pre-fork override
|
||||
// must IGNORE this and keep 0x9999 a plain account.
|
||||
params.SetRulesContext(¶ms.Rules{}, cfg, tsPostActivation)
|
||||
|
||||
db := rawdb.NewMemoryDatabase()
|
||||
statedb, err := state.New(common.Hash{}, state.NewDatabase(db), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
pre := &LuxPrecompileOverrider{chainConfig: cfg, timestamp: tsPreActivation}
|
||||
contract, dispatched := pre.PrecompileOverride(settleAddr9999)
|
||||
require.False(t, dispatched,
|
||||
"pre-fork replay must NOT dispatch the 0x9999 precompile — it is a plain account")
|
||||
require.Nil(t, contract)
|
||||
|
||||
// PrecompileOverride said "plain account", so the EVM credits the recipient. Mirror that.
|
||||
statedb.AddBalance(settleAddr9999, uint256.NewInt(sent), tracing.BalanceChangeTransfer)
|
||||
|
||||
require.Equal(t, uint256.NewInt(sent), statedb.GetBalance(settleAddr9999),
|
||||
"pre-fork value transfer to 0x9999 must credit the plain account by the sent amount")
|
||||
require.Zero(t, statedb.GetNonce(settleAddr9999),
|
||||
"replayed pre-fork credit must not touch 0x9999 nonce — the precompile never ran")
|
||||
require.Empty(t, statedb.GetCode(settleAddr9999),
|
||||
"replayed pre-fork credit must not install marker code — 0x9999 is a plain account pre-fork")
|
||||
|
||||
// Sanity: at/after the fork the SAME address DOES dispatch the precompile.
|
||||
post := &LuxPrecompileOverrider{chainConfig: cfg, timestamp: tsPostActivation}
|
||||
c2, dispatched2 := post.PrecompileOverride(settleAddr9999)
|
||||
require.True(t, dispatched2,
|
||||
"post-fork: 0x9999 must dispatch the settlement precompile, not behave as a plain account")
|
||||
require.NotNil(t, c2)
|
||||
}
|
||||
|
||||
func ptr(v uint64) *uint64 { return &v }
|
||||
|
||||
@@ -49,10 +49,30 @@ type LuxPrecompileOverrider struct {
|
||||
|
||||
// PrecompileOverride returns the precompile at the given address if it's
|
||||
// an active Lux custom precompile.
|
||||
//
|
||||
// The enabled-set decision is computed from THIS overrider's OWN per-EVM fields
|
||||
// (o.chainConfig + o.timestamp, set in precompileHook at the EVM's construction),
|
||||
// NOT from the process-global lastRulesContext that params.GetRulesExtra(Rules{})
|
||||
// would read. The geth EVM invokes PrecompileOverride lazily during opcode
|
||||
// execution (when a CALL targets a precompile address), which is a DIFFERENT
|
||||
// point in time than when precompileHook ran. Reading the last-writer-wins global
|
||||
// here is a consensus-divergence bug: on the relaunch path (admin.importChain of a
|
||||
// pre-fork RLP snapshot on a live, RPC-serving, post-fork node) a concurrent
|
||||
// eth_call/estimateGas/worker goroutine can rewrite the global timestamp to a
|
||||
// wall-clock (post-fork) value between this EVM's construction and its tx dispatch
|
||||
// — making the pre-fork block see the dated-fork 0x9999 precompile as ENABLED and
|
||||
// dispatch SettleContract.Run during what must be plain-account execution. Binding
|
||||
// the gate to o.timestamp makes every replay of a given block produce the SAME
|
||||
// enabled set on every validator, regardless of concurrent activity.
|
||||
//
|
||||
// params.GetExtrasRules is a pure function of its arguments (it does NOT read the
|
||||
// global); it is the SAME computation params.GetRulesExtra performs internally, so
|
||||
// the dated-fork 0x9999 injection (extras.IsDexSettleActive gate) is applied here
|
||||
// identically — minus the racy global read. params.ChainConfig is a type alias of
|
||||
// geth's ChainConfig, so o.chainConfig is passed directly.
|
||||
func (o *LuxPrecompileOverrider) PrecompileOverride(addr common.Address) (vm.PrecompiledContract, bool) {
|
||||
// Get the extras rules to check active precompiles
|
||||
rulesExtra := params.GetRulesExtra(gethparams.Rules{})
|
||||
if !rulesExtra.IsPrecompileEnabled(addr) {
|
||||
extrasRules := params.GetExtrasRules(gethparams.Rules{}, o.chainConfig, o.timestamp)
|
||||
if cfg, ok := extrasRules.Precompiles[addr]; !ok || cfg.IsDisabled() {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
|
||||
@@ -17,13 +17,14 @@ import (
|
||||
//
|
||||
// SubBalance(common.Address, *uint256.Int, tracing.BalanceChangeReason) uint256.Int
|
||||
//
|
||||
// and falls back to a NO-OP (logging "SubBalance fallback used") when the
|
||||
// assertion fails. The concrete adapter the EVM hands precompiles is
|
||||
// *stateDBAdapter; if it does not implement that EXACT signature, a precompile
|
||||
// that debits native value — e.g. the DEX 0x9010 custody vault, whose withdraw
|
||||
// debits the vault before releasing to the caller — would silently MINT (caller
|
||||
// credited, vault never debited). The fix added SubBalance to *stateDBAdapter so
|
||||
// the bridge's assertion succeeds and the fallback is unreachable.
|
||||
// and previously fell back to a NO-OP when the assertion failed. The concrete
|
||||
// adapter the EVM hands precompiles is *stateDBAdapter; if it does not implement
|
||||
// that EXACT signature, a precompile that debits native value — e.g. the DEX 0x9999
|
||||
// custody vault, whose withdraw debits the vault before releasing to the caller —
|
||||
// would silently MINT (caller credited, vault never debited). The fix added
|
||||
// SubBalance to *stateDBAdapter so the bridge's assertion succeeds and the fallback
|
||||
// is unreachable; the fallback itself now FAILS CLOSED (panics → reverted call)
|
||||
// rather than returning a zero "previous balance" without debiting.
|
||||
|
||||
// subBalancer is the EXACT interface precompile/registry/bridge.go type-asserts.
|
||||
// Keep it byte-identical so this test fails if the bridge's expected signature or
|
||||
|
||||
@@ -32,7 +32,7 @@ require (
|
||||
github.com/luxfi/metric v1.5.8
|
||||
github.com/luxfi/p2p v1.21.1
|
||||
github.com/luxfi/pq v1.0.3
|
||||
github.com/luxfi/precompile v0.5.53
|
||||
github.com/luxfi/precompile v0.5.54
|
||||
github.com/luxfi/runtime v1.1.1
|
||||
github.com/luxfi/sys v0.1.0
|
||||
github.com/luxfi/timer v1.0.2
|
||||
|
||||
@@ -280,6 +280,8 @@ github.com/luxfi/precompile v0.5.52 h1:1gw4iJEv5Tj8bBwNctlMympe3lIzhmFymgetKglqZ
|
||||
github.com/luxfi/precompile v0.5.52/go.mod h1:aR523IXj+VQAIU1CYP/64dVwKK93DDssHj1tGFdzLTY=
|
||||
github.com/luxfi/precompile v0.5.53 h1:R3WAgHYlk2rEasgFGzDRx6iS49Wn9ApxZUf6Kf2F0KI=
|
||||
github.com/luxfi/precompile v0.5.53/go.mod h1:aR523IXj+VQAIU1CYP/64dVwKK93DDssHj1tGFdzLTY=
|
||||
github.com/luxfi/precompile v0.5.54 h1:O/qQWg4CH5eLTOMxGGFTfZd9WhWY1ogUDGmigB6F0R8=
|
||||
github.com/luxfi/precompile v0.5.54/go.mod h1:aR523IXj+VQAIU1CYP/64dVwKK93DDssHj1tGFdzLTY=
|
||||
github.com/luxfi/proto v1.3.4 h1:dNrdQ/414Pe9otIekVUmQ9QLqemaMFK43H4N3DIbuzQ=
|
||||
github.com/luxfi/proto v1.3.4/go.mod h1:S80KrTzISCktptit/pWLzk6nHgagjJS86qbQL6gHutE=
|
||||
github.com/luxfi/resource v0.0.1 h1:mTh+ICWSy548GTUSSyx7V/X5dV18oEwxZeQEYGJQhD4=
|
||||
|
||||
@@ -354,9 +354,19 @@ func (c *ChainConfig) MarshalJSON() ([]byte, error) {
|
||||
// for all registered precompile modules (timestamp = 0). This ensures deterministic
|
||||
// genesis hash when "all precompiles active at genesis" is the intended state.
|
||||
// Call this when building genesis configs for new chains.
|
||||
//
|
||||
// AlwaysOn modules (e.g. the 0x9999 DEX settlement money path) are SKIPPED: their
|
||||
// activation is governed by a single built-in dated fork (extras.DexSettleActivationTime),
|
||||
// NOT by a genesisPrecompiles entry. Writing a timestamp-0 genesis config for them would
|
||||
// enable them from block 0 on every chain, bypassing the dated-fork gate and re-introducing
|
||||
// the genesis-marker mutation the dated fork exists to avoid. AlwaysOn modules carry no
|
||||
// per-net config, so there is nothing to seed into genesis regardless.
|
||||
func (c *ChainConfig) SetAllGenesisPrecompiles() {
|
||||
c.GenesisPrecompiles = make(Precompiles)
|
||||
for _, module := range modules.RegisteredModules() {
|
||||
if module.AlwaysOn {
|
||||
continue
|
||||
}
|
||||
c.GenesisPrecompiles[module.ConfigKey] = module.Configurator.MakeGenesisConfig()
|
||||
}
|
||||
}
|
||||
@@ -364,9 +374,15 @@ func (c *ChainConfig) SetAllGenesisPrecompiles() {
|
||||
// AllGenesisPrecompiles returns a new Precompiles map populated with default
|
||||
// genesis configs for all registered precompile modules (timestamp = 0).
|
||||
// This is the authoritative source for "all precompiles active at genesis".
|
||||
//
|
||||
// AlwaysOn modules are SKIPPED for the same reason as SetAllGenesisPrecompiles: a
|
||||
// dated-fork-gated system precompile must never receive a timestamp-0 genesis config.
|
||||
func AllGenesisPrecompiles() Precompiles {
|
||||
precompiles := make(Precompiles)
|
||||
for _, module := range modules.RegisteredModules() {
|
||||
if module.AlwaysOn {
|
||||
continue
|
||||
}
|
||||
precompiles[module.ConfigKey] = module.Configurator.MakeGenesisConfig().(precompileconfig.Config)
|
||||
}
|
||||
return precompiles
|
||||
|
||||
@@ -30,13 +30,48 @@ func TestAllGenesisPrecompilesDeterminism(t *testing.T) {
|
||||
require.Equal(t, uint64(0), *config1.Timestamp(), "genesis config timestamp should be 0")
|
||||
}
|
||||
|
||||
// Verify all registered modules are present
|
||||
// Verify all registered NON-AlwaysOn modules are present, and that NO AlwaysOn
|
||||
// module is present. AlwaysOn precompiles (e.g. the 0x9999 DEX settlement money
|
||||
// path) are activated by a built-in dated fork, NOT by a timestamp-0 genesis config;
|
||||
// emitting one would enable them from genesis on every chain and bypass the gate.
|
||||
for _, module := range modules.RegisteredModules() {
|
||||
_, ok := result1[module.ConfigKey]
|
||||
if module.AlwaysOn {
|
||||
require.Falsef(t, ok,
|
||||
"AlwaysOn module %s must NOT be in AllGenesisPrecompiles — its activation is the dated fork, not a genesis config",
|
||||
module.ConfigKey)
|
||||
continue
|
||||
}
|
||||
require.True(t, ok, "module %s should be in AllGenesisPrecompiles", module.ConfigKey)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGenesisPrecompileBuilders_SkipAlwaysOn pins the INFO1 guard: the genesis-config
|
||||
// builders (AllGenesisPrecompiles and ChainConfig.SetAllGenesisPrecompiles) must never
|
||||
// emit a timestamp-0 genesis config for an AlwaysOn module. Writing one would enable a
|
||||
// dated-fork-gated system precompile (0x9999) from block 0 on every chain — bypassing
|
||||
// the canonical activation boundary and re-introducing the genesis-marker mutation. The
|
||||
// invariant is enforced positively (skip any module with AlwaysOn) so it holds even if an
|
||||
// AlwaysOn module is later registered into this package's module view.
|
||||
func TestGenesisPrecompileBuilders_SkipAlwaysOn(t *testing.T) {
|
||||
fromFunc := AllGenesisPrecompiles()
|
||||
|
||||
var cfg ChainConfig
|
||||
cfg.SetAllGenesisPrecompiles()
|
||||
|
||||
for _, module := range modules.RegisteredModules() {
|
||||
if !module.AlwaysOn {
|
||||
continue
|
||||
}
|
||||
_, inFunc := fromFunc[module.ConfigKey]
|
||||
require.Falsef(t, inFunc,
|
||||
"AllGenesisPrecompiles must skip AlwaysOn module %s — dated-fork activation, not genesis", module.ConfigKey)
|
||||
_, inMethod := cfg.GenesisPrecompiles[module.ConfigKey]
|
||||
require.Falsef(t, inMethod,
|
||||
"SetAllGenesisPrecompiles must skip AlwaysOn module %s — dated-fork activation, not genesis", module.ConfigKey)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPrecompilesMarshalDeterministic verifies that MarshalJSONDeterministic
|
||||
// produces consistent output regardless of map iteration order.
|
||||
func TestPrecompilesMarshalDeterministic(t *testing.T) {
|
||||
|
||||
@@ -251,27 +251,28 @@ func (s *stateDBBridge) AddBalance(addr common.Address, amount *uint256.Int, _ t
|
||||
|
||||
func (s *stateDBBridge) SubBalance(addr common.Address, amount *uint256.Int, _ tracing.BalanceChangeReason) uint256.Int {
|
||||
// The internal contract.StateDB doesn't have SubBalance.
|
||||
// Try to reach the underlying vm.StateDB via type assertion.
|
||||
// Reach the underlying vm.StateDB via type assertion.
|
||||
type subBalancer interface {
|
||||
SubBalance(common.Address, *uint256.Int, tracing.BalanceChangeReason) uint256.Int
|
||||
}
|
||||
if sb, ok := s.internal.(subBalancer); ok {
|
||||
return sb.SubBalance(addr, amount, tracing.BalanceChangeUnspecified)
|
||||
}
|
||||
|
||||
// Fallback: use snapshot + manual balance calculation
|
||||
// Get current balance, compute new, use AddBalance with negative (not possible)
|
||||
// Instead, log warning. This path should not be hit in practice because the
|
||||
// concrete stateDBAdapter in precompile_overrider.go wraps vm.StateDB which has SubBalance.
|
||||
bal := s.internal.GetBalance(addr)
|
||||
if bal != nil && bal.Cmp(amount) >= 0 {
|
||||
// Workaround: We can't subtract directly, so we reconstruct via the underlying
|
||||
// types. In practice this codepath is unreachable because the internal StateDB
|
||||
// is always a stateDBAdapter{vm.StateDB} which implements SubBalance.
|
||||
log.Error("stateDBBridge: SubBalance fallback used — precompile state may be incorrect",
|
||||
sb, ok := s.internal.(subBalancer)
|
||||
if !ok {
|
||||
// FAIL CLOSED. There is NO safe fallback: the internal contract.StateDB has no
|
||||
// way to subtract, so silently returning a zero "previous balance" without
|
||||
// debiting (the old behaviour) would let a value-moving precompile — e.g. the
|
||||
// 0x9999 DEX custody vault on withdraw — treat the debit as successful while the
|
||||
// balance is untouched, MINTING native value. A failed debit on the money path
|
||||
// must abort the call, never proceed. We panic: the EVM recovers a precompile
|
||||
// panic into a reverted call, so the enclosing value move is rolled back rather
|
||||
// than committed half-done. In production the internal StateDB is always the
|
||||
// concrete stateDBAdapter{vm.StateDB} (precompile_overrider.go), which DOES
|
||||
// implement SubBalance, so this panic is structurally unreachable; it exists to
|
||||
// make any future StateDB that omits SubBalance fail loudly instead of minting.
|
||||
log.Error("stateDBBridge: SubBalance on a StateDB without subtraction support — failing closed to avoid minting native value",
|
||||
"address", addr, "amount", amount)
|
||||
panic(fmt.Sprintf("stateDBBridge: internal StateDB %T cannot SubBalance; refusing to credit without debiting (addr=%s amount=%s)", s.internal, addr, amount))
|
||||
}
|
||||
return uint256.Int{}
|
||||
return sb.SubBalance(addr, amount, tracing.BalanceChangeUnspecified)
|
||||
}
|
||||
|
||||
func (s *stateDBBridge) GetBalanceMultiCoin(addr common.Address, coinID common.Hash) *big.Int {
|
||||
|
||||
Reference in New Issue
Block a user