Skip to content

Commit d08d0f0

Browse files
authored
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
1 parent c996175 commit d08d0f0

File tree

2 files changed

+202
-62
lines changed

2 files changed

+202
-62
lines changed

params/json.libevm.go

Lines changed: 78 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -19,108 +19,124 @@ package params
1919
import (
2020
"encoding/json"
2121
"fmt"
22-
23-
"github.com/ava-labs/libevm/libevm/pseudo"
2422
)
2523

2624
var _ interface {
2725
json.Marshaler
2826
json.Unmarshaler
2927
} = (*ChainConfig)(nil)
3028

31-
// chainConfigWithoutMethods avoids infinite recurion into
29+
// chainConfigWithoutMethods avoids infinite recursion into
3230
// [ChainConfig.UnmarshalJSON].
3331
type chainConfigWithoutMethods ChainConfig
3432

35-
// chainConfigWithExportedExtra supports JSON (un)marshalling of a [ChainConfig]
36-
// while exposing the `extra` field as the "extra" JSON key.
37-
type chainConfigWithExportedExtra struct {
38-
*chainConfigWithoutMethods // embedded to achieve regular JSON unmarshalling
39-
Extra *pseudo.Type `json:"extra"` // `c.extra` is otherwise unexported
33+
// UnmarshalJSON implements the [json.Unmarshaler] interface. If extra payloads
34+
// were registered, UnmarshalJSON decodes data as described by [Extras] and
35+
// [RegisterExtras] otherwise it unmarshals directly into c as if ChainConfig
36+
// didn't implement json.Unmarshaler.
37+
func (c *ChainConfig) UnmarshalJSON(data []byte) (err error) {
38+
if !registeredExtras.Registered() {
39+
return json.Unmarshal(data, (*chainConfigWithoutMethods)(c))
40+
}
41+
ec := registeredExtras.Get()
42+
c.extra = ec.newChainConfig()
43+
return UnmarshalChainConfigJSON(data, c, c.extra, ec.reuseJSONRoot)
4044
}
4145

42-
// UnmarshalJSON implements the [json.Unmarshaler] interface.
43-
func (c *ChainConfig) UnmarshalJSON(data []byte) error {
44-
switch reg := registeredExtras; {
45-
case reg.Registered() && !reg.Get().reuseJSONRoot:
46-
return c.unmarshalJSONWithExtra(data)
46+
// UnmarshalChainConfigJSON is equivalent to [ChainConfig.UnmarshalJSON]
47+
// had [Extras] with `C` been registered, but without the need to call
48+
// [RegisterExtras]. The `extra` argument MUST NOT be nil.
49+
func UnmarshalChainConfigJSON[C any](data []byte, config *ChainConfig, extra *C, reuseJSONRoot bool) (err error) {
50+
if extra == nil {
51+
return fmt.Errorf("%T argument is nil; use %T.UnmarshalJSON() directly", extra, config)
52+
}
4753

48-
case reg.Registered() && reg.Get().reuseJSONRoot: // although the latter is redundant, it's clearer
49-
c.extra = reg.Get().newChainConfig()
50-
if err := json.Unmarshal(data, c.extra); err != nil {
51-
c.extra = nil
52-
return err
54+
if reuseJSONRoot {
55+
if err := json.Unmarshal(data, (*chainConfigWithoutMethods)(config)); err != nil {
56+
return fmt.Errorf("decoding JSON into %T: %s", config, err)
5357
}
54-
fallthrough // Important! We've only unmarshalled the extra field.
55-
default: // reg == nil
56-
return json.Unmarshal(data, (*chainConfigWithoutMethods)(c))
58+
if err := json.Unmarshal(data, extra); err != nil {
59+
return fmt.Errorf("decoding JSON into %T: %s", extra, err)
60+
}
61+
return nil
5762
}
58-
}
5963

60-
// unmarshalJSONWithExtra unmarshals JSON under the assumption that the
61-
// registered [Extras] payload is in the JSON "extra" key. All other
62-
// unmarshalling is performed as if no [Extras] were registered.
63-
func (c *ChainConfig) unmarshalJSONWithExtra(data []byte) error {
64-
cc := &chainConfigWithExportedExtra{
65-
chainConfigWithoutMethods: (*chainConfigWithoutMethods)(c),
66-
Extra: registeredExtras.Get().newChainConfig(),
64+
combined := struct {
65+
*chainConfigWithoutMethods
66+
Extra *C `json:"extra"`
67+
}{
68+
(*chainConfigWithoutMethods)(config),
69+
extra,
6770
}
68-
if err := json.Unmarshal(data, cc); err != nil {
69-
return err
71+
if err := json.Unmarshal(data, &combined); err != nil {
72+
return fmt.Errorf(`decoding JSON into combination of %T and %T (as "extra" key): %s`, config, extra, err)
7073
}
71-
c.extra = cc.Extra
7274
return nil
7375
}
7476

7577
// MarshalJSON implements the [json.Marshaler] interface.
78+
// If extra payloads were registered, MarshalJSON encodes JSON as
79+
// described by [Extras] and [RegisterExtras] otherwise it marshals
80+
// `c` as if ChainConfig didn't implement json.Marshaler.
7681
func (c *ChainConfig) MarshalJSON() ([]byte, error) {
77-
switch reg := registeredExtras; {
78-
case !reg.Registered():
82+
if !registeredExtras.Registered() {
7983
return json.Marshal((*chainConfigWithoutMethods)(c))
84+
}
85+
ec := registeredExtras.Get()
86+
return MarshalChainConfigJSON(*c, c.extra, ec.reuseJSONRoot)
87+
}
8088

81-
case !reg.Get().reuseJSONRoot:
82-
return c.marshalJSONWithExtra()
83-
84-
default: // reg.reuseJSONRoot == true
85-
// The inverse of reusing the JSON root is merging two JSON buffers,
86-
// which isn't supported by the native package. So we use
87-
// map[string]json.RawMessage intermediates.
88-
geth, err := toJSONRawMessages((*chainConfigWithoutMethods)(c))
89-
if err != nil {
90-
return nil, err
89+
// MarshalChainConfigJSON is equivalent to [ChainConfig.MarshalJSON]
90+
// had [Extras] with `C` been registered, but without the need to
91+
// call [RegisterExtras].
92+
func MarshalChainConfigJSON[C any](config ChainConfig, extra C, reuseJSONRoot bool) (data []byte, err error) {
93+
if !reuseJSONRoot {
94+
jsonExtra := struct {
95+
ChainConfig
96+
Extra C `json:"extra,omitempty"`
97+
}{
98+
config,
99+
extra,
91100
}
92-
extra, err := toJSONRawMessages(c.extra)
101+
data, err = json.Marshal(jsonExtra)
93102
if err != nil {
94-
return nil, err
103+
return nil, fmt.Errorf(`encoding combination of %T and %T (as "extra" key) to JSON: %s`, config, extra, err)
95104
}
105+
return data, nil
106+
}
96107

97-
for k, v := range extra {
98-
if _, ok := geth[k]; ok {
99-
return nil, fmt.Errorf("duplicate JSON key %q in both %T and registered extra", k, c)
100-
}
101-
geth[k] = v
102-
}
103-
return json.Marshal(geth)
108+
// The inverse of reusing the JSON root is merging two JSON buffers,
109+
// which isn't supported by the native package. So we use
110+
// map[string]json.RawMessage intermediates.
111+
// Note we cannot encode a combined struct directly because of the extra
112+
// type generic nature which cannot be embedded in such a combined struct.
113+
configJSONRaw, err := toJSONRawMessages((chainConfigWithoutMethods)(config))
114+
if err != nil {
115+
return nil, fmt.Errorf("converting config to JSON raw messages: %s", err)
116+
}
117+
extraJSONRaw, err := toJSONRawMessages(extra)
118+
if err != nil {
119+
return nil, fmt.Errorf("converting extra config to JSON raw messages: %s", err)
104120
}
105-
}
106121

107-
// marshalJSONWithExtra is the inverse of unmarshalJSONWithExtra().
108-
func (c *ChainConfig) marshalJSONWithExtra() ([]byte, error) {
109-
cc := &chainConfigWithExportedExtra{
110-
chainConfigWithoutMethods: (*chainConfigWithoutMethods)(c),
111-
Extra: c.extra,
122+
for k, v := range extraJSONRaw {
123+
_, ok := configJSONRaw[k]
124+
if ok {
125+
return nil, fmt.Errorf("duplicate JSON key %q in ChainConfig and extra %T", k, extra)
126+
}
127+
configJSONRaw[k] = v
112128
}
113-
return json.Marshal(cc)
129+
return json.Marshal(configJSONRaw)
114130
}
115131

116132
func toJSONRawMessages(v any) (map[string]json.RawMessage, error) {
117133
buf, err := json.Marshal(v)
118134
if err != nil {
119-
return nil, err
135+
return nil, fmt.Errorf("encoding %T: %s", v, err)
120136
}
121137
msgs := make(map[string]json.RawMessage)
122138
if err := json.Unmarshal(buf, &msgs); err != nil {
123-
return nil, err
139+
return nil, fmt.Errorf("decoding JSON encoding of %T into %T: %s", v, msgs, err)
124140
}
125141
return msgs, nil
126142
}

params/json.libevm_test.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"math/big"
2222
"testing"
2323

24+
"github.com/stretchr/testify/assert"
2425
"github.com/stretchr/testify/require"
2526

2627
"github.com/ava-labs/libevm/libevm/pseudo"
@@ -144,3 +145,126 @@ func TestChainConfigJSONRoundTrip(t *testing.T) {
144145
})
145146
}
146147
}
148+
149+
func TestUnmarshalChainConfigJSON_Errors(t *testing.T) {
150+
t.Parallel()
151+
152+
type testExtra struct {
153+
Field string `json:"field"`
154+
}
155+
156+
testCases := map[string]struct {
157+
jsonData string // string for convenience
158+
extra *testExtra
159+
reuseJSONRoot bool
160+
wantConfig ChainConfig
161+
wantExtra any
162+
wantErrRegex string
163+
}{
164+
"invalid_json": {
165+
extra: &testExtra{},
166+
wantExtra: &testExtra{},
167+
wantErrRegex: `^decoding JSON into combination of \*.+\.ChainConfig and \*.+\.testExtra \(as "extra" key\): .+$`,
168+
},
169+
"nil_extra_at_root_depth": {
170+
jsonData: `{"chainId": 1}`,
171+
extra: nil,
172+
reuseJSONRoot: true,
173+
wantExtra: (*testExtra)(nil),
174+
wantErrRegex: `^\*.+.testExtra argument is nil; use \*.+\.ChainConfig\.UnmarshalJSON\(\) directly$`,
175+
},
176+
"nil_extra_at_extra_key": {
177+
jsonData: `{"chainId": 1}`,
178+
extra: nil,
179+
wantExtra: (*testExtra)(nil),
180+
wantErrRegex: `^\*.+\.testExtra argument is nil; use \*.+\.ChainConfig.UnmarshalJSON\(\) directly$`,
181+
},
182+
"wrong_extra_type_at_extra_key": {
183+
jsonData: `{"chainId": 1, "extra": 1}`,
184+
extra: &testExtra{},
185+
wantConfig: ChainConfig{ChainID: big.NewInt(1)},
186+
wantExtra: &testExtra{},
187+
wantErrRegex: `^decoding JSON into combination of \*.+\.ChainConfig and \*.+\.testExtra \(as "extra" key\): .+$`,
188+
},
189+
"wrong_extra_type_at_root_depth": {
190+
jsonData: `{"chainId": 1, "field": 1}`,
191+
extra: &testExtra{},
192+
reuseJSONRoot: true,
193+
wantConfig: ChainConfig{ChainID: big.NewInt(1)},
194+
wantExtra: &testExtra{},
195+
wantErrRegex: `^decoding JSON into \*.+\.testExtra: .+`,
196+
},
197+
}
198+
199+
for name, testCase := range testCases {
200+
testCase := testCase
201+
t.Run(name, func(t *testing.T) {
202+
t.Parallel()
203+
204+
data := []byte(testCase.jsonData)
205+
config := ChainConfig{}
206+
err := UnmarshalChainConfigJSON(data, &config, testCase.extra, testCase.reuseJSONRoot)
207+
if testCase.wantErrRegex == "" {
208+
require.NoError(t, err)
209+
} else {
210+
require.Error(t, err)
211+
require.Regexp(t, testCase.wantErrRegex, err.Error())
212+
}
213+
assert.Equal(t, testCase.wantConfig, config)
214+
assert.Equal(t, testCase.wantExtra, testCase.extra)
215+
})
216+
}
217+
}
218+
219+
func TestMarshalChainConfigJSON_Errors(t *testing.T) {
220+
t.Parallel()
221+
222+
testCases := map[string]struct {
223+
config ChainConfig
224+
extra any
225+
reuseJSONRoot bool
226+
wantJSONData string // string for convenience
227+
wantErrRegex string
228+
}{
229+
"invalid_extra_at_extra_key": {
230+
extra: struct {
231+
Field chan struct{} `json:"field"`
232+
}{},
233+
wantErrRegex: `^encoding combination of .+\.ChainConfig and .+ to JSON: .+$`,
234+
},
235+
"nil_extra_at_extra_key": {
236+
wantJSONData: `{"chainId":null}`,
237+
},
238+
"invalid_extra_at_root_depth": {
239+
extra: struct {
240+
Field chan struct{} `json:"field"`
241+
}{},
242+
reuseJSONRoot: true,
243+
wantErrRegex: "^converting extra config to JSON raw messages: .+$",
244+
},
245+
"duplicate_key": {
246+
extra: struct {
247+
Field string `json:"chainId"`
248+
}{},
249+
reuseJSONRoot: true,
250+
wantErrRegex: `^duplicate JSON key "chainId" in ChainConfig and extra struct .+$`,
251+
},
252+
}
253+
254+
for name, testCase := range testCases {
255+
testCase := testCase
256+
t.Run(name, func(t *testing.T) {
257+
t.Parallel()
258+
259+
config := ChainConfig{}
260+
data, err := MarshalChainConfigJSON(config, testCase.extra, testCase.reuseJSONRoot)
261+
if testCase.wantErrRegex == "" {
262+
require.NoError(t, err)
263+
} else {
264+
require.Error(t, err)
265+
assert.Regexp(t, testCase.wantErrRegex, err.Error())
266+
}
267+
assert.Equal(t, testCase.wantJSONData, string(data))
268+
})
269+
}
270+
}

0 commit comments

Comments
 (0)