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: precompile override via params.Extras hooks #2

Merged
merged 5 commits into from
Sep 10, 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
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