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    if parent.hash() != header.parent_hash() {
283        return Err(ConsensusError::ParentHashMismatch(
284            GotExpected { got: header.parent_hash(), expected: parent.hash() }.into(),
285        ))
286    }
287
288    let Some(parent_number) = parent.number().checked_add(1) else {
289        // parent block already reached the maximum
290        return Err(ConsensusError::ParentBlockNumberMismatch {
291            parent_block_number: parent.number(),
292            block_number: u64::MAX,
293        })
294    };
295
296    // Parent number is consistent.
297    if parent_number != header.number() {
298        return Err(ConsensusError::ParentBlockNumberMismatch {
299            parent_block_number: parent.number(),
300            block_number: header.number(),
301        })
302    }
303
304    Ok(())
305}
306
307/// Validates the base fee against the parent and EIP-1559 rules.
308#[inline]
309pub fn validate_against_parent_eip1559_base_fee<ChainSpec: EthChainSpec + EthereumHardforks>(
310    header: &ChainSpec::Header,
311    parent: &ChainSpec::Header,
312    chain_spec: &ChainSpec,
313) -> Result<(), ConsensusError> {
314    if chain_spec.is_london_active_at_block(header.number()) {
315        let base_fee = header.base_fee_per_gas().ok_or(ConsensusError::BaseFeeMissing)?;
316
317        let expected_base_fee = if chain_spec
318            .ethereum_fork_activation(EthereumHardfork::London)
319            .transitions_at_block(header.number())
320        {
321            alloy_eips::eip1559::INITIAL_BASE_FEE
322        } else {
323            chain_spec
324                .next_block_base_fee(parent, header.timestamp())
325                .ok_or(ConsensusError::BaseFeeMissing)?
326        };
327        if expected_base_fee != base_fee {
328            return Err(ConsensusError::BaseFeeDiff(GotExpected {
329                expected: expected_base_fee,
330                got: base_fee,
331            }))
332        }
333    }
334
335    Ok(())
336}
337
338/// Validates that the block timestamp is greater than the parent block timestamp.
339#[inline]
340pub fn validate_against_parent_timestamp<H: BlockHeader>(
341    header: &H,
342    parent: &H,
343) -> Result<(), ConsensusError> {
344    if header.timestamp() <= parent.timestamp() {
345        return Err(ConsensusError::TimestampIsInPast {
346            parent_timestamp: parent.timestamp(),
347            timestamp: header.timestamp(),
348        })
349    }
350    Ok(())
351}
352
353/// Validates gas limit against parent gas limit.
354///
355/// The maximum allowable difference between self and parent gas limits is determined by the
356/// parent's gas limit divided by the [`GAS_LIMIT_BOUND_DIVISOR`].
357#[inline]
358pub fn validate_against_parent_gas_limit<
359    H: BlockHeader,
360    ChainSpec: EthChainSpec + EthereumHardforks,
361>(
362    header: &SealedHeader<H>,
363    parent: &SealedHeader<H>,
364    chain_spec: &ChainSpec,
365) -> Result<(), ConsensusError> {
366    // Determine the parent gas limit, considering elasticity multiplier on the London fork.
367    let parent_gas_limit = if !chain_spec.is_london_active_at_block(parent.number()) &&
368        chain_spec.is_london_active_at_block(header.number())
369    {
370        parent.gas_limit() *
371            chain_spec.base_fee_params_at_timestamp(header.timestamp()).elasticity_multiplier
372                as u64
373    } else {
374        parent.gas_limit()
375    };
376
377    // Check for an increase in gas limit beyond the allowed threshold.
378    if header.gas_limit() > parent_gas_limit {
379        if header.gas_limit() - parent_gas_limit >= parent_gas_limit / GAS_LIMIT_BOUND_DIVISOR {
380            return Err(ConsensusError::GasLimitInvalidIncrease {
381                parent_gas_limit,
382                child_gas_limit: header.gas_limit(),
383            })
384        }
385    }
386    // Check for a decrease in gas limit beyond the allowed threshold.
387    else if parent_gas_limit - header.gas_limit() >= parent_gas_limit / GAS_LIMIT_BOUND_DIVISOR {
388        return Err(ConsensusError::GasLimitInvalidDecrease {
389            parent_gas_limit,
390            child_gas_limit: header.gas_limit(),
391        })
392    }
393    // Check if the self gas limit is below the minimum required limit.
394    else if header.gas_limit() < MINIMUM_GAS_LIMIT {
395        return Err(ConsensusError::GasLimitInvalidMinimum { child_gas_limit: header.gas_limit() })
396    }
397
398    Ok(())
399}
400
401/// Validates that the EIP-4844 header fields are correct with respect to the parent block. This
402/// ensures that the `blob_gas_used` and `excess_blob_gas` fields exist in the child header, and
403/// that the `excess_blob_gas` field matches the expected `excess_blob_gas` calculated from the
404/// parent header fields.
405pub fn validate_against_parent_4844<H: BlockHeader>(
406    header: &H,
407    parent: &H,
408    blob_params: BlobParams,
409) -> Result<(), ConsensusError> {
410    // From [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844#header-extension):
411    //
412    // > For the first post-fork block, both parent.blob_gas_used and parent.excess_blob_gas
413    // > are evaluated as 0.
414    //
415    // This means in the first post-fork block, calc_excess_blob_gas will return 0.
416    let parent_blob_gas_used = parent.blob_gas_used().unwrap_or(0);
417    let parent_excess_blob_gas = parent.excess_blob_gas().unwrap_or(0);
418
419    if header.blob_gas_used().is_none() {
420        return Err(ConsensusError::BlobGasUsedMissing)
421    }
422    let excess_blob_gas = header.excess_blob_gas().ok_or(ConsensusError::ExcessBlobGasMissing)?;
423
424    let parent_base_fee_per_gas = parent.base_fee_per_gas().unwrap_or(0);
425    let expected_excess_blob_gas = blob_params.next_block_excess_blob_gas_osaka(
426        parent_excess_blob_gas,
427        parent_blob_gas_used,
428        parent_base_fee_per_gas,
429    );
430    if expected_excess_blob_gas != excess_blob_gas {
431        return Err(ConsensusError::ExcessBlobGasDiff {
432            diff: GotExpected { got: excess_blob_gas, expected: expected_excess_blob_gas },
433            parent_excess_blob_gas,
434            parent_blob_gas_used,
435        })
436    }
437
438    Ok(())
439}
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444    use alloy_consensus::{BlockBody, Header, TxEip4844};
445    use alloy_eips::eip4895::Withdrawals;
446    use alloy_primitives::{Address, Bytes, Signature, U256};
447    use rand::Rng;
448    use reth_chainspec::ChainSpecBuilder;
449    use reth_ethereum_primitives::{Transaction, TransactionSigned};
450    use reth_primitives_traits::proofs;
451
452    fn mock_blob_tx(nonce: u64, num_blobs: usize) -> TransactionSigned {
453        let mut rng = rand::rng();
454        let request = Transaction::Eip4844(TxEip4844 {
455            chain_id: 1u64,
456            nonce,
457            max_fee_per_gas: 0x28f000fff,
458            max_priority_fee_per_gas: 0x28f000fff,
459            max_fee_per_blob_gas: 0x7,
460            gas_limit: 10,
461            to: Address::default(),
462            value: U256::from(3_u64),
463            input: Bytes::from(vec![1, 2]),
464            access_list: Default::default(),
465            blob_versioned_hashes: std::iter::repeat_with(|| rng.random())
466                .take(num_blobs)
467                .collect(),
468        });
469
470        let signature = Signature::new(U256::default(), U256::default(), true);
471
472        TransactionSigned::new_unhashed(request, signature)
473    }
474
475    #[test]
476    fn cancun_block_incorrect_blob_gas_used() {
477        let chain_spec = ChainSpecBuilder::mainnet().cancun_activated().build();
478
479        // create a tx with 10 blobs
480        let transaction = mock_blob_tx(1, 10);
481
482        let header = Header {
483            base_fee_per_gas: Some(1337),
484            withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
485            blob_gas_used: Some(1),
486            transactions_root: proofs::calculate_transaction_root(std::slice::from_ref(
487                &transaction,
488            )),
489            ..Default::default()
490        };
491        let body = BlockBody {
492            transactions: vec![transaction],
493            ommers: vec![],
494            withdrawals: Some(Withdrawals::default()),
495        };
496
497        let block = SealedBlock::seal_slow(alloy_consensus::Block { header, body });
498
499        // 10 blobs times the blob gas per blob.
500        let expected_blob_gas_used = 10 * DATA_GAS_PER_BLOB;
501
502        // validate blob, it should fail blob gas used validation
503        assert_eq!(
504            validate_block_pre_execution(&block, &chain_spec),
505            Err(ConsensusError::BlobGasUsedDiff(GotExpected {
506                got: 1,
507                expected: expected_blob_gas_used
508            }))
509        );
510    }
511
512    #[test]
513    fn validate_header_extra_data_with_custom_limit() {
514        // Test with default 32 bytes - should pass
515        let header_32 = Header { extra_data: Bytes::from(vec![0; 32]), ..Default::default() };
516        assert!(validate_header_extra_data(&header_32, 32).is_ok());
517
518        // Test exceeding default - should fail
519        let header_33 = Header { extra_data: Bytes::from(vec![0; 33]), ..Default::default() };
520        assert_eq!(
521            validate_header_extra_data(&header_33, 32),
522            Err(ConsensusError::ExtraDataExceedsMax { len: 33 })
523        );
524
525        // Test with custom larger limit - should pass
526        assert!(validate_header_extra_data(&header_33, 64).is_ok());
527    }
528}