Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
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
8 changes: 0 additions & 8 deletions core/types/backwards_compat.libevm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,14 +240,6 @@ func (e *cChainBodyExtras) Copy() *cChainBodyExtras {
panic("unimplemented")
}

func (e *cChainBodyExtras) BlockRLPFieldsForEncoding(b *BlockRLPProxy) *rlp.Fields {
panic("unimplemented")
}

func (e *cChainBodyExtras) BlockRLPFieldPointersForDecoding(b *BlockRLPProxy) *rlp.Fields {
panic("unimplemented")
}

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]
Expand Down
6 changes: 3 additions & 3 deletions core/types/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ type extblock struct {
Uncles []*Header
Withdrawals []*Withdrawal `rlp:"optional"`

hooks BlockBodyHooks // libevm: MUST be unexported + populated from [Block.hooks]
extra *pseudo.Type // libevm: MUST be unexported + populated from [Block.extraOrNil]
}

// NewBlock creates a new block. The input data is copied, changes to header and to the
Expand Down Expand Up @@ -322,7 +322,7 @@ func CopyHeader(h *Header) *Header {
// DecodeRLP decodes a block from RLP.
func (b *Block) DecodeRLP(s *rlp.Stream) error {
var eb extblock
eb.hooks = b.hooks()
eb.extra = b.extraOrNil()
_, size, _ := s.Kind()
if err := s.Decode(&eb); err != nil {
return err
Expand All @@ -339,7 +339,7 @@ func (b *Block) EncodeRLP(w io.Writer) error {
Txs: b.transactions,
Uncles: b.uncles,
Withdrawals: b.withdrawals,
hooks: b.hooks(),
extra: b.extraOrNil(),
})
}

Expand Down
91 changes: 62 additions & 29 deletions core/types/block.libevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package types

import (
"encoding/json"
"fmt"
"io"

"github.com/ava-labs/libevm/internal/libevm/pseudo"
Expand Down Expand Up @@ -106,25 +107,74 @@ func (b *Body) DecodeRLP(s *rlp.Stream) error {
return b.hooks().BodyRLPFieldPointersForDecoding(b).DecodeRLP(s)
}

// BlockRLPProxy exports the geth-internal type used for RLP {en,de}coding of a
// [Block].
type BlockRLPProxy extblock

func (b *extblock) EncodeRLP(w io.Writer) error {
bb := (*BlockRLPProxy)(b)
return b.hooks.BlockRLPFieldsForEncoding(bb).EncodeRLP(w)
body := Body{
Transactions: b.Txs,
Uncles: b.Uncles,
Withdrawals: b.Withdrawals,
extra: b.extra,
}
fields := body.hooks().BodyRLPFieldsForEncoding(&body)
fields.Required = append(
[]any{b.Header},
fields.Required...,
)
return fields.EncodeRLP(w)
}

func (b *extblock) DecodeRLP(s *rlp.Stream) error {
bb := (*BlockRLPProxy)(b)
return b.hooks.BlockRLPFieldPointersForDecoding(bb).DecodeRLP(s)
body := Body{
// The body provided to the hooks is expected to contain the same extra
// as the method receiver.
Comment thread
StephenButtolph marked this conversation as resolved.
Outdated
extra: b.extra,
Comment thread
StephenButtolph marked this conversation as resolved.
Outdated
}
fields := body.hooks().BodyRLPFieldPointersForDecoding(&body)
fields.Required = append(
[]any{&b.Header},
fields.Required...,
)
if err := fields.DecodeRLP(s); err != nil {
return err
}
b.Txs = body.Transactions
b.Uncles = body.Uncles
b.Withdrawals = body.Withdrawals
return nil
}

// BlockBytes combines an RLP encoded [Header] and [Body] into an RLP encoded
// [Block].
//
// For correctly formatted inputs it is a faster equivalent of:
// - Decoding into a [Header] and [Body]
// - Combining them into a Block
// - Encoding the Block
//
// This function does NOT validate the header or body.
func BlockBytes(headerBytes, bodyBytes []byte) ([]byte, error) {
bodyFields, _, err := rlp.SplitList(bodyBytes)
if err != nil {
return nil, fmt.Errorf("splitting body: %w", err)
}

Comment thread
StephenButtolph marked this conversation as resolved.
Outdated
w := rlp.NewEncoderBuffer(nil)
l := w.List()
if _, err := w.Write(headerBytes); err != nil {
return nil, fmt.Errorf("writing header: %w", err)
}
if _, err := w.Write(bodyFields); err != nil {
return nil, fmt.Errorf("writing body: %w", err)
}
w.ListEnd(l)
blockBytes := w.ToBytes()
return blockBytes, w.Flush() // Flush returns the internal buffer to the pool.
Comment thread
StephenButtolph marked this conversation as resolved.
Outdated
}

// BlockBodyHooks are required for all types registered with [RegisterExtras]
// for [Block] and [Body] payloads.
// for [Block] and [Body] payloads. The same methods are used for both [Block]
// and [Body] {en,de}coding as a Block is encoded as its [Header] followed by
// the fields of its [Body].
type BlockBodyHooks interface {
BlockRLPFieldsForEncoding(*BlockRLPProxy) *rlp.Fields
BlockRLPFieldPointersForDecoding(*BlockRLPProxy) *rlp.Fields
BodyRLPFieldsForEncoding(*Body) *rlp.Fields
BodyRLPFieldPointersForDecoding(*Body) *rlp.Fields
Comment thread
StephenButtolph marked this conversation as resolved.
PostRPCMarshal(b *Block, marshalled map[string]any)
Expand All @@ -149,27 +199,10 @@ var (
}
_ = extblock{
&Header{}, []*Transaction{}, []*Header{}, []*Withdrawal{}, // geth
BlockBodyHooks(nil), // libevm
&pseudo.Type{}, // libevm
}
// Demonstrate identity of these two types, by definition but useful for
// inspection here.
_ = extblock(BlockRLPProxy{})
)

func (NOOPBlockBodyHooks) BlockRLPFieldsForEncoding(b *BlockRLPProxy) *rlp.Fields {
return &rlp.Fields{
Required: []any{b.Header, b.Txs, b.Uncles},
Optional: []any{b.Withdrawals},
}
}

func (NOOPBlockBodyHooks) BlockRLPFieldPointersForDecoding(b *BlockRLPProxy) *rlp.Fields {
return &rlp.Fields{
Required: []any{&b.Header, &b.Txs, &b.Uncles},
Optional: []any{&b.Withdrawals},
}
}

func (NOOPBlockBodyHooks) BodyRLPFieldsForEncoding(b *Body) *rlp.Fields {
return &rlp.Fields{
Required: []any{b.Transactions, b.Uncles},
Expand Down
213 changes: 213 additions & 0 deletions core/types/block.libevm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"fmt"
"io"
"reflect"
"slices"
"strings"
"testing"

Expand All @@ -33,6 +34,7 @@ import (
"github.com/ava-labs/libevm/internal/libevm/pseudo"
"github.com/ava-labs/libevm/libevm/ethtest"
"github.com/ava-labs/libevm/rlp"
"github.com/ava-labs/libevm/trie"
)

type stubHeaderHooks struct {
Expand Down Expand Up @@ -272,3 +274,214 @@ func TestBlockWithX(t *testing.T) {
})
}
}

// rlpBodyPayload is a [BlockBodyHooks] implementation carrying an extra field
// that is {en,de}coded as if they were regular RLP fields of both the [Block]
// and [Body].
type rlpBodyPayload struct {
Data []byte

NOOPBlockBodyHooks
}

func (p *rlpBodyPayload) Copy() *rlpBodyPayload {
return &rlpBodyPayload{
Data: slices.Clone(p.Data),
}
}

var rlpBodyPayloads pseudo.Accessor[*Body, *rlpBodyPayload]

func (*rlpBodyPayload) BodyRLPFieldsForEncoding(b *Body) *rlp.Fields {
// Rather than using the receiver directly, we access it through b. This
// demonstrates that the hooks can access their own payload via the
// [types.Body] they are passed.
Comment thread
StephenButtolph marked this conversation as resolved.
Outdated
p := rlpBodyPayloads.Get(b)
return &rlp.Fields{
Required: []any{b.Transactions, b.Uncles, p.Data},
Optional: []any{b.Withdrawals},
}
}

func (*rlpBodyPayload) BodyRLPFieldPointersForDecoding(b *Body) *rlp.Fields {
// See above comment on why we access the receiver through b rather than
// directly.
Comment thread
StephenButtolph marked this conversation as resolved.
Outdated
p := rlpBodyPayloads.Get(b)
return &rlp.Fields{
Required: []any{&b.Transactions, &b.Uncles, &p.Data},
Optional: []any{&b.Withdrawals},
}
}

// TestBodyExtraEqual demonstrates that the extra payload implementation of
// [types.BlockBodyHooks] is the same as the payload included in the
// [types.Body] when RLP encoding and decoding a block..
func TestBlockBodyPayloadRLPRoundTrip(t *testing.T) {
TestOnlyClearRegisteredExtras()
t.Cleanup(TestOnlyClearRegisteredExtras)

extras := RegisterExtras[
NOOPHeaderHooks, *NOOPHeaderHooks,
rlpBodyPayload, *rlpBodyPayload,
struct{},
]()
rlpBodyPayloads = extras.Body

rng := ethtest.NewPseudoRand(142857)
wantBlock := NewBlock(
&Header{ParentHash: rng.Hash()},
[]*Transaction{
NewTx(&LegacyTx{Nonce: rng.Uint64()}),
},
[]*Header{
{ParentHash: rng.Hash()},
},
nil,
Comment thread
ARR4N marked this conversation as resolved.
Outdated
trie.NewStackTrie(nil),
)
want := extras.Block.Get(wantBlock)
want.Data = rng.Bytes(8)

b, err := rlp.EncodeToBytes(wantBlock)
require.NoErrorf(t, err, "rlp.EncodeToBytes(%T)", wantBlock)

gotBlock := new(Block)
require.NoErrorf(t, rlp.DecodeBytes(b, gotBlock), "rlp.DecodeBytes(rlp.EncodeToBytes(%T), %T)", wantBlock, gotBlock)
got := extras.Block.Get(gotBlock)
assert.Equalf(t, want, got, "%T payload after RLP round trip", got)
}

// newHeader returns a [Header] with randomly populated fields.
func newHeader(rng *ethtest.PseudoRand) *Header {
return &Header{
ParentHash: rng.Hash(),
UncleHash: rng.Hash(),
Coinbase: rng.Address(),
Root: rng.Hash(),
TxHash: rng.Hash(),
ReceiptHash: rng.Hash(),
Bloom: rng.Bloom(),
Difficulty: rng.BigUint64(),
Number: rng.BigUint64(),
GasLimit: rng.Uint64(),
GasUsed: rng.Uint64(),
Time: rng.Uint64(),
Extra: rng.Bytes(32),
MixDigest: rng.Hash(),
Nonce: rng.BlockNonce(),
BaseFee: rng.BigUint64(),
}
}

// bodySize describes the number of items in the [Body] of a test case.
type bodySize struct {
txs, uncles, withdrawals int
}

// bodySizes are the [Body] shapes covered by [FuzzBlockBytes] seeds and by
// [BenchmarkBlockBytes]. They cover every combination of empty and non-empty
// fields, the last of which is optional in RLP.
var bodySizes = []bodySize{
{txs: 0, uncles: 0, withdrawals: 0},
{txs: 1, uncles: 0, withdrawals: 0},
{txs: 0, uncles: 1, withdrawals: 0},
{txs: 0, uncles: 0, withdrawals: 1},
{txs: 10, uncles: 0, withdrawals: 0},
{txs: 10, uncles: 2, withdrawals: 4},
{txs: 100, uncles: 0, withdrawals: 0},
{txs: 100, uncles: 2, withdrawals: 16},
}

// newBody returns a [Body] with randomly populated fields, holding the number
// of items described by `size`.
func newBody(rng *ethtest.PseudoRand, size bodySize) *Body {
body := &Body{
Transactions: make([]*Transaction, size.txs),
Uncles: make([]*Header, size.uncles),
Withdrawals: make([]*Withdrawal, size.withdrawals),
}
for i := range size.txs {
body.Transactions[i] = NewTx(&LegacyTx{
Nonce: rng.Uint64(),
GasPrice: rng.BigUint64(),
Gas: rng.Uint64(),
To: rng.AddressPtr(),
Value: rng.BigUint64(),
Data: rng.Bytes(64),
})
}
for i := range size.uncles {
body.Uncles[i] = newHeader(rng)
}
for i := range size.withdrawals {
body.Withdrawals[i] = &Withdrawal{
Index: rng.Uint64(),
Validator: rng.Uint64(),
Address: rng.Address(),
Amount: rng.Uint64(),
}
}
Comment thread
StephenButtolph marked this conversation as resolved.
return body
}

// encodeRLP RLP-encodes `v`, failing the test if it can't be encoded.
func encodeRLP(tb testing.TB, v any) []byte {
tb.Helper()
b, err := rlp.EncodeToBytes(v)
require.NoErrorf(tb, err, "rlp.EncodeToBytes(%T)", v)
return b
}

// referenceBlockBytes is the reference implementation against which
// [BlockBytes] is tested and benchmarked.
func referenceBlockBytes(headerBytes, bodyBytes []byte) ([]byte, error) {
Comment thread
StephenButtolph marked this conversation as resolved.
Outdated
header := new(Header)
if err := rlp.DecodeBytes(headerBytes, header); err != nil {
return nil, err
}
body := new(Body)
if err := rlp.DecodeBytes(bodyBytes, body); err != nil {
return nil, err
}
block := NewBlockWithHeader(header).
WithBody(*body).
WithWithdrawals(body.Withdrawals)
return rlp.EncodeToBytes(block)
}

// FuzzBlockBytes demonstrates that [BlockBytes] is equivalent to
// [referenceBlockBytes] for all inputs that the latter accepts. The seed corpus
// covers every shape in [bodySizes].
func FuzzBlockBytes(f *testing.F) {
rng := ethtest.NewPseudoRand(20250806)
for _, size := range bodySizes {
f.Add(
encodeRLP(f, newHeader(rng)),
encodeRLP(f, newBody(rng, size)),
)
}

f.Fuzz(func(t *testing.T, headerBytes, bodyBytes []byte) {
want, err := referenceBlockBytes(headerBytes, bodyBytes)
if err != nil {
t.Skip("invalid input bytes")
}

got, err := BlockBytes(headerBytes, bodyBytes)
require.NoError(t, err, "BlockBytes()")
assert.Equal(t, want, got, "referenceBlockBytes() == BlockBytes()")
})
}

func BenchmarkBlockBytes(b *testing.B) {
for _, size := range bodySizes {
rng := ethtest.NewPseudoRand(2718281828)
headerBytes := encodeRLP(b, newHeader(rng))
bodyBytes := encodeRLP(b, newBody(rng, size))
b.Run(fmt.Sprintf("%d_txs_%d_uncles_%d_withdrawals", size.txs, size.uncles, size.withdrawals), func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = BlockBytes(headerBytes, bodyBytes)
}
})
}
}
Loading