// 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() // AlwaysOn system precompiles (the DEX settlement money path 0x9999) are activated by // the Lux protocol on every chain with NO per-network config, at their protocol // ActivationTime (a fork-like timestamp constant, set by the registry bridge), NOT // unconditionally at genesis. Their EXTCODESIZE marker (nonce=1 + a non-empty code // byte) is installed exactly ONCE, on the block transition that crosses ActivationTime // — which is genesis IFF ActivationTime <= the genesis timestamp. A chain whose genesis // predates ActivationTime does NOT get the marker in its genesis state (its original // pre-precompile genesis hash is preserved byte-identically); the marker is instead // installed later, during history replay, at the first block whose timestamp reaches // ActivationTime. This mirrors the config-driven activation crossing logic below. applyAlwaysOnActivations(parentTimestamp, blockTimestamp, statedb) // 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 } // applyAlwaysOnActivations installs the EXTCODESIZE marker (nonce=1 + a non-empty code // byte) for each AlwaysOn system precompile whose protocol ActivationTime is crossed by // this block transition, so EXTCODESIZE>0 / eth_getCode!=0x / Solidity's contract- // existence guard pass from the activation block forward. // // "Crossed by this transition" is defined exactly like the config-driven precompile // activation below (GetActivatingPrecompileConfigs): // - genesis (parentTimestamp == nil): crossed iff ActivationTime <= blockTimestamp, // so the marker lands in the committed genesis state root ONLY for a chain genesised // at/after ActivationTime. A pre-activation chain's genesis root is untouched. // - later block (parentTimestamp != nil): crossed iff // *parentTimestamp < ActivationTime <= blockTimestamp — the single transition where // the precompile turns on during history replay. // // The marker is therefore written exactly once, deterministically, on every node. An // AlwaysOn module's Configurator is NOT invoked (a system precompile has no activating // config; all params resolve at runtime from the consensus context) — only the marker. func applyAlwaysOnActivations(parentTimestamp *uint64, blockTimestamp uint64, statedb *state.StateDB) { for _, module := range modules.AlwaysOnModules() { var crossed bool if parentTimestamp == nil { crossed = module.ActivationTime <= blockTimestamp } else { crossed = *parentTimestamp < module.ActivationTime && module.ActivationTime <= blockTimestamp } if !crossed { continue } statedb.SetNonce(module.Address, 1, tracing.NonceChangeUnspecified) _ = statedb.SetCode(module.Address, []byte{0x1}, tracing.CodeChangeUnspecified) } } // 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 }