reth_optimism_consensus/
lib.rs

1//! Optimism Consensus implementation.
2
3#![doc(
4    html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
5    html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
6    issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
7)]
8#![cfg_attr(docsrs, feature(doc_cfg))]
9#![cfg_attr(not(feature = "std"), no_std)]
10#![cfg_attr(not(test), warn(unused_crate_dependencies))]
11
12extern crate alloc;
13
14use alloc::{format, sync::Arc};
15use alloy_consensus::{BlockHeader as _, EMPTY_OMMER_ROOT_HASH};
16use alloy_primitives::B64;
17use core::fmt::Debug;
18use reth_chainspec::EthChainSpec;
19use reth_consensus::{Consensus, ConsensusError, FullConsensus, HeaderValidator};
20use reth_consensus_common::validation::{
21    validate_against_parent_eip1559_base_fee, validate_against_parent_hash_number,
22    validate_against_parent_timestamp, validate_cancun_gas, validate_header_base_fee,
23    validate_header_extra_data, validate_header_gas,
24};
25use reth_execution_types::BlockExecutionResult;
26use reth_optimism_forks::OpHardforks;
27use reth_optimism_primitives::DepositReceipt;
28use reth_primitives_traits::{
29    Block, BlockBody, BlockHeader, GotExpected, NodePrimitives, RecoveredBlock, SealedBlock,
30    SealedHeader,
31};
32
33mod proof;
34pub use proof::calculate_receipt_root_no_memo_optimism;
35
36pub mod validation;
37pub use validation::{canyon, isthmus, validate_block_post_execution};
38
39pub mod error;
40pub use error::OpConsensusError;
41
42/// Optimism consensus implementation.
43///
44/// Provides basic checks as outlined in the execution specs.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct OpBeaconConsensus<ChainSpec> {
47    /// Configuration
48    chain_spec: Arc<ChainSpec>,
49}
50
51impl<ChainSpec> OpBeaconConsensus<ChainSpec> {
52    /// Create a new instance of [`OpBeaconConsensus`]
53    pub const fn new(chain_spec: Arc<ChainSpec>) -> Self {
54        Self { chain_spec }
55    }
56}
57
58impl<N, ChainSpec> FullConsensus<N> for OpBeaconConsensus<ChainSpec>
59where
60    N: NodePrimitives<Receipt: DepositReceipt>,
61    ChainSpec: EthChainSpec<Header = N::BlockHeader> + OpHardforks + Debug + Send + Sync,
62{
63    fn validate_block_post_execution(
64        &self,
65        block: &RecoveredBlock<N::Block>,
66        result: &BlockExecutionResult<N::Receipt>,
67    ) -> Result<(), ConsensusError> {
68        validate_block_post_execution(block.header(), &self.chain_spec, result)
69    }
70}
71
72impl<B, ChainSpec> Consensus<B> for OpBeaconConsensus<ChainSpec>
73where
74    B: Block,
75    ChainSpec: EthChainSpec<Header = B::Header> + OpHardforks + Debug + Send + Sync,
76{
77    type Error = ConsensusError;
78
79    fn validate_body_against_header(
80        &self,
81        body: &B::Body,
82        header: &SealedHeader<B::Header>,
83    ) -> Result<(), ConsensusError> {
84        validation::validate_body_against_header_op(&self.chain_spec, body, header.header())
85    }
86
87    fn validate_block_pre_execution(&self, block: &SealedBlock<B>) -> Result<(), ConsensusError> {
88        // Check ommers hash
89        let ommers_hash = block.body().calculate_ommers_root();
90        if Some(block.ommers_hash()) != ommers_hash {
91            return Err(ConsensusError::BodyOmmersHashDiff(
92                GotExpected {
93                    got: ommers_hash.unwrap_or(EMPTY_OMMER_ROOT_HASH),
94                    expected: block.ommers_hash(),
95                }
96                .into(),
97            ))
98        }
99
100        // Check transaction root
101        if let Err(error) = block.ensure_transaction_root_valid() {
102            return Err(ConsensusError::BodyTransactionRootDiff(error.into()))
103        }
104
105        // Check empty shanghai-withdrawals
106        if self.chain_spec.is_canyon_active_at_timestamp(block.timestamp()) {
107            canyon::ensure_empty_shanghai_withdrawals(block.body()).map_err(|err| {
108                ConsensusError::Other(format!("failed to verify block {}: {err}", block.number()))
109            })?
110        } else {
111            return Ok(())
112        }
113
114        // Blob gas used validation
115        // In Jovian, the blob gas used computation has changed. We are moving the blob base fee
116        // validation to post-execution since the DA footprint calculation is stateful.
117        // Pre-execution we only validate that the blob gas used is present in the header.
118        if self.chain_spec.is_jovian_active_at_timestamp(block.timestamp()) {
119            block.blob_gas_used().ok_or(ConsensusError::BlobGasUsedMissing)?;
120        } else if self.chain_spec.is_ecotone_active_at_timestamp(block.timestamp()) {
121            validate_cancun_gas(block)?;
122        }
123
124        // Check withdrawals root field in header
125        if self.chain_spec.is_isthmus_active_at_timestamp(block.timestamp()) {
126            // storage root of withdrawals pre-deploy is verified post-execution
127            isthmus::ensure_withdrawals_storage_root_is_some(block.header()).map_err(|err| {
128                ConsensusError::Other(format!("failed to verify block {}: {err}", block.number()))
129            })?
130        } else {
131            // canyon is active, else would have returned already
132            canyon::ensure_empty_withdrawals_root(block.header())?
133        }
134
135        Ok(())
136    }
137}
138
139impl<H, ChainSpec> HeaderValidator<H> for OpBeaconConsensus<ChainSpec>
140where
141    H: BlockHeader,
142    ChainSpec: EthChainSpec<Header = H> + OpHardforks + Debug + Send + Sync,
143{
144    fn validate_header(&self, header: &SealedHeader<H>) -> Result<(), ConsensusError> {
145        let header = header.header();
146        // with OP-stack Bedrock activation number determines when TTD (eth Merge) has been reached.
147        debug_assert!(
148            self.chain_spec.is_bedrock_active_at_block(header.number()),
149            "manually import OVM blocks"
150        );
151
152        if header.nonce() != Some(B64::ZERO) {
153            return Err(ConsensusError::TheMergeNonceIsNotZero)
154        }
155
156        if header.ommers_hash() != EMPTY_OMMER_ROOT_HASH {
157            return Err(ConsensusError::TheMergeOmmerRootIsNotEmpty)
158        }
159
160        // Post-merge, the consensus layer is expected to perform checks such that the block
161        // timestamp is a function of the slot. This is different from pre-merge, where blocks
162        // are only allowed to be in the future (compared to the system's clock) by a certain
163        // threshold.
164        //
165        // Block validation with respect to the parent should ensure that the block timestamp
166        // is greater than its parent timestamp.
167
168        // validate header extra data for all networks post merge
169        validate_header_extra_data(header)?;
170        validate_header_gas(header)?;
171        validate_header_base_fee(header, &self.chain_spec)
172    }
173
174    fn validate_header_against_parent(
175        &self,
176        header: &SealedHeader<H>,
177        parent: &SealedHeader<H>,
178    ) -> Result<(), ConsensusError> {
179        validate_against_parent_hash_number(header.header(), parent)?;
180
181        if self.chain_spec.is_bedrock_active_at_block(header.number()) {
182            validate_against_parent_timestamp(header.header(), parent.header())?;
183        }
184
185        validate_against_parent_eip1559_base_fee(
186            header.header(),
187            parent.header(),
188            &self.chain_spec,
189        )?;
190
191        // Ensure that the blob gas fields for this block are correctly set.
192        // In the op-stack, the excess blob gas is always 0 for all blocks after ecotone.
193        // The blob gas used and the excess blob gas should both be set after ecotone.
194        // After Jovian, the blob gas used contains the current DA footprint.
195        if self.chain_spec.is_ecotone_active_at_timestamp(header.timestamp()) {
196            let blob_gas_used = header.blob_gas_used().ok_or(ConsensusError::BlobGasUsedMissing)?;
197
198            // Before Jovian and after ecotone, the blob gas used should be 0.
199            if !self.chain_spec.is_jovian_active_at_timestamp(header.timestamp()) &&
200                blob_gas_used != 0
201            {
202                return Err(ConsensusError::BlobGasUsedDiff(GotExpected {
203                    got: blob_gas_used,
204                    expected: 0,
205                }));
206            }
207
208            let excess_blob_gas =
209                header.excess_blob_gas().ok_or(ConsensusError::ExcessBlobGasMissing)?;
210            if excess_blob_gas != 0 {
211                return Err(ConsensusError::ExcessBlobGasDiff {
212                    diff: GotExpected { got: excess_blob_gas, expected: 0 },
213                    parent_excess_blob_gas: parent.excess_blob_gas().unwrap_or(0),
214                    parent_blob_gas_used: parent.blob_gas_used().unwrap_or(0),
215                })
216            }
217        }
218
219        Ok(())
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use std::sync::Arc;
226
227    use alloy_consensus::{BlockBody, Eip658Value, Header, Receipt, TxEip7702, TxReceipt};
228    use alloy_eips::{eip4895::Withdrawals, eip7685::Requests};
229    use alloy_primitives::{Address, Bytes, Signature, U256};
230    use op_alloy_consensus::{
231        encode_holocene_extra_data, encode_jovian_extra_data, OpTypedTransaction,
232    };
233    use reth_chainspec::BaseFeeParams;
234    use reth_consensus::{Consensus, ConsensusError, FullConsensus, HeaderValidator};
235    use reth_optimism_chainspec::{OpChainSpec, OpChainSpecBuilder, OP_MAINNET};
236    use reth_optimism_primitives::{OpPrimitives, OpReceipt, OpTransactionSigned};
237    use reth_primitives_traits::{proofs, GotExpected, RecoveredBlock, SealedBlock, SealedHeader};
238    use reth_provider::BlockExecutionResult;
239
240    use crate::OpBeaconConsensus;
241
242    fn mock_tx(nonce: u64) -> OpTransactionSigned {
243        let tx = TxEip7702 {
244            chain_id: 1u64,
245            nonce,
246            max_fee_per_gas: 0x28f000fff,
247            max_priority_fee_per_gas: 0x28f000fff,
248            gas_limit: 10,
249            to: Address::default(),
250            value: U256::from(3_u64),
251            input: Bytes::from(vec![1, 2]),
252            access_list: Default::default(),
253            authorization_list: Default::default(),
254        };
255
256        let signature = Signature::new(U256::default(), U256::default(), true);
257
258        OpTransactionSigned::new_unhashed(OpTypedTransaction::Eip7702(tx), signature)
259    }
260
261    #[test]
262    fn test_block_blob_gas_used_validation_isthmus() {
263        let chain_spec = OpChainSpecBuilder::default()
264            .isthmus_activated()
265            .genesis(OP_MAINNET.genesis.clone())
266            .chain(OP_MAINNET.chain)
267            .build();
268
269        // create a tx
270        let transaction = mock_tx(0);
271
272        let beacon_consensus = OpBeaconConsensus::new(Arc::new(chain_spec));
273
274        let header = Header {
275            base_fee_per_gas: Some(1337),
276            withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
277            blob_gas_used: Some(0),
278            transactions_root: proofs::calculate_transaction_root(std::slice::from_ref(
279                &transaction,
280            )),
281            timestamp: u64::MAX,
282            ..Default::default()
283        };
284        let body = BlockBody {
285            transactions: vec![transaction],
286            ommers: vec![],
287            withdrawals: Some(Withdrawals::default()),
288        };
289
290        let block = SealedBlock::seal_slow(alloy_consensus::Block { header, body });
291
292        // validate blob, it should pass blob gas used validation
293        let pre_execution = beacon_consensus.validate_block_pre_execution(&block);
294
295        assert!(pre_execution.is_ok());
296    }
297
298    #[test]
299    fn test_block_blob_gas_used_validation_failure_isthmus() {
300        let chain_spec = OpChainSpecBuilder::default()
301            .isthmus_activated()
302            .genesis(OP_MAINNET.genesis.clone())
303            .chain(OP_MAINNET.chain)
304            .build();
305
306        // create a tx
307        let transaction = mock_tx(0);
308
309        let beacon_consensus = OpBeaconConsensus::new(Arc::new(chain_spec));
310
311        let header = Header {
312            base_fee_per_gas: Some(1337),
313            withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
314            blob_gas_used: Some(10),
315            transactions_root: proofs::calculate_transaction_root(std::slice::from_ref(
316                &transaction,
317            )),
318            timestamp: u64::MAX,
319            ..Default::default()
320        };
321        let body = BlockBody {
322            transactions: vec![transaction],
323            ommers: vec![],
324            withdrawals: Some(Withdrawals::default()),
325        };
326
327        let block = SealedBlock::seal_slow(alloy_consensus::Block { header, body });
328
329        // validate blob, it should fail blob gas used validation
330        let pre_execution = beacon_consensus.validate_block_pre_execution(&block);
331
332        assert!(pre_execution.is_err());
333        assert_eq!(
334            pre_execution.unwrap_err(),
335            ConsensusError::BlobGasUsedDiff(GotExpected { got: 10, expected: 0 })
336        );
337    }
338
339    #[test]
340    fn test_block_blob_gas_used_validation_jovian() {
341        const BLOB_GAS_USED: u64 = 1000;
342        const GAS_USED: u64 = 10;
343
344        let chain_spec = OpChainSpecBuilder::default()
345            .jovian_activated()
346            .genesis(OP_MAINNET.genesis.clone())
347            .chain(OP_MAINNET.chain)
348            .build();
349
350        // create a tx
351        let transaction = mock_tx(0);
352
353        let beacon_consensus = OpBeaconConsensus::new(Arc::new(chain_spec));
354
355        let receipt = OpReceipt::Eip7702(Receipt {
356            status: Eip658Value::success(),
357            cumulative_gas_used: GAS_USED,
358            logs: vec![],
359        });
360
361        let header = Header {
362            base_fee_per_gas: Some(1337),
363            withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
364            blob_gas_used: Some(BLOB_GAS_USED),
365            transactions_root: proofs::calculate_transaction_root(std::slice::from_ref(
366                &transaction,
367            )),
368            timestamp: u64::MAX,
369            gas_used: GAS_USED,
370            receipts_root: proofs::calculate_receipt_root(std::slice::from_ref(
371                &receipt.with_bloom_ref(),
372            )),
373            logs_bloom: receipt.bloom(),
374            ..Default::default()
375        };
376        let body = BlockBody {
377            transactions: vec![transaction],
378            ommers: vec![],
379            withdrawals: Some(Withdrawals::default()),
380        };
381
382        let block = SealedBlock::seal_slow(alloy_consensus::Block { header, body });
383
384        let result = BlockExecutionResult::<OpReceipt> {
385            blob_gas_used: BLOB_GAS_USED,
386            receipts: vec![receipt],
387            requests: Requests::default(),
388            gas_used: GAS_USED,
389        };
390
391        // validate blob, it should pass blob gas used validation
392        let pre_execution = beacon_consensus.validate_block_pre_execution(&block);
393
394        assert!(pre_execution.is_ok());
395
396        let block = RecoveredBlock::new_sealed(block, vec![Address::default()]);
397
398        let post_execution = <OpBeaconConsensus<OpChainSpec> as FullConsensus<OpPrimitives>>::validate_block_post_execution(
399            &beacon_consensus,
400            &block,
401            &result
402        );
403
404        // validate blob, it should pass blob gas used validation
405        assert!(post_execution.is_ok());
406    }
407
408    #[test]
409    fn test_block_blob_gas_used_validation_failure_jovian() {
410        const BLOB_GAS_USED: u64 = 1000;
411        const GAS_USED: u64 = 10;
412
413        let chain_spec = OpChainSpecBuilder::default()
414            .jovian_activated()
415            .genesis(OP_MAINNET.genesis.clone())
416            .chain(OP_MAINNET.chain)
417            .build();
418
419        // create a tx
420        let transaction = mock_tx(0);
421
422        let beacon_consensus = OpBeaconConsensus::new(Arc::new(chain_spec));
423
424        let receipt = OpReceipt::Eip7702(Receipt {
425            status: Eip658Value::success(),
426            cumulative_gas_used: GAS_USED,
427            logs: vec![],
428        });
429
430        let header = Header {
431            base_fee_per_gas: Some(1337),
432            withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
433            blob_gas_used: Some(BLOB_GAS_USED),
434            transactions_root: proofs::calculate_transaction_root(std::slice::from_ref(
435                &transaction,
436            )),
437            gas_used: GAS_USED,
438            timestamp: u64::MAX,
439            receipts_root: proofs::calculate_receipt_root(std::slice::from_ref(&receipt)),
440            logs_bloom: receipt.bloom(),
441            ..Default::default()
442        };
443        let body = BlockBody {
444            transactions: vec![transaction],
445            ommers: vec![],
446            withdrawals: Some(Withdrawals::default()),
447        };
448
449        let block = SealedBlock::seal_slow(alloy_consensus::Block { header, body });
450
451        let result = BlockExecutionResult::<OpReceipt> {
452            blob_gas_used: BLOB_GAS_USED + 1,
453            receipts: vec![receipt],
454            requests: Requests::default(),
455            gas_used: GAS_USED,
456        };
457
458        // validate blob, it should pass blob gas used validation
459        let pre_execution = beacon_consensus.validate_block_pre_execution(&block);
460
461        assert!(pre_execution.is_ok());
462
463        let block = RecoveredBlock::new_sealed(block, vec![Address::default()]);
464
465        let post_execution = <OpBeaconConsensus<OpChainSpec> as FullConsensus<OpPrimitives>>::validate_block_post_execution(
466            &beacon_consensus,
467            &block,
468            &result
469        );
470
471        // validate blob, it should fail blob gas used validation post execution.
472        assert!(post_execution.is_err());
473        assert_eq!(
474            post_execution.unwrap_err(),
475            ConsensusError::BlobGasUsedDiff(GotExpected {
476                got: BLOB_GAS_USED + 1,
477                expected: BLOB_GAS_USED,
478            })
479        );
480    }
481
482    #[test]
483    fn test_header_min_base_fee_validation() {
484        const MIN_BASE_FEE: u64 = 1000;
485
486        let chain_spec = OpChainSpecBuilder::default()
487            .jovian_activated()
488            .genesis(OP_MAINNET.genesis.clone())
489            .chain(OP_MAINNET.chain)
490            .build();
491
492        // create a tx
493        let transaction = mock_tx(0);
494
495        let beacon_consensus = OpBeaconConsensus::new(Arc::new(chain_spec));
496
497        let receipt = OpReceipt::Eip7702(Receipt {
498            status: Eip658Value::success(),
499            cumulative_gas_used: 0,
500            logs: vec![],
501        });
502
503        let parent = Header {
504            number: 0,
505            base_fee_per_gas: Some(MIN_BASE_FEE / 10),
506            withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
507            blob_gas_used: Some(0),
508            excess_blob_gas: Some(0),
509            transactions_root: proofs::calculate_transaction_root(std::slice::from_ref(
510                &transaction,
511            )),
512            gas_used: 0,
513            timestamp: u64::MAX - 1,
514            receipts_root: proofs::calculate_receipt_root(std::slice::from_ref(&receipt)),
515            logs_bloom: receipt.bloom(),
516            extra_data: encode_jovian_extra_data(
517                Default::default(),
518                BaseFeeParams::optimism(),
519                MIN_BASE_FEE,
520            )
521            .unwrap(),
522            ..Default::default()
523        };
524        let parent = SealedHeader::seal_slow(parent);
525
526        let header = Header {
527            number: 1,
528            base_fee_per_gas: Some(MIN_BASE_FEE),
529            withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
530            blob_gas_used: Some(0),
531            excess_blob_gas: Some(0),
532            transactions_root: proofs::calculate_transaction_root(std::slice::from_ref(
533                &transaction,
534            )),
535            gas_used: 0,
536            timestamp: u64::MAX,
537            receipts_root: proofs::calculate_receipt_root(std::slice::from_ref(&receipt)),
538            logs_bloom: receipt.bloom(),
539            parent_hash: parent.hash(),
540            ..Default::default()
541        };
542        let header = SealedHeader::seal_slow(header);
543
544        let result = beacon_consensus.validate_header_against_parent(&header, &parent);
545
546        assert!(result.is_ok());
547    }
548
549    #[test]
550    fn test_header_min_base_fee_validation_failure() {
551        const MIN_BASE_FEE: u64 = 1000;
552
553        let chain_spec = OpChainSpecBuilder::default()
554            .jovian_activated()
555            .genesis(OP_MAINNET.genesis.clone())
556            .chain(OP_MAINNET.chain)
557            .build();
558
559        // create a tx
560        let transaction = mock_tx(0);
561
562        let beacon_consensus = OpBeaconConsensus::new(Arc::new(chain_spec));
563
564        let receipt = OpReceipt::Eip7702(Receipt {
565            status: Eip658Value::success(),
566            cumulative_gas_used: 0,
567            logs: vec![],
568        });
569
570        let parent = Header {
571            number: 0,
572            base_fee_per_gas: Some(MIN_BASE_FEE / 10),
573            withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
574            blob_gas_used: Some(0),
575            excess_blob_gas: Some(0),
576            transactions_root: proofs::calculate_transaction_root(std::slice::from_ref(
577                &transaction,
578            )),
579            gas_used: 0,
580            timestamp: u64::MAX - 1,
581            receipts_root: proofs::calculate_receipt_root(std::slice::from_ref(&receipt)),
582            logs_bloom: receipt.bloom(),
583            extra_data: encode_jovian_extra_data(
584                Default::default(),
585                BaseFeeParams::optimism(),
586                MIN_BASE_FEE,
587            )
588            .unwrap(),
589            ..Default::default()
590        };
591        let parent = SealedHeader::seal_slow(parent);
592
593        let header = Header {
594            number: 1,
595            base_fee_per_gas: Some(MIN_BASE_FEE - 1),
596            withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
597            blob_gas_used: Some(0),
598            excess_blob_gas: Some(0),
599            transactions_root: proofs::calculate_transaction_root(std::slice::from_ref(
600                &transaction,
601            )),
602            gas_used: 0,
603            timestamp: u64::MAX,
604            receipts_root: proofs::calculate_receipt_root(std::slice::from_ref(&receipt)),
605            logs_bloom: receipt.bloom(),
606            parent_hash: parent.hash(),
607            ..Default::default()
608        };
609        let header = SealedHeader::seal_slow(header);
610
611        let result = beacon_consensus.validate_header_against_parent(&header, &parent);
612
613        assert!(result.is_err());
614        assert_eq!(
615            result.unwrap_err(),
616            ConsensusError::BaseFeeDiff(GotExpected {
617                got: MIN_BASE_FEE - 1,
618                expected: MIN_BASE_FEE,
619            })
620        );
621    }
622
623    #[test]
624    fn test_header_da_footprint_validation() {
625        const MIN_BASE_FEE: u64 = 100_000;
626        const DA_FOOTPRINT: u64 = GAS_LIMIT - 1;
627        const GAS_LIMIT: u64 = 100_000_000;
628
629        let chain_spec = OpChainSpecBuilder::default()
630            .jovian_activated()
631            .genesis(OP_MAINNET.genesis.clone())
632            .chain(OP_MAINNET.chain)
633            .build();
634
635        // create a tx
636        let transaction = mock_tx(0);
637
638        let beacon_consensus = OpBeaconConsensus::new(Arc::new(chain_spec));
639
640        let receipt = OpReceipt::Eip7702(Receipt {
641            status: Eip658Value::success(),
642            cumulative_gas_used: 0,
643            logs: vec![],
644        });
645
646        let parent = Header {
647            number: 0,
648            base_fee_per_gas: Some(MIN_BASE_FEE),
649            withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
650            blob_gas_used: Some(DA_FOOTPRINT),
651            excess_blob_gas: Some(0),
652            transactions_root: proofs::calculate_transaction_root(std::slice::from_ref(
653                &transaction,
654            )),
655            gas_used: 0,
656            timestamp: u64::MAX - 1,
657            receipts_root: proofs::calculate_receipt_root(std::slice::from_ref(&receipt)),
658            logs_bloom: receipt.bloom(),
659            extra_data: encode_jovian_extra_data(
660                Default::default(),
661                BaseFeeParams::optimism(),
662                MIN_BASE_FEE,
663            )
664            .unwrap(),
665            gas_limit: GAS_LIMIT,
666            ..Default::default()
667        };
668        let parent = SealedHeader::seal_slow(parent);
669
670        let header = Header {
671            number: 1,
672            base_fee_per_gas: Some(MIN_BASE_FEE + MIN_BASE_FEE / 10),
673            withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
674            blob_gas_used: Some(DA_FOOTPRINT),
675            excess_blob_gas: Some(0),
676            transactions_root: proofs::calculate_transaction_root(std::slice::from_ref(
677                &transaction,
678            )),
679            gas_used: 0,
680            timestamp: u64::MAX,
681            receipts_root: proofs::calculate_receipt_root(std::slice::from_ref(&receipt)),
682            logs_bloom: receipt.bloom(),
683            parent_hash: parent.hash(),
684            ..Default::default()
685        };
686        let header = SealedHeader::seal_slow(header);
687
688        let result = beacon_consensus.validate_header_against_parent(&header, &parent);
689
690        assert!(result.is_ok());
691    }
692
693    #[test]
694    fn test_header_isthmus_validation() {
695        const MIN_BASE_FEE: u64 = 100_000;
696        const DA_FOOTPRINT: u64 = GAS_LIMIT - 1;
697        const GAS_LIMIT: u64 = 100_000_000;
698
699        let chain_spec = OpChainSpecBuilder::default()
700            .isthmus_activated()
701            .genesis(OP_MAINNET.genesis.clone())
702            .chain(OP_MAINNET.chain)
703            .build();
704
705        // create a tx
706        let transaction = mock_tx(0);
707
708        let beacon_consensus = OpBeaconConsensus::new(Arc::new(chain_spec));
709
710        let receipt = OpReceipt::Eip7702(Receipt {
711            status: Eip658Value::success(),
712            cumulative_gas_used: 0,
713            logs: vec![],
714        });
715
716        let parent = Header {
717            number: 0,
718            base_fee_per_gas: Some(MIN_BASE_FEE),
719            withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
720            blob_gas_used: Some(DA_FOOTPRINT),
721            excess_blob_gas: Some(0),
722            transactions_root: proofs::calculate_transaction_root(std::slice::from_ref(
723                &transaction,
724            )),
725            gas_used: 0,
726            timestamp: u64::MAX - 1,
727            receipts_root: proofs::calculate_receipt_root(std::slice::from_ref(&receipt)),
728            logs_bloom: receipt.bloom(),
729            extra_data: encode_holocene_extra_data(Default::default(), BaseFeeParams::optimism())
730                .unwrap(),
731            gas_limit: GAS_LIMIT,
732            ..Default::default()
733        };
734        let parent = SealedHeader::seal_slow(parent);
735
736        let header = Header {
737            number: 1,
738            base_fee_per_gas: Some(MIN_BASE_FEE - 2 * MIN_BASE_FEE / 100),
739            withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
740            blob_gas_used: Some(DA_FOOTPRINT),
741            excess_blob_gas: Some(0),
742            transactions_root: proofs::calculate_transaction_root(std::slice::from_ref(
743                &transaction,
744            )),
745            gas_used: 0,
746            timestamp: u64::MAX,
747            receipts_root: proofs::calculate_receipt_root(std::slice::from_ref(&receipt)),
748            logs_bloom: receipt.bloom(),
749            parent_hash: parent.hash(),
750            ..Default::default()
751        };
752        let header = SealedHeader::seal_slow(header);
753
754        let result = beacon_consensus.validate_header_against_parent(&header, &parent);
755
756        assert!(result.is_err());
757        assert_eq!(
758            result.unwrap_err(),
759            ConsensusError::BlobGasUsedDiff(GotExpected { got: DA_FOOTPRINT, expected: 0 })
760        );
761    }
762}