From f86459738a14d38d7349ee903dbd5051182be5a4 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Mon, 16 Sep 2024 16:08:35 -0400 Subject: [PATCH] feat: `vm.PrecompileEnvironment` access to block info --- core/evm.go | 1 + core/vm/contracts.libevm.go | 24 +++++++++++ core/vm/contracts.libevm_test.go | 74 +++++++++++++++++++++++++++----- core/vm/evm.go | 2 + libevm/ethtest/evm.go | 27 +++++++++++- libevm/ethtest/rand.go | 7 +++ 6 files changed, 122 insertions(+), 13 deletions(-) diff --git a/core/evm.go b/core/evm.go index 73f6d7bc20a0..2b57456c8d6e 100644 --- a/core/evm.go +++ b/core/evm.go @@ -73,6 +73,7 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common BlobBaseFee: blobBaseFee, GasLimit: header.GasLimit, Random: random, + Header: header, } } diff --git a/core/vm/contracts.libevm.go b/core/vm/contracts.libevm.go index 9bce23999f4d..88e2b2550519 100644 --- a/core/vm/contracts.libevm.go +++ b/core/vm/contracts.libevm.go @@ -2,10 +2,12 @@ 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" ) @@ -98,6 +100,10 @@ type PrecompileEnvironment interface { // ReadOnlyState will always be non-nil. ReadOnlyState() libevm.StateReader Addresses() *libevm.AddressContext + + BlockHeader() (types.Header, error) + BlockNumber() *big.Int + BlockTime() uint64 } // @@ -151,6 +157,24 @@ func (args *evmCallArgs) Addresses() *libevm.AddressContext { } } +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 diff --git a/core/vm/contracts.libevm_test.go b/core/vm/contracts.libevm_test.go index 29358900a9ca..e490ec436976 100644 --- a/core/vm/contracts.libevm_test.go +++ b/core/vm/contracts.libevm_test.go @@ -3,6 +3,8 @@ package vm_test import ( "fmt" "math/big" + "reflect" + "strings" "testing" "github.com/holiman/uint256" @@ -11,6 +13,8 @@ import ( "golang.org/x/exp/rand" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/libevm" @@ -79,6 +83,31 @@ func TestPrecompileOverride(t *testing.T) { } } +type statefulPrecompileOutput struct { + Caller, Self common.Address + StateValue common.Hash + ReadOnly bool + BlockNumber, Difficulty *big.Int + BlockTime uint64 + Input []byte +} + +func (o statefulPrecompileOutput) String() string { + var lines []string + out := reflect.ValueOf(o) + for i, n := 0, out.NumField(); i < n; i++ { + name := out.Type().Field(i).Name + fld := out.Field(i).Interface() + + verb := "%v" + if _, ok := fld.([]byte); ok { + verb = "%#x" + } + lines = append(lines, fmt.Sprintf("%s: "+verb, name, fld)) + } + return strings.Join(lines, "\n") +} + func TestNewStatefulPrecompile(t *testing.T) { rng := ethtest.NewPseudoRand(314159) precompile := rng.Address() @@ -87,20 +116,27 @@ func TestNewStatefulPrecompile(t *testing.T) { const gasLimit = 1e6 gasCost := rng.Uint64n(gasLimit) - makeOutput := func(caller, self common.Address, input []byte, stateVal common.Hash, readOnly bool) []byte { - return []byte(fmt.Sprintf( - "Caller: %v Precompile: %v State: %v Read-only: %t, Input: %#x", - caller, self, stateVal, readOnly, input, - )) - } run := func(env vm.PrecompileEnvironment, input []byte) ([]byte, error) { if got, want := env.StateDB() != nil, !env.ReadOnly(); got != want { return nil, 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, err + } addrs := env.Addresses() - val := env.ReadOnlyState().GetState(precompile, slot) - return makeOutput(addrs.Caller, addrs.Self, input, val, env.ReadOnly()), nil + out := &statefulPrecompileOutput{ + Caller: addrs.Caller, + Self: addrs.Self, + StateValue: env.ReadOnlyState().GetState(precompile, slot), + ReadOnly: env.ReadOnly(), + BlockNumber: env.BlockNumber(), + BlockTime: env.BlockTime(), + Difficulty: hdr.Difficulty, + Input: input, + } + return []byte(out.String()), nil } hooks := &hookstest.Stub{ PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{ @@ -114,11 +150,18 @@ func TestNewStatefulPrecompile(t *testing.T) { } hooks.Register(t) + header := &types.Header{ + Number: rng.BigUint64(), + Time: rng.Uint64(), + Difficulty: rng.BigUint64(), + } caller := rng.Address() input := rng.Bytes(8) value := rng.Hash() - state, evm := ethtest.NewZeroEVM(t) + state, evm := ethtest.NewZeroEVM(t, ethtest.WithBlockContext( + core.NewEVMBlockContext(header, nil, rng.AddressPtr()), + )) state.SetState(precompile, slot, value) tests := []struct { @@ -160,12 +203,21 @@ func TestNewStatefulPrecompile(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - wantReturnData := makeOutput(caller, precompile, input, value, tt.wantReadOnly) + wantReturnData := statefulPrecompileOutput{ + Caller: caller, + Self: precompile, + 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, string(wantReturnData), string(gotReturnData)) + assert.Equal(t, wantReturnData, string(gotReturnData)) assert.Equal(t, wantGasLeft, gotGasLeft) }) } diff --git a/core/vm/evm.go b/core/vm/evm.go index b846b20cbc3b..be1197fc5ba8 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -79,6 +79,8 @@ type BlockContext struct { BaseFee *big.Int // Provides information for BASEFEE (0 if vm runs with NoBaseFee flag and 0 gas price) BlobBaseFee *big.Int // Provides information for BLOBBASEFEE (0 if vm runs with NoBaseFee flag and 0 blob gas price) Random *common.Hash // Provides information for PREVRANDAO + + Header *types.Header // libevm addition; not guaranteed to be set } // TxContext provides the EVM with information about a transaction. diff --git a/libevm/ethtest/evm.go b/libevm/ethtest/evm.go index 171ff60e0308..bf79e64aff1f 100644 --- a/libevm/ethtest/evm.go +++ b/libevm/ethtest/evm.go @@ -19,13 +19,13 @@ import ( // arguments to [vm.NewEVM] are the zero values of their respective types, // except for the use of [core.CanTransfer] and [core.Transfer] instead of nil // functions. -func NewZeroEVM(tb testing.TB) (*state.StateDB, *vm.EVM) { +func NewZeroEVM(tb testing.TB, opts ...EVMOption) (*state.StateDB, *vm.EVM) { tb.Helper() sdb, err := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) require.NoError(tb, err, "state.New()") - return sdb, vm.NewEVM( + vm := vm.NewEVM( vm.BlockContext{ CanTransfer: core.CanTransfer, Transfer: core.Transfer, @@ -35,4 +35,27 @@ func NewZeroEVM(tb testing.TB) (*state.StateDB, *vm.EVM) { ¶ms.ChainConfig{}, vm.Config{}, ) + for _, o := range opts { + o.apply(vm) + } + + return sdb, vm +} + +// An EVMOption configures the EVM returned by [NewZeroEVM]. +type EVMOption interface { + apply(*vm.EVM) +} + +type funcOption func(*vm.EVM) + +var _ EVMOption = funcOption(nil) + +func (f funcOption) apply(vm *vm.EVM) { f(vm) } + +// WithBlockContext overrides the default context. +func WithBlockContext(c vm.BlockContext) EVMOption { + return funcOption(func(vm *vm.EVM) { + vm.Context = c + }) } diff --git a/libevm/ethtest/rand.go b/libevm/ethtest/rand.go index cacce61f51e4..b6319178d1e4 100644 --- a/libevm/ethtest/rand.go +++ b/libevm/ethtest/rand.go @@ -1,6 +1,8 @@ package ethtest import ( + "math/big" + "golang.org/x/exp/rand" "github.com/ethereum/go-ethereum/common" @@ -40,3 +42,8 @@ func (r *PseudoRand) Bytes(n uint) []byte { r.Read(b) //nolint:gosec,errcheck // Guaranteed nil error return b } + +// Big returns [rand.Rand.Uint64] as a [big.Int]. +func (r *PseudoRand) BigUint64() *big.Int { + return new(big.Int).SetUint64(r.Uint64()) +}