feat: vm.PrecompileEnvironment access to block info (#27)

This commit is contained in:
Arran Schlosberg 2024-09-17 13:08:42 -04:00 committed by GitHub
parent c5da3ca99e
commit ab357e0279
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 122 additions and 13 deletions

View file

@ -73,6 +73,7 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common
BlobBaseFee: blobBaseFee,
GasLimit: header.GasLimit,
Random: random,
Header: header,
}
}

View file

@ -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"
)
@ -99,6 +101,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
}
//
@ -152,6 +158,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

View file

@ -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, 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
}
addrs := env.Addresses()
val := env.ReadOnlyState().GetState(precompile, slot)
return makeOutput(addrs.Caller, addrs.Self, input, val, env.ReadOnly()), suppliedGas - gasCost, 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()), suppliedGas - gasCost, nil
}
hooks := &hookstest.Stub{
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
@ -109,11 +145,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 {
@ -155,12 +198,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)
})
}

View file

@ -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.

View file

@ -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) {
&params.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
})
}

View file

@ -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())
}