Files
evm/params/extras/config.go
T
zeekayandHanzo Dev 1abb712fe8 core: C-Chain tx-fee disbursement seam — creditTxFee (50/50 split dormant)
Single fee-disbursement seam in state_transition.go delegates to creditTxFee
(core/fee_split.go): full-fee-to-coinbase legacy path by default, or a
deterministic 50%-true-burn + 50%-to-FeeRewardVault (0x0100..0002) split once
the standalone FeeSplitTimestamp fork is active. Gated OFF everywhere
(FeeSplitTimestamp nil) so the LIVE path stays RewardManager -> DAO Gov Safe;
this is the ready in-protocol-split option the DAO can enable later.

Proven (CGO off): fee_split_test (split math, conservation, odd-wei, determinism,
legacy), fee_split_block_test (end-to-end burn on accepted blocks),
reward_manager_routing_test (100% -> DAO Safe + live on-chain setRewardAddress
redirect). params/extras suite green incl. the 3 mainnet boot-compat guards.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-17 17:12:55 -07:00

549 lines
22 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package extras
import (
"context"
"encoding/json"
"fmt"
"math/big"
"time"
"github.com/luxfi/evm/commontype"
"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"
)
var (
// UnscheduledActivationTime represents an upgrade that has no scheduled activation.
UnscheduledActivationTime = time.Unix(1<<63-1, 0) // Max time value
InitiallyActiveTime = time.Unix(0, 0) // Unix epoch
// LegacyWarpAddress is the historical warp precompile address used in C-Chain
// and legacy Subnet-EVM chains. New chains should use the LP-aligned address (0x16201).
LegacyWarpAddress = common.HexToAddress("0x0200000000000000000000000000000000000005")
// FeeRewardVault is the canonical, protocol-owned C-Chain account that accrues
// the validator (staking-reward) half of every transaction fee once the FeeSplit
// upgrade is active. Its balance is periodically exported, atomically, to the
// P-Chain fee-reward pool and paid out through the native staking-reward path
// (see the "Tx-fee 50/50 split" design in LLM.md). It is distinct from the
// BlackholeAddr (0x0100..00) used by legacy full-fee routing: the vault holds
// real, tracked balance destined for validators, whereas the burn half is never
// credited to any account (a true supply reduction).
FeeRewardVault = common.HexToAddress("0x0100000000000000000000000000000000000002")
DefaultFeeConfig = commontype.FeeConfig{
GasLimit: big.NewInt(8_000_000),
TargetBlockRate: 2, // in seconds
MinBaseFee: big.NewInt(25_000_000_000),
TargetGas: big.NewInt(15_000_000),
BaseFeeChangeDenominator: big.NewInt(36),
MinBlockGasCost: big.NewInt(0),
MaxBlockGasCost: big.NewInt(1_000_000),
BlockGasCostStep: big.NewInt(200_000),
}
// TestFeeConfig uses minimal fees for backward compatibility with tests
TestFeeConfig = commontype.FeeConfig{
GasLimit: big.NewInt(8_000_000),
TargetBlockRate: 2,
MinBaseFee: big.NewInt(1), // 1 wei for test compatibility
TargetGas: big.NewInt(15_000_000),
BaseFeeChangeDenominator: big.NewInt(36),
MinBlockGasCost: big.NewInt(0),
MaxBlockGasCost: big.NewInt(1_000_000),
BlockGasCostStep: big.NewInt(200_000),
}
EVMDefaultChainConfig = &ChainConfig{
FeeConfig: DefaultFeeConfig,
NetworkUpgrades: GetDefaultNetworkUpgrades(),
// GenesisPrecompiles is empty by default. Chains that want all
// stateful precompiles active at genesis should call
// SetAllGenesisPrecompiles() on their extras config after module
// init() has run.
GenesisPrecompiles: Precompiles{},
}
TestChainConfig = &ChainConfig{
FeeConfig: TestFeeConfig, // Use TestFeeConfig with MinBaseFee=1 for test compatibility
NetworkUpgrades: GetDefaultNetworkUpgrades(),
GenesisPrecompiles: Precompiles{},
}
TestPreEVMChainConfig = copyAndSet(TestChainConfig, func(c *ChainConfig) {
c.NetworkUpgrades = NetworkUpgrades{
EVMTimestamp: utils.TimeToNewUint64(UnscheduledActivationTime),
DurangoTimestamp: utils.TimeToNewUint64(UnscheduledActivationTime),
QuasarTimestamp: utils.TimeToNewUint64(UnscheduledActivationTime),
FortunaTimestamp: utils.TimeToNewUint64(UnscheduledActivationTime),
}
})
TestEVMChainConfig = copyAndSet(TestChainConfig, func(c *ChainConfig) {
c.NetworkUpgrades = NetworkUpgrades{
EVMTimestamp: utils.NewUint64(0),
DurangoTimestamp: utils.TimeToNewUint64(UnscheduledActivationTime),
QuasarTimestamp: utils.TimeToNewUint64(UnscheduledActivationTime),
FortunaTimestamp: utils.TimeToNewUint64(UnscheduledActivationTime),
}
})
TestDurangoChainConfig = copyAndSet(TestChainConfig, func(c *ChainConfig) {
c.NetworkUpgrades = NetworkUpgrades{
EVMTimestamp: utils.NewUint64(0),
DurangoTimestamp: utils.TimeToNewUint64(InitiallyActiveTime),
QuasarTimestamp: utils.TimeToNewUint64(UnscheduledActivationTime),
FortunaTimestamp: utils.TimeToNewUint64(UnscheduledActivationTime),
}
})
TestQuasarChainConfig = copyAndSet(TestChainConfig, func(c *ChainConfig) {
c.NetworkUpgrades = NetworkUpgrades{
EVMTimestamp: utils.NewUint64(0),
DurangoTimestamp: utils.TimeToNewUint64(InitiallyActiveTime),
QuasarTimestamp: utils.TimeToNewUint64(InitiallyActiveTime),
FortunaTimestamp: utils.TimeToNewUint64(UnscheduledActivationTime),
}
})
TestFortunaChainConfig = copyAndSet(TestChainConfig, func(c *ChainConfig) {
c.NetworkUpgrades = NetworkUpgrades{
EVMTimestamp: utils.NewUint64(0),
DurangoTimestamp: utils.TimeToNewUint64(InitiallyActiveTime),
QuasarTimestamp: utils.TimeToNewUint64(InitiallyActiveTime),
FortunaTimestamp: utils.TimeToNewUint64(InitiallyActiveTime),
}
})
TestGraniteChainConfig = copyAndSet(TestChainConfig, func(c *ChainConfig) {
c.NetworkUpgrades = NetworkUpgrades{
EVMTimestamp: utils.NewUint64(0),
DurangoTimestamp: utils.TimeToNewUint64(InitiallyActiveTime),
QuasarTimestamp: utils.TimeToNewUint64(InitiallyActiveTime),
FortunaTimestamp: utils.TimeToNewUint64(InitiallyActiveTime),
GraniteTimestamp: utils.TimeToNewUint64(InitiallyActiveTime),
}
})
)
func copyAndSet(c *ChainConfig, set func(*ChainConfig)) *ChainConfig {
newConfig := *c
set(&newConfig)
return &newConfig
}
// UpgradeConfig includes the following configs that may be specified in upgradeBytes:
// - Timestamps that enable lux network upgrades,
// - Enabling or disabling precompiles as network upgrades.
type UpgradeConfig struct {
// Config for timestamps that enable network upgrades.
NetworkUpgradeOverrides *NetworkUpgrades `json:"networkUpgradeOverrides,omitempty"`
// Config for modifying state as a network upgrade.
StateUpgrades []StateUpgrade `json:"stateUpgrades,omitempty"`
// Config for enabling and disabling precompiles as network upgrades.
PrecompileUpgrades []PrecompileUpgrade `json:"precompileUpgrades,omitempty"`
}
// LuxContext provides Lux specific context directly into the EVM.
type LuxContext struct {
ConsensusCtx context.Context
}
type ChainConfig struct {
NetworkUpgrades // Config for timestamps that enable network upgrades.
LuxContext `json:"-"` // Lux specific context set during VM initialization. Not serialized.
FeeConfig commontype.FeeConfig `json:"feeConfig"` // Set the configuration for the dynamic fee algorithm
AllowFeeRecipients bool `json:"allowFeeRecipients,omitempty"` // Allows fees to be collected by block builders.
// FeeSplitTimestamp activates the transaction-fee 50/50 split (nil = never,
// 0 = genesis). When active, each transaction fee is divided deterministically:
// half is truly burned (never credited to any account, reducing supply) and half
// accrues to FeeRewardVault for the P-Chain staking-reward fold-in. This is an
// orthogonal fee-routing policy, intentionally NOT part of the NetworkUpgrades
// opcode-fork chain (it has no ordering relationship to EVM/Durango/Quasar/...).
// It supersedes legacy coinbase/RewardManager routing while active.
FeeSplitTimestamp *uint64 `json:"feeSplitTimestamp,omitempty"`
GenesisPrecompiles Precompiles `json:"-"` // Config for enabling precompiles from genesis. JSON encode/decode will be handled by the custom marshaler/unmarshaler.
UpgradeConfig `json:"-"` // Config specified in upgradeBytes (lux network upgrades or enable/disabling precompiles). Not serialized.
// AddressBook provides per-chain address overrides for precompile addresses.
// This allows historical chains (e.g., C-Chain) to use legacy addresses
// (like 0x0200...0005 for warp) while new chains use LP-aligned addresses.
// Resolution order: AddressBook[configKey] -> module.Address (compiled-in default)
AddressBook map[string]common.Address `json:"addressBook,omitempty"`
}
// GetPrecompileAddress returns the address for a precompile given its config key.
// Resolution order:
// 1. AddressBook[configKey] if present (per-chain override)
// 2. Module's compiled-in Address (default)
//
// This allows historical chains (e.g., C-Chain with warp at 0x0200...0005) to use
// legacy addresses while new chains use LP-aligned addresses (0x16201).
func (c *ChainConfig) GetPrecompileAddress(configKey string) common.Address {
// Check addressBook first for per-chain override
if c.AddressBook != nil {
if addr, ok := c.AddressBook[configKey]; ok {
return addr
}
}
// Fall back to module's compiled-in default address
if module, ok := modules.GetPrecompileModule(configKey); ok {
return module.Address
}
// Return zero address if not found (should not happen for registered precompiles)
return common.Address{}
}
// IsFeeSplit returns whether [time] is at or after the FeeSplit activation
// timestamp. When true, transaction fees are split 50/50 (burn + fee-reward
// vault) instead of being credited wholesale to the coinbase. Deterministic:
// depends only on the configured timestamp and the block time.
func (c *ChainConfig) IsFeeSplit(time uint64) bool {
return isTimestampForked(c.FeeSplitTimestamp, time)
}
func (c *ChainConfig) CheckConfigCompatible(newConfig *ethparams.ChainConfig, headNumber *big.Int, headTimestamp uint64) *ethparams.ConfigCompatError {
if c == nil {
return nil
}
// Cannot type-assert ethparams.ChainConfig to this ChainConfig; extra
// compatibility checks (NetworkUpgrades, precompiles) are skipped here.
// The base ethparams compatibility is validated by the caller.
return nil
}
func (c *ChainConfig) checkConfigCompatible(newcfg *ChainConfig, headNumber *big.Int, headTimestamp uint64) *ethparams.ConfigCompatError {
if err := c.checkNetworkUpgradesCompatible(&newcfg.NetworkUpgrades, headTimestamp); err != nil {
return err
}
// FeeSplit is a standalone fee-routing fork (not in NetworkUpgrades); its
// activation time must not change once it would take effect, or nodes would
// diverge on fee disbursement and fork the chain.
if isForkTimestampIncompatible(c.FeeSplitTimestamp, newcfg.FeeSplitTimestamp, headTimestamp) {
return newTimestampCompatError("FeeSplit fork block timestamp", c.FeeSplitTimestamp, newcfg.FeeSplitTimestamp)
}
// Check that the precompiles on the new config are compatible with the existing precompile config.
if err := c.checkPrecompilesCompatible(newcfg.PrecompileUpgrades, headTimestamp); err != nil {
return err
}
// Check that the state upgrades on the new config are compatible with the existing state upgrade config.
if err := c.checkStateUpgradesCompatible(newcfg.StateUpgrades, headTimestamp); err != nil {
return err
}
return nil
}
func (c *ChainConfig) Description() string {
if c == nil {
return ""
}
var banner string
banner += "Lux Upgrades (timestamp based):\n"
banner += c.NetworkUpgrades.Description()
banner += "\n"
upgradeConfigBytes, err := json.Marshal(c.UpgradeConfig)
if err != nil {
upgradeConfigBytes = []byte("cannot marshal UpgradeConfig")
}
banner += fmt.Sprintf("Upgrade Config: %s", string(upgradeConfigBytes))
banner += "\n"
feeBytes, err := json.Marshal(c.FeeConfig)
if err != nil {
feeBytes = []byte("cannot marshal FeeConfig")
}
banner += fmt.Sprintf("Fee Config: %s\n", string(feeBytes))
banner += fmt.Sprintf("Allow Fee Recipients: %v\n", c.AllowFeeRecipients)
banner += fmt.Sprintf("Fee Split (50/50 burn+stake): @%v (vault %s)\n", ptrToString(c.FeeSplitTimestamp), FeeRewardVault.Hex())
return banner
}
// isForkTimestampIncompatible returns true if a fork scheduled at timestamp s1
// cannot be rescheduled to timestamp s2 because head is already past the fork.
func isForkTimestampIncompatible(s1, s2 *uint64, head uint64) bool {
return (isTimestampForked(s1, head) || isTimestampForked(s2, head)) && !configTimestampEqual(s1, s2)
}
// isTimestampForked returns whether a fork scheduled at timestamp s is active
// at the given head timestamp.
func isTimestampForked(s *uint64, head uint64) bool {
if s == nil {
return false
}
return *s <= head
}
func configTimestampEqual(x, y *uint64) bool {
if x == nil {
return y == nil
}
if y == nil {
return x == nil
}
return *x == *y
}
// UnmarshalJSON parses the JSON-encoded data and stores the result in the
// object pointed to by c.
// This is a custom unmarshaler to handle the GenesisPrecompiles field.
//
// The genesisPrecompiles field is now stored under an explicit "genesisPrecompiles" key
// to ensure deterministic serialization. For backwards compatibility, we also check
// for inline precompile configs at the root level.
//
// IMPORTANT: Explicit is authoritative. If "genesisPrecompiles" is missing or empty,
// it means NO precompiles are enabled at genesis - we do NOT fall back to defaults.
func (c *ChainConfig) UnmarshalJSON(data []byte) error {
// Alias ChainConfigExtra to avoid recursion
type _ChainConfigExtra ChainConfig
tmp := _ChainConfigExtra{}
if err := json.Unmarshal(data, &tmp); err != nil {
return err
}
// At this point we have populated all fields except GenesisPrecompiles
*c = ChainConfig(tmp)
// Try to unmarshal from explicit "genesisPrecompiles" key first (new format)
raw := make(map[string]json.RawMessage)
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
if gpData, ok := raw["genesisPrecompiles"]; ok {
// New format: explicit genesisPrecompiles key
if err := json.Unmarshal(gpData, &c.GenesisPrecompiles); err != nil {
return err
}
} else {
// Backwards compatibility: try inlined precompile configs at root level
if err := json.Unmarshal(data, &c.GenesisPrecompiles); err != nil {
return err
}
}
// Ensure GenesisPrecompiles is never nil (explicit is authoritative - empty means none)
if c.GenesisPrecompiles == nil {
c.GenesisPrecompiles = Precompiles{}
}
return nil
}
// MarshalJSON returns the JSON encoding of c.
// This is a custom marshaler to handle the GenesisPrecompiles field.
//
// GenesisPrecompiles is serialized under an explicit "genesisPrecompiles" key
// with deterministic key ordering (sorted alphabetically) to ensure consistent
// hash computation across different Go versions and map iteration orders.
func (c *ChainConfig) MarshalJSON() ([]byte, error) {
// Alias ChainConfigExtra to avoid recursion
type _ChainConfigExtra ChainConfig
tmp, err := json.Marshal(_ChainConfigExtra(*c))
if err != nil {
return nil, err
}
// Unmarshal to raw map to add genesisPrecompiles
raw := make(map[string]json.RawMessage)
if err := json.Unmarshal(tmp, &raw); err != nil {
return nil, err
}
// Marshal GenesisPrecompiles with deterministic key order
if len(c.GenesisPrecompiles) > 0 {
gpBytes, err := c.GenesisPrecompiles.MarshalJSONDeterministic()
if err != nil {
return nil, err
}
raw["genesisPrecompiles"] = gpBytes
}
return json.Marshal(raw)
}
// SetAllGenesisPrecompiles populates GenesisPrecompiles with default configs
// for all registered precompile modules (timestamp = 0). This ensures deterministic
// genesis hash when "all precompiles active at genesis" is the intended state.
// Call this when building genesis configs for new chains.
//
// AlwaysOn modules (e.g. the 0x9999 DEX settlement money path) are SKIPPED: they are
// activated by the AlwaysOn mechanism (unconditional dispatch injection in
// GetExtrasRules + a one-time genesis EXTCODESIZE marker in ApplyPrecompileActivations),
// NOT by a genesisPrecompiles entry. Writing a timestamp-0 genesis config for them would
// create a SECOND, conflicting activation path — the module's Configurator would run for
// a system precompile that has no activating config. AlwaysOn modules carry no per-net
// config, so there is nothing to seed regardless: one and only one activation path.
func (c *ChainConfig) SetAllGenesisPrecompiles() {
c.GenesisPrecompiles = make(Precompiles)
for _, module := range modules.RegisteredModules() {
if module.AlwaysOn {
continue
}
c.GenesisPrecompiles[module.ConfigKey] = module.Configurator.MakeGenesisConfig()
}
}
// AllGenesisPrecompiles returns a new Precompiles map populated with default
// genesis configs for all registered precompile modules (timestamp = 0).
// This is the authoritative source for "all precompiles active at genesis".
//
// AlwaysOn modules are SKIPPED for the same reason as SetAllGenesisPrecompiles: an
// AlwaysOn system precompile is activated by its own mechanism and must never also
// receive a timestamp-0 genesis config (one and only one activation path).
func AllGenesisPrecompiles() Precompiles {
precompiles := make(Precompiles)
for _, module := range modules.RegisteredModules() {
if module.AlwaysOn {
continue
}
precompiles[module.ConfigKey] = module.Configurator.MakeGenesisConfig().(precompileconfig.Config)
}
return precompiles
}
type fork struct {
name string
block *big.Int // some go-ethereum forks use block numbers
timestamp *uint64 // Lux forks use timestamps
optional bool // if true, the fork may be nil and next fork is still allowed
}
func (c *ChainConfig) CheckConfigForkOrder() error {
if c == nil {
return nil
}
// Note: In Lux, upgrades must take place via block timestamps instead
// of block numbers since blocks are produced asynchronously. Therefore, we do
// not check block timestamp forks in the same way as block number forks since
// it would not be a meaningful comparison. Instead, we only check that the
// Lux upgrades are enabled in order.
// Note: we do not add the precompile configs here because they are optional
// and independent, i.e. the order in which they are enabled does not impact
// the correctness of the chain config.
return checkForks(c.forkOrder(), false)
}
// checkForks checks that forks are enabled in order and returns an error if not.
// `blockFork` is true if the fork is a block number fork, false if it is a timestamp fork
func checkForks(forks []fork, blockFork bool) error {
lastFork := fork{}
for _, cur := range forks {
if lastFork.name != "" {
switch {
// Non-optional forks must all be present in the chain config up to the last defined fork
case lastFork.block == nil && lastFork.timestamp == nil && (cur.block != nil || cur.timestamp != nil):
if cur.block != nil {
return fmt.Errorf("unsupported fork ordering: %v not enabled, but %v enabled at block %v",
lastFork.name, cur.name, cur.block)
} else {
return fmt.Errorf("unsupported fork ordering: %v not enabled, but %v enabled at timestamp %v",
lastFork.name, cur.name, cur.timestamp)
}
// Fork (whether defined by block or timestamp) must follow the fork definition sequence
case (lastFork.block != nil && cur.block != nil) || (lastFork.timestamp != nil && cur.timestamp != nil):
if lastFork.block != nil && lastFork.block.Cmp(cur.block) > 0 {
return fmt.Errorf("unsupported fork ordering: %v enabled at block %v, but %v enabled at block %v",
lastFork.name, lastFork.block, cur.name, cur.block)
} else if lastFork.timestamp != nil && *lastFork.timestamp > *cur.timestamp {
return fmt.Errorf("unsupported fork ordering: %v enabled at timestamp %v, but %v enabled at timestamp %v",
lastFork.name, lastFork.timestamp, cur.name, cur.timestamp)
}
// Timestamp based forks can follow block based ones, but not the other way around
if lastFork.timestamp != nil && cur.block != nil {
return fmt.Errorf("unsupported fork ordering: %v used timestamp ordering, but %v reverted to block ordering",
lastFork.name, cur.name)
}
}
}
// If it was optional and not set, then ignore it
if !cur.optional || (cur.block != nil || cur.timestamp != nil) {
lastFork = cur
}
}
return nil
}
// Verify verifies chain config.
func (c *ChainConfig) Verify() error {
if err := c.FeeConfig.Verify(); err != nil {
return fmt.Errorf("invalid fee config: %w", err)
}
// Verify the precompile upgrades are internally consistent given the existing chainConfig.
if err := c.verifyPrecompileUpgrades(); err != nil {
return fmt.Errorf("invalid precompile upgrades: %w", err)
}
// Verify the state upgrades are internally consistent given the existing chainConfig.
if err := c.verifyStateUpgrades(); err != nil {
return fmt.Errorf("invalid state upgrades: %w", err)
}
// Verify the network upgrades are internally consistent given the existing chainConfig.
// Use default config for validation
agoUpgrades := upgrade.Config{}
if err := c.verifyNetworkUpgrades(agoUpgrades); err != nil {
return fmt.Errorf("invalid network upgrades: %w", err)
}
return nil
}
// IsPrecompileEnabled returns whether precompile with `address` is enabled at `timestamp`.
func (c *ChainConfig) IsPrecompileEnabled(address common.Address, timestamp uint64) bool {
config := c.GetActivePrecompileConfig(address, timestamp)
return config != nil && !config.IsDisabled()
}
// GetFeeConfig returns the original FeeConfig contained in the genesis ChainConfig.
// Implements precompile.ChainConfig interface.
func (c *ChainConfig) GetFeeConfig() commontype.FeeConfig {
return c.FeeConfig
}
// AllowedFeeRecipients returns the original AllowedFeeRecipients parameter contained in the genesis ChainConfig.
// Implements precompile.ChainConfig interface.
func (c *ChainConfig) AllowedFeeRecipients() bool {
return c.AllowFeeRecipients
}
// IsForkTransition returns true if `fork` activates during the transition from
// `parent` to `current`.
// Taking `parent` as a pointer allows for us to pass nil when checking forks
// that activate during genesis.
// Note: `parent` and `current` can be either both timestamp values, or both
// block number values, since this function works for both block number and
// timestamp activated forks.
func IsForkTransition(fork *uint64, parent *uint64, current uint64) bool {
var parentForked bool
if parent != nil {
parentForked = isTimestampForked(fork, *parent)
}
currentForked := isTimestampForked(fork, current)
return !parentForked && currentForked
}