use alloy_consensus::{constants::MAXIMUM_EXTRA_DATA_SIZE, BlockHeader, EMPTY_OMMER_ROOT_HASH};
use alloy_eips::{
calc_next_block_base_fee,
eip4844::{DATA_GAS_PER_BLOB, MAX_DATA_GAS_PER_BLOCK},
};
use reth_chainspec::{EthChainSpec, EthereumHardfork, EthereumHardforks};
use reth_consensus::ConsensusError;
use reth_primitives::SealedBlock;
use reth_primitives_traits::{BlockBody, GotExpected, SealedHeader};
use revm_primitives::calc_excess_blob_gas;
#[inline]
pub fn validate_header_gas<H: BlockHeader>(header: &H) -> Result<(), ConsensusError> {
if header.gas_used() > header.gas_limit() {
return Err(ConsensusError::HeaderGasUsedExceedsGasLimit {
gas_used: header.gas_used(),
gas_limit: header.gas_limit(),
})
}
Ok(())
}
#[inline]
pub fn validate_header_base_fee<H: BlockHeader, ChainSpec: EthereumHardforks>(
header: &H,
chain_spec: &ChainSpec,
) -> Result<(), ConsensusError> {
if chain_spec.is_fork_active_at_block(EthereumHardfork::London, header.number()) &&
header.base_fee_per_gas().is_none()
{
return Err(ConsensusError::BaseFeeMissing)
}
Ok(())
}
#[inline]
pub fn validate_shanghai_withdrawals<H: BlockHeader, B: BlockBody>(
block: &SealedBlock<H, B>,
) -> Result<(), ConsensusError> {
let withdrawals = block.body.withdrawals().ok_or(ConsensusError::BodyWithdrawalsMissing)?;
let withdrawals_root = alloy_consensus::proofs::calculate_withdrawals_root(withdrawals);
let header_withdrawals_root =
block.withdrawals_root().ok_or(ConsensusError::WithdrawalsRootMissing)?;
if withdrawals_root != *header_withdrawals_root {
return Err(ConsensusError::BodyWithdrawalsRootDiff(
GotExpected { got: withdrawals_root, expected: header_withdrawals_root }.into(),
));
}
Ok(())
}
#[inline]
pub fn validate_cancun_gas<H: BlockHeader, B: BlockBody>(
block: &SealedBlock<H, B>,
) -> Result<(), ConsensusError> {
let header_blob_gas_used =
block.header().blob_gas_used().ok_or(ConsensusError::BlobGasUsedMissing)?;
let total_blob_gas = block.body.blob_gas_used();
if total_blob_gas != header_blob_gas_used {
return Err(ConsensusError::BlobGasUsedDiff(GotExpected {
got: header_blob_gas_used,
expected: total_blob_gas,
}));
}
Ok(())
}
pub fn validate_body_against_header<B, H>(body: &B, header: &H) -> Result<(), ConsensusError>
where
B: BlockBody,
H: BlockHeader,
{
let ommers_hash = body.calculate_ommers_root();
if Some(header.ommers_hash()) != ommers_hash {
return Err(ConsensusError::BodyOmmersHashDiff(
GotExpected {
got: ommers_hash.unwrap_or(EMPTY_OMMER_ROOT_HASH),
expected: header.ommers_hash(),
}
.into(),
))
}
let tx_root = body.calculate_tx_root();
if header.transactions_root() != tx_root {
return Err(ConsensusError::BodyTransactionRootDiff(
GotExpected { got: tx_root, expected: header.transactions_root() }.into(),
))
}
match (header.withdrawals_root(), body.calculate_withdrawals_root()) {
(Some(header_withdrawals_root), Some(withdrawals_root)) => {
if withdrawals_root != header_withdrawals_root {
return Err(ConsensusError::BodyWithdrawalsRootDiff(
GotExpected { got: withdrawals_root, expected: header_withdrawals_root }.into(),
))
}
}
(None, None) => {
}
_ => return Err(ConsensusError::WithdrawalsRootUnexpected),
}
Ok(())
}
pub fn validate_block_pre_execution<H, B, ChainSpec>(
block: &SealedBlock<H, B>,
chain_spec: &ChainSpec,
) -> Result<(), ConsensusError>
where
H: BlockHeader,
B: BlockBody,
ChainSpec: EthereumHardforks,
{
let ommers_hash = block.body.calculate_ommers_root();
if Some(block.header.ommers_hash()) != ommers_hash {
return Err(ConsensusError::BodyOmmersHashDiff(
GotExpected {
got: ommers_hash.unwrap_or(EMPTY_OMMER_ROOT_HASH),
expected: block.header.ommers_hash(),
}
.into(),
))
}
if let Err(error) = block.ensure_transaction_root_valid() {
return Err(ConsensusError::BodyTransactionRootDiff(error.into()))
}
if chain_spec.is_shanghai_active_at_timestamp(block.timestamp()) {
validate_shanghai_withdrawals(block)?;
}
if chain_spec.is_cancun_active_at_timestamp(block.timestamp()) {
validate_cancun_gas(block)?;
}
Ok(())
}
pub fn validate_4844_header_standalone<H: BlockHeader>(header: &H) -> Result<(), ConsensusError> {
let blob_gas_used = header.blob_gas_used().ok_or(ConsensusError::BlobGasUsedMissing)?;
let excess_blob_gas = header.excess_blob_gas().ok_or(ConsensusError::ExcessBlobGasMissing)?;
if header.parent_beacon_block_root().is_none() {
return Err(ConsensusError::ParentBeaconBlockRootMissing)
}
if blob_gas_used > MAX_DATA_GAS_PER_BLOCK {
return Err(ConsensusError::BlobGasUsedExceedsMaxBlobGasPerBlock {
blob_gas_used,
max_blob_gas_per_block: MAX_DATA_GAS_PER_BLOCK,
})
}
if blob_gas_used % DATA_GAS_PER_BLOB != 0 {
return Err(ConsensusError::BlobGasUsedNotMultipleOfBlobGasPerBlob {
blob_gas_used,
blob_gas_per_blob: DATA_GAS_PER_BLOB,
})
}
if excess_blob_gas % DATA_GAS_PER_BLOB != 0 {
return Err(ConsensusError::ExcessBlobGasNotMultipleOfBlobGasPerBlob {
excess_blob_gas,
blob_gas_per_blob: DATA_GAS_PER_BLOB,
})
}
Ok(())
}
#[inline]
pub fn validate_header_extra_data<H: BlockHeader>(header: &H) -> Result<(), ConsensusError> {
let extra_data_len = header.extra_data().len();
if extra_data_len > MAXIMUM_EXTRA_DATA_SIZE {
Err(ConsensusError::ExtraDataExceedsMax { len: extra_data_len })
} else {
Ok(())
}
}
#[inline]
pub fn validate_against_parent_hash_number<H: BlockHeader>(
header: &H,
parent: &SealedHeader<H>,
) -> Result<(), ConsensusError> {
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(())
}
#[inline]
pub fn validate_against_parent_eip1559_base_fee<
H: BlockHeader,
ChainSpec: EthChainSpec + EthereumHardforks,
>(
header: &H,
parent: &H,
chain_spec: &ChainSpec,
) -> Result<(), ConsensusError> {
if chain_spec.fork(EthereumHardfork::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(EthereumHardfork::London).transitions_at_block(header.number()) {
alloy_eips::eip1559::INITIAL_BASE_FEE
} else {
let base_fee = parent.base_fee_per_gas().ok_or(ConsensusError::BaseFeeMissing)?;
calc_next_block_base_fee(
parent.gas_used(),
parent.gas_limit(),
base_fee,
chain_spec.base_fee_params_at_timestamp(header.timestamp()),
)
};
if expected_base_fee != base_fee {
return Err(ConsensusError::BaseFeeDiff(GotExpected {
expected: expected_base_fee,
got: base_fee,
}))
}
}
Ok(())
}
#[inline]
pub fn validate_against_parent_timestamp<H: BlockHeader>(
header: &H,
parent: &H,
) -> Result<(), ConsensusError> {
if header.timestamp() <= parent.timestamp() {
return Err(ConsensusError::TimestampIsInPast {
parent_timestamp: parent.timestamp(),
timestamp: header.timestamp(),
})
}
Ok(())
}
pub fn validate_against_parent_4844<H: BlockHeader>(
header: &H,
parent: &H,
) -> Result<(), ConsensusError> {
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 =
calc_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(())
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_consensus::{Header, TxEip4844, EMPTY_OMMER_ROOT_HASH, EMPTY_ROOT_HASH};
use alloy_eips::{
eip4895::{Withdrawal, Withdrawals},
BlockHashOrNumber,
};
use alloy_primitives::{
hex_literal::hex, Address, BlockHash, BlockNumber, Bytes, PrimitiveSignature as Signature,
U256,
};
use mockall::mock;
use rand::Rng;
use reth_chainspec::ChainSpecBuilder;
use reth_primitives::{proofs, Account, BlockBody, Transaction, TransactionSigned};
use reth_storage_api::{
errors::provider::ProviderResult, AccountReader, HeaderProvider, WithdrawalsProvider,
};
use std::ops::RangeBounds;
mock! {
WithdrawalsProvider {}
impl WithdrawalsProvider for WithdrawalsProvider {
fn latest_withdrawal(&self) -> ProviderResult<Option<Withdrawal>> ;
fn withdrawals_by_block(
&self,
_id: BlockHashOrNumber,
_timestamp: u64,
) -> ProviderResult<Option<Withdrawals>> ;
}
}
struct Provider {
is_known: bool,
parent: Option<Header>,
account: Option<Account>,
withdrawals_provider: MockWithdrawalsProvider,
}
impl Provider {
fn new(parent: Option<Header>) -> Self {
Self {
is_known: false,
parent,
account: None,
withdrawals_provider: MockWithdrawalsProvider::new(),
}
}
}
impl AccountReader for Provider {
fn basic_account(&self, _address: &Address) -> ProviderResult<Option<Account>> {
Ok(self.account)
}
}
impl HeaderProvider for Provider {
type Header = Header;
fn is_known(&self, _block_hash: &BlockHash) -> ProviderResult<bool> {
Ok(self.is_known)
}
fn header(&self, _block_number: &BlockHash) -> ProviderResult<Option<Header>> {
Ok(self.parent.clone())
}
fn header_by_number(&self, _num: u64) -> ProviderResult<Option<Header>> {
Ok(self.parent.clone())
}
fn header_td(&self, _hash: &BlockHash) -> ProviderResult<Option<U256>> {
Ok(None)
}
fn header_td_by_number(&self, _number: BlockNumber) -> ProviderResult<Option<U256>> {
Ok(None)
}
fn headers_range(
&self,
_range: impl RangeBounds<BlockNumber>,
) -> ProviderResult<Vec<Header>> {
Ok(vec![])
}
fn sealed_header(
&self,
_block_number: BlockNumber,
) -> ProviderResult<Option<SealedHeader>> {
Ok(None)
}
fn sealed_headers_while(
&self,
_range: impl RangeBounds<BlockNumber>,
_predicate: impl FnMut(&SealedHeader) -> bool,
) -> ProviderResult<Vec<SealedHeader>> {
Ok(vec![])
}
}
impl WithdrawalsProvider for Provider {
fn withdrawals_by_block(
&self,
_id: BlockHashOrNumber,
_timestamp: u64,
) -> ProviderResult<Option<Withdrawals>> {
self.withdrawals_provider.withdrawals_by_block(_id, _timestamp)
}
fn latest_withdrawal(&self) -> ProviderResult<Option<Withdrawal>> {
self.withdrawals_provider.latest_withdrawal()
}
}
fn mock_blob_tx(nonce: u64, num_blobs: usize) -> TransactionSigned {
let mut rng = rand::thread_rng();
let request = Transaction::Eip4844(TxEip4844 {
chain_id: 1u64,
nonce,
max_fee_per_gas: 0x28f000fff,
max_priority_fee_per_gas: 0x28f000fff,
max_fee_per_blob_gas: 0x7,
gas_limit: 10,
to: Address::default(),
value: U256::from(3_u64),
input: Bytes::from(vec![1, 2]),
access_list: Default::default(),
blob_versioned_hashes: std::iter::repeat_with(|| rng.gen()).take(num_blobs).collect(),
});
let signature = Signature::new(U256::default(), U256::default(), true);
TransactionSigned::new_unhashed(request, signature)
}
fn mock_block() -> (SealedBlock, Header) {
let header = Header {
parent_hash: hex!("859fad46e75d9be177c2584843501f2270c7e5231711e90848290d12d7c6dcdd").into(),
ommers_hash: EMPTY_OMMER_ROOT_HASH,
beneficiary: hex!("4675c7e5baafbffbca748158becba61ef3b0a263").into(),
state_root: hex!("8337403406e368b3e40411138f4868f79f6d835825d55fd0c2f6e17b1a3948e9").into(),
transactions_root: EMPTY_ROOT_HASH,
receipts_root: EMPTY_ROOT_HASH,
logs_bloom: hex!("002400000000004000220000800002000000000000000000000000000000100000000000000000100000000000000021020000000800000006000000002100040000000c0004000000000008000008200000000000000000000000008000000001040000020000020000002000000800000002000020000000022010000000000000010002001000000000020200000000000001000200880000004000000900020000000000020000000040000000000000000000000000000080000000000001000002000000000000012000200020000000000000001000000000000020000010321400000000100000000000000000000000000000400000000000000000").into(),
difficulty: U256::ZERO, number: 0xf21d20,
gas_limit: 0x1c9c380,
gas_used: 0x6e813,
timestamp: 0x635f9657,
extra_data: hex!("")[..].into(),
mix_hash: hex!("0000000000000000000000000000000000000000000000000000000000000000").into(),
nonce: 0x0000000000000000u64.into(),
base_fee_per_gas: 0x28f0001df.into(),
withdrawals_root: None,
blob_gas_used: None,
excess_blob_gas: None,
parent_beacon_block_root: None,
requests_hash: None,
target_blobs_per_block: None,
};
let mut parent = header.clone();
parent.gas_used = 17763076;
parent.gas_limit = 30000000;
parent.base_fee_per_gas = Some(0x28041f7f5);
parent.number -= 1;
parent.timestamp -= 1;
let ommers = Vec::new();
let transactions = Vec::new();
(
SealedBlock {
header: SealedHeader::seal(header),
body: BlockBody { transactions, ommers, withdrawals: None },
},
parent,
)
}
#[test]
fn valid_withdrawal_index() {
let chain_spec = ChainSpecBuilder::mainnet().shanghai_activated().build();
let create_block_with_withdrawals = |indexes: &[u64]| {
let withdrawals = Withdrawals::new(
indexes
.iter()
.map(|idx| Withdrawal { index: *idx, ..Default::default() })
.collect(),
);
let header = Header {
withdrawals_root: Some(proofs::calculate_withdrawals_root(&withdrawals)),
..Default::default()
};
SealedBlock {
header: SealedHeader::seal(header),
body: BlockBody { withdrawals: Some(withdrawals), ..Default::default() },
}
};
let block: SealedBlock = create_block_with_withdrawals(&[1]);
assert_eq!(validate_block_pre_execution(&block, &chain_spec), Ok(()));
let block = create_block_with_withdrawals(&[1, 2, 3]);
assert_eq!(validate_block_pre_execution(&block, &chain_spec), Ok(()));
let block = create_block_with_withdrawals(&[5, 6, 7, 8, 9]);
assert_eq!(validate_block_pre_execution(&block, &chain_spec), Ok(()));
let (_, parent) = mock_block();
let mut provider = Provider::new(Some(parent));
provider
.withdrawals_provider
.expect_latest_withdrawal()
.return_const(Ok(Some(Withdrawal { index: 2, ..Default::default() })));
}
#[test]
fn cancun_block_incorrect_blob_gas_used() {
let chain_spec = ChainSpecBuilder::mainnet().cancun_activated().build();
let transaction = mock_blob_tx(1, 10);
let header = Header {
base_fee_per_gas: Some(1337),
withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
blob_gas_used: Some(1),
transactions_root: proofs::calculate_transaction_root(&[transaction.clone()]),
..Default::default()
};
let header = SealedHeader::seal(header);
let body = BlockBody {
transactions: vec![transaction],
ommers: vec![],
withdrawals: Some(Withdrawals::default()),
};
let block = SealedBlock::new(header, body);
let expected_blob_gas_used = 10 * DATA_GAS_PER_BLOB;
assert_eq!(
validate_block_pre_execution(&block, &chain_spec),
Err(ConsensusError::BlobGasUsedDiff(GotExpected {
got: 1,
expected: expected_blob_gas_used
}))
);
}
}