mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-19 02:12:23 +00:00
feat: pseudo-generic extra payloads in params.ChainConfig and params.Rules
This commit is contained in:
parent
941ae33d7e
commit
bc6da7c964
8 changed files with 674 additions and 1 deletions
36
libevm/examples/extraparams/extraparams.go
Normal file
36
libevm/examples/extraparams/extraparams.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package extraparams
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/libevm/pseudo"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
)
|
||||
|
||||
func init() {
|
||||
params.RegisterExtras(params.Extras[ChainConfigExtra, RulesExtra]{
|
||||
NewForRules: constructRulesExtra,
|
||||
})
|
||||
}
|
||||
|
||||
type ChainConfigExtra struct {
|
||||
MyFeatureTime *uint64
|
||||
}
|
||||
|
||||
type RulesExtra struct {
|
||||
IsMyFeature bool
|
||||
}
|
||||
|
||||
func constructRulesExtra(c *params.ChainConfig, r *params.Rules, cEx *ChainConfigExtra, blockNum *big.Int, isMerge bool, timestamp uint64) *RulesExtra {
|
||||
return &RulesExtra{
|
||||
IsMyFeature: isMerge && cEx.MyFeatureTime != nil && *cEx.MyFeatureTime < timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
func FromChainConfig(c *params.ChainConfig) *ChainConfigExtra {
|
||||
return pseudo.NewValueUnsafe[*ChainConfigExtra](c.ExtraPayload()).Get()
|
||||
}
|
||||
|
||||
func FromRules(r *params.Rules) *RulesExtra {
|
||||
return pseudo.NewValueUnsafe[*RulesExtra](r.ExtraPayload()).Get()
|
||||
}
|
||||
13
libevm/examples/go.mod
Normal file
13
libevm/examples/go.mod
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
module libevm/examples
|
||||
|
||||
go 1.22.4
|
||||
|
||||
replace github.com/ethereum/go-ethereum => ../../
|
||||
|
||||
require github.com/ethereum/go-ethereum v0.0.0-00010101000000-000000000000
|
||||
|
||||
require (
|
||||
github.com/holiman/uint256 v1.3.1 // indirect
|
||||
golang.org/x/crypto v0.22.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
)
|
||||
14
libevm/examples/go.sum
Normal file
14
libevm/examples/go.sum
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/holiman/uint256 v1.3.1 h1:JfTzmih28bittyHM8z360dCjIA9dbPIBlcTI6lmctQs=
|
||||
github.com/holiman/uint256 v1.3.1/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
121
libevm/pseudo/type.go
Normal file
121
libevm/pseudo/type.go
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
// Package pseudo ...
|
||||
package pseudo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Zero ...
|
||||
func Zero[T any]() (*Type, *Value[T]) {
|
||||
var x T
|
||||
return From[T](x)
|
||||
}
|
||||
|
||||
// From ...
|
||||
func From[T any](x T) (*Type, *Value[T]) {
|
||||
t := &Type{
|
||||
val: &concrete[T]{
|
||||
val: x,
|
||||
},
|
||||
}
|
||||
return t, NewValueUnsafe[T](t)
|
||||
}
|
||||
|
||||
// OnlyType ...
|
||||
func OnlyType[T any](t *Type, _ *Value[T]) *Type {
|
||||
return t
|
||||
}
|
||||
|
||||
// A Type ...
|
||||
type Type struct {
|
||||
val value
|
||||
}
|
||||
|
||||
func (t *Type) Interface() any { return t.val.get() }
|
||||
|
||||
// func (t *Type) Set(v any) error { return t.val.Set(v) }
|
||||
// func (t *Type) MustSet(v any) { t.val.MustSet(v) }
|
||||
func (t *Type) MarshalJSON() ([]byte, error) { return t.val.MarshalJSON() }
|
||||
func (t *Type) UnmarshalJSON(b []byte) error { return t.val.UnmarshalJSON(b) }
|
||||
|
||||
var (
|
||||
_ json.Marshaler = (*Type)(nil)
|
||||
_ json.Unmarshaler = (*Type)(nil)
|
||||
)
|
||||
|
||||
func NewValueUnsafe[T any](t *Type) *Value[T] {
|
||||
return &Value[T]{t: t}
|
||||
}
|
||||
|
||||
func NewValue[T any](t *Type) (*Value[T], error) {
|
||||
var x T
|
||||
if !t.val.canSetTo(x) {
|
||||
return nil, fmt.Errorf("cannot create *Accessor[%T] with *Type carrying %T", x, t.val.get())
|
||||
}
|
||||
return NewValueUnsafe[T](t), nil
|
||||
}
|
||||
|
||||
type Value[T any] struct {
|
||||
t *Type
|
||||
}
|
||||
|
||||
func (a *Value[T]) Get() T { return a.t.val.get().(T) }
|
||||
func (a *Value[T]) Set(v T) { a.t.val.mustSet(v) }
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
68
libevm/pseudo/type_test.go
Normal file
68
libevm/pseudo/type_test.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
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() (*Type, *Value[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() (*Type, *Value[T]), init T, setTo T, invalid any) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
typ, val := ctor()
|
||||
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]()
|
||||
require.NoError(t, json.Unmarshal(buf, &got))
|
||||
assert.Equal(t, val.Get(), gotVal.Get())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
|
@ -340,6 +341,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.
|
||||
|
|
@ -891,6 +894,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.
|
||||
|
|
@ -902,7 +907,7 @@ func (c *ChainConfig) Rules(num *big.Int, isMerge bool, timestamp uint64) Rules
|
|||
// disallow setting Merge out of order
|
||||
isMerge = isMerge && c.IsLondon(num)
|
||||
isVerkle := isMerge && c.IsVerkle(num, timestamp)
|
||||
return Rules{
|
||||
r := Rules{
|
||||
ChainID: new(big.Int).Set(chainID),
|
||||
IsHomestead: c.IsHomestead(num),
|
||||
IsEIP150: c.IsEIP150(num),
|
||||
|
|
@ -922,4 +927,6 @@ func (c *ChainConfig) Rules(num *big.Int, isMerge bool, timestamp uint64) Rules
|
|||
IsVerkle: isVerkle,
|
||||
IsEIP4762: isVerkle,
|
||||
}
|
||||
c.addRulesExtra(&r, num, isMerge, timestamp)
|
||||
return r
|
||||
}
|
||||
|
|
|
|||
164
params/config.libevm.go
Normal file
164
params/config.libevm.go
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
package params
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"reflect"
|
||||
|
||||
"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 {
|
||||
// NewForRules, if non-nil is called at the end of [ChainConfig.Rules] with
|
||||
// the newly created [Rules] and the [ChainConfig] extra payload. Its
|
||||
// returned value will be the extra payload of the [Rules]. If NewForRules
|
||||
// is nil then so too will the [Rules] extra payload be a nil `*R`.
|
||||
//
|
||||
// NewForRules MAY modify the [Rules] but MUST NOT modify the [ChainConfig].
|
||||
NewForRules 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`. Calls to
|
||||
// [ChainConfig.Rules] will call the `NewForRules` function of the registered
|
||||
// [Extras] to create a new `*R`.
|
||||
//
|
||||
// The payloads can be accessed via the [ChainConfig.ExtraPayload] and
|
||||
// [Rules.ExtraPayload] methods, which will always return a `*C` or `*R`
|
||||
// respectively however these pointers may themselves be nil.
|
||||
//
|
||||
// As the `ExtraPayload()` methods are not generic and return `any`, their
|
||||
// values MUST be type-asserted to the returned type; failure to do so may
|
||||
// result in a typed-nil bug. This pattern most-closely resembles a fully
|
||||
// generic implementation and users SHOULD wrap the type assertions in a shared
|
||||
// package.
|
||||
func RegisterExtras[C any, R any](e Extras[C, R]) {
|
||||
if registeredExtras != nil {
|
||||
panic("re-registration of Extras")
|
||||
}
|
||||
mustBeStruct[C]()
|
||||
mustBeStruct[R]()
|
||||
registeredExtras = &e
|
||||
}
|
||||
|
||||
func mustBeStruct[T any]() {
|
||||
var x T
|
||||
if k := reflect.TypeOf(x).Kind(); k != reflect.Struct {
|
||||
panic(notStructMessage[T]())
|
||||
}
|
||||
}
|
||||
|
||||
func notStructMessage[T any]() string {
|
||||
var x T
|
||||
return fmt.Sprintf("%T is not a struct", x)
|
||||
}
|
||||
|
||||
var registeredExtras interface {
|
||||
nilForChainConfig() *pseudo.Type
|
||||
nilForRules() *pseudo.Type
|
||||
newForChainConfig() *pseudo.Type
|
||||
newForRules(_ *ChainConfig, _ *Rules, blockNum *big.Int, isMerge bool, timestamp uint64) *pseudo.Type
|
||||
}
|
||||
|
||||
var (
|
||||
_ json.Unmarshaler = (*ChainConfig)(nil)
|
||||
_ json.Marshaler = (*ChainConfig)(nil)
|
||||
)
|
||||
|
||||
// UnmarshalJSON ... TODO
|
||||
func (c *ChainConfig) UnmarshalJSON(data []byte) error {
|
||||
// We need to bypass this UnmarshalJSON() method when we again call
|
||||
// json.Unmarshal(). The `raw` type won't inherit the method.
|
||||
type raw ChainConfig
|
||||
cc := &struct {
|
||||
*raw
|
||||
Extra json.RawMessage `json:"extra"`
|
||||
}{raw: (*raw)(c)}
|
||||
|
||||
if err := json.Unmarshal(data, cc); err != nil {
|
||||
return err
|
||||
}
|
||||
if registeredExtras == nil || len(cc.Extra) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
extra := registeredExtras.newForChainConfig()
|
||||
if err := json.Unmarshal(cc.Extra, extra); err != nil {
|
||||
return err
|
||||
}
|
||||
c.extra = extra
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON ... TODO
|
||||
func (c *ChainConfig) MarshalJSON() ([]byte, error) {
|
||||
type raw ChainConfig
|
||||
cc := &struct {
|
||||
*raw
|
||||
Extra any `json:"extra"`
|
||||
}{raw: (*raw)(c), Extra: c.extra}
|
||||
return json.Marshal(cc)
|
||||
}
|
||||
|
||||
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 extra payload carried by the ChainConfig and can
|
||||
// only be called if [RegisterExtras] was called. The returned value is always
|
||||
// of type `*C` as registered, but may be nil. Callers MUST immediately
|
||||
// type-assert the returned value to `*C` to avoid typed-nil bugs. See the
|
||||
// example for the intended usage pattern.
|
||||
func (c *ChainConfig) ExtraPayload() *pseudo.Type {
|
||||
if registeredExtras == nil {
|
||||
panic(fmt.Sprintf("%T.ExtraPayload() called before RegisterExtras()", c))
|
||||
}
|
||||
if c.extra == nil {
|
||||
c.extra = registeredExtras.nilForChainConfig()
|
||||
}
|
||||
return c.extra
|
||||
}
|
||||
|
||||
// ExtraPayload returns the extra payload carried by the Rules and can only be
|
||||
// called if [RegisterExtras] was called. The returned value is always of type
|
||||
// `*R` as registered, but may be nil. Callers MUST immediately type-assert the
|
||||
// returned value to `*R` to avoid typed-nil bugs. See the example on
|
||||
// [ChainConfig.ExtraPayload] for the intended usage pattern.
|
||||
func (r *Rules) ExtraPayload() *pseudo.Type {
|
||||
if registeredExtras == nil {
|
||||
panic(fmt.Sprintf("%T.ExtraPayload() called before RegisterExtras()", r))
|
||||
}
|
||||
if r.extra == nil {
|
||||
r.extra = registeredExtras.nilForRules()
|
||||
}
|
||||
return r.extra
|
||||
}
|
||||
|
||||
func (Extras[C, R]) nilForChainConfig() *pseudo.Type { return pseudo.OnlyType(pseudo.Zero[*C]()) }
|
||||
func (Extras[C, R]) nilForRules() *pseudo.Type { return pseudo.OnlyType(pseudo.Zero[*R]()) }
|
||||
|
||||
func (*Extras[C, R]) newForChainConfig() *pseudo.Type {
|
||||
var x C
|
||||
return pseudo.OnlyType(pseudo.From(&x))
|
||||
}
|
||||
|
||||
func (e *Extras[C, R]) newForRules(c *ChainConfig, r *Rules, blockNum *big.Int, isMerge bool, timestamp uint64) *pseudo.Type {
|
||||
if e.NewForRules == nil {
|
||||
return e.nilForRules()
|
||||
}
|
||||
return pseudo.OnlyType(
|
||||
pseudo.From(e.NewForRules(c, r, c.extra.Interface().(*C), blockNum, isMerge, timestamp)),
|
||||
)
|
||||
}
|
||||
250
params/config.libevm_test.go
Normal file
250
params/config.libevm_test.go
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
package params
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/libevm/pseudo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testOnlyClearRegisteredExtras() {
|
||||
registeredExtras = nil
|
||||
}
|
||||
|
||||
func ExampleRegisterExtras() {
|
||||
type (
|
||||
chainConfigExtra struct {
|
||||
Foo string `json:"foo"`
|
||||
}
|
||||
rulesExtra struct {
|
||||
FooCopy string
|
||||
}
|
||||
)
|
||||
|
||||
// In practice, this would be called inside an init() func.
|
||||
RegisterExtras(Extras[chainConfigExtra, rulesExtra]{
|
||||
NewForRules: func(cc *ChainConfig, r *Rules, cEx *chainConfigExtra, blockNum *big.Int, isMerge bool, timestamp uint64) *rulesExtra {
|
||||
// This function is called at the end of ChainConfig.Rules(),
|
||||
// receiving a pointer to the Rules that will be returned. It MAY
|
||||
// modify the Rules but MUST NOT modify the ChainConfig. The value
|
||||
// that it returns will be available via Rules.ExtraPayload().
|
||||
return &rulesExtra{
|
||||
FooCopy: fmt.Sprintf("copy of: %q", cEx.Foo),
|
||||
}
|
||||
},
|
||||
})
|
||||
defer testOnlyClearRegisteredExtras()
|
||||
|
||||
// ChainConfig now unmarshals any JSON field named "extra" into a pointer to
|
||||
// the registered type, which is available via the ExtraPayload() method.
|
||||
buf := []byte(`{
|
||||
"chainId": 1234,
|
||||
"extra": {
|
||||
"foo": "hello, world"
|
||||
}
|
||||
}`)
|
||||
|
||||
var config ChainConfig
|
||||
if err := json.Unmarshal(buf, &config); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(config.ChainID)
|
||||
// The values returned by ExtraPayload() are guaranteed to be pointers to
|
||||
// the registered types. They MAY, however, be nil pointers. In practice,
|
||||
// callers SHOULD abstract the type assertion in a reusable function to
|
||||
// provide a seamless devex.
|
||||
ccExtra := config.ExtraPayload().Interface().(*chainConfigExtra)
|
||||
rules := config.Rules(nil, false, 0)
|
||||
rExtra := rules.ExtraPayload().Interface().(*rulesExtra)
|
||||
|
||||
if ccExtra != nil {
|
||||
fmt.Println(ccExtra.Foo)
|
||||
}
|
||||
if rExtra != nil {
|
||||
fmt.Println(rExtra.FooCopy)
|
||||
}
|
||||
|
||||
// Output:
|
||||
// 1234
|
||||
// hello, world
|
||||
// copy of: "hello, world"
|
||||
}
|
||||
|
||||
func ExampleChainConfig_ExtraPayload() {
|
||||
type (
|
||||
chainConfigExtra struct{}
|
||||
rulesExtra struct{}
|
||||
)
|
||||
// Typically called in an `init()` function.
|
||||
RegisterExtras(Extras[chainConfigExtra, rulesExtra]{ /*...*/ })
|
||||
defer testOnlyClearRegisteredExtras()
|
||||
|
||||
var c ChainConfig // Sourced from elsewhere, typically unmarshalled from JSON.
|
||||
|
||||
// Both ChainConfig.ExtraPayload() and Rules.ExtraPayload() return `any`
|
||||
// that are guaranteed to be pointers to the registered types.
|
||||
extra := c.ExtraPayload().Interface().(*chainConfigExtra)
|
||||
|
||||
// Act on the extra payload...
|
||||
if extra != nil {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
type rawJSON struct {
|
||||
json.RawMessage
|
||||
}
|
||||
|
||||
var (
|
||||
_ json.Unmarshaler = (*rawJSON)(nil)
|
||||
_ json.Marshaler = (*rawJSON)(nil)
|
||||
)
|
||||
|
||||
func TestRegisterExtras(t *testing.T) {
|
||||
type (
|
||||
ccExtraA struct {
|
||||
A string `json:"a"`
|
||||
}
|
||||
rulesExtraA struct {
|
||||
A string
|
||||
}
|
||||
ccExtraB struct {
|
||||
B string `json:"b"`
|
||||
}
|
||||
rulesExtraB struct {
|
||||
B string
|
||||
}
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
register func()
|
||||
ccExtra *pseudo.Type
|
||||
wantRulesExtra any
|
||||
}{
|
||||
{
|
||||
name: "Rules payload copied from ChainConfig payload",
|
||||
register: func() {
|
||||
RegisterExtras(Extras[ccExtraA, rulesExtraA]{
|
||||
NewForRules: func(cc *ChainConfig, r *Rules, ex *ccExtraA, _ *big.Int, _ bool, _ uint64) *rulesExtraA {
|
||||
return &rulesExtraA{
|
||||
A: ex.A,
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
ccExtra: pseudo.OnlyType(pseudo.From(&ccExtraA{
|
||||
A: "hello",
|
||||
})),
|
||||
wantRulesExtra: &rulesExtraA{
|
||||
A: "hello",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no NewForRules() function results in typed but nil pointer",
|
||||
register: func() {
|
||||
RegisterExtras(Extras[ccExtraB, rulesExtraB]{})
|
||||
},
|
||||
ccExtra: pseudo.OnlyType(pseudo.From(&ccExtraB{
|
||||
B: "world",
|
||||
})),
|
||||
wantRulesExtra: (*rulesExtraB)(nil),
|
||||
},
|
||||
{
|
||||
name: "custom JSON handling honoured",
|
||||
register: func() {
|
||||
RegisterExtras(Extras[rawJSON, struct{}]{})
|
||||
},
|
||||
ccExtra: pseudo.OnlyType(pseudo.From(&rawJSON{
|
||||
RawMessage: []byte(`"hello, world"`),
|
||||
})),
|
||||
wantRulesExtra: (*struct{})(nil),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
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) {
|
||||
assertPanics(
|
||||
t, func() {
|
||||
RegisterExtras(Extras[int, struct{}]{})
|
||||
},
|
||||
notStructMessage[int](),
|
||||
)
|
||||
|
||||
assertPanics(
|
||||
t, func() {
|
||||
RegisterExtras(Extras[struct{}, bool]{})
|
||||
},
|
||||
notStructMessage[bool](),
|
||||
)
|
||||
|
||||
assertPanics(
|
||||
t, func() {
|
||||
new(ChainConfig).ExtraPayload()
|
||||
},
|
||||
"before RegisterExtras",
|
||||
)
|
||||
|
||||
assertPanics(
|
||||
t, func() {
|
||||
new(Rules).ExtraPayload()
|
||||
},
|
||||
"before RegisterExtras",
|
||||
)
|
||||
|
||||
RegisterExtras(Extras[struct{}, struct{}]{})
|
||||
defer testOnlyClearRegisteredExtras()
|
||||
|
||||
assertPanics(
|
||||
t, func() {
|
||||
RegisterExtras(Extras[struct{}, struct{}]{})
|
||||
},
|
||||
"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()
|
||||
}
|
||||
Loading…
Reference in a new issue