mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-30 08:33:45 +00:00
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:
parent
1bfc028d43
commit
d6f222a081
14 changed files with 298 additions and 42 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
198
core/vm/precompile_cache.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
Loading…
Reference in a new issue