chore: squash arr4n/libevm into libevm (#7)

* feat: pseudo-generic extra payloads in `params.ChainConfig` and `params.Rules`

* feat: `params.ExtraPayloadGetter` for end-user type safety

* refactor: payloads only available through `params.ExtraPayloadGetter`

* chore: make `libevm/examples/extraparams` a `params` testable example

* doc: `libevm/pseudo` package comments and improved readability

* doc: `params.*Extra*` comments and improved readability

* doc: `params.ExtraPayloadGetter` comments and improved readability

* doc: `params/config.libevm_test.go` comments and improved readability

* refactor: simplify `params.ChainConfig.UnmarshalJSON()`

* refactor: abstract new/nil-pointer creation into `pseudo.Constructor`s

* feat: precompile override via `params.Extras` hooks

* doc: flesh out `PrecompileOverride()` in example

* doc: complete commentary and improve readability

* refactor: `ChainConfig.Hooks()` + `Rules` equivalent

* chore: rename precompiles test file in keeping with geth equivalent

* feat: stateful precompiles + allowlist hooks

The allowlist hooks are included in this commit because they allow for the same functionality as stateful precompiles in `ava-labs/coreth` and `ava-labs/subnet-evm`.

* fix: `StateTransition.canExecuteTransaction()` used `msg.From` instead of `To`

* test: `params.RulesHooks.CanCreateContract` integration

* test: `params.RulesHooks.CanExecuteTransaction` integration

* test: `vm.NewStatefulPrecompile()` integration

* refactor: simplify test of `CanCreateContract`

* refactor: abstract generation of random `Address`/`Hash` values

* doc: full documentation + readability refactoring/renaming

* fix: remove circular dependency in tests
This commit is contained in:
Arran Schlosberg 2024-09-10 19:20:32 +01:00 committed by GitHub
parent 2bd6bd01d2
commit 5429fd87c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 1516 additions and 7 deletions

View file

@ -365,6 +365,9 @@ func (st *StateTransition) preCheck() error {
// However if any consensus issue encountered, return the error directly with
// nil evm execution result.
func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
if err := st.canExecuteTransaction(); err != nil {
return nil, err
}
// First check this message satisfies all consensus rules before
// applying the message. The rules include these clauses
//

View file

@ -0,0 +1,9 @@
package core
// canExecuteTransaction is a convenience wrapper for calling the
// [params.RulesHooks.CanExecuteTransaction] hook.
func (st *StateTransition) canExecuteTransaction() error {
bCtx := st.evm.Context
rules := st.evm.ChainConfig().Rules(bCtx.BlockNumber, bCtx.Random != nil, bCtx.Time)
return rules.Hooks().CanExecuteTransaction(st.msg.From, st.msg.To, st.state)
}

View file

@ -0,0 +1,40 @@
package core_test
import (
"fmt"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/libevm"
"github.com/ethereum/go-ethereum/libevm/ethtest"
"github.com/ethereum/go-ethereum/libevm/hookstest"
"github.com/stretchr/testify/require"
)
func TestCanExecuteTransaction(t *testing.T) {
rng := ethtest.NewPseudoRand(42)
account := rng.Address()
slot := rng.Hash()
makeErr := func(from common.Address, to *common.Address, val common.Hash) error {
return fmt.Errorf("From: %v To: %v State: %v", from, to, val)
}
hooks := &hookstest.Stub{
CanExecuteTransactionFn: func(from common.Address, to *common.Address, s libevm.StateReader) error {
return makeErr(from, to, s.GetState(account, slot))
},
}
hooks.RegisterForRules(t)
value := rng.Hash()
state, evm := ethtest.NewZeroEVM(t)
state.SetState(account, slot, value)
msg := &core.Message{
From: rng.Address(),
To: rng.AddressPtr(),
}
_, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(30e6))
require.EqualError(t, err, makeErr(msg.From, msg.To, value).Error())
}

View file

@ -168,13 +168,13 @@ func ActivePrecompiles(rules params.Rules) []common.Address {
// - the returned bytes,
// - the _remaining_ gas,
// - any error that occurred
func RunPrecompiledContract(p PrecompiledContract, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) {
func (args *evmCallArgs) RunPrecompiledContract(p PrecompiledContract, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) {
gasCost := p.RequiredGas(input)
if suppliedGas < gasCost {
return nil, 0, ErrOutOfGas
}
suppliedGas -= gasCost
output, err := p.Run(input)
output, err := args.run(p, input)
return output, suppliedGas, err
}

View file

@ -0,0 +1,83 @@
package vm
import (
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256"
)
// evmCallArgs mirrors the parameters of the [EVM] methods Call(), CallCode(),
// DelegateCall() and StaticCall(). Its fields are identical to those of the
// parameters, prepended with the receiver name. As {Delegate,Static}Call don't
// accept a value, they MUST set the respective field to nil.
//
// Instantiation can be achieved by merely copying the parameter names, in
// order, which is trivially achieved with AST manipulation:
//
// func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *uint256.Int) ... {
// ...
// args := &evmCallArgs{evm, caller, addr, input, gas, value}
type evmCallArgs struct {
evm *EVM
caller ContractRef
addr common.Address
input []byte
gas uint64
value *uint256.Int
}
// run runs the [PrecompiledContract], differentiating between stateful and
// regular types.
func (args *evmCallArgs) run(p PrecompiledContract, input []byte) (ret []byte, err error) {
if p, ok := p.(statefulPrecompile); ok {
return p.run(args.evm.StateDB, &args.evm.chainRules, args.caller.Address(), args.addr, input)
}
return p.Run(input)
}
// PrecompiledStatefulRun is the stateful equivalent of the Run() method of a
// [PrecompiledContract].
type PrecompiledStatefulRun func(_ StateDB, _ *params.Rules, caller, self common.Address, input []byte) ([]byte, error)
// NewStatefulPrecompile constructs a new PrecompiledContract that can be used
// via an [EVM] instance but MUST NOT be called directly; a direct call to Run()
// reserves the right to panic. See other requirements defined in the comments
// on [PrecompiledContract].
func NewStatefulPrecompile(run PrecompiledStatefulRun, requiredGas func([]byte) uint64) PrecompiledContract {
return statefulPrecompile{
gas: requiredGas,
run: run,
}
}
type statefulPrecompile struct {
gas func([]byte) uint64
run PrecompiledStatefulRun
}
func (p statefulPrecompile) RequiredGas(input []byte) uint64 {
return p.gas(input)
}
func (p statefulPrecompile) Run([]byte) ([]byte, error) {
// https://google.github.io/styleguide/go/best-practices.html#when-to-panic
// This would indicate an API misuse and would occur in tests, not in
// production.
panic(fmt.Sprintf("BUG: call to %T.Run(); MUST call %T", p, p.run))
}
var (
// These lock in the assumptions made when implementing [evmCallArgs]. If
// these break then the struct fields SHOULD be changed to match these
// signatures.
_ = [](func(ContractRef, common.Address, []byte, uint64, *uint256.Int) ([]byte, uint64, error)){
(*EVM)(nil).Call,
(*EVM)(nil).CallCode,
}
_ = [](func(ContractRef, common.Address, []byte, uint64) ([]byte, uint64, error)){
(*EVM)(nil).DelegateCall,
(*EVM)(nil).StaticCall,
}
)

View file

@ -0,0 +1,178 @@
package vm_test
import (
"fmt"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/libevm"
"github.com/ethereum/go-ethereum/libevm/ethtest"
"github.com/ethereum/go-ethereum/libevm/hookstest"
"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"
)
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 vm.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) {
hooks := &hookstest.Stub{
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
tt.addr: &precompileStub{
requiredGas: tt.requiredGas,
returnData: tt.stubData,
},
},
}
hooks.RegisterForRules(t)
t.Run(fmt.Sprintf("%T.Call([overridden precompile address = %v])", &vm.EVM{}, tt.addr), func(t *testing.T) {
_, evm := ethtest.NewZeroEVM(t)
gotData, gotGasLeft, err := evm.Call(vm.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 TestNewStatefulPrecompile(t *testing.T) {
rng := ethtest.NewPseudoRand(314159)
precompile := rng.Address()
slot := rng.Hash()
const gasLimit = 1e6
gasCost := rng.Uint64n(gasLimit)
makeOutput := func(caller, self common.Address, input []byte, stateVal common.Hash) []byte {
return []byte(fmt.Sprintf(
"Caller: %v Precompile: %v State: %v Input: %#x",
caller, self, stateVal, input,
))
}
hooks := &hookstest.Stub{
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
precompile: vm.NewStatefulPrecompile(
func(state vm.StateDB, _ *params.Rules, caller, self common.Address, input []byte) ([]byte, error) {
return makeOutput(caller, self, input, state.GetState(precompile, slot)), nil
},
func(b []byte) uint64 {
return gasCost
},
),
},
}
hooks.RegisterForRules(t)
caller := rng.Address()
input := rng.Bytes(8)
value := rng.Hash()
state, evm := ethtest.NewZeroEVM(t)
state.SetState(precompile, slot, value)
wantReturnData := makeOutput(caller, precompile, input, value)
wantGasLeft := gasLimit - gasCost
gotReturnData, gotGasLeft, err := evm.Call(vm.AccountRef(caller), precompile, input, gasLimit, uint256.NewInt(0))
require.NoError(t, err)
assert.Equal(t, wantReturnData, gotReturnData)
assert.Equal(t, wantGasLeft, gotGasLeft)
}
func TestCanCreateContract(t *testing.T) {
rng := ethtest.NewPseudoRand(142857)
account := rng.Address()
slot := rng.Hash()
makeErr := func(cc *libevm.AddressContext, stateVal common.Hash) error {
return fmt.Errorf("Origin: %v Caller: %v Contract: %v State: %v", cc.Origin, cc.Caller, cc.Self, stateVal)
}
hooks := &hookstest.Stub{
CanCreateContractFn: func(cc *libevm.AddressContext, s libevm.StateReader) error {
return makeErr(cc, s.GetState(account, slot))
},
}
hooks.RegisterForRules(t)
origin := rng.Address()
caller := rng.Address()
value := rng.Hash()
code := rng.Bytes(8)
salt := rng.Hash()
create := crypto.CreateAddress(caller, 0)
create2 := crypto.CreateAddress2(caller, salt, crypto.Keccak256(code))
tests := []struct {
name string
create func(*vm.EVM) ([]byte, common.Address, uint64, error)
wantErr error
}{
{
name: "Create",
create: func(evm *vm.EVM) ([]byte, common.Address, uint64, error) {
return evm.Create(vm.AccountRef(caller), code, 1e6, uint256.NewInt(0))
},
wantErr: makeErr(&libevm.AddressContext{Origin: origin, Caller: caller, Self: create}, value),
},
{
name: "Create2",
create: func(evm *vm.EVM) ([]byte, common.Address, uint64, error) {
return evm.Create2(vm.AccountRef(caller), code, 1e6, uint256.NewInt(0), new(uint256.Int).SetBytes(salt[:]))
},
wantErr: makeErr(&libevm.AddressContext{Origin: origin, Caller: caller, Self: create2}, value),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
state, evm := ethtest.NewZeroEVM(t)
state.SetState(account, slot, value)
evm.TxContext.Origin = origin
_, _, _, err := tt.create(evm)
require.EqualError(t, err, tt.wantErr.Error())
})
}
}

View file

@ -23,6 +23,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/libevm"
"github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256"
)
@ -38,6 +39,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:
@ -224,7 +228,8 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas
}
if isPrecompile {
ret, gas, err = RunPrecompiledContract(p, input, gas)
args := &evmCallArgs{evm, caller, addr, input, gas, value}
ret, gas, err = args.RunPrecompiledContract(p, input, gas)
} else {
// Initialise a new contract and set the code that is to be used by the EVM.
// The contract is a scoped environment for this execution context only.
@ -287,7 +292,8 @@ func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte,
// It is allowed to call precompiles, even via delegatecall
if p, isPrecompile := evm.precompile(addr); isPrecompile {
ret, gas, err = RunPrecompiledContract(p, input, gas)
args := &evmCallArgs{evm, caller, addr, input, gas, value}
ret, gas, err = args.RunPrecompiledContract(p, input, gas)
} else {
addrCopy := addr
// Initialise a new contract and set the code that is to be used by the EVM.
@ -332,7 +338,8 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by
// It is allowed to call precompiles, even via delegatecall
if p, isPrecompile := evm.precompile(addr); isPrecompile {
ret, gas, err = RunPrecompiledContract(p, input, gas)
args := &evmCallArgs{evm, caller, addr, input, gas, nil}
ret, gas, err = args.RunPrecompiledContract(p, input, gas)
} else {
addrCopy := addr
// Initialise a new contract and make initialise the delegate values
@ -381,7 +388,8 @@ func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte
}
if p, isPrecompile := evm.precompile(addr); isPrecompile {
ret, gas, err = RunPrecompiledContract(p, input, gas)
args := &evmCallArgs{evm, caller, addr, input, gas, nil}
ret, gas, err = args.RunPrecompiledContract(p, input, gas)
} else {
// At this point, we use a copy of address. If we don't, the go compiler will
// leak the 'contract' to the outer scope, and make allocation for 'contract'
@ -420,6 +428,10 @@ func (c *codeAndHash) Hash() common.Hash {
// create creates a new contract using code as deployment code.
func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *uint256.Int, address common.Address, typ OpCode) ([]byte, common.Address, uint64, error) {
cc := &libevm.AddressContext{Origin: evm.Origin, Caller: caller.Address(), Self: address}
if err := evm.chainRules.Hooks().CanCreateContract(cc, evm.StateDB); err != nil {
return nil, common.Address{}, gas, err
}
// Depth check execution. Fail if we're trying to execute above the
// limit.
if evm.depth > int(params.CallCreateDepth) {

7
core/vm/libevm_test.go Normal file
View file

@ -0,0 +1,7 @@
package vm
// The original RunPrecompiledContract was migrated to being a method on
// [evmCallArgs]. We need to replace it for use by regular geth tests.
func RunPrecompiledContract(p PrecompiledContract, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) {
return (*evmCallArgs)(nil).RunPrecompiledContract(p, input, suppliedGas)
}

37
libevm/ethtest/evm.go Normal file
View file

@ -0,0 +1,37 @@
// Package ethtest provides utility functions for use in testing
// Ethereum-related functionality.
package ethtest
import (
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/params"
"github.com/stretchr/testify/require"
)
// NewZeroEVM returns a new EVM backed by a [rawdb.NewMemoryDatabase]; all other
// 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) {
tb.Helper()
sdb, err := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
require.NoError(tb, err, "state.New()")
return sdb, vm.NewEVM(
vm.BlockContext{
CanTransfer: core.CanTransfer,
Transfer: core.Transfer,
},
vm.TxContext{},
sdb,
&params.ChainConfig{},
vm.Config{},
)
}

41
libevm/ethtest/rand.go Normal file
View file

@ -0,0 +1,41 @@
package ethtest
import (
"github.com/ethereum/go-ethereum/common"
"golang.org/x/exp/rand"
)
// PseudoRand extends [rand.Rand] (*not* crypto/rand).
type PseudoRand struct {
*rand.Rand
}
// NewPseudoRand returns a new PseudoRand with the given seed.
func NewPseudoRand(seed uint64) *PseudoRand {
return &PseudoRand{rand.New(rand.NewSource(seed))}
}
// Address returns a pseudorandom address.
func (r *PseudoRand) Address() (a common.Address) {
r.Read(a[:])
return a
}
// AddressPtr returns a pointer to a pseudorandom address.
func (r *PseudoRand) AddressPtr() *common.Address {
a := r.Address()
return &a
}
// Hash returns a pseudorandom hash.
func (r *PseudoRand) Hash() (h common.Hash) {
r.Read(h[:])
return h
}
// Bytes returns `n` pseudorandom bytes.
func (r *PseudoRand) Bytes(n uint) []byte {
b := make([]byte, n)
r.Read(b)
return b
}

60
libevm/hookstest/stub.go Normal file
View file

@ -0,0 +1,60 @@
// Package hookstest provides test doubles for testing subsets of libevm hooks.
package hookstest
import (
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/libevm"
"github.com/ethereum/go-ethereum/params"
)
// A Stub is a test double for [params.ChainConfigHooks] and
// [params.RulesHooks]. Each of the fields, if non-nil, back their respective
// hook methods, which otherwise fall back to the default behaviour.
type Stub struct {
PrecompileOverrides map[common.Address]libevm.PrecompiledContract
CanExecuteTransactionFn func(common.Address, *common.Address, libevm.StateReader) error
CanCreateContractFn func(*libevm.AddressContext, libevm.StateReader) error
}
// RegisterForRules clears any registered [params.Extras] and then registers s
// as [params.RulesHooks], which are themselves cleared by the
// [testing.TB.Cleanup] routine.
func (s *Stub) RegisterForRules(tb testing.TB) {
params.TestOnlyClearRegisteredExtras()
params.RegisterExtras(params.Extras[params.NOOPHooks, Stub]{
NewRules: func(_ *params.ChainConfig, _ *params.Rules, _ *params.NOOPHooks, blockNum *big.Int, isMerge bool, timestamp uint64) *Stub {
return s
},
})
tb.Cleanup(params.TestOnlyClearRegisteredExtras)
}
func (s Stub) PrecompileOverride(a common.Address) (libevm.PrecompiledContract, bool) {
if len(s.PrecompileOverrides) == 0 {
return nil, false
}
p, ok := s.PrecompileOverrides[a]
return p, ok
}
func (s Stub) CanExecuteTransaction(from common.Address, to *common.Address, sr libevm.StateReader) error {
if f := s.CanExecuteTransactionFn; f != nil {
return f(from, to, sr)
}
return nil
}
func (s Stub) CanCreateContract(cc *libevm.AddressContext, sr libevm.StateReader) error {
if f := s.CanCreateContractFn; f != nil {
return f(cc, sr)
}
return nil
}
var _ interface {
params.ChainConfigHooks
params.RulesHooks
} = Stub{}

20
libevm/interfaces_test.go Normal file
View file

@ -0,0 +1,20 @@
package libevm_test
import (
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/libevm"
)
// IMPORTANT: if any of these break then the libevm copy MUST be updated.
// These two interfaces MUST be identical.
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)
)
// StateReader MUST be a subset vm.StateDB.
var _ libevm.StateReader = (vm.StateDB)(nil)

52
libevm/libevm.go Normal file
View file

@ -0,0 +1,52 @@
package libevm
import (
"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
)
// 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)
}
// StateReader is a subset of vm.StateDB, exposing only methods that read from
// but do not modify state. See method comments in vm.StateDB, which aren't
// copied here as they risk becoming outdated.
type StateReader interface {
GetBalance(common.Address) *uint256.Int
GetNonce(common.Address) uint64
GetCodeHash(common.Address) common.Hash
GetCode(common.Address) []byte
GetCodeSize(common.Address) int
GetRefund() uint64
GetCommittedState(common.Address, common.Hash) common.Hash
GetState(common.Address, common.Hash) common.Hash
GetTransientState(addr common.Address, key common.Hash) common.Hash
HasSelfDestructed(common.Address) bool
Exist(common.Address) bool
Empty(common.Address) bool
AddressInAccessList(addr common.Address) bool
SlotInAccessList(addr common.Address, slot common.Hash) (addressOk bool, slotOk bool)
}
// AddressContext carries addresses available to contexts such as calls and
// contract creation.
//
// With respect to contract creation, the Self address MAY be the predicted
// address of the contract about to be deployed, which may not exist yet.
type AddressContext struct {
Origin common.Address // equivalent to vm.ORIGIN op code
Caller common.Address // equivalent to vm.CALLER op code
Self common.Address // equivalent to vm.ADDRESS op code
}

View file

@ -0,0 +1,24 @@
package pseudo
// A Constructor returns newly constructed [Type] instances for a pre-registered
// concrete type.
type Constructor interface {
Zero() *Type
NewPointer() *Type
NilPointer() *Type
}
// NewConstructor returns a [Constructor] that builds `T` [Type] instances.
func NewConstructor[T any]() Constructor {
return ctor[T]{}
}
type ctor[T any] struct{}
func (ctor[T]) Zero() *Type { return Zero[T]().Type }
func (ctor[T]) NilPointer() *Type { return Zero[*T]().Type }
func (ctor[T]) NewPointer() *Type {
var x T
return From(&x).Type
}

View file

@ -0,0 +1,45 @@
package pseudo
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestConstructor(t *testing.T) {
testConstructor[uint](t)
testConstructor[string](t)
testConstructor[struct{ x string }](t)
}
func testConstructor[T any](t *testing.T) {
var zero T
t.Run(fmt.Sprintf("%T", zero), func(t *testing.T) {
ctor := NewConstructor[T]()
t.Run("NilPointer()", func(t *testing.T) {
got := get[*T](t, ctor.NilPointer())
assert.Nil(t, got)
})
t.Run("NewPointer()", func(t *testing.T) {
got := get[*T](t, ctor.NewPointer())
require.NotNil(t, got)
assert.Equal(t, zero, *got)
})
t.Run("Zero()", func(t *testing.T) {
got := get[T](t, ctor.Zero())
assert.Equal(t, zero, got)
})
})
}
func get[T any](t *testing.T, typ *Type) (x T) {
t.Helper()
val, err := NewValue[T](typ)
require.NoError(t, err, "NewValue[%T]()", x)
return val.Get()
}

175
libevm/pseudo/type.go Normal file
View file

@ -0,0 +1,175 @@
// Package pseudo provides a bridge between generic and non-generic code via
// pseudo-types and pseudo-values. With careful usage, there is minimal
// reduction in type safety.
//
// Adding generic type parameters to anything (e.g. struct, function, etc)
// "pollutes" all code that uses the generic type. Refactoring all uses isn't
// always feasible, and a [Type] acts as an intermediate fix. Although their
// constructors are generic, they are not, and they are instead coupled with a
// generic [Value] that SHOULD be used for access.
//
// Packages typically SHOULD NOT expose a [Type] and SHOULD instead provide
// users with a type-safe [Value].
package pseudo
import (
"encoding/json"
"fmt"
)
// A Type wraps a strongly-typed value without exposing information about its
// type. It can be used in lieu of a generic field / parameter.
type Type struct {
val value
}
// A Value provides strongly-typed access to the payload carried by a [Type].
type Value[T any] struct {
t *Type
}
// A Pseudo type couples a [Type] and a [Value]. If returned by a constructor
// from this package, both wrap the same payload.
type Pseudo[T any] struct {
Type *Type
Value *Value[T]
}
// TypeAndValue is a convenience function for splitting the contents of `p`,
// typically at construction.
func (p *Pseudo[T]) TypeAndValue() (*Type, *Value[T]) {
return p.Type, p.Value
}
// From returns a Pseudo[T] constructed from `v`.
func From[T any](v T) *Pseudo[T] {
t := &Type{
val: &concrete[T]{
val: v,
},
}
return &Pseudo[T]{t, MustNewValue[T](t)}
}
// Zero is equivalent to [From] called with the [zero value] of type `T`. Note
// that pointers, slices, maps, etc. will therefore be nil.
//
// [zero value]: https://go.dev/tour/basics/12
func Zero[T any]() *Pseudo[T] {
var x T
return From[T](x)
}
// Interface returns the wrapped value as an `any`, equivalent to
// [reflect.Value.Interface]. Prefer [Value.Get].
func (t *Type) Interface() any { return t.val.get() }
// NewValue constructs a [Value] from a [Type], first confirming that `t` wraps
// a payload of type `T`.
func NewValue[T any](t *Type) (*Value[T], error) {
var x T
if !t.val.canSetTo(x) {
return nil, fmt.Errorf("cannot create *Value[%T] with *Type carrying %T", x, t.val.get())
}
return &Value[T]{t}, nil
}
// MustNewValue is equivalent to [NewValue] except that it panics instead of
// returning an error.
func MustNewValue[T any](t *Type) *Value[T] {
v, err := NewValue[T](t)
if err != nil {
panic(err)
}
return v
}
// Get returns the value.
func (a *Value[T]) Get() T { return a.t.val.get().(T) }
// Set sets the value.
func (a *Value[T]) Set(v T) { a.t.val.mustSet(v) }
// MarshalJSON implements the [json.Marshaler] interface.
func (t *Type) MarshalJSON() ([]byte, error) { return t.val.MarshalJSON() }
// UnmarshalJSON implements the [json.Unmarshaler] interface.
func (t *Type) UnmarshalJSON(b []byte) error { return t.val.UnmarshalJSON(b) }
// MarshalJSON implements the [json.Marshaler] interface.
func (v *Value[T]) MarshalJSON() ([]byte, error) { return v.t.MarshalJSON() }
// UnmarshalJSON implements the [json.Unmarshaler] interface.
func (v *Value[T]) UnmarshalJSON(b []byte) error { return v.t.UnmarshalJSON(b) }
var _ = []interface {
json.Marshaler
json.Unmarshaler
}{
(*Type)(nil),
(*Value[struct{}])(nil),
(*concrete[struct{}])(nil),
}
// A value is a non-generic wrapper around a [concrete] struct.
type value interface {
get() any
canSetTo(any) bool
set(any) error
mustSet(any)
json.Marshaler
json.Unmarshaler
}
type concrete[T any] struct {
val T
}
func (c *concrete[T]) get() any { return c.val }
func (c *concrete[T]) canSetTo(v any) bool {
_, ok := v.(T)
return ok
}
// An invalidTypeError is returned by [conrete.set] if the value is incompatible
// with its type. This should never leave this package and exists only to
// provide precise testing of unhappy paths.
type invalidTypeError[T any] struct {
SetTo any
}
func (e *invalidTypeError[T]) Error() string {
var t T
return fmt.Sprintf("cannot set %T to %T", t, e.SetTo)
}
func (c *concrete[T]) set(v any) error {
vv, ok := v.(T)
if !ok {
// Other invariants in this implementation (aim to) guarantee that this
// will never happen.
return &invalidTypeError[T]{SetTo: v}
}
c.val = vv
return nil
}
func (c *concrete[T]) mustSet(v any) {
if err := c.set(v); err != nil {
panic(err)
}
_ = 0 // for happy-path coverage inspection
}
func (c *concrete[T]) MarshalJSON() ([]byte, error) { return json.Marshal(c.val) }
func (c *concrete[T]) UnmarshalJSON(b []byte) error {
var v T
if err := json.Unmarshal(b, &v); err != nil {
return err
}
c.val = v
return nil
}

View file

@ -0,0 +1,79 @@
package pseudo
import (
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestType(t *testing.T) {
testType(t, "Zero[int]", Zero[int], 0, 42, "I'm not an int")
testType(t, "Zero[string]", Zero[string], "", "hello, world", 99)
testType(
t, "From[uint](314159)",
func() *Pseudo[uint] {
return From[uint](314159)
},
314159, 0, struct{}{},
)
testType(t, "nil pointer", Zero[*float64], (*float64)(nil), new(float64), 0)
}
func testType[T any](t *testing.T, name string, ctor func() *Pseudo[T], init T, setTo T, invalid any) {
t.Run(name, func(t *testing.T) {
typ, val := ctor().TypeAndValue()
assert.Equal(t, init, val.Get())
val.Set(setTo)
assert.Equal(t, setTo, val.Get())
t.Run("set to invalid type", func(t *testing.T) {
wantErr := &invalidTypeError[T]{SetTo: invalid}
assertError := func(t *testing.T, err any) {
t.Helper()
switch err := err.(type) {
case *invalidTypeError[T]:
assert.Equal(t, wantErr, err)
default:
t.Errorf("got error %v; want %v", err, wantErr)
}
}
t.Run(fmt.Sprintf("Set(%T{%v})", invalid, invalid), func(t *testing.T) {
assertError(t, typ.val.set(invalid))
})
t.Run(fmt.Sprintf("MustSet(%T{%v})", invalid, invalid), func(t *testing.T) {
defer func() {
assertError(t, recover())
}()
typ.val.mustSet(invalid)
})
})
t.Run("JSON round trip", func(t *testing.T) {
buf, err := json.Marshal(typ)
require.NoError(t, err)
got, gotVal := Zero[T]().TypeAndValue()
require.NoError(t, json.Unmarshal(buf, &got))
assert.Equal(t, val.Get(), gotVal.Get())
})
})
}
func ExamplePseudo_TypeAndValue() {
typ, val := From("hello").TypeAndValue()
// But, if only one is needed:
typ = From("world").Type
val = From("this isn't coupled to the Type").Value
_ = typ
_ = val
}

View file

@ -21,6 +21,7 @@ import (
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/libevm/pseudo"
"github.com/ethereum/go-ethereum/params/forks"
)
@ -365,6 +366,8 @@ type ChainConfig struct {
// Various consensus engines
Ethash *EthashConfig `json:"ethash,omitempty"`
Clique *CliqueConfig `json:"clique,omitempty"`
extra *pseudo.Type // See RegisterExtras()
}
// EthashConfig is the consensus engine configs for proof-of-work based sealing.
@ -902,6 +905,8 @@ type Rules struct {
IsBerlin, IsLondon bool
IsMerge, IsShanghai, IsCancun, IsPrague bool
IsVerkle bool
extra *pseudo.Type // See RegisterExtras()
}
// Rules ensures c's ChainID is not nil.
@ -912,7 +917,7 @@ func (c *ChainConfig) Rules(num *big.Int, isMerge bool, timestamp uint64) Rules
}
// disallow setting Merge out of order
isMerge = isMerge && c.IsLondon(num)
return Rules{
r := Rules{
ChainID: new(big.Int).Set(chainID),
IsHomestead: c.IsHomestead(num),
IsEIP150: c.IsEIP150(num),
@ -930,4 +935,6 @@ func (c *ChainConfig) Rules(num *big.Int, isMerge bool, timestamp uint64) Rules
IsPrague: isMerge && c.IsPrague(num, timestamp),
IsVerkle: isMerge && c.IsVerkle(num, timestamp),
}
c.addRulesExtra(&r, num, isMerge, timestamp)
return r
}

232
params/config.libevm.go Normal file
View file

@ -0,0 +1,232 @@
package params
import (
"encoding/json"
"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 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
// nil then so too will the [Rules] extra payload be a nil `*R`.
//
// NewRules MAY modify the [Rules] but MUST NOT modify the [ChainConfig].
NewRules func(_ *ChainConfig, _ *Rules, _ *C, blockNum *big.Int, isMerge bool, timestamp uint64) *R
}
// RegisterExtras registers the types `C` and `R` such that they are carried as
// extra payloads in [ChainConfig] and [Rules] structs, respectively. It is
// expected to be called in an `init()` function and MUST NOT be called more
// than once. Both `C` and `R` MUST be structs.
//
// After registration, JSON unmarshalling of a [ChainConfig] will create a new
// `*C` and unmarshal the JSON key "extra" into it. Conversely, JSON marshalling
// will populate the "extra" key with the contents of the `*C`. Both the
// [json.Marshaler] and [json.Unmarshaler] interfaces are honoured if
// implemented by `C` and/or `R.`
//
// Calls to [ChainConfig.Rules] will call the `NewRules` function of the
// registered [Extras] to create a new `*R`.
//
// The payloads can be accessed via the [ExtraPayloadGetter.FromChainConfig] and
// [ExtraPayloadGetter.FromRules] methods of the getter returned by
// 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 getter
}
// TestOnlyClearRegisteredExtras clears the [Extras] previously passed to
// [RegisterExtras]. It panics if called from a non-testing call stack.
//
// In tests it SHOULD be called before every call to [RegisterExtras] and then
// defer-called afterwards, either directly or via testing.TB.Cleanup(). This is
// a workaround for the single-call limitation on [RegisterExtras].
func TestOnlyClearRegisteredExtras() {
pc := make([]uintptr, 10)
runtime.Callers(0, pc)
frames := runtime.CallersFrames(pc)
for {
f, more := frames.Next()
if strings.Contains(f.File, "/testing/") || strings.HasSuffix(f.File, "_test.go") {
registeredExtras = nil
return
}
if !more {
panic("no _test.go file in call stack")
}
}
}
// registeredExtras holds non-generic constructors for the [Extras] types
// registered via [RegisterExtras].
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 {
if e.NewRules == nil {
return registeredExtras.rules.NilPointer()
}
rExtra := e.NewRules(c, r, e.getter().FromChainConfig(c), blockNum, isMerge, timestamp)
return pseudo.From(rExtra).Type
}
func (*Extras[C, R]) getter() (g ExtraPayloadGetter[C, R]) { return }
// mustBeStruct panics if `T` isn't a struct.
func mustBeStruct[T any]() {
if k := reflect.TypeFor[T]().Kind(); k != reflect.Struct {
panic(notStructMessage[T]())
}
}
// notStructMessage returns the message with which [mustBeStruct] might panic.
// It exists to avoid change-detector tests should the message contents change.
func notStructMessage[T any]() string {
var x T
return fmt.Sprintf("%T is not a struct", x)
}
// 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 ChainConfigHooks, R RulesHooks] struct {
_ struct{} // make godoc show unexported fields so nobody tries to make their own getter ;)
}
// FromChainConfig returns the ChainConfig's extra payload.
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
}
if e := registeredExtras; e != nil {
cc.Extra = e.chainConfig.NilPointer() // `c.extra` is otherwise unexported
}
if err := json.Unmarshal(data, cc); err != nil {
return err
}
c.extra = cc.Extra
return nil
}
// MarshalJSON implements the [json.Marshaler] interface.
func (c *ChainConfig) MarshalJSON() ([]byte, error) {
// See UnmarshalJSON() for rationale.
type raw ChainConfig
cc := &struct {
*raw
Extra *pseudo.Type `json:"extra"`
}{raw: (*raw)(c), Extra: c.extra}
return json.Marshal(cc)
}
var _ interface {
json.Marshaler
json.Unmarshaler
} = (*ChainConfig)(nil)
// addRulesExtra is called at the end of [ChainConfig.Rules]; it exists to
// abstract the libevm-specific behaviour outside of original geth code.
func (c *ChainConfig) addRulesExtra(r *Rules, blockNum *big.Int, isMerge bool, timestamp uint64) {
r.extra = nil
if registeredExtras != nil {
r.extra = registeredExtras.newForRules(c, r, blockNum, isMerge, timestamp)
}
}
// extraPayload returns the ChainConfig's extra payload iff [RegisterExtras] has
// already been called. If the payload hasn't been populated (typically via
// unmarshalling of JSON), a nil value is constructed and returned.
func (c *ChainConfig) extraPayload() *pseudo.Type {
if registeredExtras == nil {
// This will only happen if someone constructs an [ExtraPayloadGetter]
// directly, without a call to [RegisterExtras].
//
// See https://google.github.io/styleguide/go/best-practices#when-to-panic
panic(fmt.Sprintf("%T.ExtraPayload() called before RegisterExtras()", c))
}
if c.extra == nil {
c.extra = registeredExtras.chainConfig.NilPointer()
}
return c.extra
}
// extraPayload is equivalent to [ChainConfig.extraPayload].
func (r *Rules) extraPayload() *pseudo.Type {
if registeredExtras == nil {
// See ChainConfig.extraPayload() equivalent.
panic(fmt.Sprintf("%T.ExtraPayload() called before RegisterExtras()", r))
}
if r.extra == nil {
r.extra = registeredExtras.rules.NilPointer()
}
return r.extra
}

