Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: vm.PrecompileEnvironment access to block info #27

Merged
merged 2 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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
}

//
Expand Down Expand Up @@ -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
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, 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{
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
})
}
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())
}