mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-21 06:04:33 +00:00
## Why this should be merged Completeness. ## How this works n/a ## How this was tested Originally the `DELEGATECALL` test asserted that the `CALLVALUE` was zero, but this didn't demonstrate that it was inherited (e.g. `STATICCALL` always receives zero). The value sent to the caller of the precompile is now non-zero, to precisely demonstrate correct behaviour of all call types.
880 lines
26 KiB
Go
880 lines
26 KiB
Go
// Copyright 2024 the libevm authors.
|
|
//
|
|
// The libevm additions to go-ethereum are free software: you can redistribute
|
|
// them and/or modify them 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 libevm additions are distributed in the hope that they 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_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"math/big"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/holiman/uint256"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/exp/rand"
|
|
|
|
"github.com/ava-labs/libevm/common"
|
|
"github.com/ava-labs/libevm/core"
|
|
"github.com/ava-labs/libevm/core/types"
|
|
"github.com/ava-labs/libevm/core/vm"
|
|
"github.com/ava-labs/libevm/crypto"
|
|
"github.com/ava-labs/libevm/eth/tracers"
|
|
_ "github.com/ava-labs/libevm/eth/tracers/native"
|
|
"github.com/ava-labs/libevm/libevm"
|
|
"github.com/ava-labs/libevm/libevm/ethtest"
|
|
"github.com/ava-labs/libevm/libevm/hookstest"
|
|
"github.com/ava-labs/libevm/libevm/legacy"
|
|
"github.com/ava-labs/libevm/params"
|
|
)
|
|
|
|
type precompileStub struct {
|
|
requiredGas uint64
|
|
returnData []byte
|
|
}
|
|
|
|
func (s *precompileStub) RequiredGas([]byte) uint64 { return s.requiredGas }
|
|
func (s *precompileStub) Run([]byte) ([]byte, error) { return s.returnData, nil }
|
|
|
|
func TestPrecompileOverride(t *testing.T) {
|
|
type test struct {
|
|
name string
|
|
addr common.Address
|
|
requiredGas uint64
|
|
stubData []byte
|
|
}
|
|
|
|
const gasLimit = uint64(1e7)
|
|
|
|
tests := []test{
|
|
{
|
|
name: "arbitrary values",
|
|
addr: common.Address{'p', 'r', 'e', 'c', 'o', 'm', 'p', 'i', 'l', 'e'},
|
|
requiredGas: 314159,
|
|
stubData: []byte("the return data"),
|
|
},
|
|
}
|
|
|
|
rng := rand.New(rand.NewSource(42))
|
|
for _, addr := range vm.PrecompiledAddressesCancun {
|
|
tests = append(tests, test{
|
|
name: fmt.Sprintf("existing precompile %v", addr),
|
|
addr: addr,
|
|
requiredGas: rng.Uint64n(gasLimit),
|
|
stubData: addr[:],
|
|
})
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
hooks := &hookstest.Stub{
|
|
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
|
|
tt.addr: &precompileStub{
|
|
requiredGas: tt.requiredGas,
|
|
returnData: tt.stubData,
|
|
},
|
|
},
|
|
}
|
|
hooks.Register(t)
|
|
|
|
t.Run(fmt.Sprintf("%T.Call([overridden precompile address = %v])", &vm.EVM{}, tt.addr), func(t *testing.T) {
|
|
_, evm := ethtest.NewZeroEVM(t)
|
|
gotData, gotGasLeft, err := evm.Call(vm.AccountRef{}, tt.addr, nil, gasLimit, uint256.NewInt(0))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.stubData, gotData, "contract's return data")
|
|
assert.Equal(t, gasLimit-tt.requiredGas, gotGasLeft, "gas left")
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
type statefulPrecompileOutput struct {
|
|
ChainID *big.Int
|
|
Addresses *libevm.AddressContext
|
|
StateValue common.Hash
|
|
CallValue *uint256.Int
|
|
ReadOnly bool
|
|
BlockNumber, Difficulty *big.Int
|
|
BlockTime uint64
|
|
Input []byte
|
|
IncomingCallType vm.CallType
|
|
}
|
|
|
|
func (o statefulPrecompileOutput) String() string {
|
|
var lines []string
|
|
out := reflect.ValueOf(o)
|
|
FieldLoop:
|
|
for i, n := 0, out.NumField(); i < n; i++ {
|
|
name := out.Type().Field(i).Name
|
|
fld := out.Field(i).Interface()
|
|
|
|
verb := "%v"
|
|
switch fld.(type) {
|
|
case []byte:
|
|
verb = "%#x"
|
|
case *libevm.AddressContext:
|
|
lines = append(
|
|
lines,
|
|
fmt.Sprintf("EVMSemantic addresses: %+v", o.Addresses.EVMSemantic),
|
|
fmt.Sprintf("Raw addresses: %+v", o.Addresses.Raw),
|
|
)
|
|
continue FieldLoop
|
|
case vm.CallType:
|
|
verb = "%d (%[2]q)"
|
|
}
|
|
lines = append(lines, fmt.Sprintf("%s: "+verb, name, fld))
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
|
|
func (o statefulPrecompileOutput) Bytes() []byte {
|
|
return []byte(o.String())
|
|
}
|
|
|
|
func TestNewStatefulPrecompile(t *testing.T) {
|
|
precompile := common.HexToAddress("60C0DE") // GO CODE
|
|
rng := ethtest.NewPseudoRand(314159)
|
|
slot := rng.Hash()
|
|
|
|
const gasLimit = 1e6
|
|
gasCost := rng.Uint64n(gasLimit)
|
|
|
|
run := func(env vm.PrecompileEnvironment, input []byte, suppliedGas uint64) ([]byte, uint64, error) {
|
|
if got, want := env.StateDB() != nil, !env.ReadOnly(); got != want {
|
|
return nil, 0, fmt.Errorf("PrecompileEnvironment().StateDB() must be non-nil i.f.f. not read-only; got non-nil? %t; want %t", got, want)
|
|
}
|
|
hdr, err := env.BlockHeader()
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
out := &statefulPrecompileOutput{
|
|
ChainID: env.ChainConfig().ChainID,
|
|
Addresses: env.Addresses(),
|
|
StateValue: env.ReadOnlyState().GetState(precompile, slot),
|
|
CallValue: env.Value(),
|
|
ReadOnly: env.ReadOnly(),
|
|
BlockNumber: env.BlockNumber(),
|
|
BlockTime: env.BlockTime(),
|
|
Difficulty: hdr.Difficulty,
|
|
Input: input,
|
|
IncomingCallType: env.IncomingCallType(),
|
|
}
|
|
return out.Bytes(), suppliedGas - gasCost, nil
|
|
}
|
|
hooks := &hookstest.Stub{
|
|
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
|
|
precompile: vm.NewStatefulPrecompile(
|
|
// In production, the new function signature should be used, but
|
|
// this just exercises the converter.
|
|
legacy.PrecompiledStatefulContract(run).Upgrade(),
|
|
),
|
|
},
|
|
}
|
|
hooks.Register(t)
|
|
|
|
header := &types.Header{
|
|
Number: rng.BigUint64(),
|
|
Time: rng.Uint64(),
|
|
Difficulty: rng.BigUint64(),
|
|
}
|
|
input := rng.Bytes(8)
|
|
stateValue := rng.Hash()
|
|
callCallerValue := rng.Uint256()
|
|
callPrecompileValue := rng.Uint256()
|
|
chainID := rng.BigUint64()
|
|
|
|
caller := common.HexToAddress("CA11E12") // caller of the precompile
|
|
eoa := common.HexToAddress("E0A") // caller of the precompile-caller
|
|
callerContract := vm.NewContract(vm.AccountRef(eoa), vm.AccountRef(caller), callCallerValue, 1e6)
|
|
|
|
state, evm := ethtest.NewZeroEVM(
|
|
t,
|
|
ethtest.WithBlockContext(
|
|
core.NewEVMBlockContext(header, nil, rng.AddressPtr()),
|
|
),
|
|
ethtest.WithChainConfig(
|
|
¶ms.ChainConfig{ChainID: chainID},
|
|
),
|
|
)
|
|
state.SetState(precompile, slot, stateValue)
|
|
state.SetBalance(caller, new(uint256.Int).Not(uint256.NewInt(0)))
|
|
evm.Origin = eoa
|
|
|
|
// By definition, the raw caller and self are the same for every test case,
|
|
// regardless of the incoming call type.
|
|
rawAddresses := libevm.CallerAndSelf{
|
|
Caller: caller,
|
|
Self: precompile,
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
call func() ([]byte, uint64, error)
|
|
wantAddresses *libevm.AddressContext
|
|
wantCallValue *uint256.Int
|
|
// Note that this only covers evm.readOnly being true because of the
|
|
// precompile's call. See TestInheritReadOnly for alternate case.
|
|
wantReadOnly bool
|
|
wantCallType vm.CallType
|
|
}{
|
|
{
|
|
name: "EVM.Call()",
|
|
call: func() ([]byte, uint64, error) {
|
|
return evm.Call(callerContract, precompile, input, gasLimit, callPrecompileValue)
|
|
},
|
|
wantAddresses: &libevm.AddressContext{
|
|
Origin: eoa,
|
|
EVMSemantic: rawAddresses,
|
|
Raw: &rawAddresses,
|
|
},
|
|
wantReadOnly: false,
|
|
wantCallValue: callPrecompileValue,
|
|
wantCallType: vm.Call,
|
|
},
|
|
{
|
|
name: "EVM.CallCode()",
|
|
call: func() ([]byte, uint64, error) {
|
|
return evm.CallCode(callerContract, precompile, input, gasLimit, callPrecompileValue)
|
|
},
|
|
wantAddresses: &libevm.AddressContext{
|
|
Origin: eoa,
|
|
EVMSemantic: libevm.CallerAndSelf{
|
|
Caller: caller,
|
|
Self: caller,
|
|
},
|
|
Raw: &rawAddresses,
|
|
},
|
|
wantReadOnly: false,
|
|
wantCallValue: callPrecompileValue,
|
|
wantCallType: vm.CallCode,
|
|
},
|
|
{
|
|
name: "EVM.DelegateCall()",
|
|
call: func() ([]byte, uint64, error) {
|
|
return evm.DelegateCall(callerContract, precompile, input, gasLimit)
|
|
},
|
|
wantAddresses: &libevm.AddressContext{
|
|
Origin: eoa,
|
|
EVMSemantic: libevm.CallerAndSelf{
|
|
Caller: eoa, // inherited from caller
|
|
Self: caller,
|
|
},
|
|
Raw: &rawAddresses,
|
|
},
|
|
wantReadOnly: false,
|
|
wantCallValue: callCallerValue, // Important difference from [vm.EVM.Call]
|
|
wantCallType: vm.DelegateCall,
|
|
},
|
|
{
|
|
name: "EVM.StaticCall()",
|
|
call: func() ([]byte, uint64, error) {
|
|
return evm.StaticCall(callerContract, precompile, input, gasLimit)
|
|
},
|
|
wantAddresses: &libevm.AddressContext{
|
|
Origin: eoa,
|
|
EVMSemantic: rawAddresses,
|
|
Raw: &rawAddresses,
|
|
},
|
|
wantReadOnly: true,
|
|
wantCallValue: uint256.NewInt(0),
|
|
wantCallType: vm.StaticCall,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
wantOutput := statefulPrecompileOutput{
|
|
ChainID: chainID,
|
|
Addresses: tt.wantAddresses,
|
|
StateValue: stateValue,
|
|
CallValue: tt.wantCallValue,
|
|
ReadOnly: tt.wantReadOnly,
|
|
BlockNumber: header.Number,
|
|
BlockTime: header.Time,
|
|
Difficulty: header.Difficulty,
|
|
Input: input,
|
|
IncomingCallType: tt.wantCallType,
|
|
}
|
|
|
|
wantGasLeft := gasLimit - gasCost
|
|
|
|
gotReturnData, gotGasLeft, err := tt.call()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, wantOutput.String(), string(gotReturnData))
|
|
assert.Equal(t, wantGasLeft, gotGasLeft)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPrecompileInvalidatesExecution(t *testing.T) {
|
|
errIfInvalidated := errors.New("execution invalidated")
|
|
inputToInvalidate := []byte("invalidate")
|
|
run := func(env vm.PrecompileEnvironment, input []byte) ([]byte, error) {
|
|
if bytes.Equal(input, inputToInvalidate) {
|
|
env.InvalidateExecution(errIfInvalidated)
|
|
}
|
|
return []byte{}, nil
|
|
}
|
|
|
|
precompile := common.HexToAddress("60C0DE") // GO CODE
|
|
hooks := &hookstest.Stub{
|
|
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
|
|
precompile: vm.NewStatefulPrecompile(run),
|
|
},
|
|
}
|
|
hooks.Register(t)
|
|
|
|
// The EVM instance MUST be reused across all tests to ensure that
|
|
// [vm.EVM.Reset] undoes any invalidation.
|
|
stateDB, evm := ethtest.NewZeroEVM(t)
|
|
|
|
tests := []struct {
|
|
name string
|
|
nonce uint64
|
|
input []byte
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "not_invalidating",
|
|
input: []byte{},
|
|
nonce: 0,
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "invalidating",
|
|
nonce: 1,
|
|
input: inputToInvalidate,
|
|
wantErr: errIfInvalidated,
|
|
},
|
|
{
|
|
// Tests that:
|
|
// (a) [vm.EVM.Reset] undoes the previous invalidation; and
|
|
// (b) Invalidation reverted state changes, as seen by the nonce.
|
|
name: "evm_reset_not_invalidating_after_invalid",
|
|
input: []byte{},
|
|
nonce: 1, // unchanged because the last was invalidated
|
|
wantErr: nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
msg := &core.Message{
|
|
Nonce: tt.nonce,
|
|
Data: tt.input,
|
|
|
|
// Common across all txs
|
|
To: &precompile,
|
|
GasLimit: 1e6, // arbitrary but sufficiently high
|
|
GasPrice: big.NewInt(0),
|
|
Value: big.NewInt(0),
|
|
}
|
|
|
|
evm.Reset(core.NewEVMTxContext(msg), stateDB)
|
|
|
|
gas := core.GasPool(math.MaxUint64)
|
|
_, err := core.ApplyMessage(evm, msg, &gas)
|
|
require.ErrorIs(t, err, tt.wantErr, "core.ApplyMessage()")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInheritReadOnly(t *testing.T) {
|
|
// The regular test of stateful precompiles only checks the read-only state
|
|
// when called directly via vm.EVM.*Call*() methods. That approach will not
|
|
// result in a read-only state via inheritance, which occurs when already in
|
|
// a read-only environment there is a non-static call to a precompile.
|
|
//
|
|
// Test strategy:
|
|
//
|
|
// 1. Create a precompile that echoes its read-only status in the return
|
|
// data. We MUST NOT assert inside the precompile as we need proof that
|
|
// the precompile was actually called.
|
|
//
|
|
// 2. Create a bytecode contract that calls the precompile with CALL and
|
|
// propagates the return data. Using CALL (i.e. not STATICCALL) means
|
|
// that we know for certain that [forceReadOnly] isn't being used and,
|
|
// instead, the read-only state is being read from
|
|
// evm.interpreter.readOnly.
|
|
//
|
|
// 3. Assert that the returned input is as expected for the read-only state.
|
|
|
|
// (1)
|
|
|
|
precompile := common.Address{255}
|
|
|
|
const (
|
|
ifReadOnly = iota + 1 // see contract bytecode for rationale
|
|
ifNotReadOnly
|
|
)
|
|
hooks := &hookstest.Stub{
|
|
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
|
|
precompile: vm.NewStatefulPrecompile(
|
|
func(env vm.PrecompileEnvironment, input []byte) ([]byte, error) {
|
|
if env.ReadOnly() {
|
|
return []byte{ifReadOnly}, nil
|
|
}
|
|
return []byte{ifNotReadOnly}, nil
|
|
},
|
|
),
|
|
},
|
|
}
|
|
hookstest.Register(t, params.Extras[*hookstest.Stub, *hookstest.Stub]{
|
|
NewRules: func(_ *params.ChainConfig, r *params.Rules, _ *hookstest.Stub, blockNum *big.Int, isMerge bool, timestamp uint64) *hookstest.Stub {
|
|
r.IsCancun = true // enable PUSH0
|
|
return hooks
|
|
},
|
|
})
|
|
|
|
// (2)
|
|
contract := makeReturnProxy(t, precompile, vm.CALL)
|
|
|
|
state, evm := ethtest.NewZeroEVM(t)
|
|
rng := ethtest.NewPseudoRand(42)
|
|
contractAddr := rng.Address()
|
|
state.CreateAccount(contractAddr)
|
|
state.SetCode(contractAddr, convertBytes[vm.OpCode, byte](contract...))
|
|
|
|
// (3)
|
|
|
|
caller := vm.AccountRef(rng.Address())
|
|
tests := []struct {
|
|
name string
|
|
call func() ([]byte, uint64, error)
|
|
want byte
|
|
}{
|
|
{
|
|
name: "EVM.Call()",
|
|
call: func() ([]byte, uint64, error) {
|
|
return evm.Call(caller, contractAddr, []byte{}, 1e6, uint256.NewInt(0))
|
|
},
|
|
want: ifNotReadOnly,
|
|
},
|
|
{
|
|
name: "EVM.StaticCall()",
|
|
call: func() ([]byte, uint64, error) {
|
|
return evm.StaticCall(vm.AccountRef(rng.Address()), contractAddr, []byte{}, 1e6)
|
|
},
|
|
want: ifReadOnly,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, _, err := tt.call()
|
|
require.NoError(t, err)
|
|
require.Equalf(t, []byte{tt.want}, got, "want %d if read-only, otherwise %d", ifReadOnly, ifNotReadOnly)
|
|
})
|
|
}
|
|
}
|
|
|
|
// makeReturnProxy returns the bytecode of a contract that will call `dest` with
|
|
// the specified call type and propagated the returned value.
|
|
//
|
|
// The contract does NOT check if the call reverted. In this case, the
|
|
// propagated return value will always be an empty slice. Tests using these
|
|
// proxies MUST use non-empty slices as test values.
|
|
//
|
|
// TODO(arr4n): convert this to arr4n/specops for clarity and to make it easier
|
|
// to generate a revert check.
|
|
func makeReturnProxy(t *testing.T, dest common.Address, call vm.OpCode) []vm.OpCode {
|
|
t.Helper()
|
|
const p0 = vm.PUSH0
|
|
contract := []vm.OpCode{
|
|
vm.CALLDATASIZE, p0, p0, vm.CALLDATACOPY,
|
|
|
|
p0, // retSize
|
|
p0, // retOffset
|
|
vm.CALLDATASIZE, // argSize
|
|
p0, // argOffset
|
|
}
|
|
|
|
// See CALL signature: https://www.evm.codes/#f1?fork=cancun
|
|
switch call {
|
|
case vm.CALL, vm.CALLCODE:
|
|
contract = append(contract, p0) // value
|
|
case vm.DELEGATECALL, vm.STATICCALL:
|
|
default:
|
|
t.Fatalf("Bad test setup: invalid non-CALL-type opcode %s", call)
|
|
}
|
|
|
|
contract = append(contract, vm.PUSH20)
|
|
contract = append(contract, convertBytes[byte, vm.OpCode](dest[:]...)...)
|
|
|
|
contract = append(contract,
|
|
p0, // gas
|
|
call,
|
|
|
|
// See function comment re ignored reverts.
|
|
vm.RETURNDATASIZE, p0, p0, vm.RETURNDATACOPY,
|
|
vm.RETURNDATASIZE, p0, vm.RETURN,
|
|
)
|
|
return contract
|
|
}
|
|
|
|
func convertBytes[From ~byte, To ~byte](buf ...From) []To {
|
|
out := make([]To, len(buf))
|
|
for i, b := range buf {
|
|
out[i] = To(b)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestCanCreateContract(t *testing.T) {
|
|
rng := ethtest.NewPseudoRand(142857)
|
|
account := rng.Address()
|
|
slot := rng.Hash()
|
|
|
|
const gasLimit uint64 = 1e6
|
|
gasUsage := rng.Uint64n(gasLimit)
|
|
|
|
makeErr := func(cc *libevm.AddressContext, stateVal common.Hash) error {
|
|
return fmt.Errorf("Origin: %v Caller: %v Contract: %v State: %v", cc.Origin, cc.EVMSemantic.Caller, cc.EVMSemantic.Self, stateVal)
|
|
}
|
|
hooks := &hookstest.Stub{
|
|
CanCreateContractFn: func(cc *libevm.AddressContext, gas uint64, s libevm.StateReader) (uint64, error) {
|
|
return gas - gasUsage, makeErr(cc, s.GetState(account, slot))
|
|
},
|
|
}
|
|
hooks.Register(t)
|
|
|
|
origin := rng.Address()
|
|
caller := rng.Address()
|
|
value := rng.Hash()
|
|
code := []byte{byte(vm.STOP)}
|
|
salt := rng.Hash()
|
|
|
|
create := crypto.CreateAddress(caller, 0)
|
|
create2 := crypto.CreateAddress2(caller, salt, crypto.Keccak256(code))
|
|
|
|
tests := []struct {
|
|
name string
|
|
create func(*vm.EVM) ([]byte, common.Address, uint64, error)
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "Create",
|
|
create: func(evm *vm.EVM) ([]byte, common.Address, uint64, error) {
|
|
return evm.Create(vm.AccountRef(caller), code, gasLimit, uint256.NewInt(0))
|
|
},
|
|
wantErr: makeErr(
|
|
&libevm.AddressContext{
|
|
Origin: origin,
|
|
EVMSemantic: libevm.CallerAndSelf{
|
|
Caller: caller,
|
|
Self: create,
|
|
},
|
|
// `Raw` is documented as always being nil.
|
|
},
|
|
value,
|
|
),
|
|
},
|
|
{
|
|
name: "Create2",
|
|
create: func(evm *vm.EVM) ([]byte, common.Address, uint64, error) {
|
|
return evm.Create2(vm.AccountRef(caller), code, gasLimit, uint256.NewInt(0), new(uint256.Int).SetBytes(salt[:]))
|
|
},
|
|
wantErr: makeErr(
|
|
&libevm.AddressContext{
|
|
Origin: origin,
|
|
EVMSemantic: libevm.CallerAndSelf{
|
|
Caller: caller,
|
|
Self: create2,
|
|
},
|
|
// As above re `Raw` always being nil.
|
|
},
|
|
value,
|
|
),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
state, evm := ethtest.NewZeroEVM(t)
|
|
state.SetState(account, slot, value)
|
|
evm.TxContext.Origin = origin
|
|
|
|
_, _, gasRemaining, err := tt.create(evm)
|
|
require.EqualError(t, err, tt.wantErr.Error())
|
|
// require prints uint64s in hex
|
|
require.Equal(t, int(gasLimit-gasUsage), int(gasRemaining), "gas remaining") //nolint:gosec // G115 won't overflow as <= 1e6
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestActivePrecompilesOverride(t *testing.T) {
|
|
newRules := func() params.Rules {
|
|
return new(params.ChainConfig).Rules(big.NewInt(0), false, 0)
|
|
}
|
|
defaultActive := vm.ActivePrecompiles(newRules())
|
|
|
|
rng := ethtest.NewPseudoRand(0xDecafC0ffeeBad)
|
|
precompiles := make([]common.Address, rng.Intn(10)+5)
|
|
for i := range precompiles {
|
|
precompiles[i] = rng.Address()
|
|
}
|
|
hooks := &hookstest.Stub{
|
|
ActivePrecompilesFn: func(active []common.Address) []common.Address {
|
|
assert.Equal(t, defaultActive, active, "ActivePrecompiles() hook receives default addresses")
|
|
return precompiles
|
|
},
|
|
}
|
|
hooks.Register(t)
|
|
|
|
require.Equal(t, precompiles, vm.ActivePrecompiles(newRules()), "vm.ActivePrecompiles() returns overridden addresses")
|
|
}
|
|
|
|
func TestPrecompileMakeCall(t *testing.T) {
|
|
// There is one test per *CALL* op code:
|
|
//
|
|
// 1. `eoa` makes a call to a bytecode contract, `caller`;
|
|
// 2. `caller` calls `sut`, the precompile under test, via the test's *CALL* op code;
|
|
// 3. `sut` makes a Call() to `dest`, which reflects env data for testing.
|
|
//
|
|
// This acts as a full integration test of a precompile being invoked before
|
|
// making an "outbound" call.
|
|
eoa := common.HexToAddress("E0A")
|
|
caller := common.HexToAddress("CA11E12")
|
|
sut := common.HexToAddress("7E57ED")
|
|
dest := common.HexToAddress("DE57")
|
|
|
|
rng := ethtest.NewPseudoRand(142857)
|
|
precompileCallData := rng.Bytes(8)
|
|
|
|
// If the SUT precompile receives this as its calldata then it will use the
|
|
// vm.WithUNSAFECallerAddressProxying() option.
|
|
unsafeCallerProxyOptSentinel := []byte("override-caller sentinel")
|
|
|
|
hooks := &hookstest.Stub{
|
|
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
|
|
sut: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte) (ret []byte, err error) {
|
|
var opts []vm.CallOption
|
|
if bytes.Equal(input, unsafeCallerProxyOptSentinel) {
|
|
opts = append(opts, vm.WithUNSAFECallerAddressProxying())
|
|
}
|
|
// We are ultimately testing env.Call(), hence why this is the
|
|
// SUT. If this is ever extended to include DELEGATECALL or
|
|
// CALLCODE then the expected [libevm.AddressContext.Raw] values
|
|
// of the tests cases also need to change.
|
|
return env.Call(dest, precompileCallData, env.Gas(), uint256.NewInt(0), opts...)
|
|
}),
|
|
dest: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte) (ret []byte, err error) {
|
|
out := &statefulPrecompileOutput{
|
|
Addresses: env.Addresses(),
|
|
ReadOnly: env.ReadOnly(),
|
|
Input: input, // expected to be callData
|
|
}
|
|
return out.Bytes(), nil
|
|
}),
|
|
},
|
|
}
|
|
hookstest.Register(t, params.Extras[*hookstest.Stub, *hookstest.Stub]{
|
|
NewRules: func(_ *params.ChainConfig, r *params.Rules, _ *hookstest.Stub, blockNum *big.Int, isMerge bool, timestamp uint64) *hookstest.Stub {
|
|
r.IsCancun = true // enable PUSH0
|
|
return hooks
|
|
},
|
|
})
|
|
|
|
tests := []struct {
|
|
incomingCallType vm.OpCode
|
|
eoaTxCallData []byte
|
|
// Unlike TestNewStatefulPrecompile, which tests the AddressContext of
|
|
// the precompile itself, these test the AddressContext of a contract
|
|
// called by the precompile.
|
|
want statefulPrecompileOutput
|
|
}{
|
|
{
|
|
incomingCallType: vm.CALL,
|
|
want: statefulPrecompileOutput{
|
|
Addresses: &libevm.AddressContext{
|
|
Origin: eoa,
|
|
EVMSemantic: libevm.CallerAndSelf{
|
|
Caller: sut,
|
|
Self: dest,
|
|
},
|
|
},
|
|
Input: precompileCallData,
|
|
},
|
|
},
|
|
{
|
|
incomingCallType: vm.CALL,
|
|
eoaTxCallData: unsafeCallerProxyOptSentinel,
|
|
want: statefulPrecompileOutput{
|
|
Addresses: &libevm.AddressContext{
|
|
Origin: eoa,
|
|
EVMSemantic: libevm.CallerAndSelf{
|
|
Caller: caller, // overridden by CallOption
|
|
Self: dest,
|
|
},
|
|
},
|
|
Input: precompileCallData,
|
|
},
|
|
},
|
|
{
|
|
incomingCallType: vm.CALLCODE,
|
|
want: statefulPrecompileOutput{
|
|
Addresses: &libevm.AddressContext{
|
|
Origin: eoa,
|
|
EVMSemantic: libevm.CallerAndSelf{
|
|
Caller: caller, // SUT runs as its own caller because of CALLCODE
|
|
Self: dest,
|
|
},
|
|
},
|
|
Input: precompileCallData,
|
|
},
|
|
},
|
|
{
|
|
incomingCallType: vm.CALLCODE,
|
|
eoaTxCallData: unsafeCallerProxyOptSentinel,
|
|
want: statefulPrecompileOutput{
|
|
Addresses: &libevm.AddressContext{
|
|
Origin: eoa,
|
|
EVMSemantic: libevm.CallerAndSelf{
|
|
Caller: caller, // CallOption is a NOOP
|
|
Self: dest,
|
|
},
|
|
},
|
|
Input: precompileCallData,
|
|
},
|
|
},
|
|
{
|
|
incomingCallType: vm.DELEGATECALL,
|
|
want: statefulPrecompileOutput{
|
|
Addresses: &libevm.AddressContext{
|
|
Origin: eoa,
|
|
EVMSemantic: libevm.CallerAndSelf{
|
|
Caller: caller, // as with CALLCODE
|
|
Self: dest,
|
|
},
|
|
},
|
|
Input: precompileCallData,
|
|
},
|
|
},
|
|
{
|
|
incomingCallType: vm.DELEGATECALL,
|
|
eoaTxCallData: unsafeCallerProxyOptSentinel,
|
|
want: statefulPrecompileOutput{
|
|
Addresses: &libevm.AddressContext{
|
|
Origin: eoa,
|
|
EVMSemantic: libevm.CallerAndSelf{
|
|
Caller: caller, // CallOption is a NOOP
|
|
Self: dest,
|
|
},
|
|
},
|
|
Input: precompileCallData,
|
|
},
|
|
},
|
|
{
|
|
incomingCallType: vm.STATICCALL,
|
|
want: statefulPrecompileOutput{
|
|
Addresses: &libevm.AddressContext{
|
|
Origin: eoa,
|
|
EVMSemantic: libevm.CallerAndSelf{
|
|
Caller: sut,
|
|
Self: dest,
|
|
},
|
|
},
|
|
Input: precompileCallData,
|
|
// This demonstrates that even though the precompile makes a
|
|
// (non-static) CALL, the read-only state is inherited. Yes,
|
|
// this is _another_ way to get a read-only state, different to
|
|
// the other tests.
|
|
ReadOnly: true,
|
|
},
|
|
},
|
|
{
|
|
incomingCallType: vm.STATICCALL,
|
|
eoaTxCallData: unsafeCallerProxyOptSentinel,
|
|
want: statefulPrecompileOutput{
|
|
Addresses: &libevm.AddressContext{
|
|
Origin: eoa,
|
|
EVMSemantic: libevm.CallerAndSelf{
|
|
Caller: caller, // overridden by CallOption
|
|
Self: dest,
|
|
},
|
|
},
|
|
Input: precompileCallData,
|
|
ReadOnly: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.incomingCallType.String(), func(t *testing.T) {
|
|
// From the perspective of `dest` after a CALL from `sut`.
|
|
tt.want.Addresses.Raw = &tt.want.Addresses.EVMSemantic
|
|
|
|
t.Logf("calldata = %q", tt.eoaTxCallData)
|
|
state, evm := ethtest.NewZeroEVM(t)
|
|
evm.Origin = eoa
|
|
state.CreateAccount(caller)
|
|
proxy := makeReturnProxy(t, sut, tt.incomingCallType)
|
|
state.SetCode(caller, convertBytes[vm.OpCode, byte](proxy...))
|
|
|
|
got, _, err := evm.Call(vm.AccountRef(eoa), caller, tt.eoaTxCallData, 1e6, uint256.NewInt(0))
|
|
require.NoError(t, err)
|
|
require.Equal(t, tt.want.String(), string(got))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPrecompileCallWithTracer(t *testing.T) {
|
|
// The native pre-state tracer, when logging storage, assumes an invariant
|
|
// that is broken by a precompile calling another contract. This is a test
|
|
// of the fix, ensuring that an SLOADed value is properly handled by the
|
|
// tracer.
|
|
|
|
rng := ethtest.NewPseudoRand(42 * 142857)
|
|
precompile := rng.Address()
|
|
contract := rng.Address()
|
|
|
|
hooks := &hookstest.Stub{
|
|
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
|
|
precompile: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte) (ret []byte, err error) {
|
|
return env.Call(contract, nil, env.Gas(), uint256.NewInt(0))
|
|
}),
|
|
},
|
|
}
|
|
hooks.Register(t)
|
|
|
|
state, evm := ethtest.NewZeroEVM(t)
|
|
evm.GasPrice = big.NewInt(1)
|
|
|
|
state.CreateAccount(contract)
|
|
var zeroHash common.Hash
|
|
value := rng.Hash()
|
|
state.SetState(contract, zeroHash, value)
|
|
state.SetCode(contract, convertBytes[vm.OpCode, byte](vm.PC, vm.SLOAD))
|
|
|
|
const tracerName = "prestateTracer"
|
|
tracer, err := tracers.DefaultDirectory.New(tracerName, nil, nil)
|
|
require.NoErrorf(t, err, "tracers.DefaultDirectory.New(%q)", tracerName)
|
|
evm.Config.Tracer = tracer
|
|
|
|
_, _, err = evm.Call(vm.AccountRef(rng.Address()), precompile, []byte{}, 1e6, uint256.NewInt(0))
|
|
require.NoError(t, err, "evm.Call([precompile that calls regular contract])")
|
|
|
|
gotJSON, err := tracer.GetResult()
|
|
require.NoErrorf(t, err, "%T.GetResult()", tracer)
|
|
var got map[common.Address]struct{ Storage map[common.Hash]common.Hash }
|
|
require.NoErrorf(t, json.Unmarshal(gotJSON, &got), "json.Unmarshal(%T.GetResult(), %T)", tracer, &got)
|
|
require.Equal(t, value, got[contract].Storage[zeroHash], "value loaded with SLOAD")
|
|
}
|