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