View file

@ -0,0 +1,164 @@
package params
import (
"encoding/json"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/libevm/pseudo"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type rawJSON struct {
json.RawMessage
NOOPHooks
}
var _ interface {
json.Marshaler
json.Unmarshaler
} = (*rawJSON)(nil)
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
}
)
tests := []struct {
name string
register func()
ccExtra *pseudo.Type
wantRulesExtra any
}{
{
name: "Rules payload copied from ChainConfig payload",
register: func() {
RegisterExtras(Extras[ccExtraA, rulesExtraA]{
NewRules: func(cc *ChainConfig, r *Rules, ex *ccExtraA, _ *big.Int, _ bool, _ uint64) *rulesExtraA {
return &rulesExtraA{
A: ex.A,
}
},
})
},
ccExtra: pseudo.From(&ccExtraA{
A: "hello",
}).Type,
wantRulesExtra: &rulesExtraA{
A: "hello",
},
},
{
name: "no NewForRules() function results in typed but nil pointer",
register: func() {
RegisterExtras(Extras[ccExtraB, rulesExtraB]{})
},
ccExtra: pseudo.From(&ccExtraB{
B: "world",
}).Type,
wantRulesExtra: (*rulesExtraB)(nil),
},
{
name: "custom JSON handling honoured",
register: func() {
RegisterExtras(Extras[rawJSON, struct{ RulesHooks }]{})
},
ccExtra: pseudo.From(&rawJSON{
RawMessage: []byte(`"hello, world"`),
}).Type,
wantRulesExtra: (*struct{ RulesHooks })(nil),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
TestOnlyClearRegisteredExtras()
tt.register()
defer TestOnlyClearRegisteredExtras()
in := &ChainConfig{
ChainID: big.NewInt(142857),
extra: tt.ccExtra,
}
buf, err := json.Marshal(in)
require.NoError(t, err)
got := new(ChainConfig)
require.NoError(t, json.Unmarshal(buf, got))
assert.Equal(t, tt.ccExtra.Interface(), got.extraPayload().Interface())
assert.Equal(t, in, got)
// TODO: do we need an explicit test of the JSON output, or is a
// Marshal-Unmarshal round trip sufficient?
gotRules := got.Rules(nil, false, 0)
assert.Equal(t, tt.wantRulesExtra, gotRules.extraPayload().Interface())
})
}
}
func TestExtrasPanic(t *testing.T) {
TestOnlyClearRegisteredExtras()
defer TestOnlyClearRegisteredExtras()
assertPanics(
t, func() {
new(ChainConfig).extraPayload()
},
"before RegisterExtras",
)
assertPanics(
t, func() {
new(Rules).extraPayload()
},
"before RegisterExtras",
)
assertPanics(
t, func() {
mustBeStruct[int]()
},
notStructMessage[int](),
)
RegisterExtras(Extras[struct{ ChainConfigHooks }, struct{ RulesHooks }]{})
assertPanics(
t, func() {
RegisterExtras(Extras[struct{ ChainConfigHooks }, struct{ RulesHooks }]{})
},
"re-registration",
)
}
func assertPanics(t *testing.T, fn func(), wantContains string) {
t.Helper()
defer func() {
switch r := recover().(type) {
case nil:
t.Error("function did not panic as expected")
case string:
assert.Contains(t, r, wantContains)
default:
t.Fatalf("BAD TEST SETUP: recover() got unsupported type %T", r)
}
}()
fn()
}

