mirror of
https://github.com/luxfi/evm.git
synced 2026-09-24 00:27:44 +00:00
Replace the v1.99.32 "always-on, dispatch-only, no marker" activation of the
0x9999 DEX settlement money path with the CTO's exact model: a SINGLE canonical
dated fork that both enables dispatch AND installs the EXTCODESIZE marker —
forward, at the fork boundary, never in historical genesis.
The fork: extras.DexSettleActivationTime = 1766704800 (Dec 25 2025 00:00:00 UTC),
defined ONCE in params/extras/network_upgrades.go alongside the Durango/Quasar/
Fortuna/Granite built-in upgrades. NOT per-net config (no per-net upgrade.json
precompileUpgrades entry, no genesisPrecompiles entry) — one dated fork, identical
on every Lux network, DRY. This is the proper canonical fork mechanism for a
system precompile that takes no per-net parameters (0x9999 resolves protocolFee-
Controller=DefaultDAOTreasury and dChainID at runtime from the consensus context).
Two coupled effects, both gated on that one timestamp:
1. DISPATCH (params/config_extra.go GetExtrasRules): the AlwaysOnModules() inject
into the enabled precompile set is now wrapped in extras.IsDexSettleActive(ts)
— present at/after Dec 25, ABSENT before. This is REQUIRED for canonical
history: replaying a pre-Dec-25 block (the ~/work/lux/state RLP snapshot, e.g.
lux-mainnet 96369 = 1,082,780 blocks via admin.importChain) must see 0x9999 as
a PLAIN ACCOUNT so a historical value transfer to that address behaves as it
did originally, not as a precompile dispatch. The prior unconditional inject
would have diverged replay from canonical state.
2. MARKER (core/state_processor_ext.go ApplyPrecompileActivations): on the block
transition that CROSSES the fork (params.IsDexSettleForkTransition →
extras.IsForkTransition), write the STANDARD precompile-activation marker
(SetNonce=1 + SetCode{0x1}) into 0x9999 — identical to the config-driven path.
This is the forward state upgrade: EXTCODESIZE(0x9999)>0, eth_getCode!=0x, and
Solidity's contract-existence guard passes for a typed IPoolManager(0x9999)
.swap(...). The prior change DROPPED this marker to avoid mutating genesis;
installing it forward at the fork preserves the genesis hash (pre-activation
blocks get NO marker) while making typed calls work post-activation. Reverts
the v1.99.32 "writes NO genesis state for always-on modules" skip.
IsForkTransition fires exactly once (!parentForked && currentForked). For a fresh
net whose genesis timestamp is already >= the fork (parent==nil), it fires at
genesis — so fresh/local/new nets carry the marker from block 0 via the SAME
mechanism, no separate genesisPrecompiles entry (CTO step 4). For an existing net
(genesis ts < fork) it does NOT fire at genesis — genesis hash immutable — and
fires forward at the Dec 25 block.
KEPT byte-for-byte: runtime DChainID via BCLookup.Lookup("D"), DefaultDAOTreasury
protocolFeeController, networkID/cChainID from runtime context, AlwaysOn module
tagging + AlwaysOnModules(), all settlement/money-path logic.
0x9010: with precompile v0.5.53 the 0x9010 module is no longer registered, so the
EVM never dispatches it and eth_getCode(0x9010)=0x. registry comment updated to
LP-9999. No EVM-side 0x9010 dispatch reference existed.
deps: precompile v0.5.52 -> v0.5.53.
Tests (core/precompile_alwayson_test.go, rewritten — DRY, replaces the prior
unconditional/no-marker assertions):
- TestDexSettle_DispatchGate_TimestampGated: 0x9999 absent pre-fork, present
at/after (replay safety + post-activation dispatch).
- TestDexSettle_NoGenesisMarker_ExistingNet: genesis (ts<fork) writes no marker
(genesis hash immutable).
- TestDexSettle_MarkerInstalledAtForkTransition: marker (nonce=1 + code, EXTCODESIZE>0)
installed at the boundary block.
- TestDexSettle_ForkTransitionFiresOnce: fires once; fresh-net genesis>=fork at
block 0; existing-net genesis<fork not at genesis.
- TestAlwaysOn_9999_Registered: 0x9999 bridged AlwaysOn.
core + params/extras green incl. the real-mainnet genesis-hash guards. (Two
pre-existing unrelated failures on pristine main: params/config_test.go references
a removed DurangoTimestamp field; params/extras mainnet upgrade.json ringConfig/
warpConfig timestamp guards — both fail identically with these changes stashed.)
156 lines
8.1 KiB
Go
156 lines
8.1 KiB
Go
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package core
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/big"
|
|
|
|
"github.com/luxfi/evm/core/extstate"
|
|
"github.com/luxfi/evm/core/state"
|
|
"github.com/luxfi/evm/params"
|
|
"github.com/luxfi/evm/precompile/contract"
|
|
"github.com/luxfi/evm/precompile/modules"
|
|
"github.com/luxfi/evm/stateupgrade"
|
|
"github.com/luxfi/geth/core/tracing"
|
|
log "github.com/luxfi/log"
|
|
)
|
|
|
|
// ApplyPrecompileActivations checks if any of the precompiles specified by the chain config are enabled or disabled by the block
|
|
// transition from `parentTimestamp` to the timestamp set in `blockContext`. If this is the case, it calls [modules.Module]'s Configure
|
|
// to apply the necessary state transitions for the upgrade.
|
|
//
|
|
// At genesis (parentTimestamp == nil), this activates any precompiles configured at timestamp 0.
|
|
// The precompile configs are part of the chain config, so genesis state remains deterministic.
|
|
//
|
|
// In block processing and building, [ApplyUpgrades] is called instead which also applies state upgrades.
|
|
func ApplyPrecompileActivations(c *params.ChainConfig, parentTimestamp *uint64, blockContext contract.ConfigurationBlockContext, statedb *state.StateDB) error {
|
|
blockTimestamp := blockContext.Timestamp()
|
|
|
|
// System precompiles (the DEX settlement money path 0x9999) activate at a single
|
|
// canonical dated fork — extras.DexSettleActivationTime (Dec 25 2025) — installed
|
|
// here as a forward state transition, NOT in historical genesis. On the block
|
|
// transition that CROSSES that timestamp we write the standard precompile-activation
|
|
// marker (nonce=1 + a non-empty code byte) into the module's account so EXTCODESIZE>0,
|
|
// eth_getCode!=0x, and Solidity's contract-existence guard passes for a typed call.
|
|
//
|
|
// Why a forward fork and not a genesis marker: the genesis state root is consensus-
|
|
// critical and immutable for every existing network. Writing the marker at genesis
|
|
// would change the genesis hash and fork pre-activation history (a node replaying the
|
|
// RLP snapshot would compute a hash that disagrees with the committed genesis). So
|
|
// pre-activation blocks get NO marker (0x9999 is a plain account; replay stays
|
|
// canonical) and the marker appears exactly at the Dec 25 boundary going forward.
|
|
//
|
|
// IsForkTransition(fork, parent, current) fires once, when !parentForked && currentForked.
|
|
// For a freshly-genesised network whose genesis timestamp is already >= the fork
|
|
// (parent==nil), it fires at genesis — so fresh nets carry the marker from block 0 via
|
|
// the SAME mechanism (no separate genesisPrecompiles entry). The module's Configurator
|
|
// is NOT invoked (a system precompile has no activating config; all params resolve at
|
|
// runtime from the consensus context); only the EXTCODESIZE marker is installed.
|
|
if params.IsDexSettleForkTransition(parentTimestamp, blockTimestamp) {
|
|
for _, module := range modules.AlwaysOnModules() {
|
|
// Standard precompile-activation marker (identical to the config-driven path
|
|
// below): a non-empty nonce + code so the account is non-empty (not GC'd on
|
|
// finalize) and callable from Solidity.
|
|
statedb.SetNonce(module.Address, 1, tracing.NonceChangeUnspecified)
|
|
_ = statedb.SetCode(module.Address, []byte{0x1}, tracing.CodeChangeUnspecified)
|
|
}
|
|
}
|
|
|
|
// Note: [modules.RegisteredModules] returns precompiles sorted by module addresses.
|
|
// This ensures:
|
|
// - the order we call [modules.Module]'s Configure for each precompile is consistent
|
|
// - even if precompiles read/write state other than their own they will observe
|
|
// an identical global state in a deterministic order when they are configured.
|
|
extra := params.GetExtra(c)
|
|
for _, module := range modules.RegisteredModules() {
|
|
for _, activatingConfig := range extra.GetActivatingPrecompileConfigs(module.Address, parentTimestamp, blockTimestamp, extra.PrecompileUpgrades) {
|
|
// If this transition activates the upgrade, configure the stateful precompile.
|
|
// (or deconfigure it if it is being disabled.)
|
|
if activatingConfig.IsDisabled() {
|
|
log.Info("Disabling precompile", "name", module.ConfigKey)
|
|
statedb.SelfDestruct(module.Address)
|
|
// Calling [state.StateDB]'s Finalise here effectively commits the SelfDestruct call and wipes the contract state.
|
|
// This enables re-configuration of the same contract state in the same block.
|
|
// Without an immediate Finalise call after the SelfDestruct, a reconfigured precompiled state can be wiped out
|
|
// since SelfDestruct will be committed after the reconfiguration.
|
|
statedb.Finalise(true)
|
|
continue
|
|
}
|
|
var printIntf interface{}
|
|
marshalled, err := json.Marshal(activatingConfig)
|
|
if err == nil {
|
|
printIntf = string(marshalled)
|
|
} else {
|
|
printIntf = activatingConfig
|
|
}
|
|
|
|
log.Info("Activating new precompile", "name", module.ConfigKey, "config", printIntf)
|
|
// Set the nonce of the precompile's address (as is done when a contract is created) to ensure
|
|
// that it is marked as non-empty and will not be cleaned up when the statedb is finalized.
|
|
statedb.SetNonce(module.Address, 1, tracing.NonceChangeUnspecified)
|
|
// Set the code of the precompile's address to a non-zero length byte slice to ensure that the precompile
|
|
// can be called from within Solidity contracts. Solidity adds a check before invoking a contract to ensure
|
|
// that it does not attempt to invoke a non-existent contract.
|
|
_ = statedb.SetCode(module.Address, []byte{0x1}, tracing.CodeChangeUnspecified)
|
|
extstatedb := extstate.New(statedb)
|
|
adapter := extstate.NewPrecompileAdapter(extstatedb)
|
|
if err := module.Configure(params.GetExtra(c), activatingConfig, adapter, blockContext); err != nil {
|
|
return fmt.Errorf("could not configure precompile, name: %s, reason: %w", module.ConfigKey, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// applyStateUpgrades checks if any of the state upgrades specified by the chain config are activated by the block
|
|
// transition from [parentTimestamp] to the timestamp set in [header]. If this is the case, it calls [Configure]
|
|
// to apply the necessary state transitions for the upgrade.
|
|
func applyStateUpgrades(c *params.ChainConfig, parentTimestamp *uint64, blockContext contract.ConfigurationBlockContext, statedb *state.StateDB) error {
|
|
// Apply state upgrades
|
|
configExtra := params.GetExtra(c)
|
|
for _, upgrade := range configExtra.GetActivatingStateUpgrades(parentTimestamp, blockContext.Timestamp(), configExtra.StateUpgrades) {
|
|
log.Info("Applying state upgrade", "blockNumber", blockContext.Number(), "upgrade", upgrade)
|
|
adapter := state.NewStateUpgradeAdapter(statedb)
|
|
if err := stateupgrade.Configure(&upgrade, c, adapter, blockContext); err != nil {
|
|
return fmt.Errorf("could not configure state upgrade: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ApplyUpgrades checks if any of the precompile or state upgrades specified by the chain config are activated by the block
|
|
// transition from [parentTimestamp] to the timestamp set in [header]. If this is the case, it calls [Configure]
|
|
// to apply the necessary state transitions for the upgrade.
|
|
// This function is called:
|
|
// - in block processing to update the state when processing a block.
|
|
// - in the miner to apply the state upgrades when producing a block.
|
|
func ApplyUpgrades(c *params.ChainConfig, parentTimestamp *uint64, blockContext contract.ConfigurationBlockContext, statedb *state.StateDB) error {
|
|
if err := ApplyPrecompileActivations(c, parentTimestamp, blockContext, statedb); err != nil {
|
|
return err
|
|
}
|
|
return applyStateUpgrades(c, parentTimestamp, blockContext, statedb)
|
|
}
|
|
|
|
// BlockContext implements [contract.ConfigurationBlockContext].
|
|
type BlockContext struct {
|
|
number *big.Int
|
|
timestamp uint64
|
|
}
|
|
|
|
// NewBlockContext creates a [BlockContext] using the block number
|
|
// and block timestamp provided. This function is usually necessary to convert
|
|
// a `*types.Block` to be passed as a [contract.ConfigurationBlockContext]
|
|
// to [ApplyUpgrades].
|
|
func NewBlockContext(number *big.Int, timestamp uint64) *BlockContext {
|
|
return &BlockContext{
|
|
number: number,
|
|
timestamp: timestamp,
|
|
}
|
|
}
|
|
|
|
func (bc *BlockContext) Number() *big.Int { return bc.number }
|
|
func (bc *BlockContext) Timestamp() uint64 { return bc.timestamp }
|