Skip to content

Commit d210cc4

Browse files
ARR4Nqdm12
andauthored
refactor(core/types): simplify Body RLP override (#120)
## Why this should be merged Simplification of `types.Body` RLP overriding, resulting in reduced code at both the implementation and consumer ends. ## How this works Introduction of `rlp.Fields` type, to mirror regular RLP encoding of a struct. The RLP override hook now only needs to return the fields of interest, which MAY come from either the `Body` or the registered extra. This pattern allows for arbitrary modification of upstream fields via (1) reordering; (2) addition; (3) deletion; and (4) inverting required vs optional status. While less important for `Body`, this allows for complete support of `ava-labs/coreth` `Header` modifications, which make use of 1-3. ## How this was tested Existing backwards-compatibility tests + new unit tests for introduced functionality. --------- Signed-off-by: Arran Schlosberg <[email protected]> Co-authored-by: Quentin McGaw <[email protected]>
1 parent 761c4b4 commit d210cc4

File tree

4 files changed

+412
-91
lines changed

4 files changed

+412
-91
lines changed

core/types/block.libevm.go

+28-64
Original file line numberDiff line numberDiff line change
@@ -112,79 +112,26 @@ func (*NOOPHeaderHooks) DecodeRLP(h *Header, s *rlp.Stream) error {
112112
}
113113
func (*NOOPHeaderHooks) PostCopy(dst *Header) {}
114114

115-
var (
116-
_ interface {
117-
rlp.Encoder
118-
rlp.Decoder
119-
} = (*Body)(nil)
120-
121-
// The implementations of [Body.EncodeRLP] and [Body.DecodeRLP] make
122-
// assumptions about the struct fields and their order, which we lock in here as a change
123-
// detector. If this breaks then it MUST be updated and the RLP methods
124-
// reviewed + new backwards-compatibility tests added.
125-
_ = &Body{[]*Transaction{}, []*Header{}, []*Withdrawal{}}
126-
)
115+
var _ interface {
116+
rlp.Encoder
117+
rlp.Decoder
118+
} = (*Body)(nil)
127119

128120
// EncodeRLP implements the [rlp.Encoder] interface.
129-
func (b *Body) EncodeRLP(dst io.Writer) error {
130-
w := rlp.NewEncoderBuffer(dst)
131-
132-
return w.InList(func() error {
133-
if err := rlp.EncodeListToBuffer(w, b.Transactions); err != nil {
134-
return err
135-
}
136-
if err := rlp.EncodeListToBuffer(w, b.Uncles); err != nil {
137-
return err
138-
}
139-
140-
hasLaterOptionalField := b.Withdrawals != nil
141-
if err := b.hooks().AppendRLPFields(w, hasLaterOptionalField); err != nil {
142-
return err
143-
}
144-
if !hasLaterOptionalField {
145-
return nil
146-
}
147-
return rlp.EncodeListToBuffer(w, b.Withdrawals)
148-
})
121+
func (b *Body) EncodeRLP(w io.Writer) error {
122+
return b.hooks().RLPFieldsForEncoding(b).EncodeRLP(w)
149123
}
150124

151125
// DecodeRLP implements the [rlp.Decoder] interface.
152126
func (b *Body) DecodeRLP(s *rlp.Stream) error {
153-
return s.FromList(func() error {
154-
txs, err := rlp.DecodeList[Transaction](s)
155-
if err != nil {
156-
return err
157-
}
158-
uncles, err := rlp.DecodeList[Header](s)
159-
if err != nil {
160-
return err
161-
}
162-
*b = Body{
163-
Transactions: txs,
164-
Uncles: uncles,
165-
}
166-
167-
if err := b.hooks().DecodeExtraRLPFields(s); err != nil {
168-
return err
169-
}
170-
if !s.MoreDataInList() {
171-
return nil
172-
}
173-
174-
ws, err := rlp.DecodeList[Withdrawal](s)
175-
if err != nil {
176-
return err
177-
}
178-
b.Withdrawals = ws
179-
return nil
180-
})
127+
return b.hooks().RLPFieldPointersForDecoding(b).DecodeRLP(s)
181128
}
182129

183130
// BodyHooks are required for all types registered with [RegisterExtras] for
184131
// [Body] payloads.
185132
type BodyHooks interface {
186-
AppendRLPFields(_ rlp.EncoderBuffer, mustWriteEmptyOptional bool) error
187-
DecodeExtraRLPFields(*rlp.Stream) error
133+
RLPFieldsForEncoding(*Body) *rlp.Fields
134+
RLPFieldPointersForDecoding(*Body) *rlp.Fields
188135
}
189136

190137
// TestOnlyRegisterBodyHooks is a temporary means of "registering" BodyHooks for
@@ -209,5 +156,22 @@ func (b *Body) hooks() BodyHooks {
209156
// having been registered.
210157
type NOOPBodyHooks struct{}
211158

212-
func (NOOPBodyHooks) AppendRLPFields(rlp.EncoderBuffer, bool) error { return nil }
213-
func (NOOPBodyHooks) DecodeExtraRLPFields(*rlp.Stream) error { return nil }
159+
// The RLP-related methods of [NOOPBodyHooks] make assumptions about the struct
160+
// fields and their order, which we lock in here as a change detector. If this
161+
// breaks then it MUST be updated and the RLP methods reviewed + new
162+
// backwards-compatibility tests added.
163+
var _ = &Body{[]*Transaction{}, []*Header{}, []*Withdrawal{}}
164+
165+
func (NOOPBodyHooks) RLPFieldsForEncoding(b *Body) *rlp.Fields {
166+
return &rlp.Fields{
167+
Required: []any{b.Transactions, b.Uncles},
168+
Optional: []any{b.Withdrawals},
169+
}
170+
}
171+
172+
func (NOOPBodyHooks) RLPFieldPointersForDecoding(b *Body) *rlp.Fields {
173+
return &rlp.Fields{
174+
Required: []any{&b.Transactions, &b.Uncles},
175+
Optional: []any{&b.Withdrawals},
176+
}
177+
}

core/types/rlp_backwards_compat.libevm_test.go

+27-27
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ func TestBodyRLPBackwardsCompatibility(t *testing.T) {
116116
newHdr := func(hashLow byte) *Header { return &Header{ParentHash: common.Hash{hashLow}} }
117117
newWithdraw := func(idx uint64) *Withdrawal { return &Withdrawal{Index: idx} }
118118

119-
// We build up test-case [Body] instances from the power set of each of
120-
// these components.
119+
// We build up test-case [Body] instances from the Cartesian product of each
120+
// of these components.
121121
txMatrix := [][]*Transaction{
122122
nil, {}, // Must be equivalent for non-optional field
123123
{newTx(1)},
@@ -197,35 +197,33 @@ type cChainBodyExtras struct {
197197

198198
var _ BodyHooks = (*cChainBodyExtras)(nil)
199199

200-
func (e *cChainBodyExtras) AppendRLPFields(b rlp.EncoderBuffer, _ bool) error {
201-
b.WriteUint64(uint64(e.Version))
202-
203-
var data []byte
204-
if e.ExtData != nil {
205-
data = *e.ExtData
200+
func (e *cChainBodyExtras) RLPFieldsForEncoding(b *Body) *rlp.Fields {
201+
// The Avalanche C-Chain uses all of the geth required fields (but none of
202+
// the optional ones) so there's no need to explicitly list them. This
203+
// pattern might not be ideal for readability but is used here for
204+
// demonstrative purposes.
205+
//
206+
// All new fields will always be tagged as optional for backwards
207+
// compatibility so this is safe to do, but only for the required fields.
208+
return &rlp.Fields{
209+
Required: append(
210+
NOOPBodyHooks{}.RLPFieldsForEncoding(b).Required,
211+
e.Version, e.ExtData,
212+
),
206213
}
207-
b.WriteBytes(data)
208-
209-
return nil
210214
}
211215

212-
func (e *cChainBodyExtras) DecodeExtraRLPFields(s *rlp.Stream) error {
213-
if err := s.Decode(&e.Version); err != nil {
214-
return err
215-
}
216-
217-
buf, err := s.Bytes()
218-
if err != nil {
219-
return err
220-
}
221-
if len(buf) > 0 {
222-
e.ExtData = &buf
223-
} else {
224-
// Respect the `rlp:"nil"` field tag.
225-
e.ExtData = nil
216+
func (e *cChainBodyExtras) RLPFieldPointersForDecoding(b *Body) *rlp.Fields {
217+
// An alternative to the pattern used above is to explicitly list all
218+
// fields for better introspection.
219+
return &rlp.Fields{
220+
Required: []any{
221+
&b.Transactions,
222+
&b.Uncles,
223+
&e.Version,
224+
rlp.Nillable(&e.ExtData), // equivalent to `rlp:"nil"`
225+
},
226226
}
227-
228-
return nil
229227
}
230228

231229
func TestBodyRLPCChainCompat(t *testing.T) {
@@ -256,12 +254,14 @@ func TestBodyRLPCChainCompat(t *testing.T) {
256254
wantRLPHex string
257255
}{
258256
{
257+
name: "nil_ExtData",
259258
extra: &cChainBodyExtras{
260259
Version: version,
261260
},
262261
wantRLPHex: `e5dedd2a80809400000000000000000000000000decafc0ffeebad8080808080c08304cb2f80`,
263262
},
264263
{
264+
name: "non-nil_ExtData",
265265
extra: &cChainBodyExtras{
266266
Version: version,
267267
ExtData: &[]byte{1, 4, 2, 8, 5, 7},

rlp/fields.libevm.go

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright 2025 the libevm authors.
2+
//
3+
// The libevm additions to go-ethereum are free software: you can redistribute
4+
// them and/or modify them under the terms of the GNU Lesser General Public License
5+
// as published by the Free Software Foundation, either version 3 of the License,
6+
// or (at your option) any later version.
7+
//
8+
// The libevm additions are distributed in the hope that they will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
11+
// General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU Lesser General Public License
14+
// along with the go-ethereum library. If not, see
15+
// <http://www.gnu.org/licenses/>.
16+
17+
package rlp
18+
19+
import (
20+
"errors"
21+
"fmt"
22+
"io"
23+
"reflect"
24+
)
25+
26+
// Fields mirror the RLP encoding of struct fields.
27+
type Fields struct {
28+
Required []any
29+
Optional []any // equivalent to those tagged with `rlp:"optional"`
30+
}
31+
32+
var _ interface {
33+
Encoder
34+
Decoder
35+
} = (*Fields)(nil)
36+
37+
// EncodeRLP encodes the `f.Required` and `f.Optional` slices to `w`,
38+
// concatenated as a single list, as if they were fields in a struct. The
39+
// optional values are treated identically to those tagged with
40+
// `rlp:"optional"`.
41+
func (f *Fields) EncodeRLP(w io.Writer) error {
42+
includeOptional, err := f.optionalInclusionFlags()
43+
if err != nil {
44+
return err
45+
}
46+
47+
b := NewEncoderBuffer(w)
48+
err = b.InList(func() error {
49+
for _, v := range f.Required {
50+
if err := Encode(b, v); err != nil {
51+
return err
52+
}
53+
}
54+
55+
for i, v := range f.Optional {
56+
if !includeOptional[i] {
57+
return nil
58+
}
59+
if err := Encode(b, v); err != nil {
60+
return err
61+
}
62+
}
63+
return nil
64+
})
65+
if err != nil {
66+
return err
67+
}
68+
return b.Flush()
69+
}
70+
71+
var errUnsupportedOptionalFieldType = errors.New("unsupported optional field type")
72+
73+
// optionalInclusionFlags returns a slice of booleans, the same length as
74+
// `f.Optional`, indicating whether or not the respective field MUST be written
75+
// to a list. A field must be written if it or any later field value is non-nil;
76+
// the returned slice is therefore monotonic non-increasing from true to false.
77+
func (f *Fields) optionalInclusionFlags() ([]bool, error) {
78+
flags := make([]bool, len(f.Optional))
79+
var include bool
80+
for i := len(f.Optional) - 1; i >= 0; i-- {
81+
switch v := reflect.ValueOf(f.Optional[i]); v.Kind() {
82+
case reflect.Slice, reflect.Pointer:
83+
include = include || !v.IsNil()
84+
default:
85+
return nil, fmt.Errorf("%w: %T", errUnsupportedOptionalFieldType, f.Optional[i])
86+
}
87+
flags[i] = include
88+
}
89+
return flags, nil
90+
}
91+
92+
// DecodeRLP implements the [Decoder] interface. All destination fields, be they
93+
// required or optional, MUST be pointers and all optional fields MUST be
94+
// provided in case they are present in the RLP being decoded.
95+
//
96+
// Typically, the arguments to this method mirror those passed to
97+
// [Fields.EncodeRLP] except for being pointers. See the example.
98+
func (f *Fields) DecodeRLP(s *Stream) error {
99+
return s.FromList(func() error {
100+
for _, v := range f.Required {
101+
if err := s.Decode(v); err != nil {
102+
return err
103+
}
104+
}
105+
106+
for _, v := range f.Optional {
107+
if !s.MoreDataInList() {
108+
return nil
109+
}
110+
if err := s.Decode(v); err != nil {
111+
return err
112+
}
113+
}
114+
return nil
115+
})
116+
}
117+
118+
// Nillable wraps `field` to mirror the behaviour of an `rlp:"nil"` tag; i.e. if
119+
// a zero-sized RLP item is decoded into the returned Decoder then it is dropped
120+
// and `*field` is set to nil, otherwise the RLP item is decoded directly into
121+
// `field`. The return argument is intended for use with [Fields].
122+
func Nillable[T any](field **T) Decoder {
123+
return &nillable[T]{field}
124+
}
125+
126+
type nillable[T any] struct{ v **T }
127+
128+
func (n *nillable[T]) DecodeRLP(s *Stream) error {
129+
_, size, err := s.Kind()
130+
if err != nil {
131+
return err
132+
}
133+
if size > 0 {
134+
return s.Decode(n.v)
135+
}
136+
*n.v = nil
137+
_, err = s.Raw() // consume the item
138+
return err
139+
}

0 commit comments

Comments
 (0)