Skip to main content

reth_consensus_common/
validation.rs

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