mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-18 02:40:45 +00:00
feat(params): UnmarshalChainJSONConfig and MarshalChainConfigJSON (#92)
- Allow wallets and other clients to parse blocks from both coreth and subnet-evm without registering extra types - Allow wallets and other clients to make their genesis/chain configurations without registering extra types
This commit is contained in:
parent
c996175b46
commit
d08d0f0510
2 changed files with 208 additions and 68 deletions
|
|
@ -19,8 +19,6 @@ package params
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/ava-labs/libevm/libevm/pseudo"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ interface {
|
var _ interface {
|
||||||
|
|
@ -28,99 +26,117 @@ var _ interface {
|
||||||
json.Unmarshaler
|
json.Unmarshaler
|
||||||
} = (*ChainConfig)(nil)
|
} = (*ChainConfig)(nil)
|
||||||
|
|
||||||
// chainConfigWithoutMethods avoids infinite recurion into
|
// chainConfigWithoutMethods avoids infinite recursion into
|
||||||
// [ChainConfig.UnmarshalJSON].
|
// [ChainConfig.UnmarshalJSON].
|
||||||
type chainConfigWithoutMethods ChainConfig
|
type chainConfigWithoutMethods ChainConfig
|
||||||
|
|
||||||
// chainConfigWithExportedExtra supports JSON (un)marshalling of a [ChainConfig]
|
// UnmarshalJSON implements the [json.Unmarshaler] interface. If extra payloads
|
||||||
// while exposing the `extra` field as the "extra" JSON key.
|
// were registered, UnmarshalJSON decodes data as described by [Extras] and
|
||||||
type chainConfigWithExportedExtra struct {
|
// [RegisterExtras] otherwise it unmarshals directly into c as if ChainConfig
|
||||||
*chainConfigWithoutMethods // embedded to achieve regular JSON unmarshalling
|
// didn't implement json.Unmarshaler.
|
||||||
Extra *pseudo.Type `json:"extra"` // `c.extra` is otherwise unexported
|
func (c *ChainConfig) UnmarshalJSON(data []byte) (err error) {
|
||||||
}
|
if !registeredExtras.Registered() {
|
||||||
|
|
||||||
// UnmarshalJSON implements the [json.Unmarshaler] interface.
|
|
||||||
func (c *ChainConfig) UnmarshalJSON(data []byte) error {
|
|
||||||
switch reg := registeredExtras; {
|
|
||||||
case reg.Registered() && !reg.Get().reuseJSONRoot:
|
|
||||||
return c.unmarshalJSONWithExtra(data)
|
|
||||||
|
|
||||||
case reg.Registered() && reg.Get().reuseJSONRoot: // although the latter is redundant, it's clearer
|
|
||||||
c.extra = reg.Get().newChainConfig()
|
|
||||||
if err := json.Unmarshal(data, c.extra); err != nil {
|
|
||||||
c.extra = nil
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fallthrough // Important! We've only unmarshalled the extra field.
|
|
||||||
default: // reg == nil
|
|
||||||
return json.Unmarshal(data, (*chainConfigWithoutMethods)(c))
|
return json.Unmarshal(data, (*chainConfigWithoutMethods)(c))
|
||||||
}
|
}
|
||||||
|
ec := registeredExtras.Get()
|
||||||
|
c.extra = ec.newChainConfig()
|
||||||
|
return UnmarshalChainConfigJSON(data, c, c.extra, ec.reuseJSONRoot)
|
||||||
}
|
}
|
||||||
|
|
||||||
// unmarshalJSONWithExtra unmarshals JSON under the assumption that the
|
// UnmarshalChainConfigJSON is equivalent to [ChainConfig.UnmarshalJSON]
|
||||||
// registered [Extras] payload is in the JSON "extra" key. All other
|
// had [Extras] with `C` been registered, but without the need to call
|
||||||
// unmarshalling is performed as if no [Extras] were registered.
|
// [RegisterExtras]. The `extra` argument MUST NOT be nil.
|
||||||
func (c *ChainConfig) unmarshalJSONWithExtra(data []byte) error {
|
func UnmarshalChainConfigJSON[C any](data []byte, config *ChainConfig, extra *C, reuseJSONRoot bool) (err error) {
|
||||||
cc := &chainConfigWithExportedExtra{
|
if extra == nil {
|
||||||
chainConfigWithoutMethods: (*chainConfigWithoutMethods)(c),
|
return fmt.Errorf("%T argument is nil; use %T.UnmarshalJSON() directly", extra, config)
|
||||||
Extra: registeredExtras.Get().newChainConfig(),
|
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(data, cc); err != nil {
|
|
||||||
return err
|
if reuseJSONRoot {
|
||||||
|
if err := json.Unmarshal(data, (*chainConfigWithoutMethods)(config)); err != nil {
|
||||||
|
return fmt.Errorf("decoding JSON into %T: %s", config, err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, extra); err != nil {
|
||||||
|
return fmt.Errorf("decoding JSON into %T: %s", extra, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
combined := struct {
|
||||||
|
*chainConfigWithoutMethods
|
||||||
|
Extra *C `json:"extra"`
|
||||||
|
}{
|
||||||
|
(*chainConfigWithoutMethods)(config),
|
||||||
|
extra,
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &combined); err != nil {
|
||||||
|
return fmt.Errorf(`decoding JSON into combination of %T and %T (as "extra" key): %s`, config, extra, err)
|
||||||
}
|
}
|
||||||
c.extra = cc.Extra
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON implements the [json.Marshaler] interface.
|
// MarshalJSON implements the [json.Marshaler] interface.
|
||||||
|
// If extra payloads were registered, MarshalJSON encodes JSON as
|
||||||
|
// described by [Extras] and [RegisterExtras] otherwise it marshals
|
||||||
|
// `c` as if ChainConfig didn't implement json.Marshaler.
|
||||||
func (c *ChainConfig) MarshalJSON() ([]byte, error) {
|
func (c *ChainConfig) MarshalJSON() ([]byte, error) {
|
||||||
switch reg := registeredExtras; {
|
if !registeredExtras.Registered() {
|
||||||
case !reg.Registered():
|
|
||||||
return json.Marshal((*chainConfigWithoutMethods)(c))
|
return json.Marshal((*chainConfigWithoutMethods)(c))
|
||||||
|
|
||||||
case !reg.Get().reuseJSONRoot:
|
|
||||||
return c.marshalJSONWithExtra()
|
|
||||||
|
|
||||||
default: // reg.reuseJSONRoot == true
|
|
||||||
// The inverse of reusing the JSON root is merging two JSON buffers,
|
|
||||||
// which isn't supported by the native package. So we use
|
|
||||||
// map[string]json.RawMessage intermediates.
|
|
||||||
geth, err := toJSONRawMessages((*chainConfigWithoutMethods)(c))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
extra, err := toJSONRawMessages(c.extra)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range extra {
|
|
||||||
if _, ok := geth[k]; ok {
|
|
||||||
return nil, fmt.Errorf("duplicate JSON key %q in both %T and registered extra", k, c)
|
|
||||||
}
|
|
||||||
geth[k] = v
|
|
||||||
}
|
|
||||||
return json.Marshal(geth)
|
|
||||||
}
|
}
|
||||||
|
ec := registeredExtras.Get()
|
||||||
|
return MarshalChainConfigJSON(*c, c.extra, ec.reuseJSONRoot)
|
||||||
}
|
}
|
||||||
|
|
||||||
// marshalJSONWithExtra is the inverse of unmarshalJSONWithExtra().
|
// MarshalChainConfigJSON is equivalent to [ChainConfig.MarshalJSON]
|
||||||
func (c *ChainConfig) marshalJSONWithExtra() ([]byte, error) {
|
// had [Extras] with `C` been registered, but without the need to
|
||||||
cc := &chainConfigWithExportedExtra{
|
// call [RegisterExtras].
|
||||||
chainConfigWithoutMethods: (*chainConfigWithoutMethods)(c),
|
func MarshalChainConfigJSON[C any](config ChainConfig, extra C, reuseJSONRoot bool) (data []byte, err error) {
|
||||||
Extra: c.extra,
|
if !reuseJSONRoot {
|
||||||
|
jsonExtra := struct {
|
||||||
|
ChainConfig
|
||||||
|
Extra C `json:"extra,omitempty"`
|
||||||
|
}{
|
||||||
|
config,
|
||||||
|
extra,
|
||||||
|
}
|
||||||
|
data, err = json.Marshal(jsonExtra)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(`encoding combination of %T and %T (as "extra" key) to JSON: %s`, config, extra, err)
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
}
|
}
|
||||||
return json.Marshal(cc)
|
|
||||||
|
// The inverse of reusing the JSON root is merging two JSON buffers,
|
||||||
|
// which isn't supported by the native package. So we use
|
||||||
|
// map[string]json.RawMessage intermediates.
|
||||||
|
// Note we cannot encode a combined struct directly because of the extra
|
||||||
|
// type generic nature which cannot be embedded in such a combined struct.
|
||||||
|
configJSONRaw, err := toJSONRawMessages((chainConfigWithoutMethods)(config))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("converting config to JSON raw messages: %s", err)
|
||||||
|
}
|
||||||
|
extraJSONRaw, err := toJSONRawMessages(extra)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("converting extra config to JSON raw messages: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range extraJSONRaw {
|
||||||
|
_, ok := configJSONRaw[k]
|
||||||
|
if ok {
|
||||||
|
return nil, fmt.Errorf("duplicate JSON key %q in ChainConfig and extra %T", k, extra)
|
||||||
|
}
|
||||||
|
configJSONRaw[k] = v
|
||||||
|
}
|
||||||
|
return json.Marshal(configJSONRaw)
|
||||||
}
|
}
|
||||||
|
|
||||||
func toJSONRawMessages(v any) (map[string]json.RawMessage, error) {
|
func toJSONRawMessages(v any) (map[string]json.RawMessage, error) {
|
||||||
buf, err := json.Marshal(v)
|
buf, err := json.Marshal(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("encoding %T: %s", v, err)
|
||||||
}
|
}
|
||||||
msgs := make(map[string]json.RawMessage)
|
msgs := make(map[string]json.RawMessage)
|
||||||
if err := json.Unmarshal(buf, &msgs); err != nil {
|
if err := json.Unmarshal(buf, &msgs); err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("decoding JSON encoding of %T into %T: %s", v, msgs, err)
|
||||||
}
|
}
|
||||||
return msgs, nil
|
return msgs, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"math/big"
|
"math/big"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/ava-labs/libevm/libevm/pseudo"
|
"github.com/ava-labs/libevm/libevm/pseudo"
|
||||||
|
|
@ -144,3 +145,126 @@ func TestChainConfigJSONRoundTrip(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalChainConfigJSON_Errors(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
type testExtra struct {
|
||||||
|
Field string `json:"field"`
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
jsonData string // string for convenience
|
||||||
|
extra *testExtra
|
||||||
|
reuseJSONRoot bool
|
||||||
|
wantConfig ChainConfig
|
||||||
|
wantExtra any
|
||||||
|
wantErrRegex string
|
||||||
|
}{
|
||||||
|
"invalid_json": {
|
||||||
|
extra: &testExtra{},
|
||||||
|
wantExtra: &testExtra{},
|
||||||
|
wantErrRegex: `^decoding JSON into combination of \*.+\.ChainConfig and \*.+\.testExtra \(as "extra" key\): .+$`,
|
||||||
|
},
|
||||||
|
"nil_extra_at_root_depth": {
|
||||||
|
jsonData: `{"chainId": 1}`,
|
||||||
|
extra: nil,
|
||||||
|
reuseJSONRoot: true,
|
||||||
|
wantExtra: (*testExtra)(nil),
|
||||||
|
wantErrRegex: `^\*.+.testExtra argument is nil; use \*.+\.ChainConfig\.UnmarshalJSON\(\) directly$`,
|
||||||
|
},
|
||||||
|
"nil_extra_at_extra_key": {
|
||||||
|
jsonData: `{"chainId": 1}`,
|
||||||
|
extra: nil,
|
||||||
|
wantExtra: (*testExtra)(nil),
|
||||||
|
wantErrRegex: `^\*.+\.testExtra argument is nil; use \*.+\.ChainConfig.UnmarshalJSON\(\) directly$`,
|
||||||
|
},
|
||||||
|
"wrong_extra_type_at_extra_key": {
|
||||||
|
jsonData: `{"chainId": 1, "extra": 1}`,
|
||||||
|
extra: &testExtra{},
|
||||||
|
wantConfig: ChainConfig{ChainID: big.NewInt(1)},
|
||||||
|
wantExtra: &testExtra{},
|
||||||
|
wantErrRegex: `^decoding JSON into combination of \*.+\.ChainConfig and \*.+\.testExtra \(as "extra" key\): .+$`,
|
||||||
|
},
|
||||||
|
"wrong_extra_type_at_root_depth": {
|
||||||
|
jsonData: `{"chainId": 1, "field": 1}`,
|
||||||
|
extra: &testExtra{},
|
||||||
|
reuseJSONRoot: true,
|
||||||
|
wantConfig: ChainConfig{ChainID: big.NewInt(1)},
|
||||||
|
wantExtra: &testExtra{},
|
||||||
|
wantErrRegex: `^decoding JSON into \*.+\.testExtra: .+`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
testCase := testCase
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
data := []byte(testCase.jsonData)
|
||||||
|
config := ChainConfig{}
|
||||||
|
err := UnmarshalChainConfigJSON(data, &config, testCase.extra, testCase.reuseJSONRoot)
|
||||||
|
if testCase.wantErrRegex == "" {
|
||||||
|
require.NoError(t, err)
|
||||||
|
} else {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Regexp(t, testCase.wantErrRegex, err.Error())
|
||||||
|
}
|
||||||
|
assert.Equal(t, testCase.wantConfig, config)
|
||||||
|
assert.Equal(t, testCase.wantExtra, testCase.extra)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalChainConfigJSON_Errors(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
config ChainConfig
|
||||||
|
extra any
|
||||||
|
reuseJSONRoot bool
|
||||||
|
wantJSONData string // string for convenience
|
||||||
|
wantErrRegex string
|
||||||
|
}{
|
||||||
|
"invalid_extra_at_extra_key": {
|
||||||
|
extra: struct {
|
||||||
|
Field chan struct{} `json:"field"`
|
||||||
|
}{},
|
||||||
|
wantErrRegex: `^encoding combination of .+\.ChainConfig and .+ to JSON: .+$`,
|
||||||
|
},
|
||||||
|
"nil_extra_at_extra_key": {
|
||||||
|
wantJSONData: `{"chainId":null}`,
|
||||||
|
},
|
||||||
|
"invalid_extra_at_root_depth": {
|
||||||
|
extra: struct {
|
||||||
|
Field chan struct{} `json:"field"`
|
||||||
|
}{},
|
||||||
|
reuseJSONRoot: true,
|
||||||
|
wantErrRegex: "^converting extra config to JSON raw messages: .+$",
|
||||||
|
},
|
||||||
|
"duplicate_key": {
|
||||||
|
extra: struct {
|
||||||
|
Field string `json:"chainId"`
|
||||||
|
}{},
|
||||||
|
reuseJSONRoot: true,
|
||||||
|
wantErrRegex: `^duplicate JSON key "chainId" in ChainConfig and extra struct .+$`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
testCase := testCase
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
config := ChainConfig{}
|
||||||
|
data, err := MarshalChainConfigJSON(config, testCase.extra, testCase.reuseJSONRoot)
|
||||||
|
if testCase.wantErrRegex == "" {
|
||||||
|
require.NoError(t, err)
|
||||||
|
} else {
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Regexp(t, testCase.wantErrRegex, err.Error())
|
||||||
|
}
|
||||||
|
assert.Equal(t, testCase.wantJSONData, string(data))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue