Skip to content

feat(core/types): fine-grained Body RLP override #109

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
dd502c5
test(core/types): lock in `Body` RLP encoding
ARR4N Jan 29, 2025
95828a3
feat(core/types): `Body.{En,De}codeRLP()` with hookable points
ARR4N Jan 29, 2025
d5b6b82
test(core/types): use methodless `Body` as gold-standard + add test f…
ARR4N Jan 30, 2025
1efa2eb
fix(core/types): failing tests differentiating nil/empty optional slice
ARR4N Jan 30, 2025
05463ce
test(core/types): fuzz `Body` RLP backwards compatibility
ARR4N Jan 30, 2025
7f6289d
chore: placate the linter
ARR4N Jan 30, 2025
c114751
doc: new functions in `rlp` package
ARR4N Jan 31, 2025
f61ec75
feat: `types.BodyHooks` with test for C-chain compatibility
ARR4N Jan 31, 2025
069b026
chore: placate the linter
ARR4N Jan 31, 2025
3fac457
feat: `cmpeth` package for testing C-chain `Body` RLP decoding
ARR4N Jan 31, 2025
0eed36d
chore: import ordering for `gci`
ARR4N Feb 3, 2025
1ebcae8
Update core/types/rlp_backwards_compat.libevm_test.go
ARR4N Feb 3, 2025
43eb635
Apply suggestions from code review
ARR4N Feb 3, 2025
6e24dbd
refactor: use `rlp.EncoderBuffer.WriteBytes()`
ARR4N Feb 3, 2025
3367753
doc: `types.BodyHooks` and related identifiers
ARR4N Feb 3, 2025
683245c
feat: `common.PointerTo[T](T) *T`
ARR4N Feb 3, 2025
51fe62b
test(rlp): list-helper functions
ARR4N Feb 3, 2025
d509bd2
refactor: direct testing of `Body` RLP roundtrip
ARR4N Feb 4, 2025
77188ef
Update core/types/rlp_backwards_compat.libevm_test.go
ARR4N Feb 4, 2025
f072b4d
Apply suggestions from code review
ARR4N Feb 5, 2025
b3ce131
refactor: response to review comments
ARR4N Feb 5, 2025
be3132a
refactor: remove RNG from compatibility test
ARR4N Feb 5, 2025
c07df8d
Merge branch 'main' into arr4n/body-rlp-override
ARR4N Feb 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions common/pointer.libevm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// 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
// <http://www.gnu.org/licenses/>.

package common

// PointerTo is a convenience wrapper for creating a pointer to a value.
func PointerTo[T any](x T) *T { return &x }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Should we consider adding new code to separate packages when they are not needed for use in the shared package with geth?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the overwhelming number of cases I do this (in the /libevm directory) but see my rationale for adding to common.

104 changes: 102 additions & 2 deletions core/types/block.libevm.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2024 the libevm authors.
// Copyright 2024-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
Expand All @@ -22,6 +22,7 @@ import (
"io"

"github.com/ava-labs/libevm/libevm/pseudo"
"github.com/ava-labs/libevm/libevm/testonly"
"github.com/ava-labs/libevm/rlp"
)

Expand Down Expand Up @@ -109,5 +110,104 @@ func (*NOOPHeaderHooks) DecodeRLP(h *Header, s *rlp.Stream) error {
type withoutMethods Header
return s.Decode((*withoutMethods)(h))
}
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{}}
)

// 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)
})
}

// 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
})
}

// 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
}

// TestOnlyRegisterBodyHooks is a temporary means of "registering" BodyHooks for
// the purpose of testing. It will panic if called outside of a test.
func TestOnlyRegisterBodyHooks(h BodyHooks) {
testonly.OrPanic(func() {
todoRegisteredBodyHooks = h
})
}

// todoRegisteredBodyHooks is a temporary placeholder for "registering"
// BodyHooks, before they are included in [RegisterExtras].
var todoRegisteredBodyHooks BodyHooks = NOOPBodyHooks{}

func (b *Body) hooks() BodyHooks {
// TODO(arr4n): when incorporating BodyHooks into [RegisterExtras], the
// [todoRegisteredBodyHooks] variable MUST be removed.
return todoRegisteredBodyHooks
}

// NOOPBodyHooks implements [BodyHooks] such that they are equivalent to no type
// having been registered.
type NOOPBodyHooks struct{}

func (n *NOOPHeaderHooks) PostCopy(dst *Header) {}
func (NOOPBodyHooks) AppendRLPFields(rlp.EncoderBuffer, bool) error { return nil }
func (NOOPBodyHooks) DecodeExtraRLPFields(*rlp.Stream) error { return nil }
198 changes: 197 additions & 1 deletion core/types/rlp_backwards_compat.libevm_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2024 the libevm authors.
// Copyright 2024-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
Expand All @@ -20,10 +20,14 @@ import (
"encoding/hex"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/kr/pretty"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/ava-labs/libevm/common"
. "github.com/ava-labs/libevm/core/types"
"github.com/ava-labs/libevm/libevm/cmpeth"
"github.com/ava-labs/libevm/libevm/ethtest"
"github.com/ava-labs/libevm/rlp"
)
Expand Down Expand Up @@ -106,3 +110,195 @@ func testHeaderRLPBackwardsCompatibility(t *testing.T) {
assert.Equal(t, hdr, got)
})
}

