core/vm: add configurable jumpdest analysis cache (#32143)

This adds a method on vm.EVM to set the jumpdest cache implementation.
It can be used to maintain an analysis cache across VM invocations, to improve
performance by skipping the analysis for already known contracts.

---------

Co-authored-by: lmittmann <lmittmann@users.noreply.github.com>
Co-authored-by: Felix Lange <fjl@twurst.com>
This commit is contained in:
lmittmann 2025-08-01 13:57:38 +02:00 committed by GitHub
parent 23da91f73b
commit 17d65e9451
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 74 additions and 23 deletions

View file

@ -25,16 +25,16 @@ const (
set7BitsMask = uint16(0b111_1111)
)
// bitvec is a bit vector which maps bytes in a program.
// BitVec is a bit vector which maps bytes in a program.
// An unset bit means the byte is an opcode, a set bit means
// it's data (i.e. argument of PUSHxx).
type bitvec []byte
type BitVec []byte
func (bits bitvec) set1(pos uint64) {
func (bits BitVec) set1(pos uint64) {
bits[pos/8] |= 1 << (pos % 8)
}
func (bits bitvec) setN(flag uint16, pos uint64) {
func (bits BitVec) setN(flag uint16, pos uint64) {
a := flag << (pos % 8)
bits[pos/8] |= byte(a)
if b := byte(a >> 8); b != 0 {
@ -42,13 +42,13 @@ func (bits bitvec) setN(flag uint16, pos uint64) {
}
}
func (bits bitvec) set8(pos uint64) {
func (bits BitVec) set8(pos uint64) {
a := byte(0xFF << (pos % 8))
bits[pos/8] |= a
bits[pos/8+1] = ^a
}
func (bits bitvec) set16(pos uint64) {
func (bits BitVec) set16(pos uint64) {
a := byte(0xFF << (pos % 8))
bits[pos/8] |= a
bits[pos/8+1] = 0xFF
@ -56,23 +56,23 @@ func (bits bitvec) set16(pos uint64) {
}
// codeSegment checks if the position is in a code segment.
func (bits *bitvec) codeSegment(pos uint64) bool {
func (bits *BitVec) codeSegment(pos uint64) bool {
return (((*bits)[pos/8] >> (pos % 8)) & 1) == 0
}
// codeBitmap collects data locations in code.
func codeBitmap(code []byte) bitvec {
func codeBitmap(code []byte) BitVec {
// The bitmap is 4 bytes longer than necessary, in case the code
// ends with a PUSH32, the algorithm will set bits on the
// bitvector outside the bounds of the actual code.
bits := make(bitvec, len(code)/8+1+4)
bits := make(BitVec, len(code)/8+1+4)
return codeBitmapInternal(code, bits)
}
// codeBitmapInternal is the internal implementation of codeBitmap.
// It exists for the purpose of being able to run benchmark tests
// without dynamic allocations affecting the results.
func codeBitmapInternal(code, bits bitvec) bitvec {
func codeBitmapInternal(code, bits BitVec) BitVec {
for pc := uint64(0); pc < uint64(len(code)); {
op := OpCode(code[pc])
pc++

View file

@ -90,7 +90,7 @@ func BenchmarkJumpdestOpAnalysis(bench *testing.B) {
for i := range code {
code[i] = byte(op)
}
bits := make(bitvec, len(code)/8+1+4)
bits := make(BitVec, len(code)/8+1+4)
b.ResetTimer()
for i := 0; i < b.N; i++ {
clear(bits)

View file

@ -31,8 +31,8 @@ type Contract struct {
caller common.Address
address common.Address
jumpdests map[common.Hash]bitvec // Aggregated result of JUMPDEST analysis.
analysis bitvec // Locally cached result of JUMPDEST analysis
jumpDests JumpDestCache // Aggregated result of JUMPDEST analysis.
analysis BitVec // Locally cached result of JUMPDEST analysis
Code []byte
CodeHash common.Hash
@ -47,15 +47,15 @@ type Contract struct {
}
// NewContract returns a new contract environment for the execution of EVM.
func NewContract(caller common.Address, address common.Address, value *uint256.Int, gas uint64, jumpDests map[common.Hash]bitvec) *Contract {
// Initialize the jump analysis map if it's nil, mostly for tests
func NewContract(caller common.Address, address common.Address, value *uint256.Int, gas uint64, jumpDests JumpDestCache) *Contract {
// Initialize the jump analysis cache if it's nil, mostly for tests
if jumpDests == nil {
jumpDests = make(map[common.Hash]bitvec)
jumpDests = newMapJumpDests()
}
return &Contract{
caller: caller,
address: address,
jumpdests: jumpDests,
jumpDests: jumpDests,
Gas: gas,
value: value,
}
@ -87,12 +87,12 @@ func (c *Contract) isCode(udest uint64) bool {
// contracts ( not temporary initcode), we store the analysis in a map
if c.CodeHash != (common.Hash{}) {
// Does parent context have the analysis?
analysis, exist := c.jumpdests[c.CodeHash]
analysis, exist := c.jumpDests.Load(c.CodeHash)
if !exist {
// Do the analysis and save in parent context
// We do not need to store it in c.analysis
analysis = codeBitmap(c.Code)
c.jumpdests[c.CodeHash] = analysis
c.jumpDests.Store(c.CodeHash, analysis)
}
// Also stash it in current contract for faster access
c.analysis = analysis

View file

@ -122,9 +122,8 @@ type EVM struct {
// precompiles holds the precompiled contracts for the current epoch
precompiles map[common.Address]PrecompiledContract
// jumpDests is the aggregated result of JUMPDEST analysis made through
// the life cycle of EVM.
jumpDests map[common.Hash]bitvec
// jumpDests stores results of JUMPDEST analysis.
jumpDests JumpDestCache
}
// NewEVM constructs an EVM instance with the supplied block context, state
@ -138,7 +137,7 @@ func NewEVM(blockCtx BlockContext, statedb StateDB, chainConfig *params.ChainCon
Config: config,
chainConfig: chainConfig,
chainRules: chainConfig.Rules(blockCtx.BlockNumber, blockCtx.Random != nil, blockCtx.Time),
jumpDests: make(map[common.Hash]bitvec),
jumpDests: newMapJumpDests(),
}
evm.precompiles = activePrecompiledContracts(evm.chainRules)
evm.interpreter = NewEVMInterpreter(evm)
@ -152,6 +151,11 @@ func (evm *EVM) SetPrecompiles(precompiles PrecompiledContracts) {
evm.precompiles = precompiles
}
// SetJumpDestCache configures the analysis cache.
func (evm *EVM) SetJumpDestCache(jumpDests JumpDestCache) {
evm.jumpDests = jumpDests
}
// 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) {

47
core/vm/jumpdests.go Normal file
View file

@ -0,0 +1,47 @@
// Copyright 2024 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 "github.com/ethereum/go-ethereum/common"
// JumpDestCache represents the cache of jumpdest analysis results.
type JumpDestCache interface {
// Load retrieves the cached jumpdest analysis for the given code hash.
// Returns the BitVec and true if found, or nil and false if not cached.
Load(codeHash common.Hash) (BitVec, bool)
// Store saves the jumpdest analysis for the given code hash.
Store(codeHash common.Hash, vec BitVec)
}
// mapJumpDests is the default implementation of JumpDests using a map.
// This implementation is not thread-safe and is meant to be used per EVM instance.
type mapJumpDests map[common.Hash]BitVec
// newMapJumpDests creates a new map-based JumpDests implementation.
func newMapJumpDests() JumpDestCache {
return make(mapJumpDests)
}
func (j mapJumpDests) Load(codeHash common.Hash) (BitVec, bool) {
vec, ok := j[codeHash]
return vec, ok
}
func (j mapJumpDests) Store(codeHash common.Hash, vec BitVec) {
j[codeHash] = vec
}