Skip to content

Commit d9991bb

Browse files
authored
feat: params.ChainConfig extra payload can use root JSON (#8)
* feat: `params.ChainConfig` extra payload can use root JSON * refactor: simplify `ChainConfig.UnmarshalJSON()` branches * fix: change redundant `assert` to `require` for simplicity
1 parent b6f3eb9 commit d9991bb

File tree

4 files changed

+223
-43
lines changed

4 files changed

+223
-43
lines changed

params/config.libevm.go

Lines changed: 15 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package params
22

33
import (
4-
"encoding/json"
54
"fmt"
65
"math/big"
76
"reflect"
@@ -14,6 +13,15 @@ import (
1413
// Extras are arbitrary payloads to be added as extra fields in [ChainConfig]
1514
// and [Rules] structs. See [RegisterExtras].
1615
type Extras[C ChainConfigHooks, R RulesHooks] struct {
16+
// ReuseJSONRoot, if true, signals that JSON unmarshalling of a
17+
// [ChainConfig] MUST reuse the root JSON input when unmarshalling the extra
18+
// payload. If false, it is assumed that the extra JSON payload is nested in
19+
// the "extra" key.
20+
//
21+
// *NOTE* this requires multiple passes for both marshalling and
22+
// unmarshalling of JSON so is inefficient and should be used as a last
23+
// resort.
24+
ReuseJSONRoot bool
1725
// NewRules, if non-nil is called at the end of [ChainConfig.Rules] with the
1826
// newly created [Rules] and other context from the method call. Its
1927
// returned value will be the extra payload of the [Rules]. If NewRules is
@@ -51,10 +59,11 @@ func RegisterExtras[C ChainConfigHooks, R RulesHooks](e Extras[C, R]) ExtraPaylo
5159

5260
getter := e.getter()
5361
registeredExtras = &extraConstructors{
54-
chainConfig: pseudo.NewConstructor[C](),
55-
rules: pseudo.NewConstructor[R](),
56-
newForRules: e.newForRules,
57-
getter: getter,
62+
chainConfig: pseudo.NewConstructor[C](),
63+
rules: pseudo.NewConstructor[R](),
64+
reuseJSONRoot: e.ReuseJSONRoot,
65+
newForRules: e.newForRules,
66+
getter: getter,
5867
}
5968
return getter
6069
}
@@ -87,6 +96,7 @@ var registeredExtras *extraConstructors
8796

8897
type extraConstructors struct {
8998
chainConfig, rules pseudo.Constructor
99+
reuseJSONRoot bool
90100
newForRules func(_ *ChainConfig, _ *Rules, blockNum *big.Int, isMerge bool, timestamp uint64) *pseudo.Type
91101
// use top-level hooksFrom<X>() functions instead of these as they handle
92102
// instances where no [Extras] were registered.
@@ -158,42 +168,6 @@ func (e ExtraPayloadGetter[C, R]) hooksFromRules(r *Rules) RulesHooks {
158168
return NOOPHooks{}
159169
}
160170

161-
// UnmarshalJSON implements the [json.Unmarshaler] interface.
162-
func (c *ChainConfig) UnmarshalJSON(data []byte) error {
163-
type raw ChainConfig // doesn't inherit methods so avoids recursing back here (infinitely)
164-
cc := &struct {
165-
*raw
166-
Extra *pseudo.Type `json:"extra"`
167-
}{
168-
raw: (*raw)(c), // embedded to achieve regular JSON unmarshalling
169-
}
170-
if e := registeredExtras; e != nil {
171-
cc.Extra = e.chainConfig.NilPointer() // `c.extra` is otherwise unexported
172-
}
173-
174-
if err := json.Unmarshal(data, cc); err != nil {
175-
return err
176-
}
177-
c.extra = cc.Extra
178-
return nil
179-
}
180-
181-
// MarshalJSON implements the [json.Marshaler] interface.
182-
func (c *ChainConfig) MarshalJSON() ([]byte, error) {
183-
// See UnmarshalJSON() for rationale.
184-
type raw ChainConfig
185-
cc := &struct {
186-
*raw
187-
Extra *pseudo.Type `json:"extra"`
188-
}{raw: (*raw)(c), Extra: c.extra}
189-
return json.Marshal(cc)
190-
}
191-
192-
var _ interface {
193-
json.Marshaler
194-
json.Unmarshaler
195-
} = (*ChainConfig)(nil)
196-
197171
// addRulesExtra is called at the end of [ChainConfig.Rules]; it exists to
198172
// abstract the libevm-specific behaviour outside of original geth code.
199173
func (c *ChainConfig) addRulesExtra(r *Rules, blockNum *big.Int, isMerge bool, timestamp uint64) {

params/config.libevm_test.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,6 @@ func TestRegisterExtras(t *testing.T) {
104104
require.NoError(t, json.Unmarshal(buf, got))
105105
assert.Equal(t, tt.ccExtra.Interface(), got.extraPayload().Interface())
106106
assert.Equal(t, in, got)
107-
// TODO: do we need an explicit test of the JSON output, or is a
108-
// Marshal-Unmarshal round trip sufficient?
109107

110108
gotRules := got.Rules(nil, false, 0)
111109
assert.Equal(t, tt.wantRulesExtra, gotRules.extraPayload().Interface())

params/json.libevm.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package params
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
"github.com/ethereum/go-ethereum/libevm/pseudo"
8+
)
9+
10+
var _ interface {
11+
json.Marshaler
12+
json.Unmarshaler
13+
} = (*ChainConfig)(nil)
14+
15+
// chainConfigWithoutMethods avoids infinite recurion into
16+
// [ChainConfig.UnmarshalJSON].
17+
type chainConfigWithoutMethods ChainConfig
18+
19+
// chainConfigWithExportedExtra supports JSON (un)marshalling of a [ChainConfig]
20+
// while exposing the `extra` field as the "extra" JSON key.
21+
type chainConfigWithExportedExtra struct {
22+
*chainConfigWithoutMethods // embedded to achieve regular JSON unmarshalling
23+
Extra *pseudo.Type `json:"extra"` // `c.extra` is otherwise unexported
24+
}
25+
26+
// UnmarshalJSON implements the [json.Unmarshaler] interface.
27+
func (c *ChainConfig) UnmarshalJSON(data []byte) error {
28+
switch reg := registeredExtras; {
29+
case reg != nil && !reg.reuseJSONRoot:
30+
return c.unmarshalJSONWithExtra(data)
31+
32+
case reg != nil && reg.reuseJSONRoot: // although the latter is redundant, it's clearer
33+
c.extra = reg.chainConfig.NilPointer()
34+
if err := json.Unmarshal(data, c.extra); err != nil {
35+
c.extra = nil
36+
return err
37+
}
38+
fallthrough // Important! We've only unmarshalled the extra field.
39+
default: // reg == nil
40+
return json.Unmarshal(data, (*chainConfigWithoutMethods)(c))
41+
}
42+
}
43+
44+
// unmarshalJSONWithExtra unmarshals JSON under the assumption that the
45+
// registered [Extras] payload is in the JSON "extra" key. All other
46+
// unmarshalling is performed as if no [Extras] were registered.
47+
func (c *ChainConfig) unmarshalJSONWithExtra(data []byte) error {
48+
cc := &chainConfigWithExportedExtra{
49+
chainConfigWithoutMethods: (*chainConfigWithoutMethods)(c),
50+
Extra: registeredExtras.chainConfig.NilPointer(),
51+
}
52+
if err := json.Unmarshal(data, cc); err != nil {
53+
return err
54+
}
55+
c.extra = cc.Extra
56+
return nil
57+
}
58+
59+
// MarshalJSON implements the [json.Marshaler] interface.
60+
func (c *ChainConfig) MarshalJSON() ([]byte, error) {
61+
switch reg := registeredExtras; {
62+
case reg == nil:
63+
return json.Marshal((*chainConfigWithoutMethods)(c))
64+
65+
case !reg.reuseJSONRoot:
66+
return c.marshalJSONWithExtra()
67+
68+
default: // reg.reuseJSONRoot == true
69+
// The inverse of reusing the JSON root is merging two JSON buffers,
70+
// which isn't supported by the native package. So we use
71+
// map[string]json.RawMessage intermediates.
72+
geth, err := toJSONRawMessages((*chainConfigWithoutMethods)(c))
73+
if err != nil {
74+
return nil, err
75+
}
76+
extra, err := toJSONRawMessages(c.extra)
77+
if err != nil {
78+
return nil, err
79+
}
80+
81+
for k, v := range extra {
82+
if _, ok := geth[k]; ok {
83+
return nil, fmt.Errorf("duplicate JSON key %q in both %T and registered extra", k, c)
84+
}
85+
geth[k] = v
86+
}
87+
return json.Marshal(geth)
88+
}
89+
}
90+
91+
// marshalJSONWithExtra is the inverse of unmarshalJSONWithExtra().
92+
func (c *ChainConfig) marshalJSONWithExtra() ([]byte, error) {
93+
cc := &chainConfigWithExportedExtra{
94+
chainConfigWithoutMethods: (*chainConfigWithoutMethods)(c),
95+
Extra: c.extra,
96+
}
97+
return json.Marshal(cc)
98+
}
99+
100+
func toJSONRawMessages(v any) (map[string]json.RawMessage, error) {
101+
buf, err := json.Marshal(v)
102+
if err != nil {
103+
return nil, err
104+
}
105+
msgs := make(map[string]json.RawMessage)
106+
if err := json.Unmarshal(buf, &msgs); err != nil {
107+
return nil, err
108+
}
109+
return msgs, nil
110+
}

params/json.libevm_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package params
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"math/big"
7+
"testing"
8+
9+
"github.com/ethereum/go-ethereum/libevm/pseudo"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
type nestedChainConfigExtra struct {
14+
NestedFoo string `json:"foo"`
15+
16+
NOOPHooks
17+
}
18+
19+
type rootJSONChainConfigExtra struct {
20+
TopLevelFoo string `json:"foo"`
21+
22+
NOOPHooks
23+
}
24+
25+
func TestChainConfigJSONRoundTrip(t *testing.T) {
26+
tests := []struct {
27+
name string
28+
register func()
29+
jsonInput string
30+
want *ChainConfig
31+
}{
32+
{
33+
name: "no registered extras",
34+
register: func() {},
35+
jsonInput: `{
36+
"chainId": 1234
37+
}`,
38+
want: &ChainConfig{
39+
ChainID: big.NewInt(1234),
40+
},
41+
},
42+
{
43+
name: "reuse top-level JSON",
44+
register: func() {
45+
RegisterExtras(Extras[rootJSONChainConfigExtra, NOOPHooks]{
46+
ReuseJSONRoot: true,
47+
})
48+
},
49+
jsonInput: `{
50+
"chainId": 5678,
51+
"foo": "hello"
52+
}`,
53+
want: &ChainConfig{
54+
ChainID: big.NewInt(5678),
55+
extra: pseudo.From(&rootJSONChainConfigExtra{TopLevelFoo: "hello"}).Type,
56+
},
57+
},
58+
{
59+
name: "nested JSON",
60+
register: func() {
61+
RegisterExtras(Extras[nestedChainConfigExtra, NOOPHooks]{
62+
ReuseJSONRoot: false, // explicit zero value only for tests
63+
})
64+
},
65+
jsonInput: `{
66+
"chainId": 42,
67+
"extra": {"foo": "world"}
68+
}`,
69+
want: &ChainConfig{
70+
ChainID: big.NewInt(42),
71+
extra: pseudo.From(&nestedChainConfigExtra{NestedFoo: "world"}).Type,
72+
},
73+
},
74+
}
75+
76+
for _, tt := range tests {
77+
t.Run(tt.name, func(t *testing.T) {
78+
TestOnlyClearRegisteredExtras()
79+
t.Cleanup(TestOnlyClearRegisteredExtras)
80+
tt.register()
81+
82+
t.Run("json.Unmarshal()", func(t *testing.T) {
83+
got := new(ChainConfig)
84+
require.NoError(t, json.Unmarshal([]byte(tt.jsonInput), got))
85+
require.Equal(t, tt.want, got)
86+
})
87+
88+
t.Run("json.Marshal()", func(t *testing.T) {
89+
var want bytes.Buffer
90+
require.NoError(t, json.Compact(&want, []byte(tt.jsonInput)), "json.Compact()")
91+
92+
got, err := json.Marshal(tt.want)
93+
require.NoError(t, err, "json.Marshal()")
94+
require.Equal(t, want.String(), string(got))
95+
})
96+
})
97+
}
98+
}

0 commit comments

Comments
 (0)