mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-24 07:34:31 +00:00
feat: vm.PrecompileEnvironment access to block info (#27)
This commit is contained in:
parent
c5da3ca99e
commit
ab357e0279
6 changed files with 122 additions and 13 deletions
|
|
@ -73,6 +73,7 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common
|
|||
BlobBaseFee: blobBaseFee,
|
||||
GasLimit: header.GasLimit,
|
||||
Random: random,
|
||||
Header: header,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue