Files
evm/params/config_extra.go
zeekayandHanzo Dev d260656efc evm: gate DEX 0x9999 genesis injection on protocol ActivationTime (preserve pre-2025 genesis)
The AlwaysOn DEX settlement precompile (0x9999) had its EXTCODESIZE marker
baked into EVERY chain's genesis state unconditionally (parent==nil in
ApplyPrecompileActivations), mutating the genesis state root. This changed the
genesis hash of every chain genesised before the DEX activation, breaking RLP
re-import of the real archived Lux mainnet (0x3f4fa2a0), Lux testnet
(0x1c5fe377) and Zoo mainnet (0x7c548af4) — all re-genesised Nov 2024, before
the Dec-2025 activation.

Fix: an AlwaysOn system precompile carries a protocol ActivationTime (Dec 2025
for 0x9999, set by the registry bridge — a fork-like constant, not per-net
config). Its genesis marker is installed ONLY IF ActivationTime <= the genesis
timestamp; otherwise it activates later, on the block transition that crosses
ActivationTime during history replay. Dispatch (GetExtrasRules) is gated on the
same timestamp so a codeless address never dispatches. A chain born before
activation reproduces its ORIGINAL genesis byte-for-byte; a fresh chain born
at/after activation still gets 0x9999 at genesis.

Verified against the real RLP block-0 of all three chains (byte-identical
reproduction) plus a fresh post-activation chain. Corrects two pre-existing
tests that had hardcoded the buggy with-0x9999 hashes as "canonical".

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-02 16:50:11 -07:00

