mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-20 05:41:35 +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
|
||||
- name: Run 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 test -short $(go list ./... | grep -Pv "${FLAKY_REGEX}");
|
||||
|
|
|
|||
|
|
@ -30,18 +30,20 @@ import (
|
|||
|
||||
// evmCallArgs mirrors the parameters of the [EVM] methods Call(), CallCode(),
|
||||
// DelegateCall() and StaticCall(). Its fields are identical to those of the
|
||||
// parameters, prepended with the receiver name and appended with additional
|
||||
// values. As {Delegate,Static}Call don't accept a value, they MUST set the
|
||||
// respective field to nil.
|
||||
// parameters, prepended with the receiver name and call type. As
|
||||
// {Delegate,Static}Call don't accept a value, they MAY set the respective field
|
||||
// to nil as it will be ignored.
|
||||
//
|
||||
// Instantiation can be achieved by merely copying the parameter names, in
|
||||
// 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 {
|
||||
evm *EVM
|
||||
evm *EVM
|
||||
callType callType
|
||||
|
||||
// args:start
|
||||
caller ContractRef
|
||||
addr common.Address
|
||||
|
|
@ -49,28 +51,22 @@ type evmCallArgs struct {
|
|||
gas uint64
|
||||
value *uint256.Int
|
||||
// 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 (
|
||||
inheritReadOnly rwInheritance = iota + 1
|
||||
forceReadOnly
|
||||
call callType = iota + 1
|
||||
callCode
|
||||
delegateCall
|
||||
staticCall
|
||||
)
|
||||
|
||||
// run runs the [PrecompiledContract], differentiating between stateful and
|
||||
// regular types.
|
||||
func (args *evmCallArgs) run(p PrecompiledContract, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) {
|
||||
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
|
||||
// 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))
|
||||
}
|
||||
|
||||
// A PrecompileEnvironment provides information about the context in which a
|
||||
// precompiled contract is being run.
|
||||
// A PrecompileEnvironment provides (a) information about the context in which a
|
||||
// precompiled contract is being run; and (b) a means of calling other
|
||||
// contracts.
|
||||
type PrecompileEnvironment interface {
|
||||
ChainConfig() *params.ChainConfig
|
||||
Rules() params.Rules
|
||||
|
|
@ -122,78 +119,62 @@ type PrecompileEnvironment interface {
|
|||
BlockHeader() (types.Header, error)
|
||||
BlockNumber() *big.Int
|
||||
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)
|
||||
}
|
||||
|
||||
//
|
||||
// ****** SECURITY ******
|
||||
//
|
||||
// If you are updating PrecompileEnvironment to provide the ability to call back
|
||||
// into another contract, you MUST revisit the evmCallArgs.forceReadOnly flag.
|
||||
//
|
||||
// It is possible that forceReadOnly is true but evm.interpreter.readOnly is
|
||||
// false. This is safe for now, but may not be if recursive calling *from* a
|
||||
// precompile is enabled.
|
||||
//
|
||||
// ****** SECURITY ******
|
||||
func (args *evmCallArgs) env() *environment {
|
||||
var (
|
||||
self common.Address
|
||||
value = args.value
|
||||
)
|
||||
switch args.callType {
|
||||
case staticCall:
|
||||
value = new(uint256.Int)
|
||||
fallthrough
|
||||
case call:
|
||||
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 }
|
||||
func (args *evmCallArgs) Rules() params.Rules { return args.evm.chainRules }
|
||||
// This is equivalent to the `contract` variables created by evm.*Call*()
|
||||
// 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 {
|
||||
if args.readWrite == inheritReadOnly {
|
||||
if args.evm.interpreter.readOnly { //nolint:gosimple // Clearer code coverage for difficult-to-test branch
|
||||
return true
|
||||
}
|
||||
return &environment{
|
||||
evm: args.evm,
|
||||
self: contract,
|
||||
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
|
||||
}
|
||||
// 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 (
|
||||
// These lock in the assumptions made when implementing [evmCallArgs]. If
|
||||
// these break then the struct fields SHOULD be changed to match these
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ func TestPrecompileOverride(t *testing.T) {
|
|||
|
||||
type statefulPrecompileOutput struct {
|
||||
ChainID *big.Int
|
||||
Caller, Self common.Address
|
||||
Addresses *libevm.AddressContext
|
||||
StateValue common.Hash
|
||||
ReadOnly bool
|
||||
BlockNumber, Difficulty *big.Int
|
||||
|
|
@ -116,17 +116,24 @@ func (o statefulPrecompileOutput) String() string {
|
|||
fld := out.Field(i).Interface()
|
||||
|
||||
verb := "%v"
|
||||
if _, ok := fld.([]byte); ok {
|
||||
switch fld.(type) {
|
||||
case []byte:
|
||||
verb = "%#x"
|
||||
case *libevm.AddressContext:
|
||||
verb = "%+v"
|
||||
}
|
||||
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)
|
||||
precompile := rng.Address()
|
||||
slot := rng.Hash()
|
||||
|
||||
const gasLimit = 1e6
|
||||
|
|
@ -141,11 +148,9 @@ func TestNewStatefulPrecompile(t *testing.T) {
|
|||
return nil, 0, err
|
||||
}
|
||||
|
||||
addrs := env.Addresses()
|
||||
out := &statefulPrecompileOutput{
|
||||
ChainID: env.ChainConfig().ChainID,
|
||||
Caller: addrs.Caller,
|
||||
Self: addrs.Self,
|
||||
Addresses: env.Addresses(),
|
||||
StateValue: env.ReadOnlyState().GetState(precompile, slot),
|
||||
ReadOnly: env.ReadOnly(),
|
||||
BlockNumber: env.BlockNumber(),
|
||||
|
|
@ -153,7 +158,7 @@ func TestNewStatefulPrecompile(t *testing.T) {
|
|||
Difficulty: hdr.Difficulty,
|
||||
Input: input,
|
||||
}
|
||||
return []byte(out.String()), suppliedGas - gasCost, nil
|
||||
return out.Bytes(), suppliedGas - gasCost, nil
|
||||
}
|
||||
hooks := &hookstest.Stub{
|
||||
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
|
||||
|
|
@ -167,11 +172,14 @@ func TestNewStatefulPrecompile(t *testing.T) {
|
|||
Time: rng.Uint64(),
|
||||
Difficulty: rng.BigUint64(),
|
||||
}
|
||||
caller := rng.Address()
|
||||
input := rng.Bytes(8)
|
||||
value := rng.Hash()
|
||||
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(
|
||||
t,
|
||||
ethtest.WithBlockContext(
|
||||
|
|
@ -182,39 +190,61 @@ func TestNewStatefulPrecompile(t *testing.T) {
|
|||
),
|
||||
)
|
||||
state.SetState(precompile, slot, value)
|
||||
evm.Origin = eoa
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
call func() ([]byte, uint64, error)
|
||||
// Note that this only covers evm.readWrite being set to forceReadOnly,
|
||||
// via StaticCall(). See TestInheritReadOnly for alternate case.
|
||||
name string
|
||||
call func() ([]byte, uint64, error)
|
||||
wantAddresses *libevm.AddressContext
|
||||
// Note that this only covers evm.readOnly being true because of the
|
||||
// precompile's call. See TestInheritReadOnly for alternate case.
|
||||
wantReadOnly bool
|
||||
}{
|
||||
{
|
||||
name: "EVM.Call()",
|
||||
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,
|
||||
},
|
||||
{
|
||||
name: "EVM.CallCode()",
|
||||
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,
|
||||
},
|
||||
{
|
||||
name: "EVM.DelegateCall()",
|
||||
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,
|
||||
},
|
||||
{
|
||||
name: "EVM.StaticCall()",
|
||||
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,
|
||||
},
|
||||
|
|
@ -222,22 +252,22 @@ func TestNewStatefulPrecompile(t *testing.T) {
|
|||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
wantReturnData := statefulPrecompileOutput{
|
||||
wantOutput := statefulPrecompileOutput{
|
||||
ChainID: chainID,
|
||||
Caller: caller,
|
||||
Self: precompile,
|
||||
Addresses: tt.wantAddresses,
|
||||
StateValue: value,
|
||||
ReadOnly: tt.wantReadOnly,
|
||||
BlockNumber: header.Number,
|
||||
BlockTime: header.Time,
|
||||
Difficulty: header.Difficulty,
|
||||
Input: input,
|
||||
}.String()
|
||||
}
|
||||
|
||||
wantGasLeft := gasLimit - gasCost
|
||||
|
||||
gotReturnData, gotGasLeft, err := tt.call()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, wantReturnData, string(gotReturnData))
|
||||
assert.Equal(t, wantOutput.String(), string(gotReturnData))
|
||||
assert.Equal(t, wantGasLeft, gotGasLeft)
|
||||
})
|
||||
}
|
||||
|
|
@ -265,9 +295,7 @@ func TestInheritReadOnly(t *testing.T) {
|
|||
|
||||
// (1)
|
||||
|
||||
var precompile common.Address
|
||||
const precompileAddr = 255
|
||||
precompile[common.AddressLength-1] = precompileAddr
|
||||
precompile := common.Address{255}
|
||||
|
||||
const (
|
||||
ifReadOnly = iota + 1 // see contract bytecode for rationale
|
||||
|
|
@ -293,31 +321,13 @@ func TestInheritReadOnly(t *testing.T) {
|
|||
})
|
||||
|
||||
// (2)
|
||||
|
||||
// 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,
|
||||
}
|
||||
contract := makeReturnProxy(t, precompile, vm.CALL)
|
||||
|
||||
state, evm := ethtest.NewZeroEVM(t)
|
||||
rng := ethtest.NewPseudoRand(42)
|
||||
contractAddr := rng.Address()
|
||||
state.CreateAccount(contractAddr)
|
||||
state.SetCode(contractAddr, contractCode(contract))
|
||||
state.SetCode(contractAddr, convertBytes[vm.OpCode, byte](contract))
|
||||
|
||||
// (3)
|
||||
|
||||
|
|
@ -352,14 +362,54 @@ func TestInheritReadOnly(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// contractCode converts a slice of op codes into a byte buffer for storage as
|
||||
// contract code.
|
||||
func contractCode(ops []vm.OpCode) []byte {
|
||||
ret := make([]byte, len(ops))
|
||||
for i, o := range ops {
|
||||
ret[i] = byte(o)
|
||||
// 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.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) {
|
||||
|
|
@ -445,3 +495,116 @@ func TestActivePrecompilesOverride(t *testing.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)
|
||||
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 {
|
||||
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)
|
||||
} else {
|
||||
// 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
|
||||
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)
|
||||
} else {
|
||||
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
|
||||
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)
|
||||
} else {
|
||||
addrCopy := addr
|
||||
|
|
@ -390,7 +390,7 @@ func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte
|
|||
}
|
||||
|
||||
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)
|
||||
} else {
|
||||
// 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