diff --git a/core/blockchain.go b/core/blockchain.go index 296ef6bc16..81700f0e88 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -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) 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..cf8512f22e --- /dev/null +++ b/core/jumpdest.go @@ -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 . + +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) +} diff --git a/core/state_prefetcher.go b/core/state_prefetcher.go index ed292d0beb..b36f8b7bba 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, 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) diff --git a/core/state_processor.go b/core/state_processor.go index 54ebbd047b..0994d19226 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -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) 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..935202a441 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, 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. diff --git a/core/vm/evm.go b/core/vm/evm.go index 81de3f2d7a..26b2f73a00 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -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) diff --git a/core/vm/jumpdests.go b/core/vm/jumpdests.go index 576be7e9e5..1a30c1943f 100644 --- a/core/vm/jumpdests.go +++ b/core/vm/jumpdests.go @@ -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) -} diff --git a/eth/state_accessor.go b/eth/state_accessor.go index a806a4fc56..f7ecff1488 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) }