Skip to content

Commit ab357e0

Browse files
authored
feat: vm.PrecompileEnvironment access to block info (#27)
1 parent c5da3ca commit ab357e0

File tree

6 files changed

+122
-13
lines changed

6 files changed

+122
-13
lines changed

core/evm.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common
7373
BlobBaseFee: blobBaseFee,
7474
GasLimit: header.GasLimit,
7575
Random: random,
76+
Header: header,
7677
}
7778
}
7879

core/vm/contracts.libevm.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package vm
22

33
import (
44
"fmt"
5+
"math/big"
56

67
"github.com/holiman/uint256"
78

89
"github.com/ethereum/go-ethereum/common"
10+
"github.com/ethereum/go-ethereum/core/types"
911
"github.com/ethereum/go-ethereum/libevm"
1012
"github.com/ethereum/go-ethereum/params"
1113
)
@@ -99,6 +101,10 @@ type PrecompileEnvironment interface {
99101
// ReadOnlyState will always be non-nil.
100102
ReadOnlyState() libevm.StateReader
101103
Addresses() *libevm.AddressContext
104+
105+
BlockHeader() (types.Header, error)
106+
BlockNumber() *big.Int
107+
BlockTime() uint64
102108
}
103109

104110
//
@@ -152,6 +158,24 @@ func (args *evmCallArgs) Addresses() *libevm.AddressContext {
152158
}
153159
}
154160

