diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index d85e4a83c8a0..59e6ed23fdfd 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -102,7 +102,7 @@ if one is set. Otherwise it prints the genesis from the datadir.`, utils.VMTraceJsonConfigFlag, utils.TransactionHistoryFlag, utils.StateHistoryFlag, - }, utils.DatabaseFlags), + }, utils.DatabaseFlags, utils.ExExPluginFlags()), Description: ` The import command imports blocks from an RLP-encoded form. The form can be one file with several RLP-encoded blocks, or several files can be used. diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 2675a616759c..07d3802fb4de 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -156,7 +156,7 @@ var ( utils.BeaconGenesisRootFlag, utils.BeaconGenesisTimeFlag, utils.BeaconCheckpointFlag, - }, utils.NetworkFlags, utils.DatabaseFlags) + }, utils.NetworkFlags, utils.DatabaseFlags, utils.ExExPluginFlags()) rpcFlags = []cli.Flag{ utils.HTTPEnabledFlag, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 6db88ff66183..c91f20f26412 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -41,6 +41,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/fdlimit" "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/exex/exex" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/txpool/blobpool" "github.com/ethereum/go-ethereum/core/txpool/legacypool" @@ -71,6 +72,7 @@ import ( "github.com/ethereum/go-ethereum/p2p/nat" "github.com/ethereum/go-ethereum/p2p/netutil" "github.com/ethereum/go-ethereum/params" + _ "github.com/ethereum/go-ethereum/plugins" // Load all ExEx plugins "github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/triedb" "github.com/ethereum/go-ethereum/triedb/hashdb" @@ -973,6 +975,24 @@ var ( } ) +// ExExPluginFlags constructs a live set of CLI flags from the registered plugins. +func ExExPluginFlags() []cli.Flag { + var flagset []cli.Flag + for _, name := range exex.Plugins() { + flagset = append(flagset, &cli.BoolFlag{ + Name: fmt.Sprintf("exex.%s", name), + Usage: fmt.Sprintf("Enables the '%s' execution extension plugin", name), + Category: flags.ExExCategory, + }) + flagset = append(flagset, &cli.StringFlag{ + Name: fmt.Sprintf("exex.%s.config", name), + Usage: fmt.Sprintf("Opaque config to pass to the '%s' execution extension plugin", name), + Category: flags.ExExCategory, + }) + } + return flagset +} + // MakeDataDir retrieves the currently requested data directory, terminating // if none (or the empty string) is specified. If the node is starting a testnet, // then a subdirectory of the specified datadir will be used. @@ -1908,6 +1928,20 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { cfg.VMTraceJsonConfig = config } } + // Execution extension plugins + for _, flag := range ExExPluginFlags() { + if flag, ok := flag.(*cli.BoolFlag); ok { + if ctx.IsSet(flag.Name) { + plugin, _ := strings.CutPrefix(flag.Name, "exex.") // TODO(karalabe): Custom flag + config := ctx.String(flag.Name + ".config") // TODO(karalabe): Custom flag + + if err := exex.Instantiate(plugin, config); err != nil { + Fatalf("Failed to instantiate ExEx plugin %s: %v", plugin, err) + } + log.Info("Instantiated ExEx plugin", "name", plugin) + } + } + } } // SetDNSDiscoveryDefaults configures DNS discovery with the given URL if @@ -2197,6 +2231,19 @@ func MakeChain(ctx *cli.Context, stack *node.Node, readonly bool) (*core.BlockCh vmcfg.Tracer = t } } + for _, flag := range ExExPluginFlags() { + if flag, ok := flag.(*cli.BoolFlag); ok { + if ctx.IsSet(flag.Name) { + plugin, _ := strings.CutPrefix(flag.Name, "exex.") // TODO(karalabe): Custom flag + config := ctx.String(flag.Name + ".config") // TODO(karalabe): Custom flag + + if err := exex.Instantiate(plugin, config); err != nil { + Fatalf("Failed to instantiate ExEx plugin %s: %v", plugin, err) + } + log.Info("Instantiated ExEx plugin", "name", plugin) + } + } + } // Disable transaction indexing/unindexing by default. chain, err := core.NewBlockChain(chainDb, cache, gspec, nil, engine, vmcfg, nil) if err != nil { diff --git a/core/blockchain.go b/core/blockchain.go index 02c0bbaad1cb..3e9d91f4bb89 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -35,6 +35,7 @@ import ( "github.com/ethereum/go-ethereum/common/prque" "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/misc/eip4844" + "github.com/ethereum/go-ethereum/core/exex/exex" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state/snapshot" @@ -467,6 +468,7 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, genesis *Genesis if txLookupLimit != nil { bc.txIndexer = newTxIndexer(*txLookupLimit, bc) } + exex.TriggerInitHook(bc) return bc, nil } @@ -578,6 +580,7 @@ func (bc *BlockChain) SetHead(head uint64) error { log.Error("Current block not found in database", "block", header.Number, "hash", header.Hash()) return fmt.Errorf("current block missing: #%d [%x..]", header.Number, header.Hash().Bytes()[:4]) } + exex.TriggerHeadHook(header) bc.chainHeadFeed.Send(ChainHeadEvent{Header: header}) return nil } @@ -599,6 +602,7 @@ func (bc *BlockChain) SetHeadWithTimestamp(timestamp uint64) error { log.Error("Current block not found in database", "block", header.Number, "hash", header.Hash()) return fmt.Errorf("current block missing: #%d [%x..]", header.Number, header.Hash().Bytes()[:4]) } + exex.TriggerHeadHook(header) bc.chainHeadFeed.Send(ChainHeadEvent{Header: header}) return nil } @@ -609,6 +613,7 @@ func (bc *BlockChain) SetFinalized(header *types.Header) { if header != nil { rawdb.WriteFinalizedBlockHash(bc.db, header.Hash()) headFinalizedBlockGauge.Update(int64(header.Number.Uint64())) + exex.TriggerFinalHook(header) } else { rawdb.WriteFinalizedBlockHash(bc.db, common.Hash{}) headFinalizedBlockGauge.Update(0) @@ -1157,6 +1162,8 @@ func (bc *BlockChain) Stop() { if bc.logger != nil && bc.logger.OnClose != nil { bc.logger.OnClose() } + exex.TriggerCloseHook() + // Close the trie database, release all the held resources as the last step. if err := bc.triedb.Close(); err != nil { log.Error("Failed to close trie database", "err", err) @@ -1559,6 +1566,7 @@ func (bc *BlockChain) writeBlockAndSetHead(block *types.Block, receipts []*types // canonical blocks. Avoid firing too many ChainHeadEvents, // we will fire an accumulated ChainHeadEvent and disable fire // event here. + exex.TriggerHeadHook(block.Header()) if emitHeadEvent { bc.chainHeadFeed.Send(ChainHeadEvent{Header: block.Header()}) } @@ -2254,6 +2262,10 @@ func (bc *BlockChain) reorg(oldHead *types.Header, newHead *types.Header) error // rewind the canonical chain to a lower point. log.Error("Impossible reorg, please file an issue", "oldnum", oldHead.Number, "oldhash", oldHead.Hash(), "oldblocks", len(oldChain), "newnum", newHead.Number, "newhash", newHead.Hash(), "newblocks", len(newChain)) } + // Trigger revertal reorgs + if len(oldChain) > 0 { + exex.TriggerReorgHook(oldChain, true) + } // Acquire the tx-lookup lock before mutation. This step is essential // as the txlookups should be changed atomically, and all subsequent // reads should be blocked until the mutation is complete. @@ -2361,6 +2373,12 @@ func (bc *BlockChain) reorg(oldHead *types.Header, newHead *types.Header) error // Release the tx-lookup lock after mutation. bc.txLookupLock.Unlock() + // Trigger application reorgs + if len(newChain) > 1 { + slices.Reverse(newChain) + exex.TriggerReorgHook(newChain[:len(newChain)-1], false) + slices.Reverse(newChain) + } return nil } @@ -2410,6 +2428,7 @@ func (bc *BlockChain) SetCanonical(head *types.Block) (common.Hash, error) { if len(logs) > 0 { bc.logsFeed.Send(logs) } + exex.TriggerHeadHook(head.Header()) bc.chainHeadFeed.Send(ChainHeadEvent{Header: head.Header()}) context := []interface{}{ diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go index 19c1b17f369c..3378533c9fc7 100644 --- a/core/blockchain_reader.go +++ b/core/blockchain_reader.go @@ -209,6 +209,27 @@ func (bc *BlockChain) GetBlocksFromHash(hash common.Hash, n int) (blocks []*type return } +// GetReceiptsByNumber retrieves the receipts for all transactions in a given block. +func (bc *BlockChain) GetReceiptsByNumber(number uint64) types.Receipts { + hash := rawdb.ReadCanonicalHash(bc.db, number) + if hash == (common.Hash{}) { + return nil + } + if receipts, ok := bc.receiptsCache.Get(hash); ok { + return receipts + } + header := bc.GetHeader(hash, number) + if header == nil { + return nil + } + receipts := rawdb.ReadReceipts(bc.db, hash, number, header.Time, bc.chainConfig) + if receipts == nil { + return nil + } + bc.receiptsCache.Add(hash, receipts) + return receipts +} + // GetReceiptsByHash retrieves the receipts for all transactions in a given block. func (bc *BlockChain) GetReceiptsByHash(hash common.Hash) types.Receipts { if receipts, ok := bc.receiptsCache.Get(hash); ok { diff --git a/core/exex/exex.go b/core/exex/exex.go new file mode 100644 index 000000000000..ea3b4bc2df79 --- /dev/null +++ b/core/exex/exex.go @@ -0,0 +1,52 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +// Package exex contains the stable API of the Geth Execution Extensions. +package exex + +import ( + "github.com/ethereum/go-ethereum/log" +) + +// RegisterV1 registers an execution extension plugin with a unique name. +func RegisterV1(name string, constructor NewPluginV1) { + globalRegistry.RegisterV1(name, constructor) +} + +// NewPluginV1 is the constructor signature for making a new plugin. +type NewPluginV1 func(config *ConfigV1) (*PluginV1, error) + +// ConfigV1 contains some configurations for initializing exex plugins. Some of +// the fields originate from Geth, other fields from user configs. +type ConfigV1 struct { + Logger log.Logger // Geth's logger with the plugin name injected + + User string // Opaque flag provided by the user on the CLI +} + +// PluginV1 is an Execution Extension module that can be injected into Geth's +// processing pipeline to subscribe to different node, chain and EVM lifecycle +// events. +// +// Note, V1 of the Execution Extension plugin module has not yet been stabilized. +// There might be breaking changes until it is tagged as released! +type PluginV1 struct { + OnInit InitHook // Called when the chain gets initialized within Geth + OnClose CloseHook // Called when the chain gets torn down within Geth + OnHead HeadHook // Called when the chain head block is updated in Geth + OnReorg ReorgHook // Called wnen the chain reorgs to a sidechain within Geth + OnFinal FinalHook // Called when the chain finalizes a block within Geth +} diff --git a/core/exex/exex/adapter_chain.go b/core/exex/exex/adapter_chain.go new file mode 100644 index 000000000000..fea4fdcf096c --- /dev/null +++ b/core/exex/exex/adapter_chain.go @@ -0,0 +1,104 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package exex + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/exex" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/state/snapshot" + "github.com/ethereum/go-ethereum/core/types" +) + +// gethChain provides dep-free read access to Geth's internal chain object. +// +// The methods here are not documented as this interface is not a public thing, +// rather it's a dynamic cast to break cross-package dependencies. +type gethChain interface { + CurrentBlock() *types.Header + CurrentFinalBlock() *types.Header + GetHeaderByNumber(number uint64) *types.Header + GetBlockByNumber(number uint64) *types.Block + StateAt(root common.Hash) (*state.StateDB, error) + Snapshots() *snapshot.Tree + GetReceiptsByNumber(number uint64) types.Receipts +} + +// chainAdapter is an adapter to convert Geth's internal blockchain (unstable +// and legacy API) into the exex chain interface (stable API). +type chainAdapter struct { + chain gethChain +} + +// wrapChain wraps a Geth internal chain object into an exex stable API. +func wrapChain(chain gethChain) exex.Chain { + return &chainAdapter{chain: chain} +} + +// Head retrieves the current head block's header from the canonical chain. +func (a *chainAdapter) Head() *types.Header { + // Headers have public fields, copy to prevent modification + return types.CopyHeader(a.chain.CurrentBlock()) +} + +// Final retrieves the last finalized block from the chain. If no finality +// is known yet (not synced, not past merge, etc.) or Geth crashed and is +// recovering, the returns header will be nil. +func (a *chainAdapter) Final() *types.Header { + // Headers have public fields, copy to prevent modification + return types.CopyHeader(a.chain.CurrentFinalBlock()) +} + +// Header retrieves a block header with the given number from the canonical +// chain. Headers on side-chains are not exposed by the Chain interface. +func (a *chainAdapter) Header(number uint64) *types.Header { + // Headers have public fields, copy to prevent modification + if header := a.chain.GetHeaderByNumber(number); header != nil { + return types.CopyHeader(header) + } + return nil +} + +// Block retrieves a block header with the given number from the canonical +// chain. Blocks on side-chains are not exposed by the Chain interface. +func (a *chainAdapter) Block(number uint64) *types.Block { + // Blocks don't have public fields, return live objects directly + return a.chain.GetBlockByNumber(number) +} + +// State retrieves a state accessor at a given root hash. +func (a *chainAdapter) State(root common.Hash) exex.State { + state, err := a.chain.StateAt(root) + if err != nil { + return nil + } + return wrapState(root, state, a.chain.Snapshots()) +} + +// Receipts retrieves a set of receipts belonging to all transactions within +// a block from the canonical chain. Receipts on side-chains are not exposed +// by the Chain interface. +func (a *chainAdapter) Receipts(number uint64) []*types.Receipt { + // Receipts have public fields, copy to prevent modification + receipts := a.chain.GetReceiptsByNumber(number) + + copies := make([]*types.Receipt, 0, len(receipts)) + for _, receipt := range receipts { + copies = append(copies, types.CopyReceipt(receipt)) + } + return copies +} diff --git a/core/exex/exex/adapter_state.go b/core/exex/exex/adapter_state.go new file mode 100644 index 000000000000..0e31707fdeff --- /dev/null +++ b/core/exex/exex/adapter_state.go @@ -0,0 +1,97 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package exex + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/exex" + "github.com/ethereum/go-ethereum/core/state/snapshot" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/holiman/uint256" +) + +// stateAdapter is an adapter to convert Geth's internal state db (unstable +// and legacy API) into the exex state interface (stable API). +type stateAdapter struct { + root common.Hash // Root hash of the state being accessed + state vm.StateDB // State database backed by a trie + snaps *snapshot.Tree // State database backed by a snapshot +} + +// wrapState wraps a Geth internal state object into an exex stable API. +func wrapState(root common.Hash, state vm.StateDB, snaps *snapshot.Tree) exex.State { + return &stateAdapter{ + root: root, + state: state, + snaps: snaps, + } +} + +// Balance retrieves the balance of the given account, or 0 if the account is +// not found in the state. +func (a *stateAdapter) Balance(addr common.Address) *uint256.Int { + return a.state.GetBalance(addr) +} + +// Nonce retrieves the nonce of the given account, or 0 if the account is not +// found in the state. +func (a *stateAdapter) Nonce(addr common.Address) uint64 { + return a.state.GetNonce(addr) +} + +// Code retrieves the bytecode associated with the given account, or a nil slice +// if the account is not found. +func (a *stateAdapter) Code(addr common.Address) []byte { + return common.CopyBytes(a.state.GetCode(addr)) +} + +// Storage retrieves the value associated with a specific storage slot key within +// a specific account. +func (a *stateAdapter) Storage(addr common.Address, slot common.Hash) common.Hash { + return a.state.GetState(addr, slot) +} + +// AccountIterator retrieves an iterator to walk across all the known accounts in +// the Ethereum state trie from a starting position. +func (a *stateAdapter) AccountIterator(seek common.Hash) snapshot.AccountIterator { + if a.snaps == nil { + return nil + } + it, err := a.snaps.AccountIterator(a.root, seek) + if err != nil { + return nil + } + return it +} + +// StorageIterator retrieves an iterator to walk across all the known storage slots +// the Ethereum state trie of a given account, from a starting position. +// +// The iteration order is the Merkle-Patricia order (storage slot hash alphabetically). +// +// Note, the account is the hash of the address. This is due to the AccountIterator +// also walking the state in hash order, not address order. +func (a *stateAdapter) StorageIterator(account common.Hash, seek common.Hash) snapshot.StorageIterator { + if a.snaps == nil { + return nil + } + it, err := a.snaps.StorageIterator(a.root, account, seek) + if err != nil { + return nil + } + return it +} diff --git a/core/exex/exex/registry.go b/core/exex/exex/registry.go new file mode 100644 index 000000000000..1f3a42b7f319 --- /dev/null +++ b/core/exex/exex/registry.go @@ -0,0 +1,78 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package exex + +import ( + "github.com/ethereum/go-ethereum/core/exex" + "github.com/ethereum/go-ethereum/core/types" +) + +// globalRegistry is the Geth internal version of the exex registry with the +// trigger methods exposed to be callable from within Geth. +var globalRegistry registry + +func init() { + globalRegistry = exex.Registry().(registry) +} + +// registry exposes all the hidden methods on the plugin registry to allow event +// triggers to be invoked. +type registry interface { + Plugins() []string + Instantiate(name string, userconf string) error + + TriggerInitHook(chain exex.Chain) + TriggerCloseHook() + TriggerHeadHook(head *types.Header) + TriggerReorgHook(headers []*types.Header, revert bool) + TriggerFinalHook(header *types.Header) +} + +// Plugins returns a list of all registered plugins to generate CLI flags. +func Plugins() []string { + return globalRegistry.Plugins() +} + +// Instantiate constructs an execution extension plugin from a unique name. +func Instantiate(name string, userconf string) error { + return globalRegistry.Instantiate(name, userconf) +} + +// TriggerInitHook triggers the OnInit hook in exex plugins. +func TriggerInitHook(chain gethChain) { + globalRegistry.TriggerInitHook(wrapChain(chain)) +} + +// TriggerCloseHook triggers the OnClose hook in exex plugins. +func TriggerCloseHook() { + globalRegistry.TriggerCloseHook() +} + +// TriggerHeadHook triggers the OnHead hook in exex plugins. +func TriggerHeadHook(head *types.Header) { + globalRegistry.TriggerHeadHook(head) +} + +// TriggerReorgHook triggers the OnReorg hook in exex plugins. +func TriggerReorgHook(headers []*types.Header, revert bool) { + globalRegistry.TriggerReorgHook(headers, revert) +} + +// TriggerFinalHook triggers the OnFinal hook in exex plugins. +func TriggerFinalHook(header *types.Header) { + globalRegistry.TriggerFinalHook(header) +} diff --git a/core/exex/hooks.go b/core/exex/hooks.go new file mode 100644 index 000000000000..3ba585b18253 --- /dev/null +++ b/core/exex/hooks.go @@ -0,0 +1,56 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package exex + +import "github.com/ethereum/go-ethereum/core/types" + +// InitHook is called when the chain gets initialized within Geth. +type InitHook = func(chain Chain) + +// CloseHook is called when the chain gets torn down within Geth. +type CloseHook = func() + +// HeadHook is called when the chain head block is updated. +// +// - During full sync, this will be called for each block +// - During snap sync, this will be called from pivot onwards +// - In sync, this will be called on fork-choice updates +type HeadHook = func(head *types.Header) + +// ReorgHook is called when the chain head is updated to a different parent than +// the previous head. In this case previously applied state changes need to be +// rolled back, and state changes from a sidechain need to be applied. +// +// This method is called with a set of header being operated on and the direction +// of the operation, usually both directions being called one after the other: +// +// - If revert == true, the given headers are being rolled back, they are in +// reverse order, headers[0] being the previous chain head, and the last item +// being the olders block getting undone. +// - If revert == false, the given headers are being applied after the rollback, +// they are in forward order, headers[0] being the oldest block being applied +// and the last item being the newest getting applied. Note, the chain head +// that triggered the reorg will arrive in the HeadHook. +// +// The reason the reorg event it "emitted" in two parts is for both operations to +// have access to a unified singletone view of the chain. An alternative would be +// to pass in both the reverted and applied headers at the same time, but that +// would require chain accessorts to support sidechains, which complicate APIs. +type ReorgHook = func(headers []*types.Header, revert bool) + +// FinalHook is called when the chain finalizes a past block. +type FinalHook = func(header *types.Header) diff --git a/core/exex/interface_chain.go b/core/exex/interface_chain.go new file mode 100644 index 000000000000..93e25a8b7eeb --- /dev/null +++ b/core/exex/interface_chain.go @@ -0,0 +1,53 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package exex + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +// Chain provides read access to Geth's internal chain object. +type Chain interface { + // TODO(karalabe): Wrap chain config into an exex interface + // Config() *params.ChainConfig + + // Head retrieves the current head block's header from the canonical chain. + Head() *types.Header + + // Final retrieves the last finalized block from the chain. If no finality + // is known yet (not synced, not past merge, etc.) or Geth crashed and is + // recovering, the returns header will be nil. + Final() *types.Header + + // Header retrieves a block header with the given number from the canonical + // chain. Headers on side-chains are not exposed by the Chain interface. + Header(number uint64) *types.Header + + // Block retrieves an entire block with the given number from the canonical + // chain. Blocks on side-chains are not exposed by the Chain interface. + Block(number uint64) *types.Block + + // State retrieves a state accessor at a given root hash, or nil if the state + // associated with the given block has already been pruned. + State(root common.Hash) State + + // Receipts retrieves a set of receipts belonging to all transactions within + // a block from the canonical chain. Receipts on side-chains are not exposed + // by the Chain interface. + Receipts(number uint64) []*types.Receipt +} diff --git a/core/exex/interface_state.go b/core/exex/interface_state.go new file mode 100644 index 000000000000..5a511dfc7196 --- /dev/null +++ b/core/exex/interface_state.go @@ -0,0 +1,60 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package exex + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state/snapshot" + "github.com/holiman/uint256" +) + +// State provides read access to Geth's internal state object. +type State interface { + // Balance retrieves the balance of the given account, or 0 if the account + // is not found in the state. + Balance(addr common.Address) *uint256.Int + + // Nonce retrieves the nonce of the given account, or 0 if the account is + // not found in the state. + Nonce(addr common.Address) uint64 + + // Code retrieves the bytecode associated with the given account, or a nil + // slice if the account is not found. + Code(addr common.Address) []byte + + // Storage retrieves the value associated with a specific storage slot key + // within a specific account. + Storage(addr common.Address, slot common.Hash) common.Hash + + // AccountIterator retrieves an iterator to walk across all the known accounts + // in the Ethereum state trie from a starting position, or returns nil if the + // requested state is unavailable in snapshot (accelerated access) form. + // + // Iteration is in Merkle-Patricia order (address hash alphabetically). + AccountIterator(seek common.Hash) snapshot.AccountIterator + + // StorageIterator retrieves an iterator to walk across all the known storage + // slots the Ethereum state trie of a given account, from a starting position, + // or returns nil if the requested state is unavailable in snapshot (accelerated + // access) form. + // + // Iteration is in Merkle-Patricia order (storage slot hash alphabetically). + // + // The account is the hash of the address. This is due to the AccountIterator + // also walking the state in hash order, not address order. + StorageIterator(account common.Hash, seek common.Hash) snapshot.StorageIterator +} diff --git a/core/exex/registry.go b/core/exex/registry.go new file mode 100644 index 000000000000..37d1a5aa3fba --- /dev/null +++ b/core/exex/registry.go @@ -0,0 +1,48 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package exex + +// globalRegistry is the public plugin registry to inject execution extensions into. +var globalRegistry = newRegistry() + +// Registry retrieves the global plugin registry. +// +// Note, the downcast to interface{} is deliberate to hide all the methods on the +// registry and avoid plugins from accidentally poking at unintended internals. +func Registry() interface{} { + return globalRegistry +} + +// registry is the collection of Execution Extension plugins which can be used +// to extend Geth's functionality with external code. +type registry struct { + pluginsMakersV1 map[string]NewPluginV1 + pluginsV1 map[string]*PluginV1 +} + +// newRegistry creates a new exex plugin registry. +func newRegistry() *registry { + return ®istry{ + pluginsMakersV1: make(map[string]NewPluginV1), + pluginsV1: make(map[string]*PluginV1), + } +} + +// RegisterV1 registers an execution extension plugin with a unique name. +func (reg *registry) RegisterV1(name string, constructor NewPluginV1) { + reg.pluginsMakersV1[name] = constructor +} diff --git a/core/exex/registry_internal.go b/core/exex/registry_internal.go new file mode 100644 index 000000000000..d29e637b8426 --- /dev/null +++ b/core/exex/registry_internal.go @@ -0,0 +1,98 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package exex + +import ( + "errors" + "sort" + + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" +) + +// Plugins returns a list of all registered plugins to generate CLI flags. +func (reg *registry) Plugins() []string { + plugins := make([]string, 0, len(reg.pluginsMakersV1)) + for name := range reg.pluginsMakersV1 { + plugins = append(plugins, name) + } + sort.Strings(plugins) + return plugins +} + +// Instantiate constructs an execution extension plugin from a unique name. +func (reg *registry) Instantiate(name string, userconf string) error { + // Try instantiating a V1 plugin + if constructor, ok := globalRegistry.pluginsMakersV1[name]; ok { + plugin, err := constructor(&ConfigV1{ + Logger: log.New("exex", name), + User: userconf, + }) + if err != nil { + return err + } + globalRegistry.pluginsV1[name] = plugin + return nil + } + // No plugins matched across any versions, return a failure + return errors.New("not found") +} + +// TriggerInitHook triggers the OnInit hook in exex plugins. +func (reg *registry) TriggerInitHook(chain Chain) { + for _, plugin := range globalRegistry.pluginsV1 { + if plugin.OnInit != nil { + plugin.OnInit(chain) + } + } +} + +// TriggerCloseHook triggers the OnClose hook in exex plugins. +func (reg *registry) TriggerCloseHook() { + for _, plugin := range globalRegistry.pluginsV1 { + if plugin.OnClose != nil { + plugin.OnClose() + } + } +} + +// TriggerHeadHook triggers the OnHead hook in exex plugins. +func (reg *registry) TriggerHeadHook(head *types.Header) { + for _, plugin := range globalRegistry.pluginsV1 { + if plugin.OnHead != nil { + plugin.OnHead(head) + } + } +} + +// TriggerReorgHook triggers the OnReorg hook in exex plugins. +func (reg *registry) TriggerReorgHook(headers []*types.Header, revert bool) { + for _, plugin := range globalRegistry.pluginsV1 { + if plugin.OnReorg != nil { + plugin.OnReorg(headers, revert) + } + } +} + +// TriggerFinalHook triggers the OnFinal hook in exex plugins. +func (reg *registry) TriggerFinalHook(header *types.Header) { + for _, plugin := range globalRegistry.pluginsV1 { + if plugin.OnFinal != nil { + plugin.OnFinal(header) + } + } +} diff --git a/core/types/log.go b/core/types/log.go index 54c7ff6372c8..814deec60759 100644 --- a/core/types/log.go +++ b/core/types/log.go @@ -53,6 +53,20 @@ type Log struct { Removed bool `json:"removed" rlp:"-"` } +// CopyLog creates a deep copy of a log. +func CopyLog(l *Log) *Log { + cpy := *l + if len(l.Topics) > 0 { + cpy.Topics = make([]common.Hash, len(l.Topics)) + copy(cpy.Topics, l.Topics) + } + if len(l.Data) > 0 { + cpy.Data = make([]byte, len(l.Data)) + copy(cpy.Data, l.Data) + } + return &cpy +} + type logMarshaling struct { Data hexutil.Bytes BlockNumber hexutil.Uint64 diff --git a/core/types/receipt.go b/core/types/receipt.go index 4f96fde59c44..84241a075f47 100644 --- a/core/types/receipt.go +++ b/core/types/receipt.go @@ -117,6 +117,31 @@ func NewReceipt(root []byte, failed bool, cumulativeGasUsed uint64) *Receipt { return r } +// CopyReceipt creates a deep copy of a receipt. +func CopyReceipt(r *Receipt) *Receipt { + cpy := *r + if len(r.PostState) > 0 { + cpy.PostState = make([]byte, len(r.PostState)) + copy(cpy.PostState, r.PostState) + } + if len(r.Logs) > 0 { + cpy.Logs = make([]*Log, len(r.Logs)) + for i, log := range r.Logs { + cpy.Logs[i] = CopyLog(log) + } + } + if r.EffectiveGasPrice != nil { + cpy.EffectiveGasPrice = new(big.Int).Set(r.EffectiveGasPrice) + } + if r.BlobGasPrice != nil { + cpy.BlobGasPrice = new(big.Int).Set(r.BlobGasPrice) + } + if r.BlockNumber != nil { + cpy.BlockNumber = new(big.Int).Set(r.BlockNumber) + } + return &cpy +} + // EncodeRLP implements rlp.Encoder, and flattens the consensus fields of a receipt // into an RLP stream. If no post state is present, byzantium fork is assumed. func (r *Receipt) EncodeRLP(w io.Writer) error { diff --git a/internal/flags/categories.go b/internal/flags/categories.go index d426add55b10..abab0ab4a463 100644 --- a/internal/flags/categories.go +++ b/internal/flags/categories.go @@ -32,6 +32,7 @@ const ( MinerCategory = "MINER" GasPriceCategory = "GAS PRICE ORACLE" VMCategory = "VIRTUAL MACHINE" + ExExCategory = "EXECUTION EXTENSIONS" LoggingCategory = "LOGGING AND DEBUGGING" MetricsCategory = "METRICS AND STATS" MiscCategory = "MISC" diff --git a/plugins/minimal.go b/plugins/minimal.go new file mode 100644 index 000000000000..3431187c5f95 --- /dev/null +++ b/plugins/minimal.go @@ -0,0 +1,44 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package plugins + +import ( + "github.com/ethereum/go-ethereum/core/exex" + "github.com/ethereum/go-ethereum/core/types" +) + +// Register the minimal ExEx plugin into Geth. +func init() { + exex.RegisterV1("minimal", newMinimalPlugin) +} + +// newMinimalPlugin creates a minimal Execution Extension plugin to react to some +// chain events. +func newMinimalPlugin(config *exex.ConfigV1) (*exex.PluginV1, error) { + return &exex.PluginV1{ + OnHead: func(head *types.Header) { + config.Logger.Info("Chain head updated", "number", head.Number, "hash", head.Hash()) + }, + OnReorg: func(headers []*types.Header, revert bool) { + if revert { + config.Logger.Warn("Reorging blocks out", "count", len(headers)) + } else { + config.Logger.Warn("Reorging blocks in", "count", len(headers)) + } + }, + }, nil +}