Skip to content
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

feat: validate payload versioned hashes #4417

Merged
merged 4 commits into from
Aug 31, 2023
Merged
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
87 changes: 79 additions & 8 deletions crates/consensus/beacon/src/engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1056,10 +1056,7 @@ where
payload: ExecutionPayload,
cancun_fields: Option<CancunPayloadFields>,
) -> Result<PayloadStatus, BeaconOnNewPayloadError> {
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),
};
Expand Down Expand Up @@ -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<H256>,
cancun_fields: Option<CancunPayloadFields>,
) -> Result<SealedBlock, PayloadStatus> {
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");
Expand All @@ -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::<Vec<_>>();

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:
/// <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md#specification>
fn validate_versioned_hashes(
&self,
parent_hash: H256,
block_versioned_hashes: Vec<&H256>,
cancun_fields: Option<CancunPayloadFields>,
) -> 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.
///
Expand Down
5 changes: 5 additions & 0 deletions crates/primitives/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<Address>> {
TransactionSigned::recover_signers(&self.body, self.body.len())
Expand Down
3 changes: 3 additions & 0 deletions crates/rpc/rpc-types/src/eth/engine/payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Loading