477 lines
15 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package params
import (
"encoding/json"
"errors"
"math/big"
"sync"
"github.com/luxfi/evm/params/extras"
"github.com/luxfi/evm/precompile/modules"
"github.com/luxfi/evm/precompile/precompileconfig"
"github.com/luxfi/evm/utils"
"github.com/luxfi/geth/common"
ethparams "github.com/luxfi/geth/params"
"github.com/luxfi/upgrade"
)
const (
maxJSONLen = 64 * 1024 * 1024 // 64MB
// IsMergeTODO is the default merge flag passed to geth's Rules when the
// calling context does not have an explicit merge status. Lux treats
// all chains as post-merge. (Analogous to context.TODO().)
IsMergeTODO = true
)
var (
DefaultChainID = big.NewInt(43214)
DefaultFeeConfig = extras.DefaultFeeConfig
// Use the constants from extras package
initiallyActive = uint64(extras.InitiallyActiveTime.Unix())
unscheduledActivation = uint64(extras.UnscheduledActivationTime.Unix())
// Simple map-based replacement for libevm payloads system
chainConfigExtras = make(map[*ChainConfig]*extras.ChainConfig)
chainConfigMutex sync.RWMutex
// Last Rules context - workaround since Rules is passed by value
// This assumes single-threaded test execution
lastRulesContext struct {
chainConfig *ChainConfig
timestamp uint64
}
lastRulesContextMutex sync.RWMutex
)
// getPrecompileAddress returns the address for a precompile config
func getPrecompileAddress(config precompileconfig.Config) common.Address {
// Get all registered modules
for _, module := range modules.RegisteredModules() {
// Match by config key
if module.ConfigKey == config.Key() {
return module.Address
}
}
return common.Address{}
}
// RulesExtra represents extra EVM rules - part of libevm integration
type RulesExtra struct {
IsEVM bool
IsDurango bool
IsQuasar bool
IsFortuna bool
IsGranite bool
// Fields for predicate support
PredicatersExist bool
Predicaters map[common.Address]precompileconfig.Predicater
// LuxRules for header verification
LuxRules extras.LuxRules
// Active precompiles for this rule set
activePrecompiles map[common.Address]bool
}
// IsPrecompileEnabled checks if a precompile is enabled
func (r RulesExtra) IsPrecompileEnabled(addr common.Address) bool {
if r.activePrecompiles == nil {
return false
}
return r.activePrecompiles[addr]
}
// SetEthUpgrades enables Ethereum network upgrades using the same time as
// the Lux network upgrade that enables them.
//
// Prior to Cancun, Lux upgrades are referenced inline in place of their
// Ethereum counterparts. The original Ethereum fork names should be restored
// for long-term maintainability.
func SetEthUpgrades(c *ChainConfig) error {
if c.HomesteadBlock == nil {
c.HomesteadBlock = big.NewInt(0)
}
if c.EIP150Block == nil {
c.EIP150Block = big.NewInt(0)
}
if c.EIP155Block == nil {
c.EIP155Block = big.NewInt(0)
}
if c.EIP158Block == nil {
c.EIP158Block = big.NewInt(0)
}
if c.ByzantiumBlock == nil {
c.ByzantiumBlock = big.NewInt(0)
}
if c.ConstantinopleBlock == nil {
c.ConstantinopleBlock = big.NewInt(0)
}
if c.PetersburgBlock == nil {
c.PetersburgBlock = big.NewInt(0)
}
if c.IstanbulBlock == nil {
c.IstanbulBlock = big.NewInt(0)
}
if c.MuirGlacierBlock == nil {
c.MuirGlacierBlock = big.NewInt(0)
}
if c.BerlinBlock == nil {
c.BerlinBlock = big.NewInt(0)
}
if c.LondonBlock == nil {
c.LondonBlock = big.NewInt(0)
}
extra := GetExtra(c)
// We only mark Eth upgrades as enabled if we have marked them as scheduled.
if durango := extra.DurangoTimestamp; durango != nil && *durango < unscheduledActivation {
c.ShanghaiTime = utils.NewUint64(*durango)
}
if quasar := extra.QuasarTimestamp; quasar != nil && *quasar < unscheduledActivation {
c.CancunTime = utils.NewUint64(*quasar)
// Cancun requires BlobScheduleConfig for CalcExcessBlobGas in block building.
// Without this, latestBlobConfig() returns nil causing a nil pointer panic.
if c.BlobScheduleConfig == nil {
c.BlobScheduleConfig = &ethparams.BlobScheduleConfig{
Cancun: ethparams.DefaultCancunBlobConfig,
}
} else if c.BlobScheduleConfig.Cancun == nil {
c.BlobScheduleConfig.Cancun = ethparams.DefaultCancunBlobConfig
}
}
return nil
}
func GetExtra(c *ChainConfig) *extras.ChainConfig {
chainConfigMutex.RLock()
ex, ok := chainConfigExtras[c]
chainConfigMutex.RUnlock()
if !ok || ex == nil {
chainConfigMutex.Lock()
// Double-check after acquiring write lock
ex, ok = chainConfigExtras[c]
if !ok || ex == nil {
ex = &extras.ChainConfig{}
chainConfigExtras[c] = ex
}
chainConfigMutex.Unlock()
}
return ex
}
// Copy creates a deep copy of the ChainConfig and its extras.
// Returns a pointer to ensure the extras mapping remains valid.
func Copy(c *ChainConfig) *ChainConfig {
cpy := *c
extraCpy := *GetExtra(c)
result := &cpy
WithExtra(result, &extraCpy)
return result
}
// WithExtra sets the extra payload on `c` and returns the modified argument.
func WithExtra(c *ChainConfig, extra *extras.ChainConfig) *ChainConfig {
chainConfigMutex.Lock()
chainConfigExtras[c] = extra
chainConfigMutex.Unlock()
return c
}
// SetExtra updates the ChainConfig extra payload
func SetExtra(c *ChainConfig, extra *extras.ChainConfig) {
chainConfigMutex.Lock()
chainConfigExtras[c] = extra
chainConfigMutex.Unlock()
}
type ChainConfigWithUpgradesJSON struct {
ChainConfig
UpgradeConfig extras.UpgradeConfig `json:"upgrades,omitempty"`
}
// MarshalJSON implements json.Marshaler. This is a workaround for the fact that
// the embedded ChainConfig struct has a MarshalJSON method, which prevents
// the default JSON marshalling from working for UpgradeConfig.
// This custom MarshalJSON exists because the embedded ChainConfig has its own
// MarshalJSON which shadows UpgradeConfig. An external JSON tag on the embedded
// struct would remove the need for this override.
func (cu *ChainConfigWithUpgradesJSON) MarshalJSON() ([]byte, error) {
// First get the extras
extra := GetExtra(&cu.ChainConfig)
// fmt.Printf("DEBUG ChainConfigWithUpgradesJSON.MarshalJSON: extra=%+v\n", extra)
// fmt.Printf("DEBUG extra.FeeConfig: %+v\n", extra.FeeConfig)
// Marshal the base ChainConfig
baseJSON, err := json.Marshal(&cu.ChainConfig)
if err != nil {
return nil, err
}
// Marshal the extras
extraJSON, err := extra.MarshalJSON()
if err != nil {
return nil, err
}
// Debug
// fmt.Printf("DEBUG: Base JSON: %s\n", string(baseJSON))
// fmt.Printf("DEBUG: Extra JSON: %s\n", string(extraJSON))
// Parse both as maps to merge
var baseMap map[string]json.RawMessage
if err := json.Unmarshal(baseJSON, &baseMap); err != nil {
return nil, err
}
var extraMap map[string]json.RawMessage
if err := json.Unmarshal(extraJSON, &extraMap); err != nil {
return nil, err
}
// Merge extras into base - overwrite any conflicting keys with extras values
for k, v := range extraMap {
baseMap[k] = v
}
// Now marshal the merged config
chainConfigJSON, err := json.Marshal(baseMap)
if err != nil {
return nil, err
}
if len(chainConfigJSON) > maxJSONLen {
return nil, errors.New("value too large")
}
type upgrades struct {
UpgradeConfig extras.UpgradeConfig `json:"upgrades"`
}
upgradeJSON, err := json.Marshal(upgrades{cu.UpgradeConfig})
if err != nil {
return nil, err
}
if len(upgradeJSON) > maxJSONLen {
return nil, errors.New("value too large")
}
// merge the two JSON objects
mergedJSON := make([]byte, 0, len(chainConfigJSON)+len(upgradeJSON)+1)
mergedJSON = append(mergedJSON, chainConfigJSON[:len(chainConfigJSON)-1]...)
mergedJSON = append(mergedJSON, ',')
mergedJSON = append(mergedJSON, upgradeJSON[1:]...)
return mergedJSON, nil
}
func (cu *ChainConfigWithUpgradesJSON) UnmarshalJSON(input []byte) error {
var cc ChainConfig
if err := json.Unmarshal(input, &cc); err != nil {
return err
}
type upgrades struct {
UpgradeConfig extras.UpgradeConfig `json:"upgrades"`
}
var u upgrades
if err := json.Unmarshal(input, &u); err != nil {
return err
}
cu.ChainConfig = cc
cu.UpgradeConfig = u.UpgradeConfig
return nil
}
// ToWithUpgradesJSON converts the ChainConfig to ChainConfigWithUpgradesJSON with upgrades explicitly displayed.
// ChainConfig does not include upgrades in its JSON output.
// This is a workaround for showing upgrades in the JSON output.
func ToWithUpgradesJSON(c *ChainConfig) *ChainConfigWithUpgradesJSON {
return &ChainConfigWithUpgradesJSON{
ChainConfig: *c,
UpgradeConfig: GetExtra(c).UpgradeConfig,
}
}
func SetNetworkUpgradeDefaults(c *ChainConfig) {
// NetworkUpgrades are not available via the consensus context; use empty defaults.
emptyUpgradeConfig := upgrade.Config{}
GetExtra(c).SetDefaults(emptyUpgradeConfig)
}
// SetRulesContext associates a Rules instance with its ChainConfig and timestamp
func SetRulesContext(r *Rules, c *ChainConfig, timestamp uint64) {
lastRulesContextMutex.Lock()
lastRulesContext.chainConfig = c
lastRulesContext.timestamp = timestamp
lastRulesContextMutex.Unlock()
}
// GetRulesExtra returns the RulesExtra for the given Rules
func GetRulesExtra(rules Rules) RulesExtra {
// Use the last stored context - this is a workaround since Rules is passed by value
lastRulesContextMutex.RLock()
chainConfig := lastRulesContext.chainConfig
timestamp := lastRulesContext.timestamp
lastRulesContextMutex.RUnlock()
if chainConfig == nil {
// No context found, return default RulesExtra
rulesExtra := RulesExtra{
IsEVM: true,
IsDurango: true,
IsQuasar: true,
IsFortuna: true,
IsGranite: true,
PredicatersExist: false,
Predicaters: make(map[common.Address]precompileconfig.Predicater),
}
rulesExtra.LuxRules = extras.LuxRules{
IsEVM: true,
IsDurango: true,
IsQuasar: true,
IsFortuna: true,
IsGranite: true,
}
rulesExtra.activePrecompiles = make(map[common.Address]bool)
return rulesExtra
}
// Get the extras rules for the chain config and timestamp
extrasRules := GetExtrasRules(rules, chainConfig, timestamp)
// Build RulesExtra from extras.Rules
rulesExtra := RulesExtra{
IsEVM: extrasRules.IsEVM,
IsDurango: extrasRules.IsDurango,
IsQuasar: extrasRules.IsQuasar,
IsFortuna: extrasRules.IsFortuna,
IsGranite: extrasRules.IsGranite,
PredicatersExist: len(extrasRules.Predicaters) > 0,
Predicaters: extrasRules.Predicaters,
LuxRules: extrasRules.LuxRules,
activePrecompiles: make(map[common.Address]bool),
}
// Populate active precompiles
for addr, config := range extrasRules.Precompiles {
if !config.IsDisabled() {
rulesExtra.activePrecompiles[addr] = true
}
}
return rulesExtra
}
// GetExtrasRules returns the extras.Rules for the given params.Rules and timestamp
func GetExtrasRules(ethRules Rules, c *ChainConfig, timestamp uint64) *extras.Rules {
if c == nil {
return &extras.Rules{
LuxRules: extras.LuxRules{},
Precompiles: make(map[common.Address]precompileconfig.Config),
Predicaters: make(map[common.Address]precompileconfig.Predicater),
AccepterPrecompiles: make(map[common.Address]precompileconfig.Accepter),
}
}
extra := GetExtra(c)
luxRules := extra.GetLuxRules(timestamp)
// Build extras.Rules
rules := &extras.Rules{
LuxRules: luxRules,
Precompiles: make(map[common.Address]precompileconfig.Config),
Predicaters: make(map[common.Address]precompileconfig.Predicater),
AccepterPrecompiles: make(map[common.Address]precompileconfig.Accepter),
}
// Track which precompiles have been disabled by upgrades
disabledByUpgrade := make(map[string]bool)
// Add active precompiles based on upgrades
for _, upgrade := range extra.PrecompileUpgrades {
if upgrade.Timestamp() != nil && *upgrade.Timestamp() <= timestamp {
// Get address from the registry based on the config
address := getPrecompileAddress(upgrade.Config)
if address == (common.Address{}) {
continue // Skip if no address found
}
if upgrade.IsDisabled() {
delete(rules.Precompiles, address)
delete(rules.Predicaters, address)
delete(rules.AccepterPrecompiles, address)
// Track that this precompile was disabled
disabledByUpgrade[upgrade.Config.Key()] = true
} else {
rules.Precompiles[address] = upgrade.Config
if predicater, ok := upgrade.Config.(precompileconfig.Predicater); ok {
rules.Predicaters[address] = predicater
}
if accepter, ok := upgrade.Config.(precompileconfig.Accepter); ok {
rules.AccepterPrecompiles[address] = accepter
}
// If re-enabled, remove from disabled tracking
disabledByUpgrade[upgrade.Config.Key()] = false
}
}
}
// Add genesis precompiles if they are active at this timestamp
// A genesis precompile is active if its Timestamp() <= timestamp and it's not disabled
for key, config := range extra.GenesisPrecompiles {
if config.IsDisabled() {
continue
}
// Check if the precompile was disabled by an upgrade
if disabledByUpgrade[key] {
continue // Skip - an upgrade disabled this genesis precompile
}
// Check if the precompile is active at this timestamp
configTs := config.Timestamp()
if configTs == nil || *configTs > timestamp {
continue // Not yet active
}
// Get address from the key
module, ok := modules.GetPrecompileModule(key)
if !ok {
continue // Skip unknown precompiles
}
address := module.Address
rules.Precompiles[address] = config
if predicater, ok := config.(precompileconfig.Predicater); ok {
rules.Predicaters[address] = predicater
}
if accepter, ok := config.(precompileconfig.Accepter); ok {
rules.AccepterPrecompiles[address] = accepter
}
}
// System precompiles (the DEX settlement money path 0x9999) are AlwaysOn: a
// system precompile activated by the Lux protocol on every chain with no per-net
// config. They take no per-net parameters and resolve everything at runtime (consensus
// context / atomic state), so there is nothing to seed into genesisPrecompiles or
// precompileUpgrades. They enter the enabled dispatch set at their protocol
// ActivationTime (a fork-like timestamp constant), consistent with the EXTCODESIZE
// marker installed at the same crossing transition by ApplyPrecompileActivations: a
// precompile that is not yet present in state (EXTCODESIZE==0) must not dispatch, and
// one that is present must. This is the set PrecompileOverride consults; the decision
// is a pure function of the passed-in timestamp (no global read), so it is race-safe
// and identical on every replay of a given block.
for _, module := range modules.AlwaysOnModules() {
if module.ActivationTime > timestamp {
continue // not yet activated at this timestamp
}
rules.Precompiles[module.Address] = module.Configurator.MakeGenesisConfig()
}
return rules
}