reth_consensus_common/
validation.rs

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