diff --git a/common/pointer.libevm.go b/common/pointer.libevm.go new file mode 100644 index 000000000000..c51b930ee31c --- /dev/null +++ b/common/pointer.libevm.go @@ -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 +// . + +package common + +// PointerTo is a convenience wrapper for creating a pointer to a value. +func PointerTo[T any](x T) *T { return &x } diff --git a/core/types/block.libevm.go b/core/types/block.libevm.go index 9258eefac799..094613ee1e33 100644 --- a/core/types/block.libevm.go +++ b/core/types/block.libevm.go @@ -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 @@ -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" ) @@ -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 } diff --git a/core/types/rlp_backwards_compat.libevm_test.go b/core/types/rlp_backwards_compat.libevm_test.go index 8b22eac29752..09cff5bd1e14 100644 --- a/core/types/rlp_backwards_compat.libevm_test.go +++ b/core/types/rlp_backwards_compat.libevm_test.go @@ -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)) + + // 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) + } + }) + }) + } +} diff --git a/libevm/cmpeth/cmpeth.go b/libevm/cmpeth/cmpeth.go new file mode 100644 index 000000000000..8d17e3e51921 --- /dev/null +++ b/libevm/cmpeth/cmpeth.go @@ -0,0 +1,66 @@ +// 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 cmpeth provides ETH-specific options for the cmp package. +package cmpeth + +import ( + "bytes" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/ava-labs/libevm/core/types" +) + +// CompareHeadersByHash returns an option to compare Headers based on +// [types.Header.Hash] equality. +func CompareHeadersByHash() cmp.Option { + return cmp.Comparer(func(a, b *types.Header) bool { + return a.Hash() == b.Hash() + }) +} + +// CompareTransactionsByBinary returns an option to compare Transactions based +// on [types.Transaction.MarshalBinary] equality. Two nil pointers are +// considered equal. +// +// If MarshalBinary() returns an error, it will be reported with +// [testing.TB.Fatal]. +func CompareTransactionsByBinary(tb testing.TB) cmp.Option { + tb.Helper() + return cmp.Comparer(func(a, b *types.Transaction) bool { + tb.Helper() + + if a == nil && b == nil { + return true + } + if a == nil || b == nil { + return false + } + + return bytes.Equal(marshalTxBinary(tb, a), marshalTxBinary(tb, b)) + }) +} + +func marshalTxBinary(tb testing.TB, tx *types.Transaction) []byte { + tb.Helper() + buf, err := tx.MarshalBinary() + if err != nil { + tb.Fatalf("%T.MarshalBinary() error %v", tx, err) + } + return buf +} diff --git a/rlp/list.libevm.go b/rlp/list.libevm.go new file mode 100644 index 000000000000..2fd74ce645d6 --- /dev/null +++ b/rlp/list.libevm.go @@ -0,0 +1,77 @@ +// 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 + +// InList is a convenience wrapper, calling `fn` between calls to +// [EncoderBuffer.List] and [EncoderBuffer.ListEnd]. If `fn` returns an error, +// it is propagated directly. +func (b EncoderBuffer) InList(fn func() error) error { + l := b.List() + if err := fn(); err != nil { + return err + } + b.ListEnd(l) + return nil +} + +// EncodeListToBuffer is equivalent to [Encode], writing the RLP encoding of +// each element to `b`, except that it wraps the writes inside a call to +// [EncoderBuffer.InList]. +func EncodeListToBuffer[T any](b EncoderBuffer, vals []T) error { + return b.InList(func() error { + for _, v := range vals { + if err := Encode(b, v); err != nil { + return err + } + } + return 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. +func (s *Stream) FromList(fn func() error) error { + if _, err := s.List(); err != nil { + return err + } + if err := fn(); err != nil { + return err + } + return s.ListEnd() +} + +// DecodeList assumes that the next item in `s` is a list and decodes every item +// in said list to a `*T`. +// +// The returned slice is guaranteed to be non-nil, even if the list is empty. +// This is in keeping with other behaviour in this package and it is therefore +// the responsibility of callers to respect `rlp:"nil"` struct tags. +func DecodeList[T any](s *Stream) ([]*T, error) { + vals := []*T{} + err := s.FromList(func() error { + for s.MoreDataInList() { + var v T + if err := s.Decode(&v); err != nil { + return err + } + vals = append(vals, &v) + } + return nil + }) + return vals, err +} diff --git a/rlp/list.libevm_test.go b/rlp/list.libevm_test.go new file mode 100644 index 000000000000..8e2c76711c95 --- /dev/null +++ b/rlp/list.libevm_test.go @@ -0,0 +1,56 @@ +// 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" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEncodeListToBuffer(t *testing.T) { + vals := []uint{1, 2, 3, 4, 5} + + want, err := EncodeToBytes(vals) + require.NoErrorf(t, err, "EncodeToBytes(%T{%[1]v})", vals) + + var got bytes.Buffer + buf := NewEncoderBuffer(&got) + err = EncodeListToBuffer(buf, vals) + require.NoErrorf(t, err, "EncodeListToBuffer(..., %T{%[1]v})", vals) + require.NoErrorf(t, buf.Flush(), "%T.Flush()", buf) + + assert.Equal(t, want, got.Bytes(), "EncodeListToBuffer(..., %T{%[1]v})", vals) +} + +func TestDecodeList(t *testing.T) { + vals := []uint{0, 1, 42, 314159} + + rlp, err := EncodeToBytes(vals) + require.NoErrorf(t, err, "EncodeToBytes(%T{%[1]v})", vals) + + s := NewStream(bytes.NewReader(rlp), 0) + got, err := DecodeList[uint](s) + require.NoErrorf(t, err, "DecodeList[%T]()", vals[0]) + + require.Equal(t, len(vals), len(got), "number of values returned by DecodeList()") + for i, gotPtr := range got { + assert.Equalf(t, vals[i], *gotPtr, "DecodeList()[%d]", i) + } +}