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.