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.
This commit is contained in:
Arran Schlosberg 2024-12-19 16:30:04 +00:00 committed by GitHub
parent 44f23c8869
commit 4575ced555
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 126 additions and 41 deletions

View file

@ -60,6 +60,7 @@ func (n *BlockNonce) UnmarshalText(input []byte) error {
}
//go:generate go run github.com/fjl/gencodec -type Header -field-override headerMarshaling -out gen_header_json.go
//go:generate go run ../../libevm/cmd/internalise -file gen_header_json.go Header.MarshalJSON Header.UnmarshalJSON
//go:generate go run ../../rlp/rlpgen -type Header -out gen_header_rlp.go
//go:generate go run ../../libevm/cmd/internalise -file gen_header_rlp.go Header.EncodeRLP

View file

@ -17,6 +17,7 @@
package types
import (
"encoding/json"
"fmt"
"io"
@ -27,40 +28,50 @@ import (
// HeaderHooks are required for all types registered with [RegisterExtras] for
// [Header] payloads.
type HeaderHooks interface {
MarshalJSON(*Header) ([]byte, error) //nolint:govet // Type-specific override hook
UnmarshalJSON(*Header, []byte) error //nolint:govet
EncodeRLP(*Header, io.Writer) error
DecodeRLP(*Header, *rlp.Stream) error
}
// hooks returns the Header's registered HeaderHooks, if any, otherwise a
// [NOOPHeaderHooks] suitable for running default behaviour.
func (h *Header) hooks() HeaderHooks {
if r := registeredExtras; r.Registered() {
return r.Get().hooks.hooksFromHeader(h)
}
return new(NOOPHeaderHooks)
}
func (e ExtraPayloads[HPtr, SA]) hooksFromHeader(h *Header) HeaderHooks {
return e.Header.Get(h)
}
var _ interface {
rlp.Encoder
rlp.Decoder
json.Marshaler
json.Unmarshaler
} = (*Header)(nil)
// MarshalJSON implements the [json.Marshaler] interface.
func (h *Header) MarshalJSON() ([]byte, error) {
return h.hooks().MarshalJSON(h)
}
// UnmarshalJSON implements the [json.Unmarshaler] interface.
func (h *Header) UnmarshalJSON(b []byte) error {
return h.hooks().UnmarshalJSON(h, b)
}
// EncodeRLP implements the [rlp.Encoder] interface.
func (h *Header) EncodeRLP(w io.Writer) error {
if r := registeredExtras; r.Registered() {
return r.Get().hooks.hooksFromHeader(h).EncodeRLP(h, w)
}
return h.encodeRLP(w)
}
// decodeHeaderRLPDirectly bypasses the [Header.DecodeRLP] method to avoid
// infinite recursion.
func decodeHeaderRLPDirectly(h *Header, s *rlp.Stream) error {
type withoutMethods Header
return s.Decode((*withoutMethods)(h))
return h.hooks().EncodeRLP(h, w)
}
// DecodeRLP implements the [rlp.Decoder] interface.
func (h *Header) DecodeRLP(s *rlp.Stream) error {
if r := registeredExtras; r.Registered() {
return r.Get().hooks.hooksFromHeader(h).DecodeRLP(h, s)
}
return decodeHeaderRLPDirectly(h, s)
}
func (e ExtraPayloads[HPtr, SA]) hooksFromHeader(h *Header) HeaderHooks {
return e.Header.Get(h)
return h.hooks().DecodeRLP(h, s)
}
func (h *Header) extraPayload() *pseudo.Type {
@ -81,10 +92,19 @@ type NOOPHeaderHooks struct{}
var _ HeaderHooks = (*NOOPHeaderHooks)(nil)
func (*NOOPHeaderHooks) MarshalJSON(h *Header) ([]byte, error) { //nolint:govet
return h.marshalJSON()
}
func (*NOOPHeaderHooks) UnmarshalJSON(h *Header, b []byte) error { //nolint:govet
return h.unmarshalJSON(b)
}
func (*NOOPHeaderHooks) EncodeRLP(h *Header, w io.Writer) error {
return h.encodeRLP(w)
}
func (*NOOPHeaderHooks) DecodeRLP(h *Header, s *rlp.Stream) error {
return decodeHeaderRLPDirectly(h, s)
type withoutMethods Header
return s.Decode((*withoutMethods)(h))
}

View file

@ -17,7 +17,9 @@
package types_test
import (
"encoding/json"
"errors"
"fmt"
"io"
"testing"
@ -31,19 +33,33 @@ import (
)
type stubHeaderHooks struct {
rlpSuffix []byte
gotRawRLPToDecode []byte
setHeaderToOnDecode Header
suffix []byte
gotRawJSONToUnmarshal, gotRawRLPToDecode []byte
setHeaderToOnUnmarshalOrDecode Header
errEncode, errDecode error
errMarshal, errUnmarshal, errEncode, errDecode error
}
func fakeHeaderJSON(h *Header, suffix []byte) []byte {
return []byte(fmt.Sprintf(`"%#x:%#x"`, h.ParentHash, suffix))
}
func fakeHeaderRLP(h *Header, suffix []byte) []byte {
return append(crypto.Keccak256(h.ParentHash[:]), suffix...)
}
func (hh *stubHeaderHooks) MarshalJSON(h *Header) ([]byte, error) { //nolint:govet
return fakeHeaderJSON(h, hh.suffix), hh.errMarshal
}
func (hh *stubHeaderHooks) UnmarshalJSON(h *Header, b []byte) error { //nolint:govet
hh.gotRawJSONToUnmarshal = b
*h = hh.setHeaderToOnUnmarshalOrDecode
return hh.errUnmarshal
}
func (hh *stubHeaderHooks) EncodeRLP(h *Header, w io.Writer) error {
if _, err := w.Write(fakeHeaderRLP(h, hh.rlpSuffix)); err != nil {
if _, err := w.Write(fakeHeaderRLP(h, hh.suffix)); err != nil {
return err
}
return hh.errEncode
@ -55,7 +71,7 @@ func (hh *stubHeaderHooks) DecodeRLP(h *Header, s *rlp.Stream) error {
return err
}
hh.gotRawRLPToDecode = r
*h = hh.setHeaderToOnDecode
*h = hh.setHeaderToOnUnmarshalOrDecode
return hh.errDecode
}
@ -66,14 +82,36 @@ func TestHeaderHooks(t *testing.T) {
extras := RegisterExtras[stubHeaderHooks, *stubHeaderHooks, struct{}]()
rng := ethtest.NewPseudoRand(13579)
t.Run("EncodeRLP", func(t *testing.T) {
suffix := rng.Bytes(8)
suffix := rng.Bytes(8)
hdr := &Header{
ParentHash: rng.Hash(),
}
extras.Header.Get(hdr).suffix = append([]byte{}, suffix...)
hdr := &Header{
ParentHash: rng.Hash(),
t.Run("MarshalJSON", func(t *testing.T) {
got, err := json.Marshal(hdr)
require.NoError(t, err, "json.Marshal(%T)", hdr)
assert.Equal(t, fakeHeaderJSON(hdr, suffix), got)
})
t.Run("UnmarshalJSON", func(t *testing.T) {
hdr := new(Header)
stub := &stubHeaderHooks{
setHeaderToOnUnmarshalOrDecode: Header{
Extra: []byte("can you solve this puzzle? 0xbda01b6cf56c303bd3f581599c0d5c0b"),
},
}
extras.Header.Get(hdr).rlpSuffix = append([]byte{}, suffix...)
extras.Header.Set(hdr, stub)
input := fmt.Sprintf("%q", "hello, JSON world")
err := json.Unmarshal([]byte(input), hdr)
require.NoErrorf(t, err, "json.Unmarshal()")
assert.Equal(t, input, string(stub.gotRawJSONToUnmarshal), "raw JSON received by hook")
assert.Equal(t, &stub.setHeaderToOnUnmarshalOrDecode, hdr, "%T after JSON unmarshalling with hook", hdr)
})
t.Run("EncodeRLP", func(t *testing.T) {
got, err := rlp.EncodeToBytes(hdr)
require.NoError(t, err, "rlp.EncodeToBytes(%T)", hdr)
assert.Equal(t, fakeHeaderRLP(hdr, suffix), got)
@ -85,7 +123,7 @@ func TestHeaderHooks(t *testing.T) {
hdr := new(Header)
stub := &stubHeaderHooks{
setHeaderToOnDecode: Header{
setHeaderToOnUnmarshalOrDecode: Header{
Extra: []byte("arr4n was here"),
},
}
@ -94,20 +132,46 @@ func TestHeaderHooks(t *testing.T) {
require.NoErrorf(t, err, "rlp.DecodeBytes(%#x)", input)
assert.Equal(t, input, stub.gotRawRLPToDecode, "raw RLP received by hooks")
assert.Equalf(t, &stub.setHeaderToOnDecode, hdr, "%T after RLP decoding with hook", hdr)
assert.Equalf(t, &stub.setHeaderToOnUnmarshalOrDecode, hdr, "%T after RLP decoding with hook", hdr)
})
t.Run("error_propagation", func(t *testing.T) {
errMarshal := errors.New("whoops")
errUnmarshal := errors.New("is it broken?")
errEncode := errors.New("uh oh")
errDecode := errors.New("something bad happened")
hdr := new(Header)
extras.Header.Set(hdr, &stubHeaderHooks{
errEncode: errEncode,
errDecode: errDecode,
})
setStub := func() {
extras.Header.Set(hdr, &stubHeaderHooks{
errMarshal: errMarshal,
errUnmarshal: errUnmarshal,
errEncode: errEncode,
errDecode: errDecode,
})
}
assert.Equal(t, errEncode, rlp.Encode(io.Discard, hdr), "via rlp.Encode()")
assert.Equal(t, errDecode, rlp.DecodeBytes([]byte{0}, hdr), "via rlp.DecodeBytes()")
setStub()
// The { } blocks are defensive, avoiding accidentally having the wrong
// error checked in a future refactor. The verbosity is acceptable for
// clarity in tests.
{
_, err := json.Marshal(hdr)
assert.ErrorIs(t, err, errMarshal, "via json.Marshal()") //nolint:testifylint // require is inappropriate here as we wish to keep going
}
{
err := json.Unmarshal([]byte("{}"), hdr)
assert.Equal(t, errUnmarshal, err, "via json.Unmarshal()")
}
setStub() // [stubHeaderHooks] completely overrides the Header
{
err := rlp.Encode(io.Discard, hdr)
assert.Equal(t, errEncode, err, "via rlp.Encode()")
}
{
err := rlp.DecodeBytes([]byte{0}, hdr)
assert.Equal(t, errDecode, err, "via rlp.DecodeBytes()")
}
})
}

View file

@ -14,7 +14,7 @@ import (
var _ = (*headerMarshaling)(nil)
// MarshalJSON marshals as JSON.
func (h Header) MarshalJSON() ([]byte, error) {
func (h Header) marshalJSON() ([]byte, error) {
type Header struct {
ParentHash common.Hash `json:"parentHash" gencodec:"required"`
UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"`
@ -64,7 +64,7 @@ func (h Header) MarshalJSON() ([]byte, error) {
}
// UnmarshalJSON unmarshals from JSON.
func (h *Header) UnmarshalJSON(input []byte) error {
func (h *Header) unmarshalJSON(input []byte) error {
type Header struct {
ParentHash *common.Hash `json:"parentHash" gencodec:"required"`
UncleHash *common.Hash `json:"sha3Uncles" gencodec:"required"`