Implement a call stack in EVM and expose through a precompile (#3)
implement a call stack in EVM and expose through a precompile
This commit is contained in:
parent
3f907d6a6f
commit
3dfcfc62f6
|
@ -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++
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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{
|
||||
|
|
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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()
|
||||
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue