reth_optimism_chainspec/
lib.rs

1//! OP-Reth chain specs.
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, doc_auto_cfg))]
9#![cfg_attr(not(feature = "std"), no_std)]
10
11extern crate alloc;
12
13mod base;
14mod base_sepolia;
15pub mod constants;
16mod dev;
17mod op;
18mod op_sepolia;
19
20use alloc::{boxed::Box, vec, vec::Vec};
21use alloy_chains::Chain;
22use alloy_consensus::{proofs::storage_root_unhashed, Header};
23use alloy_eips::eip7840::BlobParams;
24use alloy_genesis::Genesis;
25use alloy_primitives::{B256, U256};
26pub use base::BASE_MAINNET;
27pub use base_sepolia::BASE_SEPOLIA;
28use derive_more::{Constructor, Deref, From, Into};
29pub use dev::OP_DEV;
30pub use op::OP_MAINNET;
31pub use op_sepolia::OP_SEPOLIA;
32use reth_chainspec::{
33    BaseFeeParams, BaseFeeParamsKind, ChainSpec, ChainSpecBuilder, DepositContract, EthChainSpec,
34    EthereumHardforks, ForkFilter, ForkId, Hardforks, Head,
35};
36use reth_ethereum_forks::{ChainHardforks, EthereumHardfork, ForkCondition, Hardfork};
37use reth_network_peers::NodeRecord;
38use reth_optimism_forks::{OpHardfork, OpHardforks, OP_MAINNET_HARDFORKS};
39use reth_optimism_primitives::ADDRESS_L2_TO_L1_MESSAGE_PASSER;
40use reth_primitives_traits::{sync::LazyLock, SealedHeader};
41
42/// Chain spec builder for a OP stack chain.
43#[derive(Debug, Default, From)]
44pub struct OpChainSpecBuilder {
45    /// [`ChainSpecBuilder`]
46    inner: ChainSpecBuilder,
47}
48
49impl OpChainSpecBuilder {
50    /// Construct a new builder from the base mainnet chain spec.
51    pub fn base_mainnet() -> Self {
52        let mut inner = ChainSpecBuilder::default()
53            .chain(BASE_MAINNET.chain)
54            .genesis(BASE_MAINNET.genesis.clone());
55        let forks = BASE_MAINNET.hardforks.clone();
56        inner = inner.with_forks(forks);
57
58        Self { inner }
59    }
60
61    /// Construct a new builder from the optimism mainnet chain spec.
62    pub fn optimism_mainnet() -> Self {
63        let mut inner =
64            ChainSpecBuilder::default().chain(OP_MAINNET.chain).genesis(OP_MAINNET.genesis.clone());
65        let forks = OP_MAINNET.hardforks.clone();
66        inner = inner.with_forks(forks);
67
68        Self { inner }
69    }
70}
71
72impl OpChainSpecBuilder {
73    /// Set the chain ID
74    pub fn chain(mut self, chain: Chain) -> Self {
75        self.inner = self.inner.chain(chain);
76        self
77    }
78
79    /// Set the genesis block.
80    pub fn genesis(mut self, genesis: Genesis) -> Self {
81        self.inner = self.inner.genesis(genesis);
82        self
83    }
84
85    /// Add the given fork with the given activation condition to the spec.
86    pub fn with_fork<H: Hardfork>(mut self, fork: H, condition: ForkCondition) -> Self {
87        self.inner = self.inner.with_fork(fork, condition);
88        self
89    }
90
91    /// Add the given forks with the given activation condition to the spec.
92    pub fn with_forks(mut self, forks: ChainHardforks) -> Self {
93        self.inner = self.inner.with_forks(forks);
94        self
95    }
96
97    /// Remove the given fork from the spec.
98    pub fn without_fork(mut self, fork: OpHardfork) -> Self {
99        self.inner = self.inner.without_fork(fork);
100        self
101    }
102
103    /// Enable Bedrock at genesis
104    pub fn bedrock_activated(mut self) -> Self {
105        self.inner = self.inner.paris_activated();
106        self.inner = self.inner.with_fork(OpHardfork::Bedrock, ForkCondition::Block(0));
107        self
108    }
109
110    /// Enable Regolith at genesis
111    pub fn regolith_activated(mut self) -> Self {
112        self = self.bedrock_activated();
113        self.inner = self.inner.with_fork(OpHardfork::Regolith, ForkCondition::Timestamp(0));
114        self
115    }
116
117    /// Enable Canyon at genesis
118    pub fn canyon_activated(mut self) -> Self {
119        self = self.regolith_activated();
120        // Canyon also activates changes from L1's Shanghai hardfork
121        self.inner = self.inner.with_fork(EthereumHardfork::Shanghai, ForkCondition::Timestamp(0));
122        self.inner = self.inner.with_fork(OpHardfork::Canyon, ForkCondition::Timestamp(0));
123        self
124    }
125
126    /// Enable Ecotone at genesis
127    pub fn ecotone_activated(mut self) -> Self {
128        self = self.canyon_activated();
129        self.inner = self.inner.with_fork(EthereumHardfork::Cancun, ForkCondition::Timestamp(0));
130        self.inner = self.inner.with_fork(OpHardfork::Ecotone, ForkCondition::Timestamp(0));
131        self
132    }
133
134    /// Enable Fjord at genesis
135    pub fn fjord_activated(mut self) -> Self {
136        self = self.ecotone_activated();
137        self.inner = self.inner.with_fork(OpHardfork::Fjord, ForkCondition::Timestamp(0));
138        self
139    }
140
141    /// Enable Granite at genesis
142    pub fn granite_activated(mut self) -> Self {
143        self = self.fjord_activated();
144        self.inner = self.inner.with_fork(OpHardfork::Granite, ForkCondition::Timestamp(0));
145        self
146    }
147
148    /// Enable Holocene at genesis
149    pub fn holocene_activated(mut self) -> Self {
150        self = self.granite_activated();
151        self.inner = self.inner.with_fork(OpHardfork::Holocene, ForkCondition::Timestamp(0));
152        self
153    }
154
155    /// Enable Isthmus at genesis
156    pub fn isthmus_activated(mut self) -> Self {
157        self = self.holocene_activated();
158        self.inner = self.inner.with_fork(OpHardfork::Isthmus, ForkCondition::Timestamp(0));
159        self
160    }
161
162    /// Enable Interop at genesis
163    pub fn interop_activated(mut self) -> Self {
164        self = self.isthmus_activated();
165        self.inner = self.inner.with_fork(OpHardfork::Interop, ForkCondition::Timestamp(0));
166        self
167    }
168
169    /// Build the resulting [`OpChainSpec`].
170    ///
171    /// # Panics
172    ///
173    /// This function panics if the chain ID and genesis is not set ([`Self::chain`] and
174    /// [`Self::genesis`])
175    pub fn build(self) -> OpChainSpec {
176        let mut inner = self.inner.build();
177        inner.genesis_header =
178            SealedHeader::seal_slow(make_op_genesis_header(&inner.genesis, &inner.hardforks));
179
180        OpChainSpec { inner }
181    }
182}
183
184/// OP stack chain spec type.
185#[derive(Debug, Clone, Deref, Into, Constructor, PartialEq, Eq)]
186pub struct OpChainSpec {
187    /// [`ChainSpec`].
188    pub inner: ChainSpec,
189}
190
191impl OpChainSpec {
192    /// Converts the given [`Genesis`] into a [`OpChainSpec`].
193    pub fn from_genesis(genesis: Genesis) -> Self {
194        genesis.into()
195    }
196}
197
198impl EthChainSpec for OpChainSpec {
199    type Header = Header;
200
201    fn chain(&self) -> Chain {
202        self.inner.chain()
203    }
204
205    fn base_fee_params_at_block(&self, block_number: u64) -> BaseFeeParams {
206        self.inner.base_fee_params_at_block(block_number)
207    }
208
209    fn base_fee_params_at_timestamp(&self, timestamp: u64) -> BaseFeeParams {
210        self.inner.base_fee_params_at_timestamp(timestamp)
211    }
212
213    fn blob_params_at_timestamp(&self, timestamp: u64) -> Option<BlobParams> {
214        self.inner.blob_params_at_timestamp(timestamp)
215    }
216
217    fn deposit_contract(&self) -> Option<&DepositContract> {
218        self.inner.deposit_contract()
219    }
220
221    fn genesis_hash(&self) -> B256 {
222        self.inner.genesis_hash()
223    }
224
225    fn prune_delete_limit(&self) -> usize {
226        self.inner.prune_delete_limit()
227    }
228
229    fn display_hardforks(&self) -> Box<dyn core::fmt::Display> {
230        Box::new(ChainSpec::display_hardforks(self))
231    }
232
233    fn genesis_header(&self) -> &Self::Header {
234        self.inner.genesis_header()
235    }
236
237    fn genesis(&self) -> &Genesis {
238        self.inner.genesis()
239    }
240
241    fn bootnodes(&self) -> Option<Vec<NodeRecord>> {
242        self.inner.bootnodes()
243    }
244
245    fn is_optimism(&self) -> bool {
246        true
247    }
248
249    fn final_paris_total_difficulty(&self) -> Option<U256> {
250        self.inner.final_paris_total_difficulty()
251    }
252}
253
254impl Hardforks for OpChainSpec {
255    fn fork<H: Hardfork>(&self, fork: H) -> ForkCondition {
256        self.inner.fork(fork)
257    }
258
259    fn forks_iter(&self) -> impl Iterator<Item = (&dyn Hardfork, ForkCondition)> {
260        self.inner.forks_iter()
261    }
262
263    fn fork_id(&self, head: &Head) -> ForkId {
264        self.inner.fork_id(head)
265    }
266
267    fn latest_fork_id(&self) -> ForkId {
268        self.inner.latest_fork_id()
269    }
270
271    fn fork_filter(&self, head: Head) -> ForkFilter {
272        self.inner.fork_filter(head)
273    }
274}
275
276impl EthereumHardforks for OpChainSpec {
277    fn ethereum_fork_activation(&self, fork: EthereumHardfork) -> ForkCondition {
278        self.fork(fork)
279    }
280}
281
282impl OpHardforks for OpChainSpec {
283    fn op_fork_activation(&self, fork: OpHardfork) -> ForkCondition {
284        self.fork(fork)
285    }
286}
287
288impl From<Genesis> for OpChainSpec {
289    fn from(genesis: Genesis) -> Self {
290        use reth_optimism_forks::OpHardfork;
291        let optimism_genesis_info = OpGenesisInfo::extract_from(&genesis);
292        let genesis_info =
293            optimism_genesis_info.optimism_chain_info.genesis_info.unwrap_or_default();
294
295        // Block-based hardforks
296        let hardfork_opts = [
297            (EthereumHardfork::Frontier.boxed(), Some(0)),
298            (EthereumHardfork::Homestead.boxed(), genesis.config.homestead_block),
299            (EthereumHardfork::Tangerine.boxed(), genesis.config.eip150_block),
300            (EthereumHardfork::SpuriousDragon.boxed(), genesis.config.eip155_block),
301            (EthereumHardfork::Byzantium.boxed(), genesis.config.byzantium_block),
302            (EthereumHardfork::Constantinople.boxed(), genesis.config.constantinople_block),
303            (EthereumHardfork::Petersburg.boxed(), genesis.config.petersburg_block),
304            (EthereumHardfork::Istanbul.boxed(), genesis.config.istanbul_block),
305            (EthereumHardfork::MuirGlacier.boxed(), genesis.config.muir_glacier_block),
306            (EthereumHardfork::Berlin.boxed(), genesis.config.berlin_block),
307            (EthereumHardfork::London.boxed(), genesis.config.london_block),
308            (EthereumHardfork::ArrowGlacier.boxed(), genesis.config.arrow_glacier_block),
309            (EthereumHardfork::GrayGlacier.boxed(), genesis.config.gray_glacier_block),
310            (OpHardfork::Bedrock.boxed(), genesis_info.bedrock_block),
311        ];
312        let mut block_hardforks = hardfork_opts
313            .into_iter()
314            .filter_map(|(hardfork, opt)| opt.map(|block| (hardfork, ForkCondition::Block(block))))
315            .collect::<Vec<_>>();
316
317        // We set the paris hardfork for OP networks to zero
318        block_hardforks.push((
319            EthereumHardfork::Paris.boxed(),
320            ForkCondition::TTD {
321                activation_block_number: 0,
322                total_difficulty: U256::ZERO,
323                fork_block: genesis.config.merge_netsplit_block,
324            },
325        ));
326
327        // Time-based hardforks
328        let time_hardfork_opts = [
329            (EthereumHardfork::Shanghai.boxed(), genesis.config.shanghai_time),
330            (EthereumHardfork::Cancun.boxed(), genesis.config.cancun_time),
331            (EthereumHardfork::Prague.boxed(), genesis.config.prague_time),
332            (OpHardfork::Regolith.boxed(), genesis_info.regolith_time),
333            (OpHardfork::Canyon.boxed(), genesis_info.canyon_time),
334            (OpHardfork::Ecotone.boxed(), genesis_info.ecotone_time),
335            (OpHardfork::Fjord.boxed(), genesis_info.fjord_time),
336            (OpHardfork::Granite.boxed(), genesis_info.granite_time),
337            (OpHardfork::Holocene.boxed(), genesis_info.holocene_time),
338            (OpHardfork::Isthmus.boxed(), genesis_info.isthmus_time),
339            (OpHardfork::Interop.boxed(), genesis_info.interop_time),
340        ];
341
342        let mut time_hardforks = time_hardfork_opts
343            .into_iter()
344            .filter_map(|(hardfork, opt)| {
345                opt.map(|time| (hardfork, ForkCondition::Timestamp(time)))
346            })
347            .collect::<Vec<_>>();
348
349        block_hardforks.append(&mut time_hardforks);
350
351        // Ordered Hardforks
352        let mainnet_hardforks = OP_MAINNET_HARDFORKS.clone();
353        let mainnet_order = mainnet_hardforks.forks_iter();
354
355        let mut ordered_hardforks = Vec::with_capacity(block_hardforks.len());
356        for (hardfork, _) in mainnet_order {
357            if let Some(pos) = block_hardforks.iter().position(|(e, _)| **e == *hardfork) {
358                ordered_hardforks.push(block_hardforks.remove(pos));
359            }
360        }
361
362        // append the remaining unknown hardforks to ensure we don't filter any out
363        ordered_hardforks.append(&mut block_hardforks);
364
365        let hardforks = ChainHardforks::new(ordered_hardforks);
366        let genesis_header = SealedHeader::seal_slow(make_op_genesis_header(&genesis, &hardforks));
367
368        Self {
369            inner: ChainSpec {
370                chain: genesis.config.chain_id.into(),
371                genesis_header,
372                genesis,
373                hardforks,
374                // We assume no OP network merges, and set the paris block and total difficulty to
375                // zero
376                paris_block_and_final_difficulty: Some((0, U256::ZERO)),
377                base_fee_params: optimism_genesis_info.base_fee_params,
378                ..Default::default()
379            },
380        }
381    }
382}
383
384impl From<ChainSpec> for OpChainSpec {
385    fn from(value: ChainSpec) -> Self {
386        Self { inner: value }
387    }
388}
389
390#[derive(Default, Debug)]
391struct OpGenesisInfo {
392    optimism_chain_info: op_alloy_rpc_types::OpChainInfo,
393    base_fee_params: BaseFeeParamsKind,
394}
395
396impl OpGenesisInfo {
397    fn extract_from(genesis: &Genesis) -> Self {
398        let mut info = Self {
399            optimism_chain_info: op_alloy_rpc_types::OpChainInfo::extract_from(
400                &genesis.config.extra_fields,
401            )
402            .unwrap_or_default(),
403            ..Default::default()
404        };
405        if let Some(optimism_base_fee_info) = &info.optimism_chain_info.base_fee_info {
406            if let (Some(elasticity), Some(denominator)) = (
407                optimism_base_fee_info.eip1559_elasticity,
408                optimism_base_fee_info.eip1559_denominator,
409            ) {
410                let base_fee_params = if let Some(canyon_denominator) =
411                    optimism_base_fee_info.eip1559_denominator_canyon
412                {
413                    BaseFeeParamsKind::Variable(
414                        vec![
415                            (
416                                EthereumHardfork::London.boxed(),
417                                BaseFeeParams::new(denominator as u128, elasticity as u128),
418                            ),
419                            (
420                                OpHardfork::Canyon.boxed(),
421                                BaseFeeParams::new(canyon_denominator as u128, elasticity as u128),
422                            ),
423                        ]
424                        .into(),
425                    )
426                } else {
427                    BaseFeeParams::new(denominator as u128, elasticity as u128).into()
428                };
429
430                info.base_fee_params = base_fee_params;
431            }
432        }
433
434        info
435    }
436}
437
438/// Helper method building a [`Header`] given [`Genesis`] and [`ChainHardforks`].
439pub fn make_op_genesis_header(genesis: &Genesis, hardforks: &ChainHardforks) -> Header {
440    let mut header = reth_chainspec::make_genesis_header(genesis, hardforks);
441
442    // If Isthmus is active, overwrite the withdrawals root with the storage root of predeploy
443    // `L2ToL1MessagePasser.sol`
444    if hardforks.fork(OpHardfork::Isthmus).active_at_timestamp(header.timestamp) {
445        if let Some(predeploy) = genesis.alloc.get(&ADDRESS_L2_TO_L1_MESSAGE_PASSER) {
446            if let Some(storage) = &predeploy.storage {
447                header.withdrawals_root =
448                    Some(storage_root_unhashed(storage.iter().map(|(k, v)| (*k, (*v).into()))))
449            }
450        }
451    }
452
453    header
454}
455
456#[cfg(test)]
457mod tests {
458    use alloy_genesis::{ChainConfig, Genesis};
459    use alloy_primitives::b256;
460    use reth_chainspec::{test_fork_ids, BaseFeeParams, BaseFeeParamsKind};
461    use reth_ethereum_forks::{EthereumHardfork, ForkCondition, ForkHash, ForkId, Head};
462    use reth_optimism_forks::{OpHardfork, OpHardforks};
463
464    use crate::*;
465
466    #[test]
467    fn base_mainnet_forkids() {
468        let mut base_mainnet = OpChainSpecBuilder::base_mainnet().build();
469        base_mainnet.inner.genesis_header.set_hash(BASE_MAINNET.genesis_hash());
470        test_fork_ids(
471            &BASE_MAINNET,
472            &[
473                (
474                    Head { number: 0, ..Default::default() },
475                    ForkId { hash: ForkHash([0x67, 0xda, 0x02, 0x60]), next: 1704992401 },
476                ),
477                (
478                    Head { number: 0, timestamp: 1704992400, ..Default::default() },
479                    ForkId { hash: ForkHash([0x67, 0xda, 0x02, 0x60]), next: 1704992401 },
480                ),
481                (
482                    Head { number: 0, timestamp: 1704992401, ..Default::default() },
483                    ForkId { hash: ForkHash([0x3c, 0x28, 0x3c, 0xb3]), next: 1710374401 },
484                ),
485                (
486                    Head { number: 0, timestamp: 1710374400, ..Default::default() },
487                    ForkId { hash: ForkHash([0x3c, 0x28, 0x3c, 0xb3]), next: 1710374401 },
488                ),
489                (
490                    Head { number: 0, timestamp: 1710374401, ..Default::default() },
491                    ForkId { hash: ForkHash([0x51, 0xcc, 0x98, 0xb3]), next: 1720627201 },
492                ),
493                (
494                    Head { number: 0, timestamp: 1720627200, ..Default::default() },
495                    ForkId { hash: ForkHash([0x51, 0xcc, 0x98, 0xb3]), next: 1720627201 },
496                ),
497                (
498                    Head { number: 0, timestamp: 1720627201, ..Default::default() },
499                    ForkId { hash: ForkHash([0xe4, 0x01, 0x0e, 0xb9]), next: 1726070401 },
500                ),
501                (
502                    Head { number: 0, timestamp: 1726070401, ..Default::default() },
503                    ForkId { hash: ForkHash([0xbc, 0x38, 0xf9, 0xca]), next: 1736445601 },
504                ),
505                (
506                    Head { number: 0, timestamp: 1736445601, ..Default::default() },
507                    ForkId { hash: ForkHash([0x3a, 0x2a, 0xf1, 0x83]), next: 0 },
508                ),
509            ],
510        );
511    }
512
513    #[test]
514    fn op_sepolia_forkids() {
515        test_fork_ids(
516            &OP_SEPOLIA,
517            &[
518                (
519                    Head { number: 0, ..Default::default() },
520                    ForkId { hash: ForkHash([0x67, 0xa4, 0x03, 0x28]), next: 1699981200 },
521                ),
522                (
523                    Head { number: 0, timestamp: 1699981199, ..Default::default() },
524                    ForkId { hash: ForkHash([0x67, 0xa4, 0x03, 0x28]), next: 1699981200 },
525                ),
526                (
527                    Head { number: 0, timestamp: 1699981200, ..Default::default() },
528                    ForkId { hash: ForkHash([0xa4, 0x8d, 0x6a, 0x00]), next: 1708534800 },
529                ),
530                (
531                    Head { number: 0, timestamp: 1708534799, ..Default::default() },
532                    ForkId { hash: ForkHash([0xa4, 0x8d, 0x6a, 0x00]), next: 1708534800 },
533                ),
534                (
535                    Head { number: 0, timestamp: 1708534800, ..Default::default() },
536                    ForkId { hash: ForkHash([0xcc, 0x17, 0xc7, 0xeb]), next: 1716998400 },
537                ),
538                (
539                    Head { number: 0, timestamp: 1716998399, ..Default::default() },
540                    ForkId { hash: ForkHash([0xcc, 0x17, 0xc7, 0xeb]), next: 1716998400 },
541                ),
542                (
543                    Head { number: 0, timestamp: 1716998400, ..Default::default() },
544                    ForkId { hash: ForkHash([0x54, 0x0a, 0x8c, 0x5d]), next: 1723478400 },
545                ),
546                (
547                    Head { number: 0, timestamp: 1723478399, ..Default::default() },
548                    ForkId { hash: ForkHash([0x54, 0x0a, 0x8c, 0x5d]), next: 1723478400 },
549                ),
550                (
551                    Head { number: 0, timestamp: 1723478400, ..Default::default() },
552                    ForkId { hash: ForkHash([0x75, 0xde, 0xa4, 0x1e]), next: 1732633200 },
553                ),
554                (
555                    Head { number: 0, timestamp: 1732633200, ..Default::default() },
556                    ForkId { hash: ForkHash([0x4a, 0x1c, 0x79, 0x2e]), next: 0 },
557                ),
558            ],
559        );
560    }
561
562    #[test]
563    fn op_mainnet_forkids() {
564        let mut op_mainnet = OpChainSpecBuilder::optimism_mainnet().build();
565        // for OP mainnet we have to do this because the genesis header can't be properly computed
566        // from the genesis.json file
567        op_mainnet.inner.genesis_header.set_hash(OP_MAINNET.genesis_hash());
568        test_fork_ids(
569            &op_mainnet,
570            &[
571                (
572                    Head { number: 0, ..Default::default() },
573                    ForkId { hash: ForkHash([0xca, 0xf5, 0x17, 0xed]), next: 3950000 },
574                ),
575                // TODO: complete these, see https://github.com/paradigmxyz/reth/issues/8012
576                (
577                    Head { number: 105235063, timestamp: 1710374401, ..Default::default() },
578                    ForkId { hash: ForkHash([0x19, 0xda, 0x4c, 0x52]), next: 1720627201 },
579                ),
580            ],
581        );
582    }
583
584    #[test]
585    fn base_sepolia_forkids() {
586        test_fork_ids(
587            &BASE_SEPOLIA,
588            &[
589                (
590                    Head { number: 0, ..Default::default() },
591                    ForkId { hash: ForkHash([0xb9, 0x59, 0xb9, 0xf7]), next: 1699981200 },
592                ),
593                (
594                    Head { number: 0, timestamp: 1699981199, ..Default::default() },
595                    ForkId { hash: ForkHash([0xb9, 0x59, 0xb9, 0xf7]), next: 1699981200 },
596                ),
597                (
598                    Head { number: 0, timestamp: 1699981200, ..Default::default() },
599                    ForkId { hash: ForkHash([0x60, 0x7c, 0xd5, 0xa1]), next: 1708534800 },
600                ),
601                (
602                    Head { number: 0, timestamp: 1708534799, ..Default::default() },
603                    ForkId { hash: ForkHash([0x60, 0x7c, 0xd5, 0xa1]), next: 1708534800 },
604                ),
605                (
606                    Head { number: 0, timestamp: 1708534800, ..Default::default() },
607                    ForkId { hash: ForkHash([0xbe, 0x96, 0x9b, 0x17]), next: 1716998400 },
608                ),
609                (
610                    Head { number: 0, timestamp: 1716998399, ..Default::default() },
611                    ForkId { hash: ForkHash([0xbe, 0x96, 0x9b, 0x17]), next: 1716998400 },
612                ),
613                (
614                    Head { number: 0, timestamp: 1716998400, ..Default::default() },
615                    ForkId { hash: ForkHash([0x4e, 0x45, 0x7a, 0x49]), next: 1723478400 },
616                ),
617                (
618                    Head { number: 0, timestamp: 1723478399, ..Default::default() },
619                    ForkId { hash: ForkHash([0x4e, 0x45, 0x7a, 0x49]), next: 1723478400 },
620                ),
621                (
622                    Head { number: 0, timestamp: 1723478400, ..Default::default() },
623                    ForkId { hash: ForkHash([0x5e, 0xdf, 0xa3, 0xb6]), next: 1732633200 },
624                ),
625                (
626                    Head { number: 0, timestamp: 1732633200, ..Default::default() },
627                    ForkId { hash: ForkHash([0x8b, 0x5e, 0x76, 0x29]), next: 0 },
628                ),
629            ],
630        );
631    }
632
633    #[test]
634    fn base_mainnet_genesis() {
635        let genesis = BASE_MAINNET.genesis_header();
636        assert_eq!(
637            genesis.hash_slow(),
638            b256!("0xf712aa9241cc24369b143cf6dce85f0902a9731e70d66818a3a5845b296c73dd")
639        );
640        let base_fee = genesis
641            .next_block_base_fee(BASE_MAINNET.base_fee_params_at_timestamp(genesis.timestamp))
642            .unwrap();
643        // <https://base.blockscout.com/block/1>
644        assert_eq!(base_fee, 980000000);
645    }
646
647    #[test]
648    fn base_sepolia_genesis() {
649        let genesis = BASE_SEPOLIA.genesis_header();
650        assert_eq!(
651            genesis.hash_slow(),
652            b256!("0x0dcc9e089e30b90ddfc55be9a37dd15bc551aeee999d2e2b51414c54eaf934e4")
653        );
654        let base_fee = genesis
655            .next_block_base_fee(BASE_SEPOLIA.base_fee_params_at_timestamp(genesis.timestamp))
656            .unwrap();
657        // <https://base-sepolia.blockscout.com/block/1>
658        assert_eq!(base_fee, 980000000);
659    }
660
661    #[test]
662    fn op_sepolia_genesis() {
663        let genesis = OP_SEPOLIA.genesis_header();
664        assert_eq!(
665            genesis.hash_slow(),
666            b256!("0x102de6ffb001480cc9b8b548fd05c34cd4f46ae4aa91759393db90ea0409887d")
667        );
668        let base_fee = genesis
669            .next_block_base_fee(OP_SEPOLIA.base_fee_params_at_timestamp(genesis.timestamp))
670            .unwrap();
671        // <https://optimism-sepolia.blockscout.com/block/1>
672        assert_eq!(base_fee, 980000000);
673    }
674
675    #[test]
676    fn latest_base_mainnet_fork_id() {
677        assert_eq!(
678            ForkId { hash: ForkHash([0x3a, 0x2a, 0xf1, 0x83]), next: 0 },
679            BASE_MAINNET.latest_fork_id()
680        )
681    }
682
683    #[test]
684    fn latest_base_mainnet_fork_id_with_builder() {
685        let base_mainnet = OpChainSpecBuilder::base_mainnet().build();
686        assert_eq!(
687            ForkId { hash: ForkHash([0x3a, 0x2a, 0xf1, 0x83]), next: 0 },
688            base_mainnet.latest_fork_id()
689        )
690    }
691
692    #[test]
693    fn is_bedrock_active() {
694        let op_mainnet = OpChainSpecBuilder::optimism_mainnet().build();
695        assert!(!op_mainnet.is_bedrock_active_at_block(1))
696    }
697
698    #[test]
699    fn parse_optimism_hardforks() {
700        let geth_genesis = r#"
701    {
702      "config": {
703        "bedrockBlock": 10,
704        "regolithTime": 20,
705        "canyonTime": 30,
706        "ecotoneTime": 40,
707        "fjordTime": 50,
708        "graniteTime": 51,
709        "holoceneTime": 52,
710        "optimism": {
711          "eip1559Elasticity": 60,
712          "eip1559Denominator": 70
713        }
714      }
715    }
716    "#;
717        let genesis: Genesis = serde_json::from_str(geth_genesis).unwrap();
718
719        let actual_bedrock_block = genesis.config.extra_fields.get("bedrockBlock");
720        assert_eq!(actual_bedrock_block, Some(serde_json::Value::from(10)).as_ref());
721        let actual_regolith_timestamp = genesis.config.extra_fields.get("regolithTime");
722        assert_eq!(actual_regolith_timestamp, Some(serde_json::Value::from(20)).as_ref());
723        let actual_canyon_timestamp = genesis.config.extra_fields.get("canyonTime");
724        assert_eq!(actual_canyon_timestamp, Some(serde_json::Value::from(30)).as_ref());
725        let actual_ecotone_timestamp = genesis.config.extra_fields.get("ecotoneTime");
726        assert_eq!(actual_ecotone_timestamp, Some(serde_json::Value::from(40)).as_ref());
727        let actual_fjord_timestamp = genesis.config.extra_fields.get("fjordTime");
728        assert_eq!(actual_fjord_timestamp, Some(serde_json::Value::from(50)).as_ref());
729        let actual_granite_timestamp = genesis.config.extra_fields.get("graniteTime");
730        assert_eq!(actual_granite_timestamp, Some(serde_json::Value::from(51)).as_ref());
731        let actual_holocene_timestamp = genesis.config.extra_fields.get("holoceneTime");
732        assert_eq!(actual_holocene_timestamp, Some(serde_json::Value::from(52)).as_ref());
733
734        let optimism_object = genesis.config.extra_fields.get("optimism").unwrap();
735        assert_eq!(
736            optimism_object,
737            &serde_json::json!({
738                "eip1559Elasticity": 60,
739                "eip1559Denominator": 70,
740            })
741        );
742
743        let chain_spec: OpChainSpec = genesis.into();
744
745        assert_eq!(
746            chain_spec.base_fee_params,
747            BaseFeeParamsKind::Constant(BaseFeeParams::new(70, 60))
748        );
749
750        assert!(!chain_spec.is_fork_active_at_block(OpHardfork::Bedrock, 0));
751        assert!(!chain_spec.is_fork_active_at_timestamp(OpHardfork::Regolith, 0));
752        assert!(!chain_spec.is_fork_active_at_timestamp(OpHardfork::Canyon, 0));
753        assert!(!chain_spec.is_fork_active_at_timestamp(OpHardfork::Ecotone, 0));
754        assert!(!chain_spec.is_fork_active_at_timestamp(OpHardfork::Fjord, 0));
755        assert!(!chain_spec.is_fork_active_at_timestamp(OpHardfork::Granite, 0));
756        assert!(!chain_spec.is_fork_active_at_timestamp(OpHardfork::Holocene, 0));
757
758        assert!(chain_spec.is_fork_active_at_block(OpHardfork::Bedrock, 10));
759        assert!(chain_spec.is_fork_active_at_timestamp(OpHardfork::Regolith, 20));
760        assert!(chain_spec.is_fork_active_at_timestamp(OpHardfork::Canyon, 30));
761        assert!(chain_spec.is_fork_active_at_timestamp(OpHardfork::Ecotone, 40));
762        assert!(chain_spec.is_fork_active_at_timestamp(OpHardfork::Fjord, 50));
763        assert!(chain_spec.is_fork_active_at_timestamp(OpHardfork::Granite, 51));
764        assert!(chain_spec.is_fork_active_at_timestamp(OpHardfork::Holocene, 52));
765    }
766
767    #[test]
768    fn parse_optimism_hardforks_variable_base_fee_params() {
769        let geth_genesis = r#"
770    {
771      "config": {
772        "bedrockBlock": 10,
773        "regolithTime": 20,
774        "canyonTime": 30,
775        "ecotoneTime": 40,
776        "fjordTime": 50,
777        "graniteTime": 51,
778        "holoceneTime": 52,
779        "optimism": {
780          "eip1559Elasticity": 60,
781          "eip1559Denominator": 70,
782          "eip1559DenominatorCanyon": 80
783        }
784      }
785    }
786    "#;
787        let genesis: Genesis = serde_json::from_str(geth_genesis).unwrap();
788
789        let actual_bedrock_block = genesis.config.extra_fields.get("bedrockBlock");
790        assert_eq!(actual_bedrock_block, Some(serde_json::Value::from(10)).as_ref());
791        let actual_regolith_timestamp = genesis.config.extra_fields.get("regolithTime");
792        assert_eq!(actual_regolith_timestamp, Some(serde_json::Value::from(20)).as_ref());
793        let actual_canyon_timestamp = genesis.config.extra_fields.get("canyonTime");
794        assert_eq!(actual_canyon_timestamp, Some(serde_json::Value::from(30)).as_ref());
795        let actual_ecotone_timestamp = genesis.config.extra_fields.get("ecotoneTime");
796        assert_eq!(actual_ecotone_timestamp, Some(serde_json::Value::from(40)).as_ref());
797        let actual_fjord_timestamp = genesis.config.extra_fields.get("fjordTime");
798        assert_eq!(actual_fjord_timestamp, Some(serde_json::Value::from(50)).as_ref());
799        let actual_granite_timestamp = genesis.config.extra_fields.get("graniteTime");
800        assert_eq!(actual_granite_timestamp, Some(serde_json::Value::from(51)).as_ref());
801        let actual_holocene_timestamp = genesis.config.extra_fields.get("holoceneTime");
802        assert_eq!(actual_holocene_timestamp, Some(serde_json::Value::from(52)).as_ref());
803
804        let optimism_object = genesis.config.extra_fields.get("optimism").unwrap();
805        assert_eq!(
806            optimism_object,
807            &serde_json::json!({
808                "eip1559Elasticity": 60,
809                "eip1559Denominator": 70,
810                "eip1559DenominatorCanyon": 80
811            })
812        );
813
814        let chain_spec: OpChainSpec = genesis.into();
815
816        assert_eq!(
817            chain_spec.base_fee_params,
818            BaseFeeParamsKind::Variable(
819                vec![
820                    (EthereumHardfork::London.boxed(), BaseFeeParams::new(70, 60)),
821                    (OpHardfork::Canyon.boxed(), BaseFeeParams::new(80, 60)),
822                ]
823                .into()
824            )
825        );
826
827        assert!(!chain_spec.is_fork_active_at_block(OpHardfork::Bedrock, 0));
828        assert!(!chain_spec.is_fork_active_at_timestamp(OpHardfork::Regolith, 0));
829        assert!(!chain_spec.is_fork_active_at_timestamp(OpHardfork::Canyon, 0));
830        assert!(!chain_spec.is_fork_active_at_timestamp(OpHardfork::Ecotone, 0));
831        assert!(!chain_spec.is_fork_active_at_timestamp(OpHardfork::Fjord, 0));
832        assert!(!chain_spec.is_fork_active_at_timestamp(OpHardfork::Granite, 0));
833        assert!(!chain_spec.is_fork_active_at_timestamp(OpHardfork::Holocene, 0));
834
835        assert!(chain_spec.is_fork_active_at_block(OpHardfork::Bedrock, 10));
836        assert!(chain_spec.is_fork_active_at_timestamp(OpHardfork::Regolith, 20));
837        assert!(chain_spec.is_fork_active_at_timestamp(OpHardfork::Canyon, 30));
838        assert!(chain_spec.is_fork_active_at_timestamp(OpHardfork::Ecotone, 40));
839        assert!(chain_spec.is_fork_active_at_timestamp(OpHardfork::Fjord, 50));
840        assert!(chain_spec.is_fork_active_at_timestamp(OpHardfork::Granite, 51));
841        assert!(chain_spec.is_fork_active_at_timestamp(OpHardfork::Holocene, 52));
842    }
843
844    #[test]
845    fn parse_genesis_optimism_with_variable_base_fee_params() {
846        use op_alloy_rpc_types::OpBaseFeeInfo;
847
848        let geth_genesis = r#"
849    {
850      "config": {
851        "chainId": 8453,
852        "homesteadBlock": 0,
853        "eip150Block": 0,
854        "eip155Block": 0,
855        "eip158Block": 0,
856        "byzantiumBlock": 0,
857        "constantinopleBlock": 0,
858        "petersburgBlock": 0,
859        "istanbulBlock": 0,
860        "muirGlacierBlock": 0,
861        "berlinBlock": 0,
862        "londonBlock": 0,
863        "arrowGlacierBlock": 0,
864        "grayGlacierBlock": 0,
865        "mergeNetsplitBlock": 0,
866        "bedrockBlock": 0,
867        "regolithTime": 15,
868        "terminalTotalDifficulty": 0,
869        "terminalTotalDifficultyPassed": true,
870        "optimism": {
871          "eip1559Elasticity": 6,
872          "eip1559Denominator": 50
873        }
874      }
875    }
876    "#;
877        let genesis: Genesis = serde_json::from_str(geth_genesis).unwrap();
878        let chainspec = OpChainSpec::from(genesis.clone());
879
880        let actual_chain_id = genesis.config.chain_id;
881        assert_eq!(actual_chain_id, 8453);
882
883        assert_eq!(
884            chainspec.hardforks.get(EthereumHardfork::Istanbul),
885            Some(ForkCondition::Block(0))
886        );
887
888        let actual_bedrock_block = genesis.config.extra_fields.get("bedrockBlock");
889        assert_eq!(actual_bedrock_block, Some(serde_json::Value::from(0)).as_ref());
890        let actual_canyon_timestamp = genesis.config.extra_fields.get("canyonTime");
891        assert_eq!(actual_canyon_timestamp, None);
892
893        assert!(genesis.config.terminal_total_difficulty_passed);
894
895        let optimism_object = genesis.config.extra_fields.get("optimism").unwrap();
896        let optimism_base_fee_info =
897            serde_json::from_value::<OpBaseFeeInfo>(optimism_object.clone()).unwrap();
898
899        assert_eq!(
900            optimism_base_fee_info,
901            OpBaseFeeInfo {
902                eip1559_elasticity: Some(6),
903                eip1559_denominator: Some(50),
904                eip1559_denominator_canyon: None,
905            }
906        );
907        assert_eq!(
908            chainspec.base_fee_params,
909            BaseFeeParamsKind::Constant(BaseFeeParams {
910                max_change_denominator: 50,
911                elasticity_multiplier: 6,
912            })
913        );
914
915        assert!(chainspec.is_fork_active_at_block(OpHardfork::Bedrock, 0));
916
917        assert!(chainspec.is_fork_active_at_timestamp(OpHardfork::Regolith, 20));
918    }
919
920    #[test]
921    fn test_fork_order_optimism_mainnet() {
922        use reth_optimism_forks::OpHardfork;
923
924        let genesis = Genesis {
925            config: ChainConfig {
926                chain_id: 0,
927                homestead_block: Some(0),
928                dao_fork_block: Some(0),
929                dao_fork_support: false,
930                eip150_block: Some(0),
931                eip155_block: Some(0),
932                eip158_block: Some(0),
933                byzantium_block: Some(0),
934                constantinople_block: Some(0),
935                petersburg_block: Some(0),
936                istanbul_block: Some(0),
937                muir_glacier_block: Some(0),
938                berlin_block: Some(0),
939                london_block: Some(0),
940                arrow_glacier_block: Some(0),
941                gray_glacier_block: Some(0),
942                merge_netsplit_block: Some(0),
943                shanghai_time: Some(0),
944                cancun_time: Some(0),
945                terminal_total_difficulty: Some(U256::ZERO),
946                extra_fields: [
947                    (String::from("bedrockBlock"), 0.into()),
948                    (String::from("regolithTime"), 0.into()),
949                    (String::from("canyonTime"), 0.into()),
950                    (String::from("ecotoneTime"), 0.into()),
951                    (String::from("fjordTime"), 0.into()),
952                    (String::from("graniteTime"), 0.into()),
953                    (String::from("holoceneTime"), 0.into()),
954                ]
955                .into_iter()
956                .collect(),
957                ..Default::default()
958            },
959            ..Default::default()
960        };
961
962        let chain_spec: OpChainSpec = genesis.into();
963
964        let hardforks: Vec<_> = chain_spec.hardforks.forks_iter().map(|(h, _)| h).collect();
965        let expected_hardforks = vec![
966            EthereumHardfork::Frontier.boxed(),
967            EthereumHardfork::Homestead.boxed(),
968            EthereumHardfork::Tangerine.boxed(),
969            EthereumHardfork::SpuriousDragon.boxed(),
970            EthereumHardfork::Byzantium.boxed(),
971            EthereumHardfork::Constantinople.boxed(),
972            EthereumHardfork::Petersburg.boxed(),
973            EthereumHardfork::Istanbul.boxed(),
974            EthereumHardfork::MuirGlacier.boxed(),
975            EthereumHardfork::Berlin.boxed(),
976            EthereumHardfork::London.boxed(),
977            EthereumHardfork::ArrowGlacier.boxed(),
978            EthereumHardfork::GrayGlacier.boxed(),
979            EthereumHardfork::Paris.boxed(),
980            OpHardfork::Bedrock.boxed(),
981            OpHardfork::Regolith.boxed(),
982            EthereumHardfork::Shanghai.boxed(),
983            OpHardfork::Canyon.boxed(),
984            EthereumHardfork::Cancun.boxed(),
985            OpHardfork::Ecotone.boxed(),
986            OpHardfork::Fjord.boxed(),
987            OpHardfork::Granite.boxed(),
988            OpHardfork::Holocene.boxed(),
989            // OpHardfork::Isthmus.boxed(),
990            // OpHardfork::Interop.boxed(),
991        ];
992
993        for (expected, actual) in expected_hardforks.iter().zip(hardforks.iter()) {
994            println!("got {expected:?}, {actual:?}");
995            assert_eq!(&**expected, &**actual);
996        }
997        assert_eq!(expected_hardforks.len(), hardforks.len());
998    }
999
1000    #[test]
1001    fn json_genesis() {
1002        let geth_genesis = r#"
1003{
1004    "config": {
1005        "chainId": 1301,
1006        "homesteadBlock": 0,
1007        "eip150Block": 0,
1008        "eip155Block": 0,
1009        "eip158Block": 0,
1010        "byzantiumBlock": 0,
1011        "constantinopleBlock": 0,
1012        "petersburgBlock": 0,
1013        "istanbulBlock": 0,
1014        "muirGlacierBlock": 0,
1015        "berlinBlock": 0,
1016        "londonBlock": 0,
1017        "arrowGlacierBlock": 0,
1018        "grayGlacierBlock": 0,
1019        "mergeNetsplitBlock": 0,
1020        "shanghaiTime": 0,
1021        "cancunTime": 0,
1022        "bedrockBlock": 0,
1023        "regolithTime": 0,
1024        "canyonTime": 0,
1025        "ecotoneTime": 0,
1026        "fjordTime": 0,
1027        "graniteTime": 0,
1028        "holoceneTime": 1732633200,
1029        "terminalTotalDifficulty": 0,
1030        "terminalTotalDifficultyPassed": true,
1031        "optimism": {
1032            "eip1559Elasticity": 6,
1033            "eip1559Denominator": 50,
1034            "eip1559DenominatorCanyon": 250
1035        }
1036    },
1037    "nonce": "0x0",
1038    "timestamp": "0x66edad4c",
1039    "extraData": "0x424544524f434b",
1040    "gasLimit": "0x1c9c380",
1041    "difficulty": "0x0",
1042    "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
1043    "coinbase": "0x4200000000000000000000000000000000000011",
1044    "alloc": {},
1045    "number": "0x0",
1046    "gasUsed": "0x0",
1047    "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
1048    "baseFeePerGas": "0x3b9aca00",
1049    "excessBlobGas": "0x0",
1050    "blobGasUsed": "0x0"
1051}
1052        "#;
1053
1054        let genesis: Genesis = serde_json::from_str(geth_genesis).unwrap();
1055        let chainspec = OpChainSpec::from_genesis(genesis);
1056        assert!(chainspec.is_holocene_active_at_timestamp(1732633200));
1057    }
1058}