From 686b4819cb2ced89af8e10e81d4052a24ba8c46f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 3 Nov 2026 18:34:28 -0300 Subject: [PATCH] feat(p2p): refuse execution payloads whose block hash is not their header'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. --- Cargo.lock | 1 + .../state_transition/src/beacon/gossip/mod.rs | 5 + crates/net/p2p/Cargo.toml | 3 + crates/net/p2p/src/beacon/envelope_checks.rs | 19 +- crates/net/p2p/src/beacon/mod.rs | 1 + crates/net/p2p/src/beacon/payload_hash.rs | 365 ++++++++++++++++++ .../net/p2p/src/beacon/payload_hash/tests.rs | 233 +++++++++++ .../payload_hash/hoodi_deneb_slot_2003.ssz | Bin 0 -> 22301 bytes .../payload_hash/hoodi_electra_slot_70001.ssz | Bin 0 -> 11608 bytes .../mainnet_fulu_slot_15576000.ssz | Bin 0 -> 71490 bytes ...plataberget_gloas_envelope_slot_593101.ssz | Bin 0 -> 7311 bytes .../sepolia_bellatrix_slot_500000.ssz | Bin 0 -> 2820 bytes .../sepolia_capella_slot_1900000.ssz | Bin 0 -> 2413 bytes crates/net/p2p/src/gossipsub/handler.rs | 25 +- crates/net/p2p/src/metrics.rs | 18 + .../net/p2p/src/req_resp/envelope_client.rs | 8 +- docs/beacon_wire.md | 12 +- docs/spec_deviations.md | 31 ++ 18 files changed, 714 insertions(+), 7 deletions(-) create mode 100644 crates/net/p2p/src/beacon/payload_hash.rs create mode 100644 crates/net/p2p/src/beacon/payload_hash/tests.rs create mode 100644 crates/net/p2p/src/beacon/testdata/payload_hash/hoodi_deneb_slot_2003.ssz create mode 100644 crates/net/p2p/src/beacon/testdata/payload_hash/hoodi_electra_slot_70001.ssz create mode 100644 crates/net/p2p/src/beacon/testdata/payload_hash/mainnet_fulu_slot_15576000.ssz create mode 100644 crates/net/p2p/src/beacon/testdata/payload_hash/plataberget_gloas_envelope_slot_593101.ssz create mode 100644 crates/net/p2p/src/beacon/testdata/payload_hash/sepolia_bellatrix_slot_500000.ssz create mode 100644 crates/net/p2p/src/beacon/testdata/payload_hash/sepolia_capella_slot_1900000.ssz diff --git a/Cargo.lock b/Cargo.lock index 0bec6c9e0..49c56bc1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2116,6 +2116,7 @@ dependencies = [ "ethrex-common", "ethrex-p2p", "ethrex-rlp", + "ethrex-trie", "futures", "hex", "libp2p", diff --git a/crates/blockchain/state_transition/src/beacon/gossip/mod.rs b/crates/blockchain/state_transition/src/beacon/gossip/mod.rs index 701fa705c..1d5019069 100644 --- a/crates/blockchain/state_transition/src/beacon/gossip/mod.rs +++ b/crates/blockchain/state_transition/src/beacon/gossip/mod.rs @@ -321,6 +321,10 @@ pub enum RejectReason { BuilderIndexMismatch, /// A gloas envelope's payload block hash is not its bid's. BlockHashMismatch, + /// An execution payload's `block_hash` is not the hash of the execution + /// block header rebuilt from the payload's own fields. A check stricter + /// than the specification's, run in p2p: see `docs/spec_deviations.md`. + PayloadBlockHashMismatch, /// The root of a gloas envelope's execution requests is not its bid's. ExecutionRequestsRootMismatch, /// A gloas envelope's payload carries more withdrawals than its limit. @@ -384,6 +388,7 @@ impl RejectReason { Self::PayloadInvalid => "payload_invalid", Self::BuilderIndexMismatch => "builder_index_mismatch", Self::BlockHashMismatch => "block_hash_mismatch", + Self::PayloadBlockHashMismatch => "payload_block_hash_mismatch", Self::ExecutionRequestsRootMismatch => "execution_requests_root_mismatch", Self::TooManyWithdrawals => "too_many_withdrawals", Self::PreGloasSlot => "pre_gloas_slot", diff --git a/crates/net/p2p/Cargo.toml b/crates/net/p2p/Cargo.toml index 22154e141..e386426eb 100644 --- a/crates/net/p2p/Cargo.toml +++ b/crates/net/p2p/Cargo.toml @@ -70,6 +70,9 @@ rand = "0.8" # against with nothing in the manifest saying so. ethrex-p2p = { git = "https://github.com/lambdaclass/ethrex", tag = "v25.0.0" } ethrex-rlp = { git = "https://github.com/lambdaclass/ethrex", tag = "v25.0.0" } +# The ordered trie behind a payload's transactions root, which `ethrex-common` +# only builds from decoded transactions, not the raw bytes a payload carries. +ethrex-trie = { git = "https://github.com/lambdaclass/ethrex", tag = "v25.0.0" } ethrex-common = { git = "https://github.com/lambdaclass/ethrex", tag = "v25.0.0" } # `rand` seeds the throwaway signing keys the ENR tests build records with. diff --git a/crates/net/p2p/src/beacon/envelope_checks.rs b/crates/net/p2p/src/beacon/envelope_checks.rs index 5dbbd43de..0690e378b 100644 --- a/crates/net/p2p/src/beacon/envelope_checks.rs +++ b/crates/net/p2p/src/beacon/envelope_checks.rs @@ -33,6 +33,7 @@ use ethlambda_types::beacon::containers::gloas::SignedExecutionPayloadEnvelope; use tracing::{debug, error, warn}; use crate::P2PServer; +use crate::beacon::payload_hash; /// Whether a fetched envelope goes to the chain actor. pub(crate) fn forwards(outcome: &Outcome) -> bool { @@ -48,10 +49,14 @@ fn judge(store: &Store, envelope: &SignedExecutionPayloadEnvelope) -> Outcome { // A fresh, empty cache: a fetched envelope is not a gossip duplicate of // anything, so the cache's one rule never fires. let seen = SeenEnvelopes::new(NonZeroUsize::MIN); - match cheap_checks(&seen, store, envelope) { - Ok(()) => stateful_checks(store, envelope), - Err(outcome) => outcome, + if let Err(outcome) = cheap_checks(&seen, store, envelope) { + return outcome; } + // The same stricter-than-the-specification rule the gossip topic runs. + if let Err(outcome) = payload_hash::reject_envelope_on_mismatch(envelope) { + return outcome; + } + stateful_checks(store, envelope) } /// Run the gossip rules on `envelopes` and send the chain actor the ones that @@ -235,7 +240,13 @@ pub(crate) mod tests { } pub(crate) fn envelope(slot: u64, byte: u8) -> SignedExecutionPayloadEnvelope { - crate::beacon::encoding::test_support::envelope(H256::repeat_byte(byte), slot, H256::ZERO) + // Sealed: the fetched-envelope rules refuse a block hash that is not + // the payload's header's. + payload_hash::seal_envelope(crate::beacon::encoding::test_support::envelope( + H256::repeat_byte(byte), + slot, + H256::ZERO, + )) } #[test] diff --git a/crates/net/p2p/src/beacon/mod.rs b/crates/net/p2p/src/beacon/mod.rs index 33944b4f7..dce6adf2d 100644 --- a/crates/net/p2p/src/beacon/mod.rs +++ b/crates/net/p2p/src/beacon/mod.rs @@ -21,6 +21,7 @@ pub mod envelope_checks; pub mod fork_schedule; pub mod handler; pub mod messages; +pub mod payload_hash; pub mod protocols; pub mod scoring; pub mod subnets; diff --git a/crates/net/p2p/src/beacon/payload_hash.rs b/crates/net/p2p/src/beacon/payload_hash.rs new file mode 100644 index 000000000..b0e33b905 --- /dev/null +++ b/crates/net/p2p/src/beacon/payload_hash.rs @@ -0,0 +1,365 @@ +//! The execution block hash an execution payload claims, checked against the +//! header rebuilt from the payload's own fields. +//! +//! A payload carries its `block_hash` next to the fields that hash is taken +//! over, so a node can recompute it without an execution client: the header is +//! the payload's fields plus the transactions, withdrawals and requests tries, +//! RLP-encoded and keccak-hashed. The Engine API answers a mismatch with +//! `INVALID` and a null `latestValidHash` (execution-apis `shanghai.md`), which +//! is indistinguishable from a payload that executed badly and whose last valid +//! ancestor is unknown. Catching the structural case here keeps such a payload +//! from ever reaching fork choice. It is a rule stricter than the consensus +//! specification's gossip rules; see `docs/spec_deviations.md`. +//! +//! The header's shape per fork: +//! +//! | fork | adds to the header | +//! |-----------|-----------------------------------------------------------| +//! | bellatrix | the London fields, with `base_fee_per_gas` | +//! | capella | `withdrawals_root` | +//! | deneb | `blob_gas_used`, `excess_blob_gas`, `parent_beacon_block_root` | +//! | electra | `requests_hash` (fulu is the same) | +//! | gloas | `block_access_list_hash`, `slot_number` | +//! +//! Fields the execution layer fixes after the merge are constants here: no +//! ommers, difficulty zero, a zero nonce, `prev_randao` in the mix hash slot. +//! The header is encoded here rather than through ethrex's `BlockHeader`, +//! which holds `base_fee_per_gas` as a `u64` where the payload has 256 bits. + +use ethlambda_state_transition::beacon::gossip::{Outcome, RejectReason}; +use ethlambda_state_transition::beacon::stf::{electra, gloas as gloas_stf}; +use ethlambda_types::ShortRoot; +use ethlambda_types::beacon::containers::{SignedBeaconBlock, capella, gloas}; +use ethlambda_types::beacon::primitives::Root; +use ethrex_common::types::requests::{EncodedRequests, compute_requests_hash}; +use ethrex_common::types::{Withdrawal, compute_withdrawals_root}; +use ethrex_common::{Address, Bloom, Bytes, H256, NativeCrypto, U256}; +use ethrex_rlp::encode::RLPEncode; +use ethrex_rlp::structs::Encoder; +use ethrex_trie::Trie; +use tracing::warn; + +use crate::metrics; + +/// `keccak256(rlp([]))`: the ommers hash of every post-merge block. +const EMPTY_OMMERS_HASH: [u8; 32] = [ + 0x1d, 0xcc, 0x4d, 0xe8, 0xde, 0xc7, 0x5d, 0x7a, 0xab, 0x85, 0xb5, 0x67, 0xb6, 0xcc, 0xd4, 0x1a, + 0xd3, 0x12, 0x45, 0x1b, 0x94, 0x8a, 0x74, 0x13, 0xf0, 0xa1, 0x42, 0xfd, 0x40, 0xd4, 0x93, 0x47, +]; + +/// The hash the payload claims against the one its fields give. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HashMismatch { + pub claimed: [u8; 32], + pub computed: [u8; 32], +} + +/// What a payload and its surroundings supply to the header, whatever the fork. +struct Fields<'a> { + parent_hash: [u8; 32], + fee_recipient: [u8; 20], + state_root: [u8; 32], + receipts_root: [u8; 32], + logs_bloom: &'a [u8], + prev_randao: [u8; 32], + block_number: u64, + gas_limit: u64, + gas_used: u64, + timestamp: u64, + extra_data: &'a [u8], + /// 32 little-endian bytes, as SSZ carries it. + base_fee_per_gas: [u8; 32], + claimed_hash: [u8; 32], + transactions: Vec<&'a [u8]>, + /// `None` before capella. + withdrawals: Option<&'a [capella::Withdrawal]>, + /// `None` before deneb. + blob_gas: Option<(u64, u64)>, + /// `None` before deneb. + parent_beacon_block_root: Option<[u8; 32]>, + /// The Engine API's request list; `None` before electra. + requests: Option>>, + /// The payload's block access list bytes and slot number; `None` before gloas. + amsterdam: Option<(&'a [u8], u64)>, +} + +impl Fields<'_> { + /// `keccak256(rlp(header))`. + fn computed_hash(&self) -> [u8; 32] { + // The trie roots are over opaque bytes: a transaction is already + // `type || rlp(tx)` on the wire, so it is never decoded. + let transactions = self + .transactions + .iter() + .enumerate() + .map(|(index, tx)| (index.encode_to_vec(), tx.to_vec())); + let transactions_root = Trie::compute_hash_from_unsorted_iter(transactions, &NativeCrypto); + let withdrawals_root = self.withdrawals.map(|withdrawals| { + let withdrawals: Vec = withdrawals + .iter() + .map(|withdrawal| Withdrawal { + index: withdrawal.index, + validator_index: withdrawal.validator_index, + address: Address::from(withdrawal.address.0), + amount: withdrawal.amount, + }) + .collect(); + compute_withdrawals_root(&withdrawals, &NativeCrypto) + }); + let requests_hash = self.requests.as_ref().map(|requests| { + let requests: Vec = requests + .iter() + .map(|request| EncodedRequests(Bytes::copy_from_slice(request))) + .collect(); + compute_requests_hash(&requests) + }); + // EIP-7928: the header commits to the keccak of the RLP bytes the + // payload carries, whatever they decode to. + let block_access_list_hash = self + .amsterdam + .map(|(bal, _)| H256(ethrex_crypto_keccak(bal))); + let slot_number = self.amsterdam.map(|(_, slot)| slot); + let base_fee_per_gas = U256::from_little_endian(&self.base_fee_per_gas); + + let mut buf = Vec::with_capacity(700); + Encoder::new(&mut buf) + .encode_field(&H256(self.parent_hash)) + .encode_field(&H256(EMPTY_OMMERS_HASH)) + .encode_field(&Address::from(self.fee_recipient)) + .encode_field(&H256(self.state_root)) + .encode_field(&transactions_root) + .encode_field(&H256(self.receipts_root)) + .encode_field(&Bloom::from_slice(self.logs_bloom)) + .encode_field(&U256::zero()) + .encode_field(&self.block_number) + .encode_field(&self.gas_limit) + .encode_field(&self.gas_used) + .encode_field(&self.timestamp) + .encode_field(&Bytes::copy_from_slice(self.extra_data)) + .encode_field(&H256(self.prev_randao)) + .encode_field(&[0u8; 8]) + .encode_field(&base_fee_per_gas) + .encode_optional_field(&withdrawals_root) + .encode_optional_field(&self.blob_gas.map(|(used, _)| used)) + .encode_optional_field(&self.blob_gas.map(|(_, excess)| excess)) + .encode_optional_field(&self.parent_beacon_block_root.map(H256)) + .encode_optional_field(&requests_hash) + .encode_optional_field(&block_access_list_hash) + .encode_optional_field(&slot_number) + .finish(); + ethrex_crypto_keccak(&buf) + } + + fn verify(&self) -> Result<(), HashMismatch> { + let computed = self.computed_hash(); + if computed == self.claimed_hash { + Ok(()) + } else { + Err(HashMismatch { + claimed: self.claimed_hash, + computed, + }) + } + } +} + +fn ethrex_crypto_keccak(data: &[u8]) -> [u8; 32] { + ethrex_common::utils::keccak(data).0 +} + +/// The block's payload hash against its header, for every fork that carries +/// the payload in the block body. +/// +/// A block before bellatrix, a bellatrix block from before the merge (its +/// payload is the empty default), and a gloas block (its payload arrives in a +/// separate envelope) have nothing to check. +pub fn verify_block(block: &SignedBeaconBlock) -> Result<(), HashMismatch> { + match block { + SignedBeaconBlock::Bellatrix(block) => { + let payload = &block.message.body.execution_payload; + if payload.block_hash.0 == [0; 32] && payload.parent_hash.0 == [0; 32] { + return Ok(()); + } + Fields { + parent_hash: payload.parent_hash.0, + fee_recipient: payload.fee_recipient.0, + state_root: payload.state_root.0, + receipts_root: payload.receipts_root.0, + logs_bloom: payload.logs_bloom.as_ref(), + prev_randao: payload.prev_randao.0, + block_number: payload.block_number, + gas_limit: payload.gas_limit, + gas_used: payload.gas_used, + timestamp: payload.timestamp, + extra_data: payload.extra_data.as_ref(), + base_fee_per_gas: payload.base_fee_per_gas.0, + claimed_hash: payload.block_hash.0, + transactions: payload.transactions.iter().map(|tx| &tx[..]).collect(), + withdrawals: None, + blob_gas: None, + parent_beacon_block_root: None, + requests: None, + amsterdam: None, + } + .verify() + } + SignedBeaconBlock::Capella(block) => { + let payload = &block.message.body.execution_payload; + Fields { + parent_hash: payload.parent_hash.0, + fee_recipient: payload.fee_recipient.0, + state_root: payload.state_root.0, + receipts_root: payload.receipts_root.0, + logs_bloom: payload.logs_bloom.as_ref(), + prev_randao: payload.prev_randao.0, + block_number: payload.block_number, + gas_limit: payload.gas_limit, + gas_used: payload.gas_used, + timestamp: payload.timestamp, + extra_data: payload.extra_data.as_ref(), + base_fee_per_gas: payload.base_fee_per_gas.0, + claimed_hash: payload.block_hash.0, + transactions: payload.transactions.iter().map(|tx| &tx[..]).collect(), + withdrawals: Some(payload.withdrawals.as_ref()), + blob_gas: None, + parent_beacon_block_root: None, + requests: None, + amsterdam: None, + } + .verify() + } + // Deneb, and electra and fulu on top of it: one payload container, + // with the requests list the only addition. + SignedBeaconBlock::Deneb(_) + | SignedBeaconBlock::Electra(_) + | SignedBeaconBlock::Fulu(_) => { + let parent_root = match block { + SignedBeaconBlock::Deneb(block) => block.message.parent_root.0, + SignedBeaconBlock::Electra(block) | SignedBeaconBlock::Fulu(block) => { + block.message.parent_root.0 + } + _ => unreachable!("narrowed by the outer match"), + }; + let (payload, requests) = match block { + SignedBeaconBlock::Deneb(block) => (&block.message.body.execution_payload, None), + SignedBeaconBlock::Electra(block) | SignedBeaconBlock::Fulu(block) => ( + &block.message.body.execution_payload, + Some(electra::get_execution_requests_list( + &block.message.body.execution_requests, + )), + ), + _ => unreachable!("narrowed by the outer match"), + }; + Fields { + parent_hash: payload.parent_hash.0, + fee_recipient: payload.fee_recipient.0, + state_root: payload.state_root.0, + receipts_root: payload.receipts_root.0, + logs_bloom: payload.logs_bloom.as_ref(), + prev_randao: payload.prev_randao.0, + block_number: payload.block_number, + gas_limit: payload.gas_limit, + gas_used: payload.gas_used, + timestamp: payload.timestamp, + extra_data: payload.extra_data.as_ref(), + base_fee_per_gas: payload.base_fee_per_gas.0, + claimed_hash: payload.block_hash.0, + transactions: payload.transactions.iter().map(|tx| &tx[..]).collect(), + withdrawals: Some(payload.withdrawals.as_ref()), + blob_gas: Some((payload.blob_gas_used, payload.excess_blob_gas)), + parent_beacon_block_root: Some(parent_root), + requests, + amsterdam: None, + } + .verify() + } + SignedBeaconBlock::Phase0(_) + | SignedBeaconBlock::Altair(_) + | SignedBeaconBlock::Gloas(_) + | SignedBeaconBlock::Lean(_) => Ok(()), + } +} + +/// A gloas envelope's payload hash against its header. +pub fn verify_envelope( + envelope: &gloas::SignedExecutionPayloadEnvelope, +) -> Result<(), HashMismatch> { + let message = &envelope.message; + let payload = &message.payload; + Fields { + parent_hash: payload.parent_hash.0, + fee_recipient: payload.fee_recipient.0, + state_root: payload.state_root.0, + receipts_root: payload.receipts_root.0, + logs_bloom: payload.logs_bloom.as_ref(), + prev_randao: payload.prev_randao.0, + block_number: payload.block_number, + gas_limit: payload.gas_limit, + gas_used: payload.gas_used, + timestamp: payload.timestamp, + extra_data: payload.extra_data.as_ref(), + base_fee_per_gas: payload.base_fee_per_gas.0, + claimed_hash: payload.block_hash.0, + transactions: payload.transactions.iter().map(|tx| &tx[..]).collect(), + withdrawals: Some(payload.withdrawals.as_ref()), + blob_gas: Some((payload.blob_gas_used, payload.excess_blob_gas)), + parent_beacon_block_root: Some(message.parent_beacon_block_root.0), + requests: Some(gloas_stf::get_execution_requests_list( + &message.execution_requests, + )), + amsterdam: Some((payload.block_access_list.as_ref(), payload.slot_number)), + } + .verify() +} + +/// `envelope` with its payload's `block_hash` set to the header's, for tests +/// that build a structurally valid envelope out of arbitrary fields. +#[cfg(test)] +pub(crate) fn seal_envelope( + mut envelope: gloas::SignedExecutionPayloadEnvelope, +) -> gloas::SignedExecutionPayloadEnvelope { + if let Err(mismatch) = verify_envelope(&envelope) { + envelope.message.payload.block_hash = mismatch.computed.into(); + } + envelope +} + +/// [`verify_block`] as a gossip verdict: `Err(Reject)` on a mismatch, counted +/// and logged. +pub(crate) fn reject_block_on_mismatch( + block: &SignedBeaconBlock, + block_root: Root, +) -> Result<(), Outcome> { + verify_block(block).map_err(|mismatch| refuse("block", block.slot(), block_root, mismatch)) +} + +/// [`verify_envelope`] as a verdict, shared by the gossip topic and the +/// fetched-envelope checks. +pub(crate) fn reject_envelope_on_mismatch( + envelope: &gloas::SignedExecutionPayloadEnvelope, +) -> Result<(), Outcome> { + verify_envelope(envelope).map_err(|mismatch| { + refuse( + "envelope", + envelope.message.payload.slot_number, + envelope.message.beacon_block_root, + mismatch, + ) + }) +} + +fn refuse(kind: &str, slot: u64, root: Root, mismatch: HashMismatch) -> Outcome { + metrics::inc_beacon_payload_block_hash_mismatch(kind); + warn!( + kind, + slot, + root = %ShortRoot(&root.0), + claimed = %ShortRoot(&mismatch.claimed), + computed = %ShortRoot(&mismatch.computed), + "Refusing an execution payload whose block hash is not its header's" + ); + Outcome::Reject(RejectReason::PayloadBlockHashMismatch) +} + +#[cfg(test)] +mod tests; diff --git a/crates/net/p2p/src/beacon/payload_hash/tests.rs b/crates/net/p2p/src/beacon/payload_hash/tests.rs new file mode 100644 index 000000000..500be82c8 --- /dev/null +++ b/crates/net/p2p/src/beacon/payload_hash/tests.rs @@ -0,0 +1,233 @@ +//! Every fork the node follows, against a payload taken from a real chain. +//! The vectors are SSZ as the Beacon API served it +//! (`beacon/testdata/payload_hash/`): +//! +//! | fork | chain | object | +//! |-----------|-------------|----------------------| +//! | bellatrix | Sepolia | block, slot 500000 | +//! | capella | Sepolia | block, slot 1900000 | +//! | deneb | Hoodi | block, slot 2003 | +//! | electra | Hoodi | block, slot 70001 | +//! | fulu | mainnet | block, slot 15576000 | +//! | gloas | Plataberget | envelope, slot 593101 | + +use ethlambda_types::beacon::containers::electra; +use ethlambda_types::beacon::fork::ForkName; +use libssz::SszDecode as _; + +use super::*; + +const BELLATRIX: &[u8] = + include_bytes!("../testdata/payload_hash/sepolia_bellatrix_slot_500000.ssz"); +const CAPELLA: &[u8] = include_bytes!("../testdata/payload_hash/sepolia_capella_slot_1900000.ssz"); +const DENEB: &[u8] = include_bytes!("../testdata/payload_hash/hoodi_deneb_slot_2003.ssz"); +const ELECTRA: &[u8] = include_bytes!("../testdata/payload_hash/hoodi_electra_slot_70001.ssz"); +const FULU: &[u8] = include_bytes!("../testdata/payload_hash/mainnet_fulu_slot_15576000.ssz"); +const GLOAS: &[u8] = + include_bytes!("../testdata/payload_hash/plataberget_gloas_envelope_slot_593101.ssz"); + +fn block(fork: ForkName, bytes: &[u8]) -> SignedBeaconBlock { + SignedBeaconBlock::from_ssz(fork, bytes).expect("a real block decodes") +} + +fn envelope() -> gloas::SignedExecutionPayloadEnvelope { + gloas::SignedExecutionPayloadEnvelope::from_ssz_bytes(GLOAS).expect("a real envelope decodes") +} + +fn bellatrix_payload( + block: &mut SignedBeaconBlock, +) -> &mut ethlambda_types::beacon::containers::bellatrix::ExecutionPayload { + let SignedBeaconBlock::Bellatrix(inner) = block else { + panic!("bellatrix"); + }; + &mut inner.message.body.execution_payload +} + +/// An all-zero request: only its presence matters to the requests hash. +fn consolidation() -> electra::ConsolidationRequest { + electra::ConsolidationRequest::from_ssz_bytes(&[0; 116]).expect("a fixed-size container") +} + +fn withdrawal() -> capella::Withdrawal { + capella::Withdrawal::from_ssz_bytes(&[0; 44]).expect("a fixed-size container") +} + +fn assert_refused(result: Result<(), HashMismatch>, what: &str) { + assert!(result.is_err(), "{what}: the hash must no longer match"); +} + +#[test] +fn every_real_payload_matches_its_hash() { + for (fork, bytes) in [ + (ForkName::Bellatrix, BELLATRIX), + (ForkName::Capella, CAPELLA), + (ForkName::Deneb, DENEB), + (ForkName::Electra, ELECTRA), + (ForkName::Fulu, FULU), + ] { + let block = block(fork, bytes); + assert_eq!(verify_block(&block), Ok(()), "{fork:?}"); + } + assert_eq!(verify_envelope(&envelope()), Ok(()), "gloas"); +} + +#[test] +fn the_vectors_exercise_what_each_fork_adds() { + let SignedBeaconBlock::Capella(capella) = block(ForkName::Capella, CAPELLA) else { + panic!("capella"); + }; + assert!( + !capella + .message + .body + .execution_payload + .withdrawals + .is_empty(), + "capella's vector must carry withdrawals" + ); + let SignedBeaconBlock::Fulu(fulu) = block(ForkName::Fulu, FULU) else { + panic!("fulu"); + }; + let SignedBeaconBlock::Bellatrix(bellatrix) = block(ForkName::Bellatrix, BELLATRIX) else { + panic!("bellatrix"); + }; + // Past the merge: a pre-merge block carries the empty default payload. + assert_ne!( + bellatrix.message.body.execution_payload.block_hash.0, + [0; 32] + ); + assert!(!fulu.message.body.execution_payload.transactions.is_empty()); + assert!(!envelope().message.payload.transactions.is_empty()); +} + +#[test] +fn a_changed_bellatrix_field_is_refused() { + let mut changed = block(ForkName::Bellatrix, BELLATRIX); + bellatrix_payload(&mut changed).gas_used += 1; + assert_refused(verify_block(&changed), "gas_used"); + + let mut changed = block(ForkName::Bellatrix, BELLATRIX); + bellatrix_payload(&mut changed).block_hash.0[0] ^= 1; + assert_refused(verify_block(&changed), "block_hash"); +} + +#[test] +fn a_changed_capella_withdrawal_is_refused() { + let mut changed = block(ForkName::Capella, CAPELLA); + let SignedBeaconBlock::Capella(inner) = &mut changed else { + panic!("capella"); + }; + inner.message.body.execution_payload.withdrawals[0].amount += 1; + assert_refused(verify_block(&changed), "withdrawal amount"); +} + +#[test] +fn a_changed_deneb_field_is_refused() { + for what in ["excess_blob_gas", "parent_root", "timestamp"] { + let mut changed = block(ForkName::Deneb, DENEB); + let SignedBeaconBlock::Deneb(inner) = &mut changed else { + panic!("deneb"); + }; + match what { + "excess_blob_gas" => inner.message.body.execution_payload.excess_blob_gas += 1, + // The header commits to the parent beacon block root, which the + // payload does not carry: it comes from the block. + "parent_root" => inner.message.parent_root.0[0] ^= 1, + _ => inner.message.body.execution_payload.timestamp += 1, + } + assert_refused(verify_block(&changed), what); + } +} + +#[test] +fn a_changed_electra_or_fulu_field_is_refused() { + for (fork, bytes) in [(ForkName::Electra, ELECTRA), (ForkName::Fulu, FULU)] { + for what in ["transaction", "parent_root", "request", "extra_data"] { + let mut changed = block(fork, bytes); + let (SignedBeaconBlock::Electra(inner) | SignedBeaconBlock::Fulu(inner)) = &mut changed + else { + panic!("electra or fulu"); + }; + match what { + "transaction" => { + let payload = &mut inner.message.body.execution_payload; + let last = payload.transactions.len() - 1; + let tx = &mut payload.transactions[last]; + let end = tx.len() - 1; + tx[end] ^= 1; + } + "parent_root" => inner.message.parent_root.0[0] ^= 1, + // The requests hash covers a request however empty its + // payload: one more consolidation moves it. + "request" => inner + .message + .body + .execution_requests + .consolidations + .push(consolidation()) + .expect("room for one more request"), + _ => inner + .message + .body + .execution_payload + .extra_data + .push(1) + .expect("room for one more byte"), + } + assert_refused(verify_block(&changed), &format!("{fork:?} {what}")); + } + } +} + +#[test] +fn a_changed_gloas_field_is_refused() { + for what in [ + "slot_number", + "block_access_list", + "parent_beacon_block_root", + "execution_requests", + "withdrawal", + "transaction", + "base_fee_per_gas", + ] { + let mut changed = envelope(); + let message = &mut changed.message; + match what { + "slot_number" => message.payload.slot_number += 1, + "block_access_list" => message.payload.block_access_list.push(0), + "parent_beacon_block_root" => message.parent_beacon_block_root.0[0] ^= 1, + "execution_requests" => message + .execution_requests + .consolidations + .push(consolidation()), + "withdrawal" => message.payload.withdrawals.push(withdrawal()), + "transaction" => { + let last = message.payload.transactions.len() - 1; + let tx = &mut message.payload.transactions[last]; + let end = tx.len() - 1; + tx[end] ^= 1; + } + _ => message.payload.base_fee_per_gas.0[31] ^= 1, + } + assert_refused(verify_envelope(&changed), what); + } +} + +#[test] +fn a_pre_merge_bellatrix_payload_has_nothing_to_check() { + // Before the merge a block carries the empty default payload. + let mut empty = block(ForkName::Bellatrix, BELLATRIX); + let payload = bellatrix_payload(&mut empty); + payload.block_hash = Default::default(); + payload.parent_hash = Default::default(); + assert_eq!(verify_block(&empty), Ok(())); +} + +#[test] +fn a_mismatch_is_a_reject_with_its_own_reason() { + let mut changed = envelope(); + changed.message.payload.gas_used += 1; + let outcome = reject_envelope_on_mismatch(&changed).expect_err("a changed envelope"); + assert_eq!(outcome.labels(), ("reject", "payload_block_hash_mismatch")); + assert!(reject_envelope_on_mismatch(&envelope()).is_ok()); +} diff --git a/crates/net/p2p/src/beacon/testdata/payload_hash/hoodi_deneb_slot_2003.ssz b/crates/net/p2p/src/beacon/testdata/payload_hash/hoodi_deneb_slot_2003.ssz new file mode 100644 index 0000000000000000000000000000000000000000..0520bc7e8cf7f08444f9f1c34cabe7aa8bf03d6e GIT binary patch literal 22301 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 Xt&O3!0h{;@Z0QND-S6N8HMai)3H96R literal 0 HcmV?d00001 diff --git a/crates/net/p2p/src/beacon/testdata/payload_hash/mainnet_fulu_slot_15576000.ssz b/crates/net/p2p/src/beacon/testdata/payload_hash/mainnet_fulu_slot_15576000.ssz new file mode 100644 index 0000000000000000000000000000000000000000..3f2355c3d4cf286ec6c1d44a794f7037fe25839d GIT binary patch literal 71490 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 R%zZ^B!2}QhkB$F3{sUUl literal 0 HcmV?d00001 diff --git a/crates/net/p2p/src/beacon/testdata/payload_hash/sepolia_bellatrix_slot_500000.ssz b/crates/net/p2p/src/beacon/testdata/payload_hash/sepolia_bellatrix_slot_500000.ssz new file mode 100644 index 0000000000000000000000000000000000000000..3f721faeaaa047ca37a6f493635173414d88c28b GIT binary patch literal 2820 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 literal 0 HcmV?d00001 diff --git a/crates/net/p2p/src/gossipsub/handler.rs b/crates/net/p2p/src/gossipsub/handler.rs index d6b90bce0..bc06df996 100644 --- a/crates/net/p2p/src/gossipsub/handler.rs +++ b/crates/net/p2p/src/gossipsub/handler.rs @@ -36,6 +36,7 @@ use super::{ }; use crate::beacon::constants::ATTESTATION_SUBNET_COUNT; use crate::beacon::decode::BeaconGossip; +use crate::beacon::payload_hash; use crate::beacon::verdict::{self, Dispatch, GossipId, Validated}; use crate::beacon::{BeaconWire, decode as beacon_decode, topics as beacon_topics}; use crate::{P2PServer, metrics}; @@ -323,6 +324,11 @@ fn triage_block(server: &P2PServer, wire: &BeaconWire, payload: &[u8]) -> Dispat { return Dispatch::Report(outcome); } + // Stricter than the specification: see `beacon::payload_hash`. After the + // rules that drop most messages, since it hashes every transaction. + if let Err(outcome) = payload_hash::reject_block_on_mismatch(&block, block_root) { + return Dispatch::Report(outcome); + } Dispatch::Validate(Validated::Block { block: Box::new(block), block_root, @@ -529,6 +535,9 @@ fn triage_envelope(server: &P2PServer, payload: &[u8]) -> Dispatch { { return Dispatch::Report(outcome); } + if let Err(outcome) = payload_hash::reject_envelope_on_mismatch(&envelope) { + return Dispatch::Report(outcome); + } Dispatch::Validate(Validated::Envelope(Box::new(envelope))) } @@ -1762,7 +1771,7 @@ mod tests { #[tokio::test] async fn an_envelope_passes_triage_unless_its_key_was_seen() { let mut server = unconnected_beacon_server(Config::mainnet(), 0).await; - let envelope = crate::test_support::envelope(1, 3); + let envelope = payload_hash::seal_envelope(crate::test_support::envelope(1, 3)); let payload = envelope.to_ssz(); assert!(matches!( @@ -1780,6 +1789,20 @@ mod tests { )); } + /// An envelope whose block hash is not its payload header's is rejected + /// in triage, before any state is read, and counted. + #[tokio::test] + async fn an_envelope_with_a_wrong_block_hash_is_rejected_in_triage() { + let server = unconnected_beacon_server(Config::mainnet(), 0).await; + let mut envelope = payload_hash::seal_envelope(crate::test_support::envelope(1, 3)); + envelope.message.payload.gas_used += 1; + + assert!(matches!( + triage_envelope(&server, &envelope.to_ssz()), + Dispatch::Report(Outcome::Reject(RejectReason::PayloadBlockHashMismatch)) + )); + } + /// A payload attestation for a slot before gloas is rejected by the cheap /// checks; mainnet's gloas epoch is not zero, so slot zero is before it. #[tokio::test] diff --git a/crates/net/p2p/src/metrics.rs b/crates/net/p2p/src/metrics.rs index 643300967..a38546377 100644 --- a/crates/net/p2p/src/metrics.rs +++ b/crates/net/p2p/src/metrics.rs @@ -474,6 +474,24 @@ pub fn inc_beacon_gossip_verdict_expired(kind: &str) { .inc(); } +static LEAN_BEACON_PAYLOAD_BLOCK_HASH_MISMATCH_TOTAL: LazyLock = LazyLock::new( + || { + register_int_counter_vec!( + "lean_beacon_payload_block_hash_mismatch_total", + "Execution payloads refused because their block hash is not the hash of the header rebuilt from their fields, by kind (block, envelope)", + &["kind"] + ) + .unwrap() + }, +); + +/// Count one payload refused for a block hash that is not its header's. +pub fn inc_beacon_payload_block_hash_mismatch(kind: &str) { + LEAN_BEACON_PAYLOAD_BLOCK_HASH_MISMATCH_TOTAL + .with_label_values(&[kind]) + .inc(); +} + pub fn inc_beacon_status_digest_mismatch() { LEAN_BEACON_STATUS_DIGEST_MISMATCH_TOTAL.inc(); } diff --git a/crates/net/p2p/src/req_resp/envelope_client.rs b/crates/net/p2p/src/req_resp/envelope_client.rs index 1714ce3dc..e10f16529 100644 --- a/crates/net/p2p/src/req_resp/envelope_client.rs +++ b/crates/net/p2p/src/req_resp/envelope_client.rs @@ -384,7 +384,13 @@ mod tests { use crate::{BACKOFF_MULTIPLIER, ConnectionDirection, INITIAL_BACKOFF_MS, MAX_FETCH_RETRIES}; fn envelope_for(slot: u64, block_root: H256) -> SignedExecutionPayloadEnvelope { - crate::beacon::encoding::test_support::envelope(block_root, slot, H256::ZERO) + // Sealed: the fetched-envelope rules refuse a block hash that is not + // the payload's header's. + crate::beacon::payload_hash::seal_envelope(crate::beacon::encoding::test_support::envelope( + block_root, + slot, + H256::ZERO, + )) } fn fulu_block_at(slot: u64) -> SignedBeaconBlock { diff --git a/docs/beacon_wire.md b/docs/beacon_wire.md index c2138785b..8d3afd997 100644 --- a/docs/beacon_wire.md +++ b/docs/beacon_wire.md @@ -212,6 +212,14 @@ an unbounded progressive bitlist, so one longer than electra's type bound is REJECTed first, reading its length without expanding it, since expanding is what the seen-set check does before any other. +One rule on `beacon_block` (bellatrix to fulu) and `execution_payload` is +stricter than the specification: the payload's `block_hash` must equal the hash +of the execution header rebuilt from the payload's own fields, or the message is +REJECTed (`beacon/payload_hash.rs`; the reason is `payload_block_hash_mismatch`, +and `lean_beacon_payload_block_hash_mismatch_total{kind}` counts it). It runs +after the cheap rules and before the stateful half. See +[spec_deviations.md](./spec_deviations.md#a-payloads-block-hash-must-be-its-headers). + Three deliberate departures from the gloas gossip rules, each for want of state this node does not keep. Where the specification rejects a message whose block failed validation, this node (having no bad-block cache) cannot tell that from @@ -603,7 +611,8 @@ not imported only briefly and in small numbers, so a batch's envelopes must arrive before the next batch's blocks do. Everything fetched goes through the same rules as an `execution_payload` gossip message (`beacon/envelope_checks.rs`) before the actor sees it; a `Queue` verdict is -forwarded too. +forwarded too. That includes the payload block hash rule above, which the +specification does not have. ### Data column sidecars @@ -806,6 +815,7 @@ as a query filter, so a `quic`-only record is invisible to it. | `lean_beacon_gossip_messages_total{topic,result}` | Gossip received, by topic and by `decoded` / `decode_failed` / `decompress_failed` / `unsupported_fork` | | `lean_beacon_gossip_validation_total{kind,outcome,reason}` | Gossip verdicts, now including `beacon_aggregate_and_proof` and `beacon_attestation`; see [metrics.md](./metrics.md#beacon-gossip-validation) | | `lean_beacon_gossip_verdict_expired_total{kind}` | Verdicts that came too late to propagate anything; should stay at zero | +| `lean_beacon_payload_block_hash_mismatch_total{kind}` | Payloads refused because `block_hash` is not the hash of the header rebuilt from their fields, by `block` / `envelope` | | `lean_beacon_status_digest_mismatch_total` | Handshakes seen from another fork digest | | `lean_beacon_fork_digest{digest}` | The digest computed at startup, as a label | | `lean_beacon_aggregate_decode_seconds` | Time spent decoding one aggregate off the wire | diff --git a/docs/spec_deviations.md b/docs/spec_deviations.md index 191be46cd..2eb21418d 100644 --- a/docs/spec_deviations.md +++ b/docs/spec_deviations.md @@ -428,6 +428,37 @@ that the tests compare like with like. - **Equivalence:** each is the specification's answer wherever the specification has one; they differ only where it would have raised. +## A payload's block hash must be its header's + +`beacon_block` (bellatrix to fulu) and `execution_payload` gossip, and every +envelope fetched by root or range, are REJECTed when the payload's `block_hash` +is not `keccak256(rlp(header))` for the execution block header rebuilt from the +payload's own fields. + +- **The specification:** has no such rule. A hash that does not match is the + execution client's to find, when the chain actor hands it the payload. +- **ethlambda:** rebuilds the header in `ethlambda-p2p` + (`beacon/payload_hash.rs`) and refuses the message before the stateful rules + run. The header takes the payload's fields, the transactions and withdrawals + tries, the parent beacon block root (the block's `parent_root`; the + envelope's own field in gloas), the EIP-7685 requests hash (the Engine API's + request list, `get_execution_requests_list`), and in gloas the keccak of the + block access list bytes (EIP-7928) and the slot number (EIP-7843). It is run + per fork against real chain data: see the vector table in the module's tests. + A bellatrix payload that is the empty default (before the merge) is skipped. +- **Why:** since `engine_newPayloadV2` the Engine API answers a block-hash + mismatch with `INVALID` and a null `latestValidHash`, the same answer as for + a payload that executed badly and whose last valid ancestor is unknown. + Applying payloads to fork choice before the execution client answers is only + safe if the structural case cannot get that far. +- **Cost and counter:** one trie over the transactions per accepted-looking + message, after the cheap rules that drop most of them. + `lean_beacon_payload_block_hash_mismatch_total{kind}` counts refusals and a + `warn!` names the slot and root. +- **Equivalence:** a stricter filter, never a looser one: every payload it + refuses is one the execution client would call `INVALID`. Blocks fetched by + `beacon_blocks_by_range/root` are not checked here. + ## The envelope check reads the cached state root `verify_execution_payload_envelope` compares `envelope.beacon_block_root` with