Skip to content

Commit

Permalink
Implement a call stack in EVM and expose through a precompile (#3)
Browse files Browse the repository at this point in the history
implement a call stack in EVM and expose through a precompile
  • Loading branch information
canercidam authored Feb 7, 2024
1 parent 3f907d6 commit 3dfcfc6
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 1 deletion.
138 changes: 138 additions & 0 deletions core/vm/call_stack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// 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 Genercs 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 Genercs Public License for more details.
//
// You should have received a copy of the GNU Lesser Genercs Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

package vm

import (
"sync"

"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
)

var callStackPool = sync.Pool{
New: func() interface{} {
return &callStack{calls: make([]*csCall, 0, 16)}
},
}

// callStack keeps track of the calls.
type callStack struct {
calls []*csCall
}

type csCall struct {
Op OpCode
Address common.Address
Signature []byte
}

// newCallStack creates a new call stack.
func newCallStack() *callStack {
return callStackPool.Get().(*callStack)
}

// Push pushes given call to the stack.
func (cs *callStack) Push(op OpCode, addr common.Address, input []byte) {
var signature []byte
if len(input) >= 4 {
signature = input[:4]
}
cs.calls = append(cs.calls, &csCall{
Op: op,
Address: addr,
Signature: signature,
})
}

// Pop pops the latest call from the stack and returns the stack back to the
// pool if no calls are left after the final pop.
func (cs *callStack) Pop() {
cs.calls = cs.calls[:len(cs.calls)-1]
if len(cs.calls) == 0 {
callStackPool.Put(cs)
}
}

func isAddrIn(checkAddr common.Address, addrs []common.Address) bool {
for _, addr := range addrs {
if addr.Cmp(checkAddr) == 0 {
return true
}
}
return false
}

// RequiredGas implements the precompiled contract interface.
func (cs *callStack) RequiredGas(input []byte) uint64 {
// Assume 100 gas base cost
// ORIGIN, ADDRESS and CALLER opcodes spend 2 gas
// Assume 2 gas per called address and 2 gas per created and called address
// TODO: Move these constants to params/protocol_params.go?
const baseGasCost = 0 // TBD
return baseGasCost + uint64(2*len(cs.calls))
}

// Run runs the precompiled contract.
func (cs *callStack) Run(input []byte) ([]byte, error) {
return newCallStackEncoder(cs.calls).Encode()
}

var (
callStackEncodingArrayOffset = uint256.NewInt(32)
)

type callStackEncoder struct {
calls []*csCall

i int
result []byte
}

func newCallStackEncoder(calls []*csCall) *callStackEncoder {
return &callStackEncoder{
calls: calls,
// 1 for offset, 1 for list length
// 3 x list length for the elements
// rest for the actucs elements in both lists
result: make([]byte, (2+3*len(calls))*32),
}
}

func (enc *callStackEncoder) Encode() ([]byte, error) {
// add the array offset and the call list length
enc.appendNum(callStackEncodingArrayOffset)
enc.appendNum(uint256.NewInt(uint64(len(enc.calls))))

// add call info from each call
for _, call := range enc.calls {
enc.appendNum(uint256.NewInt(uint64(call.Op)))
enc.appendAddr(call.Address)
enc.appendNum(uint256.NewInt(0).SetBytes(call.Signature))
}

return enc.result, nil
}

func (enc *callStackEncoder) appendNum(n *uint256.Int) {
copy(enc.result[enc.i*32:(enc.i+1)*32], n.PaddedBytes(32))
enc.i++
}

func (enc *callStackEncoder) appendAddr(addr common.Address) {
copy(enc.result[enc.i*32+12:(enc.i+1)*32], addr.Bytes())
enc.i++
}
84 changes: 84 additions & 0 deletions core/vm/call_stack_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// 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 (
"encoding/hex"
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)

func BenchmarkCallStackPrecompile1(b *testing.B) {
benchCallStackPrecompileN(b, 1)
}

func BenchmarkCallStackPrecompile10(b *testing.B) {
benchCallStackPrecompileN(b, 10)
}

func BenchmarkCallStackPrecompile100(b *testing.B) {
benchCallStackPrecompileN(b, 100)
}

func benchCallStackPrecompileN(b *testing.B, n int) {
var calls []*csCall
for i := 0; i < n; i++ {
calls = append(calls, &csCall{
Op: OpCode(i),
Address: common.HexToAddress("0xCdA8dcaEe60ce9d63165Ef025fD98CDA2B99B5B2"),
Signature: []byte{0xde, 0xad, 0xbe, 0xef},
})
}
callStack := newCallStack()
callStack.calls = calls
for i := 0; i < b.N; i++ {
callStack.Run(nil)
}
}

