From d6f222a08162745d2609caa7020a5efad9f7d103 Mon Sep 17 00:00:00 2001 From: Jonny Rhea <5555162+jrhea@users.noreply.github.com> Date: Mon, 27 Jul 2026 21:47:33 -0500 Subject: [PATCH] core, core/vm: precompile result caching (#35388) Blocks heavy in precompile calls (e.g. Aztec's proof settlement txs) spend most of their processing time on operations (ECMUL, pairings, KZG point evaluation, MODEXP) that the state prefetcher has already computed and thrown away. This PR adds a precompile result cache shared between the prefetcher and block processing (and the miner), following the JumpDestCache pattern. Note that cached precompiles are keyed by address and input, with entries partitioned by the active precompile set, so a fork that changes the behavior behind an address can never be served results from before it. --------- Co-authored-by: Gary Rong --- core/blockchain.go | 26 +++-- core/blockchain_reader.go | 6 + core/blockchain_test.go | 2 +- core/state_prefetcher.go | 8 +- core/state_processor.go | 5 +- core/stateless.go | 2 +- core/types.go | 4 +- core/vm/contracts.go | 56 ++++++++-- core/vm/contracts_fuzz_test.go | 2 +- core/vm/contracts_test.go | 8 +- core/vm/evm.go | 20 +++- core/vm/precompile_cache.go | 198 +++++++++++++++++++++++++++++++++ eth/state_accessor.go | 2 +- miner/worker.go | 1 + 14 files changed, 298 insertions(+), 42 deletions(-) create mode 100644 core/vm/precompile_cache.go 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{