Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cmd, core, eth: add support for Reth style ExEx plugins #30611

Closed
wants to merge 9 commits into from
2 changes: 1 addition & 1 deletion cmd/geth/chaincmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion cmd/geth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
47 changes: 47 additions & 0 deletions cmd/utils/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
19 changes: 19 additions & 0 deletions core/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()})
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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{}{
Expand Down
21 changes: 21 additions & 0 deletions core/blockchain_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
52 changes: 52 additions & 0 deletions core/exex/exex.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

// 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
}
104 changes: 104 additions & 0 deletions core/exex/exex/adapter_chain.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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