reth_consensus_common/
validation.rs

1//! Collection of methods for block validation.
2
3use alloy_consensus::{BlockHeader as _, Transaction, EMPTY_OMMER_ROOT_HASH};
4use alloy_eips::{eip4844::DATA_GAS_PER_BLOB, eip7840::BlobParams};
5use reth_chainspec::{EthChainSpec, EthereumHardfork, EthereumHardforks};
6use reth_consensus::{ConsensusError, TxGasLimitTooHighErr};
7use reth_primitives_traits::{
8    constants::{
9        GAS_LIMIT_BOUND_DIVISOR, MAXIMUM_GAS_LIMIT_BLOCK, MAX_TX_GAS_LIMIT_OSAKA, MINIMUM_GAS_LIMIT,
10    },
11    transaction::TxHashRef,
12    Block, BlockBody, BlockHeader, GotExpected, SealedBlock, SealedHeader,
13};
14
15/// The maximum RLP length of a block, defined in [EIP-7934](https://eips.ethereum.org/EIPS/eip-7934).
16///
17/// Calculated as `MAX_BLOCK_SIZE` - `SAFETY_MARGIN` where
18/// `MAX_BLOCK_SIZE` = `10_485_760`
19/// `SAFETY_MARGIN` = `2_097_152`
20pub const MAX_RLP_BLOCK_SIZE: usize = 8_388_608;
21
22/// Gas used needs to be less than gas limit. Gas used is going to be checked after execution.
23#[inline]
24pub fn validate_header_gas<H: BlockHeader>(header: &H) -> Result<(), ConsensusError> {
25    if header.gas_used() > header.gas_limit() {
26        return Err(ConsensusError::HeaderGasUsedExceedsGasLimit {
27            gas_used: header.gas_used(),
28            gas_limit: header.gas_limit(),
29        })
30    }
31    // Check that the gas limit is below the maximum allowed gas limit
32    if header.gas_limit() > MAXIMUM_GAS_LIMIT_BLOCK {
33        return Err(ConsensusError::HeaderGasLimitExceedsMax { gas_limit: header.gas_limit() })
34    }
35    Ok(())
36}
37
38/// Ensure the EIP-1559 base fee is set if the London hardfork is active.
39#[inline]
40pub fn validate_header_base_fee<H: BlockHeader, ChainSpec: EthereumHardforks>(
41    header: &H,
42    chain_spec: &ChainSpec,
43) -> Result<(), ConsensusError> {
44    if chain_spec.is_london_active_at_block(header.number()) && header.base_fee_per_gas().is_none()
45    {
46        return Err(ConsensusError::BaseFeeMissing)
47    }
48    Ok(())
49}
50
51/// Validate that withdrawals are present in Shanghai
52///
53/// See [EIP-4895]: Beacon chain push withdrawals as operations
54///
55/// [EIP-4895]: https://eips.ethereum.org/EIPS/eip-4895
56#[inline]
57pub fn validate_shanghai_withdrawals<B: Block>(
58    block: &SealedBlock<B>,
59) -> Result<(), ConsensusError> {
60    let withdrawals = block.body().withdrawals().ok_or(ConsensusError::BodyWithdrawalsMissing)?;
61    let withdrawals_root = alloy_consensus::proofs::calculate_withdrawals_root(withdrawals);
62    let header_withdrawals_root =
63        block.withdrawals_root().ok_or(ConsensusError::WithdrawalsRootMissing)?;
64    if withdrawals_root != *header_withdrawals_root {
65        return Err(ConsensusError::BodyWithdrawalsRootDiff(
66            GotExpected { got: withdrawals_root, expected: header_withdrawals_root }.into(),
67        ));
68    }
69    Ok(())
70}
71
72/// Validate that blob gas is present in the block if Cancun is active.
73///
74/// See [EIP-4844]: Shard Blob Transactions
75///
76/// [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844
77#[inline]
78pub fn validate_cancun_gas<B: Block>(block: &SealedBlock<B>) -> Result<(), ConsensusError> {
79    // Check that the blob gas used in the header matches the sum of the blob gas used by each
80    // blob tx
81    let header_blob_gas_used = block.blob_gas_used().ok_or(ConsensusError::BlobGasUsedMissing)?;
82    let total_blob_gas = block.body().blob_gas_used();
83    if total_blob_gas != header_blob_gas_used {
84        return Err(ConsensusError::BlobGasUsedDiff(GotExpected {
85            got: header_blob_gas_used,
86            expected: total_blob_gas,
87        }));
88    }
89    Ok(())
90}
91
92/// Ensures the block response data matches the header.
93///
94/// This ensures the body response items match the header's hashes:
95///   - ommer hash
96///   - transaction root
97///   - withdrawals root
98pub fn validate_body_against_header<B, H>(body: &B, header: &H) -> Result<(), ConsensusError>
99where
100    B: BlockBody,
101    H: BlockHeader,
102{
103    let ommers_hash = body.calculate_ommers_root();
104    if Some(header.ommers_hash()) != ommers_hash {
105        return Err(ConsensusError::BodyOmmersHashDiff(
106            GotExpected {
107                got: ommers_hash.unwrap_or(EMPTY_OMMER_ROOT_HASH),
108                expected: header.ommers_hash(),
109            }
110            .into(),
111        ))
112    }
113
114    let tx_root = body.calculate_tx_root();
115    if header.transactions_root() != tx_root {
116        return Err(ConsensusError::BodyTransactionRootDiff(
117            GotExpected { got: tx_root, expected: header.transactions_root() }.into(),
118        ))
119    }
120
121    match (header.withdrawals_root(), body.calculate_withdrawals_root()) {
122        (Some(header_withdrawals_root), Some(withdrawals_root)) => {
123            if withdrawals_root != header_withdrawals_root {
124                return Err(ConsensusError::BodyWithdrawalsRootDiff(
125                    GotExpected { got: withdrawals_root, expected: header_withdrawals_root }.into(),
126                ))
127            }
128        }
129        (None, None) => {
130            // this is ok because we assume the fork is not active in this case
131        }
132        _ => return Err(ConsensusError::WithdrawalsRootUnexpected),
133    }
134
135    Ok(())
136}
137
138/// Validate a block without regard for state:
139///
140/// - Compares the ommer hash in the block header to the block body
141/// - Compares the transactions root in the block header to the block body
142/// - Pre-execution transaction validation
143pub fn validate_block_pre_execution<B, ChainSpec>(
144    block: &SealedBlock<B>,
145    chain_spec: &ChainSpec,
146) -> Result<(), ConsensusError>
147where
148    B: Block,
149    ChainSpec: EthereumHardforks,
150{
151    post_merge_hardfork_fields(block, chain_spec)?;
152
153    // Check transaction root
154    if let Err(error) = block.ensure_transaction_root_valid() {
155        return Err(ConsensusError::BodyTransactionRootDiff(error.into()))
156    }
157    // EIP-7825 validation
158    if chain_spec.is_osaka_active_at_timestamp(block.timestamp()) {
159        for tx in block.body().transactions() {
160            if tx.gas_limit() > MAX_TX_GAS_LIMIT_OSAKA {
161                return Err(TxGasLimitTooHighErr {
162                    tx_hash: *tx.tx_hash(),
163                    gas_limit: tx.gas_limit(),
164                    max_allowed: MAX_TX_GAS_LIMIT_OSAKA,
165                }
166                .into());
167            }
168        }
169    }
170
171    Ok(())
172}
173
174/// Validates the ommers hash and other fork-specific fields.
175///
176/// These fork-specific validations are:
177/// * EIP-4895 withdrawals validation, if shanghai is active based on the given chainspec. See more
178///   information about the specific checks in [`validate_shanghai_withdrawals`].
179/// * EIP-4844 blob gas validation, if cancun is active based on the given chainspec. See more
180///   information about the specific checks in [`validate_cancun_gas`].
181/// * EIP-7934 block size limit validation, if osaka is active based on the given chainspec.
182pub fn post_merge_hardfork_fields<B, ChainSpec>(
183    block: &SealedBlock<B>,
184    chain_spec: &ChainSpec,
185) -> Result<(), ConsensusError>
186where
187    B: Block,
188    ChainSpec: EthereumHardforks,
189{
190    // Check ommers hash
191    let ommers_hash = block.body().calculate_ommers_root();
192    if Some(block.ommers_hash()) != ommers_hash {
193        return Err(ConsensusError::BodyOmmersHashDiff(
194            GotExpected {
195                got: ommers_hash.unwrap_or(EMPTY_OMMER_ROOT_HASH),
196                expected: block.ommers_hash(),
197            }
198            .into(),
199        ))
200    }
201
202    // EIP-4895: Beacon chain push withdrawals as operations
203    if chain_spec.is_shanghai_active_at_timestamp(block.timestamp()) {
204        validate_shanghai_withdrawals(block)?;
205    }
206
207    if chain_spec.is_cancun_active_at_timestamp(block.timestamp()) {
208        validate_cancun_gas(block)?;
209    }
210
211    if chain_spec.is_osaka_active_at_timestamp(block.timestamp()) &&
212        block.rlp_length() > MAX_RLP_BLOCK_SIZE
213    {
214        return Err(ConsensusError::BlockTooLarge {
215            rlp_length: block.rlp_length(),
216            max_rlp_length: MAX_RLP_BLOCK_SIZE,
217        })
218    }
219
220    Ok(())
221}
222
223/// Validates that the EIP-4844 header fields exist and conform to the spec. This ensures that:
224///
225///  * `blob_gas_used` exists as a header field
226///  * `parent_beacon_block_root` exists as a header field
227///  * `blob_gas_used` is a multiple of `DATA_GAS_PER_BLOB`
228///  * `blob_gas_used` doesn't exceed the max allowed blob gas based on the given params
229pub fn validate_4844_header_standalone<H: BlockHeader>(
230    header: &H,
231    blob_params: BlobParams,
232) -> Result<(), ConsensusError> {
233    let blob_gas_used = header.blob_gas_used().ok_or(ConsensusError::BlobGasUsedMissing)?;
234
235    if header.parent_beacon_block_root().is_none() {
236        return Err(ConsensusError::ParentBeaconBlockRootMissing)
237    }
238
239    if !blob_gas_used.is_multiple_of(DATA_GAS_PER_BLOB) {
240        return Err(ConsensusError::BlobGasUsedNotMultipleOfBlobGasPerBlob {
241            blob_gas_used,
242            blob_gas_per_blob: DATA_GAS_PER_BLOB,
243        })
244    }
245
246    if blob_gas_used > blob_params.max_blob_gas_per_block() {
247        return Err(ConsensusError::BlobGasUsedExceedsMaxBlobGasPerBlock {
248            blob_gas_used,
249            max_blob_gas_per_block: blob_params.max_blob_gas_per_block(),
250        })
251    }
252
253    Ok(())
254}
255
256/// Validates the header's extra data according to the beacon consensus rules.
257///
258/// From yellow paper: extraData: An arbitrary byte array containing data relevant to this block.
259/// This must be 32 bytes or fewer; formally Hx.
260#[inline]
261pub fn validate_header_extra_data<H: BlockHeader>(
262    header: &H,
263    max_size: usize,
264) -> Result<(), ConsensusError> {
265    let extra_data_len = header.extra_data().len();
266    if extra_data_len > max_size {
267        Err(ConsensusError::ExtraDataExceedsMax { len: extra_data_len })
268    } else {
269        Ok(())
270    }
271}
272
273/// Validates against the parent hash and number.
274///
275/// This function ensures that the header block number is sequential and that the hash of the parent
276/// header matches the parent hash in the header.
277#[inline]
278pub fn validate_against_parent_hash_number<H: BlockHeader>(
279    header: &H,
280    parent: &SealedHeader<H>,
281) -> Result<(), ConsensusError> {
282    // Parent number is consistent.
283    if parent.number() + 1 != header.number() {
284        return Err(ConsensusError::ParentBlockNumberMismatch {
285            parent_block_number: parent.number(),
286            block_number: header.number(),
287        })
288    }
289
290    if parent.hash() != header.parent_hash() {
291        return Err(ConsensusError::ParentHashMismatch(
292            GotExpected { got: header.parent_hash(), expected: parent.hash() }.into(),
293        ))
294    }
295
296    Ok(())
297}
298
299/// Validates the base fee against the parent and EIP-1559 rules.
300#[inline]
301pub fn validate_against_parent_eip1559_base_fee<ChainSpec: EthChainSpec + EthereumHardforks>(
302    header: &ChainSpec::Header,
303    parent: &ChainSpec::Header,
304    chain_spec: &ChainSpec,
305) -> Result<(), ConsensusError> {
306    if chain_spec.is_london_active_at_block(header.number()) {
307        let base_fee = header.base_fee_per_gas().ok_or(ConsensusError::BaseFeeMissing)?;
308
309        let expected_base_fee = if chain_spec
310            .ethereum_fork_activation(EthereumHardfork::London)
311            .transitions_at_block(header.number())
312        {
313            alloy_eips::eip1559::INITIAL_BASE_FEE
314        } else {
315            chain_spec
316                .next_block_base_fee(parent, header.timestamp())
317                .ok_or(ConsensusError::BaseFeeMissing)?
318        };
319        if expected_base_fee != base_fee {
320            return Err(ConsensusError::BaseFeeDiff(GotExpected {
321                expected: expected_base_fee,
322                got: base_fee,
323            }))
324        }
325    }
326
327    Ok(())
328}
329
330/// Validates that the block timestamp is greater than the parent block timestamp.
331#[inline]
332pub fn validate_against_parent_timestamp<H: BlockHeader>(
333    header: &H,
334    parent: &H,
335) -> Result<(), ConsensusError> {
336    if header.timestamp() <= parent.timestamp() {
337        return Err(ConsensusError::TimestampIsInPast {
338            parent_timestamp: parent.timestamp(),
339            timestamp: header.timestamp(),
340        })
341    }
342    Ok(())
343}
344
345/// Validates gas limit against parent gas limit.
346///
347/// The maximum allowable difference between self and parent gas limits is determined by the
348/// parent's gas limit divided by the [`GAS_LIMIT_BOUND_DIVISOR`].
349#[inline]
350pub fn validate_against_parent_gas_limit<
351    H: BlockHeader,
352    ChainSpec: EthChainSpec + EthereumHardforks,
353>(
354    header: &SealedHeader<H>,
355    parent: &SealedHeader<H>,
356    chain_spec: &ChainSpec,
357) -> Result<(), ConsensusError> {
358    // Determine the parent gas limit, considering elasticity multiplier on the London fork.
359    let parent_gas_limit = if !chain_spec.is_london_active_at_block(parent.number()) &&
360        chain_spec.is_london_active_at_block(header.number())
361    {
362        parent.gas_limit() *
363            chain_spec.base_fee_params_at_timestamp(header.timestamp()).elasticity_multiplier
364                as u64
365    } else {
366        parent.gas_limit()
367    };
368
369    // Check for an increase in gas limit beyond the allowed threshold.
370    if header.gas_limit() > parent_gas_limit {
371        if header.gas_limit() - parent_gas_limit >= parent_gas_limit / GAS_LIMIT_BOUND_DIVISOR {
372            return Err(ConsensusError::GasLimitInvalidIncrease {
373                parent_gas_limit,
374                child_gas_limit: header.gas_limit(),
375            })
376        }
377    }
378    // Check for a decrease in gas limit beyond the allowed threshold.
379    else if parent_gas_limit - header.gas_limit() >= parent_gas_limit / GAS_LIMIT_BOUND_DIVISOR {
380        return Err(ConsensusError::GasLimitInvalidDecrease {
381            parent_gas_limit,
382            child_gas_limit: header.gas_limit(),
383        })
384    }
385    // Check if the self gas limit is below the minimum required limit.
386    else if header.gas_limit() < MINIMUM_GAS_LIMIT {
387        return Err(ConsensusError::GasLimitInvalidMinimum { child_gas_limit: header.gas_limit() })
388    }
389
390    Ok(())
391}
392
393/// Validates that the EIP-4844 header fields are correct with respect to the parent block. This
394/// ensures that the `blob_gas_used` and `excess_blob_gas` fields exist in the child header, and
395/// that the `excess_blob_gas` field matches the expected `excess_blob_gas` calculated from the
396/// parent header fields.
397pub fn validate_against_parent_4844<H: BlockHeader>(
398    header: &H,
399    parent: &H,
400    blob_params: BlobParams,
401) -> Result<(), ConsensusError> {
402    // From [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844#header-extension):
403    //
404    // > For the first post-fork block, both parent.blob_gas_used and parent.excess_blob_gas
405    // > are evaluated as 0.
406    //
407    // This means in the first post-fork block, calc_excess_blob_gas will return 0.
408    let parent_blob_gas_used = parent.blob_gas_used().unwrap_or(0);
409    let parent_excess_blob_gas = parent.excess_blob_gas().unwrap_or(0);
410
411    if header.blob_gas_used().is_none() {
412        return Err(ConsensusError::BlobGasUsedMissing)
413    }
414    let excess_blob_gas = header.excess_blob_gas().ok_or(ConsensusError::ExcessBlobGasMissing)?;
415
416    let parent_base_fee_per_gas = parent.base_fee_per_gas().unwrap_or(0);
417    let expected_excess_blob_gas = blob_params.next_block_excess_blob_gas_osaka(
418        parent_excess_blob_gas,
419        parent_blob_gas_used,
420        parent_base_fee_per_gas,
421    );
422    if expected_excess_blob_gas != excess_blob_gas {
423        return Err(ConsensusError::ExcessBlobGasDiff {
424            diff: GotExpected { got: excess_blob_gas, expected: expected_excess_blob_gas },
425            parent_excess_blob_gas,
426            parent_blob_gas_used,
427        })
428    }
429
430    Ok(())
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436    use alloy_consensus::{BlockBody, Header, TxEip4844};
437    use alloy_eips::eip4895::Withdrawals;
438    use alloy_primitives::{Address, Bytes, Signature, U256};
439    use rand::Rng;
440    use reth_chainspec::ChainSpecBuilder;
441    use reth_ethereum_primitives::{Transaction, TransactionSigned};
442    use reth_primitives_traits::proofs;
443
444    fn mock_blob_tx(nonce: u64, num_blobs: usize) -> TransactionSigned {
445        let mut rng = rand::rng();
446        let request = Transaction::Eip4844(TxEip4844 {
447            chain_id: 1u64,
448            nonce,
449            max_fee_per_gas: 0x28f000fff,
450            max_priority_fee_per_gas: 0x28f000fff,
451            max_fee_per_blob_gas: 0x7,
452            gas_limit: 10,
453            to: Address::default(),
454            value: U256::from(3_u64),
455            input: Bytes::from(vec![1, 2]),
456            access_list: Default::default(),
457            blob_versioned_hashes: std::iter::repeat_with(|| rng.random())
458                .take(num_blobs)
459                .collect(),
460        });
461
462        let signature = Signature::new(U256::default(), U256::default(), true);
463
464        TransactionSigned::new_unhashed(request, signature)
465    }
466
467    #[test]
468    fn cancun_block_incorrect_blob_gas_used() {
469        let chain_spec = ChainSpecBuilder::mainnet().cancun_activated().build();
470
471        // create a tx with 10 blobs
472        let transaction = mock_blob_tx(1, 10);
473
474        let header = Header {
475            base_fee_per_gas: Some(1337),
476            withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
477            blob_gas_used: Some(1),
478            transactions_root: proofs::calculate_transaction_root(std::slice::from_ref(
479                &transaction,
480            )),
481            ..Default::default()
482        };
483        let body = BlockBody {
484            transactions: vec![transaction],
485            ommers: vec![],
486            withdrawals: Some(Withdrawals::default()),
487        };
488
489        let block = SealedBlock::seal_slow(alloy_consensus::Block { header, body });
490
491        // 10 blobs times the blob gas per blob.
492        let expected_blob_gas_used = 10 * DATA_GAS_PER_BLOB;
493
494        // validate blob, it should fail blob gas used validation
495        assert_eq!(
496            validate_block_pre_execution(&block, &chain_spec),
497            Err(ConsensusError::BlobGasUsedDiff(GotExpected {
498                got: 1,
499                expected: expected_blob_gas_used
500            }))
501        );
502    }
503
504    #[test]
505    fn validate_header_extra_data_with_custom_limit() {
506        // Test with default 32 bytes - should pass
507        let header_32 = Header { extra_data: Bytes::from(vec![0; 32]), ..Default::default() };
508        assert!(validate_header_extra_data(&header_32, 32).is_ok());
509
510        // Test exceeding default - should fail
511        let header_33 = Header { extra_data: Bytes::from(vec![0; 33]), ..Default::default() };
512        assert_eq!(
513            validate_header_extra_data(&header_33, 32),
514            Err(ConsensusError::ExtraDataExceedsMax { len: 33 })
515        );
516
517        // Test with custom larger limit - should pass
518        assert!(validate_header_extra_data(&header_33, 64).is_ok());
519    }
520}