Files
evm/params/config_extra.go
zeekayandHanzo Dev 7df2ba44c9 Each Rules carries the context its Lux rules follow from
GetRulesExtra derived a Rules' Lux rules from a process-wide last context:
the chain config and timestamp of whichever Rules any goroutine evaluated
most recently. Rules are evaluated concurrently, by block processing, the
miner, the tx pool and every eth_call, so a Rules could read another
evaluation's upgrades: its IsDurango, its predicaters, its enabled
precompiles, and the intrinsic gas that depends on them.

The context now travels with the Rules value. core's rules hook already put
the chain config and timestamp in Rules.Payload for its precompile
overrider; the overrider answers them, and GetRulesExtra reads them from
the Rules it is given. RulesAt attaches them where no hook did. The global
and SetRulesContext are gone. Every call site evaluates its Rules for the
block it reads them for, so a caller that no other goroutine races sees the
same rules as before. The blob pool's state checks passed a zero Rules and
read the head's context the check before them had left; they now pass the
head's Rules.

TestRulesExtraIsTheRulesOwn evaluates two rule sets and reads the first;
TestRulesExtraUnderConcurrentEvaluation reads four rule sets on four
goroutines at once, across two times and two chains, including the
intrinsic gas of a creation. TestActivePrecompiles, skipped for this bug,
runs again.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 11:44:42 -07:00

483 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
)
// rulesContext is what a Rules value carries in its Payload: the chain config
// and timestamp it was evaluated at, which is what its Lux rules follow from.
// core's rules hook gives every Rules that ChainConfig.Rules makes a Payload
// that answers it, so each Rules names its own and GetRulesExtra reads nothing
// that another evaluation can change.
type rulesContext interface {
RulesContext() (*ChainConfig, uint64)
}
// evaluatedAt is the rulesContext RulesAt attaches to a Rules that carries none:
// one made where core's rules hook is not linked.
type evaluatedAt struct {
config *ChainConfig
timestamp uint64
}
func (e evaluatedAt) RulesContext() (*ChainConfig, uint64) { return e.config, e.timestamp }
// 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)
}
// GetRulesExtra returns the RulesExtra for the given Rules: the Lux rules of
// the chain config and timestamp those Rules were evaluated at, which they
// carry in their Payload. Rules that carry none get the default RulesExtra.
func GetRulesExtra(rules Rules) RulesExtra {
var (
chainConfig *ChainConfig
timestamp uint64
)
if rc, ok := rules.Payload.(rulesContext); ok {
chainConfig, timestamp = rc.RulesContext()
}
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
}