From 71037818a3cd45d0528132e2e4be3e4fca593a2e Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Wed, 5 Feb 2025 16:46:49 +0000 Subject: [PATCH 01/13] feat(rlp): `EncodeStructFields()` --- rlp/list.libevm.go | 63 +++++++++++++++++++++++++++++++++++++++++ rlp/list.libevm_test.go | 58 +++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) diff --git a/rlp/list.libevm.go b/rlp/list.libevm.go index 2fd74ce645d..48006e52d11 100644 --- a/rlp/list.libevm.go +++ b/rlp/list.libevm.go @@ -16,6 +16,13 @@ package rlp +import ( + "errors" + "fmt" + "io" + "reflect" +) + // InList is a convenience wrapper, calling `fn` between calls to // [EncoderBuffer.List] and [EncoderBuffer.ListEnd]. If `fn` returns an error, // it is propagated directly. @@ -42,6 +49,62 @@ func EncodeListToBuffer[T any](b EncoderBuffer, vals []T) error { }) } +// EncodeStructFields encodes the `required` and `optional` slices to `w`, +// concatenated as a single list, as if they were fields in a struct. The +// optional "fields" are treated identically to those tagged with +// `rlp:"optional"`. +func EncodeStructFields(w io.Writer, required, optional []any) error { + includeOptional, err := optionalFieldInclusionFlags(optional) + if err != nil { + return err + } + + b := NewEncoderBuffer(w) + err = b.InList(func() error { + for _, v := range required { + if err := Encode(b, v); err != nil { + return err + } + } + + for i, v := range optional { + if !includeOptional[i] { + return nil + } + if err := Encode(b, v); err != nil { + return err + } + } + return nil + }) + if err != nil { + return err + } + return b.Flush() +} + +var errUnsupportedOptionalFieldType = errors.New("unsupported optional field type") + +// optionalFieldInclusionFlags returns a slice of booleans, the same length as +// `vals`, indicating whether or not the respective optional value MUST be +// written to a list. A value must be written if it or any later value is +// non-nil; the returned slice is therefore monotonic non-increasing from true +// to false. +func optionalFieldInclusionFlags(vals []any) ([]bool, error) { + flags := make([]bool, len(vals)) + var include bool + for i := len(vals) - 1; i >= 0; i-- { + switch v := reflect.ValueOf(vals[i]); v.Kind() { + case reflect.Slice, reflect.Pointer: + include = include || !v.IsNil() + default: + return nil, fmt.Errorf("%w: %T", errUnsupportedOptionalFieldType, vals[i]) + } + flags[i] = include + } + return flags, nil +} + // FromList is a convenience wrapper, calling `fn` between calls to // [Stream.List] and [Stream.ListEnd]. If `fn` returns an error, it is // propagated directly. diff --git a/rlp/list.libevm_test.go b/rlp/list.libevm_test.go index 8e2c76711c9..0a8e8dc7483 100644 --- a/rlp/list.libevm_test.go +++ b/rlp/list.libevm_test.go @@ -20,8 +20,11 @@ import ( "bytes" "testing" + "github.com/kr/pretty" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/ava-labs/libevm/common" ) func TestEncodeListToBuffer(t *testing.T) { @@ -54,3 +57,58 @@ func TestDecodeList(t *testing.T) { assert.Equalf(t, vals[i], *gotPtr, "DecodeList()[%d]", i) } } + +func TestEncodeStructFields(t *testing.T) { + type goldStandard struct { + A uint64 + B uint64 + C *uint64 + D *uint64 `rlp:"optional"` + E []uint64 `rlp:"optional"` + F *[]uint64 `rlp:"optional"` + } + + const ( + a uint64 = iota + b + cVal + dVal + ) + c := common.PointerTo(cVal) + d := common.PointerTo(dVal) + e := []uint64{40, 41} + f := common.PointerTo([]uint64{50, 51}) + + tests := []goldStandard{ + {a, b, c, d, e, f}, // 000 (which of d/e/f are nil) + {a, b, c, d, e, nil}, // 001 + {a, b, c, d, nil, f}, // 010 + {a, b, c, d, nil, nil}, // 011 + {a, b, c, nil, e, f}, // 100 + {a, b, c, nil, e, nil}, // 101 + {a, b, c, nil, nil, f}, // 110 + {a, b, c, nil, nil, nil}, // 111 + // Empty and nil slices are treated differently when optional + {a, b, c, nil, []uint64{}, nil}, + } + + for _, obj := range tests { + obj := obj + t.Run("", func(t *testing.T) { + t.Logf("\n%s", pretty.Sprint(obj)) + + want, err := EncodeToBytes(obj) + require.NoErrorf(t, err, "EncodeToBytes([actual struct])") + + var got bytes.Buffer + err = EncodeStructFields( + &got, + []any{obj.A, obj.B, obj.C}, + []any{obj.D, obj.E, obj.F}, + ) + require.NoErrorf(t, err, "EncodeStructFields(..., [required], [optional])") + + assert.Equal(t, want, got.Bytes(), "EncodeToBytes() vs EncodeStructFields()") + }) + } +} From ddfca367e4576f163f0b089dbcd7b41cdf047a9f Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Wed, 5 Feb 2025 20:22:54 +0000 Subject: [PATCH 02/13] feat(rlp): `DecodeStructFields()` --- rlp/list.libevm.go | 29 ++++++++++++++ rlp/list.libevm_test.go | 87 +++++++++++++++++++++++++++++++++++------ 2 files changed, 103 insertions(+), 13 deletions(-) diff --git a/rlp/list.libevm.go b/rlp/list.libevm.go index 48006e52d11..9f6c43ebfc0 100644 --- a/rlp/list.libevm.go +++ b/rlp/list.libevm.go @@ -53,6 +53,8 @@ func EncodeListToBuffer[T any](b EncoderBuffer, vals []T) error { // concatenated as a single list, as if they were fields in a struct. The // optional "fields" are treated identically to those tagged with // `rlp:"optional"`. +// +// See the example for [DecodeStructFields]. func EncodeStructFields(w io.Writer, required, optional []any) error { includeOptional, err := optionalFieldInclusionFlags(optional) if err != nil { @@ -138,3 +140,30 @@ func DecodeList[T any](s *Stream) ([]*T, error) { }) return vals, err } + +// DecodeStructFields is the inverse of [EncodeStructFields]. All destination +// fields, be they `required` or `optional`, MUST be pointers and all `optional` +// fields MUST be provided in case they are present in the RLP being decoded. +// +// Typically, the arguments to this function mirror those passed to +// [EncodeStructFields] except for being pointers. See the example. +func DecodeStructFields(r io.Reader, required, optional []any) error { + s := NewStream(r, 0) + return s.FromList(func() error { + for _, v := range required { + if err := s.Decode(v); err != nil { + return err + } + } + + for _, v := range optional { + if !s.MoreDataInList() { + return nil + } + if err := s.Decode(v); err != nil { + return err + } + } + return nil + }) +} diff --git a/rlp/list.libevm_test.go b/rlp/list.libevm_test.go index 0a8e8dc7483..fcb8fd90aeb 100644 --- a/rlp/list.libevm_test.go +++ b/rlp/list.libevm_test.go @@ -18,6 +18,7 @@ package rlp import ( "bytes" + "io" "testing" "github.com/kr/pretty" @@ -58,8 +59,8 @@ func TestDecodeList(t *testing.T) { } } -func TestEncodeStructFields(t *testing.T) { - type goldStandard struct { +func TestStructFieldHelpers(t *testing.T) { + type foo struct { A uint64 B uint64 C *uint64 @@ -79,7 +80,7 @@ func TestEncodeStructFields(t *testing.T) { e := []uint64{40, 41} f := common.PointerTo([]uint64{50, 51}) - tests := []goldStandard{ + tests := []foo{ {a, b, c, d, e, f}, // 000 (which of d/e/f are nil) {a, b, c, d, e, nil}, // 001 {a, b, c, d, nil, f}, // 010 @@ -97,18 +98,78 @@ func TestEncodeStructFields(t *testing.T) { t.Run("", func(t *testing.T) { t.Logf("\n%s", pretty.Sprint(obj)) - want, err := EncodeToBytes(obj) + wantRLP, err := EncodeToBytes(obj) require.NoErrorf(t, err, "EncodeToBytes([actual struct])") - var got bytes.Buffer - err = EncodeStructFields( - &got, - []any{obj.A, obj.B, obj.C}, - []any{obj.D, obj.E, obj.F}, - ) - require.NoErrorf(t, err, "EncodeStructFields(..., [required], [optional])") - - assert.Equal(t, want, got.Bytes(), "EncodeToBytes() vs EncodeStructFields()") + t.Run("EncodeStructFields", func(t *testing.T) { + var got bytes.Buffer + err = EncodeStructFields( + &got, + []any{obj.A, obj.B, obj.C}, + []any{obj.D, obj.E, obj.F}, + ) + require.NoErrorf(t, err, "EncodeStructFields(..., [required], [optional])") + + assert.Equal(t, wantRLP, got.Bytes(), "EncodeToBytes() vs EncodeStructFields()") + }) + + t.Run("DecodeStructFields", func(t *testing.T) { + var got foo + err := DecodeStructFields( + bytes.NewReader(wantRLP), + []any{&got.A, &got.B, &got.C}, + []any{&got.D, &got.E, &got.F}, + ) + require.NoError(t, err, "DecodeStructFields(...)") + + var want foo + err = DecodeBytes(wantRLP, &want) + require.NoError(t, err, "DecodeBytes(...)") + + assert.Equal(t, want, got, "DecodeBytes(..., [original struct]) vs DecodeStructFields(...)") + }) }) } } + +//nolint:testableexamples // Demonstrating code equivalence, not outputs. +func ExampleDecodeStructFields() { + type inner struct { + X uint64 + } + + type outer struct { + A uint64 + B *inner `rlp:"optional"` + } + + val := outer{ + A: 42, + B: &inner{X: 99}, + } + + // Errors are dropped for brevity for the sake of the example only. + + _ = Encode(io.Discard, val) + // is equivalent to + _ = EncodeStructFields( + io.Discard, + []any{val.A}, + []any{val.B}, + ) + + r := bytes.NewReader(nil /*arbitrary RLP buffer*/) + var decoded outer + _ = Decode(r, &decoded) + // is equivalent to + _ = DecodeStructFields( + r, + []any{&val.A}, + []any{&val.B}, + ) + + // Note the parallels between the arguments passed to + // {En,De}codeStructFields() and that, when decoding an optional field, a + // pointer to the _field_ is required even though in this example it will be + // a `**inner`. +} From d4c671b879cfd649ce6117e80856705500bf12e2 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Wed, 5 Feb 2025 20:42:03 +0000 Subject: [PATCH 03/13] refactor(core/types): simplify `BodyHooks` RLP methods --- core/types/block.libevm.go | 86 +++++-------------- .../types/rlp_backwards_compat.libevm_test.go | 31 +------ rlp/list.libevm.go | 4 + 3 files changed, 31 insertions(+), 90 deletions(-) diff --git a/core/types/block.libevm.go b/core/types/block.libevm.go index 094613ee1e3..1450730bcf0 100644 --- a/core/types/block.libevm.go +++ b/core/types/block.libevm.go @@ -112,79 +112,28 @@ func (*NOOPHeaderHooks) DecodeRLP(h *Header, s *rlp.Stream) error { } func (*NOOPHeaderHooks) PostCopy(dst *Header) {} -var ( - _ interface { - rlp.Encoder - rlp.Decoder - } = (*Body)(nil) - - // The implementations of [Body.EncodeRLP] and [Body.DecodeRLP] make - // assumptions about the struct fields and their order, which we lock in here as a change - // detector. If this breaks then it MUST be updated and the RLP methods - // reviewed + new backwards-compatibility tests added. - _ = &Body{[]*Transaction{}, []*Header{}, []*Withdrawal{}} -) +var _ interface { + rlp.Encoder + rlp.Decoder +} = (*Body)(nil) // EncodeRLP implements the [rlp.Encoder] interface. func (b *Body) EncodeRLP(dst io.Writer) error { - w := rlp.NewEncoderBuffer(dst) - - return w.InList(func() error { - if err := rlp.EncodeListToBuffer(w, b.Transactions); err != nil { - return err - } - if err := rlp.EncodeListToBuffer(w, b.Uncles); err != nil { - return err - } - - hasLaterOptionalField := b.Withdrawals != nil - if err := b.hooks().AppendRLPFields(w, hasLaterOptionalField); err != nil { - return err - } - if !hasLaterOptionalField { - return nil - } - return rlp.EncodeListToBuffer(w, b.Withdrawals) - }) + req, opt := b.hooks().RLPFieldsForEncoding(b) + return rlp.EncodeStructFields(dst, req, opt) } // DecodeRLP implements the [rlp.Decoder] interface. func (b *Body) DecodeRLP(s *rlp.Stream) error { - return s.FromList(func() error { - txs, err := rlp.DecodeList[Transaction](s) - if err != nil { - return err - } - uncles, err := rlp.DecodeList[Header](s) - if err != nil { - return err - } - *b = Body{ - Transactions: txs, - Uncles: uncles, - } - - if err := b.hooks().DecodeExtraRLPFields(s); err != nil { - return err - } - if !s.MoreDataInList() { - return nil - } - - ws, err := rlp.DecodeList[Withdrawal](s) - if err != nil { - return err - } - b.Withdrawals = ws - return nil - }) + req, opt := b.hooks().RLPFieldPointersForDecoding(b) + return s.DecodeStructFields(req, opt) } // BodyHooks are required for all types registered with [RegisterExtras] for // [Body] payloads. type BodyHooks interface { - AppendRLPFields(_ rlp.EncoderBuffer, mustWriteEmptyOptional bool) error - DecodeExtraRLPFields(*rlp.Stream) error + RLPFieldsForEncoding(*Body) (required, optional []any) + RLPFieldPointersForDecoding(*Body) (required, optional []any) } // TestOnlyRegisterBodyHooks is a temporary means of "registering" BodyHooks for @@ -209,5 +158,16 @@ func (b *Body) hooks() BodyHooks { // having been registered. type NOOPBodyHooks struct{} -func (NOOPBodyHooks) AppendRLPFields(rlp.EncoderBuffer, bool) error { return nil } -func (NOOPBodyHooks) DecodeExtraRLPFields(*rlp.Stream) error { return nil } +// The RLP-related methods of [NOOPBodyHooks] make assumptions about the struct +// fields and their order, which we lock in here as a change detector. If this +// breaks then it MUST be updated and the RLP methods reviewed + new +// backwards-compatibility tests added. +var _ = &Body{[]*Transaction{}, []*Header{}, []*Withdrawal{}} + +func (NOOPBodyHooks) RLPFieldsForEncoding(b *Body) ([]any, []any) { + return []any{b.Transactions, b.Uncles}, []any{b.Withdrawals} +} + +func (NOOPBodyHooks) RLPFieldPointersForDecoding(b *Body) ([]any, []any) { + return []any{&b.Transactions, &b.Uncles}, []any{&b.Withdrawals} +} diff --git a/core/types/rlp_backwards_compat.libevm_test.go b/core/types/rlp_backwards_compat.libevm_test.go index 09cff5bd1e1..979ecd6210e 100644 --- a/core/types/rlp_backwards_compat.libevm_test.go +++ b/core/types/rlp_backwards_compat.libevm_test.go @@ -197,35 +197,12 @@ type cChainBodyExtras struct { var _ BodyHooks = (*cChainBodyExtras)(nil) -func (e *cChainBodyExtras) AppendRLPFields(b rlp.EncoderBuffer, _ bool) error { - b.WriteUint64(uint64(e.Version)) - - var data []byte - if e.ExtData != nil { - data = *e.ExtData - } - b.WriteBytes(data) - - return nil +func (e *cChainBodyExtras) RLPFieldsForEncoding(b *Body) ([]any, []any) { + return []any{b.Transactions, b.Uncles, e.Version, e.ExtData}, nil } -func (e *cChainBodyExtras) DecodeExtraRLPFields(s *rlp.Stream) error { - if err := s.Decode(&e.Version); err != nil { - return err - } - - buf, err := s.Bytes() - if err != nil { - return err - } - if len(buf) > 0 { - e.ExtData = &buf - } else { - // Respect the `rlp:"nil"` field tag. - e.ExtData = nil - } - - return nil +func (e *cChainBodyExtras) RLPFieldPointersForDecoding(b *Body) ([]any, []any) { + return []any{&b.Transactions, &b.Uncles, &e.Version, &e.ExtData}, nil } func TestBodyRLPCChainCompat(t *testing.T) { diff --git a/rlp/list.libevm.go b/rlp/list.libevm.go index 9f6c43ebfc0..eb616f13d75 100644 --- a/rlp/list.libevm.go +++ b/rlp/list.libevm.go @@ -149,6 +149,10 @@ func DecodeList[T any](s *Stream) ([]*T, error) { // [EncodeStructFields] except for being pointers. See the example. func DecodeStructFields(r io.Reader, required, optional []any) error { s := NewStream(r, 0) + return s.DecodeStructFields(required, optional) +} + +func (s *Stream) DecodeStructFields(required, optional []any) error { return s.FromList(func() error { for _, v := range required { if err := s.Decode(v); err != nil { From b3f6c5b7728d4859a767d70a57884cf1a47e26e6 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Thu, 6 Feb 2025 10:18:29 +0000 Subject: [PATCH 04/13] feat(rlp): `Nillable()` --- .../types/rlp_backwards_compat.libevm_test.go | 14 ++++- rlp/list.libevm.go | 23 +++++++ rlp/list.libevm_test.go | 61 +++++++++++++++++++ 3 files changed, 96 insertions(+), 2 deletions(-) diff --git a/core/types/rlp_backwards_compat.libevm_test.go b/core/types/rlp_backwards_compat.libevm_test.go index 979ecd6210e..396460dae0d 100644 --- a/core/types/rlp_backwards_compat.libevm_test.go +++ b/core/types/rlp_backwards_compat.libevm_test.go @@ -198,11 +198,21 @@ type cChainBodyExtras struct { var _ BodyHooks = (*cChainBodyExtras)(nil) func (e *cChainBodyExtras) RLPFieldsForEncoding(b *Body) ([]any, []any) { - return []any{b.Transactions, b.Uncles, e.Version, e.ExtData}, nil + return []any{ + b.Transactions, + b.Uncles, + e.Version, + e.ExtData, + }, nil } func (e *cChainBodyExtras) RLPFieldPointersForDecoding(b *Body) ([]any, []any) { - return []any{&b.Transactions, &b.Uncles, &e.Version, &e.ExtData}, nil + return []any{ + &b.Transactions, + &b.Uncles, + &e.Version, + rlp.Nillable(&e.ExtData), // equivalent to `rlp:"nil"` + }, nil } func TestBodyRLPCChainCompat(t *testing.T) { diff --git a/rlp/list.libevm.go b/rlp/list.libevm.go index eb616f13d75..7a34fdff214 100644 --- a/rlp/list.libevm.go +++ b/rlp/list.libevm.go @@ -171,3 +171,26 @@ func (s *Stream) DecodeStructFields(required, optional []any) error { return nil }) } + +// Nillable wraps `field` to mirror the behaviour of an `rlp:"nil"` tag; i.e. if +// an empty RLP string is decoded into the returned Decoder then it is dropped, +// otherwise it is decoded into `field` as normal. The return argument is +// intended for use with [Stream.DecodeStructFields]. +func Nillable[T any](field **T) Decoder { + return &nillable[T]{field} +} + +type nillable[T any] struct{ v **T } + +func (n *nillable[T]) DecodeRLP(s *Stream) error { + _, size, err := s.Kind() + if err != nil { + return err + } + if size > 0 { + return s.Decode(*n.v) + } + *n.v = nil + _, err = s.Raw() // consume the item + return err +} diff --git a/rlp/list.libevm_test.go b/rlp/list.libevm_test.go index fcb8fd90aeb..c927fd96256 100644 --- a/rlp/list.libevm_test.go +++ b/rlp/list.libevm_test.go @@ -173,3 +173,64 @@ func ExampleDecodeStructFields() { // pointer to the _field_ is required even though in this example it will be // a `**inner`. } + +func TestNillable(t *testing.T) { + type inner struct { + X uint64 + } + + type outer struct { + A *uint64 `rlp:"nil"` + B *inner `rlp:"nil"` + C *[]uint64 `rlp:"nil"` + } + + aMatrix := []*uint64{nil, common.PointerTo[uint64](0)} + bMatrix := []*inner{nil, {0}} + cMatrix := []*[]uint64{nil, {}, {0}} + + var tests []outer + for _, a := range aMatrix { + for _, b := range bMatrix { + for _, c := range cMatrix { + tests = append(tests, outer{a, b, c}) + } + } + } + + // When a Nillable encounters an empty list it MUST set the field to nil, + // not just ignore it. + corruptInitialValue := func() outer { + return outer{common.PointerTo[uint64](42), &inner{42}, &[]uint64{42}} + } + + for _, obj := range tests { + obj := obj + t.Run("", func(t *testing.T) { + rlp, err := EncodeToBytes(obj) + require.NoErrorf(t, err, "EncodeToBytes(%+v)", obj) + t.Logf("%s => %#x", pretty.Sprint(obj), rlp) + + // Although this is an immediate inversion of the line above, it + // provides us with the canonical RLP decoding, which our input + // struct may not honour. + want := corruptInitialValue() + err = DecodeBytes(rlp, &want) + require.NoErrorf(t, err, "DecodeBytes(%#x, %T)", rlp, &want) + + got := corruptInitialValue() + err = DecodeStructFields( + bytes.NewReader(rlp), + []any{ + Nillable(&got.A), + Nillable(&got.B), + Nillable(&got.C), + }, + nil, + ) + require.NoError(t, err, "DecodeStructFields(...)") + + assert.Equal(t, want, got, "DecodeBytes(...) vs DecodeStructFields(...)") + }) + } +} From c6f199d776a557f819a78f699f8cb1f56d0d392d Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Thu, 6 Feb 2025 10:36:07 +0000 Subject: [PATCH 05/13] refactor(rlp): remove top-level `DecodeStructFields()` --- rlp/list.libevm.go | 7 +------ rlp/list.libevm_test.go | 44 ++++++++++++++++++++++------------------- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/rlp/list.libevm.go b/rlp/list.libevm.go index 7a34fdff214..b22a68643ee 100644 --- a/rlp/list.libevm.go +++ b/rlp/list.libevm.go @@ -54,7 +54,7 @@ func EncodeListToBuffer[T any](b EncoderBuffer, vals []T) error { // optional "fields" are treated identically to those tagged with // `rlp:"optional"`. // -// See the example for [DecodeStructFields]. +// See the example for [Stream.DecodeStructFields]. func EncodeStructFields(w io.Writer, required, optional []any) error { includeOptional, err := optionalFieldInclusionFlags(optional) if err != nil { @@ -147,11 +147,6 @@ func DecodeList[T any](s *Stream) ([]*T, error) { // // Typically, the arguments to this function mirror those passed to // [EncodeStructFields] except for being pointers. See the example. -func DecodeStructFields(r io.Reader, required, optional []any) error { - s := NewStream(r, 0) - return s.DecodeStructFields(required, optional) -} - func (s *Stream) DecodeStructFields(required, optional []any) error { return s.FromList(func() error { for _, v := range required { diff --git a/rlp/list.libevm_test.go b/rlp/list.libevm_test.go index c927fd96256..3970eb319a1 100644 --- a/rlp/list.libevm_test.go +++ b/rlp/list.libevm_test.go @@ -114,38 +114,40 @@ func TestStructFieldHelpers(t *testing.T) { }) t.Run("DecodeStructFields", func(t *testing.T) { + s := NewStream(bytes.NewReader(wantRLP), 0) var got foo - err := DecodeStructFields( - bytes.NewReader(wantRLP), + err := s.DecodeStructFields( []any{&got.A, &got.B, &got.C}, []any{&got.D, &got.E, &got.F}, ) - require.NoError(t, err, "DecodeStructFields(...)") + require.NoError(t, err, "Stream.DecodeStructFields(...)") var want foo err = DecodeBytes(wantRLP, &want) require.NoError(t, err, "DecodeBytes(...)") - assert.Equal(t, want, got, "DecodeBytes(..., [original struct]) vs DecodeStructFields(...)") + assert.Equal(t, want, got, "DecodeBytes(..., [original struct]) vs Stream.DecodeStructFields(...)") }) }) } } //nolint:testableexamples // Demonstrating code equivalence, not outputs. -func ExampleDecodeStructFields() { +func ExampleStream_DecodeStructFields() { type inner struct { X uint64 } type outer struct { A uint64 - B *inner `rlp:"optional"` + B *inner `rlp:"nil"` + C *inner `rlp:"optional"` } val := outer{ A: 42, - B: &inner{X: 99}, + B: &inner{X: 42}, + C: &inner{X: 99}, } // Errors are dropped for brevity for the sake of the example only. @@ -154,24 +156,26 @@ func ExampleDecodeStructFields() { // is equivalent to _ = EncodeStructFields( io.Discard, - []any{val.A}, - []any{val.B}, + []any{val.A, val.B}, + []any{val.C}, ) r := bytes.NewReader(nil /*arbitrary RLP buffer*/) var decoded outer _ = Decode(r, &decoded) // is equivalent to - _ = DecodeStructFields( - r, - []any{&val.A}, - []any{&val.B}, + _ = NewStream(r, 0).DecodeStructFields( + []any{ + &val.A, + Nillable(&val.B), + }, + []any{&val.C}, ) // Note the parallels between the arguments passed to - // {En,De}codeStructFields() and that, when decoding an optional field, a - // pointer to the _field_ is required even though in this example it will be - // a `**inner`. + // {En,De}codeStructFields() and that, when decoding an optional or + // `rlp:"nil`-tagged field, a pointer to the _field_ is required even though + // in this example it will be a `**inner`. } func TestNillable(t *testing.T) { @@ -218,9 +222,9 @@ func TestNillable(t *testing.T) { err = DecodeBytes(rlp, &want) require.NoErrorf(t, err, "DecodeBytes(%#x, %T)", rlp, &want) + s := NewStream(bytes.NewReader(rlp), 0) got := corruptInitialValue() - err = DecodeStructFields( - bytes.NewReader(rlp), + err = s.DecodeStructFields( []any{ Nillable(&got.A), Nillable(&got.B), @@ -228,9 +232,9 @@ func TestNillable(t *testing.T) { }, nil, ) - require.NoError(t, err, "DecodeStructFields(...)") + require.NoError(t, err, "Stream.DecodeStructFields(...)") - assert.Equal(t, want, got, "DecodeBytes(...) vs DecodeStructFields(...)") + assert.Equal(t, want, got, "DecodeBytes(...) vs Stream.DecodeStructFields([fields wrapped in Nillable()])") }) } } From 6e350cf7c816d86dda0c1f4b5c71860ece405387 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Thu, 6 Feb 2025 10:44:41 +0000 Subject: [PATCH 06/13] doc: demonstrate use of all non-optional upstream RLP fields --- core/types/rlp_backwards_compat.libevm_test.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/core/types/rlp_backwards_compat.libevm_test.go b/core/types/rlp_backwards_compat.libevm_test.go index 396460dae0d..e9ec1a3cda9 100644 --- a/core/types/rlp_backwards_compat.libevm_test.go +++ b/core/types/rlp_backwards_compat.libevm_test.go @@ -198,15 +198,20 @@ type cChainBodyExtras struct { var _ BodyHooks = (*cChainBodyExtras)(nil) func (e *cChainBodyExtras) RLPFieldsForEncoding(b *Body) ([]any, []any) { - return []any{ - b.Transactions, - b.Uncles, - e.Version, - e.ExtData, - }, nil + // The Avalanche C-Chain uses all of the geth required fields (but none of + // the optional ones) so there's no need to explicitly list them. This + // pattern might not be ideal for readability but is used here for + // demonstrative purposes. + // + // All new fields will always be tagged as optional for backwards + // compatibility so this is safe to do, but only for the required fields. + req, _ /*drop all optional*/ := NOOPBodyHooks{}.RLPFieldsForEncoding(b) + return append(req, e.Version, e.ExtData), nil } func (e *cChainBodyExtras) RLPFieldPointersForDecoding(b *Body) ([]any, []any) { + // An alternative to the pattern used above is to explicitly list all + // fields for better introspection. return []any{ &b.Transactions, &b.Uncles, From c44b1d9a3b2de28b6c833fd0d27fa11636c75331 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Thu, 6 Feb 2025 11:01:19 +0000 Subject: [PATCH 07/13] fix(rlp): `Nillable()` decoding into field pointer --- core/types/rlp_backwards_compat.libevm_test.go | 2 ++ rlp/list.libevm.go | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/core/types/rlp_backwards_compat.libevm_test.go b/core/types/rlp_backwards_compat.libevm_test.go index e9ec1a3cda9..c8db2532c3b 100644 --- a/core/types/rlp_backwards_compat.libevm_test.go +++ b/core/types/rlp_backwards_compat.libevm_test.go @@ -248,12 +248,14 @@ func TestBodyRLPCChainCompat(t *testing.T) { wantRLPHex string }{ { + name: "nil ExtData", extra: &cChainBodyExtras{ Version: version, }, wantRLPHex: `e5dedd2a80809400000000000000000000000000decafc0ffeebad8080808080c08304cb2f80`, }, { + name: "non-nil ExtData", extra: &cChainBodyExtras{ Version: version, ExtData: &[]byte{1, 4, 2, 8, 5, 7}, diff --git a/rlp/list.libevm.go b/rlp/list.libevm.go index b22a68643ee..b54e3d626fe 100644 --- a/rlp/list.libevm.go +++ b/rlp/list.libevm.go @@ -168,9 +168,10 @@ func (s *Stream) DecodeStructFields(required, optional []any) error { } // Nillable wraps `field` to mirror the behaviour of an `rlp:"nil"` tag; i.e. if -// an empty RLP string is decoded into the returned Decoder then it is dropped, -// otherwise it is decoded into `field` as normal. The return argument is -// intended for use with [Stream.DecodeStructFields]. +// a zero-sized RLP item is decoded into the returned Decoder then it is dropped +// and `*field` is set to nil, otherwise the RLP item is decoded directly into +// `field`. The return argument is intended for use with +// [Stream.DecodeStructFields]. func Nillable[T any](field **T) Decoder { return &nillable[T]{field} } @@ -183,7 +184,7 @@ func (n *nillable[T]) DecodeRLP(s *Stream) error { return err } if size > 0 { - return s.Decode(*n.v) + return s.Decode(n.v) } *n.v = nil _, err = s.Raw() // consume the item From 64d39caced88c714ff816da2f2ff028521bea076 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Thu, 6 Feb 2025 12:42:44 +0000 Subject: [PATCH 08/13] test(rlp): tweak test cases for new functionality --- .../types/rlp_backwards_compat.libevm_test.go | 4 +-- rlp/list.libevm_test.go | 34 +++++++++++++------ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/core/types/rlp_backwards_compat.libevm_test.go b/core/types/rlp_backwards_compat.libevm_test.go index c8db2532c3b..d503c7fbdcb 100644 --- a/core/types/rlp_backwards_compat.libevm_test.go +++ b/core/types/rlp_backwards_compat.libevm_test.go @@ -116,8 +116,8 @@ func TestBodyRLPBackwardsCompatibility(t *testing.T) { newHdr := func(hashLow byte) *Header { return &Header{ParentHash: common.Hash{hashLow}} } newWithdraw := func(idx uint64) *Withdrawal { return &Withdrawal{Index: idx} } - // We build up test-case [Body] instances from the power set of each of - // these components. + // We build up test-case [Body] instances from the Cartesian product of each + // of these components. txMatrix := [][]*Transaction{ nil, {}, // Must be equivalent for non-optional field {newTx(1)}, diff --git a/rlp/list.libevm_test.go b/rlp/list.libevm_test.go index 3970eb319a1..3dc1e8488d7 100644 --- a/rlp/list.libevm_test.go +++ b/rlp/list.libevm_test.go @@ -78,7 +78,7 @@ func TestStructFieldHelpers(t *testing.T) { c := common.PointerTo(cVal) d := common.PointerTo(dVal) e := []uint64{40, 41} - f := common.PointerTo([]uint64{50, 51}) + f := &[]uint64{50, 51} tests := []foo{ {a, b, c, d, e, f}, // 000 (which of d/e/f are nil) @@ -91,6 +91,7 @@ func TestStructFieldHelpers(t *testing.T) { {a, b, c, nil, nil, nil}, // 111 // Empty and nil slices are treated differently when optional {a, b, c, nil, []uint64{}, nil}, + {a, b, c, nil, nil, &[]uint64{}}, } for _, obj := range tests { @@ -189,17 +190,28 @@ func TestNillable(t *testing.T) { C *[]uint64 `rlp:"nil"` } - aMatrix := []*uint64{nil, common.PointerTo[uint64](0)} - bMatrix := []*inner{nil, {0}} - cMatrix := []*[]uint64{nil, {}, {0}} - + // Unlike the `rlp:"optional"` tag, there is no interplay between nil-tagged + // fields so we don't need the Cartesian product of all possible + // combinations. var tests []outer - for _, a := range aMatrix { - for _, b := range bMatrix { - for _, c := range cMatrix { - tests = append(tests, outer{a, b, c}) - } - } + for _, a := range []*uint64{ + nil, + common.PointerTo[uint64](0), + } { + tests = append(tests, outer{a, nil, nil}) + } + for _, b := range []*inner{ + nil, + {0}, + } { + tests = append(tests, outer{nil, b, nil}) + } + for _, c := range []*[]uint64{ + nil, + {}, + {0}, + } { + tests = append(tests, outer{nil, nil, c}) } // When a Nillable encounters an empty list it MUST set the field to nil, From 13e3e76600aa2fa44ebb0151ffd5533a2581d3f1 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Fri, 7 Feb 2025 10:37:24 +0000 Subject: [PATCH 09/13] refactor: introduce `rlp.OptionalFields` instead of `[]any` --- core/types/block.libevm.go | 21 +++--- .../types/rlp_backwards_compat.libevm_test.go | 8 +-- rlp/list.libevm.go | 70 +++++++++++++------ rlp/list.libevm_test.go | 8 +-- 4 files changed, 69 insertions(+), 38 deletions(-) diff --git a/core/types/block.libevm.go b/core/types/block.libevm.go index 1450730bcf0..9a0bf5c060a 100644 --- a/core/types/block.libevm.go +++ b/core/types/block.libevm.go @@ -119,21 +119,22 @@ var _ interface { // EncodeRLP implements the [rlp.Encoder] interface. func (b *Body) EncodeRLP(dst io.Writer) error { - req, opt := b.hooks().RLPFieldsForEncoding(b) - return rlp.EncodeStructFields(dst, req, opt) + required, optional := b.hooks().RLPFieldsForEncoding(b) + return rlp.EncodeStructFields(dst, required, optional) } // DecodeRLP implements the [rlp.Decoder] interface. func (b *Body) DecodeRLP(s *rlp.Stream) error { - req, opt := b.hooks().RLPFieldPointersForDecoding(b) - return s.DecodeStructFields(req, opt) + return s.DecodeStructFields( + b.hooks().RLPFieldPointersForDecoding(b), + ) } // BodyHooks are required for all types registered with [RegisterExtras] for // [Body] payloads. type BodyHooks interface { - RLPFieldsForEncoding(*Body) (required, optional []any) - RLPFieldPointersForDecoding(*Body) (required, optional []any) + RLPFieldsForEncoding(*Body) ([]any, *rlp.OptionalFields) + RLPFieldPointersForDecoding(*Body) ([]any, *rlp.OptionalFields) } // TestOnlyRegisterBodyHooks is a temporary means of "registering" BodyHooks for @@ -164,10 +165,10 @@ type NOOPBodyHooks struct{} // backwards-compatibility tests added. var _ = &Body{[]*Transaction{}, []*Header{}, []*Withdrawal{}} -func (NOOPBodyHooks) RLPFieldsForEncoding(b *Body) ([]any, []any) { - return []any{b.Transactions, b.Uncles}, []any{b.Withdrawals} +func (NOOPBodyHooks) RLPFieldsForEncoding(b *Body) ([]any, *rlp.OptionalFields) { + return []any{b.Transactions, b.Uncles}, rlp.Optional(b.Withdrawals) } -func (NOOPBodyHooks) RLPFieldPointersForDecoding(b *Body) ([]any, []any) { - return []any{&b.Transactions, &b.Uncles}, []any{&b.Withdrawals} +func (NOOPBodyHooks) RLPFieldPointersForDecoding(b *Body) ([]any, *rlp.OptionalFields) { + return []any{&b.Transactions, &b.Uncles}, rlp.Optional(&b.Withdrawals) } diff --git a/core/types/rlp_backwards_compat.libevm_test.go b/core/types/rlp_backwards_compat.libevm_test.go index d503c7fbdcb..62ee37de49d 100644 --- a/core/types/rlp_backwards_compat.libevm_test.go +++ b/core/types/rlp_backwards_compat.libevm_test.go @@ -197,7 +197,7 @@ type cChainBodyExtras struct { var _ BodyHooks = (*cChainBodyExtras)(nil) -func (e *cChainBodyExtras) RLPFieldsForEncoding(b *Body) ([]any, []any) { +func (e *cChainBodyExtras) RLPFieldsForEncoding(b *Body) ([]any, *rlp.OptionalFields) { // The Avalanche C-Chain uses all of the geth required fields (but none of // the optional ones) so there's no need to explicitly list them. This // pattern might not be ideal for readability but is used here for @@ -205,11 +205,11 @@ func (e *cChainBodyExtras) RLPFieldsForEncoding(b *Body) ([]any, []any) { // // All new fields will always be tagged as optional for backwards // compatibility so this is safe to do, but only for the required fields. - req, _ /*drop all optional*/ := NOOPBodyHooks{}.RLPFieldsForEncoding(b) - return append(req, e.Version, e.ExtData), nil + required, _ /*drop all optional*/ := NOOPBodyHooks{}.RLPFieldsForEncoding(b) + return append(required, e.Version, e.ExtData), nil } -func (e *cChainBodyExtras) RLPFieldPointersForDecoding(b *Body) ([]any, []any) { +func (e *cChainBodyExtras) RLPFieldPointersForDecoding(b *Body) ([]any, *rlp.OptionalFields) { // An alternative to the pattern used above is to explicitly list all // fields for better introspection. return []any{ diff --git a/rlp/list.libevm.go b/rlp/list.libevm.go index b54e3d626fe..864dca428b5 100644 --- a/rlp/list.libevm.go +++ b/rlp/list.libevm.go @@ -49,14 +49,14 @@ func EncodeListToBuffer[T any](b EncoderBuffer, vals []T) error { }) } -// EncodeStructFields encodes the `required` and `optional` slices to `w`, +// EncodeStructFields encodes the required and optional slices to `w`, // concatenated as a single list, as if they were fields in a struct. The -// optional "fields" are treated identically to those tagged with -// `rlp:"optional"`. +// optional "fields", which MAY be nil, are treated identically to those tagged +// with `rlp:"optional"`. // // See the example for [Stream.DecodeStructFields]. -func EncodeStructFields(w io.Writer, required, optional []any) error { - includeOptional, err := optionalFieldInclusionFlags(optional) +func EncodeStructFields(w io.Writer, required []any, opt *OptionalFields) error { + includeOptional, err := opt.inclusionFlags() if err != nil { return err } @@ -69,7 +69,7 @@ func EncodeStructFields(w io.Writer, required, optional []any) error { } } - for i, v := range optional { + for i, v := range opt.vals() { if !includeOptional[i] { return nil } @@ -85,22 +85,51 @@ func EncodeStructFields(w io.Writer, required, optional []any) error { return b.Flush() } +// Optional returns the `vals` as [OptionalFields]; see the type's documentation +// for the resulting behaviour. +func Optional(vals ...any) *OptionalFields { + return &OptionalFields{vals} +} + +// OptionalFields are treated by [EncodeStructFields] and +// [Stream.DecodeStructFields] as if they were tagged with `rlp:"optional"`. +type OptionalFields struct { + // Note that the [OptionalFields] type exists primarily to improve + // readability at the call sites of [EncodeStructFields] and + // [Stream.DecodeStructFields]. While an `[]any` slice would suffice, it + // results in ambiguous usage of field functionality. + + v []any +} + +// vals is a convenience wrapper, returning o.v, but allowing for a nil +// receiver, in which case it returns a nil slice. +func (o *OptionalFields) vals() []any { + if o == nil { + return nil + } + return o.v +} + var errUnsupportedOptionalFieldType = errors.New("unsupported optional field type") -// optionalFieldInclusionFlags returns a slice of booleans, the same length as -// `vals`, indicating whether or not the respective optional value MUST be -// written to a list. A value must be written if it or any later value is -// non-nil; the returned slice is therefore monotonic non-increasing from true -// to false. -func optionalFieldInclusionFlags(vals []any) ([]bool, error) { - flags := make([]bool, len(vals)) +// inclusionFlags returns a slice of booleans, the same length as `fs`, +// indicating whether or not the respective field MUST be written to a list. A +// field must be written if it or any later field value is non-nil; the returned +// slice is therefore monotonic non-increasing from true to false. +func (o *OptionalFields) inclusionFlags() ([]bool, error) { + if o == nil { + return nil, nil + } + + flags := make([]bool, len(o.v)) var include bool - for i := len(vals) - 1; i >= 0; i-- { - switch v := reflect.ValueOf(vals[i]); v.Kind() { + for i := len(o.v) - 1; i >= 0; i-- { + switch v := reflect.ValueOf(o.v[i]); v.Kind() { case reflect.Slice, reflect.Pointer: include = include || !v.IsNil() default: - return nil, fmt.Errorf("%w: %T", errUnsupportedOptionalFieldType, vals[i]) + return nil, fmt.Errorf("%w: %T", errUnsupportedOptionalFieldType, o.v[i]) } flags[i] = include } @@ -142,12 +171,13 @@ func DecodeList[T any](s *Stream) ([]*T, error) { } // DecodeStructFields is the inverse of [EncodeStructFields]. All destination -// fields, be they `required` or `optional`, MUST be pointers and all `optional` -// fields MUST be provided in case they are present in the RLP being decoded. +// fields, be they required or optional, MUST be pointers and all optional +// fields MUST be provided in case they are present in the RLP being decoded. If +// no optional fields exist, the argument MAY be nil. // // Typically, the arguments to this function mirror those passed to // [EncodeStructFields] except for being pointers. See the example. -func (s *Stream) DecodeStructFields(required, optional []any) error { +func (s *Stream) DecodeStructFields(required []any, opt *OptionalFields) error { return s.FromList(func() error { for _, v := range required { if err := s.Decode(v); err != nil { @@ -155,7 +185,7 @@ func (s *Stream) DecodeStructFields(required, optional []any) error { } } - for _, v := range optional { + for _, v := range opt.vals() { if !s.MoreDataInList() { return nil } diff --git a/rlp/list.libevm_test.go b/rlp/list.libevm_test.go index 3dc1e8488d7..e7ec2e18750 100644 --- a/rlp/list.libevm_test.go +++ b/rlp/list.libevm_test.go @@ -107,7 +107,7 @@ func TestStructFieldHelpers(t *testing.T) { err = EncodeStructFields( &got, []any{obj.A, obj.B, obj.C}, - []any{obj.D, obj.E, obj.F}, + Optional(obj.D, obj.E, obj.F), ) require.NoErrorf(t, err, "EncodeStructFields(..., [required], [optional])") @@ -119,7 +119,7 @@ func TestStructFieldHelpers(t *testing.T) { var got foo err := s.DecodeStructFields( []any{&got.A, &got.B, &got.C}, - []any{&got.D, &got.E, &got.F}, + Optional(&got.D, &got.E, &got.F), ) require.NoError(t, err, "Stream.DecodeStructFields(...)") @@ -158,7 +158,7 @@ func ExampleStream_DecodeStructFields() { _ = EncodeStructFields( io.Discard, []any{val.A, val.B}, - []any{val.C}, + Optional(val.C), ) r := bytes.NewReader(nil /*arbitrary RLP buffer*/) @@ -170,7 +170,7 @@ func ExampleStream_DecodeStructFields() { &val.A, Nillable(&val.B), }, - []any{&val.C}, + Optional(&val.C), ) // Note the parallels between the arguments passed to From b9eeacaab2ae522491701fdc92999a33a42d9605 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> Date: Fri, 7 Feb 2025 10:44:47 +0000 Subject: [PATCH 10/13] Apply suggestions from code review Co-authored-by: Quentin McGaw Signed-off-by: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> --- core/types/rlp_backwards_compat.libevm_test.go | 4 ++-- rlp/list.libevm_test.go | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/core/types/rlp_backwards_compat.libevm_test.go b/core/types/rlp_backwards_compat.libevm_test.go index 62ee37de49d..f0f239f86a2 100644 --- a/core/types/rlp_backwards_compat.libevm_test.go +++ b/core/types/rlp_backwards_compat.libevm_test.go @@ -248,14 +248,14 @@ func TestBodyRLPCChainCompat(t *testing.T) { wantRLPHex string }{ { - name: "nil ExtData", + name: "nil_ExtData", extra: &cChainBodyExtras{ Version: version, }, wantRLPHex: `e5dedd2a80809400000000000000000000000000decafc0ffeebad8080808080c08304cb2f80`, }, { - name: "non-nil ExtData", + name: "non-nil_ExtData", extra: &cChainBodyExtras{ Version: version, ExtData: &[]byte{1, 4, 2, 8, 5, 7}, diff --git a/rlp/list.libevm_test.go b/rlp/list.libevm_test.go index e7ec2e18750..10c27ecfbd4 100644 --- a/rlp/list.libevm_test.go +++ b/rlp/list.libevm_test.go @@ -95,7 +95,6 @@ func TestStructFieldHelpers(t *testing.T) { } for _, obj := range tests { - obj := obj t.Run("", func(t *testing.T) { t.Logf("\n%s", pretty.Sprint(obj)) @@ -221,7 +220,6 @@ func TestNillable(t *testing.T) { } for _, obj := range tests { - obj := obj t.Run("", func(t *testing.T) { rlp, err := EncodeToBytes(obj) require.NoErrorf(t, err, "EncodeToBytes(%+v)", obj) From f0045f46cbf1799648944df102a89e68a38e7ec8 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Fri, 7 Feb 2025 15:04:02 +0000 Subject: [PATCH 11/13] refactor(rlp): introduce `Fields` type --- core/types/block.libevm.go | 27 ++- .../types/rlp_backwards_compat.libevm_test.go | 26 ++- rlp/fields.libevm.go | 140 +++++++++++ rlp/fields.libevm_test.go | 217 ++++++++++++++++++ rlp/list.libevm.go | 145 ------------ rlp/list.libevm_test.go | 194 ---------------- 6 files changed, 388 insertions(+), 361 deletions(-) create mode 100644 rlp/fields.libevm.go create mode 100644 rlp/fields.libevm_test.go diff --git a/core/types/block.libevm.go b/core/types/block.libevm.go index 9a0bf5c060a..832fe421f0a 100644 --- a/core/types/block.libevm.go +++ b/core/types/block.libevm.go @@ -118,23 +118,20 @@ var _ interface { } = (*Body)(nil) // EncodeRLP implements the [rlp.Encoder] interface. -func (b *Body) EncodeRLP(dst io.Writer) error { - required, optional := b.hooks().RLPFieldsForEncoding(b) - return rlp.EncodeStructFields(dst, required, optional) +func (b *Body) EncodeRLP(w io.Writer) error { + return b.hooks().RLPFieldsForEncoding(b).EncodeRLP(w) } // DecodeRLP implements the [rlp.Decoder] interface. func (b *Body) DecodeRLP(s *rlp.Stream) error { - return s.DecodeStructFields( - b.hooks().RLPFieldPointersForDecoding(b), - ) + return b.hooks().RLPFieldPointersForDecoding(b).DecodeRLP(s) } // BodyHooks are required for all types registered with [RegisterExtras] for // [Body] payloads. type BodyHooks interface { - RLPFieldsForEncoding(*Body) ([]any, *rlp.OptionalFields) - RLPFieldPointersForDecoding(*Body) ([]any, *rlp.OptionalFields) + RLPFieldsForEncoding(*Body) *rlp.Fields + RLPFieldPointersForDecoding(*Body) *rlp.Fields } // TestOnlyRegisterBodyHooks is a temporary means of "registering" BodyHooks for @@ -165,10 +162,16 @@ type NOOPBodyHooks struct{} // backwards-compatibility tests added. var _ = &Body{[]*Transaction{}, []*Header{}, []*Withdrawal{}} -func (NOOPBodyHooks) RLPFieldsForEncoding(b *Body) ([]any, *rlp.OptionalFields) { - return []any{b.Transactions, b.Uncles}, rlp.Optional(b.Withdrawals) +func (NOOPBodyHooks) RLPFieldsForEncoding(b *Body) *rlp.Fields { + return &rlp.Fields{ + Required: []any{b.Transactions, b.Uncles}, + Optional: []any{b.Withdrawals}, + } } -func (NOOPBodyHooks) RLPFieldPointersForDecoding(b *Body) ([]any, *rlp.OptionalFields) { - return []any{&b.Transactions, &b.Uncles}, rlp.Optional(&b.Withdrawals) +func (NOOPBodyHooks) RLPFieldPointersForDecoding(b *Body) *rlp.Fields { + return &rlp.Fields{ + Required: []any{&b.Transactions, &b.Uncles}, + Optional: []any{&b.Withdrawals}, + } } diff --git a/core/types/rlp_backwards_compat.libevm_test.go b/core/types/rlp_backwards_compat.libevm_test.go index f0f239f86a2..673bf548c39 100644 --- a/core/types/rlp_backwards_compat.libevm_test.go +++ b/core/types/rlp_backwards_compat.libevm_test.go @@ -197,7 +197,7 @@ type cChainBodyExtras struct { var _ BodyHooks = (*cChainBodyExtras)(nil) -func (e *cChainBodyExtras) RLPFieldsForEncoding(b *Body) ([]any, *rlp.OptionalFields) { +func (e *cChainBodyExtras) RLPFieldsForEncoding(b *Body) *rlp.Fields { // The Avalanche C-Chain uses all of the geth required fields (but none of // the optional ones) so there's no need to explicitly list them. This // pattern might not be ideal for readability but is used here for @@ -205,19 +205,25 @@ func (e *cChainBodyExtras) RLPFieldsForEncoding(b *Body) ([]any, *rlp.OptionalFi // // All new fields will always be tagged as optional for backwards // compatibility so this is safe to do, but only for the required fields. - required, _ /*drop all optional*/ := NOOPBodyHooks{}.RLPFieldsForEncoding(b) - return append(required, e.Version, e.ExtData), nil + return &rlp.Fields{ + Required: append( + NOOPBodyHooks{}.RLPFieldsForEncoding(b).Required, + e.Version, e.ExtData, + ), + } } -func (e *cChainBodyExtras) RLPFieldPointersForDecoding(b *Body) ([]any, *rlp.OptionalFields) { +func (e *cChainBodyExtras) RLPFieldPointersForDecoding(b *Body) *rlp.Fields { // An alternative to the pattern used above is to explicitly list all // fields for better introspection. - return []any{ - &b.Transactions, - &b.Uncles, - &e.Version, - rlp.Nillable(&e.ExtData), // equivalent to `rlp:"nil"` - }, nil + return &rlp.Fields{ + Required: []any{ + &b.Transactions, + &b.Uncles, + &e.Version, + rlp.Nillable(&e.ExtData), // equivalent to `rlp:"nil"` + }, + } } func TestBodyRLPCChainCompat(t *testing.T) { diff --git a/rlp/fields.libevm.go b/rlp/fields.libevm.go new file mode 100644 index 00000000000..20e4261cd17 --- /dev/null +++ b/rlp/fields.libevm.go @@ -0,0 +1,140 @@ +// Copyright 2025 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them under the terms of the GNU Lesser General Public License +// as published by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// The libevm additions are distributed in the hope that they will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see +// . + +package rlp + +import ( + "errors" + "fmt" + "io" + "reflect" +) + +// Fields mirror the RLP encoding of struct fields. +type Fields struct { + Required []any + Optional []any // equivalent to those tagged with `rlp:"optional"` +} + +var _ interface { + Encoder + Decoder +} = (*Fields)(nil) + +// EncodeRLP encodes the `f.Required` and `f.Optional` slices to `w`, +// concatenated as a single list, as if they were fields in a struct. The +// optional values are treated identically to those tagged with +// `rlp:"optional"`. +func (f *Fields) EncodeRLP(w io.Writer) error { + includeOptional, err := f.optionalInclusionFlags() + if err != nil { + return err + } + + b := NewEncoderBuffer(w) + err = b.InList(func() error { + for _, v := range f.Required { + if err := Encode(b, v); err != nil { + return err + } + } + + for i, v := range f.Optional { + if !includeOptional[i] { + return nil + } + if err := Encode(b, v); err != nil { + return err + } + } + return nil + }) + if err != nil { + return err + } + return b.Flush() +} + +var errUnsupportedOptionalFieldType = errors.New("unsupported optional field type") + +// optionalInclusionFlags returns a slice of booleans, the same length as +// `f.Optional`, indicating whether or not the respective field MUST be written +// to a list. A field must be written if it or any later field value is non-nil; +// the returned slice is therefore monotonic non-increasing from true to false. +func (f *Fields) optionalInclusionFlags() ([]bool, error) { + flags := make([]bool, len(f.Optional)) + var include bool + for i := len(f.Optional) - 1; i >= 0; i-- { + switch v := reflect.ValueOf(f.Optional[i]); v.Kind() { + case reflect.Slice, reflect.Pointer: + include = include || !v.IsNil() + default: + return nil, fmt.Errorf("%w: %T", errUnsupportedOptionalFieldType, f.Optional[i]) + } + flags[i] = include + } + return flags, nil +} + +// DecodeRLP implements the [Decoder] interface. All destination fields, be they +// required or optional, MUST be pointers and all optional fields MUST be +// provided in case they are present in the RLP being decoded. +// +// Typically, the arguments to this method mirror those passed to +// [Fields.EncodeRLP] except for being pointers. See the example. +func (f *Fields) DecodeRLP(s *Stream) error { + return s.FromList(func() error { + for _, v := range f.Required { + if err := s.Decode(v); err != nil { + return err + } + } + + for _, v := range f.Optional { + if !s.MoreDataInList() { + return nil + } + if err := s.Decode(v); err != nil { + return err + } + } + return nil + }) +} + +// Nillable wraps `field` to mirror the behaviour of an `rlp:"nil"` tag; i.e. if +// a zero-sized RLP item is decoded into the returned Decoder then it is dropped +// and `*field` is set to nil, otherwise the RLP item is decoded directly into +// `field`. The return argument is intended for use with +// [Fields.DecodeRLP]. +func Nillable[T any](field **T) Decoder { + return &nillable[T]{field} +} + +type nillable[T any] struct{ v **T } + +func (n *nillable[T]) DecodeRLP(s *Stream) error { + _, size, err := s.Kind() + if err != nil { + return err + } + if size > 0 { + return s.Decode(n.v) + } + *n.v = nil + _, err = s.Raw() // consume the item + return err +} diff --git a/rlp/fields.libevm_test.go b/rlp/fields.libevm_test.go new file mode 100644 index 00000000000..e6a98b8e33f --- /dev/null +++ b/rlp/fields.libevm_test.go @@ -0,0 +1,217 @@ +// Copyright 2025 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them under the terms of the GNU Lesser General Public License +// as published by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// The libevm additions are distributed in the hope that they will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see +// . + +package rlp + +import ( + "bytes" + "io" + "testing" + + "github.com/ava-labs/libevm/common" + "github.com/kr/pretty" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFields(t *testing.T) { + type foo struct { + A uint64 + B uint64 + C *uint64 + D *uint64 `rlp:"optional"` + E []uint64 `rlp:"optional"` + F *[]uint64 `rlp:"optional"` + } + + const ( + a uint64 = iota + b + cVal + dVal + ) + c := common.PointerTo(cVal) + d := common.PointerTo(dVal) + e := []uint64{40, 41} + f := &[]uint64{50, 51} + + tests := []foo{ + {a, b, c, d, e, f}, // 000 (which of d/e/f are nil) + {a, b, c, d, e, nil}, // 001 + {a, b, c, d, nil, f}, // 010 + {a, b, c, d, nil, nil}, // 011 + {a, b, c, nil, e, f}, // 100 + {a, b, c, nil, e, nil}, // 101 + {a, b, c, nil, nil, f}, // 110 + {a, b, c, nil, nil, nil}, // 111 + // Empty and nil slices are treated differently when optional + {a, b, c, nil, []uint64{}, nil}, + {a, b, c, nil, nil, &[]uint64{}}, + } + + for _, obj := range tests { + t.Run("", func(t *testing.T) { + t.Logf("\n%s", pretty.Sprint(obj)) + + wantRLP, err := EncodeToBytes(obj) + require.NoErrorf(t, err, "EncodeToBytes([actual struct])") + + t.Run("Fields.EncodeRLP", func(t *testing.T) { + got, err := EncodeToBytes(&Fields{ + Required: []any{obj.A, obj.B, obj.C}, + Optional: []any{obj.D, obj.E, obj.F}, + }) + require.NoError(t, err) + assert.Equal(t, wantRLP, got, "vs EncodeToBytes([actual struct])") + }) + + t.Run("Fields.DecodeRLP", func(t *testing.T) { + var got foo + err := DecodeBytes(wantRLP, &Fields{ + Required: []any{&got.A, &got.B, &got.C}, + Optional: []any{&got.D, &got.E, &got.F}, + }) + require.NoError(t, err, "DecodeBytes(..., %T)", &Fields{}) + + var want foo + err = DecodeBytes(wantRLP, &want) + require.NoError(t, err, "DecodeBytes(..., [actual struct])") + + assert.Equal(t, want, got, "vs DecodeBytes(..., [original struct])") + }) + }) + } +} + +//nolint:testableexamples // Demonstrating code equivalence, not outputs. +func ExampleFields() { + type inner struct { + X uint64 + } + + type outer struct { + A uint64 + B *inner `rlp:"nil"` + C *inner `rlp:"optional"` + } + + val := outer{ + A: 42, + B: &inner{X: 42}, + C: &inner{X: 99}, + } + + // Errors are dropped for brevity for the sake of the example only. + + _ = Encode(io.Discard, val) + // is equivalent to + _ = Encode( + io.Discard, + &Fields{ + Required: []any{val.A, val.B}, + Optional: []any{val.C}, + }, + ) + + var ( + r *bytes.Reader // arbitrary RLP buffer + decoded outer + ) + + _ = Decode(r, &decoded) + // is equivalent to + _ = Decode(r, &Fields{ + Required: []any{ + &decoded.A, + Nillable(&decoded.B), + }, + Optional: []any{&decoded.C}, + }) + + // Note the parallels between the arguments passed to + // Fields.{En,De}codeRLP() and that, when decoding an optional or + // `rlp:"nil`-tagged field, a pointer to the _field_ is required even though + // in this example it will be a `**inner`. +} + +func TestNillable(t *testing.T) { + type inner struct { + X uint64 + } + + type outer struct { + A *uint64 `rlp:"nil"` + B *inner `rlp:"nil"` + C *[]uint64 `rlp:"nil"` + } + + // Unlike the `rlp:"optional"` tag, there is no interplay between nil-tagged + // fields so we don't need the Cartesian product of all possible + // combinations. + var tests []outer + for _, a := range []*uint64{ + nil, + common.PointerTo[uint64](0), + } { + tests = append(tests, outer{a, nil, nil}) + } + for _, b := range []*inner{ + nil, + {0}, + } { + tests = append(tests, outer{nil, b, nil}) + } + for _, c := range []*[]uint64{ + nil, + {}, + {0}, + } { + tests = append(tests, outer{nil, nil, c}) + } + + // When a Nillable encounters an empty list it MUST set the field to nil, + // not just ignore it. + corruptInitialValue := func() outer { + return outer{common.PointerTo[uint64](42), &inner{42}, &[]uint64{42}} + } + + for _, obj := range tests { + t.Run("", func(t *testing.T) { + rlp, err := EncodeToBytes(obj) + require.NoErrorf(t, err, "EncodeToBytes(%+v)", obj) + t.Logf("%s => %#x", pretty.Sprint(obj), rlp) + + // Although this is an immediate inversion of the line above, it + // provides us with the canonical RLP decoding, which our input + // struct may not honour. + want := corruptInitialValue() + err = DecodeBytes(rlp, &want) + require.NoErrorf(t, err, "DecodeBytes(%#x, %T)", rlp, &want) + + got := corruptInitialValue() + err = DecodeBytes(rlp, &Fields{ + Required: []any{ + Nillable(&got.A), + Nillable(&got.B), + Nillable(&got.C), + }, + }) + require.NoErrorf(t, err, "DecodeBytes(..., %T)", &Fields{}) + + assert.Equal(t, want, got, "DecodeBytes(..., [actual struct]) vs DecodeBytes(..., [fields wrapped in Nillable()])") + }) + } +} diff --git a/rlp/list.libevm.go b/rlp/list.libevm.go index 864dca428b5..2fd74ce645d 100644 --- a/rlp/list.libevm.go +++ b/rlp/list.libevm.go @@ -16,13 +16,6 @@ package rlp -import ( - "errors" - "fmt" - "io" - "reflect" -) - // InList is a convenience wrapper, calling `fn` between calls to // [EncoderBuffer.List] and [EncoderBuffer.ListEnd]. If `fn` returns an error, // it is propagated directly. @@ -49,93 +42,6 @@ func EncodeListToBuffer[T any](b EncoderBuffer, vals []T) error { }) } -// EncodeStructFields encodes the required and optional slices to `w`, -// concatenated as a single list, as if they were fields in a struct. The -// optional "fields", which MAY be nil, are treated identically to those tagged -// with `rlp:"optional"`. -// -// See the example for [Stream.DecodeStructFields]. -func EncodeStructFields(w io.Writer, required []any, opt *OptionalFields) error { - includeOptional, err := opt.inclusionFlags() - if err != nil { - return err - } - - b := NewEncoderBuffer(w) - err = b.InList(func() error { - for _, v := range required { - if err := Encode(b, v); err != nil { - return err - } - } - - for i, v := range opt.vals() { - if !includeOptional[i] { - return nil - } - if err := Encode(b, v); err != nil { - return err - } - } - return nil - }) - if err != nil { - return err - } - return b.Flush() -} - -// Optional returns the `vals` as [OptionalFields]; see the type's documentation -// for the resulting behaviour. -func Optional(vals ...any) *OptionalFields { - return &OptionalFields{vals} -} - -// OptionalFields are treated by [EncodeStructFields] and -// [Stream.DecodeStructFields] as if they were tagged with `rlp:"optional"`. -type OptionalFields struct { - // Note that the [OptionalFields] type exists primarily to improve - // readability at the call sites of [EncodeStructFields] and - // [Stream.DecodeStructFields]. While an `[]any` slice would suffice, it - // results in ambiguous usage of field functionality. - - v []any -} - -// vals is a convenience wrapper, returning o.v, but allowing for a nil -// receiver, in which case it returns a nil slice. -func (o *OptionalFields) vals() []any { - if o == nil { - return nil - } - return o.v -} - -var errUnsupportedOptionalFieldType = errors.New("unsupported optional field type") - -// inclusionFlags returns a slice of booleans, the same length as `fs`, -// indicating whether or not the respective field MUST be written to a list. A -// field must be written if it or any later field value is non-nil; the returned -// slice is therefore monotonic non-increasing from true to false. -func (o *OptionalFields) inclusionFlags() ([]bool, error) { - if o == nil { - return nil, nil - } - - flags := make([]bool, len(o.v)) - var include bool - for i := len(o.v) - 1; i >= 0; i-- { - switch v := reflect.ValueOf(o.v[i]); v.Kind() { - case reflect.Slice, reflect.Pointer: - include = include || !v.IsNil() - default: - return nil, fmt.Errorf("%w: %T", errUnsupportedOptionalFieldType, o.v[i]) - } - flags[i] = include - } - return flags, nil -} - // FromList is a convenience wrapper, calling `fn` between calls to // [Stream.List] and [Stream.ListEnd]. If `fn` returns an error, it is // propagated directly. @@ -169,54 +75,3 @@ func DecodeList[T any](s *Stream) ([]*T, error) { }) return vals, err } - -// DecodeStructFields is the inverse of [EncodeStructFields]. All destination -// fields, be they required or optional, MUST be pointers and all optional -// fields MUST be provided in case they are present in the RLP being decoded. If -// no optional fields exist, the argument MAY be nil. -// -// Typically, the arguments to this function mirror those passed to -// [EncodeStructFields] except for being pointers. See the example. -func (s *Stream) DecodeStructFields(required []any, opt *OptionalFields) error { - return s.FromList(func() error { - for _, v := range required { - if err := s.Decode(v); err != nil { - return err - } - } - - for _, v := range opt.vals() { - if !s.MoreDataInList() { - return nil - } - if err := s.Decode(v); err != nil { - return err - } - } - return nil - }) -} - -// Nillable wraps `field` to mirror the behaviour of an `rlp:"nil"` tag; i.e. if -// a zero-sized RLP item is decoded into the returned Decoder then it is dropped -// and `*field` is set to nil, otherwise the RLP item is decoded directly into -// `field`. The return argument is intended for use with -// [Stream.DecodeStructFields]. -func Nillable[T any](field **T) Decoder { - return &nillable[T]{field} -} - -type nillable[T any] struct{ v **T } - -func (n *nillable[T]) DecodeRLP(s *Stream) error { - _, size, err := s.Kind() - if err != nil { - return err - } - if size > 0 { - return s.Decode(n.v) - } - *n.v = nil - _, err = s.Raw() // consume the item - return err -} diff --git a/rlp/list.libevm_test.go b/rlp/list.libevm_test.go index 10c27ecfbd4..8e2c76711c9 100644 --- a/rlp/list.libevm_test.go +++ b/rlp/list.libevm_test.go @@ -18,14 +18,10 @@ package rlp import ( "bytes" - "io" "testing" - "github.com/kr/pretty" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/ava-labs/libevm/common" ) func TestEncodeListToBuffer(t *testing.T) { @@ -58,193 +54,3 @@ func TestDecodeList(t *testing.T) { assert.Equalf(t, vals[i], *gotPtr, "DecodeList()[%d]", i) } } - -func TestStructFieldHelpers(t *testing.T) { - type foo struct { - A uint64 - B uint64 - C *uint64 - D *uint64 `rlp:"optional"` - E []uint64 `rlp:"optional"` - F *[]uint64 `rlp:"optional"` - } - - const ( - a uint64 = iota - b - cVal - dVal - ) - c := common.PointerTo(cVal) - d := common.PointerTo(dVal) - e := []uint64{40, 41} - f := &[]uint64{50, 51} - - tests := []foo{ - {a, b, c, d, e, f}, // 000 (which of d/e/f are nil) - {a, b, c, d, e, nil}, // 001 - {a, b, c, d, nil, f}, // 010 - {a, b, c, d, nil, nil}, // 011 - {a, b, c, nil, e, f}, // 100 - {a, b, c, nil, e, nil}, // 101 - {a, b, c, nil, nil, f}, // 110 - {a, b, c, nil, nil, nil}, // 111 - // Empty and nil slices are treated differently when optional - {a, b, c, nil, []uint64{}, nil}, - {a, b, c, nil, nil, &[]uint64{}}, - } - - for _, obj := range tests { - t.Run("", func(t *testing.T) { - t.Logf("\n%s", pretty.Sprint(obj)) - - wantRLP, err := EncodeToBytes(obj) - require.NoErrorf(t, err, "EncodeToBytes([actual struct])") - - t.Run("EncodeStructFields", func(t *testing.T) { - var got bytes.Buffer - err = EncodeStructFields( - &got, - []any{obj.A, obj.B, obj.C}, - Optional(obj.D, obj.E, obj.F), - ) - require.NoErrorf(t, err, "EncodeStructFields(..., [required], [optional])") - - assert.Equal(t, wantRLP, got.Bytes(), "EncodeToBytes() vs EncodeStructFields()") - }) - - t.Run("DecodeStructFields", func(t *testing.T) { - s := NewStream(bytes.NewReader(wantRLP), 0) - var got foo - err := s.DecodeStructFields( - []any{&got.A, &got.B, &got.C}, - Optional(&got.D, &got.E, &got.F), - ) - require.NoError(t, err, "Stream.DecodeStructFields(...)") - - var want foo - err = DecodeBytes(wantRLP, &want) - require.NoError(t, err, "DecodeBytes(...)") - - assert.Equal(t, want, got, "DecodeBytes(..., [original struct]) vs Stream.DecodeStructFields(...)") - }) - }) - } -} - -//nolint:testableexamples // Demonstrating code equivalence, not outputs. -func ExampleStream_DecodeStructFields() { - type inner struct { - X uint64 - } - - type outer struct { - A uint64 - B *inner `rlp:"nil"` - C *inner `rlp:"optional"` - } - - val := outer{ - A: 42, - B: &inner{X: 42}, - C: &inner{X: 99}, - } - - // Errors are dropped for brevity for the sake of the example only. - - _ = Encode(io.Discard, val) - // is equivalent to - _ = EncodeStructFields( - io.Discard, - []any{val.A, val.B}, - Optional(val.C), - ) - - r := bytes.NewReader(nil /*arbitrary RLP buffer*/) - var decoded outer - _ = Decode(r, &decoded) - // is equivalent to - _ = NewStream(r, 0).DecodeStructFields( - []any{ - &val.A, - Nillable(&val.B), - }, - Optional(&val.C), - ) - - // Note the parallels between the arguments passed to - // {En,De}codeStructFields() and that, when decoding an optional or - // `rlp:"nil`-tagged field, a pointer to the _field_ is required even though - // in this example it will be a `**inner`. -} - -func TestNillable(t *testing.T) { - type inner struct { - X uint64 - } - - type outer struct { - A *uint64 `rlp:"nil"` - B *inner `rlp:"nil"` - C *[]uint64 `rlp:"nil"` - } - - // Unlike the `rlp:"optional"` tag, there is no interplay between nil-tagged - // fields so we don't need the Cartesian product of all possible - // combinations. - var tests []outer - for _, a := range []*uint64{ - nil, - common.PointerTo[uint64](0), - } { - tests = append(tests, outer{a, nil, nil}) - } - for _, b := range []*inner{ - nil, - {0}, - } { - tests = append(tests, outer{nil, b, nil}) - } - for _, c := range []*[]uint64{ - nil, - {}, - {0}, - } { - tests = append(tests, outer{nil, nil, c}) - } - - // When a Nillable encounters an empty list it MUST set the field to nil, - // not just ignore it. - corruptInitialValue := func() outer { - return outer{common.PointerTo[uint64](42), &inner{42}, &[]uint64{42}} - } - - for _, obj := range tests { - t.Run("", func(t *testing.T) { - rlp, err := EncodeToBytes(obj) - require.NoErrorf(t, err, "EncodeToBytes(%+v)", obj) - t.Logf("%s => %#x", pretty.Sprint(obj), rlp) - - // Although this is an immediate inversion of the line above, it - // provides us with the canonical RLP decoding, which our input - // struct may not honour. - want := corruptInitialValue() - err = DecodeBytes(rlp, &want) - require.NoErrorf(t, err, "DecodeBytes(%#x, %T)", rlp, &want) - - s := NewStream(bytes.NewReader(rlp), 0) - got := corruptInitialValue() - err = s.DecodeStructFields( - []any{ - Nillable(&got.A), - Nillable(&got.B), - Nillable(&got.C), - }, - nil, - ) - require.NoError(t, err, "Stream.DecodeStructFields(...)") - - assert.Equal(t, want, got, "DecodeBytes(...) vs Stream.DecodeStructFields([fields wrapped in Nillable()])") - }) - } -} From ef97380e645239d428d6724197a2f6f0703424a5 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Fri, 7 Feb 2025 15:25:37 +0000 Subject: [PATCH 12/13] doc: fix `Nillable()` --- rlp/fields.libevm.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rlp/fields.libevm.go b/rlp/fields.libevm.go index 20e4261cd17..9595e2e32e3 100644 --- a/rlp/fields.libevm.go +++ b/rlp/fields.libevm.go @@ -118,8 +118,7 @@ func (f *Fields) DecodeRLP(s *Stream) error { // Nillable wraps `field` to mirror the behaviour of an `rlp:"nil"` tag; i.e. if // a zero-sized RLP item is decoded into the returned Decoder then it is dropped // and `*field` is set to nil, otherwise the RLP item is decoded directly into -// `field`. The return argument is intended for use with -// [Fields.DecodeRLP]. +// `field`. The return argument is intended for use with [Fields]. func Nillable[T any](field **T) Decoder { return &nillable[T]{field} } From 95e2a9141d2078d318d05c2de05763f192ca673e Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Fri, 7 Feb 2025 15:32:32 +0000 Subject: [PATCH 13/13] chore: `gci` --- rlp/fields.libevm_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rlp/fields.libevm_test.go b/rlp/fields.libevm_test.go index e6a98b8e33f..0eead78489b 100644 --- a/rlp/fields.libevm_test.go +++ b/rlp/fields.libevm_test.go @@ -21,10 +21,11 @@ import ( "io" "testing" - "github.com/ava-labs/libevm/common" "github.com/kr/pretty" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/ava-labs/libevm/common" ) func TestFields(t *testing.T) {