Skip to content

Commit eda3b59

Browse files
ARR4Nqdm12
andauthored
feat(core/types): fine-grained Body RLP override (#109)
## Why this should be merged Allows for modification of `types.Body` payload data + RLP encoding without placing the entire RLP burden on the `libevm` user as we did with `types.HeaderHooks`. ## How this works RLP encoding of a struct is simply a concatenation of RLP encodings of fields, encompassed by an RLP "list". The `AppendRLPFields(rlp.EncoderBuffer, ...)` hook exploits this and plugs in before all `rlp:"optional"`-tagged fields to allow for inclusion of any new fields. The `EncoderBuffer` SHOULD be used as the `io.Writer` passed when encoding each field: `rlp.Encode(buffer, fieldValue)`. `Body` doesn't have `{En,De}codeRLP` methods so they are implemented to identically replicate original behaviour when a no-op hook is present. This pattern is sufficient for the `ava-labs/coreth` modifications of `Body` but can be modified / extended for more complex scenarios, like `Header`. > [!NOTE] > This PR does not include registration of the hooks as that was not the initial goal and adding them would create too much PR bloat. There is a placeholder `var todoRegisteredBodyHooks` global variable that can only be set in tests. ## How this was tested - Backwards compatibility: the new methods are fuzzed against a `type withoutMethods Body` passed directly to `rlp.{En,De}code()` - `coreth` compatibility: unit test of a local implementation of `BodyHooks` demonstrating reproducibility of RLP encoding. --------- Signed-off-by: Arran Schlosberg <[email protected]> Co-authored-by: Quentin McGaw <[email protected]>
1 parent be6e93e commit eda3b59

File tree

6 files changed

+518
-3
lines changed

6 files changed

+518
-3
lines changed

common/pointer.libevm.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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 common
18+
19+
// PointerTo is a convenience wrapper for creating a pointer to a value.
20+
func PointerTo[T any](x T) *T { return &x }

core/types/block.libevm.go

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2024 the libevm authors.
1+
// Copyright 2024-2025 the libevm authors.
22
//
33
// The libevm additions to go-ethereum are free software: you can redistribute
44
// them and/or modify them under the terms of the GNU Lesser General Public License
@@ -22,6 +22,7 @@ import (
2222
"io"
2323

2424
"github.com/ava-labs/libevm/libevm/pseudo"
25+
"github.com/ava-labs/libevm/libevm/testonly"
2526
"github.com/ava-labs/libevm/rlp"
2627
)
2728

@@ -109,5 +110,104 @@ func (*NOOPHeaderHooks) DecodeRLP(h *Header, s *rlp.Stream) error {
109110
type withoutMethods Header
110111
return s.Decode((*withoutMethods)(h))
111112
}
113+
func (*NOOPHeaderHooks) PostCopy(dst *Header) {}
114+
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+
)
127+
128+
// 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+
})
149+
}
150+
151+
// DecodeRLP implements the [rlp.Decoder] interface.
152+
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+
})
181+
}
182+
183+
// BodyHooks are required for all types registered with [RegisterExtras] for
184+
// [Body] payloads.
185+
type BodyHooks interface {
186+
AppendRLPFields(_ rlp.EncoderBuffer, mustWriteEmptyOptional bool) error
187+
DecodeExtraRLPFields(*rlp.Stream) error
188+
}
189+
190+
// TestOnlyRegisterBodyHooks is a temporary means of "registering" BodyHooks for
191+
// the purpose of testing. It will panic if called outside of a test.
192+
func TestOnlyRegisterBodyHooks(h BodyHooks) {
193+
testonly.OrPanic(func() {
194+
todoRegisteredBodyHooks = h
195+
})
196+
}
197+
198+
// todoRegisteredBodyHooks is a temporary placeholder for "registering"
199+
// BodyHooks, before they are included in [RegisterExtras].
200+
var todoRegisteredBodyHooks BodyHooks = NOOPBodyHooks{}
201+
202+
func (b *Body) hooks() BodyHooks {
203+
// TODO(arr4n): when incorporating BodyHooks into [RegisterExtras], the
204+
// [todoRegisteredBodyHooks] variable MUST be removed.
205+
return todoRegisteredBodyHooks
206+
}
207+
208+
// NOOPBodyHooks implements [BodyHooks] such that they are equivalent to no type
209+
// having been registered.
210+
type NOOPBodyHooks struct{}
112211

