Skip to content

Commit 3ab3cd2

Browse files
ARR4Nqdm12
andauthored
feat(core/types): Block RLP overriding (#133)
## Why this should be merged Support for configurable `core/types.Block` with RLP encoding, including interplay with `Body`. ## How this works `Block` doesn't export most of its fields so relies on an internal type, `extblock`, for RLP encoding. This type is modified to implement the `rlp.Encoder` and `Decoder` methods as a point to inject hooks using `rlp.Fields` (as in #120 for `Body`). `Block` shares the same registered extra type as `Body`. Unlike `Header`, which has its own field in a `Block`, the fields in `Body` are promoted to be carried directly. This suggests that (at least for pure data payloads) the modifications might be equivalent (and `ava-labs/coreth` evidences this). Should different payloads be absolutely required in the future, we can split the types—the `RegisterExtras` signature is already too verbose though 😢. ## How this was tested Explicit inclusion of a backwards-compatibility test for `NOOPBlockBodyHooks` + implicit testing via the multiple upstream tests in `block_test.go`. Re implicit testing: default behaviour is now to use the noop hooks even when no registration is performed, but if we change this then the tests in `block_test.go` can still be called as subtests from a test that explicitly registers noops. --------- Signed-off-by: Arran Schlosberg <[email protected]> Co-authored-by: Quentin McGaw <[email protected]>
1 parent 0eb029a commit 3ab3cd2

10 files changed

+354
-65
lines changed

core/state/state.libevm_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func TestGetSetExtra(t *testing.T) {
4747
// test deep copying.
4848
payloads := types.RegisterExtras[
4949
types.NOOPHeaderHooks, *types.NOOPHeaderHooks,
50-
types.NOOPBodyHooks, *types.NOOPBodyHooks,
50+
types.NOOPBlockBodyHooks, *types.NOOPBlockBodyHooks,
5151
*accountExtra,
5252
]().StateAccount
5353

core/state/state_object.libevm_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func TestStateObjectEmpty(t *testing.T) {
4848
registerAndSet: func(acc *types.StateAccount) {
4949
types.RegisterExtras[
5050
types.NOOPHeaderHooks, *types.NOOPHeaderHooks,
51-
types.NOOPBodyHooks, *types.NOOPBodyHooks,
51+
types.NOOPBlockBodyHooks, *types.NOOPBlockBodyHooks,
5252
bool,
5353
]().StateAccount.Set(acc, false)
5454
},
@@ -59,7 +59,7 @@ func TestStateObjectEmpty(t *testing.T) {
5959
registerAndSet: func(*types.StateAccount) {
6060
types.RegisterExtras[
6161
types.NOOPHeaderHooks, *types.NOOPHeaderHooks,
62-
types.NOOPBodyHooks, *types.NOOPBodyHooks,
62+
types.NOOPBlockBodyHooks, *types.NOOPBlockBodyHooks,
6363
bool,
6464
]()
6565
},
@@ -70,7 +70,7 @@ func TestStateObjectEmpty(t *testing.T) {
7070
registerAndSet: func(acc *types.StateAccount) {
7171
types.RegisterExtras[
7272
types.NOOPHeaderHooks, *types.NOOPHeaderHooks,
73-
types.NOOPBodyHooks, *types.NOOPBodyHooks,
73+
types.NOOPBlockBodyHooks, *types.NOOPBlockBodyHooks,
7474
bool,
7575
]().StateAccount.Set(acc, true)
7676
},

core/types/backwards_compat.libevm_test.go

Lines changed: 109 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ import (
3030
"github.com/ava-labs/libevm/rlp"
3131
)
3232

33-
func TestBodyRLPBackwardsCompatibility(t *testing.T) {
34-
newTx := func(nonce uint64) *Transaction { return NewTx(&LegacyTx{Nonce: nonce}) }
35-
newHdr := func(hashLow byte) *Header { return &Header{ParentHash: common.Hash{hashLow}} }
36-
newWithdraw := func(idx uint64) *Withdrawal { return &Withdrawal{Index: idx} }
33+
func newTx(nonce uint64) *Transaction { return NewTx(&LegacyTx{Nonce: nonce}) }
34+
func newHdr(parentHashHigh byte) *Header { return &Header{ParentHash: common.Hash{parentHashHigh}} }
35+
func newWithdraw(idx uint64) *Withdrawal { return &Withdrawal{Index: idx} }
3736

37+
func blockBodyRLPTestInputs() []*Body {
3838
// We build up test-case [Body] instances from the Cartesian product of each
3939
// of these components.
4040
txMatrix := [][]*Transaction{
@@ -61,8 +61,11 @@ func TestBodyRLPBackwardsCompatibility(t *testing.T) {
6161
}
6262
}
6363
}
64+
return bodies
65+
}
6466

65-
for _, body := range bodies {
67+
func TestBodyRLPBackwardsCompatibility(t *testing.T) {
68+
for _, body := range blockBodyRLPTestInputs() {
6669
t.Run("", func(t *testing.T) {
6770
t.Cleanup(func() {
6871
if t.Failed() {
@@ -86,8 +89,10 @@ func TestBodyRLPBackwardsCompatibility(t *testing.T) {
8689
t.Run("Decode", func(t *testing.T) {
8790
got := new(Body)
8891
err := rlp.DecodeBytes(wantRLP, got)
89-
require.NoErrorf(t, err, "rlp.DecodeBytes(rlp.EncodeToBytes(%T), %T) resulted in %s",
90-
(*withoutMethods)(body), got, pretty.Sprint(got))
92+
require.NoErrorf(
93+
t, err, "rlp.DecodeBytes(rlp.EncodeToBytes(%T), %T) resulted in %s",
94+
(*withoutMethods)(body), got, pretty.Sprint(got),
95+
)
9196

9297
want := body
9398
// Regular RLP decoding will never leave these non-optional
@@ -112,17 +117,94 @@ func TestBodyRLPBackwardsCompatibility(t *testing.T) {
112117
}
113118
}
114119

120+
func TestBlockRLPBackwardsCompatibility(t *testing.T) {
121+
TestOnlyClearRegisteredExtras()
122+
t.Cleanup(TestOnlyClearRegisteredExtras)
123+
124+
RegisterExtras[
125+
NOOPHeaderHooks, *NOOPHeaderHooks,
126+
NOOPBlockBodyHooks, *NOOPBlockBodyHooks, // types under test
127+
struct{},
128+
]()
129+
130+
// Note that there are also a number of tests in `block_test.go` that ensure
131+
// backwards compatibility as [NOOPBlockBodyHooks] are used by default when
132+
// nothing is registered (the above registration is only for completeness).
133+
134+
for _, body := range blockBodyRLPTestInputs() {
135+
t.Run("", func(t *testing.T) {
136+
// [Block] doesn't export most of its fields so uses [extblock] as a
137+
// proxy for RLP encoding, which is what we therefore use as the
138+
// backwards-compatible gold standard.
139+
hdr := newHdr(99)
140+
block := extblock{
141+
Header: hdr,
142+
Txs: body.Transactions,
143+
Uncles: body.Uncles,
144+
Withdrawals: body.Withdrawals,
145+
}
146+
147+
// We've added [extblock.EncodeRLP] and [extblock.DecodeRLP] for our
148+
// hooks.
149+
type withoutMethods extblock
150+
151+
wantRLP, err := rlp.EncodeToBytes(withoutMethods(block))
152+
require.NoErrorf(t, err, "rlp.EncodeToBytes([%T with methods stripped])", block)
153+
154+
// Our input to RLP might not be the canonical RLP output.
155+
var wantBlock extblock
156+
err = rlp.DecodeBytes(wantRLP, (*withoutMethods)(&wantBlock))
157+
require.NoErrorf(t, err, "rlp.DecodeBytes(..., [%T with methods stripped])", &wantBlock)
158+
159+
t.Run("Encode", func(t *testing.T) {
160+
b := NewBlockWithHeader(hdr).WithBody(*body).WithWithdrawals(body.Withdrawals)
161+
got, err := rlp.EncodeToBytes(b)
162+
require.NoErrorf(t, err, "rlp.EncodeToBytes(%T)", b)
163+
164+
assert.Equalf(t, wantRLP, got, "expect %T RLP identical to that from %T struct stripped of methods", got, extblock{})
165+
})
166+
167+
t.Run("Decode", func(t *testing.T) {
168+
var gotBlock Block
169+
err := rlp.DecodeBytes(wantRLP, &gotBlock)
170+
require.NoErrorf(t, err, "rlp.DecodeBytes(..., %T)", &gotBlock)
171+
172+
got := extblock{
173+
gotBlock.Header(),
174+
gotBlock.Transactions(),
175+
gotBlock.Uncles(),
176+
gotBlock.Withdrawals(),
177+
nil, // unexported libevm hooks
178+
}
179+
180+
opts := cmp.Options{
181+
cmp.Comparer((*Header).equalHash),
182+
cmp.Comparer((*Transaction).equalHash),
183+
cmpopts.IgnoreUnexported(extblock{}),
184+
}
185+
if diff := cmp.Diff(wantBlock, got, opts); diff != "" {
186+
t.Errorf("rlp.DecodeBytes([RLP from %T stripped of methods], ...) diff (-want +got):\n%s", extblock{}, diff)
187+
}
188+
})
189+
})
190+
}
191+
}
192+
115193
// cChainBodyExtras carries the same additional fields as the Avalanche C-Chain
116-
// (ava-labs/coreth) [Body] and implements [BodyHooks] to achieve equivalent RLP
117-
// {en,de}coding.
194+
// (ava-labs/coreth) [Body] and implements [BlockBodyHooks] to achieve
195+
// equivalent RLP {en,de}coding.
196+
//
197+
// It is not intended as a full test of ava-labs/coreth existing functionality,
198+
// which should be implemented when that module consumes libevm, but as proof of
199+
// equivalence of the [rlp.Fields] approach.
118200
type cChainBodyExtras struct {
119201
Version uint32
120202
ExtData *[]byte
121203
}
122204

123-
var _ BodyHooks = (*cChainBodyExtras)(nil)
205+
var _ BlockBodyHooks = (*cChainBodyExtras)(nil)
124206

125-
func (e *cChainBodyExtras) RLPFieldsForEncoding(b *Body) *rlp.Fields {
207+
func (e *cChainBodyExtras) BodyRLPFieldsForEncoding(b *Body) *rlp.Fields {
126208
// The Avalanche C-Chain uses all of the geth required fields (but none of
127209
// the optional ones) so there's no need to explicitly list them. This
128210
// pattern might not be ideal for readability but is used here for
@@ -132,13 +214,13 @@ func (e *cChainBodyExtras) RLPFieldsForEncoding(b *Body) *rlp.Fields {
132214
// compatibility so this is safe to do, but only for the required fields.
133215
return &rlp.Fields{
134216
Required: append(
135-
NOOPBodyHooks{}.RLPFieldsForEncoding(b).Required,
217+
NOOPBlockBodyHooks{}.BodyRLPFieldsForEncoding(b).Required,
136218
e.Version, e.ExtData,
137219
),
138220
}
139221
}
140222

141-
func (e *cChainBodyExtras) RLPFieldPointersForDecoding(b *Body) *rlp.Fields {
223+
func (e *cChainBodyExtras) BodyRLPFieldPointersForDecoding(b *Body) *rlp.Fields {
142224
// An alternative to the pattern used above is to explicitly list all
143225
// fields for better introspection.
144226
return &rlp.Fields{
@@ -151,6 +233,20 @@ func (e *cChainBodyExtras) RLPFieldPointersForDecoding(b *Body) *rlp.Fields {
151233
}
152234
}
153235

236+
// See [cChainBodyExtras] intent.
237+
238+
func (e *cChainBodyExtras) Copy() *cChainBodyExtras {
239+
panic("unimplemented")
240+
}
241+
242+
func (e *cChainBodyExtras) BlockRLPFieldsForEncoding(b *BlockRLPProxy) *rlp.Fields {
243+
panic("unimplemented")
244+
}
245+
246+
func (e *cChainBodyExtras) BlockRLPFieldPointersForDecoding(b *BlockRLPProxy) *rlp.Fields {
247+
panic("unimplemented")
248+
}
249+
154250
func TestBodyRLPCChainCompat(t *testing.T) {
155251
// The inputs to this test were used to generate the expected RLP with
156252
// ava-labs/coreth. This serves as both an example of how to use [BodyHooks]

core/types/backwards_compat_diffpkg.libevm_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func TestHeaderRLPBackwardsCompatibility(t *testing.T) {
4242
register: func() {
4343
RegisterExtras[
4444
NOOPHeaderHooks, *NOOPHeaderHooks,
45-
NOOPBodyHooks, *NOOPBodyHooks,
45+
NOOPBlockBodyHooks, *NOOPBlockBodyHooks,
4646
struct{},
4747
]()
4848
},

core/types/block.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@ type Block struct {
211211
// inter-peer block relay.
212212
ReceivedAt time.Time
213213
ReceivedFrom interface{}
214+
215+
extra *pseudo.Type // See [RegisterExtras]
214216
}
215217

216218
// "external" block encoding. used for eth protocol, etc.
@@ -219,6 +221,8 @@ type extblock struct {
219221
Txs []*Transaction
220222
Uncles []*Header
221223
Withdrawals []*Withdrawal `rlp:"optional"`
224+
225+
hooks BlockBodyHooks // libevm: MUST be unexported + populated from [Block.hooks]
222226
}
223227

224228
// NewBlock creates a new block. The input data is copied, changes to header and to the
@@ -318,6 +322,7 @@ func CopyHeader(h *Header) *Header {
318322
// DecodeRLP decodes a block from RLP.
319323
func (b *Block) DecodeRLP(s *rlp.Stream) error {
320324
var eb extblock
325+
eb.hooks = b.hooks()
321326
_, size, _ := s.Kind()
322327
if err := s.Decode(&eb); err != nil {
323328
return err
@@ -334,13 +339,14 @@ func (b *Block) EncodeRLP(w io.Writer) error {
334339
Txs: b.transactions,
335340
Uncles: b.uncles,
336341
Withdrawals: b.withdrawals,
342+
hooks: b.hooks(),
337343
})
338344
}
339345

340346
// Body returns the non-header content of the block.
341347
// Note the returned data is not an independent copy.
342348
func (b *Block) Body() *Body {
343-
return &Body{b.transactions, b.uncles, b.withdrawals, nil /* unexported extras field */}
349+
return &Body{b.transactions, b.uncles, b.withdrawals, b.cloneExtra()}
344350
}
345351

346352
// Accessors for body data. These do not return a copy because the content
@@ -458,6 +464,7 @@ func (b *Block) WithSeal(header *Header) *Block {
458464
transactions: b.transactions,
459465
uncles: b.uncles,
460466
withdrawals: b.withdrawals,
467+
extra: b.cloneExtra(),
461468
}
462469
}
463470

@@ -468,6 +475,7 @@ func (b *Block) WithBody(body Body) *Block {
468475
transactions: make([]*Transaction, len(body.Transactions)),
469476
uncles: make([]*Header, len(body.Uncles)),
470477
withdrawals: b.withdrawals,
478+
extra: body.cloneExtra(),
471479
}
472480
copy(block.transactions, body.Transactions)
473481
for i := range body.Uncles {
@@ -482,6 +490,7 @@ func (b *Block) WithWithdrawals(withdrawals []*Withdrawal) *Block {
482490
header: b.header,
483491
transactions: b.transactions,
484492
uncles: b.uncles,
493+
extra: b.cloneExtra(),
485494
}
486495
if withdrawals != nil {
487496
block.withdrawals = make([]*Withdrawal, len(withdrawals))

0 commit comments

Comments
 (0)