func TestBodyRLPBackwardsCompatibility(t *testing.T) {
newTx := func(nonce uint64) *Transaction { return NewTx(&LegacyTx{Nonce: nonce}) }
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.
txMatrix := [][]*Transaction{
nil, {}, // Must be equivalent for non-optional field
{newTx(1)},
{newTx(2), newTx(3)}, // Demonstrates nested lists
}
uncleMatrix := [][]*Header{
nil, {},
{newHdr(1)},
{newHdr(2), newHdr(3)},
}
withdrawMatrix := [][]*Withdrawal{
nil, {}, // Must be different for optional field
{newWithdraw(1)},
{newWithdraw(2), newWithdraw(3)},
}

var bodies []*Body
for _, tx := range txMatrix {
for _, u := range uncleMatrix {
for _, w := range withdrawMatrix {
bodies = append(bodies, &Body{tx, u, w})
}
}
}

for _, body := range bodies {
t.Run("", func(t *testing.T) {
t.Logf("\n%s", pretty.Sprint(body))
Comment on lines +147 to +148
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit there is no point running this in a subtest then really

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It limits the effects of t.Fatal and require to only the sub test.


// The original [Body] doesn't implement [rlp.Encoder] nor
// [rlp.Decoder] so we can use a methodless equivalent as the gold
// standard.
type withoutMethods Body
wantRLP, err := rlp.EncodeToBytes((*withoutMethods)(body))
require.NoErrorf(t, err, "rlp.EncodeToBytes([%T with methods stripped])", body)

t.Run("Encode", func(t *testing.T) {
got, err := rlp.EncodeToBytes(body)
require.NoErrorf(t, err, "rlp.EncodeToBytes(%#v)", body)
assert.Equalf(t, wantRLP, got, "rlp.EncodeToBytes(%#v)", body)
})

t.Run("Decode", func(t *testing.T) {
got := new(Body)
err := rlp.DecodeBytes(wantRLP, got)
require.NoErrorf(t, err, "rlp.DecodeBytes(%v, %T)", wantRLP, got)

want := body
// Regular RLP decoding will never leave these non-optional
// fields nil.
if want.Transactions == nil {
want.Transactions = []*Transaction{}
}
if want.Uncles == nil {
want.Uncles = []*Header{}
}

opts := cmp.Options{
cmpeth.CompareHeadersByHash(),
cmpeth.CompareTransactionsByBinary(t),
}
if diff := cmp.Diff(body, got, opts); diff != "" {
t.Errorf("rlp.DecodeBytes(rlp.EncodeToBytes(%#v)) diff (-want +got):\n%s", body, diff)
}
})
})
}
}

// cChainBodyExtras carries the same additional fields as the Avalanche C-Chain
// (ava-labs/coreth) [Body] and implements [BodyHooks] to achieve equivalent RLP
// {en,de}coding.
type cChainBodyExtras struct {
Version uint32
ExtData *[]byte
}

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) 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 TestBodyRLPCChainCompat(t *testing.T) {
// The inputs to this test were used to generate the expected RLP with
// ava-labs/coreth. This serves as both an example of how to use [BodyHooks]
// and a test of compatibility.

t.Cleanup(func() {
TestOnlyRegisterBodyHooks(NOOPBodyHooks{})
})

body := &Body{
Transactions: []*Transaction{
NewTx(&LegacyTx{
Nonce: 42,
To: common.PointerTo(common.HexToAddress(`decafc0ffeebad`)),
}),
},
Uncles: []*Header{ /* RLP encoding differs in ava-labs/coreth */ },
}

const version = 314159
tests := []struct {
name string
extra *cChainBodyExtras
// WARNING: changing these values might break backwards compatibility of
// RLP encoding!
wantRLPHex string
}{
{
extra: &cChainBodyExtras{
Version: version,
},
wantRLPHex: `e5dedd2a80809400000000000000000000000000decafc0ffeebad8080808080c08304cb2f80`,
},
{
extra: &cChainBodyExtras{
Version: version,
ExtData: &[]byte{1, 4, 2, 8, 5, 7},
},
wantRLPHex: `ebdedd2a80809400000000000000000000000000decafc0ffeebad8080808080c08304cb2f86010402080507`,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
wantRLP, err := hex.DecodeString(tt.wantRLPHex)
require.NoErrorf(t, err, "hex.DecodeString(%q)", tt.wantRLPHex)

t.Run("Encode", func(t *testing.T) {
TestOnlyRegisterBodyHooks(tt.extra)
got, err := rlp.EncodeToBytes(body)
require.NoErrorf(t, err, "rlp.EncodeToBytes(%+v)", body)
assert.Equalf(t, wantRLP, got, "rlp.EncodeToBytes(%+v)", body)
})

t.Run("Decode", func(t *testing.T) {
var extra cChainBodyExtras
TestOnlyRegisterBodyHooks(&extra)

got := new(Body)
err := rlp.DecodeBytes(wantRLP, got)
require.NoErrorf(t, err, "rlp.DecodeBytes(%#x, %T)", wantRLP, got)
assert.Equal(t, tt.extra, &extra, "rlp.DecodeBytes(%#x, [%T as registered extra in %T carrier])", wantRLP, &extra, got)

opts := cmp.Options{
cmpeth.CompareHeadersByHash(),
cmpeth.CompareTransactionsByBinary(t),
}
if diff := cmp.Diff(body, got, opts); diff != "" {
t.Errorf("rlp.DecodeBytes(%#x, [%T while carrying registered %T extra payload]) diff (-want +got):\n%s", wantRLP, got, &extra, diff)
}
})
})
}
}
Loading