Skip to content

Commit 4575ced

Browse files
authored
feat: types.HeaderHooks JSON round-trip support (#94)
## Why this should be merged JSON equivalent of #89. ## How this works The check for registered extras, previously used in `{En,De}codeRLP()` methods is abstracted into a `Header.hooks() HeaderHooks` method that either returns (a) an instance of the registered type or (b) a `NOOPHeaderHooks` if no registration was performed. This is then used for all hooks, new (JSON) and old (RLP). ## How this was tested Extension of existing unit tests.
1 parent 44f23c8 commit 4575ced

File tree

4 files changed

+126
-41
lines changed

4 files changed

+126
-41
lines changed

core/types/block.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ func (n *BlockNonce) UnmarshalText(input []byte) error {
6060
}
6161

6262
//go:generate go run github.com/fjl/gencodec -type Header -field-override headerMarshaling -out gen_header_json.go
63+
//go:generate go run ../../libevm/cmd/internalise -file gen_header_json.go Header.MarshalJSON Header.UnmarshalJSON
6364
//go:generate go run ../../rlp/rlpgen -type Header -out gen_header_rlp.go
6465
//go:generate go run ../../libevm/cmd/internalise -file gen_header_rlp.go Header.EncodeRLP
6566

core/types/block.libevm.go

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package types
1818

1919
import (
20+
"encoding/json"
2021
"fmt"
2122
"io"
2223

@@ -27,40 +28,50 @@ import (
2728
// HeaderHooks are required for all types registered with [RegisterExtras] for
2829
// [Header] payloads.
2930
type HeaderHooks interface {
31+
MarshalJSON(*Header) ([]byte, error) //nolint:govet // Type-specific override hook
32+
UnmarshalJSON(*Header, []byte) error //nolint:govet
3033
EncodeRLP(*Header, io.Writer) error
3134
DecodeRLP(*Header, *rlp.Stream) error
3235
}
3336

37+
// hooks returns the Header's registered HeaderHooks, if any, otherwise a
38+
// [NOOPHeaderHooks] suitable for running default behaviour.
39+
func (h *Header) hooks() HeaderHooks {
40+
if r := registeredExtras; r.Registered() {
41+
return r.Get().hooks.hooksFromHeader(h)
42+
}
43+
return new(NOOPHeaderHooks)
44+
}
45+
46+
func (e ExtraPayloads[HPtr, SA]) hooksFromHeader(h *Header) HeaderHooks {
47+
return e.Header.Get(h)
48+
}
49+
3450
var _ interface {
3551
rlp.Encoder
3652
rlp.Decoder
53+
json.Marshaler
54+
json.Unmarshaler
3755
} = (*Header)(nil)
3856

39-
// EncodeRLP implements the [rlp.Encoder] interface.
40-
func (h *Header) EncodeRLP(w io.Writer) error {
41-
if r := registeredExtras; r.Registered() {
42-
return r.Get().hooks.hooksFromHeader(h).EncodeRLP(h, w)
43-
}
44-
return h.encodeRLP(w)
57+
// MarshalJSON implements the [json.Marshaler] interface.
58+
func (h *Header) MarshalJSON() ([]byte, error) {
59+
return h.hooks().MarshalJSON(h)
4560
}
4661

47-
// decodeHeaderRLPDirectly bypasses the [Header.DecodeRLP] method to avoid
48-
// infinite recursion.
49-
func decodeHeaderRLPDirectly(h *Header, s *rlp.Stream) error {
50-
type withoutMethods Header
51-
return s.Decode((*withoutMethods)(h))
62+
// UnmarshalJSON implements the [json.Unmarshaler] interface.
63+
func (h *Header) UnmarshalJSON(b []byte) error {
64+
return h.hooks().UnmarshalJSON(h, b)
5265
}
5366

54-
// DecodeRLP implements the [rlp.Decoder] interface.
55-
func (h *Header) DecodeRLP(s *rlp.Stream) error {
56-
if r := registeredExtras; r.Registered() {
57-
return r.Get().hooks.hooksFromHeader(h).DecodeRLP(h, s)
58-
}
59-
return decodeHeaderRLPDirectly(h, s)
67+
// EncodeRLP implements the [rlp.Encoder] interface.
68+
func (h *Header) EncodeRLP(w io.Writer) error {
69+
return h.hooks().EncodeRLP(h, w)
6070
}
6171

62-
func (e ExtraPayloads[HPtr, SA]) hooksFromHeader(h *Header) HeaderHooks {
63-
return e.Header.Get(h)
72+
// DecodeRLP implements the [rlp.Decoder] interface.
73+
func (h *Header) DecodeRLP(s *rlp.Stream) error {
74+
return h.hooks().DecodeRLP(h, s)
6475
}
6576

6677
func (h *Header) extraPayload() *pseudo.Type {
@@ -81,10 +92,19 @@ type NOOPHeaderHooks struct{}
8192

8293
var _ HeaderHooks = (*NOOPHeaderHooks)(nil)
8394

95+
func (*NOOPHeaderHooks) MarshalJSON(h *Header) ([]byte, error) { //nolint:govet
96+
return h.marshalJSON()
97+
}
98+
99+
func (*NOOPHeaderHooks) UnmarshalJSON(h *Header, b []byte) error { //nolint:govet
100+
return h.unmarshalJSON(b)
101+
}
102+
84103
func (*NOOPHeaderHooks) EncodeRLP(h *Header, w io.Writer) error {
85104
return h.encodeRLP(w)
86105
}
87106

88107
func (*NOOPHeaderHooks) DecodeRLP(h *Header, s *rlp.Stream) error {
89-
return decodeHeaderRLPDirectly(h, s)
108+
type withoutMethods Header
109+
return s.Decode((*withoutMethods)(h))
90110
}

core/types/block.libevm_test.go

Lines changed: 83 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
package types_test
1818

1919
import (
20+
"encoding/json"
2021
"errors"
22+
"fmt"
2123
"io"
2224
"testing"
2325

@@ -31,19 +33,33 @@ import (
3133
)
3234

3335
type stubHeaderHooks struct {
34-
rlpSuffix []byte
35-
gotRawRLPToDecode []byte
36-
setHeaderToOnDecode Header
36+
suffix []byte
37+
gotRawJSONToUnmarshal, gotRawRLPToDecode []byte
38+
setHeaderToOnUnmarshalOrDecode Header
3739

38-
errEncode, errDecode error
40+
errMarshal, errUnmarshal, errEncode, errDecode error
41+
}
42+
43+
func fakeHeaderJSON(h *Header, suffix []byte) []byte {
44+
return []byte(fmt.Sprintf(`"%#x:%#x"`, h.ParentHash, suffix))
3945
}
4046

4147
func fakeHeaderRLP(h *Header, suffix []byte) []byte {
4248
return append(crypto.Keccak256(h.ParentHash[:]), suffix...)
4349
}
4450

51+
func (hh *stubHeaderHooks) MarshalJSON(h *Header) ([]byte, error) { //nolint:govet
52+
return fakeHeaderJSON(h, hh.suffix), hh.errMarshal
53+
}
54+
55+
func (hh *stubHeaderHooks) UnmarshalJSON(h *Header, b []byte) error { //nolint:govet
56+
hh.gotRawJSONToUnmarshal = b
57+
*h = hh.setHeaderToOnUnmarshalOrDecode
58+
return hh.errUnmarshal
59+
}
60+
4561
func (hh *stubHeaderHooks) EncodeRLP(h *Header, w io.Writer) error {
46-
if _, err := w.Write(fakeHeaderRLP(h, hh.rlpSuffix)); err != nil {
62+
if _, err := w.Write(fakeHeaderRLP(h, hh.suffix)); err != nil {
4763
return err
4864
}
4965
return hh.errEncode
@@ -55,7 +71,7 @@ func (hh *stubHeaderHooks) DecodeRLP(h *Header, s *rlp.Stream) error {
5571
return err
5672
}
5773
hh.gotRawRLPToDecode = r
58-
*h = hh.setHeaderToOnDecode
74+
*h = hh.setHeaderToOnUnmarshalOrDecode
5975
return hh.errDecode
6076
}
6177

@@ -66,14 +82,36 @@ func TestHeaderHooks(t *testing.T) {
6682
extras := RegisterExtras[stubHeaderHooks, *stubHeaderHooks, struct{}]()
6783
rng := ethtest.NewPseudoRand(13579)
6884

69-
t.Run("EncodeRLP", func(t *testing.T) {
70-
suffix := rng.Bytes(8)
85+
suffix := rng.Bytes(8)
86+
hdr := &Header{
87+
ParentHash: rng.Hash(),
88+
}
89+
extras.Header.Get(hdr).suffix = append([]byte{}, suffix...)
90+
91+
t.Run("MarshalJSON", func(t *testing.T) {
92+
got, err := json.Marshal(hdr)
93+
require.NoError(t, err, "json.Marshal(%T)", hdr)
94+
assert.Equal(t, fakeHeaderJSON(hdr, suffix), got)
95+
})
7196

72-
hdr := &Header{
73-
ParentHash: rng.Hash(),
97+
t.Run("UnmarshalJSON", func(t *testing.T) {
98+
hdr := new(Header)
99+
stub := &stubHeaderHooks{
100+
setHeaderToOnUnmarshalOrDecode: Header{
101+
Extra: []byte("can you solve this puzzle? 0xbda01b6cf56c303bd3f581599c0d5c0b"),
102+
},
74103
}
75-
extras.Header.Get(hdr).rlpSuffix = append([]byte{}, suffix...)
104+
extras.Header.Set(hdr, stub)
105+
106+
input := fmt.Sprintf("%q", "hello, JSON world")
107+
err := json.Unmarshal([]byte(input), hdr)
108+
require.NoErrorf(t, err, "json.Unmarshal()")
76109

110+
assert.Equal(t, input, string(stub.gotRawJSONToUnmarshal), "raw JSON received by hook")
111+
assert.Equal(t, &stub.setHeaderToOnUnmarshalOrDecode, hdr, "%T after JSON unmarshalling with hook", hdr)
112+
})
113+
114+
t.Run("EncodeRLP", func(t *testing.T) {
77115
got, err := rlp.EncodeToBytes(hdr)
78116
require.NoError(t, err, "rlp.EncodeToBytes(%T)", hdr)
79117
assert.Equal(t, fakeHeaderRLP(hdr, suffix), got)
@@ -85,7 +123,7 @@ func TestHeaderHooks(t *testing.T) {
85123

86124
hdr := new(Header)
87125
stub := &stubHeaderHooks{
88-
setHeaderToOnDecode: Header{
126+
setHeaderToOnUnmarshalOrDecode: Header{
89127
Extra: []byte("arr4n was here"),
90128
},
91129
}
@@ -94,20 +132,46 @@ func TestHeaderHooks(t *testing.T) {
94132
require.NoErrorf(t, err, "rlp.DecodeBytes(%#x)", input)
95133

96134
assert.Equal(t, input, stub.gotRawRLPToDecode, "raw RLP received by hooks")
97-
assert.Equalf(t, &stub.setHeaderToOnDecode, hdr, "%T after RLP decoding with hook", hdr)
135+
assert.Equalf(t, &stub.setHeaderToOnUnmarshalOrDecode, hdr, "%T after RLP decoding with hook", hdr)
98136
})
99137

100138
t.Run("error_propagation", func(t *testing.T) {
139+
errMarshal := errors.New("whoops")
140+
errUnmarshal := errors.New("is it broken?")
101141
errEncode := errors.New("uh oh")
102142
errDecode := errors.New("something bad happened")
103143

104144
hdr := new(Header)
105-
extras.Header.Set(hdr, &stubHeaderHooks{
106-
errEncode: errEncode,
107-
errDecode: errDecode,
108-
})
145+
setStub := func() {
146+
extras.Header.Set(hdr, &stubHeaderHooks{
147+
errMarshal: errMarshal,
148+
errUnmarshal: errUnmarshal,
149+
errEncode: errEncode,
150+
errDecode: errDecode,
151+
})
152+
}
153+
154+
setStub()
155+
// The { } blocks are defensive, avoiding accidentally having the wrong
156+
// error checked in a future refactor. The verbosity is acceptable for
157+
// clarity in tests.
158+
{
159+
_, err := json.Marshal(hdr)
160+
assert.ErrorIs(t, err, errMarshal, "via json.Marshal()") //nolint:testifylint // require is inappropriate here as we wish to keep going
161+
}
162+
{
163+
err := json.Unmarshal([]byte("{}"), hdr)
164+
assert.Equal(t, errUnmarshal, err, "via json.Unmarshal()")
165+
}
109166

110-
assert.Equal(t, errEncode, rlp.Encode(io.Discard, hdr), "via rlp.Encode()")
111-
assert.Equal(t, errDecode, rlp.DecodeBytes([]byte{0}, hdr), "via rlp.DecodeBytes()")
167+
setStub() // [stubHeaderHooks] completely overrides the Header
168+
{
169+
err := rlp.Encode(io.Discard, hdr)
170+
assert.Equal(t, errEncode, err, "via rlp.Encode()")
171+
}
172+
{
173+
err := rlp.DecodeBytes([]byte{0}, hdr)
174+
assert.Equal(t, errDecode, err, "via rlp.DecodeBytes()")
175+
}
112176
})
113177
}

core/types/gen_header_json.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)