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, 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#[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 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 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 if let Err(error) = block.ensure_transaction_root_valid() {
116 return Err(ConsensusError::BodyTransactionRootDiff(error.into()))
117 }
118
119 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 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 if self.chain_spec.is_isthmus_active_at_timestamp(block.timestamp()) {
140 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::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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}