mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-25 07:56:16 +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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/ava-labs/libevm/libevm/pseudo"
|
||||
)
|
||||
|
||||
var _ interface {
|
||||
|
|
@ -28,99 +26,117 @@ var _ interface {
|
|||
json.Unmarshaler
|
||||
} = (*ChainConfig)(nil)
|
||||
|
||||
// chainConfigWithoutMethods avoids infinite recurion into
|
||||
// chainConfigWithoutMethods avoids infinite recursion into
|
||||
// [ChainConfig.UnmarshalJSON].
|
||||
type chainConfigWithoutMethods ChainConfig
|
||||
|
||||
// chainConfigWithExportedExtra supports JSON (un)marshalling of a [ChainConfig]
|
||||
// while exposing the `extra` field as the "extra" JSON key.
|
||||
type chainConfigWithExportedExtra struct {
|
||||
*chainConfigWithoutMethods // embedded to achieve regular JSON unmarshalling
|
||||
Extra *pseudo.Type `json:"extra"` // `c.extra` is otherwise unexported
|
||||
}
|
||||
|
||||
// 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
|
||||
// UnmarshalJSON implements the [json.Unmarshaler] interface. If extra payloads
|
||||
// were registered, UnmarshalJSON decodes data as described by [Extras] and
|
||||
// [RegisterExtras] otherwise it unmarshals directly into c as if ChainConfig
|
||||
// didn't implement json.Unmarshaler.
|
||||
func (c *ChainConfig) UnmarshalJSON(data []byte) (err error) {
|
||||
if !registeredExtras.Registered() {
|
||||
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
|
||||
// registered [Extras] payload is in the JSON "extra" key. All other
|
||||
// unmarshalling is performed as if no [Extras] were registered.
|
||||
func (c *ChainConfig) unmarshalJSONWithExtra(data []byte) error {
|
||||
cc := &chainConfigWithExportedExtra{
|
||||
chainConfigWithoutMethods: (*chainConfigWithoutMethods)(c),
|
||||
Extra: registeredExtras.Get().newChainConfig(),
|
||||
// UnmarshalChainConfigJSON is equivalent to [ChainConfig.UnmarshalJSON]
|
||||
// had [Extras] with `C` been registered, but without the need to call
|
||||
// [RegisterExtras]. The `extra` argument MUST NOT be nil.
|
||||
func UnmarshalChainConfigJSON[C any](data []byte, config *ChainConfig, extra *C, reuseJSONRoot bool) (err error) {
|
||||
if extra == nil {
|
||||
return fmt.Errorf("%T argument is nil; use %T.UnmarshalJSON() directly", extra, config)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 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) {
|
||||
switch reg := registeredExtras; {
|
||||
case !reg.Registered():
|
||||
if !registeredExtras.Registered() {
|
||||
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().
|
||||
func (c *ChainConfig) marshalJSONWithExtra() ([]byte, error) {
|
||||
cc := &chainConfigWithExportedExtra{
|
||||
chainConfigWithoutMethods: (*chainConfigWithoutMethods)(c),
|
||||
Extra: c.extra,
|
||||
// MarshalChainConfigJSON is equivalent to [ChainConfig.MarshalJSON]
|
||||
// had [Extras] with `C` been registered, but without the need to
|
||||
// call [RegisterExtras].
|
||||
func MarshalChainConfigJSON[C any](config ChainConfig, extra C, reuseJSONRoot bool) (data []byte, err error) {
|
||||
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) {
|
||||
buf, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("encoding %T: %s", v, err)
|
||||
}
|
||||
msgs := make(map[string]json.RawMessage)
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import (
|
|||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"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