161+
func (args *evmCallArgs) BlockHeader() (types.Header, error) {
162+
hdr := args.evm.Context.Header
163+
if hdr == nil {
164+
// Although [core.NewEVMBlockContext] sets the field and is in the
165+
// typical hot path (e.g. miner), there are other ways to create a
166+
// [vm.BlockContext] (e.g. directly in tests) that may result in no
167+
// available header.
168+
return types.Header{}, fmt.Errorf("nil %T in current %T", hdr, args.evm.Context)
169+
}
170+
return *hdr, nil
171+
}
172+
173+
func (args *evmCallArgs) BlockNumber() *big.Int {
174+
return new(big.Int).Set(args.evm.Context.BlockNumber)
175+
}
176+
177+
func (args *evmCallArgs) BlockTime() uint64 { return args.evm.Context.Time }
178+
155179
var (
156180
// These lock in the assumptions made when implementing [evmCallArgs]. If
157181
// these break then the struct fields SHOULD be changed to match these

core/vm/contracts.libevm_test.go

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package vm_test
33
import (
44
"fmt"
55
"math/big"
6+
"reflect"
7+
"strings"
68
"testing"
79

810
"github.com/holiman/uint256"
@@ -11,6 +13,8 @@ import (
1113
"golang.org/x/exp/rand"
1214

1315
"github.com/ethereum/go-ethereum/common"
16+
"github.com/ethereum/go-ethereum/core"
17+
"github.com/ethereum/go-ethereum/core/types"
1418
"github.com/ethereum/go-ethereum/core/vm"
1519
"github.com/ethereum/go-ethereum/crypto"
1620
"github.com/ethereum/go-ethereum/libevm"
@@ -79,6 +83,31 @@ func TestPrecompileOverride(t *testing.T) {
7983
}
8084
}
8185

86+
type statefulPrecompileOutput struct {
87+
Caller, Self common.Address
88+
StateValue common.Hash
89+
ReadOnly bool
90+
BlockNumber, Difficulty *big.Int
91+
BlockTime uint64
92+
Input []byte
93+
}
94+
95+
func (o statefulPrecompileOutput) String() string {
96+
var lines []string
97+
out := reflect.ValueOf(o)
98+
for i, n := 0, out.NumField(); i < n; i++ {
99+
name := out.Type().Field(i).Name
100+
fld := out.Field(i).Interface()
101+
102+
verb := "%v"
103+
if _, ok := fld.([]byte); ok {
104+
verb = "%#x"
105+
}
106+
lines = append(lines, fmt.Sprintf("%s: "+verb, name, fld))
107+
}
108+
return strings.Join(lines, "\n")
109+
}
110+
82111
func TestNewStatefulPrecompile(t *testing.T) {
83112
rng := ethtest.NewPseudoRand(314159)
84113
precompile := rng.Address()
@@ -87,20 +116,27 @@ func TestNewStatefulPrecompile(t *testing.T) {
87116
const gasLimit = 1e6
88117
gasCost := rng.Uint64n(gasLimit)
89118

90-
makeOutput := func(caller, self common.Address, input []byte, stateVal common.Hash, readOnly bool) []byte {
91-
return []byte(fmt.Sprintf(
92-
"Caller: %v Precompile: %v State: %v Read-only: %t, Input: %#x",
93-
caller, self, stateVal, readOnly, input,
94-
))
95-
}
96119
run := func(env vm.PrecompileEnvironment, input []byte, suppliedGas uint64) ([]byte, uint64, error) {
97120
if got, want := env.StateDB() != nil, !env.ReadOnly(); got != want {
98121
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)
99122
}
123+
hdr, err := env.BlockHeader()
124+
if err != nil {
125+
return nil, 0, err
126+
}
100127

101128
addrs := env.Addresses()
102-
val := env.ReadOnlyState().GetState(precompile, slot)
103-
return makeOutput(addrs.Caller, addrs.Self, input, val, env.ReadOnly()), suppliedGas - gasCost, nil
129+
out := &statefulPrecompileOutput{
130+
Caller: addrs.Caller,
131+
Self: addrs.Self,
132+
StateValue: env.ReadOnlyState().GetState(precompile, slot),
133+
ReadOnly: env.ReadOnly(),
134+
BlockNumber: env.BlockNumber(),
135+
BlockTime: env.BlockTime(),
136+
Difficulty: hdr.Difficulty,
137+
Input: input,
138+
}
139+
return []byte(out.String()), suppliedGas - gasCost, nil
104140
}
105141
hooks := &hookstest.Stub{
106142
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
@@ -109,11 +145,18 @@ func TestNewStatefulPrecompile(t *testing.T) {
109145
}
110146
hooks.Register(t)
111147

148+
header := &types.Header{
149+
Number: rng.BigUint64(),
150+
Time: rng.Uint64(),
151+
Difficulty: rng.BigUint64(),
152+
}
112153
caller := rng.Address()
113154
input := rng.Bytes(8)
114155
value := rng.Hash()
115156

116-
state, evm := ethtest.NewZeroEVM(t)
157+
state, evm := ethtest.NewZeroEVM(t, ethtest.WithBlockContext(
158+
core.NewEVMBlockContext(header, nil, rng.AddressPtr()),
159+
))
117160
state.SetState(precompile, slot, value)
118161

119162
tests := []struct {
@@ -155,12 +198,21 @@ func TestNewStatefulPrecompile(t *testing.T) {
155198

156199
for _, tt := range tests {
157200
t.Run(tt.name, func(t *testing.T) {
158-
wantReturnData := makeOutput(caller, precompile, input, value, tt.wantReadOnly)
201+
wantReturnData := statefulPrecompileOutput{
202+
Caller: caller,
203+
Self: precompile,
204+
StateValue: value,
205+
ReadOnly: tt.wantReadOnly,
206+
BlockNumber: header.Number,
207+
BlockTime: header.Time,
208+
Difficulty: header.Difficulty,
209+
Input: input,
210+
}.String()
159211
wantGasLeft := gasLimit - gasCost
160212

161213
gotReturnData, gotGasLeft, err := tt.call()
162214
require.NoError(t, err)
163-
assert.Equal(t, string(wantReturnData), string(gotReturnData))
215+
assert.Equal(t, wantReturnData, string(gotReturnData))
164216
assert.Equal(t, wantGasLeft, gotGasLeft)
165217
})
166218
}

core/vm/evm.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ type BlockContext struct {
7979
BaseFee *big.Int // Provides information for BASEFEE (0 if vm runs with NoBaseFee flag and 0 gas price)
8080
BlobBaseFee *big.Int // Provides information for BLOBBASEFEE (0 if vm runs with NoBaseFee flag and 0 blob gas price)
8181
Random *common.Hash // Provides information for PREVRANDAO
82+
83+
Header *types.Header // libevm addition; not guaranteed to be set
8284
}
8385

8486
// TxContext provides the EVM with information about a transaction.

libevm/ethtest/evm.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ import (
1919
// arguments to [vm.NewEVM] are the zero values of their respective types,
2020
// except for the use of [core.CanTransfer] and [core.Transfer] instead of nil
2121
// functions.
22-
func NewZeroEVM(tb testing.TB) (*state.StateDB, *vm.EVM) {
22+
func NewZeroEVM(tb testing.TB, opts ...EVMOption) (*state.StateDB, *vm.EVM) {
2323
tb.Helper()
2424

2525
sdb, err := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
2626
require.NoError(tb, err, "state.New()")
2727

28-
return sdb, vm.NewEVM(
28+
vm := vm.NewEVM(
2929
vm.BlockContext{
3030
CanTransfer: core.CanTransfer,
3131
Transfer: core.Transfer,
@@ -35,4 +35,27 @@ func NewZeroEVM(tb testing.TB) (*state.StateDB, *vm.EVM) {
3535
&params.ChainConfig{},
3636
vm.Config{},
3737
)
38+
for _, o := range opts {
39+
o.apply(vm)
40+
}
41+
42+
return sdb, vm
43+
}
44+
45+
// An EVMOption configures the EVM returned by [NewZeroEVM].
46+
type EVMOption interface {
47+
apply(*vm.EVM)
48+
}
49+
50+
type funcOption func(*vm.EVM)
51+
52+
var _ EVMOption = funcOption(nil)
53+
54+
func (f funcOption) apply(vm *vm.EVM) { f(vm) }
55+
56+
// WithBlockContext overrides the default context.
57+
func WithBlockContext(c vm.BlockContext) EVMOption {
58+
return funcOption(func(vm *vm.EVM) {
59+
vm.Context = c
60+
})
3861
}

libevm/ethtest/rand.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package ethtest
22

33
import (
4+
"math/big"
5+
46
"golang.org/x/exp/rand"
57

68
"github.com/ethereum/go-ethereum/common"
@@ -40,3 +42,8 @@ func (r *PseudoRand) Bytes(n uint) []byte {
4042
r.Read(b) //nolint:gosec,errcheck // Guaranteed nil error
4143
return b
4244
}
45+
46+
// Big returns [rand.Rand.Uint64] as a [big.Int].
47+
func (r *PseudoRand) BigUint64() *big.Int {
48+
return new(big.Int).SetUint64(r.Uint64())
49+
}

0 commit comments

Comments
 (0)