diff --git a/core/types/backwards_compat.libevm_test.go b/core/types/backwards_compat.libevm_test.go index 0c20a83f35c7..34601ca88496 100644 --- a/core/types/backwards_compat.libevm_test.go +++ b/core/types/backwards_compat.libevm_test.go @@ -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] diff --git a/core/types/block.go b/core/types/block.go index a065961fdf91..6cacb0916d37 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -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 @@ -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 @@ -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(), }) } diff --git a/core/types/block.libevm.go b/core/types/block.libevm.go index 65ba5654a072..3a975f1d31ef 100644 --- a/core/types/block.libevm.go +++ b/core/types/block.libevm.go @@ -18,6 +18,7 @@ package types import ( "encoding/json" + "fmt" "io" "github.com/ava-labs/libevm/internal/libevm/pseudo" @@ -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, + } + 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 PostRPCMarshal(b *Block, marshalled map[string]any) @@ -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}, diff --git a/core/types/block.libevm_test.go b/core/types/block.libevm_test.go index 2d0c885298a5..c5294cf708d2 100644 --- a/core/types/block.libevm_test.go +++ b/core/types/block.libevm_test.go @@ -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) @@ -224,7 +238,7 @@ func TestBlockWithX(t *testing.T) { struct{}, ]() - typ := reflect.TypeOf(&Block{}) + typ := reflect.TypeFor[*Block]() for i := 0; i < typ.NumMethod(); i++ { method := typ.Method(i).Name if method == "Withdrawals" || !strings.HasPrefix(method, "With") { @@ -232,7 +246,7 @@ func TestBlockWithX(t *testing.T) { } block := NewBlockWithHeader(&Header{}) - const initialPayload = int(42) + const initialPayload uint64 = 42 payload := &blockPayload{ x: initialPayload, } @@ -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(), + }) + } + 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) + } + }) + } +} diff --git a/core/types/rlp_payload.libevm.go b/core/types/rlp_payload.libevm.go index ba13666a5284..647207e8730e 100644 --- a/core/types/rlp_payload.libevm.go +++ b/core/types/rlp_payload.libevm.go @@ -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. diff --git a/core/types/tempextras.libevm_test.go b/core/types/tempextras.libevm_test.go index b01b78021385..9041c85633ea 100644 --- a/core/types/tempextras.libevm_test.go +++ b/core/types/tempextras.libevm_test.go @@ -35,7 +35,7 @@ func (b *tempBlockBodyHooks) Copy() *tempBlockBodyHooks { return &tempBlockBodyHooks{X: b.X} } -func (b *tempBlockBodyHooks) BlockRLPFieldsForEncoding(*BlockRLPProxy) *rlp.Fields { +func (b *tempBlockBodyHooks) BodyRLPFieldsForEncoding(*Body) *rlp.Fields { return &rlp.Fields{ Required: []any{b.X}, } @@ -62,12 +62,12 @@ func TestTempRegisteredExtras(t *testing.T) { err := libevm.WithTemporaryExtrasLock(func(lock libevm.ExtrasLock) error { return WithTempRegisteredExtras(lock, func(extras ExtraPayloads[*NOOPHeaderHooks, *tempBlockBodyHooks, bool]) error { const val = "Hello, world" - b := new(Block) + b := new(Body) payload := &tempBlockBodyHooks{X: val} - extras.Block.Set(b, payload) + extras.Body.Set(b, payload) got, err := rlp.EncodeToBytes(b) - require.NoErrorf(t, err, "rlp.EncodeToBytes(%T) with %T hooks", b, extras.Block.Get(b)) + require.NoErrorf(t, err, "rlp.EncodeToBytes(%T) with %T hooks", b, extras.Body.Get(b)) want, err := rlp.EncodeToBytes([]string{val}) require.NoErrorf(t, err, "rlp.EncodeToBytes(%T{%[1]v})", []string{val})