diff --git a/core/vm/call_stack.go b/core/vm/call_stack.go new file mode 100644 index 0000000000..842ade0176 --- /dev/null +++ b/core/vm/call_stack.go @@ -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 . + +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++ +} diff --git a/core/vm/call_stack_test.go b/core/vm/call_stack_test.go new file mode 100644 index 0000000000..655c20f849 --- /dev/null +++ b/core/vm/call_stack_test.go @@ -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 . + +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) +} diff --git a/core/vm/contracts.go b/core/vm/contracts.go index 574bb9bef6..8e09d2eb57 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -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{ diff --git a/core/vm/evm.go b/core/vm/evm.go index 2c6cc7d484..9c39137b21 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -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.