reth_consensus_common/
validation.rs

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