Skip to content

Commit b2c38ce

Browse files
qdm12ARR4N
andauthored
feat(core/types): body RLP hooks registration (#130)
Allow to register body extras in consumers of libevm. Co-authored-by: Arran Schlosberg <[email protected]>
1 parent 80fbed6 commit b2c38ce

10 files changed

+134
-57
lines changed

core/state/state.libevm_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ func TestGetSetExtra(t *testing.T) {
4545
t.Cleanup(types.TestOnlyClearRegisteredExtras)
4646
// Just as its Data field is a pointer, the registered type is a pointer to
4747
// test deep copying.
48-
payloads := types.RegisterExtras[types.NOOPHeaderHooks, *types.NOOPHeaderHooks, *accountExtra]().StateAccount
48+
payloads := types.RegisterExtras[
49+
types.NOOPHeaderHooks, *types.NOOPHeaderHooks,
50+
types.NOOPBodyHooks, *types.NOOPBodyHooks,
51+
*accountExtra,
52+
]().StateAccount
4953

5054
rng := ethtest.NewPseudoRand(42)
5155
addr := rng.Address()

core/state/state_object.libevm_test.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,21 +46,33 @@ func TestStateObjectEmpty(t *testing.T) {
4646
{
4747
name: "explicit false bool",
4848
registerAndSet: func(acc *types.StateAccount) {
49-
types.RegisterExtras[types.NOOPHeaderHooks, *types.NOOPHeaderHooks, bool]().StateAccount.Set(acc, false)
49+
types.RegisterExtras[
50+
types.NOOPHeaderHooks, *types.NOOPHeaderHooks,
51+
types.NOOPBodyHooks, *types.NOOPBodyHooks,
52+
bool,
53+
]().StateAccount.Set(acc, false)
5054
},
5155
wantEmpty: true,
5256
},
5357
{
5458
name: "implicit false bool",
5559
registerAndSet: func(*types.StateAccount) {
56-
types.RegisterExtras[types.NOOPHeaderHooks, *types.NOOPHeaderHooks, bool]()
60+
types.RegisterExtras[
61+
types.NOOPHeaderHooks, *types.NOOPHeaderHooks,
62+
types.NOOPBodyHooks, *types.NOOPBodyHooks,
63+
bool,
64+
]()
5765
},
5866
wantEmpty: true,
5967
},
6068
{
6169
name: "true bool",
6270
registerAndSet: func(acc *types.StateAccount) {
63-
types.RegisterExtras[types.NOOPHeaderHooks, *types.NOOPHeaderHooks, bool]().StateAccount.Set(acc, true)
71+
types.RegisterExtras[
72+
types.NOOPHeaderHooks, *types.NOOPHeaderHooks,
73+
types.NOOPBodyHooks, *types.NOOPBodyHooks,
74+
bool,
75+
]().StateAccount.Set(acc, true)
6476
},
6577
wantEmpty: false,
6678
},

core/types/backwards_compat.libevm_test.go

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"testing"
2222

2323
"github.com/google/go-cmp/cmp"
24+
"github.com/google/go-cmp/cmp/cmpopts"
2425
"github.com/kr/pretty"
2526
"github.com/stretchr/testify/assert"
2627
"github.com/stretchr/testify/require"
@@ -56,14 +57,18 @@ func TestBodyRLPBackwardsCompatibility(t *testing.T) {
5657
for _, tx := range txMatrix {
5758
for _, u := range uncleMatrix {
5859
for _, w := range withdrawMatrix {
59-
bodies = append(bodies, &Body{tx, u, w})
60+
bodies = append(bodies, &Body{tx, u, w, nil /* extra field */})
6061
}
6162
}
6263
}
6364

6465
for _, body := range bodies {
6566
t.Run("", func(t *testing.T) {
66-
t.Logf("\n%s", pretty.Sprint(body))
67+
t.Cleanup(func() {
68+
if t.Failed() {
69+
t.Logf("\n%s", pretty.Sprint(body))
70+
}
71+
})
6772

6873
// The original [Body] doesn't implement [rlp.Encoder] nor
6974
// [rlp.Decoder] so we can use a methodless equivalent as the gold
@@ -74,14 +79,15 @@ func TestBodyRLPBackwardsCompatibility(t *testing.T) {
7479

7580
t.Run("Encode", func(t *testing.T) {
7681
got, err := rlp.EncodeToBytes(body)
77-
require.NoErrorf(t, err, "rlp.EncodeToBytes(%#v)", body)
78-
assert.Equalf(t, wantRLP, got, "rlp.EncodeToBytes(%#v)", body)
82+
require.NoErrorf(t, err, "rlp.EncodeToBytes(%T)", body)
83+
assert.Equalf(t, wantRLP, got, "rlp.EncodeToBytes(%T)", body)
7984
})
8085

8186
t.Run("Decode", func(t *testing.T) {
8287
got := new(Body)
8388
err := rlp.DecodeBytes(wantRLP, got)
84-
require.NoErrorf(t, err, "rlp.DecodeBytes(%v, %T)", wantRLP, got)
89+
require.NoErrorf(t, err, "rlp.DecodeBytes(rlp.EncodeToBytes(%T), %T) resulted in %s",
90+
(*withoutMethods)(body), got, pretty.Sprint(got))
8591

8692
want := body
8793
// Regular RLP decoding will never leave these non-optional
@@ -96,9 +102,10 @@ func TestBodyRLPBackwardsCompatibility(t *testing.T) {
96102
opts := cmp.Options{
97103
cmp.Comparer((*Header).equalHash),
98104
cmp.Comparer((*Transaction).equalHash),
105+
cmpopts.IgnoreUnexported(Body{}),
99106
}
100-
if diff := cmp.Diff(body, got, opts); diff != "" {
101-
t.Errorf("rlp.DecodeBytes(rlp.EncodeToBytes(%#v)) diff (-want +got):\n%s", body, diff)
107+
if diff := cmp.Diff(want, got, opts); diff != "" {
108+
t.Errorf("rlp.DecodeBytes(rlp.EncodeToBytes(%T)) diff (-want +got):\n%s", (*withoutMethods)(body), diff)
102109
}
103110
})
104111
})
@@ -148,10 +155,13 @@ func TestBodyRLPCChainCompat(t *testing.T) {
148155
// The inputs to this test were used to generate the expected RLP with
149156
// ava-labs/coreth. This serves as both an example of how to use [BodyHooks]
150157
// and a test of compatibility.
151-
152-
t.Cleanup(func() {
153-
TestOnlyRegisterBodyHooks(NOOPBodyHooks{})
154-
})
158+
TestOnlyClearRegisteredExtras()
159+
t.Cleanup(TestOnlyClearRegisteredExtras)
160+
extras := RegisterExtras[
161+
NOOPHeaderHooks, *NOOPHeaderHooks,
162+
cChainBodyExtras, *cChainBodyExtras,
163+
struct{},
164+
]()
155165

156166
body := &Body{
157167
Transactions: []*Transaction{
@@ -194,24 +204,24 @@ func TestBodyRLPCChainCompat(t *testing.T) {
194204
require.NoErrorf(t, err, "hex.DecodeString(%q)", tt.wantRLPHex)
195205

196206
t.Run("Encode", func(t *testing.T) {
197-
TestOnlyRegisterBodyHooks(tt.extra)
207+
extras.Body.Set(body, tt.extra)
198208
got, err := rlp.EncodeToBytes(body)
199209
require.NoErrorf(t, err, "rlp.EncodeToBytes(%+v)", body)
200210
assert.Equalf(t, wantRLP, got, "rlp.EncodeToBytes(%+v)", body)
201211
})
202212

203213
t.Run("Decode", func(t *testing.T) {
204214
var extra cChainBodyExtras
205-
TestOnlyRegisterBodyHooks(&extra)
206-
207215
got := new(Body)
216+
extras.Body.Set(got, &extra)
208217
err := rlp.DecodeBytes(wantRLP, got)
209218
require.NoErrorf(t, err, "rlp.DecodeBytes(%#x, %T)", wantRLP, got)
210219
assert.Equal(t, tt.extra, &extra, "rlp.DecodeBytes(%#x, [%T as registered extra in %T carrier])", wantRLP, &extra, got)
211220

212221
opts := cmp.Options{
213222
cmp.Comparer((*Header).equalHash),
214223
cmp.Comparer((*Transaction).equalHash),
224+
cmpopts.IgnoreUnexported(Body{}),
215225
}
216226
if diff := cmp.Diff(body, got, opts); diff != "" {
217227
t.Errorf("rlp.DecodeBytes(%#x, [%T while carrying registered %T extra payload]) diff (-want +got):\n%s", wantRLP, got, &extra, diff)

core/types/backwards_compat_diffpkg.libevm_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ func TestHeaderRLPBackwardsCompatibility(t *testing.T) {
4040
{
4141
name: "no-op header hooks",
4242
register: func() {
43-
RegisterExtras[NOOPHeaderHooks, *NOOPHeaderHooks, struct{}]()
43+
RegisterExtras[
44+
NOOPHeaderHooks, *NOOPHeaderHooks,
45+
NOOPBodyHooks, *NOOPBodyHooks,
46+
struct{},
47+
]()
4448
},
4549
},
4650
}

core/types/block.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,8 @@ type Body struct {
176176
Transactions []*Transaction
177177
Uncles []*Header
178178
Withdrawals []*Withdrawal `rlp:"optional"`
179+
180+
extra *pseudo.Type // See [RegisterExtras]
179181
}
180182

181183
// Block represents an Ethereum block.
@@ -338,7 +340,7 @@ func (b *Block) EncodeRLP(w io.Writer) error {
338340
// Body returns the non-header content of the block.
339341
// Note the returned data is not an independent copy.
340342
func (b *Block) Body() *Body {
341-
return &Body{b.transactions, b.uncles, b.withdrawals}
343+
return &Body{b.transactions, b.uncles, b.withdrawals, nil /* unexported extras field */}
342344
}
343345

344346
// Accessors for body data. These do not return a copy because the content

core/types/block.libevm.go

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import (
2222
"io"
2323

2424
"github.com/ava-labs/libevm/libevm/pseudo"
25-
"github.com/ava-labs/libevm/libevm/testonly"
2625
"github.com/ava-labs/libevm/rlp"
2726
)
2827

@@ -45,7 +44,7 @@ func (h *Header) hooks() HeaderHooks {
4544
return new(NOOPHeaderHooks)
4645
}
4746

48-
func (e ExtraPayloads[HPtr, SA]) hooksFromHeader(h *Header) HeaderHooks {
47+
func (e ExtraPayloads[HPtr, BPtr, SA]) hooksFromHeader(h *Header) HeaderHooks {
4948
return e.Header.Get(h)
5049
}
5150

@@ -134,22 +133,11 @@ type BodyHooks interface {
134133
RLPFieldPointersForDecoding(*Body) *rlp.Fields
135134
}
136135

137-
// TestOnlyRegisterBodyHooks is a temporary means of "registering" BodyHooks for
138-
// the purpose of testing. It will panic if called outside of a test.
139-
func TestOnlyRegisterBodyHooks(h BodyHooks) {
140-
testonly.OrPanic(func() {
141-
todoRegisteredBodyHooks = h
142-
})
143-
}
144-
145-
// todoRegisteredBodyHooks is a temporary placeholder for "registering"
146-
// BodyHooks, before they are included in [RegisterExtras].
147-
var todoRegisteredBodyHooks BodyHooks = NOOPBodyHooks{}
148-
149136
func (b *Body) hooks() BodyHooks {
150-
// TODO(arr4n): when incorporating BodyHooks into [RegisterExtras], the
151-
// [todoRegisteredBodyHooks] variable MUST be removed.
152-
return todoRegisteredBodyHooks
137+
if r := registeredExtras; r.Registered() {
138+
return r.Get().hooks.hooksFromBody(b)
139+
}
140+
return NOOPBodyHooks{}
153141
}
154142

155143
// NOOPBodyHooks implements [BodyHooks] such that they are equivalent to no type
@@ -160,7 +148,7 @@ type NOOPBodyHooks struct{}
160148
// fields and their order, which we lock in here as a change detector. If this
161149
// breaks then it MUST be updated and the RLP methods reviewed + new
162150
// backwards-compatibility tests added.
163-
var _ = &Body{[]*Transaction{}, []*Header{}, []*Withdrawal{}}
151+
var _ = &Body{[]*Transaction{}, []*Header{}, []*Withdrawal{}, nil /* extra unexported type */}
164152

165153
func (NOOPBodyHooks) RLPFieldsForEncoding(b *Body) *rlp.Fields {
166154
return &rlp.Fields{
@@ -175,3 +163,19 @@ func (NOOPBodyHooks) RLPFieldPointersForDecoding(b *Body) *rlp.Fields {
175163
Optional: []any{&b.Withdrawals},
176164
}
177165
}
166+
167+
func (e ExtraPayloads[HPtr, BPtr, SA]) hooksFromBody(b *Body) BodyHooks {
168+
return e.Body.Get(b)
169+
}
170+
171+
func (b *Body) extraPayload() *pseudo.Type {
172+
r := registeredExtras
173+
if !r.Registered() {
174+
// See params.ChainConfig.extraPayload() for panic rationale.
175+
panic(fmt.Sprintf("%T.extraPayload() called before RegisterExtras()", r))
176+
}
177+
if b.extra == nil {
178+
b.extra = r.Get().newBody()
179+
}
180+
return b.extra
181+
}

core/types/block.libevm_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,11 @@ func TestHeaderHooks(t *testing.T) {
8686
TestOnlyClearRegisteredExtras()
8787
defer TestOnlyClearRegisteredExtras()
8888

89-
extras := RegisterExtras[stubHeaderHooks, *stubHeaderHooks, struct{}]()
89+
extras := RegisterExtras[
90+
stubHeaderHooks, *stubHeaderHooks,
91+
NOOPBodyHooks, *NOOPBodyHooks,
92+
struct{},
93+
]()
9094
rng := ethtest.NewPseudoRand(13579)
9195

9296
suffix := rng.Bytes(8)

core/types/rlp_payload.libevm.go

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,21 @@ func RegisterExtras[
4646
HeaderHooks
4747
*H
4848
},
49+
B any, BPtr interface {
50+
BodyHooks
51+
*B
52+
},
4953
SA any,
50-
]() ExtraPayloads[HPtr, SA] {
51-
extra := ExtraPayloads[HPtr, SA]{
54+
]() ExtraPayloads[HPtr, BPtr, SA] {
55+
extra := ExtraPayloads[HPtr, BPtr, SA]{
5256
Header: pseudo.NewAccessor[*Header, HPtr](
5357
(*Header).extraPayload,
5458
func(h *Header, t *pseudo.Type) { h.extra = t },
5559
),
60+
Body: pseudo.NewAccessor[*Body, BPtr](
61+
(*Body).extraPayload,
62+
func(b *Body, t *pseudo.Type) { b.extra = t },
63+
),
5664
StateAccount: pseudo.NewAccessor[StateOrSlimAccount, SA](
5765
func(a StateOrSlimAccount) *pseudo.Type { return a.extra().payload() },
5866
func(a StateOrSlimAccount, t *pseudo.Type) { a.extra().t = t },
@@ -63,10 +71,11 @@ func RegisterExtras[
6371
var x SA
6472
return fmt.Sprintf("%T", x)
6573
}(),
66-
// The [ExtraPayloads] that we returns is based on [HPtr,SA], not [H,SA]
67-
// so our constructors MUST match that. This guarantees that calls to
68-
// the [HeaderHooks] methods will never be performed on a nil pointer.
74+
// The [ExtraPayloads] that we returns is based on [HPtr,BPtr,SA], not
75+
// [H,B,SA] so our constructors MUST match that. This guarantees that calls to
76+
// the [HeaderHooks] and [BodyHooks] methods will never be performed on a nil pointer.
6977
newHeader: pseudo.NewConstructor[H]().NewPointer, // i.e. non-nil HPtr
78+
newBody: pseudo.NewConstructor[B]().NewPointer, // i.e. non-nil BPtr
7079
newStateAccount: pseudo.NewConstructor[SA]().Zero,
7180
cloneStateAccount: extra.cloneStateAccount,
7281
hooks: extra,
@@ -87,11 +96,14 @@ func TestOnlyClearRegisteredExtras() {
8796
var registeredExtras register.AtMostOnce[*extraConstructors]
8897

8998
type extraConstructors struct {
90-
stateAccountType string
91-
newHeader, newStateAccount func() *pseudo.Type
92-
cloneStateAccount func(*StateAccountExtra) *StateAccountExtra
93-
hooks interface {
99+
stateAccountType string
100+
newHeader func() *pseudo.Type
101+
newBody func() *pseudo.Type
102+
newStateAccount func() *pseudo.Type
103+
cloneStateAccount func(*StateAccountExtra) *StateAccountExtra
104+
hooks interface {
94105
hooksFromHeader(*Header) HeaderHooks
106+
hooksFromBody(*Body) BodyHooks
95107
}
96108
}
97109

@@ -105,14 +117,15 @@ func (e *StateAccountExtra) clone() *StateAccountExtra {
105117
}
106118

107119
// ExtraPayloads provides strongly typed access to the extra payload carried by
108-
// [Header], [StateAccount], and [SlimAccount] structs. The only valid way to
120+
// [Header], [Body], [StateAccount], and [SlimAccount] structs. The only valid way to
109121
// construct an instance is by a call to [RegisterExtras].
110-
type ExtraPayloads[HPtr HeaderHooks, SA any] struct {
122+
type ExtraPayloads[HPtr HeaderHooks, BPtr BodyHooks, SA any] struct {
111123
Header pseudo.Accessor[*Header, HPtr]
124+
Body pseudo.Accessor[*Body, BPtr]
112125
StateAccount pseudo.Accessor[StateOrSlimAccount, SA] // Also provides [SlimAccount] access.
113126
}
114127

115-
func (ExtraPayloads[HPtr, SA]) cloneStateAccount(s *StateAccountExtra) *StateAccountExtra {
128+
func (ExtraPayloads[HPtr, BPtr, SA]) cloneStateAccount(s *StateAccountExtra) *StateAccountExtra {
116129
v := pseudo.MustNewValue[SA](s.t)
117130
return &StateAccountExtra{
118131
t: pseudo.From(v.Get()).Type,

core/types/state_account.libevm_test.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ func TestStateAccountRLP(t *testing.T) {
4646
explicitFalseBoolean := test{
4747
name: "explicit false-boolean extra",
4848
register: func() {
49-
RegisterExtras[NOOPHeaderHooks, *NOOPHeaderHooks, bool]()
49+
RegisterExtras[
50+
NOOPHeaderHooks, *NOOPHeaderHooks,
51+
NOOPBodyHooks, *NOOPBodyHooks,
52+
bool,
53+
]()
5054
},
5155
acc: &StateAccount{
5256
Nonce: 0x444444,
@@ -76,7 +80,11 @@ func TestStateAccountRLP(t *testing.T) {
7680
{
7781
name: "true-boolean extra",
7882
register: func() {
79-
RegisterExtras[NOOPHeaderHooks, *NOOPHeaderHooks, bool]()
83+
RegisterExtras[
84+
NOOPHeaderHooks, *NOOPHeaderHooks,
85+
NOOPBodyHooks, *NOOPBodyHooks,
86+
bool,
87+
]()
8088
},
8189
acc: &StateAccount{
8290
Nonce: 0x444444,

0 commit comments

Comments
 (0)