Remove Block RLP hooks - #304
Conversation
| // The body provided to the hooks is expected to contain the same extra | ||
| // as the method receiver. | ||
| extra: b.extra, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Updates block/body RLP encoding to route extras through Body hooks (removing the block-specific proxy), and adds a fast path to combine RLP-encoded header/body into block bytes.
Changes:
- Replace
BlockRLPProxy-based block encoding/decoding withBodyhook-driven field assembly. - Introduce
BlockBytes(headerBytes, bodyBytes)fast-path and add fuzz/bench coverage for equivalence/performance. - Update tests and temporary extras to use
Bodyhooks and payload access patterns.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| core/types/tempextras.libevm_test.go | Updates temporary extras test to set/get body extras via extras.Body and BodyRLPFieldsForEncoding. |
| core/types/rlp_payload.libevm.go | Adds (*Block).extraOrNil() helper to avoid allocating extras when unregistered. |
| core/types/block.libevm_test.go | Adds body-extra roundtrip test plus fuzz/bench coverage for the new BlockBytes constructor. |
| core/types/block.libevm.go | Reworks block RLP encoding/decoding to use Body hook fields; adds BlockBytes. |
| core/types/block.go | Replaces extblock.hooks with extblock.extra and wires encode/decode through extraOrNil(). |
| core/types/backwards_compat.libevm_test.go | Removes now-obsolete BlockRLP* hook methods from compat extras test type. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Rather than using the receiver directly, we access it through b. This | ||
| // demonstrates that the hooks can access their own payload via the | ||
| // [types.Body] they are passed. |
There was a problem hiding this comment.
I think this is an anti-pattern because it doesn't demonstrate that the hook was called on the correct payload. The idiomatic implementation is to use the receiver as it doesn't require hooks to have access to the pseudo.Accessor.
Is there a specific reason you'd want a hook to access its payload via the carrying struct?
There was a problem hiding this comment.
I think this is an anti-pattern because it doesn't demonstrate that the hook was called on the correct payload. The idiomatic implementation is to use the receiver as it doesn't require hooks to have access to the pseudo.Accessor.
I agree. This test is specifically ensuring that a "non-idiomatic" implementation works.
Is there a specific reason you'd want a hook to access its payload via the carrying struct?
I don't think an implementation SHOULD do this. But they CAN. So (imo) we MUST support that (or very clearly document that this isn't allowed, and vet that we don't do this in coreth / subnet-evm).
There was a problem hiding this comment.
I iterated on this for awhile and eventually realized the thing I was asserting wasn't even checking for the bug I was trying to prevent.
I do think it could be useful to test that the receiver == the provided arg... But really the concern with this change is that we need to put the block extra (without a copy) as the body extra so that marshal and unmarshal works correctly.
By switching just to this I was able to reuse blockPayload and reduce the testing surface.
I had initially considered adding this test as part of TestBlockWithX (and instead making that TestBlockHooks) - but I feel like that would be missing a bunch of coverage.
| []*Header{ | ||
| {ParentHash: rng.Hash()}, | ||
| }, | ||
| nil, |
There was a problem hiding this comment.
I replaced NewBlock with NewBlockWithHeader. I had originally not provided receipts because they don't impact the serialized format... But this test doesn't really care about any of the fields (other than the extras) - so I just made it as minimal as I could.
| ]() | ||
|
|
||
| typ := reflect.TypeOf(&Block{}) | ||
| typ := reflect.TypeFor[*Block]() |
There was a problem hiding this comment.
Not required - but my IDE lints these... So figured I might as well modernize this while I was here.
| // Rather than using the receiver directly, we access it through b. This | ||
| // demonstrates that the hooks can access their own payload via the | ||
| // [types.Body] they are passed. |
There was a problem hiding this comment.
I iterated on this for awhile and eventually realized the thing I was asserting wasn't even checking for the bug I was trying to prevent.
I do think it could be useful to test that the receiver == the provided arg... But really the concern with this change is that we need to put the block extra (without a copy) as the body extra so that marshal and unmarshal works correctly.
By switching just to this I was able to reuse blockPayload and reduce the testing surface.
I had initially considered adding this test as part of TestBlockWithX (and instead making that TestBlockHooks) - but I feel like that would be missing a bunch of coverage.
Why this should be merged
While I do think it's nice to reduce the number of required hooks to implement. The primary point of this PR is introducing an efficient
types.BlockBytesfunction that efficiently combines header bytes and body bytes into block bytes.Technically, this
BlockBytesfunction doesn't need to live in libevm (I could implement it in SAE) - but I think that it makes sense to solidify the hook implementations against a concrete requirement.benchstat: referenceBlockBytes (reencode.txt) vs BlockBytes (blockbytes.txt)
Block reencoding showed up as a significant performance bottleneck while serving blocks. Reencoding currently happens when calling
rawdb.ReadBlock. By implementing this change the GetAncestors optimization observes:benchstat: GetAncestors optimization in avalanchego before & after this change
How this works
While the goal of this PR was adding
BlockBytes- most of the effort fell to removingBlockRLPFieldsForEncodingandBlockRLPFieldPointersForDecoding.These functions needed to be removed because they allowed a hook provider to break the
Header+Body=Blockassumption. There was nothing stopping someone from re-ordering body fields when they were serialized just as a body vs when they were serialized as part of a block.This PR re-uses
BodyRLPFieldsForEncodingandBodyRLPFieldPointersForDecodingso that we are guaranteed that the body bytes follow the same order as the block bytes.How this was tested
BlockBytes