func TestCallStackPrecompile(t *testing.T) {
r := require.New(t)
callStack := newCallStack()
callStack.calls = []*csCall{
{
Op: OpCode(0x10),
Address: common.HexToAddress("0xCdA8dcaEe60ce9d63165Ef025fD98CDA2B99B5B2"),
Signature: []byte{0xab, 0xcd, 0xef, 0x12},
},
{
Op: OpCode(0x20),
Address: common.HexToAddress("0xCdA8dcaEe60ce9d63165Ef025fD98CDA2B99B5B2"),
Signature: []byte{0xde, 0xad, 0xbe, 0xef},
},
}
b, err := callStack.Run(nil)
r.NoError(err)
expectedB, err := hex.DecodeString(
"0000000000000000000000000000000000000000000000000000000000000020" +
"0000000000000000000000000000000000000000000000000000000000000002" +
"0000000000000000000000000000000000000000000000000000000000000010" +
"000000000000000000000000cda8dcaee60ce9d63165ef025fd98cda2b99b5b2" +
"00000000000000000000000000000000000000000000000000000000abcdef12" +
"0000000000000000000000000000000000000000000000000000000000000020" +
"000000000000000000000000cda8dcaee60ce9d63165ef025fd98cda2b99b5b2" +
"00000000000000000000000000000000000000000000000000000000deadbeef",
)
r.NoError(err)
r.Equal(expectedB, b)
}
3 changes: 3 additions & 0 deletions core/vm/contracts.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ type PrecompiledContract interface {
Run(input []byte) ([]byte, error) // Run runs the precompiled contract
}

// CallStackPrecompileAddress is the default address for this precompiled contract.
var CallStackPrecompileAddress = common.BytesToAddress([]byte{0x20})

// PrecompiledContractsHomestead contains the default set of pre-compiled Ethereum
// contracts used in the Frontier and Homestead releases.
var PrecompiledContractsHomestead = map[common.Address]PrecompiledContract{
Expand Down
23 changes: 22 additions & 1 deletion core/vm/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,13 @@ func (evm *EVM) precompile(addr common.Address) (PrecompiledContract, bool) {
precompiles = PrecompiledContractsHomestead
}
p, ok := precompiles[addr]
return p, ok
if ok {
return p, ok
}
if addr.Cmp(CallStackPrecompileAddress) == 0 {
return evm.callStack, true
}
return nil, false
}

// BlockContext provides the EVM with auxiliary information. Once provided
Expand Down Expand Up @@ -120,6 +126,8 @@ type EVM struct {
// available gas is calculated in gasCall* according to the 63/64 rule and later
// applied in opCall*.
callGasTemp uint64
// callStack keeps track of the calls
callStack *callStack
}

// NewEVM returns a new EVM. The returned EVM is not thread safe and should
Expand All @@ -132,6 +140,7 @@ func NewEVM(blockCtx BlockContext, txCtx TxContext, statedb StateDB, chainConfig
Config: config,
chainConfig: chainConfig,
chainRules: chainConfig.Rules(blockCtx.BlockNumber, blockCtx.Random != nil, blockCtx.Time),
callStack: newCallStack(),
}
evm.interpreter = NewEVMInterpreter(evm)
return evm
Expand Down Expand Up @@ -222,6 +231,9 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas
if isPrecompile {
ret, gas, err = RunPrecompiledContract(p, input, gas)
} else {
// Push the call to the call stack.
evm.callStack.Push(CALL, addr, input)
defer evm.callStack.Pop()
// 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.
code := evm.StateDB.GetCode(addr)
Expand Down Expand Up @@ -285,6 +297,9 @@ func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte,
if p, isPrecompile := evm.precompile(addr); isPrecompile {
ret, gas, err = RunPrecompiledContract(p, input, gas)
} else {
// Push the call to the call stack.
evm.callStack.Push(CALLCODE, addr, input)
defer evm.callStack.Pop()
addrCopy := addr
// 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.
Expand Down Expand Up @@ -330,6 +345,9 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by
if p, isPrecompile := evm.precompile(addr); isPrecompile {
ret, gas, err = RunPrecompiledContract(p, input, gas)
} else {
// Push the call to the call stack.
evm.callStack.Push(DELEGATECALL, addr, input)
defer evm.callStack.Pop()
addrCopy := addr
// Initialise a new contract and make initialise the delegate values
contract := NewContract(caller, AccountRef(caller.Address()), nil, gas).AsDelegate()
Expand Down Expand Up @@ -379,6 +397,9 @@ func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte
if p, isPrecompile := evm.precompile(addr); isPrecompile {
ret, gas, err = RunPrecompiledContract(p, input, gas)
} else {
// Push the call to the call stack.
evm.callStack.Push(STATICCALL, addr, input)
defer evm.callStack.Pop()
// At this point, we use a copy of address. If we don't, the go compiler will
// leak the 'contract' to the outer scope, and make allocation for 'contract'
// even if the actual execution ends on RunPrecompiled above.
Expand Down

0 comments on commit 3dfcfc6

Please sign in to comment.