Skip to content

feat(core/types): Body RLP codec hooks #107

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
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
5 changes: 4 additions & 1 deletion core/state/state.libevm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ func TestGetSetExtra(t *testing.T) {
t.Cleanup(types.TestOnlyClearRegisteredExtras)
// Just as its Data field is a pointer, the registered type is a pointer to
// test deep copying.
payloads := types.RegisterExtras[types.NOOPHeaderHooks, *types.NOOPHeaderHooks, *accountExtra]().StateAccount
payloads := types.RegisterExtras[
types.NOOPHeaderHooks, *types.NOOPHeaderHooks,
types.NOOPBodyHooks, *types.NOOPBodyHooks,
*accountExtra]().StateAccount

rng := ethtest.NewPseudoRand(42)
addr := rng.Address()
Expand Down
19 changes: 16 additions & 3 deletions core/state/state_object.libevm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,34 @@ func TestStateObjectEmpty(t *testing.T) {
{
name: "explicit false bool",
registerAndSet: func(acc *types.StateAccount) {
types.RegisterExtras[types.NOOPHeaderHooks, *types.NOOPHeaderHooks, bool]().StateAccount.Set(acc, false)
types.RegisterExtras[
types.NOOPHeaderHooks, *types.NOOPHeaderHooks,
types.NOOPBodyHooks, *types.NOOPBodyHooks,
bool]().StateAccount.Set(acc, false)
types.RegisterExtras[
types.NOOPHeaderHooks, *types.NOOPHeaderHooks,
types.NOOPBodyHooks, *types.NOOPBodyHooks,
bool]().StateAccount.Set(acc, false)
},
wantEmpty: true,
},
{
name: "implicit false bool",
registerAndSet: func(*types.StateAccount) {
types.RegisterExtras[types.NOOPHeaderHooks, *types.NOOPHeaderHooks, bool]()
types.RegisterExtras[
types.NOOPHeaderHooks, *types.NOOPHeaderHooks,
types.NOOPBodyHooks, *types.NOOPBodyHooks,
bool]()
},
wantEmpty: true,
},
{
name: "true bool",
registerAndSet: func(acc *types.StateAccount) {
types.RegisterExtras[types.NOOPHeaderHooks, *types.NOOPHeaderHooks, bool]().StateAccount.Set(acc, true)
types.RegisterExtras[
types.NOOPHeaderHooks, *types.NOOPHeaderHooks,
types.NOOPBodyHooks, *types.NOOPBodyHooks,
bool]().StateAccount.Set(acc, true)
},
wantEmpty: false,
},
Expand Down
13 changes: 10 additions & 3 deletions core/types/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ type Body struct {
Transactions []*Transaction
Uncles []*Header
Withdrawals []*Withdrawal `rlp:"optional"`

extra *pseudo.Type // See [RegisterExtras]
}

// Block represents an Ethereum block.
Expand Down Expand Up @@ -277,8 +279,9 @@ func NewBlockWithWithdrawals(header *Header, txs []*Transaction, uncles []*Heade
return b.WithWithdrawals(withdrawals)
}

// CopyHeader creates a deep copy of a block header.
func CopyHeader(h *Header) *Header {
// CopyEthHeader creates a deep copy of an Ethereum block header.
// Use [CopyHeader] instead if your header has any registered extra.
func CopyEthHeader(h *Header) *Header {
cpy := *h
if cpy.Difficulty = new(big.Int); h.Difficulty != nil {
cpy.Difficulty.Set(h.Difficulty)
Expand Down Expand Up @@ -337,7 +340,11 @@ func (b *Block) EncodeRLP(w io.Writer) error {
// Body returns the non-header content of the block.
// Note the returned data is not an independent copy.
func (b *Block) Body() *Body {
return &Body{b.transactions, b.uncles, b.withdrawals}
return &Body{
Transactions: b.transactions,
Uncles: b.uncles,
Withdrawals: b.withdrawals,
}
Comment on lines +343 to +347
Copy link
Author

Choose a reason for hiding this comment

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

note this change is compulsory since there are more fields present in the struct than these 3.

}

// Accessors for body data. These do not return a copy because the content
Expand Down
75 changes: 74 additions & 1 deletion core/types/block.libevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type HeaderHooks interface {
UnmarshalJSON(*Header, []byte) error //nolint:govet
EncodeRLP(*Header, io.Writer) error
DecodeRLP(*Header, *rlp.Stream) error
Copy(*Header) *Header
}

// hooks returns the Header's registered HeaderHooks, if any, otherwise a
Expand All @@ -43,7 +44,7 @@ func (h *Header) hooks() HeaderHooks {
return new(NOOPHeaderHooks)
}

func (e ExtraPayloads[HPtr, SA]) hooksFromHeader(h *Header) HeaderHooks {
func (e ExtraPayloads[HPtr, BodyExtraPtr, SA]) hooksFromHeader(h *Header) HeaderHooks {
return e.Header.Get(h)
}

Expand Down Expand Up @@ -108,3 +109,75 @@ func (*NOOPHeaderHooks) DecodeRLP(h *Header, s *rlp.Stream) error {
type withoutMethods Header
return s.Decode((*withoutMethods)(h))
}

func (n *NOOPHeaderHooks) Copy(h *Header) *Header {
return CopyEthHeader(h)
}

// CopyHeader creates a deep copy of a block header.
func CopyHeader(h *Header) *Header {
return h.hooks().Copy(h)
}

// BodyHooks are required for all types registered with [RegisterExtras] for
// [Body] payloads.
type BodyHooks interface {
EncodeRLP(*Body, io.Writer) error
DecodeRLP(*Body, *rlp.Stream) error
}

// hooks returns the Body's registered BodyHooks, if any, otherwise a
// [*NOOPBodyHooks] suitable for running the default behaviour.
func (b *Body) hooks() BodyHooks {
if r := registeredExtras; r.Registered() {
return r.Get().hooks.hooksFromBody(b)
}
return new(NOOPBodyHooks)
}

func (e ExtraPayloads[HPtr, BodyExtraPtr, SA]) hooksFromBody(b *Body) BodyHooks {
return e.Body.Get(b)
}

var _ interface {
rlp.Encoder
rlp.Decoder
} = (*Body)(nil)

// EncodeRLP implements the [rlp.Encoder] interface.
func (b *Body) EncodeRLP(w io.Writer) error {
return b.hooks().EncodeRLP(b, w)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Completely overriding RLP {en,de}coding is a blunt tool that we used for Header only because of the complexity of the modifications to that type (with the view to hopefully changing it in the future). I think it's possible to achieve this for Body without a hook, using just the rules of RLP optional, but I need to tinker a bit to confirm.

My greatest concern with a complete override is that the user still has to implement a lot of the upstream functionality, reducing the benefit of libevm.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Progress on #109 suggests that this should be possible, with a hook that only needs to write fields to an rlp.EncoderBuffer. I'll confirm tomorrow.

}

// DecodeRLP implements the [rlp.Decoder] interface.
func (b *Body) DecodeRLP(s *rlp.Stream) error {
return b.hooks().DecodeRLP(b, s)
}

func (b *Body) extraPayload() *pseudo.Type {
r := registeredExtras
if !r.Registered() {
// See params.ChainConfig.extraPayload() for panic rationale.
panic(fmt.Sprintf("%T.extraPayload() called before RegisterExtras()", r))
}
if b.extra == nil {
b.extra = r.Get().newBody()
}
return b.extra
}

// NOOPBodyHooks implements [BodyHooks] such that they are equivalent to
// no type having been registered.
type NOOPBodyHooks struct{}

var _ BodyHooks = (*NOOPBodyHooks)(nil)

func (*NOOPBodyHooks) EncodeRLP(b *Body, w io.Writer) error {
type withoutMethods Body
return rlp.Encode(w, (*withoutMethods)(b))
}

func (*NOOPBodyHooks) DecodeRLP(b *Body, s *rlp.Stream) error {
type withoutMethods Body
return s.Decode((*withoutMethods)(b))
}
34 changes: 33 additions & 1 deletion core/types/block.libevm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,43 @@ func (hh *stubHeaderHooks) DecodeRLP(h *Header, s *rlp.Stream) error {
return hh.errDecode
}

func (hh *stubHeaderHooks) Copy(h *Header) *Header {
return h
}

type stubBodyHooks struct {
encoding []byte
gotRawRLPToDecode []byte
setBodyToOnUnmarshalOrDecode Body

errEncode, errDecode error
}

func (bh *stubBodyHooks) EncodeRLP(b *Body, w io.Writer) error {
if _, err := w.Write(bh.encoding); err != nil {
return err
}
return bh.errEncode
}

func (bh *stubBodyHooks) DecodeRLP(b *Body, s *rlp.Stream) error {
r, err := s.Raw()
if err != nil {
return err
}
bh.gotRawRLPToDecode = r
*b = bh.setBodyToOnUnmarshalOrDecode
return bh.errDecode
}

func TestHeaderHooks(t *testing.T) {
TestOnlyClearRegisteredExtras()
defer TestOnlyClearRegisteredExtras()

extras := RegisterExtras[stubHeaderHooks, *stubHeaderHooks, struct{}]()
extras := RegisterExtras[
stubHeaderHooks, *stubHeaderHooks,
stubBodyHooks, *stubBodyHooks,
struct{}]()
rng := ethtest.NewPseudoRand(13579)

suffix := rng.Bytes(8)
Expand Down
5 changes: 4 additions & 1 deletion core/types/rlp_backwards_compat.libevm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ func TestHeaderRLPBackwardsCompatibility(t *testing.T) {
{
name: "no-op header hooks",
register: func() {
RegisterExtras[NOOPHeaderHooks, *NOOPHeaderHooks, struct{}]()
RegisterExtras[
NOOPHeaderHooks, *NOOPHeaderHooks,
NOOPBodyHooks, *NOOPBodyHooks,
struct{}]()
},
},
}
Expand Down
39 changes: 26 additions & 13 deletions core/types/rlp_payload.libevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,21 @@ func RegisterExtras[
HeaderHooks
*H
},
BodyExtra any, BodyExtraPtr interface {
BodyHooks
*BodyExtra
},
SA any,
]() ExtraPayloads[HPtr, SA] {
extra := ExtraPayloads[HPtr, SA]{
]() ExtraPayloads[HPtr, BodyExtraPtr, SA] {
extra := ExtraPayloads[HPtr, BodyExtraPtr, SA]{
Header: pseudo.NewAccessor[*Header, HPtr](
(*Header).extraPayload,
func(h *Header, t *pseudo.Type) { h.extra = t },
),
Body: pseudo.NewAccessor[*Body, BodyExtraPtr](
(*Body).extraPayload,
func(b *Body, t *pseudo.Type) { b.extra = t },
),
StateAccount: pseudo.NewAccessor[StateOrSlimAccount, SA](
func(a StateOrSlimAccount) *pseudo.Type { return a.extra().payload() },
func(a StateOrSlimAccount, t *pseudo.Type) { a.extra().t = t },
Expand All @@ -63,10 +71,11 @@ func RegisterExtras[
var x SA
return fmt.Sprintf("%T", x)
}(),
// The [ExtraPayloads] that we returns is based on [HPtr,SA], not [H,SA]
// so our constructors MUST match that. This guarantees that calls to
// the [HeaderHooks] methods will never be performed on a nil pointer.
newHeader: pseudo.NewConstructor[H]().NewPointer, // i.e. non-nil HPtr
// The [ExtraPayloads] that we returns is based on [HPtr,BodyExtraPtr,SA], not
// [H,BodyExtra,SA] so our constructors MUST match that. This guarantees that calls to
// the [HeaderHooks] and [BodyHooks] methods will never be performed on a nil pointer.
newHeader: pseudo.NewConstructor[H]().NewPointer, // i.e. non-nil HPtr
newBody: pseudo.NewConstructor[BodyExtra]().NewPointer, // i.e. non-nil BodyExtraPtr
newStateAccount: pseudo.NewConstructor[SA]().Zero,
cloneStateAccount: extra.cloneStateAccount,
hooks: extra,
Expand All @@ -87,11 +96,14 @@ func TestOnlyClearRegisteredExtras() {
var registeredExtras register.AtMostOnce[*extraConstructors]

type extraConstructors struct {
stateAccountType string
newHeader, newStateAccount func() *pseudo.Type
cloneStateAccount func(*StateAccountExtra) *StateAccountExtra
hooks interface {
stateAccountType string
newHeader func() *pseudo.Type
newBody func() *pseudo.Type
newStateAccount func() *pseudo.Type
cloneStateAccount func(*StateAccountExtra) *StateAccountExtra
hooks interface {
hooksFromHeader(*Header) HeaderHooks
hooksFromBody(*Body) BodyHooks
}
}

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

// ExtraPayloads provides strongly typed access to the extra payload carried by
// [Header], [StateAccount], and [SlimAccount] structs. The only valid way to
// [Header], [Body], [StateAccount], and [SlimAccount] structs. The only valid way to
// construct an instance is by a call to [RegisterExtras].
type ExtraPayloads[HPtr HeaderHooks, SA any] struct {
type ExtraPayloads[HPtr HeaderHooks, BodyExtraPtr BodyHooks, SA any] struct {
Header pseudo.Accessor[*Header, HPtr]
Body pseudo.Accessor[*Body, BodyExtraPtr]
StateAccount pseudo.Accessor[StateOrSlimAccount, SA] // Also provides [SlimAccount] access.
}

func (ExtraPayloads[HPtr, SA]) cloneStateAccount(s *StateAccountExtra) *StateAccountExtra {
func (ExtraPayloads[HPtr, BodyExtraPtr, SA]) cloneStateAccount(s *StateAccountExtra) *StateAccountExtra {
v := pseudo.MustNewValue[SA](s.t)
return &StateAccountExtra{
t: pseudo.From(v.Get()).Type,
Expand Down
10 changes: 8 additions & 2 deletions core/types/state_account.libevm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ func TestStateAccountRLP(t *testing.T) {
explicitFalseBoolean := test{
name: "explicit false-boolean extra",
register: func() {
RegisterExtras[NOOPHeaderHooks, *NOOPHeaderHooks, bool]()
RegisterExtras[
NOOPHeaderHooks, *NOOPHeaderHooks,
NOOPBodyHooks, *NOOPBodyHooks,
bool]()
},
acc: &StateAccount{
Nonce: 0x444444,
Expand Down Expand Up @@ -76,7 +79,10 @@ func TestStateAccountRLP(t *testing.T) {
{
name: "true-boolean extra",
register: func() {
RegisterExtras[NOOPHeaderHooks, *NOOPHeaderHooks, bool]()
RegisterExtras[
NOOPHeaderHooks, *NOOPHeaderHooks,
NOOPBodyHooks, *NOOPBodyHooks,
bool]()
},
acc: &StateAccount{
Nonce: 0x444444,
Expand Down
20 changes: 16 additions & 4 deletions core/types/state_account_storage.libevm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ func TestStateAccountExtraViaTrieStorage(t *testing.T) {
{
name: "true-boolean payload",
registerAndSetExtra: func(a *types.StateAccount) (*types.StateAccount, assertion) {
e := types.RegisterExtras[types.NOOPHeaderHooks, *types.NOOPHeaderHooks, bool]()
e := types.RegisterExtras[
types.NOOPHeaderHooks, *types.NOOPHeaderHooks,
types.NOOPBodyHooks, *types.NOOPBodyHooks,
bool]()
e.StateAccount.Set(a, true)
return a, func(t *testing.T, got *types.StateAccount) { //nolint:thelper
assert.Truef(t, e.StateAccount.Get(got), "")
Expand All @@ -84,7 +87,10 @@ func TestStateAccountExtraViaTrieStorage(t *testing.T) {
{
name: "explicit false-boolean payload",
registerAndSetExtra: func(a *types.StateAccount) (*types.StateAccount, assertion) {
e := types.RegisterExtras[types.NOOPHeaderHooks, *types.NOOPHeaderHooks, bool]()
e := types.RegisterExtras[
types.NOOPHeaderHooks, *types.NOOPHeaderHooks,
types.NOOPBodyHooks, *types.NOOPBodyHooks,
bool]()
e.StateAccount.Set(a, false) // the explicit part

return a, func(t *testing.T, got *types.StateAccount) { //nolint:thelper
Expand All @@ -96,7 +102,10 @@ func TestStateAccountExtraViaTrieStorage(t *testing.T) {
{
name: "implicit false-boolean payload",
registerAndSetExtra: func(a *types.StateAccount) (*types.StateAccount, assertion) {
e := types.RegisterExtras[types.NOOPHeaderHooks, *types.NOOPHeaderHooks, bool]()
e := types.RegisterExtras[
types.NOOPHeaderHooks, *types.NOOPHeaderHooks,
types.NOOPBodyHooks, *types.NOOPBodyHooks,
bool]()
// Note that `a` is reflected, unchanged (the implicit part).
return a, func(t *testing.T, got *types.StateAccount) { //nolint:thelper
assert.Falsef(t, e.StateAccount.Get(got), "")
Expand All @@ -107,7 +116,10 @@ func TestStateAccountExtraViaTrieStorage(t *testing.T) {
{
name: "arbitrary payload",
registerAndSetExtra: func(a *types.StateAccount) (*types.StateAccount, assertion) {
e := types.RegisterExtras[types.NOOPHeaderHooks, *types.NOOPHeaderHooks, arbitraryPayload]()
e := types.RegisterExtras[
types.NOOPHeaderHooks, *types.NOOPHeaderHooks,
types.NOOPBodyHooks, *types.NOOPBodyHooks,
arbitraryPayload]()
p := arbitraryPayload{arbitraryData}
e.StateAccount.Set(a, p)
return a, func(t *testing.T, got *types.StateAccount) { //nolint:thelper
Expand Down
2 changes: 1 addition & 1 deletion libevm/legacy/legacy.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func (c PrecompiledStatefulContract) Upgrade() vm.PrecompiledStatefulContract {
return func(env vm.PrecompileEnvironment, input []byte) ([]byte, error) {
gas := env.Gas()
ret, remainingGas, err := c(env, input, gas)
if used := gas - remainingGas; used < gas {
if used := gas - remainingGas; used <= gas {
env.UseGas(used)
}
return ret, err
Expand Down
Loading