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 <garyrong0905@gmail.com>
This commit is contained in:
Jonny Rhea 2026-07-27 21:47:33 -05:00 committed by GitHub
parent 1bfc028d43
commit d6f222a081
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 298 additions and 42 deletions

View file

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

View file

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

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

View file

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

View file

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

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, nil, vmconfig, nil)
res, err := processor.Process(ctx, block, db, nil, nil, vmconfig, nil)
if err != nil {
return common.Hash{}, common.Hash{}, err
}

View file

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

View file

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

View file

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

View file

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

View file

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

198
core/vm/precompile_cache.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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)
}

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

View file

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