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
+}