forked from ethereum/go-ethereum
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
4 changed files
with
223 additions
and
43 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
package params | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
|
||
"github.com/ethereum/go-ethereum/libevm/pseudo" | ||
) | ||
|
||
var _ interface { | ||
json.Marshaler | ||
json.Unmarshaler | ||
} = (*ChainConfig)(nil) | ||
|
||
// chainConfigWithoutMethods avoids infinite recurion 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 != nil && !reg.reuseJSONRoot: | ||
return c.unmarshalJSONWithExtra(data) | ||
|
||
case reg != nil && reg.reuseJSONRoot: // although the latter is redundant, it's clearer | ||
c.extra = reg.chainConfig.NilPointer() | ||
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)) | ||
} | ||
} | ||
|
||
// 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.chainConfig.NilPointer(), | ||
} | ||
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) { | ||
switch reg := registeredExtras; { | ||
case reg == nil: | ||
return json.Marshal((*chainConfigWithoutMethods)(c)) | ||
|
||
case !reg.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) | ||
} | ||
} | ||
|
||
// marshalJSONWithExtra is the inverse of unmarshalJSONWithExtra(). | ||
func (c *ChainConfig) marshalJSONWithExtra() ([]byte, error) { | ||
cc := &chainConfigWithExportedExtra{ | ||
chainConfigWithoutMethods: (*chainConfigWithoutMethods)(c), | ||
Extra: c.extra, | ||
} | ||
return json.Marshal(cc) | ||
} | ||
|
||
func toJSONRawMessages(v any) (map[string]json.RawMessage, error) { | ||
buf, err := json.Marshal(v) | ||
if err != nil { | ||
return nil, err | ||
} | ||
msgs := make(map[string]json.RawMessage) | ||
if err := json.Unmarshal(buf, &msgs); err != nil { | ||
return nil, err | ||
} | ||
return msgs, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
package params | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"math/big" | ||
"testing" | ||
|
||
"github.com/ethereum/go-ethereum/libevm/pseudo" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
type nestedChainConfigExtra struct { | ||
NestedFoo string `json:"foo"` | ||
|
||
NOOPHooks | ||
} | ||
|
||
type rootJSONChainConfigExtra struct { | ||
TopLevelFoo string `json:"foo"` | ||
|
||
NOOPHooks | ||
} | ||
|
||
func TestChainConfigJSONRoundTrip(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
register func() | ||
jsonInput string | ||
want *ChainConfig | ||
}{ | ||
{ | ||
name: "no registered extras", | ||
register: func() {}, | ||
jsonInput: `{ | ||
"chainId": 1234 | ||
}`, | ||
want: &ChainConfig{ | ||
ChainID: big.NewInt(1234), | ||
}, | ||
}, | ||
{ | ||
name: "reuse top-level JSON", | ||
register: func() { | ||
RegisterExtras(Extras[rootJSONChainConfigExtra, NOOPHooks]{ | ||
ReuseJSONRoot: true, | ||
}) | ||
}, | ||
jsonInput: `{ | ||
"chainId": 5678, | ||
"foo": "hello" | ||
}`, | ||
want: &ChainConfig{ | ||
ChainID: big.NewInt(5678), | ||
extra: pseudo.From(&rootJSONChainConfigExtra{TopLevelFoo: "hello"}).Type, | ||
}, | ||
}, | ||
{ | ||
name: "nested JSON", | ||
register: func() { | ||
RegisterExtras(Extras[nestedChainConfigExtra, NOOPHooks]{ | ||
ReuseJSONRoot: false, // explicit zero value only for tests | ||
}) | ||
}, | ||
jsonInput: `{ | ||
"chainId": 42, | ||
"extra": {"foo": "world"} | ||
}`, | ||
want: &ChainConfig{ | ||
ChainID: big.NewInt(42), | ||
extra: pseudo.From(&nestedChainConfigExtra{NestedFoo: "world"}).Type, | ||
}, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
TestOnlyClearRegisteredExtras() | ||
t.Cleanup(TestOnlyClearRegisteredExtras) | ||
tt.register() | ||
|
||
t.Run("json.Unmarshal()", func(t *testing.T) { | ||
got := new(ChainConfig) | ||
require.NoError(t, json.Unmarshal([]byte(tt.jsonInput), got)) | ||
require.Equal(t, tt.want, got) | ||
}) | ||
|
||
t.Run("json.Marshal()", func(t *testing.T) { | ||
var want bytes.Buffer | ||
require.NoError(t, json.Compact(&want, []byte(tt.jsonInput)), "json.Compact()") | ||
|
||
got, err := json.Marshal(tt.want) | ||
require.NoError(t, err, "json.Marshal()") | ||
require.Equal(t, want.String(), string(got)) | ||
}) | ||
}) | ||
} | ||
} |