diff --git a/libevm/pseudo/constructor.go b/libevm/pseudo/constructor.go new file mode 100644 index 000000000000..91429240340a --- /dev/null +++ b/libevm/pseudo/constructor.go @@ -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 +} diff --git a/libevm/pseudo/constructor_test.go b/libevm/pseudo/constructor_test.go new file mode 100644 index 000000000000..a28f3dd42de6 --- /dev/null +++ b/libevm/pseudo/constructor_test.go @@ -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() +} diff --git a/libevm/pseudo/type.go b/libevm/pseudo/type.go new file mode 100644 index 000000000000..8c453f4cb0e7 --- /dev/null +++ b/libevm/pseudo/type.go @@ -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 +} diff --git a/libevm/pseudo/type_test.go b/libevm/pseudo/type_test.go new file mode 100644 index 000000000000..27ecf7e497ea --- /dev/null +++ b/libevm/pseudo/type_test.go @@ -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 +} diff --git a/params/config.go b/params/config.go index 21ede457fd68..2e5850c440dc 100644 --- a/params/config.go +++ b/params/config.go @@ -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 } diff --git a/params/config.libevm.go b/params/config.libevm.go new file mode 100644 index 000000000000..66e995f9a6a8 --- /dev/null +++ b/params/config.libevm.go @@ -0,0 +1,175 @@ +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 { + // 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. +func RegisterExtras[C any, R any](e Extras[C, R]) ExtraPayloadGetter[C, R] { + if registeredExtras != nil { + panic("re-registration of Extras") + } + mustBeStruct[C]() + mustBeStruct[R]() + registeredExtras = &extraConstructors{ + chainConfig: pseudo.NewConstructor[C](), + rules: pseudo.NewConstructor[R](), + newForRules: e.newForRules, + } + return e.getter() +} + +// 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 +} + +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 any, R any] 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() +} + +// FromRules returns the Rules' extra payload. +func (ExtraPayloadGetter[C, R]) FromRules(r *Rules) *R { + return pseudo.MustNewValue[*R](r.extraPayload()).Get() +} + +// 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 + } + + 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 +} diff --git a/params/config.libevm_test.go b/params/config.libevm_test.go new file mode 100644 index 000000000000..7f74e748a704 --- /dev/null +++ b/params/config.libevm_test.go @@ -0,0 +1,173 @@ +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" +) + +// 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 +} + +var _ interface { + json.Marshaler + json.Unmarshaler +} = (*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]{ + 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{}]{}) + }, + ccExtra: pseudo.From(&rawJSON{ + RawMessage: []byte(`"hello, world"`), + }).Type, + wantRulesExtra: (*struct{})(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() { + 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{}]{}) + + 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() +} diff --git a/params/example.libevm_test.go b/params/example.libevm_test.go new file mode 100644 index 000000000000..c6c99219fa7d --- /dev/null +++ b/params/example.libevm_test.go @@ -0,0 +1,108 @@ +// 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" + "fmt" + "log" + "math/big" + + "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() { + // 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, + } +} + +// 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 +} + +// 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) +} + +// 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("%+v\n", rExtra) + } + } + + // Output: + // Chain ID 1234 + // Fork time 530003640 + // &{IsMyFork:false} + // &{IsMyFork:true} + // &{IsMyFork:true} +}