Skip to content

Commit

Permalink
feat: vm.PrecompileEnvironment access to block info
Browse files Browse the repository at this point in the history
  • Loading branch information
ARR4N committed Sep 16, 2024
1 parent 58f3883 commit f864597
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 13 deletions.
1 change: 1 addition & 0 deletions core/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common
BlobBaseFee: blobBaseFee,
GasLimit: header.GasLimit,
Random: random,
Header: header,
}
}

Expand Down
24 changes: 24 additions & 0 deletions core/vm/contracts.libevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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
}

//
Expand Down Expand Up @@ -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
Expand Down
74 changes: 63 additions & 11 deletions core/vm/contracts.libevm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package vm_test
import (
"fmt"
"math/big"
"reflect"
"strings"
"testing"

"github.com/holiman/uint256"
Expand All @@ -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"
Expand Down Expand Up @@ -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()
Expand All @@ -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{
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
})
}
Expand Down
2 changes: 2 additions & 0 deletions core/vm/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
27 changes: 25 additions & 2 deletions libevm/ethtest/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
})
}
7 changes: 7 additions & 0 deletions libevm/ethtest/rand.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package ethtest

import (
"math/big"

"golang.org/x/exp/rand"

"github.com/ethereum/go-ethereum/common"
Expand Down Expand Up @@ -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())
}

0 comments on commit f864597

Please sign in to comment.