core/vm: use methods over direct access to stack internals

This commit is contained in:
Martin Holst Swende 2024-08-16 10:21:13 +02:00 committed by MariusVanDerWijden
parent a4959685a2
commit 0c4ece0166
6 changed files with 193 additions and 86 deletions

View file

@ -125,6 +125,8 @@ type EVM struct {
// jumpDests is the aggregated result of JUMPDEST analysis made through // jumpDests is the aggregated result of JUMPDEST analysis made through
// the life cycle of EVM. // the life cycle of EVM.
jumpDests map[common.Hash]bitvec jumpDests map[common.Hash]bitvec
arena *stackArena
} }
// NewEVM constructs an EVM instance with the supplied block context, state // NewEVM constructs an EVM instance with the supplied block context, state
@ -139,6 +141,7 @@ func NewEVM(blockCtx BlockContext, statedb StateDB, chainConfig *params.ChainCon
chainConfig: chainConfig, chainConfig: chainConfig,
chainRules: chainConfig.Rules(blockCtx.BlockNumber, blockCtx.Random != nil, blockCtx.Time), chainRules: chainConfig.Rules(blockCtx.BlockNumber, blockCtx.Random != nil, blockCtx.Time),
jumpDests: make(map[common.Hash]bitvec), jumpDests: make(map[common.Hash]bitvec),
arena: newArena(),
} }
evm.precompiles = activePrecompiledContracts(evm.chainRules) evm.precompiles = activePrecompiledContracts(evm.chainRules)
evm.interpreter = NewEVMInterpreter(evm) evm.interpreter = NewEVMInterpreter(evm)

View file

