diff --git a/core/blockchain.go b/core/blockchain.go index 0c495966a5..7027a17ddd 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -323,16 +323,17 @@ type BlockChain struct { chainConfig *params.ChainConfig // Chain & network configuration cfg *BlockChainConfig // Blockchain configuration - db ethdb.Database // Low level persistent database to store final content in - snaps *snapshot.Tree // Snapshot tree for fast trie leaf access - triegc *prque.Prque[int64, common.Hash] // Priority queue mapping block numbers to tries to gc - gcproc time.Duration // Accumulates canonical block processing for trie dumping - lastWrite uint64 // Last block when the state was flushed - 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 + db ethdb.Database // Low level persistent database to store final content in + snaps *snapshot.Tree // Snapshot tree for fast trie leaf access + triegc *prque.Prque[int64, common.Hash] // Priority queue mapping block numbers to tries to gc + gcproc time.Duration // Accumulates canonical block processing for trie dumping + lastWrite uint64 // Last block when the state was flushed + 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 + precompileCache *vm.PrecompileCache // Shared precompile result cache for block processing, nil when disabled + txIndexer *txIndexer // Transaction indexer, might be nil if not enabled hc *HeaderChain rmLogsFeed event.Feed @@ -415,6 +416,7 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine, triedb: triedb, codedb: state.NewCodeDB(db), jumpDestCache: NewJumpDestCache(), + precompileCache: vm.NewPrecompileCache(), triegc: prque.New[int64, common.Hash](nil), chainmu: syncx.NewClosableMutex(), bodyCache: lru.NewCache[common.Hash, *types.Body](bodyCacheLimit), @@ -2196,7 +2198,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, bc.jumpDestCache, vmCfg, &interrupt, &execIndex) + bc.prefetcher.Prefetch(block, throwaway, bc.jumpDestCache, bc.precompileCache.PrefetchView(), vmCfg, &interrupt, &execIndex) blockPrefetchExecuteTimer.Update(time.Since(start)) if interrupt.Load() { @@ -2242,7 +2244,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.jumpDestCache, bc.cfg.VmConfig, &execIndex) + res, err := bc.processor.Process(pctx, block, statedb, bc.jumpDestCache, bc.precompileCache, bc.cfg.VmConfig, &execIndex) spanEnd(&err) if err != nil { bc.reportBadBlock(block, res, err) diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go index 314226ebc8..0e65aa4059 100644 --- a/core/blockchain_reader.go +++ b/core/blockchain_reader.go @@ -524,6 +524,12 @@ func (bc *BlockChain) CodeDB() *state.CodeDB { return bc.codedb } +// PrecompileCache retrieves the shared precompile result cache, so callers +// such as the miner can reuse results already computed during block import. +func (bc *BlockChain) PrecompileCache() *vm.PrecompileCache { + return bc.precompileCache +} + // JumpDestCache retrieves the shared JUMPDEST analysis cache, so callers such // as the miner can reuse bitmaps already computed during block import. func (bc *BlockChain) JumpDestCache() vm.JumpDestCache { diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 18b9f837f4..863e53474f 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, nil, vm.Config{}, nil) + res, err := blockchain.processor.Process(context.Background(), block, statedb, nil, nil, vm.Config{}, nil) if err != nil { blockchain.reportBadBlock(block, res, err) return err diff --git a/core/state_prefetcher.go b/core/state_prefetcher.go index 93181f5451..0279a45cfd 100644 --- a/core/state_prefetcher.go +++ b/core/state_prefetcher.go @@ -50,7 +50,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, jumpDestCache vm.JumpDestCache, cfg vm.Config, interrupt *atomic.Bool, execIndex *atomic.Int64) { +func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, jumpDestCache vm.JumpDestCache, precompileCache *vm.PrecompileCache, cfg vm.Config, interrupt *atomic.Bool, execIndex *atomic.Int64) { var ( fails atomic.Int64 skips atomic.Int64 @@ -104,10 +104,14 @@ func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, j // Execute the message to preload the implicit touched states evm := vm.NewEVM(NewEVMBlockContext(header, p.chain, nil), stateCpy, p.config, cfg) defer evm.Release() + + // Set the caches for EVM interpreter if jumpDestCache != nil { evm.SetJumpDestCache(jumpDestCache) } - + if precompileCache != nil { + evm.SetPrecompileCache(precompileCache) + } // Convert the transaction into an executable message and pre-cache its sender msg, err := TransactionToMessage(tx, signer, header.BaseFee) if err != nil { diff --git a/core/state_processor.go b/core/state_processor.go index ea677d42d3..c962c3dc5e 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -64,7 +64,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, jumpDestCache vm.JumpDestCache, cfg vm.Config, execIndex *atomic.Int64) (*ProcessResult, error) { +func (p *StateProcessor) Process(ctx context.Context, block *types.Block, statedb *state.StateDB, jumpDestCache vm.JumpDestCache, precompileCache *vm.PrecompileCache, cfg vm.Config, execIndex *atomic.Int64) (*ProcessResult, error) { var ( config = p.chainConfig() receipts = make(types.Receipts, 0, len(block.Transactions())) @@ -97,6 +97,9 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated if jumpDestCache != nil { evm.SetJumpDestCache(jumpDestCache) } + if precompileCache != nil { + evm.SetPrecompileCache(precompileCache) + } // Run the pre-execution system calls blockAccessList.Merge(PreExecution(ctx, block.BeaconRoot(), parent, config, evm, block.Number(), block.Time())) diff --git a/core/stateless.go b/core/stateless.go index 3cbdc549d7..6c094248bd 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, nil, vmconfig, nil) + res, err := processor.Process(ctx, block, db, nil, nil, vmconfig, nil) if err != nil { return common.Hash{}, common.Hash{}, err } diff --git a/core/types.go b/core/types.go index 76354f81dd..82c898916f 100644 --- a/core/types.go +++ b/core/types.go @@ -42,7 +42,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, jumpDestCache vm.JumpDestCache, cfg vm.Config, interrupt *atomic.Bool, execIndex *atomic.Int64) + Prefetch(block *types.Block, statedb *state.StateDB, jumpDestCache vm.JumpDestCache, precompileCache *vm.PrecompileCache, cfg vm.Config, interrupt *atomic.Bool, execIndex *atomic.Int64) } // Processor is an interface for processing blocks using a given initial state. @@ -50,7 +50,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, jumpDestCache vm.JumpDestCache, cfg vm.Config, execIndex *atomic.Int64) (*ProcessResult, error) + Process(ctx context.Context, block *types.Block, statedb *state.StateDB, jumpDestCache vm.JumpDestCache, precompileCache *vm.PrecompileCache, cfg vm.Config, execIndex *atomic.Int64) (*ProcessResult, error) } // ProcessResult contains the values computed by Process. diff --git a/core/vm/contracts.go b/core/vm/contracts.go index 7cfe63be2a..9b2f14d0a1 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -211,32 +211,35 @@ func init() { } } -func activePrecompiledContracts(rules params.Rules) PrecompiledContracts { +// activePrecompiledContracts returns a pointer to the precompile set variable +// of the given rules. The pointer doubles as the identity of the set in the +// precompile result cache, forks reusing a set share its cache entries. +func activePrecompiledContracts(rules params.Rules) *PrecompiledContracts { switch { case rules.IsUBT: - return PrecompiledContractsVerkle + return &PrecompiledContractsVerkle case rules.IsBogota: - return PrecompiledContractsOsaka + return &PrecompiledContractsOsaka case rules.IsOsaka: - return PrecompiledContractsOsaka + return &PrecompiledContractsOsaka case rules.IsPrague: - return PrecompiledContractsPrague + return &PrecompiledContractsPrague case rules.IsCancun: - return PrecompiledContractsCancun + return &PrecompiledContractsCancun case rules.IsBerlin: - return PrecompiledContractsBerlin + return &PrecompiledContractsBerlin case rules.IsIstanbul: - return PrecompiledContractsIstanbul + return &PrecompiledContractsIstanbul case rules.IsByzantium: - return PrecompiledContractsByzantium + return &PrecompiledContractsByzantium default: - return PrecompiledContractsHomestead + return &PrecompiledContractsHomestead } } // ActivePrecompiledContracts returns a copy of precompiled contracts enabled with the current configuration. func ActivePrecompiledContracts(rules params.Rules) PrecompiledContracts { - return maps.Clone(activePrecompiledContracts(rules)) + return maps.Clone(*activePrecompiledContracts(rules)) } // ActivePrecompiles returns the precompile addresses enabled with the current configuration. @@ -266,7 +269,7 @@ func ActivePrecompiles(rules params.Rules) []common.Address { // - the returned bytes, // - the remaining gas budget, // - any error that occurred -func RunPrecompiledContract(stateDB StateDB, p PrecompiledContract, address common.Address, input []byte, gas GasBudget, logger *tracing.Hooks, rules params.Rules) (ret []byte, remaining GasBudget, err error) { +func RunPrecompiledContract(stateDB StateDB, p PrecompiledContract, address common.Address, input []byte, gas GasBudget, logger *tracing.Hooks, rules params.Rules, cache *PrecompileCache) (ret []byte, remaining GasBudget, err error) { gasCost := p.RequiredGas(input) prior, ok := gas.ChargeRegular(gasCost) if !ok { @@ -280,6 +283,23 @@ func RunPrecompiledContract(stateDB StateDB, p PrecompiledContract, address comm if rules.IsAmsterdam { stateDB.Touch(address) } + // Serve pure precompiles from the shared result cache if one is attached. + // Gas accounting and state touching above are identical on hit and miss, + // only the recomputation is skipped. + if cache != nil && cacheablePrecompile(p, input) { + var ( + set = activePrecompiledContracts(rules) + key = precompileCacheKey(address, input) + ) + if output, ok := cache.load(set, address, key); ok { + return output, gas, nil + } + output, err := p.Run(input) + if err == nil && len(output) <= maxCacheablePrecompileOutput { + cache.store(set, address, key, output) + } + return output, gas, err + } output, err := p.Run(input) return output, gas, err } @@ -345,6 +365,10 @@ func (c *sha256hash) Name() string { return "SHA256" } +// Cacheable opts out of result caching, deriving the cache key costs about +// as much as running the hash itself. +func (c *sha256hash) Cacheable() bool { return false } + // RIPEMD160 implemented as a native contract. type ripemd160hash struct{} @@ -365,6 +389,10 @@ func (c *ripemd160hash) Name() string { return "RIPEMD160" } +// Cacheable opts out of result caching, hashing the input for the cache key +// costs about as much as running it. +func (c *ripemd160hash) Cacheable() bool { return false } + // data copy implemented as a native contract. type dataCopy struct{} @@ -383,6 +411,10 @@ func (c *dataCopy) Name() string { return "ID" } +// Cacheable opts out of result caching, identity is cheaper to rerun than +// to cache. +func (c *dataCopy) Cacheable() bool { return false } + // bigModExp implements a native big integer exponential modular operation. type bigModExp struct { eip2565 bool diff --git a/core/vm/contracts_fuzz_test.go b/core/vm/contracts_fuzz_test.go index 4d28df6a6a..8d81197e93 100644 --- a/core/vm/contracts_fuzz_test.go +++ b/core/vm/contracts_fuzz_test.go @@ -37,7 +37,7 @@ func FuzzPrecompiledContracts(f *testing.F) { return } inWant := string(input) - RunPrecompiledContract(nil, p, a, input, NewGasBudget(gas, 0), nil, params.Rules{}) + RunPrecompiledContract(nil, p, a, input, NewGasBudget(gas, 0), nil, params.Rules{}, nil) if inHave := string(input); inWant != inHave { t.Errorf("Precompiled %v modified input data", a) } diff --git a/core/vm/contracts_test.go b/core/vm/contracts_test.go index c6975bd0a6..4bc0bddf60 100644 --- a/core/vm/contracts_test.go +++ b/core/vm/contracts_test.go @@ -100,7 +100,7 @@ func testPrecompiled(addr string, test precompiledTest, t *testing.T) { in := common.Hex2Bytes(test.Input) gas := p.RequiredGas(in) t.Run(fmt.Sprintf("%s-Gas=%d", test.Name, gas), func(t *testing.T) { - if res, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas, 0), nil, params.Rules{}); err != nil { + if res, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas, 0), nil, params.Rules{}, nil); err != nil { t.Error(err) } else if common.Bytes2Hex(res) != test.Expected { t.Errorf("Expected %v, got %v", test.Expected, common.Bytes2Hex(res)) @@ -122,7 +122,7 @@ func testPrecompiledOOG(addr string, test precompiledTest, t *testing.T) { gas := test.Gas - 1 t.Run(fmt.Sprintf("%s-Gas=%d", test.Name, gas), func(t *testing.T) { - _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas, 0), nil, params.Rules{}) + _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas, 0), nil, params.Rules{}, nil) if err.Error() != "out of gas" { t.Errorf("Expected error [out of gas], got [%v]", err) } @@ -139,7 +139,7 @@ func testPrecompiledFailure(addr string, test precompiledFailureTest, t *testing in := common.Hex2Bytes(test.Input) gas := p.RequiredGas(in) t.Run(test.Name, func(t *testing.T) { - _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas, 0), nil, params.Rules{}) + _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas, 0), nil, params.Rules{}, nil) if err.Error() != test.ExpectedError { t.Errorf("Expected error [%v], got [%v]", test.ExpectedError, err) } @@ -170,7 +170,7 @@ func benchmarkPrecompiled(addr string, test precompiledTest, bench *testing.B) { start := time.Now() for bench.Loop() { copy(data, in) - res, _, err = RunPrecompiledContract(nil, p, common.HexToAddress(addr), data, NewGasBudget(reqGas, 0), nil, params.Rules{}) + res, _, err = RunPrecompiledContract(nil, p, common.HexToAddress(addr), data, NewGasBudget(reqGas, 0), nil, params.Rules{}, nil) } elapsed := uint64(time.Since(start)) if elapsed < 1 { diff --git a/core/vm/evm.go b/core/vm/evm.go index 18aa50a8c8..b94215277f 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -127,6 +127,9 @@ type EVM struct { // jumpDests stores results of JUMPDEST analysis. jumpDests JumpDestCache + // precompileCache stores outputs of pure precompile runs, may be nil. + precompileCache *PrecompileCache + readOnly bool // Whether to throw on stateful modifications returnData []byte // Last CALL's return data for subsequent reuse @@ -147,7 +150,7 @@ func NewEVM(blockCtx BlockContext, statedb StateDB, chainConfig *params.ChainCon jumpDests: newMapJumpDests(), arena: newArena(), } - evm.precompiles = activePrecompiledContracts(evm.chainRules) + evm.precompiles = *activePrecompiledContracts(evm.chainRules) switch { case evm.chainRules.IsBogota: @@ -208,6 +211,8 @@ func NewEVM(blockCtx BlockContext, statedb StateDB, chainConfig *params.ChainCon // It is not thread-safe. func (evm *EVM) SetPrecompiles(precompiles PrecompiledContracts) { evm.precompiles = precompiles + // Overridden precompiles no longer match the address keyed result cache. + evm.precompileCache = nil } // SetJumpDestCache configures the analysis cache. @@ -215,6 +220,11 @@ func (evm *EVM) SetJumpDestCache(jumpDests JumpDestCache) { evm.jumpDests = jumpDests } +// SetPrecompileCache configures the precompile result cache. +func (evm *EVM) SetPrecompileCache(cache *PrecompileCache) { + evm.precompileCache = cache +} + // SetTxContext resets the EVM with a new transaction context. // This is not threadsafe and should only be done very cautiously. func (evm *EVM) SetTxContext(txCtx TxContext) { @@ -299,7 +309,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g } if isPrecompile { - ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules) + ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules, evm.precompileCache) } else { // Initialise a new contract and set the code that is to be used by the EVM. code := evm.resolveCode(addr) @@ -356,7 +366,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { - ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules) + ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules, evm.precompileCache) } else { // Initialise a new contract and set the code that is to be used by the EVM. // The contract is a scoped environment for this execution context only. @@ -402,7 +412,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { - ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules) + ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules, evm.precompileCache) } else { contract := NewContract(originCaller, caller, value, gas, evm.jumpDests) contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr)) @@ -454,7 +464,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b evm.StateDB.AddBalance(addr, new(uint256.Int), tracing.BalanceChangeTouchAccount) if p, isPrecompile := evm.precompile(addr); isPrecompile { - ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules) + ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules, evm.precompileCache) } else { contract := NewContract(caller, addr, new(uint256.Int), gas, evm.jumpDests) contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr)) diff --git a/core/vm/precompile_cache.go b/core/vm/precompile_cache.go new file mode 100644 index 0000000000..b725d30bc4 --- /dev/null +++ b/core/vm/precompile_cache.go @@ -0,0 +1,198 @@ +// 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 vm + +import ( + "fmt" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/lru" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/metrics" +) + +var ( + precompileCacheHitMeter = metrics.NewRegisteredMeter("chain/cache/precompile/hit", nil) + precompileCacheMissMeter = metrics.NewRegisteredMeter("chain/cache/precompile/miss", nil) + precompileCachePrefetchHitMeter = metrics.NewRegisteredMeter("chain/cache/precompile/prefetch/hit", nil) + precompileCachePrefetchMissMeter = metrics.NewRegisteredMeter("chain/cache/precompile/prefetch/miss", nil) + precompileCacheEntryGauge = metrics.NewRegisteredGauge("chain/cache/precompile/entries", nil) +) + +const ( + // maxCacheablePrecompileInput bounds the input size eligible for result + // caching. Larger inputs are rare one-offs and hashing them for the key + // eats into the win. + maxCacheablePrecompileInput = 8192 + + // maxCacheablePrecompileOutput bounds the output size stored in the + // cache, keeping the worst case memory use of an entry small. + maxCacheablePrecompileOutput = 1024 + + // precompileCacheEntries is the maximum number of cached results. With + // outputs capped by maxCacheablePrecompileOutput, the worst case memory + // use stays at a few megabytes. + precompileCacheEntries = 4096 +) + +// PrecompileCache is a thread-safe LRU of precompile outputs, shared between +// the state prefetcher and block processing so the serial pass can reuse +// results the prefetcher already computed. Entries are namespaced by +// precompile set, so forks never share results across a behaviour change. +type PrecompileCache struct { + data *precompileCacheData + + // Meters are per handle, split between the main pass and the prefetcher + // so the hit rate of the main pass stays readable on its own. + prefix string + hit *metrics.Meter + miss *metrics.Meter + mu sync.RWMutex + meters map[common.Address]*precompileCacheMeters + prefetch *PrecompileCache +} + +// precompileCacheData is the storage shared by the two cache handles. +type precompileCacheData struct { + mu sync.RWMutex + sets map[*PrecompiledContracts]*lru.Cache[common.Hash, []byte] +} + +// precompileCacheMeters holds the per-address hit and miss meters. +type precompileCacheMeters struct { + hit *metrics.Meter + miss *metrics.Meter +} + +// NewPrecompileCache constructs a precompile result cache. +func NewPrecompileCache() *PrecompileCache { + data := &precompileCacheData{ + sets: make(map[*PrecompiledContracts]*lru.Cache[common.Hash, []byte]), + } + return &PrecompileCache{ + data: data, + prefix: "chain/cache/precompile", + hit: precompileCacheHitMeter, + miss: precompileCacheMissMeter, + meters: make(map[common.Address]*precompileCacheMeters), + prefetch: &PrecompileCache{ + data: data, + prefix: "chain/cache/precompile/prefetch", + hit: precompileCachePrefetchHitMeter, + miss: precompileCachePrefetchMissMeter, + meters: make(map[common.Address]*precompileCacheMeters), + }, + } +} + +// PrefetchView returns a handle of the same cache that marks the prefetcher +// meters instead of the main pass ones. +func (c *PrecompileCache) PrefetchView() *PrecompileCache { + if c == nil { + return nil + } + return c.prefetch +} + +// load retrieves the cached output for the given key. The returned slice is +// a private copy owned by the caller, entries cross goroutine boundaries. +func (c *PrecompileCache) load(set *PrecompiledContracts, addr common.Address, key common.Hash) ([]byte, bool) { + c.data.mu.RLock() + results := c.data.sets[set] + c.data.mu.RUnlock() + + meters := c.metersFor(addr) + if results != nil { + if output, ok := results.Get(key); ok { + c.hit.Mark(1) + meters.hit.Mark(1) + return common.CopyBytes(output), true + } + } + c.miss.Mark(1) + meters.miss.Mark(1) + return nil, false +} + +// store saves the output of a precompile run under the given key. The value +// is copied, the cache never aliases caller memory. +func (c *PrecompileCache) store(set *PrecompiledContracts, addr common.Address, key common.Hash, output []byte) { + c.data.mu.RLock() + results := c.data.sets[set] + c.data.mu.RUnlock() + + if results == nil { + c.data.mu.Lock() + if results = c.data.sets[set]; results == nil { + results = lru.NewCache[common.Hash, []byte](precompileCacheEntries) + c.data.sets[set] = results + } + c.data.mu.Unlock() + } + results.Add(key, common.CopyBytes(output)) + precompileCacheEntryGauge.Update(int64(results.Len())) +} + +// metersFor returns the hit and miss meters of the given precompile address, +// registering them on first use. +func (c *PrecompileCache) metersFor(addr common.Address) *precompileCacheMeters { + c.mu.RLock() + meters, ok := c.meters[addr] + c.mu.RUnlock() + if ok { + return meters + } + c.mu.Lock() + defer c.mu.Unlock() + if meters, ok = c.meters[addr]; ok { + return meters + } + prefix := fmt.Sprintf("%s/%#x", c.prefix, addr.Big()) + meters = &precompileCacheMeters{ + hit: metrics.GetOrRegisterMeter(prefix+"/hit", nil), + miss: metrics.GetOrRegisterMeter(prefix+"/miss", nil), + } + c.meters[addr] = meters + return meters +} + +// CacheablePrecompile lets a precompile opt out of result caching, either +// because its output is not a pure function of the input or because it is +// cheaper to rerun than to cache. +type CacheablePrecompile interface { + Cacheable() bool +} + +// cacheablePrecompile reports whether an invocation is eligible for result +// caching. +func cacheablePrecompile(p PrecompiledContract, input []byte) bool { + if len(input) > maxCacheablePrecompileInput { + return false + } + if c, ok := p.(CacheablePrecompile); ok { + return c.Cacheable() + } + return true +} + +// precompileCacheKey derives the cache key for a precompile invocation. Fork +// discrimination is handled by the set namespacing, so the key only covers +// the address and input. +func precompileCacheKey(addr common.Address, input []byte) common.Hash { + return crypto.Keccak256Hash(addr[:], input) +} diff --git a/eth/state_accessor.go b/eth/state_accessor.go index ebf6053921..de28fbee8f 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, nil, vm.Config{}, nil) + _, err := eth.blockchain.Processor().Process(ctx, current, statedb, nil, nil, vm.Config{}, nil) if err != nil { return nil, nil, fmt.Errorf("processing block %d failed: %v", current.NumberU64(), err) } diff --git a/miner/worker.go b/miner/worker.go index e0e49217f8..fe7043c1dc 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -360,6 +360,7 @@ func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase state.StartPrefetcher("miner", bundle) evm := vm.NewEVM(core.NewEVMBlockContext(header, miner.chain, &coinbase), state, miner.chainConfig, vm.Config{}) evm.SetJumpDestCache(miner.chain.JumpDestCache()) + evm.SetPrecompileCache(miner.chain.PrecompileCache()) // Note the passed coinbase may be different with header.Coinbase. return &environment{