View file

@ -0,0 +1,158 @@
// In practice, everything in this file except for the Example() function SHOULD
// be a standalone package, typically called `extraparams`. As long as this new
// package is imported anywhere, its init() function will register the "extra"
// types, which can be accessed via [extraparams.FromChainConfig] and/or
// [extraparams.FromRules]. In all other respects, the [params.ChainConfig] and
// [params.Rules] types will act as expected.
//
// The Example() function demonstrates how the `extraparams` package might be
// used from elsewhere.
package params_test
import (
"encoding/json"
"errors"
"fmt"
"log"
"math/big"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/libevm"
"github.com/ethereum/go-ethereum/params"
)
// In practice this would be a regular init() function but nuances around the
// testing of this package require it to be called in the Example().
func initFn() {
params.TestOnlyClearRegisteredExtras() // not necessary outside of the example
// This registration makes *all* [params.ChainConfig] and [params.Rules]
// instances respect the payload types. They do not need to be modified to
// know about `extraparams`.
getter = params.RegisterExtras(params.Extras[ChainConfigExtra, RulesExtra]{
NewRules: constructRulesExtra,
})
}
var getter params.ExtraPayloadGetter[ChainConfigExtra, RulesExtra]
// constructRulesExtra acts as an adjunct to the [params.ChainConfig.Rules]
// method. Its primary purpose is to construct the extra payload for the
// [params.Rules] but it MAY also modify the [params.Rules].
func constructRulesExtra(c *params.ChainConfig, r *params.Rules, cEx *ChainConfigExtra, blockNum *big.Int, isMerge bool, timestamp uint64) *RulesExtra {
return &RulesExtra{
IsMyFork: cEx.MyForkTime != nil && *cEx.MyForkTime <= timestamp,
timestamp: timestamp,
}
}
// ChainConfigExtra can be any struct. Here it just mirrors a common pattern in
// the standard [params.ChainConfig] struct.
type ChainConfigExtra struct {
MyForkTime *uint64 `json:"myForkTime"`
}
// RulesExtra can be any struct. It too mirrors a common pattern in
// [params.Rules].
type RulesExtra struct {
IsMyFork bool
timestamp uint64
// (Optional) If not all hooks are desirable then embedding a [NOOPHooks]
// allows the type to satisfy the [RulesHooks] interface, resulting in
// default Ethereum behaviour.
params.NOOPHooks
}
// FromChainConfig returns the extra payload carried by the ChainConfig.
func FromChainConfig(c *params.ChainConfig) *ChainConfigExtra {
return getter.FromChainConfig(c)
}
// FromRules returns the extra payload carried by the Rules.
func FromRules(r *params.Rules) *RulesExtra {
return getter.FromRules(r)
}
// myForkPrecompiledContracts is analogous to the vm.PrecompiledContracts<Fork>
// maps. Note [RulesExtra.PrecompileOverride] treatment of nil values here.
var myForkPrecompiledContracts = map[common.Address]vm.PrecompiledContract{
//...
common.BytesToAddress([]byte{0x2}): nil, // i.e disabled
//...
}
// PrecompileOverride implements the required [params.RuleHooks] method.
func (r RulesExtra) PrecompileOverride(addr common.Address) (_ libevm.PrecompiledContract, override bool) {
if !r.IsMyFork {
return nil, false
}
p, ok := myForkPrecompiledContracts[addr]
// The returned boolean indicates whether or not [vm.EVMInterpreter] MUST
// override the address, not what it returns as its own `isPrecompile`
// boolean.
//
// Therefore returning `nil, true` here indicates that the precompile will
// be disabled. Returning `false` here indicates that the default precompile
// behaviour will be exhibited.
//
// The same pattern can alternatively be implemented with an explicit
// `disabledPrecompiles` set to make the behaviour clearer.
return p, ok
}
// CanCreateContract implements the required [params.RuleHooks] method. Access
// to state allows it to be configured on-chain however this is an optional
// implementation detail.
func (r RulesExtra) CanCreateContract(*libevm.AddressContext, libevm.StateReader) error {
if time.Unix(int64(r.timestamp), 0).UTC().Day() != int(time.Tuesday) {
return errors.New("uh oh!")
}
return nil
}
// This example demonstrates how the rest of this file would be used from a
// *different* package.
func ExampleExtraPayloadGetter() {
initFn() // Outside of an example this is unnecessary as the function will be a regular init().
const forkTime = 530003640
jsonData := fmt.Sprintf(`{
"chainId": 1234,
"extra": {
"myForkTime": %d
}
}`, forkTime)
// Because [params.RegisterExtras] has been called, unmarshalling a JSON
// field of "extra" into a [params.ChainConfig] will populate a new value of
// the registered type. This can be accessed with the [FromChainConfig]
// function.
config := new(params.ChainConfig)
if err := json.Unmarshal([]byte(jsonData), config); err != nil {
log.Fatal(err)
}
fmt.Println("Chain ID", config.ChainID) // original geth fields work as expected
ccExtra := FromChainConfig(config) // extraparams.FromChainConfig() in practice
if ccExtra != nil && ccExtra.MyForkTime != nil {
fmt.Println("Fork time", *ccExtra.MyForkTime)
}
for _, time := range []uint64{forkTime - 1, forkTime, forkTime + 1} {
rules := config.Rules(nil, false, time)
rExtra := FromRules(&rules) // extraparams.FromRules() in practice
if rExtra != nil {
fmt.Printf("IsMyFork at %v: %t\n", rExtra.timestamp, rExtra.IsMyFork)
}
}
// Output:
// Chain ID 1234
// Fork time 530003640
// IsMyFork at 530003639: false
// IsMyFork at 530003640: true
// IsMyFork at 530003641: true
}

