1#![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#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct OpBeaconConsensus<ChainSpec> {
49 chain_spec: Arc<ChainSpec>,
51 max_extra_data_size: usize,
53}
54
55impl<ChainSpec> OpBeaconConsensus<ChainSpec> {
56 pub const fn new(chain_spec: Arc<ChainSpec>) -> Self {
58 Self { chain_spec, max_extra_data_size: MAXIMUM_EXTRA_DATA_SIZE }
59 }
60
61 pub const fn max_extra_data_size(&self) -> usize {
63 self.max_extra_data_size
64 }
65
66 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 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 if let Err(error) = block.ensure_transaction_root_valid() {
117 return Err(ConsensusError::BodyTransactionRootDiff(error.into()))
118 }
119
120 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 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 if self.chain_spec.is_isthmus_active_at_timestamp(block.timestamp()) {
141 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::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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}