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