From 68e902db2e22492b0042ba76f13acff32191f2ba Mon Sep 17 00:00:00 2001 From: joshieDo <93316087+joshieDo@users.noreply.github.com> Date: Fri, 14 Jun 2024 13:21:45 +0200 Subject: [PATCH] chore: move header validation from `reth-primitives` to consensus crates (#8802) --- crates/consensus/common/src/validation.rs | 138 +++++--- crates/consensus/consensus/src/lib.rs | 65 +++- crates/ethereum/consensus/src/lib.rs | 233 ++++++++++++- crates/optimism/consensus/src/lib.rs | 17 +- crates/primitives/src/header.rs | 392 +--------------------- crates/primitives/src/lib.rs | 2 +- 6 files changed, 390 insertions(+), 457 deletions(-) diff --git a/crates/consensus/common/src/validation.rs b/crates/consensus/common/src/validation.rs index e06cceec9..1b482fd5b 100644 --- a/crates/consensus/common/src/validation.rs +++ b/crates/consensus/common/src/validation.rs @@ -9,56 +9,29 @@ use reth_primitives::{ ChainSpec, GotExpected, Hardfork, Header, SealedBlock, SealedHeader, }; -/// Validate header standalone -pub fn validate_header_standalone( - header: &SealedHeader, - chain_spec: &ChainSpec, -) -> Result<(), ConsensusError> { - // Gas used needs to be less than gas limit. Gas used is going to be checked after execution. +/// Gas used needs to be less than gas limit. Gas used is going to be checked after execution. +#[inline] +pub fn validate_header_gas(header: &SealedHeader) -> Result<(), ConsensusError> { if header.gas_used > header.gas_limit { return Err(ConsensusError::HeaderGasUsedExceedsGasLimit { gas_used: header.gas_used, gas_limit: header.gas_limit, }) } + Ok(()) +} - // Check if base fee is set. +/// Ensure the EIP-1559 base fee is set if the London hardfork is active. +#[inline] +pub fn validate_header_base_fee( + header: &SealedHeader, + chain_spec: &ChainSpec, +) -> Result<(), ConsensusError> { if chain_spec.fork(Hardfork::London).active_at_block(header.number) && header.base_fee_per_gas.is_none() { return Err(ConsensusError::BaseFeeMissing) } - - let wd_root_missing = header.withdrawals_root.is_none() && !chain_spec.is_optimism(); - - // EIP-4895: Beacon chain push withdrawals as operations - if chain_spec.is_shanghai_active_at_timestamp(header.timestamp) && wd_root_missing { - return Err(ConsensusError::WithdrawalsRootMissing) - } else if !chain_spec.is_shanghai_active_at_timestamp(header.timestamp) && - header.withdrawals_root.is_some() - { - return Err(ConsensusError::WithdrawalsRootUnexpected) - } - - // Ensures that EIP-4844 fields are valid once cancun is active. - if chain_spec.is_cancun_active_at_timestamp(header.timestamp) { - validate_4844_header_standalone(header)?; - } else if header.blob_gas_used.is_some() { - return Err(ConsensusError::BlobGasUsedUnexpected) - } else if header.excess_blob_gas.is_some() { - return Err(ConsensusError::ExcessBlobGasUnexpected) - } else if header.parent_beacon_block_root.is_some() { - return Err(ConsensusError::ParentBeaconBlockRootUnexpected) - } - - if chain_spec.is_prague_active_at_timestamp(header.timestamp) { - if header.requests_root.is_none() { - return Err(ConsensusError::RequestsRootMissing) - } - } else if header.requests_root.is_some() { - return Err(ConsensusError::RequestsRootUnexpected) - } - Ok(()) } @@ -175,6 +148,7 @@ pub fn validate_4844_header_standalone(header: &SealedHeader) -> Result<(), Cons /// /// From yellow paper: extraData: An arbitrary byte array containing data relevant to this block. /// This must be 32 bytes or fewer; formally Hx. +#[inline] pub fn validate_header_extradata(header: &Header) -> Result<(), ConsensusError> { if header.extra_data.len() > MAXIMUM_EXTRA_DATA_SIZE { Err(ConsensusError::ExtraDataExceedsMax { len: header.extra_data.len() }) @@ -183,6 +157,78 @@ pub fn validate_header_extradata(header: &Header) -> Result<(), ConsensusError> } } +/// Validates against the parent hash and number. +/// +/// This function ensures that the header block number is sequential and that the hash of the parent +/// header matches the parent hash in the header. +#[inline] +pub fn validate_against_parent_hash_number( + header: &SealedHeader, + parent: &SealedHeader, +) -> Result<(), ConsensusError> { + // Parent number is consistent. + if parent.number + 1 != header.number { + return Err(ConsensusError::ParentBlockNumberMismatch { + parent_block_number: parent.number, + block_number: header.number, + }) + } + + if parent.hash() != header.parent_hash { + return Err(ConsensusError::ParentHashMismatch( + GotExpected { got: header.parent_hash, expected: parent.hash() }.into(), + )) + } + + Ok(()) +} + +/// Validates the base fee against the parent and EIP-1559 rules. +#[inline] +pub fn validate_against_parent_eip1559_base_fee( + header: &SealedHeader, + parent: &SealedHeader, + chain_spec: &ChainSpec, +) -> Result<(), ConsensusError> { + if chain_spec.fork(Hardfork::London).active_at_block(header.number) { + let base_fee = header.base_fee_per_gas.ok_or(ConsensusError::BaseFeeMissing)?; + + let expected_base_fee = + if chain_spec.fork(Hardfork::London).transitions_at_block(header.number) { + reth_primitives::constants::EIP1559_INITIAL_BASE_FEE + } else { + // This BaseFeeMissing will not happen as previous blocks are checked to have + // them. + parent + .next_block_base_fee(chain_spec.base_fee_params_at_timestamp(header.timestamp)) + .ok_or(ConsensusError::BaseFeeMissing)? + }; + if expected_base_fee != base_fee { + return Err(ConsensusError::BaseFeeDiff(GotExpected { + expected: expected_base_fee, + got: base_fee, + })) + } + } + + Ok(()) +} + +/// Validates the timestamp against the parent to make sure it is in the past. +#[inline] +pub fn validate_against_parent_timestamp( + header: &SealedHeader, + parent: &SealedHeader, +) -> Result<(), ConsensusError> { + if header.is_timestamp_in_past(parent.timestamp) { + return Err(ConsensusError::TimestampIsInPast { + parent_timestamp: parent.timestamp, + timestamp: header.timestamp, + }) + } + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -410,22 +456,6 @@ mod tests { .return_const(Ok(Some(Withdrawal { index: 2, ..Default::default() }))); } - #[test] - fn shanghai_block_zero_withdrawals() { - // ensures that if shanghai is activated, and we include a block with a withdrawals root, - // that the header is valid - let chain_spec = ChainSpecBuilder::mainnet().shanghai_activated().build(); - - let header = Header { - base_fee_per_gas: Some(1337u64), - withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])), - ..Default::default() - } - .seal_slow(); - - assert_eq!(validate_header_standalone(&header, &chain_spec), Ok(())); - } - #[test] fn cancun_block_incorrect_blob_gas_used() { let chain_spec = ChainSpecBuilder::mainnet().cancun_activated().build(); diff --git a/crates/consensus/consensus/src/lib.rs b/crates/consensus/consensus/src/lib.rs index 5768fee46..7aee9f15e 100644 --- a/crates/consensus/consensus/src/lib.rs +++ b/crates/consensus/consensus/src/lib.rs @@ -10,8 +10,8 @@ #![cfg_attr(not(feature = "std"), no_std)] use reth_primitives::{ - BlockHash, BlockNumber, BlockWithSenders, Bloom, GotExpected, GotExpectedBoxed, Header, - HeaderValidationError, InvalidTransactionError, Receipt, Request, SealedBlock, SealedHeader, + constants::MINIMUM_GAS_LIMIT, BlockHash, BlockNumber, BlockWithSenders, Bloom, GotExpected, + GotExpectedBoxed, Header, InvalidTransactionError, Receipt, Request, SealedBlock, SealedHeader, B256, U256, }; @@ -206,6 +206,10 @@ pub enum ConsensusError { block_number: BlockNumber, }, + /// Error when the parent hash does not match the expected parent hash. + #[error("mismatched parent hash: {0}")] + ParentHashMismatch(GotExpectedBoxed), + /// Error when the block timestamp is in the future compared to our clock time. #[error("block timestamp {timestamp} is in the future compared to our clock time {present_timestamp}")] TimestampIsInFuture { @@ -329,9 +333,60 @@ pub enum ConsensusError { #[error(transparent)] InvalidTransaction(#[from] InvalidTransactionError), - /// Error type transparently wrapping `HeaderValidationError`. - #[error(transparent)] - HeaderValidationError(#[from] HeaderValidationError), + /// Error when the block's base fee is different from the expected base fee. + #[error("block base fee mismatch: {0}")] + BaseFeeDiff(GotExpected), + + /// Error when there is an invalid excess blob gas. + #[error( + "invalid excess blob gas: {diff}; \ + parent excess blob gas: {parent_excess_blob_gas}, \ + parent blob gas used: {parent_blob_gas_used}" + )] + ExcessBlobGasDiff { + /// The excess blob gas diff. + diff: GotExpected, + /// The parent excess blob gas. + parent_excess_blob_gas: u64, + /// The parent blob gas used. + parent_blob_gas_used: u64, + }, + + /// Error when the child gas limit exceeds the maximum allowed increase. + #[error("child gas_limit {child_gas_limit} max increase is {parent_gas_limit}/1024")] + GasLimitInvalidIncrease { + /// The parent gas limit. + parent_gas_limit: u64, + /// The child gas limit. + child_gas_limit: u64, + }, + + /// Error indicating that the child gas limit is below the minimum allowed limit. + /// + /// This error occurs when the child gas limit is less than the specified minimum gas limit. + #[error("child gas limit {child_gas_limit} is below the minimum allowed limit ({MINIMUM_GAS_LIMIT})")] + GasLimitInvalidMinimum { + /// The child gas limit. + child_gas_limit: u64, + }, + + /// Error when the child gas limit exceeds the maximum allowed decrease. + #[error("child gas_limit {child_gas_limit} max decrease is {parent_gas_limit}/1024")] + GasLimitInvalidDecrease { + /// The parent gas limit. + parent_gas_limit: u64, + /// The child gas limit. + child_gas_limit: u64, + }, + + /// Error when the block timestamp is in the past compared to the parent timestamp. + #[error("block timestamp {timestamp} is in the past compared to the parent timestamp {parent_timestamp}")] + TimestampIsInPast { + /// The parent block's timestamp. + parent_timestamp: u64, + /// The block's timestamp. + timestamp: u64, + }, } impl ConsensusError { diff --git a/crates/ethereum/consensus/src/lib.rs b/crates/ethereum/consensus/src/lib.rs index dc9821d4f..d37b8bbc3 100644 --- a/crates/ethereum/consensus/src/lib.rs +++ b/crates/ethereum/consensus/src/lib.rs @@ -10,11 +10,15 @@ use reth_consensus::{Consensus, ConsensusError, PostExecutionInput}; use reth_consensus_common::validation::{ - validate_block_pre_execution, validate_header_extradata, validate_header_standalone, + validate_4844_header_standalone, validate_against_parent_eip1559_base_fee, + validate_against_parent_hash_number, validate_against_parent_timestamp, + validate_block_pre_execution, validate_header_base_fee, validate_header_extradata, + validate_header_gas, }; use reth_primitives::{ - BlockWithSenders, Chain, ChainSpec, Hardfork, Header, SealedBlock, SealedHeader, - EMPTY_OMMER_ROOT_HASH, U256, + constants::MINIMUM_GAS_LIMIT, eip4844::calculate_excess_blob_gas, BlockWithSenders, Chain, + ChainSpec, GotExpected, Hardfork, Header, SealedBlock, SealedHeader, EMPTY_OMMER_ROOT_HASH, + U256, }; use std::{sync::Arc, time::SystemTime}; @@ -35,11 +39,122 @@ impl EthBeaconConsensus { pub const fn new(chain_spec: Arc) -> Self { Self { chain_spec } } + + /// Validates that the EIP-4844 header fields are correct with respect to the parent block. This + /// ensures that the `blob_gas_used` and `excess_blob_gas` fields exist in the child header, and + /// that the `excess_blob_gas` field matches the expected `excess_blob_gas` calculated from the + /// parent header fields. + pub fn validate_against_parent_4844( + header: &SealedHeader, + parent: &SealedHeader, + ) -> Result<(), ConsensusError> { + // From [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844#header-extension): + // + // > For the first post-fork block, both parent.blob_gas_used and parent.excess_blob_gas + // > are evaluated as 0. + // + // This means in the first post-fork block, calculate_excess_blob_gas will return 0. + let parent_blob_gas_used = parent.blob_gas_used.unwrap_or(0); + let parent_excess_blob_gas = parent.excess_blob_gas.unwrap_or(0); + + if header.blob_gas_used.is_none() { + return Err(ConsensusError::BlobGasUsedMissing) + } + let excess_blob_gas = header.excess_blob_gas.ok_or(ConsensusError::ExcessBlobGasMissing)?; + + let expected_excess_blob_gas = + calculate_excess_blob_gas(parent_excess_blob_gas, parent_blob_gas_used); + if expected_excess_blob_gas != excess_blob_gas { + return Err(ConsensusError::ExcessBlobGasDiff { + diff: GotExpected { got: excess_blob_gas, expected: expected_excess_blob_gas }, + parent_excess_blob_gas, + parent_blob_gas_used, + }) + } + + Ok(()) + } + + /// Checks the gas limit for consistency between parent and self headers. + /// + /// The maximum allowable difference between self and parent gas limits is determined by the + /// parent's gas limit divided by the elasticity multiplier (1024). + fn validate_against_parent_gas_limit( + &self, + header: &SealedHeader, + parent: &SealedHeader, + ) -> Result<(), ConsensusError> { + // Determine the parent gas limit, considering elasticity multiplier on the London fork. + let parent_gas_limit = + if self.chain_spec.fork(Hardfork::London).transitions_at_block(header.number) { + parent.gas_limit * + self.chain_spec + .base_fee_params_at_timestamp(header.timestamp) + .elasticity_multiplier as u64 + } else { + parent.gas_limit + }; + + // Check for an increase in gas limit beyond the allowed threshold. + if header.gas_limit > parent_gas_limit { + if header.gas_limit - parent_gas_limit >= parent_gas_limit / 1024 { + return Err(ConsensusError::GasLimitInvalidIncrease { + parent_gas_limit, + child_gas_limit: header.gas_limit, + }) + } + } + // Check for a decrease in gas limit beyond the allowed threshold. + else if parent_gas_limit - header.gas_limit >= parent_gas_limit / 1024 { + return Err(ConsensusError::GasLimitInvalidDecrease { + parent_gas_limit, + child_gas_limit: header.gas_limit, + }) + } + // Check if the self gas limit is below the minimum required limit. + else if header.gas_limit < MINIMUM_GAS_LIMIT { + return Err(ConsensusError::GasLimitInvalidMinimum { child_gas_limit: header.gas_limit }) + } + + Ok(()) + } } impl Consensus for EthBeaconConsensus { fn validate_header(&self, header: &SealedHeader) -> Result<(), ConsensusError> { - validate_header_standalone(header, &self.chain_spec)?; + validate_header_gas(header)?; + validate_header_base_fee(header, &self.chain_spec)?; + + // EIP-4895: Beacon chain push withdrawals as operations + if self.chain_spec.is_shanghai_active_at_timestamp(header.timestamp) && + header.withdrawals_root.is_none() + { + return Err(ConsensusError::WithdrawalsRootMissing) + } else if !self.chain_spec.is_shanghai_active_at_timestamp(header.timestamp) && + header.withdrawals_root.is_some() + { + return Err(ConsensusError::WithdrawalsRootUnexpected) + } + + // Ensures that EIP-4844 fields are valid once cancun is active. + if self.chain_spec.is_cancun_active_at_timestamp(header.timestamp) { + validate_4844_header_standalone(header)?; + } else if header.blob_gas_used.is_some() { + return Err(ConsensusError::BlobGasUsedUnexpected) + } else if header.excess_blob_gas.is_some() { + return Err(ConsensusError::ExcessBlobGasUnexpected) + } else if header.parent_beacon_block_root.is_some() { + return Err(ConsensusError::ParentBeaconBlockRootUnexpected) + } + + if self.chain_spec.is_prague_active_at_timestamp(header.timestamp) { + if header.requests_root.is_none() { + return Err(ConsensusError::RequestsRootMissing) + } + } else if header.requests_root.is_some() { + return Err(ConsensusError::RequestsRootUnexpected) + } + Ok(()) } @@ -48,7 +163,21 @@ impl Consensus for EthBeaconConsensus { header: &SealedHeader, parent: &SealedHeader, ) -> Result<(), ConsensusError> { - header.validate_against_parent(parent, &self.chain_spec).map_err(ConsensusError::from)?; + validate_against_parent_hash_number(header, parent)?; + + validate_against_parent_timestamp(header, parent)?; + + // TODO Check difficulty increment between parent and self + // Ace age did increment it by some formula that we need to follow. + self.validate_against_parent_gas_limit(header, parent)?; + + validate_against_parent_eip1559_base_fee(header, parent, &self.chain_spec)?; + + // ensure that the blob gas fields for this block + if self.chain_spec.is_cancun_active_at_timestamp(header.timestamp) { + Self::validate_against_parent_4844(header, parent)?; + } + Ok(()) } @@ -127,3 +256,97 @@ impl Consensus for EthBeaconConsensus { validate_block_post_execution(block, &self.chain_spec, input.receipts, input.requests) } } + +#[cfg(test)] +mod tests { + use reth_primitives::{proofs, ChainSpecBuilder, B256}; + + use super::*; + + fn header_with_gas_limit(gas_limit: u64) -> SealedHeader { + let header = Header { gas_limit, ..Default::default() }; + header.seal(B256::ZERO) + } + + #[test] + fn test_valid_gas_limit_increase() { + let parent = header_with_gas_limit(1024 * 10); + let child = header_with_gas_limit(parent.gas_limit + 5); + + assert_eq!( + EthBeaconConsensus::new(Arc::new(ChainSpec::default())) + .validate_against_parent_gas_limit(&child, &parent), + Ok(()) + ); + } + + #[test] + fn test_gas_limit_below_minimum() { + let parent = header_with_gas_limit(MINIMUM_GAS_LIMIT); + let child = header_with_gas_limit(MINIMUM_GAS_LIMIT - 1); + + assert_eq!( + EthBeaconConsensus::new(Arc::new(ChainSpec::default())) + .validate_against_parent_gas_limit(&child, &parent), + Err(ConsensusError::GasLimitInvalidMinimum { child_gas_limit: child.gas_limit }) + ); + } + + #[test] + fn test_invalid_gas_limit_increase_exceeding_limit() { + let parent = header_with_gas_limit(1024 * 10); + let child = header_with_gas_limit(parent.gas_limit + parent.gas_limit / 1024 + 1); + + assert_eq!( + EthBeaconConsensus::new(Arc::new(ChainSpec::default())) + .validate_against_parent_gas_limit(&child, &parent), + Err(ConsensusError::GasLimitInvalidIncrease { + parent_gas_limit: parent.gas_limit, + child_gas_limit: child.gas_limit, + }) + ); + } + + #[test] + fn test_valid_gas_limit_decrease_within_limit() { + let parent = header_with_gas_limit(1024 * 10); + let child = header_with_gas_limit(parent.gas_limit - 5); + + assert_eq!( + EthBeaconConsensus::new(Arc::new(ChainSpec::default())) + .validate_against_parent_gas_limit(&child, &parent), + Ok(()) + ); + } + + #[test] + fn test_invalid_gas_limit_decrease_exceeding_limit() { + let parent = header_with_gas_limit(1024 * 10); + let child = header_with_gas_limit(parent.gas_limit - parent.gas_limit / 1024 - 1); + + assert_eq!( + EthBeaconConsensus::new(Arc::new(ChainSpec::default())) + .validate_against_parent_gas_limit(&child, &parent), + Err(ConsensusError::GasLimitInvalidDecrease { + parent_gas_limit: parent.gas_limit, + child_gas_limit: child.gas_limit, + }) + ); + } + + #[test] + fn shanghai_block_zero_withdrawals() { + // ensures that if shanghai is activated, and we include a block with a withdrawals root, + // that the header is valid + let chain_spec = Arc::new(ChainSpecBuilder::mainnet().shanghai_activated().build()); + + let header = Header { + base_fee_per_gas: Some(1337u64), + withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])), + ..Default::default() + } + .seal_slow(); + + assert_eq!(EthBeaconConsensus::new(chain_spec).validate_header(&header), Ok(())); + } +} diff --git a/crates/optimism/consensus/src/lib.rs b/crates/optimism/consensus/src/lib.rs index 5ac9cf924..96ba25a8f 100644 --- a/crates/optimism/consensus/src/lib.rs +++ b/crates/optimism/consensus/src/lib.rs @@ -11,7 +11,9 @@ use reth_consensus::{Consensus, ConsensusError, PostExecutionInput}; use reth_consensus_common::validation::{ - validate_block_pre_execution, validate_header_extradata, validate_header_standalone, + validate_against_parent_eip1559_base_fee, validate_against_parent_hash_number, + validate_against_parent_timestamp, validate_block_pre_execution, validate_header_base_fee, + validate_header_extradata, validate_header_gas, }; use reth_primitives::{ BlockWithSenders, ChainSpec, Header, SealedBlock, SealedHeader, EMPTY_OMMER_ROOT_HASH, U256, @@ -44,8 +46,8 @@ impl OptimismBeaconConsensus { impl Consensus for OptimismBeaconConsensus { fn validate_header(&self, header: &SealedHeader) -> Result<(), ConsensusError> { - validate_header_standalone(header, &self.chain_spec)?; - Ok(()) + validate_header_gas(header)?; + validate_header_base_fee(header, &self.chain_spec) } fn validate_header_against_parent( @@ -53,7 +55,14 @@ impl Consensus for OptimismBeaconConsensus { header: &SealedHeader, parent: &SealedHeader, ) -> Result<(), ConsensusError> { - header.validate_against_parent(parent, &self.chain_spec).map_err(ConsensusError::from)?; + validate_against_parent_hash_number(header, parent)?; + + if self.chain_spec.is_bedrock_active_at_block(header.number) { + validate_against_parent_timestamp(header, parent)?; + } + + validate_against_parent_eip1559_base_fee(header, parent, &self.chain_spec)?; + Ok(()) } diff --git a/crates/primitives/src/header.rs b/crates/primitives/src/header.rs index abb5e1f34..11aa79379 100644 --- a/crates/primitives/src/header.rs +++ b/crates/primitives/src/header.rs @@ -2,14 +2,10 @@ use crate::block::{generate_valid_header, valid_header_strategy}; use crate::{ basefee::calc_next_block_base_fee, - constants, - constants::{ - ALLOWED_FUTURE_BLOCK_TIME_SECONDS, EMPTY_OMMER_ROOT_HASH, EMPTY_ROOT_HASH, - MINIMUM_GAS_LIMIT, - }, + constants::{ALLOWED_FUTURE_BLOCK_TIME_SECONDS, EMPTY_OMMER_ROOT_HASH, EMPTY_ROOT_HASH}, eip4844::{calc_blob_gasprice, calculate_excess_blob_gas}, - keccak256, Address, BaseFeeParams, BlockHash, BlockNumHash, BlockNumber, Bloom, Bytes, - ChainSpec, GotExpected, GotExpectedBoxed, Hardfork, B256, B64, U256, + keccak256, Address, BaseFeeParams, BlockHash, BlockNumHash, BlockNumber, Bloom, Bytes, B256, + B64, U256, }; use alloy_rlp::{length_of_length, Decodable, Encodable}; use bytes::BufMut; @@ -517,92 +513,6 @@ impl Decodable for Header { } } -/// Errors that can occur during header sanity checks. -#[derive(thiserror::Error, Debug, PartialEq, Eq, Clone)] -pub enum HeaderValidationError { - /// Error when the block number does not match the parent block number. - #[error( - "block number {block_number} does not match parent block number {parent_block_number}" - )] - ParentBlockNumberMismatch { - /// The parent block number. - parent_block_number: BlockNumber, - /// The block number. - block_number: BlockNumber, - }, - - /// Error when the parent hash does not match the expected parent hash. - #[error("mismatched parent hash: {0}")] - ParentHashMismatch(GotExpectedBoxed), - - /// Error when the block timestamp is in the past compared to the parent timestamp. - #[error("block timestamp {timestamp} is in the past compared to the parent timestamp {parent_timestamp}")] - TimestampIsInPast { - /// The parent block's timestamp. - parent_timestamp: u64, - /// The block's timestamp. - timestamp: u64, - }, - - /// Error when the base fee is missing. - #[error("base fee missing")] - BaseFeeMissing, - - /// Error when the block's base fee is different from the expected base fee. - #[error("block base fee mismatch: {0}")] - BaseFeeDiff(GotExpected), - - /// Error when the child gas limit exceeds the maximum allowed decrease. - #[error("child gas_limit {child_gas_limit} max decrease is {parent_gas_limit}/1024")] - GasLimitInvalidDecrease { - /// The parent gas limit. - parent_gas_limit: u64, - /// The child gas limit. - child_gas_limit: u64, - }, - - /// Error when the child gas limit exceeds the maximum allowed increase. - #[error("child gas_limit {child_gas_limit} max increase is {parent_gas_limit}/1024")] - GasLimitInvalidIncrease { - /// The parent gas limit. - parent_gas_limit: u64, - /// The child gas limit. - child_gas_limit: u64, - }, - - /// Error indicating that the child gas limit is below the minimum allowed limit. - /// - /// This error occurs when the child gas limit is less than the specified minimum gas limit. - #[error("child gas limit {child_gas_limit} is below the minimum allowed limit ({MINIMUM_GAS_LIMIT})")] - GasLimitInvalidMinimum { - /// The child gas limit. - child_gas_limit: u64, - }, - - /// Error when blob gas used is missing. - #[error("missing blob gas used")] - BlobGasUsedMissing, - - /// Error when excess blob gas is missing. - #[error("missing excess blob gas")] - ExcessBlobGasMissing, - - /// Error when there is an invalid excess blob gas. - #[error( - "invalid excess blob gas: {diff}; \ - parent excess blob gas: {parent_excess_blob_gas}, \ - parent blob gas used: {parent_blob_gas_used}" - )] - ExcessBlobGasDiff { - /// The excess blob gas diff. - diff: GotExpected, - /// The parent excess blob gas. - parent_excess_blob_gas: u64, - /// The parent blob gas used. - parent_blob_gas_used: u64, - }, -} - /// A [`Header`] that is sealed at a precalculated hash, use [`SealedHeader::unseal()`] if you want /// to modify header. #[main_codec(no_arbitrary)] @@ -670,198 +580,6 @@ impl SealedHeader { self.header.difficulty = difficulty; } - /// Checks the gas limit for consistency between parent and self headers. - /// - /// The maximum allowable difference between self and parent gas limits is determined by the - /// parent's gas limit divided by the elasticity multiplier (1024). - /// - /// This check is skipped if the Optimism flag is enabled in the chain spec, as gas limits on - /// Optimism can adjust instantly. - #[inline(always)] - fn validate_gas_limit( - &self, - parent: &Self, - chain_spec: &ChainSpec, - ) -> Result<(), HeaderValidationError> { - // Determine the parent gas limit, considering elasticity multiplier on the London fork. - let parent_gas_limit = - if chain_spec.fork(Hardfork::London).transitions_at_block(self.number) { - parent.gas_limit * - chain_spec.base_fee_params_at_timestamp(self.timestamp).elasticity_multiplier - as u64 - } else { - parent.gas_limit - }; - - // Check for an increase in gas limit beyond the allowed threshold. - if self.gas_limit > parent_gas_limit { - if self.gas_limit - parent_gas_limit >= parent_gas_limit / 1024 { - return Err(HeaderValidationError::GasLimitInvalidIncrease { - parent_gas_limit, - child_gas_limit: self.gas_limit, - }) - } - } - // Check for a decrease in gas limit beyond the allowed threshold. - else if parent_gas_limit - self.gas_limit >= parent_gas_limit / 1024 { - return Err(HeaderValidationError::GasLimitInvalidDecrease { - parent_gas_limit, - child_gas_limit: self.gas_limit, - }) - } - // Check if the self gas limit is below the minimum required limit. - else if self.gas_limit < MINIMUM_GAS_LIMIT { - return Err(HeaderValidationError::GasLimitInvalidMinimum { - child_gas_limit: self.gas_limit, - }) - } - - Ok(()) - } - - /// Validates the integrity and consistency of a sealed block header in relation to its parent - /// header. - /// - /// This function checks various properties of the sealed header against its parent header and - /// the chain specification. It ensures that the block forms a valid and secure continuation - /// of the blockchain. - /// - /// ## Arguments - /// - /// * `parent` - The sealed header of the parent block. - /// * `chain_spec` - The chain specification providing configuration parameters for the - /// blockchain. - /// - /// ## Errors - /// - /// Returns a [`HeaderValidationError`] if any validation check fails, indicating specific - /// issues with the sealed header. The possible errors include mismatched block numbers, - /// parent hash mismatches, timestamp inconsistencies, gas limit violations, base fee - /// discrepancies (for EIP-1559), and errors related to the blob gas fields (EIP-4844). - /// - /// ## Note - /// - /// Some checks, such as gas limit validation, are conditionally skipped based on the presence - /// of certain features (e.g., Optimism feature) or the activation of specific hardforks. - pub fn validate_against_parent( - &self, - parent: &Self, - chain_spec: &ChainSpec, - ) -> Result<(), HeaderValidationError> { - // Parent number is consistent. - if parent.number + 1 != self.number { - return Err(HeaderValidationError::ParentBlockNumberMismatch { - parent_block_number: parent.number, - block_number: self.number, - }) - } - - if parent.hash != self.parent_hash { - return Err(HeaderValidationError::ParentHashMismatch( - GotExpected { got: self.parent_hash, expected: parent.hash }.into(), - )) - } - - // timestamp in past check - #[cfg(feature = "optimism")] - if chain_spec.is_bedrock_active_at_block(self.header.number) && - self.header.is_timestamp_in_past(parent.timestamp) - { - return Err(HeaderValidationError::TimestampIsInPast { - parent_timestamp: parent.timestamp, - timestamp: self.timestamp, - }) - } - - #[cfg(not(feature = "optimism"))] - if self.header.is_timestamp_in_past(parent.timestamp) { - return Err(HeaderValidationError::TimestampIsInPast { - parent_timestamp: parent.timestamp, - timestamp: self.timestamp, - }) - } - - // TODO Check difficulty increment between parent and self - // Ace age did increment it by some formula that we need to follow. - - if cfg!(feature = "optimism") { - // On Optimism, the gas limit can adjust instantly, so we skip this check - // if the optimism feature is enabled in the chain spec. - if !chain_spec.is_optimism() { - self.validate_gas_limit(parent, chain_spec)?; - } - } else { - self.validate_gas_limit(parent, chain_spec)?; - } - - // EIP-1559 check base fee - if chain_spec.fork(Hardfork::London).active_at_block(self.number) { - let base_fee = self.base_fee_per_gas.ok_or(HeaderValidationError::BaseFeeMissing)?; - - let expected_base_fee = if chain_spec - .fork(Hardfork::London) - .transitions_at_block(self.number) - { - constants::EIP1559_INITIAL_BASE_FEE - } else { - // This BaseFeeMissing will not happen as previous blocks are checked to have - // them. - parent - .next_block_base_fee(chain_spec.base_fee_params_at_timestamp(self.timestamp)) - .ok_or(HeaderValidationError::BaseFeeMissing)? - }; - if expected_base_fee != base_fee { - return Err(HeaderValidationError::BaseFeeDiff(GotExpected { - expected: expected_base_fee, - got: base_fee, - })) - } - } - - // ensure that the blob gas fields for this block - if chain_spec.is_cancun_active_at_timestamp(self.timestamp) { - self.validate_4844_header_against_parent(parent)?; - } - - Ok(()) - } - - /// Validates that the EIP-4844 header fields are correct with respect to the parent block. This - /// ensures that the `blob_gas_used` and `excess_blob_gas` fields exist in the child header, and - /// that the `excess_blob_gas` field matches the expected `excess_blob_gas` calculated from the - /// parent header fields. - pub fn validate_4844_header_against_parent( - &self, - parent: &Self, - ) -> Result<(), HeaderValidationError> { - // From [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844#header-extension): - // - // > For the first post-fork block, both parent.blob_gas_used and parent.excess_blob_gas - // > are evaluated as 0. - // - // This means in the first post-fork block, calculate_excess_blob_gas will return 0. - let parent_blob_gas_used = parent.blob_gas_used.unwrap_or(0); - let parent_excess_blob_gas = parent.excess_blob_gas.unwrap_or(0); - - if self.blob_gas_used.is_none() { - return Err(HeaderValidationError::BlobGasUsedMissing) - } - let excess_blob_gas = - self.excess_blob_gas.ok_or(HeaderValidationError::ExcessBlobGasMissing)?; - - let expected_excess_blob_gas = - calculate_excess_blob_gas(parent_excess_blob_gas, parent_blob_gas_used); - if expected_excess_blob_gas != excess_blob_gas { - return Err(HeaderValidationError::ExcessBlobGasDiff { - diff: GotExpected { got: excess_blob_gas, expected: expected_excess_blob_gas }, - parent_excess_blob_gas, - parent_blob_gas_used, - }) - } - - Ok(()) - } - /// Extract raw header that can be modified. pub fn unseal(self) -> Header { self.header @@ -1035,10 +753,7 @@ impl From for bool { #[cfg(test)] mod tests { use super::{Bytes, Decodable, Encodable, Header, B256}; - use crate::{ - address, b256, bloom, bytes, header::MINIMUM_GAS_LIMIT, hex, Address, ChainSpec, - HeaderValidationError, HeadersDirection, SealedHeader, U256, - }; + use crate::{address, b256, bloom, bytes, hex, Address, HeadersDirection, U256}; use std::str::FromStr; // Test vector from: https://eips.ethereum.org/EIPS/eip-2481 @@ -1301,103 +1016,4 @@ mod tests { Header::decode(&mut data.as_slice()) .expect_err("blob_gas_used size should make this header decoding fail"); } - - #[test] - fn test_valid_gas_limit_increase() { - let parent = SealedHeader { - header: Header { gas_limit: 1024 * 10, ..Default::default() }, - ..Default::default() - }; - let child = SealedHeader { - header: Header { gas_limit: parent.header.gas_limit + 5, ..Default::default() }, - ..Default::default() - }; - let chain_spec = ChainSpec::default(); - - assert_eq!(child.validate_gas_limit(&parent, &chain_spec), Ok(())); - } - - #[test] - fn test_gas_limit_below_minimum() { - let parent = SealedHeader { - header: Header { gas_limit: MINIMUM_GAS_LIMIT, ..Default::default() }, - ..Default::default() - }; - let child = SealedHeader { - header: Header { gas_limit: MINIMUM_GAS_LIMIT - 1, ..Default::default() }, - ..Default::default() - }; - let chain_spec = ChainSpec::default(); - - assert_eq!( - child.validate_gas_limit(&parent, &chain_spec), - Err(HeaderValidationError::GasLimitInvalidMinimum { child_gas_limit: child.gas_limit }) - ); - } - - #[test] - fn test_invalid_gas_limit_increase_exceeding_limit() { - let gas_limit = 1024 * 10; - let parent = SealedHeader { - header: Header { gas_limit, ..Default::default() }, - ..Default::default() - }; - let child = SealedHeader { - header: Header { - gas_limit: parent.header.gas_limit + parent.header.gas_limit / 1024 + 1, - ..Default::default() - }, - ..Default::default() - }; - let chain_spec = ChainSpec::default(); - - assert_eq!( - child.validate_gas_limit(&parent, &chain_spec), - Err(HeaderValidationError::GasLimitInvalidIncrease { - parent_gas_limit: parent.header.gas_limit, - child_gas_limit: child.header.gas_limit, - }) - ); - } - - #[test] - fn test_valid_gas_limit_decrease_within_limit() { - let gas_limit = 1024 * 10; - let parent = SealedHeader { - header: Header { gas_limit, ..Default::default() }, - ..Default::default() - }; - let child = SealedHeader { - header: Header { gas_limit: parent.header.gas_limit - 5, ..Default::default() }, - ..Default::default() - }; - let chain_spec = ChainSpec::default(); - - assert_eq!(child.validate_gas_limit(&parent, &chain_spec), Ok(())); - } - - #[test] - fn test_invalid_gas_limit_decrease_exceeding_limit() { - let gas_limit = 1024 * 10; - let parent = SealedHeader { - header: Header { gas_limit, ..Default::default() }, - ..Default::default() - }; - let child = SealedHeader { - header: Header { - gas_limit: parent.header.gas_limit - parent.header.gas_limit / 1024 - 1, - ..Default::default() - }, - ..Default::default() - }; - let chain_spec = ChainSpec::default(); - - assert_eq!( - child.validate_gas_limit(&parent, &chain_spec), - Err(HeaderValidationError::GasLimitInvalidDecrease { - parent_gas_limit: parent.header.gas_limit, - child_gas_limit: child.header.gas_limit, - }) - ); - } } diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index d9cb4d326..02e86c459 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -63,7 +63,7 @@ pub use constants::{ }; pub use error::{GotExpected, GotExpectedBoxed}; pub use genesis::{ChainConfig, Genesis, GenesisAccount}; -pub use header::{Header, HeaderValidationError, HeadersDirection, SealedHeader}; +pub use header::{Header, HeadersDirection, SealedHeader}; pub use integer_list::IntegerList; pub use log::{logs_bloom, Log}; pub use net::{