83
params/hooks.libevm.go Normal file
View file

@ -0,0 +1,83 @@
package params
import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/libevm"
)
// ChainConfigHooks are required for all types registered as [Extras] for
// [ChainConfig] payloads.
type ChainConfigHooks interface{}
// TODO(arr4n): given the choice of whether a hook should be defined on a
// ChainConfig or on the Rules, what are the guiding principles? A ChainConfig
// carries the most general information while Rules benefit from "knowing" the
// block number and timestamp. I am leaning towards the default choice being
// on Rules (as it's trivial to copy information from ChainConfig to Rules in
// [Extras.NewRules]) unless the call site only has access to a ChainConfig.
// RulesHooks are required for all types registered as [Extras] for [Rules]
// payloads.
type RulesHooks interface {
RulesAllowlistHooks
// PrecompileOverride signals whether or not the EVM interpreter MUST
// override its treatment of the address when deciding if it is a
// precompiled contract. If PrecompileOverride returns `true` then the
// interpreter will treat the address as a precompile i.f.f the
// [PrecompiledContract] is non-nil. If it returns `false` then the default
// precompile behaviour is honoured.
PrecompileOverride(common.Address) (_ libevm.PrecompiledContract, override bool)
}
// RulesAllowlistHooks are a subset of [RulesHooks] that gate actions, signalled
// by returning a nil (allowed) or non-nil (blocked) error.
type RulesAllowlistHooks interface {
CanCreateContract(*libevm.AddressContext, libevm.StateReader) error
CanExecuteTransaction(from common.Address, to *common.Address, _ libevm.StateReader) error
}
// Hooks returns the hooks registered with [RegisterExtras], or [NOOPHooks] if
// none were registered.
func (c *ChainConfig) Hooks() ChainConfigHooks {
if e := registeredExtras; e != nil {
return e.getter.hooksFromChainConfig(c)
}
return NOOPHooks{}
}
// Hooks returns the hooks registered with [RegisterExtras], or [NOOPHooks] if
// none were registered.
func (r *Rules) Hooks() RulesHooks {
if e := registeredExtras; e != nil {
return e.getter.hooksFromRules(r)
}
return NOOPHooks{}
}
// NOOPHooks implements both [ChainConfigHooks] and [RulesHooks] such that every
// hook is a no-op. This allows it to be returned instead of a nil interface,
// which would otherwise require every usage site to perform a nil check. It can
// also be embedded in structs that only wish to implement a sub-set of hooks.
// Use of a NOOPHooks is equivalent to default Ethereum behaviour.
type NOOPHooks struct{}
var _ interface {
ChainConfigHooks
RulesHooks
} = NOOPHooks{}
// CanExecuteTransaction allows all (otherwise valid) transactions.
func (NOOPHooks) CanExecuteTransaction(_ common.Address, _ *common.Address, _ libevm.StateReader) error {
return nil
}
// CanCreateContract allows all (otherwise valid) contract deployment.
func (NOOPHooks) CanCreateContract(*libevm.AddressContext, libevm.StateReader) error {
return nil
}
// PrecompileOverride instructs the EVM interpreter to use the default
// precompile behaviour.
func (NOOPHooks) PrecompileOverride(common.Address) (libevm.PrecompiledContract, bool) {
return nil, false
}