Skip to main content

tempo_chainspec/
spec.rs

1pub use crate::constants::gas::*;
2
3use crate::{
4    bootnodes::{moderato_nodes, presto_nodes},
5    hardfork::{TempoHardfork, TempoHardforks},
6};
7use alloc::{boxed::Box, sync::Arc, vec::Vec};
8use alloy_eips::eip7840::BlobParams;
9use alloy_evm::eth::spec::EthExecutorSpec;
10use alloy_genesis::Genesis;
11use alloy_primitives::{Address, B256, U256};
12use once_cell as _;
13#[cfg(not(feature = "std"))]
14use once_cell::sync::Lazy as LazyLock;
15use reth_chainspec::{
16    BaseFeeParams, Chain, ChainSpec, DepositContract, DisplayHardforks, EthChainSpec,
17    EthereumHardfork, EthereumHardforks, ForkCondition, ForkFilter, ForkId, Hardfork, Hardforks,
18    Head,
19};
20use reth_network_peers::NodeRecord;
21#[cfg(feature = "std")]
22use std::sync::LazyLock;
23use tempo_primitives::TempoHeader;
24
25// End-of-block system transactions
26pub const SYSTEM_TX_COUNT: usize = 1;
27pub const SYSTEM_TX_ADDRESSES: [Address; SYSTEM_TX_COUNT] = [Address::ZERO];
28
29/// Tempo genesis info extracted from genesis extra_fields
30#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
31#[serde(rename_all = "camelCase")]
32pub struct TempoGenesisInfo {
33    /// The epoch length used by consensus.
34    #[serde(skip_serializing_if = "Option::is_none")]
35    epoch_length: Option<u64>,
36    /// Activation timestamp for T0 hardfork.
37    #[serde(skip_serializing_if = "Option::is_none")]
38    t0_time: Option<u64>,
39    /// Activation timestamp for T1 hardfork.
40    #[serde(skip_serializing_if = "Option::is_none")]
41    t1_time: Option<u64>,
42    /// Activation timestamp for T1.A hardfork.
43    #[serde(skip_serializing_if = "Option::is_none")]
44    t1a_time: Option<u64>,
45    /// Activation timestamp for T1.B hardfork.
46    #[serde(skip_serializing_if = "Option::is_none")]
47    t1b_time: Option<u64>,
48    /// Activation timestamp for T1.C hardfork.
49    #[serde(skip_serializing_if = "Option::is_none")]
50    t1c_time: Option<u64>,
51    /// Activation timestamp for T2 hardfork.
52    #[serde(skip_serializing_if = "Option::is_none")]
53    t2_time: Option<u64>,
54    /// Activation timestamp for T3 hardfork.
55    #[serde(skip_serializing_if = "Option::is_none")]
56    t3_time: Option<u64>,
57    /// Activation timestamp for T4 hardfork.
58    #[serde(skip_serializing_if = "Option::is_none")]
59    t4_time: Option<u64>,
60    /// Activation timestamp for T5 hardfork.
61    #[serde(skip_serializing_if = "Option::is_none")]
62    t5_time: Option<u64>,
63    /// Activation timestamp for T6 hardfork.
64    #[serde(skip_serializing_if = "Option::is_none")]
65    t6_time: Option<u64>,
66}
67
68impl TempoGenesisInfo {
69    /// Extract Tempo genesis info from genesis extra_fields
70    fn extract_from(genesis: &Genesis) -> Self {
71        genesis
72            .config
73            .extra_fields
74            .deserialize_as::<Self>()
75            .unwrap_or_default()
76    }
77
78    pub fn epoch_length(&self) -> Option<u64> {
79        self.epoch_length
80    }
81
82    /// Returns the activation timestamp for a given hardfork, or `None` if not scheduled.
83    pub fn fork_time(&self, fork: TempoHardfork) -> Option<u64> {
84        match fork {
85            TempoHardfork::Genesis => Some(0),
86            TempoHardfork::T0 => self.t0_time,
87            TempoHardfork::T1 => self.t1_time,
88            TempoHardfork::T1A => self.t1a_time,
89            TempoHardfork::T1B => self.t1b_time,
90            TempoHardfork::T1C => self.t1c_time,
91            TempoHardfork::T2 => self.t2_time,
92            TempoHardfork::T3 => self.t3_time,
93            TempoHardfork::T4 => self.t4_time,
94            TempoHardfork::T5 => self.t5_time,
95            TempoHardfork::T6 => self.t6_time,
96        }
97    }
98}
99
100/// Tempo chain specification parser.
101#[derive(Debug, Clone, Default)]
102pub struct TempoChainSpecParser;
103
104/// Chains supported by Tempo. First value should be used as the default.
105pub const SUPPORTED_CHAINS: &[&str] = &["mainnet", "moderato", "testnet"];
106
107/// Clap value parser for [`ChainSpec`]s.
108///
109/// The value parser matches either a known chain, the path
110/// to a json file, or a json formatted string in-memory. The json needs to be a Genesis struct.
111#[cfg(feature = "cli")]
112pub fn chain_value_parser(s: &str) -> eyre::Result<Arc<TempoChainSpec>> {
113    Ok(match s {
114        "mainnet" => PRESTO.clone(),
115        "testnet" | "moderato" => MODERATO.clone(),
116        "dev" => DEV.clone(),
117        _ => TempoChainSpec::from_genesis(reth_cli::chainspec::parse_genesis(s)?).into(),
118    })
119}
120
121#[cfg(feature = "cli")]
122impl reth_cli::chainspec::ChainSpecParser for TempoChainSpecParser {
123    type ChainSpec = TempoChainSpec;
124
125    const SUPPORTED_CHAINS: &'static [&'static str] = SUPPORTED_CHAINS;
126
127    fn parse(s: &str) -> eyre::Result<Arc<Self::ChainSpec>> {
128        chain_value_parser(s)
129    }
130}
131
132/// Resolve a [`TempoChainSpec`] from a chain id.
133///
134/// Returns `None` for unknown chain ids.
135pub fn chainspec_from_chain_id(chain_id: u64) -> Option<Arc<TempoChainSpec>> {
136    match chain_id {
137        4217 => Some(PRESTO.clone()),
138        42431 => Some(MODERATO.clone()),
139        _ => None,
140    }
141}
142
143pub static MODERATO: LazyLock<Arc<TempoChainSpec>> = LazyLock::new(|| {
144    let genesis: Genesis = serde_json::from_str(include_str!("./genesis/moderato.json"))
145        .expect("`./genesis/moderato.json` must be present and deserializable");
146    TempoChainSpec::from_genesis(genesis)
147        .with_default_follow_url("wss://rpc.moderato.tempo.xyz")
148        .into()
149});
150
151pub static PRESTO: LazyLock<Arc<TempoChainSpec>> = LazyLock::new(|| {
152    let genesis: Genesis = serde_json::from_str(include_str!("./genesis/presto.json"))
153        .expect("`./genesis/presto.json` must be present and deserializable");
154    TempoChainSpec::from_genesis(genesis)
155        .with_default_follow_url("wss://rpc.presto.tempo.xyz")
156        .into()
157});
158
159/// Development chainspec with funded dev accounts and activated tempo hardforks
160///
161/// `cargo x generate-genesis -o dev.json --accounts 10 --no-dkg-in-genesis`
162pub static DEV: LazyLock<Arc<TempoChainSpec>> = LazyLock::new(|| {
163    let genesis: Genesis = serde_json::from_str(include_str!("./genesis/dev.json"))
164        .expect("`./genesis/dev.json` must be present and deserializable");
165    TempoChainSpec::from_genesis(genesis).into()
166});
167
168/// Tempo chain spec type.
169#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct TempoChainSpec {
171    /// [`ChainSpec`].
172    pub inner: ChainSpec<TempoHeader>,
173    pub info: TempoGenesisInfo,
174    /// Default RPC URL for following this chain.
175    pub default_follow_url: Option<&'static str>,
176}
177
178impl TempoChainSpec {
179    /// Returns the default RPC URL for following this chain.
180    pub fn default_follow_url(&self) -> Option<&'static str> {
181        self.default_follow_url
182    }
183
184    /// Converts the given [`Genesis`] into a [`TempoChainSpec`].
185    pub fn from_genesis(genesis: Genesis) -> Self {
186        // Extract Tempo genesis info from extra_fields
187        let info = TempoGenesisInfo::extract_from(&genesis);
188
189        // Create base chainspec from genesis (already has ordered Ethereum hardforks)
190        let mut base_spec = ChainSpec::from_genesis(genesis);
191
192        let tempo_forks = TempoHardfork::VARIANTS.iter().filter_map(|&fork| {
193            info.fork_time(fork)
194                .map(|time| (fork, ForkCondition::Timestamp(time)))
195        });
196        base_spec.hardforks.extend(tempo_forks);
197
198        Self {
199            inner: base_spec.map_header(|inner| TempoHeader {
200                general_gas_limit: 0,
201                timestamp_millis_part: inner.timestamp % 1000,
202                shared_gas_limit: 0,
203                consensus_context: None,
204                inner,
205            }),
206            info,
207            default_follow_url: None,
208        }
209    }
210
211    /// Sets the default follow URL for this chain spec.
212    pub fn with_default_follow_url(mut self, url: &'static str) -> Self {
213        self.default_follow_url = Some(url);
214        self
215    }
216
217    /// Returns the moderato chainspec.
218    pub fn moderato() -> Self {
219        MODERATO.as_ref().clone()
220    }
221
222    /// Returns the mainnet chainspec.
223    pub fn mainnet() -> Self {
224        PRESTO.as_ref().clone()
225    }
226}
227
228// Required by reth's e2e-test-utils for integration tests.
229// The test utilities need to convert from standard ChainSpec to custom chain specs.
230impl From<ChainSpec> for TempoChainSpec {
231    fn from(spec: ChainSpec) -> Self {
232        Self {
233            inner: spec.map_header(|inner| TempoHeader {
234                general_gas_limit: 0,
235                timestamp_millis_part: inner.timestamp % 1000,
236                shared_gas_limit: 0,
237                consensus_context: None,
238                inner,
239            }),
240            info: TempoGenesisInfo::default(),
241            default_follow_url: None,
242        }
243    }
244}
245
246impl Hardforks for TempoChainSpec {
247    fn fork<H: Hardfork>(&self, fork: H) -> ForkCondition {
248        self.inner.fork(fork)
249    }
250
251    fn forks_iter(&self) -> impl Iterator<Item = (&dyn Hardfork, ForkCondition)> {
252        self.inner.forks_iter()
253    }
254
255    fn fork_id(&self, head: &Head) -> ForkId {
256        self.inner.fork_id(head)
257    }
258
259    fn latest_fork_id(&self) -> ForkId {
260        self.inner.latest_fork_id()
261    }
262
263    fn fork_filter(&self, head: Head) -> ForkFilter {
264        self.inner.fork_filter(head)
265    }
266}
267
268impl EthChainSpec for TempoChainSpec {
269    type Header = TempoHeader;
270
271    fn chain(&self) -> Chain {
272        self.inner.chain()
273    }
274
275    fn base_fee_params_at_timestamp(&self, timestamp: u64) -> BaseFeeParams {
276        self.inner.base_fee_params_at_timestamp(timestamp)
277    }
278
279    fn blob_params_at_timestamp(&self, timestamp: u64) -> Option<BlobParams> {
280        self.inner.blob_params_at_timestamp(timestamp)
281    }
282
283    fn deposit_contract(&self) -> Option<&DepositContract> {
284        self.inner.deposit_contract()
285    }
286
287    fn genesis_hash(&self) -> B256 {
288        self.inner.genesis_hash()
289    }
290
291    fn prune_delete_limit(&self) -> usize {
292        self.inner.prune_delete_limit()
293    }
294
295    fn display_hardforks(&self) -> Box<dyn core::fmt::Display> {
296        // filter only tempo hardforks
297        let tempo_forks = self.inner.hardforks.forks_iter().filter(|(fork, _)| {
298            !EthereumHardfork::VARIANTS
299                .iter()
300                .any(|h| h.name() == (*fork).name())
301        });
302
303        Box::new(DisplayHardforks::new(tempo_forks))
304    }
305
306    fn genesis_header(&self) -> &Self::Header {
307        self.inner.genesis_header()
308    }
309
310    fn genesis(&self) -> &Genesis {
311        self.inner.genesis()
312    }
313
314    fn bootnodes(&self) -> Option<Vec<NodeRecord>> {
315        match self.inner.chain_id() {
316            4217 => Some(presto_nodes()),
317            42431 => Some(moderato_nodes()),
318            _ => self.inner.bootnodes(),
319        }
320    }
321
322    fn final_paris_total_difficulty(&self) -> Option<U256> {
323        self.inner.get_final_paris_total_difficulty()
324    }
325
326    fn next_block_base_fee(&self, _parent: &TempoHeader, target_timestamp: u64) -> Option<u64> {
327        Some(self.tempo_hardfork_at(target_timestamp).base_fee())
328    }
329}
330
331impl EthereumHardforks for TempoChainSpec {
332    fn ethereum_fork_activation(&self, fork: EthereumHardfork) -> ForkCondition {
333        self.inner.ethereum_fork_activation(fork)
334    }
335}
336
337impl EthExecutorSpec for TempoChainSpec {
338    fn deposit_contract_address(&self) -> Option<Address> {
339        self.inner.deposit_contract_address()
340    }
341}
342
343impl TempoHardforks for TempoChainSpec {
344    fn tempo_fork_activation(&self, fork: TempoHardfork) -> ForkCondition {
345        self.fork(fork)
346    }
347}
348
349#[cfg(test)]
350mod tests {
351    use crate::hardfork::{TempoHardfork, TempoHardforks};
352    use reth_chainspec::{ForkCondition, Hardforks};
353    use reth_cli::chainspec::ChainSpecParser as _;
354
355    #[test]
356    fn can_load_testnet() {
357        let _ = super::TempoChainSpecParser::parse("testnet")
358            .expect("the testnet chainspec must always be well formed");
359    }
360
361    #[test]
362    fn can_load_dev() {
363        let _ = super::TempoChainSpecParser::parse("dev")
364            .expect("the dev chainspec must always be well formed");
365    }
366
367    #[test]
368    fn test_tempo_chainspec_has_tempo_hardforks() {
369        let chainspec = super::TempoChainSpecParser::parse("mainnet")
370            .expect("the mainnet chainspec must always be well formed");
371
372        // Genesis should be active at timestamp 0
373        let activation = chainspec.tempo_fork_activation(TempoHardfork::Genesis);
374        assert_eq!(activation, ForkCondition::Timestamp(0));
375
376        // T0 should be active at timestamp 0
377        let activation = chainspec.tempo_fork_activation(TempoHardfork::T0);
378        assert_eq!(activation, ForkCondition::Timestamp(0));
379    }
380
381    #[test]
382    fn test_tempo_chainspec_implements_tempo_hardforks_trait() {
383        let chainspec = super::TempoChainSpecParser::parse("mainnet")
384            .expect("the mainnet chainspec must always be well formed");
385
386        // Should be able to query Tempo hardfork activation through trait
387        let activation = chainspec.tempo_fork_activation(TempoHardfork::T0);
388        assert_eq!(activation, ForkCondition::Timestamp(0));
389    }
390
391    #[test]
392    fn test_tempo_hardforks_in_inner_hardforks() {
393        let chainspec = super::TempoChainSpecParser::parse("mainnet")
394            .expect("the mainnet chainspec must always be well formed");
395
396        // Tempo hardforks should be queryable from inner.hardforks via Hardforks trait
397        let activation = chainspec.fork(TempoHardfork::T0);
398        assert_eq!(activation, ForkCondition::Timestamp(0));
399
400        // Verify Genesis appears in forks iterator
401        let has_genesis = chainspec
402            .forks_iter()
403            .any(|(fork, _)| fork.name() == "Genesis");
404        assert!(has_genesis, "Genesis hardfork should be in inner.hardforks");
405    }
406
407    #[test]
408    fn test_from_genesis_with_hardforks_at_zero() {
409        use alloy_genesis::Genesis;
410
411        // Build genesis config with every post-Genesis fork at timestamp 0
412        let mut config = serde_json::Map::new();
413        config.insert("chainId".into(), 1234.into());
414        for &fork in TempoHardfork::VARIANTS {
415            if fork != TempoHardfork::Genesis {
416                let key = format!("{}Time", fork.name().to_lowercase());
417                config.insert(key, 0.into());
418            }
419        }
420        let json = serde_json::json!({ "config": config, "alloc": {} });
421        let genesis: Genesis = serde_json::from_value(json).unwrap();
422        let chainspec = super::TempoChainSpec::from_genesis(genesis);
423
424        // Every fork should be active at any timestamp
425        for &fork in TempoHardfork::VARIANTS {
426            assert!(
427                chainspec.tempo_fork_activation(fork).active_at_timestamp(0),
428                "{fork:?} should be active at timestamp 0"
429            );
430            assert!(
431                chainspec
432                    .tempo_fork_activation(fork)
433                    .active_at_timestamp(1000),
434                "{fork:?} should be active at timestamp 1000"
435            );
436        }
437
438        // tempo_hardfork_at should return the latest fork
439        let latest = *TempoHardfork::VARIANTS.last().unwrap();
440        assert_eq!(chainspec.tempo_hardfork_at(0), latest);
441        assert_eq!(chainspec.tempo_hardfork_at(1000), latest);
442        assert_eq!(chainspec.tempo_hardfork_at(u64::MAX), latest);
443    }
444
445    mod tempo_hardfork_at {
446        use super::*;
447
448        #[test]
449        fn mainnet() {
450            let cs = super::super::TempoChainSpecParser::parse("mainnet")
451                .expect("the mainnet chainspec must always be well formed");
452
453            // Before T1 activation (1770908400 = Feb 12th 2026 16:00 CET)
454            assert_eq!(cs.tempo_hardfork_at(0), TempoHardfork::T0);
455            assert_eq!(cs.tempo_hardfork_at(1000), TempoHardfork::T0);
456            assert_eq!(cs.tempo_hardfork_at(1770908399), TempoHardfork::T0);
457
458            // At and after T1/T1A activation (both activate at 1770908400)
459            assert!(cs.is_t1_active_at_timestamp(1770908400));
460            assert!(cs.is_t1a_active_at_timestamp(1770908400));
461            assert_eq!(cs.tempo_hardfork_at(1770908400), TempoHardfork::T1A);
462            assert_eq!(cs.tempo_hardfork_at(1770908401), TempoHardfork::T1A);
463
464            // Before T1B activation (1771858800 = Feb 23rd 2026 16:00 CET)
465            assert!(!cs.is_t1b_active_at_timestamp(1771858799));
466            assert_eq!(cs.tempo_hardfork_at(1771858799), TempoHardfork::T1A);
467
468            // At and after T1B activation
469            assert!(cs.is_t1b_active_at_timestamp(1771858800));
470            assert_eq!(cs.tempo_hardfork_at(1771858800), TempoHardfork::T1B);
471
472            // Before T1C activation (1773327600 = Mar 12th 2026 16:00 CET)
473            assert!(!cs.is_t1c_active_at_timestamp(1773327599));
474            assert_eq!(cs.tempo_hardfork_at(1773327599), TempoHardfork::T1B);
475
476            // At and after T1C activation
477            assert!(cs.is_t1c_active_at_timestamp(1773327600));
478            assert_eq!(cs.tempo_hardfork_at(1773327600), TempoHardfork::T1C);
479
480            // Before T2 activation (1774965600 = Mar 31st 2026 16:00 CEST)
481            assert!(!cs.is_t2_active_at_timestamp(1774965599));
482            assert_eq!(cs.tempo_hardfork_at(1774965599), TempoHardfork::T1C);
483
484            // At and after T2 activation
485            assert!(cs.is_t2_active_at_timestamp(1774965600));
486            assert_eq!(cs.tempo_hardfork_at(1774965600), TempoHardfork::T2);
487
488            // Before T3 activation (1777298400 = Apr 27th 2026 16:00 CEST)
489            assert!(!cs.is_t3_active_at_timestamp(1777298399));
490            assert_eq!(cs.tempo_hardfork_at(1777298399), TempoHardfork::T2);
491
492            // At and after T3 activation
493            assert!(cs.is_t3_active_at_timestamp(1777298400));
494            assert_eq!(cs.tempo_hardfork_at(1777298400), TempoHardfork::T3);
495
496            // Before T4 activation (1779112800 = May 18th 2026 16:00 CEST)
497            assert!(!cs.is_t4_active_at_timestamp(1779112799));
498            assert_eq!(cs.tempo_hardfork_at(1779112799), TempoHardfork::T3);
499
500            // At and after T4 activation
501            assert!(cs.is_t4_active_at_timestamp(1779112800));
502            assert_eq!(cs.tempo_hardfork_at(1779112800), TempoHardfork::T4);
503            assert!(!cs.is_t5_active_at_timestamp(u64::MAX));
504            assert!(!cs.is_t6_active_at_timestamp(u64::MAX));
505            assert_eq!(cs.tempo_hardfork_at(u64::MAX), TempoHardfork::T4);
506        }
507
508        #[test]
509        fn moderato() {
510            let cs = super::super::TempoChainSpecParser::parse("moderato")
511                .expect("the moderato chainspec must always be well formed");
512
513            // Before T0/T1 activation (1770303600 = Feb 5th 2026 16:00 CET)
514            assert_eq!(cs.tempo_hardfork_at(0), TempoHardfork::Genesis);
515            assert_eq!(cs.tempo_hardfork_at(1770303599), TempoHardfork::Genesis);
516
517            // At and after T0/T1 activation
518            assert_eq!(cs.tempo_hardfork_at(1770303600), TempoHardfork::T1);
519            assert_eq!(cs.tempo_hardfork_at(1770303601), TempoHardfork::T1);
520
521            // Before T1A/T1B activation (1771858800 = Feb 23rd 2026 16:00 CET)
522            assert_eq!(cs.tempo_hardfork_at(1771858799), TempoHardfork::T1);
523
524            // At and after T1A/T1B activation (both activate at 1771858800)
525            assert!(cs.is_t1a_active_at_timestamp(1771858800));
526            assert!(cs.is_t1b_active_at_timestamp(1771858800));
527            assert_eq!(cs.tempo_hardfork_at(1771858800), TempoHardfork::T1B);
528
529            // Before T1C activation (1773068400 = Mar 9th 2026 16:00 CET)
530            assert!(!cs.is_t1c_active_at_timestamp(1773068399));
531            assert_eq!(cs.tempo_hardfork_at(1773068399), TempoHardfork::T1B);
532
533            // At and after T1C activation
534            assert!(cs.is_t1c_active_at_timestamp(1773068400));
535            assert_eq!(cs.tempo_hardfork_at(1773068400), TempoHardfork::T1C);
536
537            // Before T2 activation (1774537200 = Mar 26th 2026 16:00 CET)
538            assert!(!cs.is_t2_active_at_timestamp(1774537199));
539            assert_eq!(cs.tempo_hardfork_at(1774537199), TempoHardfork::T1C);
540
541            // At and after T2 activation
542            assert!(cs.is_t2_active_at_timestamp(1774537200));
543            assert_eq!(cs.tempo_hardfork_at(1774537200), TempoHardfork::T2);
544
545            // Before T3 activation (1776780000 = Apr 21st 2026 16:00 CEST)
546            assert!(!cs.is_t3_active_at_timestamp(1776779999));
547            assert_eq!(cs.tempo_hardfork_at(1776779999), TempoHardfork::T2);
548
549            // At and after T3 activation
550            assert!(cs.is_t3_active_at_timestamp(1776780000));
551            assert_eq!(cs.tempo_hardfork_at(1776780000), TempoHardfork::T3);
552
553            // Before T4 activation (1778767200 = May 14th 2026 16:00 CEST)
554            assert!(!cs.is_t4_active_at_timestamp(1778767199));
555            assert_eq!(cs.tempo_hardfork_at(1778767199), TempoHardfork::T3);
556
557            // At and after T4 activation
558            assert!(cs.is_t4_active_at_timestamp(1778767200));
559            assert_eq!(cs.tempo_hardfork_at(1778767200), TempoHardfork::T4);
560            assert!(!cs.is_t5_active_at_timestamp(u64::MAX));
561            assert!(!cs.is_t6_active_at_timestamp(u64::MAX));
562            assert_eq!(cs.tempo_hardfork_at(u64::MAX), TempoHardfork::T4);
563        }
564
565        #[test]
566        fn testnet() {
567            let cs = super::super::TempoChainSpecParser::parse("testnet")
568                .expect("the testnet chainspec must always be well formed");
569
570            // "testnet" is an alias for moderato
571            let moderato = super::super::TempoChainSpecParser::parse("moderato")
572                .expect("the moderato chainspec must always be well formed");
573            assert_eq!(cs.inner.chain(), moderato.inner.chain());
574        }
575    }
576
577    #[test]
578    #[allow(clippy::expect_fun_call)]
579    fn chainspec_from_chain_id_roundtrips_supported_chains() {
580        use reth_chainspec::EthChainSpec;
581
582        for &name in super::SUPPORTED_CHAINS {
583            let spec =
584                super::chain_value_parser(name).expect(&format!("failed to parse chain `{name}`"));
585
586            let resolved = super::chainspec_from_chain_id(spec.chain().id())
587                .expect(&format!("failed to parse chain `{name}`"));
588
589            assert_eq!(spec.chain(), resolved.chain(), "chain mismatch for {name}");
590        }
591    }
592}