mirror of
https://github.com/luxfi/evm.git
synced 2026-09-24 00:27:44 +00:00
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.
63 lines
2.9 KiB
Go
63 lines
2.9 KiB
Go
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package core
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/holiman/uint256"
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/luxfi/geth/core/tracing"
|
|
)
|
|
|
|
// precompile_statedb_subbalance_test.go pins the fix for a LATENT bug: the
|
|
// precompile registry's stateDBBridge.SubBalance type-asserts the internal
|
|
// contract.StateDB for the EXTERNAL SubBalance signature
|
|
//
|
|
// SubBalance(common.Address, *uint256.Int, tracing.BalanceChangeReason) uint256.Int
|
|
//
|
|
// 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
|
|
// the adapter's method ever drifts.
|
|
type subBalancer interface {
|
|
SubBalance(common.Address, *uint256.Int, tracing.BalanceChangeReason) uint256.Int
|
|
}
|
|
|
|
// TestStateDBAdapter_SatisfiesSubBalancer is the regression guard: *stateDBAdapter
|
|
// MUST satisfy the subBalancer interface the registry bridge type-asserts. If this
|
|
// fails to compile or asserts false, the bridge would hit its mint fallback for
|
|
// every precompile native-value debit (the DEX withdraw vault-release).
|
|
func TestStateDBAdapter_SatisfiesSubBalancer(t *testing.T) {
|
|
// Compile-time assertion (the primary guard against signature drift).
|
|
var _ subBalancer = (*stateDBAdapter)(nil)
|
|
|
|
// Runtime assertion mirroring exactly what the bridge does:
|
|
// if sb, ok := s.internal.(subBalancer); ok { return sb.SubBalance(...) }
|
|
var adapter contractStateDBSurface = (*stateDBAdapter)(nil)
|
|
if _, ok := interface{}(adapter).(subBalancer); !ok {
|
|
t.Fatal("*stateDBAdapter does NOT satisfy subBalancer — the registry bridge would hit the mint fallback for native-value debits")
|
|
}
|
|
// A non-nil *uint256.Int sanity reference (keeps the import meaningful and the
|
|
// signature exercised in source).
|
|
_ = uint256.NewInt(0)
|
|
}
|
|
|
|
// contractStateDBSurface is the minimal surface we hold the adapter as for the
|
|
// runtime assertion — it is satisfied by *stateDBAdapter (which implements the
|
|
// full internal contract.StateDB). Held as an interface value so the type
|
|
// assertion to subBalancer is a genuine runtime check, not a compile-time tautology.
|
|
type contractStateDBSurface interface {
|
|
GetBalance(common.Address) *uint256.Int
|
|
AddBalance(common.Address, *uint256.Int)
|
|
}
|