reth_optimism_consensus/validation/
mod.rs

1//! Verification of blocks w.r.t. Optimism hardforks.
2
3pub mod canyon;
4pub mod isthmus;
5
6// Re-export the decode_holocene_base_fee function for compatibility
7use reth_execution_types::BlockExecutionResult;
8pub use reth_optimism_chainspec::decode_holocene_base_fee;
9
10use crate::proof::calculate_receipt_root_optimism;
11use alloc::vec::Vec;
12use alloy_consensus::{BlockHeader, TxReceipt, EMPTY_OMMER_ROOT_HASH};
13use alloy_eips::Encodable2718;
14use alloy_primitives::{Bloom, Bytes, B256};
15use alloy_trie::EMPTY_ROOT_HASH;
16use reth_consensus::ConsensusError;
17use reth_optimism_forks::OpHardforks;
18use reth_optimism_primitives::DepositReceipt;
19use reth_primitives_traits::{receipt::gas_spent_by_transactions, BlockBody, GotExpected};
20
21/// Ensures the block response data matches the header.
22///
23/// This ensures the body response items match the header's hashes:
24///   - ommer hash
25///   - transaction root
26///   - withdrawals root: the body's withdrawals root must only match the header's before isthmus
27pub fn validate_body_against_header_op<B, H>(
28    chain_spec: impl OpHardforks,
29    body: &B,
30    header: &H,
31) -> Result<(), ConsensusError>
32where
33    B: BlockBody,
34    H: reth_primitives_traits::BlockHeader,
35{
36    let ommers_hash = body.calculate_ommers_root();
37    if Some(header.ommers_hash()) != ommers_hash {
38        return Err(ConsensusError::BodyOmmersHashDiff(
39            GotExpected {
40                got: ommers_hash.unwrap_or(EMPTY_OMMER_ROOT_HASH),
41                expected: header.ommers_hash(),
42            }
43            .into(),
44        ))
45    }
46
47    let tx_root = body.calculate_tx_root();
48    if header.transactions_root() != tx_root {
49        return Err(ConsensusError::BodyTransactionRootDiff(
50            GotExpected { got: tx_root, expected: header.transactions_root() }.into(),
51        ))
52    }
53
54    match (header.withdrawals_root(), body.calculate_withdrawals_root()) {
55        (Some(header_withdrawals_root), Some(withdrawals_root)) => {
56            // after isthmus, the withdrawals root field is repurposed and no longer mirrors the
57            // withdrawals root computed from the body
58            if chain_spec.is_isthmus_active_at_timestamp(header.timestamp()) {
59                // After isthmus we only ensure that the body has empty withdrawals
60                if withdrawals_root != EMPTY_ROOT_HASH {
61                    return Err(ConsensusError::BodyWithdrawalsRootDiff(
62                        GotExpected { got: withdrawals_root, expected: EMPTY_ROOT_HASH }.into(),
63                    ))
64                }
65            } else {
66                // before isthmus we ensure that the header root matches the body
67                if withdrawals_root != header_withdrawals_root {
68                    return Err(ConsensusError::BodyWithdrawalsRootDiff(
69                        GotExpected { got: withdrawals_root, expected: header_withdrawals_root }
70                            .into(),
71                    ))
72                }
73            }
74        }
75        (None, None) => {
76            // this is ok because we assume the fork is not active in this case
77        }
78        _ => return Err(ConsensusError::WithdrawalsRootUnexpected),
79    }
80
81    Ok(())
82}
83
84/// Validate a block with regard to execution results:
85///
86/// - Compares the receipts root in the block header to the block body
87/// - Compares the gas used in the block header to the actual gas usage after execution
88pub fn validate_block_post_execution<R: DepositReceipt>(
89    header: impl BlockHeader,
90    chain_spec: impl OpHardforks,
91    result: &BlockExecutionResult<R>,
92) -> Result<(), ConsensusError> {
93    // Validate that the blob gas used is present and correctly computed if Jovian is active.
94    if chain_spec.is_jovian_active_at_timestamp(header.timestamp()) {
95        let computed_blob_gas_used = result.blob_gas_used;
96        let header_blob_gas_used =
97            header.blob_gas_used().ok_or(ConsensusError::BlobGasUsedMissing)?;
98
99        if computed_blob_gas_used != header_blob_gas_used {
100            return Err(ConsensusError::BlobGasUsedDiff(GotExpected {
101                got: computed_blob_gas_used,
102                expected: header_blob_gas_used,
103            }));
104        }
105    }
106
107    let receipts = &result.receipts;
108
109    // Before Byzantium, receipts contained state root that would mean that expensive
110    // operation as hashing that is required for state root got calculated in every
111    // transaction This was replaced with is_success flag.
112    // See more about EIP here: https://eips.ethereum.org/EIPS/eip-658
113    if chain_spec.is_byzantium_active_at_block(header.number()) &&
114        let Err(error) = verify_receipts_optimism(
115            header.receipts_root(),
116            header.logs_bloom(),
117            receipts,
118            chain_spec,
119            header.timestamp(),
120        )
121    {
122        let receipts = receipts
123            .iter()
124            .map(|r| Bytes::from(r.with_bloom_ref().encoded_2718()))
125            .collect::<Vec<_>>();
126        tracing::debug!(%error, ?receipts, "receipts verification failed");
127        return Err(error)
128    }
129
130    // Check if gas used matches the value set in header.
131    let cumulative_gas_used =
132        receipts.last().map(|receipt| receipt.cumulative_gas_used()).unwrap_or(0);
133    if header.gas_used() != cumulative_gas_used {
134        return Err(ConsensusError::BlockGasUsed {
135            gas: GotExpected { got: cumulative_gas_used, expected: header.gas_used() },
136            gas_spent_by_tx: gas_spent_by_transactions(receipts),
137        })
138    }
139
140    Ok(())
141}
142
143/// Verify the calculated receipts root against the expected receipts root.
144fn verify_receipts_optimism<R: DepositReceipt>(
145    expected_receipts_root: B256,
146    expected_logs_bloom: Bloom,
147    receipts: &[R],
148    chain_spec: impl OpHardforks,
149    timestamp: u64,
150) -> Result<(), ConsensusError> {
151    // Calculate receipts root.
152    let receipts_with_bloom = receipts.iter().map(TxReceipt::with_bloom_ref).collect::<Vec<_>>();
153    let receipts_root =
154        calculate_receipt_root_optimism(&receipts_with_bloom, chain_spec, timestamp);
155
156    // Calculate header logs bloom.
157    let logs_bloom = receipts_with_bloom.iter().fold(Bloom::ZERO, |bloom, r| bloom | r.bloom_ref());
158
159    compare_receipts_root_and_logs_bloom(
160        receipts_root,
161        logs_bloom,
162        expected_receipts_root,
163        expected_logs_bloom,
164    )?;
165
166    Ok(())
167}
168
169/// Compare the calculated receipts root with the expected receipts root, also compare
170/// the calculated logs bloom with the expected logs bloom.
171fn compare_receipts_root_and_logs_bloom(
172    calculated_receipts_root: B256,
173    calculated_logs_bloom: Bloom,
174    expected_receipts_root: B256,
175    expected_logs_bloom: Bloom,
176) -> Result<(), ConsensusError> {
177    if calculated_receipts_root != expected_receipts_root {
178        return Err(ConsensusError::BodyReceiptRootDiff(
179            GotExpected { got: calculated_receipts_root, expected: expected_receipts_root }.into(),
180        ))
181    }
182
183    if calculated_logs_bloom != expected_logs_bloom {
184        return Err(ConsensusError::BodyBloomLogDiff(
185            GotExpected { got: calculated_logs_bloom, expected: expected_logs_bloom }.into(),
186        ))
187    }
188
189    Ok(())
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use alloy_consensus::Header;
196    use alloy_eips::eip7685::Requests;
197    use alloy_primitives::{b256, hex, Bytes, U256};
198    use op_alloy_consensus::OpTxEnvelope;
199    use reth_chainspec::{BaseFeeParams, ChainSpec, EthChainSpec, ForkCondition, Hardfork};
200    use reth_optimism_chainspec::{OpChainSpec, BASE_SEPOLIA};
201    use reth_optimism_forks::{OpHardfork, BASE_SEPOLIA_HARDFORKS};
202    use reth_optimism_primitives::OpReceipt;
203    use std::sync::Arc;
204
205    const HOLOCENE_TIMESTAMP: u64 = 1700000000;
206    const ISTHMUS_TIMESTAMP: u64 = 1750000000;
207    const JOVIAN_TIMESTAMP: u64 = 1800000000;
208    const BLOCK_TIME_SECONDS: u64 = 2;
209
210    fn holocene_chainspec() -> Arc<OpChainSpec> {
211        let mut hardforks = BASE_SEPOLIA_HARDFORKS.clone();
212        hardforks
213            .insert(OpHardfork::Holocene.boxed(), ForkCondition::Timestamp(HOLOCENE_TIMESTAMP));
214        Arc::new(OpChainSpec {
215            inner: ChainSpec {
216                chain: BASE_SEPOLIA.inner.chain,
217                genesis: BASE_SEPOLIA.inner.genesis.clone(),
218                genesis_header: BASE_SEPOLIA.inner.genesis_header.clone(),
219                paris_block_and_final_difficulty: Some((0, U256::from(0))),
220                hardforks,
221                base_fee_params: BASE_SEPOLIA.inner.base_fee_params.clone(),
222                prune_delete_limit: 10000,
223                ..Default::default()
224            },
225        })
226    }
227
228    fn isthmus_chainspec() -> OpChainSpec {
229        let mut chainspec = BASE_SEPOLIA.as_ref().clone();
230        chainspec
231            .inner
232            .hardforks
233            .insert(OpHardfork::Isthmus.boxed(), ForkCondition::Timestamp(ISTHMUS_TIMESTAMP));
234        chainspec
235    }
236
237    fn jovian_chainspec() -> OpChainSpec {
238        let mut chainspec = BASE_SEPOLIA.as_ref().clone();
239        chainspec
240            .inner
241            .hardforks
242            .insert(OpHardfork::Jovian.boxed(), ForkCondition::Timestamp(JOVIAN_TIMESTAMP));
243        chainspec
244    }
245
246    #[test]
247    fn test_get_base_fee_pre_holocene() {
248        let op_chain_spec = BASE_SEPOLIA.clone();
249        let parent = Header {
250            base_fee_per_gas: Some(1),
251            gas_used: 15763614,
252            gas_limit: 144000000,
253            ..Default::default()
254        };
255        let base_fee =
256            reth_optimism_chainspec::OpChainSpec::next_block_base_fee(&op_chain_spec, &parent, 0);
257        assert_eq!(
258            base_fee.unwrap(),
259            op_chain_spec.next_block_base_fee(&parent, 0).unwrap_or_default()
260        );
261    }
262
263    #[test]
264    fn test_get_base_fee_holocene_extra_data_not_set() {
265        let op_chain_spec = holocene_chainspec();
266        let parent = Header {
267            base_fee_per_gas: Some(1),
268            gas_used: 15763614,
269            gas_limit: 144000000,
270            timestamp: HOLOCENE_TIMESTAMP + 3,
271            extra_data: Bytes::from_static(&[0, 0, 0, 0, 0, 0, 0, 0, 0]),
272            ..Default::default()
273        };
274        let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
275            &op_chain_spec,
276            &parent,
277            HOLOCENE_TIMESTAMP + 5,
278        );
279        assert_eq!(
280            base_fee.unwrap(),
281            op_chain_spec.next_block_base_fee(&parent, 0).unwrap_or_default()
282        );
283    }
284
285    #[test]
286    fn test_get_base_fee_holocene_extra_data_set() {
287        let parent = Header {
288            base_fee_per_gas: Some(1),
289            gas_used: 15763614,
290            gas_limit: 144000000,
291            extra_data: Bytes::from_static(&[0, 0, 0, 0, 8, 0, 0, 0, 8]),
292            timestamp: HOLOCENE_TIMESTAMP + 3,
293            ..Default::default()
294        };
295
296        let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
297            &holocene_chainspec(),
298            &parent,
299            HOLOCENE_TIMESTAMP + 5,
300        );
301        assert_eq!(
302            base_fee.unwrap(),
303            parent
304                .next_block_base_fee(BaseFeeParams::new(0x00000008, 0x00000008))
305                .unwrap_or_default()
306        );
307    }
308
309    // <https://sepolia.basescan.org/block/19773628>
310    #[test]
311    fn test_get_base_fee_holocene_extra_data_set_base_sepolia() {
312        let parent = Header {
313            base_fee_per_gas: Some(507),
314            gas_used: 4847634,
315            gas_limit: 60000000,
316            extra_data: hex!("00000000fa0000000a").into(),
317            timestamp: 1735315544,
318            ..Default::default()
319        };
320
321        let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
322            &*BASE_SEPOLIA,
323            &parent,
324            1735315546,
325        )
326        .unwrap();
327        assert_eq!(base_fee, 507);
328    }
329
330    #[test]
331    fn test_get_base_fee_holocene_extra_data_set_and_min_base_fee_set() {
332        const MIN_BASE_FEE: u64 = 10;
333
334        let mut extra_data = Vec::new();
335        // eip1559 params
336        extra_data.append(&mut hex!("00000000fa0000000a").to_vec());
337        // min base fee
338        extra_data.append(&mut MIN_BASE_FEE.to_be_bytes().to_vec());
339        let extra_data = Bytes::from(extra_data);
340
341        let parent = Header {
342            base_fee_per_gas: Some(507),
343            gas_used: 4847634,
344            gas_limit: 60000000,
345            extra_data,
346            timestamp: 1735315544,
347            ..Default::default()
348        };
349
350        let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
351            &*BASE_SEPOLIA,
352            &parent,
353            1735315546,
354        );
355        assert_eq!(base_fee, None);
356    }
357
358    /// The version byte for Jovian is 1.
359    const JOVIAN_EXTRA_DATA_VERSION_BYTE: u8 = 1;
360
361    #[test]
362    fn test_get_base_fee_jovian_extra_data_and_min_base_fee_not_set() {
363        let op_chain_spec = jovian_chainspec();
364
365        let mut extra_data = Vec::new();
366        extra_data.push(JOVIAN_EXTRA_DATA_VERSION_BYTE);
367        // eip1559 params
368        extra_data.append(&mut [0_u8; 8].to_vec());
369        let extra_data = Bytes::from(extra_data);
370
371        let parent = Header {
372            base_fee_per_gas: Some(1),
373            gas_used: 15763614,
374            gas_limit: 144000000,
375            timestamp: JOVIAN_TIMESTAMP,
376            extra_data,
377            ..Default::default()
378        };
379        let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
380            &op_chain_spec,
381            &parent,
382            JOVIAN_TIMESTAMP + BLOCK_TIME_SECONDS,
383        );
384        assert_eq!(base_fee, None);
385    }
386
387    /// After Jovian, the next block base fee cannot be less than the minimum base fee.
388    #[test]
389    fn test_get_base_fee_jovian_default_extra_data_and_min_base_fee() {
390        const CURR_BASE_FEE: u64 = 1;
391        const MIN_BASE_FEE: u64 = 10;
392
393        let mut extra_data = Vec::new();
394        extra_data.push(JOVIAN_EXTRA_DATA_VERSION_BYTE);
395        // eip1559 params
396        extra_data.append(&mut [0_u8; 8].to_vec());
397        // min base fee
398        extra_data.append(&mut MIN_BASE_FEE.to_be_bytes().to_vec());
399        let extra_data = Bytes::from(extra_data);
400
401        let op_chain_spec = jovian_chainspec();
402        let parent = Header {
403            base_fee_per_gas: Some(CURR_BASE_FEE),
404            gas_used: 15763614,
405            gas_limit: 144000000,
406            timestamp: JOVIAN_TIMESTAMP,
407            extra_data,
408            ..Default::default()
409        };
410        let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
411            &op_chain_spec,
412            &parent,
413            JOVIAN_TIMESTAMP + BLOCK_TIME_SECONDS,
414        );
415        assert_eq!(base_fee, Some(MIN_BASE_FEE));
416    }
417
418    /// After Jovian, the next block base fee cannot be less than the minimum base fee.
419    #[test]
420    fn test_jovian_min_base_fee_cannot_decrease() {
421        const MIN_BASE_FEE: u64 = 10;
422
423        let mut extra_data = Vec::new();
424        extra_data.push(JOVIAN_EXTRA_DATA_VERSION_BYTE);
425        // eip1559 params
426        extra_data.append(&mut [0_u8; 8].to_vec());
427        // min base fee
428        extra_data.append(&mut MIN_BASE_FEE.to_be_bytes().to_vec());
429        let extra_data = Bytes::from(extra_data);
430
431        let op_chain_spec = jovian_chainspec();
432
433        // If we're currently at the minimum base fee, the next block base fee cannot decrease.
434        let parent = Header {
435            base_fee_per_gas: Some(MIN_BASE_FEE),
436            gas_used: 10,
437            gas_limit: 144000000,
438            timestamp: JOVIAN_TIMESTAMP,
439            extra_data: extra_data.clone(),
440            ..Default::default()
441        };
442        let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
443            &op_chain_spec,
444            &parent,
445            JOVIAN_TIMESTAMP + BLOCK_TIME_SECONDS,
446        );
447        assert_eq!(base_fee, Some(MIN_BASE_FEE));
448
449        // The next block can increase the base fee
450        let parent = Header {
451            base_fee_per_gas: Some(MIN_BASE_FEE),
452            gas_used: 144000000,
453            gas_limit: 144000000,
454            timestamp: JOVIAN_TIMESTAMP,
455            extra_data,
456            ..Default::default()
457        };
458        let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
459            &op_chain_spec,
460            &parent,
461            JOVIAN_TIMESTAMP + 2 * BLOCK_TIME_SECONDS,
462        );
463        assert_eq!(base_fee, Some(MIN_BASE_FEE + 1));
464    }
465
466    #[test]
467    fn test_jovian_base_fee_can_decrease_if_above_min_base_fee() {
468        const MIN_BASE_FEE: u64 = 10;
469
470        let mut extra_data = Vec::new();
471        extra_data.push(JOVIAN_EXTRA_DATA_VERSION_BYTE);
472        // eip1559 params
473        extra_data.append(&mut [0_u8; 8].to_vec());
474        // min base fee
475        extra_data.append(&mut MIN_BASE_FEE.to_be_bytes().to_vec());
476        let extra_data = Bytes::from(extra_data);
477
478        let op_chain_spec = jovian_chainspec();
479
480        let parent = Header {
481            base_fee_per_gas: Some(100 * MIN_BASE_FEE),
482            gas_used: 10,
483            gas_limit: 144000000,
484            timestamp: JOVIAN_TIMESTAMP,
485            extra_data,
486            ..Default::default()
487        };
488        let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
489            &op_chain_spec,
490            &parent,
491            JOVIAN_TIMESTAMP + BLOCK_TIME_SECONDS,
492        )
493        .unwrap();
494        assert_eq!(
495            base_fee,
496            op_chain_spec
497                .inner
498                .next_block_base_fee(&parent, JOVIAN_TIMESTAMP + BLOCK_TIME_SECONDS)
499                .unwrap()
500        );
501    }
502
503    #[test]
504    fn body_against_header_isthmus() {
505        let chainspec = isthmus_chainspec();
506        let header = Header {
507            base_fee_per_gas: Some(507),
508            gas_used: 4847634,
509            gas_limit: 60000000,
510            extra_data: hex!("00000000fa0000000a").into(),
511            timestamp: 1800000000,
512            withdrawals_root: Some(b256!(
513                "0x611e1d75cbb77fa782d79485a8384e853bc92e56883c313a51e3f9feef9a9a71"
514            )),
515            ..Default::default()
516        };
517        let mut body = alloy_consensus::BlockBody::<OpTxEnvelope> {
518            transactions: vec![],
519            ommers: vec![],
520            withdrawals: Some(Default::default()),
521        };
522        validate_body_against_header_op(&chainspec, &body, &header).unwrap();
523
524        body.withdrawals.take();
525        validate_body_against_header_op(&chainspec, &body, &header).unwrap_err();
526    }
527
528    #[test]
529    fn test_jovian_blob_gas_used_validation() {
530        const BLOB_GAS_USED: u64 = 1000;
531        const GAS_USED: u64 = 5000;
532
533        let chainspec = jovian_chainspec();
534        let header = Header {
535            timestamp: JOVIAN_TIMESTAMP,
536            blob_gas_used: Some(BLOB_GAS_USED),
537            ..Default::default()
538        };
539
540        let result = BlockExecutionResult::<OpReceipt> {
541            blob_gas_used: BLOB_GAS_USED,
542            receipts: vec![],
543            requests: Requests::default(),
544            gas_used: GAS_USED,
545        };
546        validate_block_post_execution(&header, &chainspec, &result).unwrap();
547    }
548
549    #[test]
550    fn test_jovian_blob_gas_used_validation_mismatched() {
551        const BLOB_GAS_USED: u64 = 1000;
552        const GAS_USED: u64 = 5000;
553
554        let chainspec = jovian_chainspec();
555        let header = Header {
556            timestamp: JOVIAN_TIMESTAMP,
557            blob_gas_used: Some(BLOB_GAS_USED + 1),
558            ..Default::default()
559        };
560
561        let result = BlockExecutionResult::<OpReceipt> {
562            blob_gas_used: BLOB_GAS_USED,
563            receipts: vec![],
564            requests: Requests::default(),
565            gas_used: GAS_USED,
566        };
567        assert_eq!(
568            validate_block_post_execution(&header, &chainspec, &result),
569            Err(ConsensusError::BlobGasUsedDiff(GotExpected {
570                got: BLOB_GAS_USED,
571                expected: BLOB_GAS_USED + 1,
572            }))
573        );
574    }
575}