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