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    network_identity::NetworkIdentity,
7};
8use alloc::{boxed::Box, sync::Arc, vec::Vec};
9use alloy_eips::eip7840::BlobParams;
10use alloy_evm::eth::spec::EthExecutorSpec;
11use alloy_genesis::Genesis;
12use alloy_primitives::{Address, B256, U256};
13use once_cell as _;
14#[cfg(not(feature = "std"))]
15use once_cell::sync::Lazy as LazyLock;
16use reth_chainspec::{
17    BaseFeeParams, Chain, ChainSpec, DepositContract, DisplayHardforks, EthChainSpec,
18    EthereumHardfork, EthereumHardforks, ForkCondition, ForkFilter, ForkId, Hardfork, Hardforks,
19    Head,
20};
21use reth_network_peers::NodeRecord;
22#[cfg(feature = "std")]
23use std::sync::LazyLock;
24use tempo_primitives::TempoHeader;
25
26// End-of-block system transactions
27pub const SYSTEM_TX_COUNT: usize = 1;
28pub const SYSTEM_TX_ADDRESSES: [Address; SYSTEM_TX_COUNT] = [Address::ZERO];
29
30/// Tempo genesis info extracted from genesis extra_fields
31#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
32#[serde(rename_all = "camelCase")]
33pub struct TempoGenesisInfo {
34    /// The epoch length used by consensus.
35    #[serde(skip_serializing_if = "Option::is_none")]
36    epoch_length: Option<u64>,
37    /// Activation timestamp for T0 hardfork.
38    #[serde(skip_serializing_if = "Option::is_none")]
39    t0_time: Option<u64>,
40    /// Activation timestamp for T1 hardfork.
41    #[serde(skip_serializing_if = "Option::is_none")]
42    t1_time: Option<u64>,
43    /// Activation timestamp for T1.A hardfork.
44    #[serde(skip_serializing_if = "Option::is_none")]
45    t1a_time: Option<u64>,
46    /// Activation timestamp for T1.B hardfork.
47    #[serde(skip_serializing_if = "Option::is_none")]
48    t1b_time: Option<u64>,
49    /// Activation timestamp for T1.C hardfork.
50    #[serde(skip_serializing_if = "Option::is_none")]
51    t1c_time: Option<u64>,
52    /// Activation timestamp for T2 hardfork.
53    #[serde(skip_serializing_if = "Option::is_none")]
54    t2_time: Option<u64>,
55    /// Activation timestamp for T3 hardfork.
56    #[serde(skip_serializing_if = "Option::is_none")]
57    t3_time: Option<u64>,
58    /// Activation timestamp for T4 hardfork.
59    #[serde(skip_serializing_if = "Option::is_none")]
60    t4_time: Option<u64>,
61    /// Activation timestamp for T5 hardfork.
62    #[serde(skip_serializing_if = "Option::is_none")]
63    t5_time: Option<u64>,
64    /// Activation timestamp for T6 hardfork.
65    #[serde(skip_serializing_if = "Option::is_none")]
66    t6_time: Option<u64>,
67    /// Activation timestamp for T7 hardfork.
68    #[serde(skip_serializing_if = "Option::is_none")]
69    t7_time: Option<u64>,
70    /// Activation timestamp for T8 hardfork.
71    #[serde(skip_serializing_if = "Option::is_none")]
72    t8_time: Option<u64>,
73}
74
75impl TempoGenesisInfo {
76    /// Extract Tempo genesis info from genesis extra_fields
77    fn extract_from(genesis: &Genesis) -> Self {
78        genesis
79            .config
80            .extra_fields
81            .deserialize_as::<Self>()
82            .unwrap_or_default()
83    }
84
85    pub fn epoch_length(&self) -> Option<u64> {
86        self.epoch_length
87    }
88
89    /// Returns the activation timestamp for a given hardfork, or `None` if not scheduled.
90    pub fn fork_time(&self, fork: TempoHardfork) -> Option<u64> {
91        match fork {
92            TempoHardfork::Genesis => Some(0),
93            TempoHardfork::T0 => self.t0_time,
94            TempoHardfork::T1 => self.t1_time,
95            TempoHardfork::T1A => self.t1a_time,
96            TempoHardfork::T1B => self.t1b_time,
97            TempoHardfork::T1C => self.t1c_time,
98            TempoHardfork::T2 => self.t2_time,
99            TempoHardfork::T3 => self.t3_time,
100            TempoHardfork::T4 => self.t4_time,
101            TempoHardfork::T5 => self.t5_time,
102            TempoHardfork::T6 => self.t6_time,
103            TempoHardfork::T7 => self.t7_time,
104            TempoHardfork::T8 => self.t8_time,
105        }
106    }
107}
108
109/// Tempo chain specification parser.
110#[derive(Debug, Clone, Default)]
111pub struct TempoChainSpecParser;
112
113/// Chains supported by Tempo. First value should be used as the default.
114pub const SUPPORTED_CHAINS: &[&str] = &["mainnet", "moderato", "testnet"];
115
116/// Clap value parser for [`ChainSpec`]s.
117///
118/// The value parser matches either a known chain, the path
119/// to a json file, or a json formatted string in-memory. The json needs to be a Genesis struct.
120#[cfg(feature = "cli")]
121pub fn chain_value_parser(s: &str) -> eyre::Result<Arc<TempoChainSpec>> {
122    Ok(match s {
123        "mainnet" => PRESTO.clone(),
124        "testnet" | "moderato" => MODERATO.clone(),
125        "dev" => DEV.clone(),
126        _ => TempoChainSpec::from_genesis(reth_cli::chainspec::parse_genesis(s)?).into(),
127    })
128}
129
130#[cfg(feature = "cli")]
131impl reth_cli::chainspec::ChainSpecParser for TempoChainSpecParser {
132    type ChainSpec = TempoChainSpec;
133
134    const SUPPORTED_CHAINS: &'static [&'static str] = SUPPORTED_CHAINS;
135
136    fn parse(s: &str) -> eyre::Result<Arc<Self::ChainSpec>> {
137        chain_value_parser(s)
138    }
139}
140
141/// Resolve a [`TempoChainSpec`] from a chain id.
142///
143/// Returns `None` for unknown chain ids.
144pub fn chainspec_from_chain_id(chain_id: u64) -> Option<Arc<TempoChainSpec>> {
145    match chain_id {
146        4217 => Some(PRESTO.clone()),
147        42431 => Some(MODERATO.clone()),
148        _ => None,
149    }
150}
151
152pub static MODERATO: LazyLock<Arc<TempoChainSpec>> = LazyLock::new(|| {
153    let genesis: Genesis = serde_json::from_str(include_str!("./genesis/moderato.json"))
154        .expect("`./genesis/moderato.json` must be present and deserializable");
155
156    TempoChainSpec::from_genesis(genesis)
157        .with_network_identity(NetworkIdentity::testnet())
158        .with_default_follow_url("wss://rpc.moderato.tempo.xyz")
159        .into()
160});
161
162pub static PRESTO: LazyLock<Arc<TempoChainSpec>> = LazyLock::new(|| {
163    let genesis: Genesis = serde_json::from_str(include_str!("./genesis/presto.json"))
164        .expect("`./genesis/presto.json` must be present and deserializable");
165
166    TempoChainSpec::from_genesis(genesis)
167        .with_network_identity(NetworkIdentity::mainnet())
168        .with_default_follow_url("wss://rpc.presto.tempo.xyz")
169        .into()
170});
171
172/// Development chainspec with funded dev accounts and activated tempo hardforks
173///
174/// `cargo x generate-genesis -o dev.json --accounts 10 --no-dkg-in-genesis`
175pub static DEV: LazyLock<Arc<TempoChainSpec>> = LazyLock::new(|| {
176    let genesis: Genesis = serde_json::from_str(include_str!("./genesis/dev.json"))
177        .expect("`./genesis/dev.json` must be present and deserializable");
178
179    TempoChainSpec::from_genesis(genesis).into()
180});
181
182/// Tempo chain spec type.
183#[derive(Debug, Clone, PartialEq, Eq)]
184pub struct TempoChainSpec {
185    /// [`ChainSpec`].
186    pub inner: ChainSpec<TempoHeader>,
187    pub info: TempoGenesisInfo,
188    /// Consensus network identity derived from genesis
189    pub network_identity: Option<NetworkIdentity>,
190    /// Default RPC URL for following this chain.
191    pub default_follow_url: Option<&'static str>,
192}
193
194impl TempoChainSpec {
195    /// Returns the default RPC URL for following this chain.
196    pub fn default_follow_url(&self) -> Option<&'static str> {
197        self.default_follow_url
198    }
199
200    /// Converts the given [`Genesis`] into a [`TempoChainSpec`].
201    pub fn from_genesis(genesis: Genesis) -> Self {
202        // Extract Tempo genesis info from extra_fields
203        let info = TempoGenesisInfo::extract_from(&genesis);
204
205        // Create base chainspec from genesis (already has ordered Ethereum hardforks)
206        let mut base_spec = ChainSpec::from_genesis(genesis);
207
208        let tempo_forks = TempoHardfork::VARIANTS.iter().filter_map(|&fork| {
209            info.fork_time(fork)
210                .map(|time| (fork, ForkCondition::Timestamp(time)))
211        });
212
213        base_spec.hardforks.extend(tempo_forks);
214
215        let inner = base_spec.map_header(|inner| TempoHeader {
216            general_gas_limit: 0,
217            timestamp_millis_part: inner.timestamp % 1000,
218            shared_gas_limit: 0,
219            consensus_context: None,
220            inner,
221        });
222
223        // TODO(hamdi): Dev networks are allowed to have a non-dkg outcome in extra data. Update such
224        // that we always require a valid dkg outcome, thus network identity for all networks
225        let network_identity =
226            NetworkIdentity::from_extra_data(inner.genesis_header().inner.extra_data.as_ref()).ok();
227
228        Self {
229            inner,
230            info,
231            network_identity,
232            default_follow_url: None,
233        }
234    }
235
236    /// Sets the compiled consensus network identity for this chain.
237    pub fn with_network_identity(mut self, identity: NetworkIdentity) -> Self {
238        self.network_identity = Some(identity);
239        self
240    }
241
242    /// Sets the default follow URL for this chain spec.
243    pub fn with_default_follow_url(mut self, url: &'static str) -> Self {
244        self.default_follow_url = Some(url);
245        self
246    }
247
248    /// Returns the moderato chainspec.
249    pub fn moderato() -> Self {
250        MODERATO.as_ref().clone()
251    }
252
253    /// Returns the mainnet chainspec.
254    pub fn mainnet() -> Self {
255        PRESTO.as_ref().clone()
256    }
257}
258
259// Required by reth's e2e-test-utils for integration tests.
260// The test utilities need to convert from standard ChainSpec to custom chain specs.
261impl From<ChainSpec> for TempoChainSpec {
262    fn from(spec: ChainSpec) -> Self {
263        let inner = spec.map_header(|inner| TempoHeader {
264            general_gas_limit: 0,
265            timestamp_millis_part: inner.timestamp % 1000,
266            shared_gas_limit: 0,
267            consensus_context: None,
268            inner,
269        });
270
271        let network_identity =
272            NetworkIdentity::from_extra_data(inner.genesis_header().inner.extra_data.as_ref()).ok();
273
274        Self {
275            inner,
276            info: TempoGenesisInfo::default(),
277            network_identity,
278            default_follow_url: None,
279        }
280    }
281}
282
283impl Hardforks for TempoChainSpec {
284    fn fork<H: Hardfork>(&self, fork: H) -> ForkCondition {
285        self.inner.fork(fork)
286    }
287
288    fn forks_iter(&self) -> impl Iterator<Item = (&dyn Hardfork, ForkCondition)> {
289        self.inner.forks_iter()
290    }
291
292    fn fork_id(&self, head: &Head) -> ForkId {
293        self.inner.fork_id(head)
294    }
295
296    fn latest_fork_id(&self) -> ForkId {
297        self.inner.latest_fork_id()
298    }
299
300    fn fork_filter(&self, head: Head) -> ForkFilter {
301        self.inner.fork_filter(head)
302    }
303}
304
305impl EthChainSpec for TempoChainSpec {
306    type Header = TempoHeader;
307
308    fn chain(&self) -> Chain {
309        self.inner.chain()
310    }
311
312    fn base_fee_params_at_timestamp(&self, timestamp: u64) -> BaseFeeParams {
313        self.inner.base_fee_params_at_timestamp(timestamp)
314    }
315
316    fn blob_params_at_timestamp(&self, timestamp: u64) -> Option<BlobParams> {
317        self.inner.blob_params_at_timestamp(timestamp)
318    }
319
320    fn deposit_contract(&self) -> Option<&DepositContract> {
321        self.inner.deposit_contract()
322    }
323
324    fn genesis_hash(&self) -> B256 {
325        self.inner.genesis_hash()
326    }
327
328    fn prune_delete_limit(&self) -> usize {
329        self.inner.prune_delete_limit()
330    }
331
332    fn display_hardforks(&self) -> Box<dyn core::fmt::Display> {
333        // filter only tempo hardforks
334        let tempo_forks = self.inner.hardforks.forks_iter().filter(|(fork, _)| {
335            !EthereumHardfork::VARIANTS
336                .iter()
337                .any(|h| h.name() == (*fork).name())
338        });
339
340        Box::new(DisplayHardforks::new(tempo_forks))
341    }
342
343    fn genesis_header(&self) -> &Self::Header {
344        self.inner.genesis_header()
345    }
346
347    fn genesis(&self) -> &Genesis {
348        self.inner.genesis()
349    }
350
351    fn bootnodes(&self) -> Option<Vec<NodeRecord>> {
352        match self.inner.chain_id() {
353            4217 => Some(presto_nodes()),
354            42431 => Some(moderato_nodes()),
355            _ => self.inner.bootnodes(),
356        }
357    }
358
359    fn final_paris_total_difficulty(&self) -> Option<U256> {
360        self.inner.get_final_paris_total_difficulty()
361    }
362
363    fn next_block_base_fee(&self, parent: &TempoHeader, target_timestamp: u64) -> Option<u64> {
364        let target_fork = self.tempo_hardfork_at(target_timestamp);
365
366        if target_fork.is_t7() {
367            let parent_base_fee = parent
368                .inner
369                .base_fee_per_gas
370                .expect("tempo blocks are expected to have a base fee");
371            Some(tempo_t7_next_block_base_fee(
372                parent_base_fee,
373                parent.inner.gas_used,
374            ))
375        } else if target_fork.is_t1() {
376            Some(TEMPO_T1_BASE_FEE)
377        } else {
378            Some(TEMPO_T0_BASE_FEE)
379        }
380    }
381}
382
383impl EthereumHardforks for TempoChainSpec {
384    fn ethereum_fork_activation(&self, fork: EthereumHardfork) -> ForkCondition {
385        self.inner.ethereum_fork_activation(fork)
386    }
387}
388
389impl EthExecutorSpec for TempoChainSpec {
390    fn deposit_contract_address(&self) -> Option<Address> {
391        self.inner.deposit_contract_address()
392    }
393}
394
395impl TempoHardforks for TempoChainSpec {
396    fn tempo_fork_activation(&self, fork: TempoHardfork) -> ForkCondition {
397        self.fork(fork)
398    }
399}
400
401#[cfg(test)]
402mod tests {
403    use crate::{
404        hardfork::{TempoHardfork, TempoHardforks},
405        spec::{TEMPO_T1_BASE_FEE, TEMPO_T7_BASE_FEE_CAP, TEMPO_T7_BASE_FEE_FLOOR},
406    };
407    use alloy_primitives::hex;
408    use commonware_codec::Encode as _;
409    use reth_chainspec::EthChainSpec;
410    #[cfg(feature = "cli")]
411    use reth_chainspec::{ForkCondition, Hardforks};
412    #[cfg(feature = "cli")]
413    use reth_cli::chainspec::ChainSpecParser as _;
414    use tempo_primitives::Header;
415
416    #[test]
417    #[cfg(feature = "cli")]
418    fn can_load_testnet() {
419        let _ = super::TempoChainSpecParser::parse("testnet")
420            .expect("the testnet chainspec must always be well formed");
421    }
422
423    #[test]
424    #[cfg(feature = "cli")]
425    fn can_load_dev() {
426        let _ = super::TempoChainSpecParser::parse("dev")
427            .expect("the dev chainspec must always be well formed");
428    }
429
430    #[test]
431    #[cfg(feature = "cli")]
432    fn test_tempo_chainspec_has_tempo_hardforks() {
433        let chainspec = super::TempoChainSpecParser::parse("mainnet")
434            .expect("the mainnet chainspec must always be well formed");
435
436        // Genesis should be active at timestamp 0
437        let activation = chainspec.tempo_fork_activation(TempoHardfork::Genesis);
438        assert_eq!(activation, ForkCondition::Timestamp(0));
439
440        // T0 should be active at timestamp 0
441        let activation = chainspec.tempo_fork_activation(TempoHardfork::T0);
442        assert_eq!(activation, ForkCondition::Timestamp(0));
443    }
444
445    #[test]
446    #[cfg(feature = "cli")]
447    fn test_tempo_chainspec_implements_tempo_hardforks_trait() {
448        let chainspec = super::TempoChainSpecParser::parse("mainnet")
449            .expect("the mainnet chainspec must always be well formed");
450
451        // Should be able to query Tempo hardfork activation through trait
452        let activation = chainspec.tempo_fork_activation(TempoHardfork::T0);
453        assert_eq!(activation, ForkCondition::Timestamp(0));
454    }
455
456    #[test]
457    fn network_identity_defaults_to_genesis_extra_data() {
458        let genesis: alloy_genesis::Genesis =
459            serde_json::from_str(include_str!("./genesis/presto.json"))
460                .expect("the mainnet genesis must always be well formed");
461
462        let chainspec = super::TempoChainSpec::from_genesis(genesis);
463        let identity = chainspec
464            .network_identity
465            .expect("presto genesis contains a DKG outcome");
466
467        assert_eq!(identity.from_epoch, 0);
468        assert_eq!(
469            identity.identity.encode().as_ref(),
470            &hex!(
471                "0xa217bb85001d4dcf8e5c50136f77af88cb2cab1857279b91c6240f41cca95c4f"
472                "43f6dcab3e0dfb87dafb3ecbeb6251e90a5df2e6c47432482821cd8b84665ee4"
473                "642589d2d9628a92b03e2bbfb00e006d038cd98def76d2a41b7c228c05f5a193"
474            )
475        );
476    }
477
478    #[test]
479    #[cfg(feature = "cli")]
480    fn named_network_identities_use_compiled_identities() {
481        let moderato = super::TempoChainSpecParser::parse("testnet")
482            .expect("the moderato chainspec must always be well formed");
483        assert_eq!(
484            moderato.network_identity,
485            Some(super::NetworkIdentity::testnet())
486        );
487
488        assert_eq!(
489            moderato.network_identity,
490            Some(super::NetworkIdentity::testnet())
491        );
492
493        let presto = super::TempoChainSpecParser::parse("mainnet")
494            .expect("the mainnet chainspec must always be well formed");
495        assert_eq!(
496            presto.network_identity,
497            Some(super::NetworkIdentity::mainnet())
498        );
499    }
500
501    #[test]
502    fn network_identity_is_absent_without_genesis_dkg_outcome() {
503        let genesis: alloy_genesis::Genesis = serde_json::from_value(serde_json::json!({
504            "config": { "chainId": 1234 },
505            "alloc": {}
506        }))
507        .unwrap();
508
509        let chainspec = super::TempoChainSpec::from_genesis(genesis);
510        assert!(chainspec.network_identity.is_none());
511    }
512
513    #[test]
514    #[cfg(feature = "cli")]
515    fn test_tempo_hardforks_in_inner_hardforks() {
516        let chainspec = super::TempoChainSpecParser::parse("mainnet")
517            .expect("the mainnet chainspec must always be well formed");
518
519        // Tempo hardforks should be queryable from inner.hardforks via Hardforks trait
520        let activation = chainspec.fork(TempoHardfork::T0);
521        assert_eq!(activation, ForkCondition::Timestamp(0));
522
523        // Verify Genesis appears in forks iterator
524        let has_genesis = chainspec
525            .forks_iter()
526            .any(|(fork, _)| fork.name() == "Genesis");
527        assert!(has_genesis, "Genesis hardfork should be in inner.hardforks");
528    }
529
530    #[test]
531    fn test_from_genesis_with_hardforks_at_zero() {
532        use alloy_genesis::Genesis;
533
534        // Build genesis config with every post-Genesis fork at timestamp 0
535        let mut config = serde_json::Map::new();
536        config.insert("chainId".into(), 1234.into());
537        for &fork in TempoHardfork::VARIANTS {
538            if fork != TempoHardfork::Genesis {
539                let key = format!("{}Time", fork.name().to_lowercase());
540                config.insert(key, 0.into());
541            }
542        }
543        let json = serde_json::json!({ "config": config, "alloc": {} });
544        let genesis: Genesis = serde_json::from_value(json).unwrap();
545        let chainspec = super::TempoChainSpec::from_genesis(genesis);
546
547        // Every fork should be active at any timestamp
548        for &fork in TempoHardfork::VARIANTS {
549            assert!(
550                chainspec.tempo_fork_activation(fork).active_at_timestamp(0),
551                "{fork:?} should be active at timestamp 0"
552            );
553            assert!(
554                chainspec
555                    .tempo_fork_activation(fork)
556                    .active_at_timestamp(1000),
557                "{fork:?} should be active at timestamp 1000"
558            );
559        }
560
561        // tempo_hardfork_at should return the latest fork
562        let latest = *TempoHardfork::VARIANTS.last().unwrap();
563        assert_eq!(chainspec.tempo_hardfork_at(0), latest);
564        assert_eq!(chainspec.tempo_hardfork_at(1000), latest);
565        assert_eq!(chainspec.tempo_hardfork_at(u64::MAX), latest);
566    }
567
568    fn header(timestamp: u64, base_fee: u64, gas_used: u64) -> super::TempoHeader {
569        super::TempoHeader {
570            inner: Header {
571                timestamp,
572                base_fee_per_gas: Some(base_fee),
573                gas_used,
574                ..Default::default()
575            },
576            ..Default::default()
577        }
578    }
579
580    fn chainspec_with_t7_at(t7_time: u64) -> super::TempoChainSpec {
581        let genesis = serde_json::json!({
582            "config": {
583                "chainId": 99999,
584                "homesteadBlock": 0,
585                "daoForkSupport": false,
586                "eip150Block": 0,
587                "eip155Block": 0,
588                "eip158Block": 0,
589                "byzantiumBlock": 0,
590                "constantinopleBlock": 0,
591                "petersburgBlock": 0,
592                "istanbulBlock": 0,
593                "berlinBlock": 0,
594                "londonBlock": 0,
595                "mergeNetsplitBlock": 0,
596                "shanghaiTime": 0,
597                "cancunTime": 0,
598                "pragueTime": 0,
599                "osakaTime": 0,
600                "terminalTotalDifficulty": 0,
601                "terminalTotalDifficultyPassed": true,
602                "t0Time": 0,
603                "t1Time": 0,
604                "t7Time": t7_time
605            },
606            "nonce": "0x42",
607            "timestamp": "0x0",
608            "extraData": "0x",
609            "gasLimit": "0x1dcd6500",
610            "difficulty": "0x0",
611            "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
612            "coinbase": "0x0000000000000000000000000000000000000000",
613            "alloc": {}
614        });
615        let genesis: alloy_genesis::Genesis = serde_json::from_value(genesis).unwrap();
616        super::TempoChainSpec::from_genesis(genesis)
617    }
618
619    #[test]
620    fn next_block_base_fee_fixed_before_t7() {
621        let chainspec = chainspec_with_t7_at(10);
622        let parent = header(8, TEMPO_T1_BASE_FEE / 2, 0);
623
624        assert_eq!(
625            chainspec.next_block_base_fee(&parent, 9),
626            Some(TEMPO_T1_BASE_FEE)
627        );
628    }
629
630    #[test]
631    fn next_block_base_fee_seeds_cap_on_t7_activation() {
632        let chainspec = chainspec_with_t7_at(10);
633        let parent = header(9, TEMPO_T1_BASE_FEE, 0);
634
635        assert_eq!(
636            chainspec.next_block_base_fee(&parent, 10),
637            Some(TEMPO_T7_BASE_FEE_CAP)
638        );
639    }
640
641    #[test]
642    fn next_block_base_fee_adjusts_after_t7_activation() {
643        let chainspec = chainspec_with_t7_at(10);
644        let parent = header(10, TEMPO_T7_BASE_FEE_CAP, 0);
645
646        assert_eq!(
647            chainspec.next_block_base_fee(&parent, 11),
648            Some(TEMPO_T7_BASE_FEE_CAP * 7 / 8)
649        );
650    }
651
652    #[test]
653    fn next_block_base_fee_uses_parent_gas_used_after_t7_activation() {
654        let chainspec = chainspec_with_t7_at(10);
655        let parent = header(10, TEMPO_T7_BASE_FEE_FLOOR, 30_000_000);
656
657        assert_eq!(
658            chainspec.next_block_base_fee(&parent, 11),
659            Some(750_000_000)
660        );
661    }
662
663    #[cfg(feature = "cli")]
664    mod tempo_hardfork_at {
665        use super::*;
666
667        #[test]
668        fn mainnet() {
669            let cs = super::super::TempoChainSpecParser::parse("mainnet")
670                .expect("the mainnet chainspec must always be well formed");
671
672            // Before T1 activation (1770908400 = Feb 12th 2026 16:00 CET)
673            assert_eq!(cs.tempo_hardfork_at(0), TempoHardfork::T0);
674            assert_eq!(cs.tempo_hardfork_at(1000), TempoHardfork::T0);
675            assert_eq!(cs.tempo_hardfork_at(1770908399), TempoHardfork::T0);
676
677            // At and after T1/T1A activation (both activate at 1770908400)
678            assert!(cs.is_t1_active_at_timestamp(1770908400));
679            assert!(cs.is_t1a_active_at_timestamp(1770908400));
680            assert_eq!(cs.tempo_hardfork_at(1770908400), TempoHardfork::T1A);
681            assert_eq!(cs.tempo_hardfork_at(1770908401), TempoHardfork::T1A);
682
683            // Before T1B activation (1771858800 = Feb 23rd 2026 16:00 CET)
684            assert!(!cs.is_t1b_active_at_timestamp(1771858799));
685            assert_eq!(cs.tempo_hardfork_at(1771858799), TempoHardfork::T1A);
686
687            // At and after T1B activation
688            assert!(cs.is_t1b_active_at_timestamp(1771858800));
689            assert_eq!(cs.tempo_hardfork_at(1771858800), TempoHardfork::T1B);
690
691            // Before T1C activation (1773327600 = Mar 12th 2026 16:00 CET)
692            assert!(!cs.is_t1c_active_at_timestamp(1773327599));
693            assert_eq!(cs.tempo_hardfork_at(1773327599), TempoHardfork::T1B);
694
695            // At and after T1C activation
696            assert!(cs.is_t1c_active_at_timestamp(1773327600));
697            assert_eq!(cs.tempo_hardfork_at(1773327600), TempoHardfork::T1C);
698
699            // Before T2 activation (1774965600 = Mar 31st 2026 16:00 CEST)
700            assert!(!cs.is_t2_active_at_timestamp(1774965599));
701            assert_eq!(cs.tempo_hardfork_at(1774965599), TempoHardfork::T1C);
702
703            // At and after T2 activation
704            assert!(cs.is_t2_active_at_timestamp(1774965600));
705            assert_eq!(cs.tempo_hardfork_at(1774965600), TempoHardfork::T2);
706
707            // Before T3 activation (1777298400 = Apr 27th 2026 16:00 CEST)
708            assert!(!cs.is_t3_active_at_timestamp(1777298399));
709            assert_eq!(cs.tempo_hardfork_at(1777298399), TempoHardfork::T2);
710
711            // At and after T3 activation
712            assert!(cs.is_t3_active_at_timestamp(1777298400));
713            assert_eq!(cs.tempo_hardfork_at(1777298400), TempoHardfork::T3);
714
715            // Before T4 activation (1779112800 = May 18th 2026 16:00 CEST)
716            assert!(!cs.is_t4_active_at_timestamp(1779112799));
717            assert_eq!(cs.tempo_hardfork_at(1779112799), TempoHardfork::T3);
718
719            // At and after T4 activation
720            assert!(cs.is_t4_active_at_timestamp(1779112800));
721            assert_eq!(cs.tempo_hardfork_at(1779112800), TempoHardfork::T4);
722
723            // Before T5 activation (1781013600 = Jun 9th 2026 16:00 CEST)
724            assert!(!cs.is_t5_active_at_timestamp(1781013599));
725            assert_eq!(cs.tempo_hardfork_at(1781013599), TempoHardfork::T4);
726
727            // At and after T5 activation
728            assert!(cs.is_t5_active_at_timestamp(1781013600));
729            assert_eq!(cs.tempo_hardfork_at(1781013600), TempoHardfork::T5);
730            assert!(!cs.is_t6_active_at_timestamp(1781013600));
731
732            // Before T6 activation (1782223200 = Jun 23rd 2026 16:00 CEST)
733            assert!(!cs.is_t6_active_at_timestamp(1782223199));
734            assert_eq!(cs.tempo_hardfork_at(1782223199), TempoHardfork::T5);
735
736            // At and after T6 activation
737            assert!(cs.is_t6_active_at_timestamp(1782223200));
738            assert_eq!(cs.tempo_hardfork_at(1782223200), TempoHardfork::T6);
739            assert_eq!(cs.tempo_hardfork_at(u64::MAX), TempoHardfork::T6);
740        }
741
742        #[test]
743        fn moderato() {
744            let cs = super::super::TempoChainSpecParser::parse("moderato")
745                .expect("the moderato chainspec must always be well formed");
746
747            // Before T0/T1 activation (1770303600 = Feb 5th 2026 16:00 CET)
748            assert_eq!(cs.tempo_hardfork_at(0), TempoHardfork::Genesis);
749            assert_eq!(cs.tempo_hardfork_at(1770303599), TempoHardfork::Genesis);
750
751            // At and after T0/T1 activation
752            assert_eq!(cs.tempo_hardfork_at(1770303600), TempoHardfork::T1);
753            assert_eq!(cs.tempo_hardfork_at(1770303601), TempoHardfork::T1);
754
755            // Before T1A/T1B activation (1771858800 = Feb 23rd 2026 16:00 CET)
756            assert_eq!(cs.tempo_hardfork_at(1771858799), TempoHardfork::T1);
757
758            // At and after T1A/T1B activation (both activate at 1771858800)
759            assert!(cs.is_t1a_active_at_timestamp(1771858800));
760            assert!(cs.is_t1b_active_at_timestamp(1771858800));
761            assert_eq!(cs.tempo_hardfork_at(1771858800), TempoHardfork::T1B);
762
763            // Before T1C activation (1773068400 = Mar 9th 2026 16:00 CET)
764            assert!(!cs.is_t1c_active_at_timestamp(1773068399));
765            assert_eq!(cs.tempo_hardfork_at(1773068399), TempoHardfork::T1B);
766
767            // At and after T1C activation
768            assert!(cs.is_t1c_active_at_timestamp(1773068400));
769            assert_eq!(cs.tempo_hardfork_at(1773068400), TempoHardfork::T1C);
770
771            // Before T2 activation (1774537200 = Mar 26th 2026 16:00 CET)
772            assert!(!cs.is_t2_active_at_timestamp(1774537199));
773            assert_eq!(cs.tempo_hardfork_at(1774537199), TempoHardfork::T1C);
774
775            // At and after T2 activation
776            assert!(cs.is_t2_active_at_timestamp(1774537200));
777            assert_eq!(cs.tempo_hardfork_at(1774537200), TempoHardfork::T2);
778
779            // Before T3 activation (1776780000 = Apr 21st 2026 16:00 CEST)
780            assert!(!cs.is_t3_active_at_timestamp(1776779999));
781            assert_eq!(cs.tempo_hardfork_at(1776779999), TempoHardfork::T2);
782
783            // At and after T3 activation
784            assert!(cs.is_t3_active_at_timestamp(1776780000));
785            assert_eq!(cs.tempo_hardfork_at(1776780000), TempoHardfork::T3);
786
787            // Before T4 activation (1778767200 = May 14th 2026 16:00 CEST)
788            assert!(!cs.is_t4_active_at_timestamp(1778767199));
789            assert_eq!(cs.tempo_hardfork_at(1778767199), TempoHardfork::T3);
790
791            // At and after T4 activation
792            assert!(cs.is_t4_active_at_timestamp(1778767200));
793            assert_eq!(cs.tempo_hardfork_at(1778767200), TempoHardfork::T4);
794
795            // Before T5 activation (1780495200 = Jun 3rd 2026 16:00 CEST)
796            assert!(!cs.is_t5_active_at_timestamp(1780495199));
797            assert_eq!(cs.tempo_hardfork_at(1780495199), TempoHardfork::T4);
798
799            // At and after T5 activation
800            assert!(cs.is_t5_active_at_timestamp(1780495200));
801            assert_eq!(cs.tempo_hardfork_at(1780495200), TempoHardfork::T5);
802            assert!(!cs.is_t6_active_at_timestamp(1780495200));
803
804            // Before T6 activation (1781791200 = Jun 18th 2026 16:00 CEST)
805            assert!(!cs.is_t6_active_at_timestamp(1781791199));
806            assert_eq!(cs.tempo_hardfork_at(1781791199), TempoHardfork::T5);
807
808            // At and after T6 activation
809            assert!(cs.is_t6_active_at_timestamp(1781791200));
810            assert_eq!(cs.tempo_hardfork_at(1781791200), TempoHardfork::T6);
811            assert_eq!(cs.tempo_hardfork_at(u64::MAX), TempoHardfork::T6);
812        }
813
814        #[test]
815        fn testnet() {
816            let cs = super::super::TempoChainSpecParser::parse("testnet")
817                .expect("the testnet chainspec must always be well formed");
818
819            // "testnet" is an alias for moderato
820            let moderato = super::super::TempoChainSpecParser::parse("moderato")
821                .expect("the moderato chainspec must always be well formed");
822            assert_eq!(cs.inner.chain(), moderato.inner.chain());
823        }
824    }
825
826    #[test]
827    #[cfg(feature = "cli")]
828    #[allow(clippy::expect_fun_call)]
829    fn chainspec_from_chain_id_roundtrips_supported_chains() {
830        use reth_chainspec::EthChainSpec;
831
832        for &name in super::SUPPORTED_CHAINS {
833            let spec =
834                super::chain_value_parser(name).expect(&format!("failed to parse chain `{name}`"));
835
836            let resolved = super::chainspec_from_chain_id(spec.chain().id())
837                .expect(&format!("failed to parse chain `{name}`"));
838
839            assert_eq!(spec.chain(), resolved.chain(), "chain mismatch for {name}");
840        }
841    }
842}