Repository navigation
feat(p2p): refuse execution payloads whose block hash is not their header's - #770
Draft
MegaRedHand wants to merge 2 commits into
Draft
MegaRedHand wants to merge 2 commits into
MegaRedHand wants to merge 2 commits into
Conversation
…ader's Rebuild the execution block header from a payload's own fields and reject the message when keccak256(rlp(header)) differs from the claimed block_hash. Runs on pre-gloas beacon_block gossip (bellatrix to fulu), on gloas execution_payload gossip, and on every envelope fetched by root or range. Since engine_newPayloadV2 the Engine API answers a block-hash mismatch with INVALID and a null latestValidHash, indistinguishable from an executed-invalid payload whose last valid ancestor is unknown. Catching the structural case on the consensus side keeps such payloads from being applied to fork choice ahead of the execution client's answer. The header is encoded in ethlambda-p2p rather than through ethrex's BlockHeader, which holds base_fee_per_gas as a u64 where the payload has 256 bits. Each fork is tested against a real block or envelope (Sepolia, Hoodi, mainnet, Plataberget) plus a one-field-changed refusal. This is stricter than the consensus specs' gossip rules, documented in docs/spec_deviations.md and docs/beacon_wire.md.
MegaRedHand
added a commit
that referenced
this pull request
Nov 5, 2026
…ader's (#770) Rebuild the execution block header from a payload's own fields and reject the message when keccak256(rlp(header)) differs from the claimed block_hash. Runs on pre-gloas beacon_block gossip (bellatrix to fulu), on gloas execution_payload gossip, and on every envelope fetched by root or range. Since engine_newPayloadV2 the Engine API answers a block-hash mismatch with INVALID and a null latestValidHash, indistinguishable from an executed-invalid payload whose last valid ancestor is unknown. Catching the structural case on the consensus side keeps such payloads from being applied to fork choice ahead of the execution client's answer. The header is encoded in ethlambda-p2p rather than through ethrex's BlockHeader, which holds base_fee_per_gas as a u64 where the payload has 256 bits. Each fork is tested against a real block or envelope (Sepolia, Hoodi, mainnet, Plataberget) plus a one-field-changed refusal. This is stricter than the consensus specs' gossip rules, documented in docs/spec_deviations.md and docs/beacon_wire.md. Cherry-picked from feat/gossip-payload-block-hash at 686b481. Claude-Session: https://claude.ai/code/session_011vghNmndKPkZinHRkVTwVB
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Beacon p2p now rebuilds the execution block header from a payload's own fields and REJECTs the message when
keccak256(rlp(header))is not the payload'sblock_hash:beacon_blockgossip (bellatrix to fulu), intriage_block, after the cheap rulesexecution_payloadgossip, intriage_envelopeenvelope_checks::judge)The check lives in
crates/net/p2p/src/beacon/payload_hash.rs(verify_block,verify_envelope). NewRejectReason::PayloadBlockHashMismatch(payload_block_hash_mismatch), counterlean_beacon_payload_block_hash_mismatch_total{kind}and awarn!with slot and root.It sits in p2p, not in
state_transition::gossip::*, so the spec-fixture gossip vectors (which carry arbitrary payload hashes) are unaffected. Header encoding is done here withethrex-rlp'sEncoder, because ethrex'sBlockHeaderholdsbase_fee_per_gasasu64. Trie roots reuseethrex-common(withdrawals, requests hash) andethrex-trie(transactions over raw bytes; same tag v25.0.0, already in the lockfile, no new crate).Why
Since
engine_newPayloadV2the Engine API answers a block-hash mismatch withINVALIDand a nulllatestValidHash, indistinguishable from an executed-invalid payload whose last valid ancestor is unknown. Upcoming work applies payloads to fork choice before the EL answers; catching the structural case here keeps those payloads from being applied.Deviation
Stricter than the consensus-specs gossip rules. Documented in
docs/spec_deviations.md("A payload's block hash must be its header's") anddocs/beacon_wire.md. Blocks fetched viabeacon_blocks_by_range/rootare not checked.Header per fork
bellatrix: London fields; capella:
withdrawals_root; deneb:blob_gas_used,excess_blob_gas,parent_beacon_block_root(the block'sparent_root); electra/fulu:requests_hash(from the Engine API request list); gloas:block_access_list_hash(keccak of the payload's BAL bytes, EIP-7928) andslot_number(EIP-7843), parent root from the envelope. A bellatrix block with the empty default payload (pre-merge) is skipped.Real-chain vectors (SSZ,
crates/net/p2p/src/beacon/testdata/payload_hash/)Each vector must match, and per fork a changed field (gas_used, block_hash, withdrawal amount, excess_blob_gas, parent root, transaction byte, extra_data, an extra request, slot_number, BAL byte, base fee, ...) must be refused. A triage-level test covers the gossip reject.
Testing
make fmt,make lint,cargo test -p ethlambda-p2p --profile release-fast --lib(372 passed),cargo test -p ethlambda-state-transition --profile release-fast --lib gossip. Existing p2p tests that forwarded structurally arbitrary envelopes now seal them with a test-onlyseal_envelope. No leanSpec or consensus-spec fixtures were run.