@ -345,7 +345,7 @@ func gasCreate2Eip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory,
} }
func gasExpFrontier(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { func gasExpFrontier(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
expByteLen := uint64((stack.data[stack.len()-2].BitLen() + 7) / 8) expByteLen := uint64((stack.Back(1).BitLen() + 7) / 8)
var ( var (
gas = expByteLen * params.ExpByteFrontier // no overflow check required. Max is 256 * ExpByte gas gas = expByteLen * params.ExpByteFrontier // no overflow check required. Max is 256 * ExpByte gas
@ -358,7 +358,7 @@ func gasExpFrontier(evm *EVM, contract *Contract, stack *Stack, mem *Memory, mem
} }
func gasExpEIP158(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { func gasExpEIP158(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
expByteLen := uint64((stack.data[stack.len()-2].BitLen() + 7) / 8) expByteLen := uint64((stack.Back(1).BitLen() + 7) / 8)
var ( var (
gas = expByteLen * params.ExpByteEIP158 // no overflow check required. Max is 256 * ExpByte gas gas = expByteLen * params.ExpByteEIP158 // no overflow check required. Max is 256 * ExpByte gas

View file

@ -97,7 +97,7 @@ func init() {
func testTwoOperandOp(t *testing.T, tests []TwoOperandTestcase, opFn executionFunc, name string) { func testTwoOperandOp(t *testing.T, tests []TwoOperandTestcase, opFn executionFunc, name string) {
var ( var (
evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{}) evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{})
stack = newstack() stack = newStackForTesting()
pc = uint64(0) pc = uint64(0)
) )
@ -108,8 +108,8 @@ func testTwoOperandOp(t *testing.T, tests []TwoOperandTestcase, opFn executionFu
stack.push(x) stack.push(x)
stack.push(y) stack.push(y)
opFn(&pc, evm.interpreter, &ScopeContext{nil, stack, nil}) opFn(&pc, evm.interpreter, &ScopeContext{nil, stack, nil})
if len(stack.data) != 1 { if stack.len() != 1 {
t.Errorf("Expected one item on stack after %v, got %d: ", name, len(stack.data)) t.Errorf("Expected one item on stack after %v, got %d: ", name, stack.len())
} }
actual := stack.pop() actual := stack.pop()
@ -195,7 +195,7 @@ func TestSAR(t *testing.T) {
func TestAddMod(t *testing.T) { func TestAddMod(t *testing.T) {
var ( var (
evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{}) evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{})
stack = newstack() stack = newStackForTesting()
pc = uint64(0) pc = uint64(0)
) )
tests := []struct { tests := []struct {
@ -238,7 +238,7 @@ func TestWriteExpectedValues(t *testing.T) {
getResult := func(args []*twoOperandParams, opFn executionFunc) []TwoOperandTestcase { getResult := func(args []*twoOperandParams, opFn executionFunc) []TwoOperandTestcase {
var ( var (
evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{}) evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{})
stack = newstack() stack = newStackForTesting()
pc = uint64(0) pc = uint64(0)
) )
result := make([]TwoOperandTestcase, len(args)) result := make([]TwoOperandTestcase, len(args))
@ -282,7 +282,7 @@ func TestJsonTestcases(t *testing.T) {
func opBenchmark(bench *testing.B, op executionFunc, args ...string) { func opBenchmark(bench *testing.B, op executionFunc, args ...string) {
var ( var (
evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{}) evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{})
stack = newstack() stack = newStackForTesting()
scope = &ScopeContext{nil, stack, nil} scope = &ScopeContext{nil, stack, nil}
) )
// convert args // convert args
@ -520,7 +520,7 @@ func BenchmarkOpIsZero(b *testing.B) {
func TestOpMstore(t *testing.T) { func TestOpMstore(t *testing.T) {
var ( var (
evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{}) evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{})
stack = newstack() stack = newStackForTesting()
mem = NewMemory() mem = NewMemory()
) )
mem.Resize(64) mem.Resize(64)
@ -543,7 +543,7 @@ func TestOpMstore(t *testing.T) {
func BenchmarkOpMstore(bench *testing.B) { func BenchmarkOpMstore(bench *testing.B) {
var ( var (
evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{}) evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{})
stack = newstack() stack = newStackForTesting()
mem = NewMemory() mem = NewMemory()
) )
mem.Resize(64) mem.Resize(64)
@ -563,7 +563,7 @@ func TestOpTstore(t *testing.T) {
var ( var (
statedb, _ = state.New(types.EmptyRootHash, state.NewDatabaseForTesting()) statedb, _ = state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
evm = NewEVM(BlockContext{}, statedb, params.TestChainConfig, Config{}) evm = NewEVM(BlockContext{}, statedb, params.TestChainConfig, Config{})
stack = newstack() stack = newStackForTesting()
mem = NewMemory() mem = NewMemory()
caller = common.Address{} caller = common.Address{}
to = common.Address{1} to = common.Address{1}
@ -602,7 +602,7 @@ func TestOpTstore(t *testing.T) {
func BenchmarkOpKeccak256(bench *testing.B) { func BenchmarkOpKeccak256(bench *testing.B) {
var ( var (
evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{}) evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{})
stack = newstack() stack = newStackForTesting()
mem = NewMemory() mem = NewMemory()
) )
mem.Resize(32) mem.Resize(32)
@ -675,7 +675,7 @@ func TestCreate2Addresses(t *testing.T) {
codeHash := crypto.Keccak256(code) codeHash := crypto.Keccak256(code)
address := crypto.CreateAddress2(origin, salt, codeHash) address := crypto.CreateAddress2(origin, salt, codeHash)
/* /*
stack := newstack() stack := newStackForTesting()
// salt, but we don't need that for this test // salt, but we don't need that for this test
stack.push(big.NewInt(int64(len(code)))) //size stack.push(big.NewInt(int64(len(code)))) //size
stack.push(big.NewInt(0)) // memstart stack.push(big.NewInt(0)) // memstart
@ -704,12 +704,12 @@ func TestRandom(t *testing.T) {
} { } {
var ( var (
evm = NewEVM(BlockContext{Random: &tt.random}, nil, params.TestChainConfig, Config{}) evm = NewEVM(BlockContext{Random: &tt.random}, nil, params.TestChainConfig, Config{})
stack = newstack() stack = newStackForTesting()
pc = uint64(0) pc = uint64(0)
) )
opRandom(&pc, evm.interpreter, &ScopeContext{nil, stack, nil}) opRandom(&pc, evm.interpreter, &ScopeContext{nil, stack, nil})
if len(stack.data) != 1 { if have, want := stack.len(), 1; have != want {
t.Errorf("Expected one item on stack after %v, got %d: ", tt.name, len(stack.data)) t.Errorf("test '%v': want %d item(s) on stack, have %d: ", tt.name, have, want)
} }
actual := stack.pop() actual := stack.pop()
expected, overflow := uint256.FromBig(new(big.Int).SetBytes(tt.random.Bytes())) expected, overflow := uint256.FromBig(new(big.Int).SetBytes(tt.random.Bytes()))
@ -744,14 +744,14 @@ func TestBlobHash(t *testing.T) {
} { } {
var ( var (
evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{}) evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{})
stack = newstack() stack = newStackForTesting()
pc = uint64(0) pc = uint64(0)
) )
evm.SetTxContext(TxContext{BlobHashes: tt.hashes}) evm.SetTxContext(TxContext{BlobHashes: tt.hashes})
stack.push(uint256.NewInt(tt.idx)) stack.push(uint256.NewInt(tt.idx))
opBlobHash(&pc, evm.interpreter, &ScopeContext{nil, stack, nil}) opBlobHash(&pc, evm.interpreter, &ScopeContext{nil, stack, nil})
if len(stack.data) != 1 { if have, want := stack.len(), 1; have != want {
t.Errorf("Expected one item on stack after %v, got %d: ", tt.name, len(stack.data)) t.Errorf("test '%v': want %d item(s) on stack, have %d: ", tt.name, have, want)
} }
actual := stack.pop() actual := stack.pop()
expected, overflow := uint256.FromBig(new(big.Int).SetBytes(tt.expect.Bytes())) expected, overflow := uint256.FromBig(new(big.Int).SetBytes(tt.expect.Bytes()))
@ -847,7 +847,7 @@ func TestOpMCopy(t *testing.T) {
} { } {
var ( var (
evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{}) evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{})
stack = newstack() stack = newStackForTesting()
pc = uint64(0) pc = uint64(0)
) )
data := common.FromHex(strings.ReplaceAll(tc.pre, " ", "")) data := common.FromHex(strings.ReplaceAll(tc.pre, " ", ""))
@ -910,7 +910,7 @@ func TestPush(t *testing.T) {
scope := &ScopeContext{ scope := &ScopeContext{
Memory: nil, Memory: nil,
Stack: newstack(), Stack: newStackForTesting(),
Contract: &Contract{ Contract: &Contract{
Code: code, Code: code,
}, },

View file

@ -183,7 +183,7 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (
var ( var (
op OpCode // current opcode op OpCode // current opcode
mem = NewMemory() // bound memory mem = NewMemory() // bound memory
stack = newstack() // local stack stack = in.evm.arena.stack() // local stack
callContext = &ScopeContext{ callContext = &ScopeContext{
Memory: mem, Memory: mem,
Stack: stack, Stack: stack,
@ -205,7 +205,7 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (
// so that it gets executed _after_: the OnOpcode needs the stacks before // so that it gets executed _after_: the OnOpcode needs the stacks before
// they are returned to the pools // they are returned to the pools
defer func() { defer func() {
returnStack(stack) stack.release()
mem.Free() mem.Free()
}() }()
contract.Input = input contract.Input = input

View file

@ -937,3 +937,63 @@ func TestDelegatedAccountAccessCost(t *testing.T) {
} }
} }
} }
func TestManyLargeStacks(t *testing.T) {
// This piece of code will push 512 items to the stack, and then call itself
// recursively.
code := make([]byte, 10)
for i := range code {
code[i] = byte(vm.PUSH0)
}
code = append(code, []byte{
byte(vm.ADDRESS), // address to call
byte(vm.GAS),
byte(vm.CALL),
}...)
main := common.HexToAddress("0xbb")
statedb, _ := state.New(types.EmptyRootHash, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
statedb.SetCode(main, code)
//tracer := logger.NewJSONLogger(nil, os.Stdout)
var tracer *tracing.Hooks
_, _, err := Call(main, nil, &Config{
GasLimit: 10_000_000,
State: statedb,
EVMConfig: vm.Config{
Tracer: tracer,
}})
if err != nil {
t.Fatal("didn't expect error", err)
}
}
func BenchmarkLargeDeepStacks(b *testing.B) {
// This piece of code will push 512 items to the stack, and then call itself
// recursively.
code := make([]byte, 512)
for i := range code {
code[i] = byte(vm.PUSH0)
}
code = append(code, []byte{
byte(vm.ADDRESS), // address to call
byte(vm.GAS),
byte(vm.CALL),
}...)
benchmarkNonModifyingCode(10_000_000, code, "deep-large-stacks-10M", "", b)
}
func BenchmarkShortDeepStacks(b *testing.B) {
// This piece of code will push a few items to the stack, and then call itself
// recursively.
code := make([]byte, 8)
for i := range code {
code[i] = byte(vm.PUSH0)
}
code = append(code, []byte{
byte(vm.ADDRESS), // address to call
byte(vm.GAS),
byte(vm.CALL),
}...)
benchmarkNonModifyingCode(10_000_000, code, "deep-short-stacks-10M", "", b)
}

View file

@ -17,111 +17,155 @@
package vm package vm
import ( import (
"sync" "slices"
"github.com/holiman/uint256" "github.com/holiman/uint256"
) )
var stackPool = sync.Pool{ // stackArena is an arena which actual evm stacks use for data storage
New: func() interface{} { type stackArena struct {
return &Stack{data: make([]uint256.Int, 0, 16)} data []uint256.Int
}, top int // first free slot
}
func (sa *stackArena) push(value *uint256.Int) {
if len(sa.data) <= sa.top {
// we need to grow the arena
sa.data = slices.Grow(sa.data, 512)
sa.data = sa.data[:cap(sa.data)]
}
sa.data[sa.top] = *value
sa.top++
}
func (sa *stackArena) pop() {
sa.top--
}
func newArena() *stackArena {
return &stackArena{
data: make([]uint256.Int, 1024),
}
}
// stack returns an instance of a stack which uses the underlying arena. The instance
// must be released by invoking the (*Stack).release() method
func (sa *stackArena) stack() *Stack {
return &Stack{
bottom: sa.top,
size: 0,
inner: sa,
}
}
// newStackForTesting is meant to be used solely for testing. It creates a stack
// backed by a newly allocated arena.
func newStackForTesting() *Stack {
arena := &stackArena{
data: make([]uint256.Int, 256),
}
return arena.stack()
} }
// Stack is an object for basic stack operations. Items popped to the stack are // Stack is an object for basic stack operations. Items popped to the stack are
// expected to be changed and modified. stack does not take care of adding newly // expected to be changed and modified. stack does not take care of adding newly
// initialized objects. // initialized objects.
type Stack struct { type Stack struct {
data []uint256.Int bottom int // bottom is the index of the first element of this stack
size int // size is the number of elements in this stack
inner *stackArena
} }
func newstack() *Stack { // release un-claims the area of the arena which was claimed by the stack.
return stackPool.Get().(*Stack) func (s *Stack) release() {
} // When the stack is returned, need to notify the arena that the new 'top' is
// the returned stack's bottom.
func returnStack(s *Stack) { s.inner.top = s.bottom
s.data = s.data[:0]
stackPool.Put(s)
} }
// Data returns the underlying uint256.Int array. // Data returns the underlying uint256.Int array.
func (st *Stack) Data() []uint256.Int { func (s *Stack) Data() []uint256.Int {
return st.data return s.inner.data[s.bottom : s.bottom+s.size]
} }
func (st *Stack) push(d *uint256.Int) { func (s *Stack) push(d *uint256.Int) {
// NOTE push limit (1024) is checked in baseCheck // NOTE push limit (1024) is checked in baseCheck
st.data = append(st.data, *d) s.inner.push(d)
s.size++
} }
func (st *Stack) pop() (ret uint256.Int) { func (s *Stack) pop() uint256.Int {
ret = st.data[len(st.data)-1] ret := s.inner.data[s.bottom+s.size-1]
st.data = st.data[:len(st.data)-1] s.inner.pop()
return s.size--
return ret
} }
func (st *Stack) len() int { func (s *Stack) len() int {
return len(st.data) return s.size
} }
func (st *Stack) swap1() { func (s *Stack) swap1() {
st.data[st.len()-2], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-2] s.inner.data[s.bottom+s.size-2], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-2]
} }
func (st *Stack) swap2() { func (s *Stack) swap2() {
st.data[st.len()-3], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-3] s.inner.data[s.bottom+s.size-3], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-3]
} }
func (st *Stack) swap3() { func (s *Stack) swap3() {
st.data[st.len()-4], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-4] s.inner.data[s.bottom+s.size-4], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-4]
} }
func (st *Stack) swap4() { func (s *Stack) swap4() {
st.data[st.len()-5], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-5] s.inner.data[s.bottom+s.size-5], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-5]
} }
func (st *Stack) swap5() { func (s *Stack) swap5() {
st.data[st.len()-6], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-6] s.inner.data[s.bottom+s.size-6], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-6]
} }
func (st *Stack) swap6() { func (s *Stack) swap6() {
st.data[st.len()-7], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-7] s.inner.data[s.bottom+s.size-7], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-7]
} }
func (st *Stack) swap7() { func (s *Stack) swap7() {
st.data[st.len()-8], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-8] s.inner.data[s.bottom+s.size-8], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-8]
} }
func (st *Stack) swap8() { func (s *Stack) swap8() {
st.data[st.len()-9], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-9] s.inner.data[s.bottom+s.size-9], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-9]
} }
func (st *Stack) swap9() { func (s *Stack) swap9() {
st.data[st.len()-10], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-10] s.inner.data[s.bottom+s.size-10], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-10]
} }
func (st *Stack) swap10() { func (s *Stack) swap10() {
st.data[st.len()-11], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-11] s.inner.data[s.bottom+s.size-11], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-11]
} }
func (st *Stack) swap11() { func (s *Stack) swap11() {
st.data[st.len()-12], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-12] s.inner.data[s.bottom+s.size-12], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-12]
} }
func (st *Stack) swap12() { func (s *Stack) swap12() {
st.data[st.len()-13], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-13] s.inner.data[s.bottom+s.size-13], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-13]
} }
func (st *Stack) swap13() { func (s *Stack) swap13() {
st.data[st.len()-14], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-14] s.inner.data[s.bottom+s.size-14], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-14]
} }
func (st *Stack) swap14() { func (s *Stack) swap14() {
st.data[st.len()-15], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-15] s.inner.data[s.bottom+s.size-15], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-15]
} }
func (st *Stack) swap15() { func (s *Stack) swap15() {
st.data[st.len()-16], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-16] s.inner.data[s.bottom+s.size-16], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-16]
} }
func (st *Stack) swap16() { func (s *Stack) swap16() {
st.data[st.len()-17], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-17] s.inner.data[s.bottom+s.size-17], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-17]
} }
func (st *Stack) dup(n int) { func (s *Stack) dup(n int) {
st.push(&st.data[st.len()-n]) // TODO: check size of inner
s.inner.data[s.bottom+s.size] = s.inner.data[s.bottom+s.size-n]
s.size++
s.inner.top++
} }
func (st *Stack) peek() *uint256.Int { func (s *Stack) peek() *uint256.Int {
return &st.data[st.len()-1] return &s.inner.data[s.bottom+s.size-1]
} }
// Back returns the n'th item in stack // Back returns the n'th item in stack
func (st *Stack) Back(n int) *uint256.Int { func (s *Stack) Back(n int) *uint256.Int {
return &st.data[st.len()-n-1] return &s.inner.data[s.bottom+s.size-n-1]
} }