Skip to content

Commit

Permalink
Merge pull request #2 from ava-labs/arr4n/libevm-precompile-overrides
Browse files Browse the repository at this point in the history
feat: precompile override via `params.Extras` hooks
  • Loading branch information
ARR4N authored Sep 10, 2024
2 parents 9c2c214 + 639aa1c commit a33baa3
Show file tree
Hide file tree
Showing 8 changed files with 304 additions and 34 deletions.
113 changes: 113 additions & 0 deletions core/vm/contracts.libevm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package vm

import (
"fmt"
"math/big"
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/libevm"
"github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/exp/rand"
)

// precompileOverrides is a [params.RulesHooks] that overrides precompiles from
// a map of predefined addresses.
type precompileOverrides struct {
contracts map[common.Address]PrecompiledContract
params.NOOPHooks // all other hooks
}

func (o precompileOverrides) PrecompileOverride(a common.Address) (libevm.PrecompiledContract, bool) {
c, ok := o.contracts[a]
return c, ok
}

// A precompileStub is a [PrecompiledContract] that always returns the same
// values.
type precompileStub struct {
requiredGas uint64
returnData []byte
}

func (s *precompileStub) RequiredGas([]byte) uint64 { return s.requiredGas }
func (s *precompileStub) Run([]byte) ([]byte, error) { return s.returnData, nil }

func TestPrecompileOverride(t *testing.T) {
type test struct {
name string
addr common.Address
requiredGas uint64
stubData []byte
}

const gasLimit = uint64(1e7)

tests := []test{
{
name: "arbitrary values",
addr: common.Address{'p', 'r', 'e', 'c', 'o', 'm', 'p', 'i', 'l', 'e'},
requiredGas: 314159,
stubData: []byte("the return data"),
},
}

rng := rand.New(rand.NewSource(42))
for _, addr := range PrecompiledAddressesCancun {
tests = append(tests, test{
name: fmt.Sprintf("existing precompile %v", addr),
addr: addr,
requiredGas: rng.Uint64n(gasLimit),
stubData: addr[:],
})
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
precompile := &precompileStub{
requiredGas: tt.requiredGas,
returnData: tt.stubData,
}

params.TestOnlyClearRegisteredExtras()
params.RegisterExtras(params.Extras[params.NOOPHooks, precompileOverrides]{
NewRules: func(_ *params.ChainConfig, _ *params.Rules, _ *params.NOOPHooks, blockNum *big.Int, isMerge bool, timestamp uint64) *precompileOverrides {
return &precompileOverrides{
contracts: map[common.Address]PrecompiledContract{
tt.addr: precompile,
},
}
},
})

t.Run(fmt.Sprintf("%T.Call([overridden precompile address = %v])", &EVM{}, tt.addr), func(t *testing.T) {
gotData, gotGasLeft, err := newEVM(t).Call(AccountRef{}, tt.addr, nil, gasLimit, uint256.NewInt(0))
require.NoError(t, err)
assert.Equal(t, tt.stubData, gotData, "contract's return data")
assert.Equal(t, gasLimit-tt.requiredGas, gotGasLeft, "gas left")
})
})
}
}

func newEVM(t *testing.T) *EVM {
t.Helper()

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

return NewEVM(
BlockContext{
Transfer: func(_ StateDB, _, _ common.Address, _ *uint256.Int) {},
},
TxContext{},
sdb,
&params.ChainConfig{},
Config{},
)
}
3 changes: 3 additions & 0 deletions core/vm/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ type (
)

