mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-17 18:30:45 +00:00
feat: vm.PrecompiledStatefulContract can make CALLs (#40)
* feat: `vm.PrecompiledStatefulContract` can make `CALL`s
* fix: caller propagation
* feat: precompile can override default caller in `Call()`
* refactor: `WithUNSAFEForceDelegate()` replaces `WithCaller()`
* refactor: `WithUNSAFECallerAddressProxying()` instead of `ForceDelegate`
This matches the pattern used by `ava-labs/coreth` `NativeAssetCall`.
* refactor: `type callType` replaces `rwInheritance` + `delegation` types
* refactor: abstract return-data-proxy contract bytecode
* doc: fix comments from `46346f51`
* fix: `PrecompileEnvironment.Addresses()` for all call types
* chore: readability, linting & mark upstream test flaky
* test: `PrecompileEnvironment.Call()`
* refactor: improved {read,maintain}ability
* doc: fix `evmCallArgs` example
* test: `PrecompileEnvironment.Call()` input data
* fix: write protection for non-zero call value
This commit is contained in:
parent
f1dba53688
commit
210f8ab8e1
6 changed files with 441 additions and 139 deletions
2
.github/workflows/go.yml
vendored
2
.github/workflows/go.yml
vendored
|
|
@ -18,6 +18,6 @@ jobs:
|
||||||
go-version: 1.21.4
|
go-version: 1.21.4
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: | # Upstream flakes are race conditions exacerbated by concurrent tests
|
run: | # Upstream flakes are race conditions exacerbated by concurrent tests
|
||||||
FLAKY_REGEX='go-ethereum/(eth|accounts/keystore|eth/downloader|miner|ethclient/gethclient|eth/catalyst)$';
|
FLAKY_REGEX='go-ethereum/(eth|accounts/keystore|eth/downloader|miner|ethclient|ethclient/gethclient|eth/catalyst)$';
|
||||||
go list ./... | grep -P "${FLAKY_REGEX}" | xargs -n 1 go test -short;
|
go list ./... | grep -P "${FLAKY_REGEX}" | xargs -n 1 go test -short;
|
||||||
go test -short $(go list ./... | grep -Pv "${FLAKY_REGEX}");
|
go test -short $(go list ./... | grep -Pv "${FLAKY_REGEX}");
|
||||||
|
|
|
||||||
|
|
@ -30,18 +30,20 @@ import (
|
||||||
|
|
||||||
// evmCallArgs mirrors the parameters of the [EVM] methods Call(), CallCode(),
|
// evmCallArgs mirrors the parameters of the [EVM] methods Call(), CallCode(),
|
||||||
// DelegateCall() and StaticCall(). Its fields are identical to those of the
|
// DelegateCall() and StaticCall(). Its fields are identical to those of the
|
||||||
// parameters, prepended with the receiver name and appended with additional
|
// parameters, prepended with the receiver name and call type. As
|
||||||
// values. As {Delegate,Static}Call don't accept a value, they MUST set the
|
// {Delegate,Static}Call don't accept a value, they MAY set the respective field
|
||||||
// respective field to nil.
|
// to nil as it will be ignored.
|
||||||
//
|
//
|
||||||
// Instantiation can be achieved by merely copying the parameter names, in
|
// Instantiation can be achieved by merely copying the parameter names, in
|
||||||
// order, which is trivially achieved with AST manipulation:
|
// order, which is trivially achieved with AST manipulation:
|
||||||
//
|
//
|
||||||
// func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *uint256.Int) ... {
|
// func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte, gas uint64) ... {
|
||||||
// ...
|
// ...
|
||||||
// args := &evmCallArgs{evm, caller, addr, input, gas, value, false}
|
// args := &evmCallArgs{evm, staticCall, caller, addr, input, gas, nil /*value*/}
|
||||||
type evmCallArgs struct {
|
type evmCallArgs struct {
|
||||||
evm *EVM
|
evm *EVM
|
||||||
|
callType callType
|
||||||
|
|
||||||
// args:start
|
// args:start
|
||||||
caller ContractRef
|
caller ContractRef
|
||||||
addr common.Address
|
addr common.Address
|
||||||
|
|
@ -49,28 +51,22 @@ type evmCallArgs struct {
|
||||||
gas uint64
|
gas uint64
|
||||||
value *uint256.Int
|
value *uint256.Int
|
||||||
// args:end
|
// args:end
|
||||||
|
|
||||||
// evm.interpreter.readOnly is only set to true via a call to
|
|
||||||
// EVMInterpreter.Run() so, if a precompile is called directly with
|
|
||||||
// StaticCall(), then readOnly might not be set yet. StaticCall() MUST set
|
|
||||||
// this to forceReadOnly and all other methods MUST set it to
|
|
||||||
// inheritReadOnly; i.e. equivalent to the boolean they each pass to
|
|
||||||
// EVMInterpreter.Run().
|
|
||||||
readWrite rwInheritance
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type rwInheritance uint8
|
type callType uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
inheritReadOnly rwInheritance = iota + 1
|
call callType = iota + 1
|
||||||
forceReadOnly
|
callCode
|
||||||
|
delegateCall
|
||||||
|
staticCall
|
||||||
)
|
)
|
||||||
|
|
||||||
// run runs the [PrecompiledContract], differentiating between stateful and
|
// run runs the [PrecompiledContract], differentiating between stateful and
|
||||||
// regular types.
|
// regular types.
|
||||||
func (args *evmCallArgs) run(p PrecompiledContract, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) {
|
func (args *evmCallArgs) run(p PrecompiledContract, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) {
|
||||||
if p, ok := p.(statefulPrecompile); ok {
|
if p, ok := p.(statefulPrecompile); ok {
|
||||||
return p(args, input, suppliedGas)
|
return p(args.env(), input, suppliedGas)
|
||||||
}
|
}
|
||||||
// Gas consumption for regular precompiles was already handled by the native
|
// Gas consumption for regular precompiles was already handled by the native
|
||||||
// RunPrecompiledContract(), which called this method.
|
// RunPrecompiledContract(), which called this method.
|
||||||
|
|
@ -107,8 +103,9 @@ func (p statefulPrecompile) Run([]byte) ([]byte, error) {
|
||||||
panic(fmt.Sprintf("BUG: call to %T.Run(); MUST call %T itself", p, p))
|
panic(fmt.Sprintf("BUG: call to %T.Run(); MUST call %T itself", p, p))
|
||||||
}
|
}
|
||||||
|
|
||||||
// A PrecompileEnvironment provides information about the context in which a
|
// A PrecompileEnvironment provides (a) information about the context in which a
|
||||||
// precompiled contract is being run.
|
// precompiled contract is being run; and (b) a means of calling other
|
||||||
|
// contracts.
|
||||||
type PrecompileEnvironment interface {
|
type PrecompileEnvironment interface {
|
||||||
ChainConfig() *params.ChainConfig
|
ChainConfig() *params.ChainConfig
|
||||||
Rules() params.Rules
|
Rules() params.Rules
|
||||||
|
|
@ -122,78 +119,62 @@ type PrecompileEnvironment interface {
|
||||||
BlockHeader() (types.Header, error)
|
BlockHeader() (types.Header, error)
|
||||||
BlockNumber() *big.Int
|
BlockNumber() *big.Int
|
||||||
BlockTime() uint64
|
BlockTime() uint64
|
||||||
|
|
||||||
|
// Call is equivalent to [EVM.Call] except that the `caller` argument is
|
||||||
|
// removed and automatically determined according to the type of call that
|
||||||
|
// invoked the precompile.
|
||||||
|
Call(addr common.Address, input []byte, gas uint64, value *uint256.Int, _ ...CallOption) (ret []byte, gasRemaining uint64, _ error)
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
func (args *evmCallArgs) env() *environment {
|
||||||
// ****** SECURITY ******
|
var (
|
||||||
//
|
self common.Address
|
||||||
// If you are updating PrecompileEnvironment to provide the ability to call back
|
value = args.value
|
||||||
// into another contract, you MUST revisit the evmCallArgs.forceReadOnly flag.
|
)
|
||||||
//
|
switch args.callType {
|
||||||
// It is possible that forceReadOnly is true but evm.interpreter.readOnly is
|
case staticCall:
|
||||||
// false. This is safe for now, but may not be if recursive calling *from* a
|
value = new(uint256.Int)
|
||||||
// precompile is enabled.
|
fallthrough
|
||||||
//
|
case call:
|
||||||
// ****** SECURITY ******
|
self = args.addr
|
||||||
|
|
||||||
var _ PrecompileEnvironment = (*evmCallArgs)(nil)
|
case delegateCall:
|
||||||
|
value = nil
|
||||||
|
fallthrough
|
||||||
|
case callCode:
|
||||||
|
self = args.caller.Address()
|
||||||
|
}
|
||||||
|
|
||||||
func (args *evmCallArgs) ChainConfig() *params.ChainConfig { return args.evm.chainConfig }
|
// This is equivalent to the `contract` variables created by evm.*Call*()
|
||||||
func (args *evmCallArgs) Rules() params.Rules { return args.evm.chainRules }
|
// methods, for non precompiles, to pass to [EVMInterpreter.Run].
|
||||||
|
contract := NewContract(args.caller, AccountRef(self), value, args.gas)
|
||||||
|
if args.callType == delegateCall {
|
||||||
|
contract = contract.AsDelegate()
|
||||||
|
}
|
||||||
|
|
||||||
func (args *evmCallArgs) ReadOnly() bool {
|
return &environment{
|
||||||
if args.readWrite == inheritReadOnly {
|
evm: args.evm,
|
||||||
if args.evm.interpreter.readOnly { //nolint:gosimple // Clearer code coverage for difficult-to-test branch
|
self: contract,
|
||||||
return true
|
forceReadOnly: args.readOnly(),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (args *evmCallArgs) readOnly() bool {
|
||||||
|
// A switch statement provides clearer code coverage for difficult-to-test
|
||||||
|
// cases.
|
||||||
|
switch {
|
||||||
|
case args.callType == staticCall:
|
||||||
|
// evm.interpreter.readOnly is only set to true via a call to
|
||||||
|
// EVMInterpreter.Run() so, if a precompile is called directly with
|
||||||
|
// StaticCall(), then readOnly might not be set yet.
|
||||||
|
return true
|
||||||
|
case args.evm.interpreter.readOnly:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// Even though args.readWrite may be some value other than forceReadOnly,
|
|
||||||
// that would be an invalid use of the API so we default to read-only as the
|
|
||||||
// safest failure mode.
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (args *evmCallArgs) StateDB() StateDB {
|
|
||||||
if args.ReadOnly() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return args.evm.StateDB
|
|
||||||
}
|
|
||||||
|
|
||||||
func (args *evmCallArgs) ReadOnlyState() libevm.StateReader {
|
|
||||||
// Even though we're actually returning a full state database, the user
|
|
||||||
// would have to actively circumvent the returned interface to use it. At
|
|
||||||
// that point they're off-piste and it's not our problem.
|
|
||||||
return args.evm.StateDB
|
|
||||||
}
|
|
||||||
|
|
||||||
func (args *evmCallArgs) Addresses() *libevm.AddressContext {
|
|
||||||
return &libevm.AddressContext{
|
|
||||||
Origin: args.evm.TxContext.Origin,
|
|
||||||
Caller: args.caller.Address(),
|
|
||||||
Self: args.addr,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (args *evmCallArgs) BlockHeader() (types.Header, error) {
|
|
||||||
hdr := args.evm.Context.Header
|
|
||||||
if hdr == nil {
|
|
||||||
// Although [core.NewEVMBlockContext] sets the field and is in the
|
|
||||||
// typical hot path (e.g. miner), there are other ways to create a
|
|
||||||
// [vm.BlockContext] (e.g. directly in tests) that may result in no
|
|
||||||
// available header.
|
|
||||||
return types.Header{}, fmt.Errorf("nil %T in current %T", hdr, args.evm.Context)
|
|
||||||
}
|
|
||||||
return *hdr, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (args *evmCallArgs) BlockNumber() *big.Int {
|
|
||||||
return new(big.Int).Set(args.evm.Context.BlockNumber)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (args *evmCallArgs) BlockTime() uint64 { return args.evm.Context.Time }
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// These lock in the assumptions made when implementing [evmCallArgs]. If
|
// These lock in the assumptions made when implementing [evmCallArgs]. If
|
||||||
// these break then the struct fields SHOULD be changed to match these
|
// these break then the struct fields SHOULD be changed to match these
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ func TestPrecompileOverride(t *testing.T) {
|
||||||
|
|
||||||
type statefulPrecompileOutput struct {
|
type statefulPrecompileOutput struct {
|
||||||
ChainID *big.Int
|
ChainID *big.Int
|
||||||
Caller, Self common.Address
|
Addresses *libevm.AddressContext
|
||||||
StateValue common.Hash
|
StateValue common.Hash
|
||||||
ReadOnly bool
|
ReadOnly bool
|
||||||
BlockNumber, Difficulty *big.Int
|
BlockNumber, Difficulty *big.Int
|
||||||
|
|
@ -116,17 +116,24 @@ func (o statefulPrecompileOutput) String() string {
|
||||||
fld := out.Field(i).Interface()
|
fld := out.Field(i).Interface()
|
||||||
|
|
||||||
verb := "%v"
|
verb := "%v"
|
||||||
if _, ok := fld.([]byte); ok {
|
switch fld.(type) {
|
||||||
|
case []byte:
|
||||||
verb = "%#x"
|
verb = "%#x"
|
||||||
|
case *libevm.AddressContext:
|
||||||
|
verb = "%+v"
|
||||||
}
|
}
|
||||||
lines = append(lines, fmt.Sprintf("%s: "+verb, name, fld))
|
lines = append(lines, fmt.Sprintf("%s: "+verb, name, fld))
|
||||||
}
|
}
|
||||||
return strings.Join(lines, "\n")
|
return strings.Join(lines, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o statefulPrecompileOutput) Bytes() []byte {
|
||||||
|
return []byte(o.String())
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewStatefulPrecompile(t *testing.T) {
|
func TestNewStatefulPrecompile(t *testing.T) {
|
||||||
|
precompile := common.HexToAddress("60C0DE") // GO CODE
|
||||||
rng := ethtest.NewPseudoRand(314159)
|
rng := ethtest.NewPseudoRand(314159)
|
||||||
precompile := rng.Address()
|
|
||||||
slot := rng.Hash()
|
slot := rng.Hash()
|
||||||
|
|
||||||
const gasLimit = 1e6
|
const gasLimit = 1e6
|
||||||
|
|
@ -141,11 +148,9 @@ func TestNewStatefulPrecompile(t *testing.T) {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
addrs := env.Addresses()
|
|
||||||
out := &statefulPrecompileOutput{
|
out := &statefulPrecompileOutput{
|
||||||
ChainID: env.ChainConfig().ChainID,
|
ChainID: env.ChainConfig().ChainID,
|
||||||
Caller: addrs.Caller,
|
Addresses: env.Addresses(),
|
||||||
Self: addrs.Self,
|
|
||||||
StateValue: env.ReadOnlyState().GetState(precompile, slot),
|
StateValue: env.ReadOnlyState().GetState(precompile, slot),
|
||||||
ReadOnly: env.ReadOnly(),
|
ReadOnly: env.ReadOnly(),
|
||||||
BlockNumber: env.BlockNumber(),
|
BlockNumber: env.BlockNumber(),
|
||||||
|
|
@ -153,7 +158,7 @@ func TestNewStatefulPrecompile(t *testing.T) {
|
||||||
Difficulty: hdr.Difficulty,
|
Difficulty: hdr.Difficulty,
|
||||||
Input: input,
|
Input: input,
|
||||||
}
|
}
|
||||||
return []byte(out.String()), suppliedGas - gasCost, nil
|
return out.Bytes(), suppliedGas - gasCost, nil
|
||||||
}
|
}
|
||||||
hooks := &hookstest.Stub{
|
hooks := &hookstest.Stub{
|
||||||
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
|
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
|
||||||
|
|
@ -167,11 +172,14 @@ func TestNewStatefulPrecompile(t *testing.T) {
|
||||||
Time: rng.Uint64(),
|
Time: rng.Uint64(),
|
||||||
Difficulty: rng.BigUint64(),
|
Difficulty: rng.BigUint64(),
|
||||||
}
|
}
|
||||||
caller := rng.Address()
|
|
||||||
input := rng.Bytes(8)
|
input := rng.Bytes(8)
|
||||||
value := rng.Hash()
|
value := rng.Hash()
|
||||||
chainID := rng.BigUint64()
|
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), uint256.NewInt(0), 1e6)
|
||||||
|
|
||||||
state, evm := ethtest.NewZeroEVM(
|
state, evm := ethtest.NewZeroEVM(
|
||||||
t,
|
t,
|
||||||
ethtest.WithBlockContext(
|
ethtest.WithBlockContext(
|
||||||
|
|
@ -182,39 +190,61 @@ func TestNewStatefulPrecompile(t *testing.T) {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
state.SetState(precompile, slot, value)
|
state.SetState(precompile, slot, value)
|
||||||
|
evm.Origin = eoa
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
call func() ([]byte, uint64, error)
|
call func() ([]byte, uint64, error)
|
||||||
// Note that this only covers evm.readWrite being set to forceReadOnly,
|
wantAddresses *libevm.AddressContext
|
||||||
// via StaticCall(). See TestInheritReadOnly for alternate case.
|
// Note that this only covers evm.readOnly being true because of the
|
||||||
|
// precompile's call. See TestInheritReadOnly for alternate case.
|
||||||
wantReadOnly bool
|
wantReadOnly bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "EVM.Call()",
|
name: "EVM.Call()",
|
||||||
call: func() ([]byte, uint64, error) {
|
call: func() ([]byte, uint64, error) {
|
||||||
return evm.Call(vm.AccountRef(caller), precompile, input, gasLimit, uint256.NewInt(0))
|
return evm.Call(callerContract, precompile, input, gasLimit, uint256.NewInt(0))
|
||||||
|
},
|
||||||
|
wantAddresses: &libevm.AddressContext{
|
||||||
|
Origin: eoa,
|
||||||
|
Caller: caller,
|
||||||
|
Self: precompile,
|
||||||
},
|
},
|
||||||
wantReadOnly: false,
|
wantReadOnly: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "EVM.CallCode()",
|
name: "EVM.CallCode()",
|
||||||
call: func() ([]byte, uint64, error) {
|
call: func() ([]byte, uint64, error) {
|
||||||
return evm.CallCode(vm.AccountRef(caller), precompile, input, gasLimit, uint256.NewInt(0))
|
return evm.CallCode(callerContract, precompile, input, gasLimit, uint256.NewInt(0))
|
||||||
|
},
|
||||||
|
wantAddresses: &libevm.AddressContext{
|
||||||
|
Origin: eoa,
|
||||||
|
Caller: caller,
|
||||||
|
Self: caller,
|
||||||
},
|
},
|
||||||
wantReadOnly: false,
|
wantReadOnly: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "EVM.DelegateCall()",
|
name: "EVM.DelegateCall()",
|
||||||
call: func() ([]byte, uint64, error) {
|
call: func() ([]byte, uint64, error) {
|
||||||
return evm.DelegateCall(vm.AccountRef(caller), precompile, input, gasLimit)
|
return evm.DelegateCall(callerContract, precompile, input, gasLimit)
|
||||||
|
},
|
||||||
|
wantAddresses: &libevm.AddressContext{
|
||||||
|
Origin: eoa,
|
||||||
|
Caller: eoa, // inherited from caller
|
||||||
|
Self: caller,
|
||||||
},
|
},
|
||||||
wantReadOnly: false,
|
wantReadOnly: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "EVM.StaticCall()",
|
name: "EVM.StaticCall()",
|
||||||
call: func() ([]byte, uint64, error) {
|
call: func() ([]byte, uint64, error) {
|
||||||
return evm.StaticCall(vm.AccountRef(caller), precompile, input, gasLimit)
|
return evm.StaticCall(callerContract, precompile, input, gasLimit)
|
||||||
|
},
|
||||||
|
wantAddresses: &libevm.AddressContext{
|
||||||
|
Origin: eoa,
|
||||||
|
Caller: caller,
|
||||||
|
Self: precompile,
|
||||||
},
|
},
|
||||||
wantReadOnly: true,
|
wantReadOnly: true,
|
||||||
},
|
},
|
||||||
|
|
@ -222,22 +252,22 @@ func TestNewStatefulPrecompile(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
wantReturnData := statefulPrecompileOutput{
|
wantOutput := statefulPrecompileOutput{
|
||||||
ChainID: chainID,
|
ChainID: chainID,
|
||||||
Caller: caller,
|
Addresses: tt.wantAddresses,
|
||||||
Self: precompile,
|
|
||||||
StateValue: value,
|
StateValue: value,
|
||||||
ReadOnly: tt.wantReadOnly,
|
ReadOnly: tt.wantReadOnly,
|
||||||
BlockNumber: header.Number,
|
BlockNumber: header.Number,
|
||||||
BlockTime: header.Time,
|
BlockTime: header.Time,
|
||||||
Difficulty: header.Difficulty,
|
Difficulty: header.Difficulty,
|
||||||
Input: input,
|
Input: input,
|
||||||
}.String()
|
}
|
||||||
|
|
||||||
wantGasLeft := gasLimit - gasCost
|
wantGasLeft := gasLimit - gasCost
|
||||||
|
|
||||||
gotReturnData, gotGasLeft, err := tt.call()
|
gotReturnData, gotGasLeft, err := tt.call()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, wantReturnData, string(gotReturnData))
|
assert.Equal(t, wantOutput.String(), string(gotReturnData))
|
||||||
assert.Equal(t, wantGasLeft, gotGasLeft)
|
assert.Equal(t, wantGasLeft, gotGasLeft)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -265,9 +295,7 @@ func TestInheritReadOnly(t *testing.T) {
|
||||||
|
|
||||||
// (1)
|
// (1)
|
||||||
|
|
||||||
var precompile common.Address
|
precompile := common.Address{255}
|
||||||
const precompileAddr = 255
|
|
||||||
precompile[common.AddressLength-1] = precompileAddr
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ifReadOnly = iota + 1 // see contract bytecode for rationale
|
ifReadOnly = iota + 1 // see contract bytecode for rationale
|
||||||
|
|
@ -293,31 +321,13 @@ func TestInheritReadOnly(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
// (2)
|
// (2)
|
||||||
|
contract := makeReturnProxy(t, precompile, vm.CALL)
|
||||||
// See CALL signature: https://www.evm.codes/#f1?fork=cancun
|
|
||||||
const p0 = vm.PUSH0
|
|
||||||
contract := []vm.OpCode{
|
|
||||||
vm.PUSH1, 1, // retSize (bytes)
|
|
||||||
p0, // retOffset
|
|
||||||
p0, // argSize
|
|
||||||
p0, // argOffset
|
|
||||||
p0, // value
|
|
||||||
vm.PUSH1, precompileAddr,
|
|
||||||
p0, // gas
|
|
||||||
vm.CALL,
|
|
||||||
// It's ok to ignore the return status. If the CALL failed then we'll
|
|
||||||
// return []byte{0} next, and both non-failure return buffers are
|
|
||||||
// non-zero because of the `iota + 1`.
|
|
||||||
vm.PUSH1, 1, // size (byte)
|
|
||||||
p0,
|
|
||||||
vm.RETURN,
|
|
||||||
}
|
|
||||||
|
|
||||||
state, evm := ethtest.NewZeroEVM(t)
|
state, evm := ethtest.NewZeroEVM(t)
|
||||||
rng := ethtest.NewPseudoRand(42)
|
rng := ethtest.NewPseudoRand(42)
|
||||||
contractAddr := rng.Address()
|
contractAddr := rng.Address()
|
||||||
state.CreateAccount(contractAddr)
|
state.CreateAccount(contractAddr)
|
||||||
state.SetCode(contractAddr, contractCode(contract))
|
state.SetCode(contractAddr, convertBytes[vm.OpCode, byte](contract))
|
||||||
|
|
||||||
// (3)
|
// (3)
|
||||||
|
|
||||||
|
|
@ -352,14 +362,54 @@ func TestInheritReadOnly(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// contractCode converts a slice of op codes into a byte buffer for storage as
|
// makeReturnProxy returns the bytecode of a contract that will call `dest` with
|
||||||
// contract code.
|
// the specified call type and propagated the returned value.
|
||||||
func contractCode(ops []vm.OpCode) []byte {
|
//
|
||||||
ret := make([]byte, len(ops))
|
// The contract does NOT check if the call reverted. In this case, the
|
||||||
for i, o := range ops {
|
// propagated return value will always be an empty slice. Tests using these
|
||||||
ret[i] = byte(o)
|
// 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.PUSH1, 1, // retSize (bytes)
|
||||||
|
p0, // retOffset
|
||||||
|
p0, // argSize
|
||||||
|
p0, // argOffset
|
||||||
}
|
}
|
||||||
return ret
|
|
||||||
|
// 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) {
|
func TestCanCreateContract(t *testing.T) {
|
||||||
|
|
@ -445,3 +495,116 @@ func TestActivePrecompilesOverride(t *testing.T) {
|
||||||
|
|
||||||
require.Equal(t, precompiles, vm.ActivePrecompiles(newRules()), "vm.ActivePrecompiles() returns overridden addresses")
|
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)
|
||||||
|
callData := rng.Bytes(8)
|
||||||
|
|
||||||
|
hooks := &hookstest.Stub{
|
||||||
|
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
|
||||||
|
sut: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) {
|
||||||
|
// We are ultimately testing env.Call(), hence why this is the SUT.
|
||||||
|
return env.Call(dest, callData, suppliedGas, uint256.NewInt(0))
|
||||||
|
}),
|
||||||
|
dest: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) {
|
||||||
|
out := &statefulPrecompileOutput{
|
||||||
|
Addresses: env.Addresses(),
|
||||||
|
ReadOnly: env.ReadOnly(),
|
||||||
|
Input: input, // expected to be callData
|
||||||
|
}
|
||||||
|
return out.Bytes(), suppliedGas, 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
|
||||||
|
// 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,
|
||||||
|
Caller: sut,
|
||||||
|
Self: dest,
|
||||||
|
},
|
||||||
|
Input: callData,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
incomingCallType: vm.CALLCODE,
|
||||||
|
want: statefulPrecompileOutput{
|
||||||
|
Addresses: &libevm.AddressContext{
|
||||||
|
Origin: eoa,
|
||||||
|
Caller: caller, // SUT runs as its own caller because of CALLCODE
|
||||||
|
Self: dest,
|
||||||
|
},
|
||||||
|
Input: callData,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
incomingCallType: vm.DELEGATECALL,
|
||||||
|
want: statefulPrecompileOutput{
|
||||||
|
Addresses: &libevm.AddressContext{
|
||||||
|
Origin: eoa,
|
||||||
|
Caller: caller, // as with CALLCODE
|
||||||
|
Self: dest,
|
||||||
|
},
|
||||||
|
Input: callData,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
incomingCallType: vm.STATICCALL,
|
||||||
|
want: statefulPrecompileOutput{
|
||||||
|
Addresses: &libevm.AddressContext{
|
||||||
|
Origin: eoa,
|
||||||
|
Caller: sut,
|
||||||
|
Self: dest,
|
||||||
|
},
|
||||||
|
Input: callData,
|
||||||
|
// 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(fmt.Sprintf("via %s", tt.incomingCallType), func(t *testing.T) {
|
||||||
|
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, nil, 1e6, uint256.NewInt(0))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tt.want.String(), string(got))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
120
core/vm/environment.libevm.go
Normal file
120
core/vm/environment.libevm.go
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
// 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
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
"github.com/holiman/uint256"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/ethereum/go-ethereum/libevm"
|
||||||
|
"github.com/ethereum/go-ethereum/params"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ PrecompileEnvironment = (*environment)(nil)
|
||||||
|
|
||||||
|
type environment struct {
|
||||||
|
evm *EVM
|
||||||
|
self *Contract
|
||||||
|
forceReadOnly bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *environment) ChainConfig() *params.ChainConfig { return e.evm.chainConfig }
|
||||||
|
func (e *environment) Rules() params.Rules { return e.evm.chainRules }
|
||||||
|
func (e *environment) ReadOnly() bool { return e.forceReadOnly || e.evm.interpreter.readOnly }
|
||||||
|
func (e *environment) ReadOnlyState() libevm.StateReader { return e.evm.StateDB }
|
||||||
|
func (e *environment) BlockNumber() *big.Int { return new(big.Int).Set(e.evm.Context.BlockNumber) }
|
||||||
|
func (e *environment) BlockTime() uint64 { return e.evm.Context.Time }
|
||||||
|
|
||||||
|
func (e *environment) Addresses() *libevm.AddressContext {
|
||||||
|
return &libevm.AddressContext{
|
||||||
|
Origin: e.evm.Origin,
|
||||||
|
Caller: e.self.CallerAddress,
|
||||||
|
Self: e.self.Address(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *environment) StateDB() StateDB {
|
||||||
|
if e.ReadOnly() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return e.evm.StateDB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *environment) BlockHeader() (types.Header, error) {
|
||||||
|
hdr := e.evm.Context.Header
|
||||||
|
if hdr == nil {
|
||||||
|
// Although [core.NewEVMBlockContext] sets the field and is in the
|
||||||
|
// typical hot path (e.g. miner), there are other ways to create a
|
||||||
|
// [vm.BlockContext] (e.g. directly in tests) that may result in no
|
||||||
|
// available header.
|
||||||
|
return types.Header{}, fmt.Errorf("nil %T in current %T", hdr, e.evm.Context)
|
||||||
|
}
|
||||||
|
return *hdr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *environment) Call(addr common.Address, input []byte, gas uint64, value *uint256.Int, opts ...CallOption) ([]byte, uint64, error) {
|
||||||
|
return e.callContract(call, addr, input, gas, value, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *environment) callContract(typ callType, addr common.Address, input []byte, gas uint64, value *uint256.Int, opts ...CallOption) ([]byte, uint64, error) {
|
||||||
|
// Depth and read-only setting are handled by [EVMInterpreter.Run], which
|
||||||
|
// isn't used for precompiles, so we need to do it ourselves to maintain the
|
||||||
|
// expected invariants.
|
||||||
|
in := e.evm.interpreter
|
||||||
|
|
||||||
|
in.evm.depth++
|
||||||
|
defer func() { in.evm.depth-- }()
|
||||||
|
|
||||||
|
if e.forceReadOnly && !in.readOnly { // i.e. the precompile was StaticCall()ed
|
||||||
|
in.readOnly = true
|
||||||
|
defer func() { in.readOnly = false }()
|
||||||
|
}
|
||||||
|
|
||||||
|
var caller ContractRef = e.self
|
||||||
|
for _, o := range opts {
|
||||||
|
switch o := o.(type) {
|
||||||
|
case callOptUNSAFECallerAddressProxy:
|
||||||
|
// Note that, in addition to being unsafe, this breaks an EVM
|
||||||
|
// assumption that the caller ContractRef is always a *Contract.
|
||||||
|
caller = AccountRef(e.self.CallerAddress)
|
||||||
|
case nil:
|
||||||
|
default:
|
||||||
|
return nil, gas, fmt.Errorf("unsupported option %T", o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch typ {
|
||||||
|
case call:
|
||||||
|
if in.readOnly && !value.IsZero() {
|
||||||
|
return nil, gas, ErrWriteProtection
|
||||||
|
}
|
||||||
|
return e.evm.Call(caller, addr, input, gas, value)
|
||||||
|
case callCode, delegateCall, staticCall:
|
||||||
|
// TODO(arr4n): these cases should be very similar to CALL, hence the
|
||||||
|
// early abstraction, to signal to future maintainers. If implementing
|
||||||
|
// them, there's likely no need to honour the
|
||||||
|
// [callOptUNSAFECallerAddressProxy] because it's purely for backwards
|
||||||
|
// compatibility.
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
return nil, gas, fmt.Errorf("unimplemented precompile call type %v", typ)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -230,7 +230,7 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas
|
||||||
}
|
}
|
||||||
|
|
||||||
if isPrecompile {
|
if isPrecompile {
|
||||||
args := &evmCallArgs{evm, caller, addr, input, gas, value, inheritReadOnly}
|
args := &evmCallArgs{evm, call, caller, addr, input, gas, value}
|
||||||
ret, gas, err = args.RunPrecompiledContract(p, input, gas)
|
ret, gas, err = args.RunPrecompiledContract(p, input, gas)
|
||||||
} else {
|
} else {
|
||||||
// Initialise a new contract and set the code that is to be used by the EVM.
|
// Initialise a new contract and set the code that is to be used by the EVM.
|
||||||
|
|
@ -294,7 +294,7 @@ func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte,
|
||||||
|
|
||||||
// It is allowed to call precompiles, even via delegatecall
|
// It is allowed to call precompiles, even via delegatecall
|
||||||
if p, isPrecompile := evm.precompile(addr); isPrecompile {
|
if p, isPrecompile := evm.precompile(addr); isPrecompile {
|
||||||
args := &evmCallArgs{evm, caller, addr, input, gas, value, inheritReadOnly}
|
args := &evmCallArgs{evm, callCode, caller, addr, input, gas, value}
|
||||||
ret, gas, err = args.RunPrecompiledContract(p, input, gas)
|
ret, gas, err = args.RunPrecompiledContract(p, input, gas)
|
||||||
} else {
|
} else {
|
||||||
addrCopy := addr
|
addrCopy := addr
|
||||||
|
|
@ -340,7 +340,7 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by
|
||||||
|
|
||||||
// It is allowed to call precompiles, even via delegatecall
|
// It is allowed to call precompiles, even via delegatecall
|
||||||
if p, isPrecompile := evm.precompile(addr); isPrecompile {
|
if p, isPrecompile := evm.precompile(addr); isPrecompile {
|
||||||
args := &evmCallArgs{evm, caller, addr, input, gas, nil, inheritReadOnly}
|
args := &evmCallArgs{evm, delegateCall, caller, addr, input, gas, nil}
|
||||||
ret, gas, err = args.RunPrecompiledContract(p, input, gas)
|
ret, gas, err = args.RunPrecompiledContract(p, input, gas)
|
||||||
} else {
|
} else {
|
||||||
addrCopy := addr
|
addrCopy := addr
|
||||||
|
|
@ -390,7 +390,7 @@ func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
if p, isPrecompile := evm.precompile(addr); isPrecompile {
|
if p, isPrecompile := evm.precompile(addr); isPrecompile {
|
||||||
args := &evmCallArgs{evm, caller, addr, input, gas, nil, forceReadOnly}
|
args := &evmCallArgs{evm, staticCall, caller, addr, input, gas, nil}
|
||||||
ret, gas, err = args.RunPrecompiledContract(p, input, gas)
|
ret, gas, err = args.RunPrecompiledContract(p, input, gas)
|
||||||
} else {
|
} else {
|
||||||
// At this point, we use a copy of address. If we don't, the go compiler will
|
// At this point, we use a copy of address. If we don't, the go compiler will
|
||||||
|
|
|
||||||
38
core/vm/options.libevm.go
Normal file
38
core/vm/options.libevm.go
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// A CallOption modifies the default behaviour of a contract call.
|
||||||
|
type CallOption interface {
|
||||||
|
libevmCallOption() // noop to only allow internally defined options
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithUNSAFECallerAddressProxying results in precompiles making contract calls
|
||||||
|
// specifying their own caller's address as the caller. This is NOT SAFE for
|
||||||
|
// regular use as callers of the precompile may not understand that they are
|
||||||
|
// escalating the precompile's privileges.
|
||||||
|
//
|
||||||
|
// Deprecated: this option MUST NOT be used other than to allow migration to
|
||||||
|
// libevm when backwards compatibility is required.
|
||||||
|
func WithUNSAFECallerAddressProxying() CallOption {
|
||||||
|
return callOptUNSAFECallerAddressProxy{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: see [WithUNSAFECallerAddressProxying].
|
||||||
|
type callOptUNSAFECallerAddressProxy struct{}
|
||||||
|
|
||||||
|
func (callOptUNSAFECallerAddressProxy) libevmCallOption() {}
|
||||||
Loading…
Reference in a new issue