113-
func (n *NOOPHeaderHooks) PostCopy(dst *Header) {}
212+
func (NOOPBodyHooks) AppendRLPFields(rlp.EncoderBuffer, bool) error { return nil }
213+
func (NOOPBodyHooks) DecodeExtraRLPFields(*rlp.Stream) error { return nil }

core/types/rlp_backwards_compat.libevm_test.go

Lines changed: 197 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2024 the libevm authors.
1+
// Copyright 2024-2025 the libevm authors.
22
//
33
// The libevm additions to go-ethereum are free software: you can redistribute
44
// them and/or modify them under the terms of the GNU Lesser General Public License
@@ -20,10 +20,14 @@ import (
2020
"encoding/hex"
2121
"testing"
2222

23+
"github.com/google/go-cmp/cmp"
24+
"github.com/kr/pretty"
2325
"github.com/stretchr/testify/assert"
2426
"github.com/stretchr/testify/require"
2527

28+
"github.com/ava-labs/libevm/common"
2629
. "github.com/ava-labs/libevm/core/types"
30+
"github.com/ava-labs/libevm/libevm/cmpeth"
2731
"github.com/ava-labs/libevm/libevm/ethtest"
2832
"github.com/ava-labs/libevm/rlp"
2933
)
@@ -106,3 +110,195 @@ func testHeaderRLPBackwardsCompatibility(t *testing.T) {
106110
assert.Equal(t, hdr, got)
107111
})
108112
}
113+
114+
func TestBodyRLPBackwardsCompatibility(t *testing.T) {
115+
newTx := func(nonce uint64) *Transaction { return NewTx(&LegacyTx{Nonce: nonce}) }
116+
newHdr := func(hashLow byte) *Header { return &Header{ParentHash: common.Hash{hashLow}} }
117+
newWithdraw := func(idx uint64) *Withdrawal { return &Withdrawal{Index: idx} }
118+
119+
// We build up test-case [Body] instances from the power set of each of
120+
// these components.
121+
txMatrix := [][]*Transaction{
122+
nil, {}, // Must be equivalent for non-optional field
123+
{newTx(1)},
124+
{newTx(2), newTx(3)}, // Demonstrates nested lists
125+
}
126+
uncleMatrix := [][]*Header{
127+
nil, {},
128+
{newHdr(1)},
129+
{newHdr(2), newHdr(3)},
130+
}
131+
withdrawMatrix := [][]*Withdrawal{
132+
nil, {}, // Must be different for optional field
133+
{newWithdraw(1)},
134+
{newWithdraw(2), newWithdraw(3)},
135+
}
136+
137+
var bodies []*Body
138+
for _, tx := range txMatrix {
139+
for _, u := range uncleMatrix {
140+
for _, w := range withdrawMatrix {
141+
bodies = append(bodies, &Body{tx, u, w})
142+
}
143+
}
144+
}
145+
146+
for _, body := range bodies {
147+
t.Run("", func(t *testing.T) {
148+
t.Logf("\n%s", pretty.Sprint(body))
149+
150+
// The original [Body] doesn't implement [rlp.Encoder] nor
151+
// [rlp.Decoder] so we can use a methodless equivalent as the gold
152+
// standard.
153+
type withoutMethods Body
154+
wantRLP, err := rlp.EncodeToBytes((*withoutMethods)(body))
155+
require.NoErrorf(t, err, "rlp.EncodeToBytes([%T with methods stripped])", body)
156+
157+
t.Run("Encode", func(t *testing.T) {
158+
got, err := rlp.EncodeToBytes(body)
159+
require.NoErrorf(t, err, "rlp.EncodeToBytes(%#v)", body)
160+
assert.Equalf(t, wantRLP, got, "rlp.EncodeToBytes(%#v)", body)
161+
})
162+
163+
t.Run("Decode", func(t *testing.T) {
164+
got := new(Body)
165+
err := rlp.DecodeBytes(wantRLP, got)
166+
require.NoErrorf(t, err, "rlp.DecodeBytes(%v, %T)", wantRLP, got)
167+
168+
want := body
169+
// Regular RLP decoding will never leave these non-optional
170+
// fields nil.
171+
if want.Transactions == nil {
172+
want.Transactions = []*Transaction{}
173+
}
174+
if want.Uncles == nil {
175+
want.Uncles = []*Header{}
176+
}
177+
178+
opts := cmp.Options{
179+
cmpeth.CompareHeadersByHash(),
180+
cmpeth.CompareTransactionsByBinary(t),
181+
}
182+
if diff := cmp.Diff(body, got, opts); diff != "" {
183+
t.Errorf("rlp.DecodeBytes(rlp.EncodeToBytes(%#v)) diff (-want +got):\n%s", body, diff)
184+
}
185+
})
186+
})
187+
}
188+
}
189+
190+
// cChainBodyExtras carries the same additional fields as the Avalanche C-Chain
191+
// (ava-labs/coreth) [Body] and implements [BodyHooks] to achieve equivalent RLP
192+
// {en,de}coding.
193+
type cChainBodyExtras struct {
194+
Version uint32
195+
ExtData *[]byte
196+
}
197+
198+
var _ BodyHooks = (*cChainBodyExtras)(nil)
199+
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
206+
}
207+
b.WriteBytes(data)
208+
209+
return nil
210+
}
211+
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
226+
}
227+
228+
return nil
229+
}
230+
231+
func TestBodyRLPCChainCompat(t *testing.T) {
232+
// The inputs to this test were used to generate the expected RLP with
233+
// ava-labs/coreth. This serves as both an example of how to use [BodyHooks]
234+
// and a test of compatibility.
235+
236+
t.Cleanup(func() {
237+
TestOnlyRegisterBodyHooks(NOOPBodyHooks{})
238+
})
239+
240+
body := &Body{
241+
Transactions: []*Transaction{
242+
NewTx(&LegacyTx{
243+
Nonce: 42,
244+
To: common.PointerTo(common.HexToAddress(`decafc0ffeebad`)),
245+
}),
246+
},
247+
Uncles: []*Header{ /* RLP encoding differs in ava-labs/coreth */ },
248+
}
249+
250+
const version = 314159
251+
tests := []struct {
252+
name string
253+
extra *cChainBodyExtras
254+
// WARNING: changing these values might break backwards compatibility of
255+
// RLP encoding!
256+
wantRLPHex string
257+
}{
258+
{
259+
extra: &cChainBodyExtras{
260+
Version: version,
261+
},
262+
wantRLPHex: `e5dedd2a80809400000000000000000000000000decafc0ffeebad8080808080c08304cb2f80`,
263+
},
264+
{
265+
extra: &cChainBodyExtras{
266+
Version: version,
267+
ExtData: &[]byte{1, 4, 2, 8, 5, 7},
268+
},
269+
wantRLPHex: `ebdedd2a80809400000000000000000000000000decafc0ffeebad8080808080c08304cb2f86010402080507`,
270+
},
271+
}
272+
273+
for _, tt := range tests {
274+
t.Run(tt.name, func(t *testing.T) {
275+
wantRLP, err := hex.DecodeString(tt.wantRLPHex)
276+
require.NoErrorf(t, err, "hex.DecodeString(%q)", tt.wantRLPHex)
277+
278+
t.Run("Encode", func(t *testing.T) {
279+
TestOnlyRegisterBodyHooks(tt.extra)
280+
got, err := rlp.EncodeToBytes(body)
281+
require.NoErrorf(t, err, "rlp.EncodeToBytes(%+v)", body)
282+
assert.Equalf(t, wantRLP, got, "rlp.EncodeToBytes(%+v)", body)
283+
})
284+
285+
t.Run("Decode", func(t *testing.T) {
286+
var extra cChainBodyExtras
287+
TestOnlyRegisterBodyHooks(&extra)
288+
289+
got := new(Body)
290+
err := rlp.DecodeBytes(wantRLP, got)
291+
require.NoErrorf(t, err, "rlp.DecodeBytes(%#x, %T)", wantRLP, got)
292+
assert.Equal(t, tt.extra, &extra, "rlp.DecodeBytes(%#x, [%T as registered extra in %T carrier])", wantRLP, &extra, got)
293+
294+
opts := cmp.Options{
295+
cmpeth.CompareHeadersByHash(),
296+
cmpeth.CompareTransactionsByBinary(t),
297+
}
298+
if diff := cmp.Diff(body, got, opts); diff != "" {
299+
t.Errorf("rlp.DecodeBytes(%#x, [%T while carrying registered %T extra payload]) diff (-want +got):\n%s", wantRLP, got, &extra, diff)
300+
}
301+
})
302+
})
303+
}
304+
}

0 commit comments

Comments
 (0)