-
Notifications
You must be signed in to change notification settings - Fork 3
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
Changes from all commits
dd502c5
95828a3
d5b6b82
1efa2eb
05463ce
7f6289d
c114751
f61ec75
069b026
3fac457
0eed36d
1ebcae8
43eb635
6e24dbd
3367753
683245c
51fe62b
d509bd2
77188ef
f072b4d
b3ce131
be3132a
c07df8d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the overwhelming number of cases I do this (in the |
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 | ||
|
@@ -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" | ||
) | ||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit there is no point running this in a subtest then really There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It limits the effects of |
||
|
||
// 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{} | ||
} | ||
qdm12 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
opts := cmp.Options{ | ||
cmpeth.CompareHeadersByHash(), | ||
cmpeth.CompareTransactionsByBinary(t), | ||
ARR4N marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
if diff := cmp.Diff(body, got, opts); diff != "" { | ||
t.Errorf("rlp.DecodeBytes(rlp.EncodeToBytes(%#v)) diff (-want +got):\n%s", body, diff) | ||
} | ||
ARR4N marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}) | ||
}) | ||
} | ||
} | ||
|
||
// 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 { | ||
ARR4N marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) | ||
} | ||
}) | ||
}) | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.