func (evm *EVM) precompile(addr common.Address) (PrecompiledContract, bool) {
if p, override := evm.chainRules.Hooks().PrecompileOverride(addr); override {
return p, p != nil
}
var precompiles map[common.Address]PrecompiledContract
switch {
case evm.chainRules.IsCancun:
Expand Down
16 changes: 16 additions & 0 deletions libevm/interfaces_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package libevm_test

import (
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/libevm"
)

// These two interfaces MUST be identical. If this breaks then the libevm copy
// MUST be updated.
var (
// Each assignment demonstrates that the methods of the LHS interface are a
// (non-strict) subset of the RHS interface's; both being possible
// proves that they are identical.
_ vm.PrecompiledContract = (libevm.PrecompiledContract)(nil)
_ libevm.PrecompiledContract = (vm.PrecompiledContract)(nil)
)
9 changes: 9 additions & 0 deletions libevm/libevm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package libevm

// PrecompiledContract is an exact copy of vm.PrecompiledContract, mirrored here
// for instances where importing that package would result in a circular
// dependency.
type PrecompiledContract interface {
RequiredGas(input []byte) uint64
Run(input []byte) ([]byte, error)
}
63 changes: 56 additions & 7 deletions params/config.libevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import (
"fmt"
"math/big"
"reflect"
"runtime"
"strings"

"github.com/ethereum/go-ethereum/libevm/pseudo"
)

// Extras are arbitrary payloads to be added as extra fields in [ChainConfig]
// and [Rules] structs. See [RegisterExtras].
type Extras[C any, R any] struct {
type Extras[C ChainConfigHooks, R RulesHooks] struct {
// NewRules, if non-nil is called at the end of [ChainConfig.Rules] with the
// newly created [Rules] and other context from the method call. Its
// returned value will be the extra payload of the [Rules]. If NewRules is
Expand All @@ -37,19 +39,38 @@ type Extras[C any, R any] struct {
//
// The payloads can be accessed via the [ExtraPayloadGetter.FromChainConfig] and
// [ExtraPayloadGetter.FromRules] methods of the getter returned by
// RegisterExtras.
func RegisterExtras[C any, R any](e Extras[C, R]) ExtraPayloadGetter[C, R] {
// RegisterExtras. Where stated in the interface definitions, they will also be
// used as hooks to alter Ethereum behaviour; if this isn't desired then they
// can embed [NOOPHooks] to satisfy either interface.
func RegisterExtras[C ChainConfigHooks, R RulesHooks](e Extras[C, R]) ExtraPayloadGetter[C, R] {
if registeredExtras != nil {
panic("re-registration of Extras")
}
mustBeStruct[C]()
mustBeStruct[R]()

getter := e.getter()
registeredExtras = &extraConstructors{
chainConfig: pseudo.NewConstructor[C](),
rules: pseudo.NewConstructor[R](),
newForRules: e.newForRules,
getter: getter,
}
return e.getter()
return getter
}

// TestOnlyClearRegisteredExtras clears the [Extras] previously passed to
// [RegisterExtras]. It panics if called from a non-test file.
//
// In tests it SHOULD be called before every call to [RegisterExtras] and then
// defer-called afterwards. This is a workaround for the single-call limitation
// on [RegisterExtras].
func TestOnlyClearRegisteredExtras() {
_, file, _, ok := runtime.Caller(1 /* 0 would be here, not our caller */)
if !ok || !strings.HasSuffix(file, "_test.go") {
panic("call from non-test file")
}
registeredExtras = nil
}

// registeredExtras holds non-generic constructors for the [Extras] types
Expand All @@ -59,6 +80,12 @@ var registeredExtras *extraConstructors
type extraConstructors struct {
chainConfig, rules pseudo.Constructor
newForRules func(_ *ChainConfig, _ *Rules, blockNum *big.Int, isMerge bool, timestamp uint64) *pseudo.Type
// use top-level hooksFrom<X>() functions instead of these as they handle
// instances where no [Extras] were registered.
getter interface {
hooksFromChainConfig(*ChainConfig) ChainConfigHooks
hooksFromRules(*Rules) RulesHooks
}
}

func (e *Extras[C, R]) newForRules(c *ChainConfig, r *Rules, blockNum *big.Int, isMerge bool, timestamp uint64) *pseudo.Type {
Expand Down Expand Up @@ -88,7 +115,7 @@ func notStructMessage[T any]() string {
// An ExtraPayloadGettter provides strongly typed access to the extra payloads
// carried by [ChainConfig] and [Rules] structs. The only valid way to construct
// a getter is by a call to [RegisterExtras].
type ExtraPayloadGetter[C any, R any] struct {
type ExtraPayloadGetter[C ChainConfigHooks, R RulesHooks] struct {
_ struct{} // make godoc show unexported fields so nobody tries to make their own getter ;)
}

Expand All @@ -97,20 +124,42 @@ func (ExtraPayloadGetter[C, R]) FromChainConfig(c *ChainConfig) *C {
return pseudo.MustNewValue[*C](c.extraPayload()).Get()
}

// hooksFromChainConfig is equivalent to FromChainConfig(), but returns an
// interface instead of the concrete type implementing it; this allows it to be
// used in non-generic code. If the concrete-type value is nil (typically
// because no [Extras] were registered) a [noopHooks] is returned so it can be
// used without nil checks.
func (e ExtraPayloadGetter[C, R]) hooksFromChainConfig(c *ChainConfig) ChainConfigHooks {
if h := e.FromChainConfig(c); h != nil {
return *h
}
return NOOPHooks{}
}

// FromRules returns the Rules' extra payload.
func (ExtraPayloadGetter[C, R]) FromRules(r *Rules) *R {
return pseudo.MustNewValue[*R](r.extraPayload()).Get()
}

// hooksFromRules is the [RulesHooks] equivalent of hooksFromChainConfig().
func (e ExtraPayloadGetter[C, R]) hooksFromRules(r *Rules) RulesHooks {
if h := e.FromRules(r); h != nil {
return *h
}
return NOOPHooks{}
}

// UnmarshalJSON implements the [json.Unmarshaler] interface.
func (c *ChainConfig) UnmarshalJSON(data []byte) error {
type raw ChainConfig // doesn't inherit methods so avoids recursing back here (infinitely)
cc := &struct {
*raw
Extra *pseudo.Type `json:"extra"`
}{
raw: (*raw)(c), // embedded to achieve regular JSON unmarshalling
Extra: registeredExtras.chainConfig.NilPointer(), // `c.extra` is otherwise unexported
raw: (*raw)(c), // embedded to achieve regular JSON unmarshalling
}
if e := registeredExtras; e != nil {
cc.Extra = e.chainConfig.NilPointer() // `c.extra` is otherwise unexported
}

if err := json.Unmarshal(data, cc); err != nil {
Expand Down
45 changes: 18 additions & 27 deletions params/config.libevm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,9 @@ import (
"github.com/stretchr/testify/require"
)

// testOnlyClearRegisteredExtras SHOULD be called before every call to
// [RegisterExtras] and then defer-called afterwards. This is a workaround for
// the single-call limitation on [RegisterExtras].
func testOnlyClearRegisteredExtras() {
registeredExtras = nil
}

type rawJSON struct {
json.RawMessage
NOOPHooks
}

var _ interface {
Expand All @@ -30,15 +24,19 @@ func TestRegisterExtras(t *testing.T) {
type (
ccExtraA struct {
A string `json:"a"`
ChainConfigHooks
}
rulesExtraA struct {
A string
RulesHooks
}
ccExtraB struct {
B string `json:"b"`
ChainConfigHooks
}
rulesExtraB struct {
B string
RulesHooks
}
)

Expand Down Expand Up @@ -79,20 +77,20 @@ func TestRegisterExtras(t *testing.T) {
{
name: "custom JSON handling honoured",
register: func() {
RegisterExtras(Extras[rawJSON, struct{}]{})
RegisterExtras(Extras[rawJSON, struct{ RulesHooks }]{})
},
ccExtra: pseudo.From(&rawJSON{
RawMessage: []byte(`"hello, world"`),
}).Type,
wantRulesExtra: (*struct{})(nil),
wantRulesExtra: (*struct{ RulesHooks })(nil),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
testOnlyClearRegisteredExtras()
TestOnlyClearRegisteredExtras()
tt.register()
defer testOnlyClearRegisteredExtras()
defer TestOnlyClearRegisteredExtras()

in := &ChainConfig{
ChainID: big.NewInt(142857),
Expand All @@ -116,42 +114,35 @@ func TestRegisterExtras(t *testing.T) {
}

func TestExtrasPanic(t *testing.T) {
testOnlyClearRegisteredExtras()
defer testOnlyClearRegisteredExtras()

assertPanics(
t, func() {
RegisterExtras(Extras[int, struct{}]{})
},
notStructMessage[int](),
)
TestOnlyClearRegisteredExtras()
defer TestOnlyClearRegisteredExtras()

assertPanics(
t, func() {
RegisterExtras(Extras[struct{}, bool]{})
new(ChainConfig).extraPayload()
},
notStructMessage[bool](),
"before RegisterExtras",
)

assertPanics(
t, func() {
new(ChainConfig).extraPayload()
new(Rules).extraPayload()
},
"before RegisterExtras",
)

assertPanics(
t, func() {
new(Rules).extraPayload()
mustBeStruct[int]()
},
"before RegisterExtras",
notStructMessage[int](),
)

RegisterExtras(Extras[struct{}, struct{}]{})
RegisterExtras(Extras[struct{ ChainConfigHooks }, struct{ RulesHooks }]{})

assertPanics(
t, func() {
RegisterExtras(Extras[struct{}, struct{}]{})
RegisterExtras(Extras[struct{ ChainConfigHooks }, struct{ RulesHooks }]{})
},
"re-registration",
)
Expand Down
Loading

0 comments on commit a33baa3

Please sign in to comment.