diff --git a/core/blockchain.go b/core/blockchain.go index 7b5a910b7a..fcefd6bad6 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -326,6 +326,7 @@ type BlockChain struct { flushInterval atomic.Int64 // Time interval (processing time) after which to flush a state triedb *triedb.Database // The database handler for maintaining trie nodes. codedb *state.CodeDB // The database handler for maintaining contract codes. + jumpDestCache vm.JumpDestCache // Shared JUMPDEST analysis cache for block processing txIndexer *txIndexer // Transaction indexer, might be nil if not enabled hc *HeaderChain @@ -408,6 +409,7 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine, db: db, triedb: triedb, codedb: state.NewCodeDB(db), + jumpDestCache: NewJumpDestCache(), triegc: prque.New[int64, common.Hash](nil), chainmu: syncx.NewClosableMutex(), bodyCache: lru.NewCache[common.Hash, *types.Body](bodyCacheLimit), @@ -2179,7 +2181,7 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, // Disable tracing for prefetcher executions. vmCfg := bc.cfg.VmConfig vmCfg.Tracer = nil - bc.prefetcher.Prefetch(block, throwaway, vmCfg, &interrupt) + bc.prefetcher.Prefetch(block, throwaway, bc.jumpDestCache, vmCfg, &interrupt) blockPrefetchExecuteTimer.Update(time.Since(start)) if interrupt.Load() { @@ -2225,7 +2227,7 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, // Process block using the parent state as reference point pstart := time.Now() pctx, _, spanEnd := telemetry.StartSpan(ctx, "bc.processor.Process") - res, err := bc.processor.Process(pctx, block, statedb, bc.cfg.VmConfig) + res, err := bc.processor.Process(pctx, block, statedb, bc.jumpDestCache, bc.cfg.VmConfig) spanEnd(&err) if err != nil { bc.reportBadBlock(block, res, err) diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 1a2ee45291..a8ddf5caa8 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -160,7 +160,7 @@ func testBlockChainImport(chain types.Blocks, blockchain *BlockChain) error { if err != nil { return err } - res, err := blockchain.processor.Process(context.Background(), block, statedb, vm.Config{}) + res, err := blockchain.processor.Process(context.Background(), block, statedb, nil, vm.Config{}) if err != nil { blockchain.reportBadBlock(block, res, err) return err diff --git a/core/jumpdest.go b/core/jumpdest.go new file mode 100644 index 0000000000..d2c861b70f --- /dev/null +++ b/core/jumpdest.go @@ -0,0 +1,76 @@ +// Copyright 2026 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 core + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/lru" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/metrics" +) + +var ( + jumpDestHitMeter = metrics.NewRegisteredMeter("chain/cache/jumpdest/hit", nil) + jumpDestMissMeter = metrics.NewRegisteredMeter("chain/cache/jumpdest/miss", nil) +) + +const ( + // jumpDestBuckets is the number of independent LRU shards. Code hashes + // are dispatched by the low bits of the first byte to spread load across + // shards and reduce mutex contention from the parallel prefetcher. + jumpDestBuckets = 8 + + // jumpDestBucketSize is the per-shard byte budget. + jumpDestBucketSize = 8 * 1024 * 1024 +) + +// shardedJumpDestCache is a thread-safe, byte-bounded LRU of JUMPDEST analysis +// bitmaps, sharded into independent buckets to reduce lock contention. It is +// owned by BlockChain and shared across block processing and prefetching, +// keyed by the immutable contract code hash. +type shardedJumpDestCache struct { + buckets [jumpDestBuckets]struct { + dest *lru.SizeConstrainedCache[common.Hash, vm.BitVec] + } +} + +// NewJumpDestCache constructs the analysis cache. +func NewJumpDestCache() vm.JumpDestCache { + c := new(shardedJumpDestCache) + for i := range c.buckets { + c.buckets[i].dest = lru.NewSizeConstrainedCache[common.Hash, vm.BitVec](jumpDestBucketSize) + } + return c +} + +// Load retrieves the cached jumpdest analysis for the given code hash. +func (c *shardedJumpDestCache) Load(hash common.Hash) (vm.BitVec, bool) { + bucket := &c.buckets[hash[0]&(jumpDestBuckets-1)] + v, ok := bucket.dest.Get(hash) + if ok { + jumpDestHitMeter.Mark(1) + } else { + jumpDestMissMeter.Mark(1) + } + return v, ok +} + +// Store saves the jumpdest analysis for the given code hash. +func (c *shardedJumpDestCache) Store(hash common.Hash, b vm.BitVec) { + bucket := &c.buckets[hash[0]&(jumpDestBuckets-1)] + bucket.dest.Add(hash, b) +} diff --git a/core/state_prefetcher.go b/core/state_prefetcher.go index d99611ff2c..c635481730 100644 --- a/core/state_prefetcher.go +++ b/core/state_prefetcher.go @@ -49,7 +49,7 @@ func newStatePrefetcher(config *params.ChainConfig, chain *HeaderChain) *statePr // Prefetch processes the state changes according to the Ethereum rules by running // the transaction messages using the statedb, but any changes are discarded. The // only goal is to warm the state caches. -func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, cfg vm.Config, interrupt *atomic.Bool) { +func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, jumpDestCache vm.JumpDestCache, cfg vm.Config, interrupt *atomic.Bool) { var ( fails atomic.Int64 header = block.Header() @@ -94,6 +94,9 @@ func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, c // Execute the message to preload the implicit touched states evm := vm.NewEVM(NewEVMBlockContext(header, p.chain, nil), stateCpy, p.config, cfg) defer evm.Release() + if jumpDestCache != nil { + evm.SetJumpDestCache(jumpDestCache) + } // Convert the transaction into an executable message and pre-cache its sender msg, err := TransactionToMessage(tx, signer, header.BaseFee) diff --git a/core/state_processor.go b/core/state_processor.go index 13466b7815..b7db261873 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -62,7 +62,7 @@ func (p *StateProcessor) chainConfig() *params.ChainConfig { // Process returns the receipts and logs accumulated during the process and // returns the amount of gas that was used in the process. If any of the // transactions failed to execute due to insufficient gas it will return an error. -func (p *StateProcessor) Process(ctx context.Context, block *types.Block, statedb *state.StateDB, cfg vm.Config) (*ProcessResult, error) { +func (p *StateProcessor) Process(ctx context.Context, block *types.Block, statedb *state.StateDB, jumpDestCache vm.JumpDestCache, cfg vm.Config) (*ProcessResult, error) { var ( config = p.chainConfig() receipts = make(types.Receipts, 0, len(block.Transactions())) @@ -86,6 +86,9 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated evm = vm.NewEVM(context, tracingStateDB, config, cfg) ) defer evm.Release() + if jumpDestCache != nil { + evm.SetJumpDestCache(jumpDestCache) + } // Run the pre-execution system calls PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), config, evm, block.Number(), block.Time()) // Iterate over and process the individual transactions diff --git a/core/stateless.go b/core/stateless.go index 86d4dc304b..805ef7ffbe 100644 --- a/core/stateless.go +++ b/core/stateless.go @@ -68,7 +68,7 @@ func ExecuteStateless(ctx context.Context, config *params.ChainConfig, vmconfig validator := NewBlockValidator(config, nil) // No chain, we only validate the state, not the block // Run the stateless blocks processing and self-validate certain fields - res, err := processor.Process(ctx, block, db, vmconfig) + res, err := processor.Process(ctx, block, db, nil, vmconfig) if err != nil { return common.Hash{}, common.Hash{}, err } diff --git a/core/types.go b/core/types.go index 87bbfcff58..6608fd6660 100644 --- a/core/types.go +++ b/core/types.go @@ -41,7 +41,7 @@ type Prefetcher interface { // Prefetch processes the state changes according to the Ethereum rules by running // the transaction messages using the statedb, but any changes are discarded. The // only goal is to pre-cache transaction signatures and state trie nodes. - Prefetch(block *types.Block, statedb *state.StateDB, cfg vm.Config, interrupt *atomic.Bool) + Prefetch(block *types.Block, statedb *state.StateDB, jumpDestCache vm.JumpDestCache, cfg vm.Config, interrupt *atomic.Bool) } // Processor is an interface for processing blocks using a given initial state. @@ -49,7 +49,7 @@ type Processor interface { // Process processes the state changes according to the Ethereum rules by running // the transaction messages using the statedb and applying any rewards to both // the processor (coinbase) and any included uncles. - Process(ctx context.Context, block *types.Block, statedb *state.StateDB, cfg vm.Config) (*ProcessResult, error) + Process(ctx context.Context, block *types.Block, statedb *state.StateDB, jumpDestCache vm.JumpDestCache, cfg vm.Config) (*ProcessResult, error) } // ProcessResult contains the values computed by Process. diff --git a/eth/state_accessor.go b/eth/state_accessor.go index 284ddf4305..3c3539dbdb 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -151,7 +151,7 @@ func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, base *st if current = eth.blockchain.GetBlockByNumber(next); current == nil { return nil, nil, fmt.Errorf("block #%d not found", next) } - _, err := eth.blockchain.Processor().Process(ctx, current, statedb, vm.Config{}) + _, err := eth.blockchain.Processor().Process(ctx, current, statedb, nil, vm.Config{}) if err != nil { return nil, nil, fmt.Errorf("processing block %d failed: %v", current.NumberU64(), err) }