Files
evm/core/precompile_statedb_subbalance_test.go
zeekay 8e04c4e23f 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.
2026-06-21 12:42:42 -07:00

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)
}