reth_consensus_common/
validation.rs

1//! Collection of methods for block validation.
2
3use alloy_consensus::{constants::MAXIMUM_EXTRA_DATA_SIZE, BlockHeader, EMPTY_OMMER_ROOT_HASH};
4use alloy_eips::{
5    calc_next_block_base_fee,
6    eip4844::{DATA_GAS_PER_BLOB, MAX_DATA_GAS_PER_BLOCK},
7};
8use reth_chainspec::{EthChainSpec, EthereumHardfork, EthereumHardforks};
9use reth_consensus::ConsensusError;
10use reth_primitives::SealedBlock;
11use reth_primitives_traits::{BlockBody, GotExpected, SealedHeader};
12use revm_primitives::calc_excess_blob_gas;
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    Ok(())
24}
25
26/// Ensure the EIP-1559 base fee is set if the London hardfork is active.
27#[inline]
28pub fn validate_header_base_fee<H: BlockHeader, ChainSpec: EthereumHardforks>(
29    header: &H,
30    chain_spec: &ChainSpec,
31) -> Result<(), ConsensusError> {
32    if chain_spec.is_fork_active_at_block(EthereumHardfork::London, header.number()) &&
33        header.base_fee_per_gas().is_none()
34    {
35        return Err(ConsensusError::BaseFeeMissing)
36    }
37    Ok(())
38}
39
40/// Validate that withdrawals are present in Shanghai
41///
42/// See [EIP-4895]: Beacon chain push withdrawals as operations
43///
44/// [EIP-4895]: https://eips.ethereum.org/EIPS/eip-4895
45#[inline]
46pub fn validate_shanghai_withdrawals<H: BlockHeader, B: BlockBody>(
47    block: &SealedBlock<H, B>,
48) -> Result<(), ConsensusError> {
49    let withdrawals = block.body.withdrawals().ok_or(ConsensusError::BodyWithdrawalsMissing)?;
50    let withdrawals_root = alloy_consensus::proofs::calculate_withdrawals_root(withdrawals);
51    let header_withdrawals_root =
52        block.withdrawals_root().ok_or(ConsensusError::WithdrawalsRootMissing)?;
53    if withdrawals_root != *header_withdrawals_root {
54        return Err(ConsensusError::BodyWithdrawalsRootDiff(
55            GotExpected { got: withdrawals_root, expected: header_withdrawals_root }.into(),
56        ));
57    }
58    Ok(())
59}
60
61/// Validate that blob gas is present in the block if Cancun is active.
62///
63/// See [EIP-4844]: Shard Blob Transactions
64///
65/// [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844
66#[inline]
67pub fn validate_cancun_gas<H: BlockHeader, B: BlockBody>(
68    block: &SealedBlock<H, B>,
69) -> Result<(), ConsensusError> {
70    // Check that the blob gas used in the header matches the sum of the blob gas used by each
71    // blob tx
72    let header_blob_gas_used =
73        block.header().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<H, B, ChainSpec>(
137    block: &SealedBlock<H, B>,
138    chain_spec: &ChainSpec,
139) -> Result<(), ConsensusError>
140where
141    H: BlockHeader,
142    B: BlockBody,
143    ChainSpec: EthereumHardforks,
144{
145    // Check ommers hash
146    let ommers_hash = block.body.calculate_ommers_root();
147    if Some(block.header.ommers_hash()) != ommers_hash {
148        return Err(ConsensusError::BodyOmmersHashDiff(
149            GotExpected {
150                got: ommers_hash.unwrap_or(EMPTY_OMMER_ROOT_HASH),
151                expected: block.header.ommers_hash(),
152            }
153            .into(),
154        ))
155    }
156
157    // Check transaction root
158    if let Err(error) = block.ensure_transaction_root_valid() {
159        return Err(ConsensusError::BodyTransactionRootDiff(error.into()))
160    }
161
162    // EIP-4895: Beacon chain push withdrawals as operations
163    if chain_spec.is_shanghai_active_at_timestamp(block.timestamp()) {
164        validate_shanghai_withdrawals(block)?;
165    }
166
167    if chain_spec.is_cancun_active_at_timestamp(block.timestamp()) {
168        validate_cancun_gas(block)?;
169    }
170
171    Ok(())
172}
173
174/// Validates that the EIP-4844 header fields exist and conform to the spec. This ensures that:
175///
176///  * `blob_gas_used` exists as a header field
177///  * `excess_blob_gas` exists as a header field
178///  * `parent_beacon_block_root` exists as a header field
179///  * `blob_gas_used` is less than or equal to `MAX_DATA_GAS_PER_BLOCK`
180///  * `blob_gas_used` is a multiple of `DATA_GAS_PER_BLOB`
181///  * `excess_blob_gas` is a multiple of `DATA_GAS_PER_BLOB`
182pub fn validate_4844_header_standalone<H: BlockHeader>(header: &H) -> Result<(), ConsensusError> {
183    let blob_gas_used = header.blob_gas_used().ok_or(ConsensusError::BlobGasUsedMissing)?;
184    let excess_blob_gas = header.excess_blob_gas().ok_or(ConsensusError::ExcessBlobGasMissing)?;
185
186    if header.parent_beacon_block_root().is_none() {
187        return Err(ConsensusError::ParentBeaconBlockRootMissing)
188    }
189
190    if blob_gas_used > MAX_DATA_GAS_PER_BLOCK {
191        return Err(ConsensusError::BlobGasUsedExceedsMaxBlobGasPerBlock {
192            blob_gas_used,
193            max_blob_gas_per_block: MAX_DATA_GAS_PER_BLOCK,
194        })
195    }
196
197    if blob_gas_used % DATA_GAS_PER_BLOB != 0 {
198        return Err(ConsensusError::BlobGasUsedNotMultipleOfBlobGasPerBlob {
199            blob_gas_used,
200            blob_gas_per_blob: DATA_GAS_PER_BLOB,
201        })
202    }
203
204    // `excess_blob_gas` must also be a multiple of `DATA_GAS_PER_BLOB`. This will be checked later
205    // (via `calc_excess_blob_gas`), but it doesn't hurt to catch the problem sooner.
206    if excess_blob_gas % DATA_GAS_PER_BLOB != 0 {
207        return Err(ConsensusError::ExcessBlobGasNotMultipleOfBlobGasPerBlob {
208            excess_blob_gas,
209            blob_gas_per_blob: DATA_GAS_PER_BLOB,
210        })
211    }
212
213    Ok(())
214}
215
216/// Validates the header's extradata according to the beacon consensus rules.
217///
218/// From yellow paper: extraData: An arbitrary byte array containing data relevant to this block.
219/// This must be 32 bytes or fewer; formally Hx.
220#[inline]
221pub fn validate_header_extradata<H: BlockHeader>(header: &H) -> Result<(), ConsensusError> {
222    let extradata_len = header.extra_data().len();
223    if extradata_len > MAXIMUM_EXTRA_DATA_SIZE {
224        Err(ConsensusError::ExtraDataExceedsMax { len: extradata_len })
225    } else {
226        Ok(())
227    }
228}
229
230/// Validates against the parent hash and number.
231///
232/// This function ensures that the header block number is sequential and that the hash of the parent
233/// header matches the parent hash in the header.
234#[inline]
235pub fn validate_against_parent_hash_number<H: BlockHeader>(
236    header: &H,
237    parent: &SealedHeader<H>,
238) -> Result<(), ConsensusError> {
239    // Parent number is consistent.
240    if parent.number() + 1 != header.number() {
241        return Err(ConsensusError::ParentBlockNumberMismatch {
242            parent_block_number: parent.number(),
243            block_number: header.number(),
244        })
245    }
246
247    if parent.hash() != header.parent_hash() {
248        return Err(ConsensusError::ParentHashMismatch(
249            GotExpected { got: header.parent_hash(), expected: parent.hash() }.into(),
250        ))
251    }
252
253    Ok(())
254}
255
256/// Validates the base fee against the parent and EIP-1559 rules.
257#[inline]
258pub fn validate_against_parent_eip1559_base_fee<
259    H: BlockHeader,
260    ChainSpec: EthChainSpec + EthereumHardforks,
261>(
262    header: &H,
263    parent: &H,
264    chain_spec: &ChainSpec,
265) -> Result<(), ConsensusError> {
266    if chain_spec.fork(EthereumHardfork::London).active_at_block(header.number()) {
267        let base_fee = header.base_fee_per_gas().ok_or(ConsensusError::BaseFeeMissing)?;
268
269        let expected_base_fee =
270            if chain_spec.fork(EthereumHardfork::London).transitions_at_block(header.number()) {
271                alloy_eips::eip1559::INITIAL_BASE_FEE
272            } else {
273                // This BaseFeeMissing will not happen as previous blocks are checked to have
274                // them.
275                let base_fee = parent.base_fee_per_gas().ok_or(ConsensusError::BaseFeeMissing)?;
276                calc_next_block_base_fee(
277                    parent.gas_used(),
278                    parent.gas_limit(),
279                    base_fee,
280                    chain_spec.base_fee_params_at_timestamp(header.timestamp()),
281                )
282            };
283        if expected_base_fee != base_fee {
284            return Err(ConsensusError::BaseFeeDiff(GotExpected {
285                expected: expected_base_fee,
286                got: base_fee,
287            }))
288        }
289    }
290
291    Ok(())
292}
293
294/// Validates the timestamp against the parent to make sure it is in the past.
295#[inline]
296pub fn validate_against_parent_timestamp<H: BlockHeader>(
297    header: &H,
298    parent: &H,
299) -> Result<(), ConsensusError> {
300    if header.timestamp() <= parent.timestamp() {
301        return Err(ConsensusError::TimestampIsInPast {
302            parent_timestamp: parent.timestamp(),
303            timestamp: header.timestamp(),
304        })
305    }
306    Ok(())
307}
308
309/// Validates that the EIP-4844 header fields are correct with respect to the parent block. This
310/// ensures that the `blob_gas_used` and `excess_blob_gas` fields exist in the child header, and
311/// that the `excess_blob_gas` field matches the expected `excess_blob_gas` calculated from the
312/// parent header fields.
313pub fn validate_against_parent_4844<H: BlockHeader>(
314    header: &H,
315    parent: &H,
316) -> Result<(), ConsensusError> {
317    // From [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844#header-extension):
318    //
319    // > For the first post-fork block, both parent.blob_gas_used and parent.excess_blob_gas
320    // > are evaluated as 0.
321    //
322    // This means in the first post-fork block, calc_excess_blob_gas will return 0.
323    let parent_blob_gas_used = parent.blob_gas_used().unwrap_or(0);
324    let parent_excess_blob_gas = parent.excess_blob_gas().unwrap_or(0);
325
326    if header.blob_gas_used().is_none() {
327        return Err(ConsensusError::BlobGasUsedMissing)
328    }
329    let excess_blob_gas = header.excess_blob_gas().ok_or(ConsensusError::ExcessBlobGasMissing)?;
330
331    let expected_excess_blob_gas =
332        calc_excess_blob_gas(parent_excess_blob_gas, parent_blob_gas_used);
333    if expected_excess_blob_gas != excess_blob_gas {
334        return Err(ConsensusError::ExcessBlobGasDiff {
335            diff: GotExpected { got: excess_blob_gas, expected: expected_excess_blob_gas },
336            parent_excess_blob_gas,
337            parent_blob_gas_used,
338        })
339    }
340
341    Ok(())
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347    use alloy_consensus::{Header, TxEip4844, EMPTY_OMMER_ROOT_HASH, EMPTY_ROOT_HASH};
348    use alloy_eips::{
349        eip4895::{Withdrawal, Withdrawals},
350        BlockHashOrNumber,
351    };
352    use alloy_primitives::{
353        hex_literal::hex, Address, BlockHash, BlockNumber, Bytes, PrimitiveSignature as Signature,
354        U256,
355    };
356    use mockall::mock;
357    use rand::Rng;
358    use reth_chainspec::ChainSpecBuilder;
359    use reth_primitives::{proofs, Account, BlockBody, Transaction, TransactionSigned};
360    use reth_storage_api::{
361        errors::provider::ProviderResult, AccountReader, HeaderProvider, WithdrawalsProvider,
362    };
363    use std::ops::RangeBounds;
364
365    mock! {
366        WithdrawalsProvider {}
367
368        impl WithdrawalsProvider for WithdrawalsProvider {
369            fn latest_withdrawal(&self) -> ProviderResult<Option<Withdrawal>> ;
370
371            fn withdrawals_by_block(
372                &self,
373                _id: BlockHashOrNumber,
374                _timestamp: u64,
375            ) -> ProviderResult<Option<Withdrawals>> ;
376        }
377    }
378
379    struct Provider {
380        is_known: bool,
381        parent: Option<Header>,
382        account: Option<Account>,
383        withdrawals_provider: MockWithdrawalsProvider,
384    }
385
386    impl Provider {
387        /// New provider with parent
388        fn new(parent: Option<Header>) -> Self {
389            Self {
390                is_known: false,
391                parent,
392                account: None,
393                withdrawals_provider: MockWithdrawalsProvider::new(),
394            }
395        }
396    }
397
398    impl AccountReader for Provider {
399        fn basic_account(&self, _address: Address) -> ProviderResult<Option<Account>> {
400            Ok(self.account)
401        }
402    }
403
404    impl HeaderProvider for Provider {
405        type Header = Header;
406
407        fn is_known(&self, _block_hash: &BlockHash) -> ProviderResult<bool> {
408            Ok(self.is_known)
409        }
410
411        fn header(&self, _block_number: &BlockHash) -> ProviderResult<Option<Header>> {
412            Ok(self.parent.clone())
413        }
414
415        fn header_by_number(&self, _num: u64) -> ProviderResult<Option<Header>> {
416            Ok(self.parent.clone())
417        }
418
419        fn header_td(&self, _hash: &BlockHash) -> ProviderResult<Option<U256>> {
420            Ok(None)
421        }
422
423        fn header_td_by_number(&self, _number: BlockNumber) -> ProviderResult<Option<U256>> {
424            Ok(None)
425        }
426
427        fn headers_range(
428            &self,
429            _range: impl RangeBounds<BlockNumber>,
430        ) -> ProviderResult<Vec<Header>> {
431            Ok(vec![])
432        }
433
434        fn sealed_header(
435            &self,
436            _block_number: BlockNumber,
437        ) -> ProviderResult<Option<SealedHeader>> {
438            Ok(None)
439        }
440
441        fn sealed_headers_while(
442            &self,
443            _range: impl RangeBounds<BlockNumber>,
444            _predicate: impl FnMut(&SealedHeader) -> bool,
445        ) -> ProviderResult<Vec<SealedHeader>> {
446            Ok(vec![])
447        }
448    }
449
450    impl WithdrawalsProvider for Provider {
451        fn withdrawals_by_block(
452            &self,
453            _id: BlockHashOrNumber,
454            _timestamp: u64,
455        ) -> ProviderResult<Option<Withdrawals>> {
456            self.withdrawals_provider.withdrawals_by_block(_id, _timestamp)
457        }
458
459        fn latest_withdrawal(&self) -> ProviderResult<Option<Withdrawal>> {
460            self.withdrawals_provider.latest_withdrawal()
461        }
462    }
463
464    fn mock_blob_tx(nonce: u64, num_blobs: usize) -> TransactionSigned {
465        let mut rng = rand::thread_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.gen()).take(num_blobs).collect(),
478        });
479
480        let signature = Signature::new(U256::default(), U256::default(), true);
481
482        TransactionSigned::new_unhashed(request, signature)
483    }
484
485    /// got test block
486    fn mock_block() -> (SealedBlock, Header) {
487        // https://etherscan.io/block/15867168 where transaction root and receipts root are cleared
488        // empty merkle tree: 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421
489
490        let header = Header {
491            parent_hash: hex!("859fad46e75d9be177c2584843501f2270c7e5231711e90848290d12d7c6dcdd").into(),
492            ommers_hash: EMPTY_OMMER_ROOT_HASH,
493            beneficiary: hex!("4675c7e5baafbffbca748158becba61ef3b0a263").into(),
494            state_root: hex!("8337403406e368b3e40411138f4868f79f6d835825d55fd0c2f6e17b1a3948e9").into(),
495            transactions_root: EMPTY_ROOT_HASH,
496            receipts_root: EMPTY_ROOT_HASH,
497            logs_bloom: hex!("002400000000004000220000800002000000000000000000000000000000100000000000000000100000000000000021020000000800000006000000002100040000000c0004000000000008000008200000000000000000000000008000000001040000020000020000002000000800000002000020000000022010000000000000010002001000000000020200000000000001000200880000004000000900020000000000020000000040000000000000000000000000000080000000000001000002000000000000012000200020000000000000001000000000000020000010321400000000100000000000000000000000000000400000000000000000").into(),
498            difficulty: U256::ZERO, // total difficulty: 0xc70d815d562d3cfa955).into(),
499            number: 0xf21d20,
500            gas_limit: 0x1c9c380,
501            gas_used: 0x6e813,
502            timestamp: 0x635f9657,
503            extra_data: hex!("")[..].into(),
504            mix_hash: hex!("0000000000000000000000000000000000000000000000000000000000000000").into(),
505            nonce: 0x0000000000000000u64.into(),
506            base_fee_per_gas: 0x28f0001df.into(),
507            withdrawals_root: None,
508            blob_gas_used: None,
509            excess_blob_gas: None,
510            parent_beacon_block_root: None,
511            requests_hash: None,
512            target_blobs_per_block: None,
513        };
514        // size: 0x9b5
515
516        let mut parent = header.clone();
517        parent.gas_used = 17763076;
518        parent.gas_limit = 30000000;
519        parent.base_fee_per_gas = Some(0x28041f7f5);
520        parent.number -= 1;
521        parent.timestamp -= 1;
522
523        let ommers = Vec::new();
524        let transactions = Vec::new();
525
526        (
527            SealedBlock {
528                header: SealedHeader::seal(header),
529                body: BlockBody { transactions, ommers, withdrawals: None },
530            },
531            parent,
532        )
533    }
534
535    #[test]
536    fn valid_withdrawal_index() {
537        let chain_spec = ChainSpecBuilder::mainnet().shanghai_activated().build();
538
539        let create_block_with_withdrawals = |indexes: &[u64]| {
540            let withdrawals = Withdrawals::new(
541                indexes
542                    .iter()
543                    .map(|idx| Withdrawal { index: *idx, ..Default::default() })
544                    .collect(),
545            );
546
547            let header = Header {
548                withdrawals_root: Some(proofs::calculate_withdrawals_root(&withdrawals)),
549                ..Default::default()
550            };
551
552            SealedBlock {
553                header: SealedHeader::seal(header),
554                body: BlockBody { withdrawals: Some(withdrawals), ..Default::default() },
555            }
556        };
557
558        // Single withdrawal
559        let block = create_block_with_withdrawals(&[1]);
560        assert_eq!(validate_block_pre_execution(&block, &chain_spec), Ok(()));
561
562        // Multiple increasing withdrawals
563        let block = create_block_with_withdrawals(&[1, 2, 3]);
564        assert_eq!(validate_block_pre_execution(&block, &chain_spec), Ok(()));
565        let block = create_block_with_withdrawals(&[5, 6, 7, 8, 9]);
566        assert_eq!(validate_block_pre_execution(&block, &chain_spec), Ok(()));
567        let (_, parent) = mock_block();
568
569        // Withdrawal index should be the last withdrawal index + 1
570        let mut provider = Provider::new(Some(parent));
571        provider
572            .withdrawals_provider
573            .expect_latest_withdrawal()
574            .return_const(Ok(Some(Withdrawal { index: 2, ..Default::default() })));
575    }
576
577    #[test]
578    fn cancun_block_incorrect_blob_gas_used() {
579        let chain_spec = ChainSpecBuilder::mainnet().cancun_activated().build();
580
581        // create a tx with 10 blobs
582        let transaction = mock_blob_tx(1, 10);
583
584        let header = Header {
585            base_fee_per_gas: Some(1337),
586            withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
587            blob_gas_used: Some(1),
588            transactions_root: proofs::calculate_transaction_root(&[transaction.clone()]),
589            ..Default::default()
590        };
591        let header = SealedHeader::seal(header);
592
593        let body = BlockBody {
594            transactions: vec![transaction],
595            ommers: vec![],
596            withdrawals: Some(Withdrawals::default()),
597        };
598
599        let block = SealedBlock::new(header, body);
600
601        // 10 blobs times the blob gas per blob.
602        let expected_blob_gas_used = 10 * DATA_GAS_PER_BLOB;
603
604        // validate blob, it should fail blob gas used validation
605        assert_eq!(
606            validate_block_pre_execution(&block, &chain_spec),
607            Err(ConsensusError::BlobGasUsedDiff(GotExpected {
608                got: 1,
609                expected: expected_blob_gas_used
610            }))
611        );
612    }
613}