core, eth: move jumpdest cache to BlockChain

Replaces the process-global LRU with a chain-owned, byte-bounded
SizeConstrainedCache (128MB) shared between block processor and
prefetcher. Threaded through Process/Prefetch as an optional argument;
RPC, tracing, and stateless callers pass nil and the EVM falls back to
its per-instance map. Adds chain/cache/jumpdest/{hit,miss} meters.
This commit is contained in:
Sina Mahmoodi 2026-05-07 19:46:51 +00:00
parent 3f49929091
commit 5c4a89562d
10 changed files with 82 additions and 34 deletions

View file

@ -324,6 +324,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.
jumpDest *JumpDestCache // Shared JUMPDEST analysis cache for block processing
txIndexer *txIndexer // Transaction indexer, might be nil if not enabled
hc *HeaderChain
@ -406,6 +407,7 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine,
db: db,
triedb: triedb,
codedb: state.NewCodeDB(db),
jumpDest: NewJumpDestCache(),
triegc: prque.New[int64, common.Hash](nil),
chainmu: syncx.NewClosableMutex(),
bodyCache: lru.NewCache[common.Hash, *types.Body](bodyCacheLimit),
@ -2176,7 +2178,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.jumpDest, vmCfg, &interrupt)
blockPrefetchExecuteTimer.Update(time.Since(start))
if interrupt.Load() {
@ -2222,7 +2224,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.jumpDest, bc.cfg.VmConfig)
spanEnd(&err)
if err != nil {
bc.reportBadBlock(block, res, err)

View file

@ -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

63
core/jumpdest.go Normal file
View file

@ -0,0 +1,63 @@
// 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 <http://www.gnu.org/licenses/>.
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)
)
// jumpDestCacheSize is the total memory budget granted to the jumpdest
// analysis cache.
const jumpDestCacheSize = 128 * 1024 * 1024
// JumpDestCache is a thread-safe, byte-bounded LRU of JUMPDEST analysis
// bitmaps. It is owned by BlockChain and shared across block processing and
// prefetching, keyed by the immutable contract code hash.
type JumpDestCache struct {
cache *lru.SizeConstrainedCache[common.Hash, vm.BitVec]
}
// NewJumpDestCache constructs the analysis cache.
func NewJumpDestCache() *JumpDestCache {
return &JumpDestCache{
cache: lru.NewSizeConstrainedCache[common.Hash, vm.BitVec](jumpDestCacheSize),
}
}
// Load retrieves the cached jumpdest analysis for the given code hash.
func (c *JumpDestCache) Load(hash common.Hash) (vm.BitVec, bool) {
v, ok := c.cache.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 *JumpDestCache) Store(hash common.Hash, b vm.BitVec) {
c.cache.Add(hash, b)
}

View file

@ -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, jumpDests *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 jumpDests != nil {
evm.SetJumpDestCache(jumpDests)
}
// Convert the transaction into an executable message and pre-cache its sender
msg, err := TransactionToMessage(tx, signer, header.BaseFee)

View file

@ -61,7 +61,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, jumpDests *JumpDestCache, cfg vm.Config) (*ProcessResult, error) {
var (
config = p.chainConfig()
receipts = make(types.Receipts, 0, len(block.Transactions()))
@ -89,6 +89,9 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated
context = NewEVMBlockContext(header, p.chain, nil)
evm := vm.NewEVM(context, tracingStateDB, config, cfg)
defer evm.Release()
if jumpDests != nil {
evm.SetJumpDestCache(jumpDests)
}
if beaconRoot := block.BeaconRoot(); beaconRoot != nil {
ProcessBeaconBlockRoot(*beaconRoot, evm)

View file

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

View file

@ -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, jumpDests *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, jumpDests *JumpDestCache, cfg vm.Config) (*ProcessResult, error)
}
// ProcessResult contains the values computed by Process.

View file

@ -142,7 +142,7 @@ func NewEVM(blockCtx BlockContext, statedb StateDB, chainConfig *params.ChainCon
Config: config,
chainConfig: chainConfig,
chainRules: chainConfig.Rules(blockCtx.BlockNumber, blockCtx.Random != nil, blockCtx.Time),
jumpDests: globalJumpDests,
jumpDests: newMapJumpDests(),
arena: newArena(),
}
evm.precompiles = activePrecompiledContracts(evm.chainRules)

View file

@ -16,14 +16,7 @@
package vm
import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/lru"
)
// globalJumpDestCacheSize caps the global cache. Worst case ~48MB (24KB code
// → ~3KB bitmap × 16384 entries).
const globalJumpDestCacheSize = 16384
import "github.com/ethereum/go-ethereum/common"
// JumpDestCache represents the cache of jumpdest analysis results.
type JumpDestCache interface {
@ -52,19 +45,3 @@ func (j mapJumpDests) Load(codeHash common.Hash) (BitVec, bool) {
func (j mapJumpDests) Store(codeHash common.Hash, vec BitVec) {
j[codeHash] = vec
}
// globalJumpDests is a process-global LRU of JUMPDEST bitmaps, shared across
// every EVM instance and keyed by the immutable contract code hash.
var globalJumpDests = &lruJumpDests{cache: lru.NewCache[common.Hash, BitVec](globalJumpDestCacheSize)}
type lruJumpDests struct {
cache *lru.Cache[common.Hash, BitVec]
}
func (j *lruJumpDests) Load(codeHash common.Hash) (BitVec, bool) {
return j.cache.Get(codeHash)
}
func (j *lruJumpDests) Store(codeHash common.Hash, vec BitVec) {
j.cache.Add(codeHash, vec)
}

View file

@ -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)
}