From 893f4cf2a27034f4550d3f4443a0fa7c2cd2444b Mon Sep 17 00:00:00 2001 From: Dan Cline <6798349+Rjected@users.noreply.github.com> Date: Wed, 30 Aug 2023 17:05:46 -0700 Subject: [PATCH] feat: validate payload versioned hashes (#4417) Co-authored-by: Matthias Seitz --- crates/consensus/beacon/src/engine/mod.rs | 87 +++++++++++++++++-- crates/primitives/src/block.rs | 5 ++ .../rpc/rpc-types/src/eth/engine/payload.rs | 3 + 3 files changed, 87 insertions(+), 8 deletions(-) diff --git a/crates/consensus/beacon/src/engine/mod.rs b/crates/consensus/beacon/src/engine/mod.rs index 71bc15da3..30ca834d0 100644 --- a/crates/consensus/beacon/src/engine/mod.rs +++ b/crates/consensus/beacon/src/engine/mod.rs @@ -31,8 +31,8 @@ use reth_provider::{ }; use reth_prune::Pruner; use reth_rpc_types::engine::{ - CancunPayloadFields, ExecutionPayload, PayloadAttributes, PayloadStatus, PayloadStatusEnum, - PayloadValidationError, + CancunPayloadFields, ExecutionPayload, PayloadAttributes, PayloadError, PayloadStatus, + PayloadStatusEnum, PayloadValidationError, }; use reth_stages::{ControlFlow, Pipeline, PipelineError}; use reth_tasks::TaskSpawner; @@ -1056,10 +1056,7 @@ where payload: ExecutionPayload, cancun_fields: Option, ) -> Result { - let block = match self.ensure_well_formed_payload( - payload, - cancun_fields.map(|fields| fields.parent_beacon_block_root), - ) { + let block = match self.ensure_well_formed_payload(payload, cancun_fields) { Ok(block) => block, Err(status) => return Ok(status), }; @@ -1120,13 +1117,18 @@ where /// - missing or invalid base fee /// - invalid extra data /// - invalid transactions + /// - incorrect hash + /// - the versioned hashes passed with the payload do not exactly match transaction + /// versioned hashes fn ensure_well_formed_payload( &self, payload: ExecutionPayload, - parent_beacon_block_root: Option, + cancun_fields: Option, ) -> Result { let parent_hash = payload.parent_hash(); - let block = match payload.try_into_sealed_block(parent_beacon_block_root) { + let block = match payload.try_into_sealed_block( + cancun_fields.as_ref().map(|fields| fields.parent_beacon_block_root), + ) { Ok(block) => block, Err(error) => { error!(target: "consensus::engine", ?error, "Invalid payload"); @@ -1144,9 +1146,78 @@ where } }; + let block_versioned_hashes = block + .blob_transactions() + .iter() + .filter_map(|tx| tx.as_eip4844().map(|blob_tx| &blob_tx.blob_versioned_hashes)) + .flatten() + .collect::>(); + + self.validate_versioned_hashes(parent_hash, block_versioned_hashes, cancun_fields)?; + Ok(block) } + /// Validates that the versioned hashes in the block match the versioned hashes passed in the + /// [CancunPayloadFields], if the cancun payload fields are provided. If the payload fields are + /// not provided, but versioned hashes exist in the block, this returns a [PayloadStatus] with + /// the [PayloadError::InvalidVersionedHashes] error. + /// + /// This validates versioned hashes according to the Engine API Cancun spec: + /// + fn validate_versioned_hashes( + &self, + parent_hash: H256, + block_versioned_hashes: Vec<&H256>, + cancun_fields: Option, + ) -> Result<(), PayloadStatus> { + // This validates the following engine API rule: + // + // 3. Given the expected array of blob versioned hashes client software **MUST** run its + // validation by taking the following steps: + // + // 1. Obtain the actual array by concatenating blob versioned hashes lists + // (`tx.blob_versioned_hashes`) of each [blob + // transaction](https://eips.ethereum.org/EIPS/eip-4844#new-transaction-type) included + // in the payload, respecting the order of inclusion. If the payload has no blob + // transactions the expected array **MUST** be `[]`. + // + // 2. Return `{status: INVALID, latestValidHash: null, validationError: errorMessage | + // null}` if the expected and the actual arrays don't match. + // + // This validation **MUST** be instantly run in all cases even during active sync process. + if let Some(fields) = cancun_fields { + if block_versioned_hashes.len() != fields.versioned_hashes.len() { + // if the lengths don't match then we know that the payload is invalid + let latest_valid_hash = + self.latest_valid_hash_for_invalid_payload(parent_hash, None); + let status = PayloadStatusEnum::from(PayloadError::InvalidVersionedHashes); + return Err(PayloadStatus::new(status, latest_valid_hash)) + } + + // we can use `zip` safely here because we already compared their length + let zipped_versioned_hashes = + fields.versioned_hashes.iter().zip(block_versioned_hashes); + for (payload_versioned_hash, block_versioned_hash) in zipped_versioned_hashes { + if payload_versioned_hash != block_versioned_hash { + // One of the hashes does not match - return invalid + let latest_valid_hash = + self.latest_valid_hash_for_invalid_payload(parent_hash, None); + let status = PayloadStatusEnum::from(PayloadError::InvalidVersionedHashes); + return Err(PayloadStatus::new(status, latest_valid_hash)) + } + } + } else if !block_versioned_hashes.is_empty() { + // there are versioned hashes in the block but no expected versioned hashes were + // provided in the new payload call, so the payload is invalid + let latest_valid_hash = self.latest_valid_hash_for_invalid_payload(parent_hash, None); + let status = PayloadStatusEnum::from(PayloadError::InvalidVersionedHashes); + return Err(PayloadStatus::new(status, latest_valid_hash)) + } + + Ok(()) + } + /// When the pipeline or the pruner is active, the tree is unable to commit any additional /// blocks since the pipeline holds exclusive access to the database. /// diff --git a/crates/primitives/src/block.rs b/crates/primitives/src/block.rs index 5248787c1..01a477482 100644 --- a/crates/primitives/src/block.rs +++ b/crates/primitives/src/block.rs @@ -161,6 +161,11 @@ impl SealedBlock { ) } + /// Returns only the blob transactions, if any, from the block body. + pub fn blob_transactions(&self) -> Vec<&TransactionSigned> { + self.body.iter().filter(|tx| tx.is_eip4844()).collect() + } + /// Expensive operation that recovers transaction signer. See [SealedBlockWithSenders]. pub fn senders(&self) -> Option> { TransactionSigned::recover_signers(&self.body, self.body.len()) diff --git a/crates/rpc/rpc-types/src/eth/engine/payload.rs b/crates/rpc/rpc-types/src/eth/engine/payload.rs index 7749ce41b..3026260d3 100644 --- a/crates/rpc/rpc-types/src/eth/engine/payload.rs +++ b/crates/rpc/rpc-types/src/eth/engine/payload.rs @@ -557,6 +557,9 @@ pub enum PayloadError { /// The block hash provided with the payload. consensus: H256, }, + /// Expected blob versioned hashes do not match the given transactions. + #[error("Expected blob versioned hashes do not match the given transactions")] + InvalidVersionedHashes, /// Encountered decoding error. #[error(transparent)] Decode(#[from] reth_rlp::DecodeError),