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