Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
97 changes: 68 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,80 @@ 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,
}
bodyFields := body.hooks().BodyRLPFieldsForEncoding(&body)
blockFields := rlp.Fields{
Required: append(
[]any{b.Header},
bodyFields.Required...,
),
Optional: bodyFields.Optional,
}
return blockFields.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.
extra: b.extra,
Comment on lines +130 to +132

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This was really the annoying part. It's kind of weird, since I don't think an implementation should actually use this... But I think we should guarantee that it is provided correctly. Thoughts on simplifying this @ARR4N?

}
bodyFields := body.hooks().BodyRLPFieldPointersForDecoding(&body)
blockFields := rlp.Fields{
Required: append(
[]any{&b.Header},
bodyFields.Required...,
),
Optional: bodyFields.Optional,
}
if err := blockFields.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(header, body rlp.RawValue) (rlp.RawValue, error) {
bodyFields, _, err := rlp.SplitList(body)
if err != nil {
return nil, fmt.Errorf("splitting body: %w", err)
}

w := rlp.NewEncoderBuffer(nil)
l := w.List()
if _, err := w.Write(header); 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)
block := w.ToBytes()
return block, w.Flush() // Flush returns the internal buffer to the pool.
}

// 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 +205,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
177 changes: 174 additions & 3 deletions core/types/block.libevm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,13 +207,27 @@ func TestHeaderHooks(t *testing.T) {

type blockPayload struct {
NOOPBlockBodyHooks
x int
x uint64
}

func (p *blockPayload) Copy() *blockPayload {
return &blockPayload{x: p.x}
}

func (p *blockPayload) BodyRLPFieldsForEncoding(b *Body) *rlp.Fields {
return &rlp.Fields{
Required: []any{b.Transactions, b.Uncles, p.x},
Optional: []any{b.Withdrawals},
}
}

func (p *blockPayload) BodyRLPFieldPointersForDecoding(b *Body) *rlp.Fields {
return &rlp.Fields{
Required: []any{&b.Transactions, &b.Uncles, &p.x},
Optional: []any{&b.Withdrawals},
}
}

func TestBlockWithX(t *testing.T) {
TestOnlyClearRegisteredExtras()
t.Cleanup(TestOnlyClearRegisteredExtras)
Expand All @@ -224,15 +238,15 @@ func TestBlockWithX(t *testing.T) {
struct{},
]()

typ := reflect.TypeOf(&Block{})
typ := reflect.TypeFor[*Block]()

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Not required - but my IDE lints these... So figured I might as well modernize this while I was here.

for i := 0; i < typ.NumMethod(); i++ {
method := typ.Method(i).Name
if method == "Withdrawals" || !strings.HasPrefix(method, "With") {
continue
}

block := NewBlockWithHeader(&Header{})
const initialPayload = int(42)
const initialPayload uint64 = 42
payload := &blockPayload{
x: initialPayload,
}
Expand Down Expand Up @@ -272,3 +286,160 @@ func TestBlockWithX(t *testing.T) {
})
}
}

// TestBodyExtraRoundTrip demonstrates that the body extra's round-trip
// correctly through RLP serialization.
func TestBodyExtraRoundTrip(t *testing.T) {
TestOnlyClearRegisteredExtras()
t.Cleanup(TestOnlyClearRegisteredExtras)

extras := RegisterExtras[
NOOPHeaderHooks, *NOOPHeaderHooks,
blockPayload, *blockPayload,
struct{},
]()

rng := ethtest.NewPseudoRand(142857)
wantBlock := NewBlockWithHeader(newHeader(rng))
want := extras.Block.Get(wantBlock)
want.x = rng.Uint64()

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{}
for range size.txs {
body.Transactions = append(body.Transactions, NewTx(&LegacyTx{
Nonce: rng.Uint64(),
GasPrice: rng.BigUint64(),
Gas: rng.Uint64(),
To: rng.AddressPtr(),
Value: rng.BigUint64(),
Data: rng.Bytes(64),
}))
}
for range size.uncles {
body.Uncles = append(body.Uncles, newHeader(rng))
}
for range size.withdrawals {
body.Withdrawals = append(body.Withdrawals, &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) {
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 b.Loop() {
_, _ = BlockBytes(headerBytes, bodyBytes)
}
})
}
}
7 changes: 7 additions & 0 deletions core/types/rlp_payload.libevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,13 @@ func (b *Block) cloneExtra() *pseudo.Type {
return nil
}

func (b *Block) extraOrNil() *pseudo.Type {
if registeredExtras.Registered() {
return b.extraPayload()
}
return nil
}

// StateOrSlimAccount is implemented by both [StateAccount] and [SlimAccount],
// allowing for their [StateAccountExtra] payloads to be accessed in a type-safe
// manner by [ExtraPayloads] instances.
Expand Down
Loading
Loading