tempo_chainspec/
spec.rs

1use crate::{
2    bootnodes::andantino_nodes,
3    hardfork::{TempoHardfork, TempoHardforks},
4};
5use alloy_eips::eip7840::BlobParams;
6use alloy_genesis::Genesis;
7use alloy_primitives::{Address, B256, U256};
8use reth_chainspec::{
9    BaseFeeParams, Chain, ChainSpec, DepositContract, DisplayHardforks, EthChainSpec,
10    EthereumHardfork, EthereumHardforks, ForkCondition, ForkFilter, ForkId, Hardfork, Hardforks,
11    Head,
12};
13use reth_cli::chainspec::{ChainSpecParser, parse_genesis};
14use reth_ethereum::evm::primitives::eth::spec::EthExecutorSpec;
15use reth_network_peers::NodeRecord;
16use std::sync::{Arc, LazyLock};
17use tempo_commonware_node_config::{Peers, PublicPolynomial};
18use tempo_primitives::TempoHeader;
19
20pub const TEMPO_BASE_FEE: u64 = 10_000_000_000;
21
22/// Tempo genesis info extracted from genesis extra_fields
23#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
24#[serde(rename_all = "camelCase")]
25pub struct TempoGenesisInfo {
26    /// Timestamp of Adagio hardfork activation
27    #[serde(skip_serializing_if = "Option::is_none")]
28    adagio_time: Option<u64>,
29
30    /// Timestamp of Andantino hardfork activation
31    #[serde(skip_serializing_if = "Option::is_none")]
32    moderato_time: Option<u64>,
33
34    /// Timestamp of Allegretto hardfork activation
35    #[serde(skip_serializing_if = "Option::is_none")]
36    allegretto_time: Option<u64>,
37
38    /// Timestamp of Allegro-Moderato hardfork activation
39    #[serde(skip_serializing_if = "Option::is_none")]
40    allegro_moderato_time: Option<u64>,
41
42    /// The epoch length used by consensus.
43    #[serde(skip_serializing_if = "Option::is_none")]
44    epoch_length: Option<u64>,
45
46    /// The public polynomial all nodes are to use at genesis.
47    #[serde(skip_serializing_if = "Option::is_none")]
48    public_polynomial: Option<PublicPolynomial>,
49
50    /// The initial set of peers.
51    #[serde(skip_serializing_if = "Option::is_none")]
52    validators: Option<Peers>,
53}
54
55impl TempoGenesisInfo {
56    /// Extract Tempo genesis info from genesis extra_fields
57    fn extract_from(genesis: &Genesis) -> Self {
58        genesis
59            .config
60            .extra_fields
61            .deserialize_as::<Self>()
62            .unwrap_or_default()
63    }
64
65    pub fn epoch_length(&self) -> Option<u64> {
66        self.epoch_length
67    }
68
69    pub fn public_polynomial(&self) -> &Option<PublicPolynomial> {
70        &self.public_polynomial
71    }
72
73    pub fn validators(&self) -> &Option<Peers> {
74        &self.validators
75    }
76}
77
78/// Tempo chain specification parser.
79#[derive(Debug, Clone, Default)]
80pub struct TempoChainSpecParser;
81
82/// Chains supported by Tempo. First value should be used as the default.
83pub const SUPPORTED_CHAINS: &[&str] = &["testnet"];
84
85/// Clap value parser for [`ChainSpec`]s.
86///
87/// The value parser matches either a known chain, the path
88/// to a json file, or a json formatted string in-memory. The json needs to be a Genesis struct.
89pub fn chain_value_parser(s: &str) -> eyre::Result<Arc<TempoChainSpec>> {
90    Ok(match s {
91        "testnet" => ANDANTINO.clone(),
92        "dev" => DEV.clone(),
93        _ => TempoChainSpec::from_genesis(parse_genesis(s)?).into(),
94    })
95}
96
97impl ChainSpecParser for TempoChainSpecParser {
98    type ChainSpec = TempoChainSpec;
99
100    const SUPPORTED_CHAINS: &'static [&'static str] = SUPPORTED_CHAINS;
101
102    fn parse(s: &str) -> eyre::Result<Arc<Self::ChainSpec>> {
103        chain_value_parser(s)
104    }
105}
106
107pub static ANDANTINO: LazyLock<Arc<TempoChainSpec>> = LazyLock::new(|| {
108    let genesis: Genesis = serde_json::from_str(include_str!("./genesis/andantino.json"))
109        .expect("`./genesis/andantino.json` must be present and deserializable");
110    TempoChainSpec::from_genesis(genesis).into()
111});
112
113/// Development chainspec with funded dev accounts and activated tempo hardforks
114///
115/// `cargo x generate-genesis -o dev.json --accounts 10`
116pub static DEV: LazyLock<Arc<TempoChainSpec>> = LazyLock::new(|| {
117    let genesis: Genesis = serde_json::from_str(include_str!("./genesis/dev.json"))
118        .expect("`./genesis/dev.json` must be present and deserializable");
119    TempoChainSpec::from_genesis(genesis).into()
120});
121
122/// Tempo chain spec type.
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct TempoChainSpec {
125    /// [`ChainSpec`].
126    pub inner: ChainSpec<TempoHeader>,
127    pub info: TempoGenesisInfo,
128}
129
130impl TempoChainSpec {
131    /// Converts the given [`Genesis`] into a [`TempoChainSpec`].
132    pub fn from_genesis(genesis: Genesis) -> Self {
133        // Extract Tempo genesis info from extra_fields
134        let info @ TempoGenesisInfo {
135            adagio_time,
136            moderato_time,
137            allegretto_time,
138            allegro_moderato_time,
139            ..
140        } = TempoGenesisInfo::extract_from(&genesis);
141
142        // Create base chainspec from genesis (already has ordered Ethereum hardforks)
143        let mut base_spec = ChainSpec::from_genesis(genesis);
144
145        let tempo_forks = vec![
146            (TempoHardfork::Adagio, adagio_time),
147            (TempoHardfork::Moderato, moderato_time),
148            (TempoHardfork::Allegretto, allegretto_time),
149            (TempoHardfork::AllegroModerato, allegro_moderato_time),
150        ]
151        .into_iter()
152        .filter_map(|(fork, time)| time.map(|time| (fork, ForkCondition::Timestamp(time))));
153
154        base_spec.hardforks.extend(tempo_forks);
155
156        Self {
157            inner: base_spec.map_header(|inner| TempoHeader {
158                general_gas_limit: 0,
159                timestamp_millis_part: inner.timestamp * 1000,
160                shared_gas_limit: 0,
161                inner,
162            }),
163            info,
164        }
165    }
166}
167
168// Required by reth's e2e-test-utils for integration tests.
169// The test utilities need to convert from standard ChainSpec to custom chain specs.
170impl From<ChainSpec> for TempoChainSpec {
171    fn from(spec: ChainSpec) -> Self {
172        Self {
173            inner: spec.map_header(|inner| TempoHeader {
174                general_gas_limit: 0,
175                timestamp_millis_part: inner.timestamp * 1000,
176                inner,
177                shared_gas_limit: 0,
178            }),
179            info: TempoGenesisInfo::default(),
180        }
181    }
182}
183
184impl Hardforks for TempoChainSpec {
185    fn fork<H: Hardfork>(&self, fork: H) -> ForkCondition {
186        self.inner.fork(fork)
187    }
188
189    fn forks_iter(&self) -> impl Iterator<Item = (&dyn Hardfork, ForkCondition)> {
190        self.inner.forks_iter()
191    }
192
193    fn fork_id(&self, head: &Head) -> ForkId {
194        self.inner.fork_id(head)
195    }
196
197    fn latest_fork_id(&self) -> ForkId {
198        self.inner.latest_fork_id()
199    }
200
201    fn fork_filter(&self, head: Head) -> ForkFilter {
202        self.inner.fork_filter(head)
203    }
204}
205
206impl EthChainSpec for TempoChainSpec {
207    type Header = TempoHeader;
208
209    fn chain(&self) -> Chain {
210        self.inner.chain()
211    }
212
213    fn base_fee_params_at_timestamp(&self, timestamp: u64) -> BaseFeeParams {
214        self.inner.base_fee_params_at_timestamp(timestamp)
215    }
216
217    fn blob_params_at_timestamp(&self, timestamp: u64) -> Option<BlobParams> {
218        self.inner.blob_params_at_timestamp(timestamp)
219    }
220
221    fn deposit_contract(&self) -> Option<&DepositContract> {
222        self.inner.deposit_contract()
223    }
224
225    fn genesis_hash(&self) -> B256 {
226        self.inner.genesis_hash()
227    }
228
229    fn prune_delete_limit(&self) -> usize {
230        self.inner.prune_delete_limit()
231    }
232
233    fn display_hardforks(&self) -> Box<dyn std::fmt::Display> {
234        // filter only tempo hardforks
235        let tempo_forks = self.inner.hardforks.forks_iter().filter(|(fork, _)| {
236            !EthereumHardfork::VARIANTS
237                .iter()
238                .any(|h| h.name() == (*fork).name())
239        });
240
241        Box::new(DisplayHardforks::new(tempo_forks))
242    }
243
244    fn genesis_header(&self) -> &Self::Header {
245        self.inner.genesis_header()
246    }
247
248    fn genesis(&self) -> &Genesis {
249        self.inner.genesis()
250    }
251
252    fn bootnodes(&self) -> Option<Vec<NodeRecord>> {
253        match self.inner.chain_id() {
254            42429 => Some(andantino_nodes()),
255            _ => self.inner.bootnodes(),
256        }
257    }
258
259    fn final_paris_total_difficulty(&self) -> Option<U256> {
260        self.inner.get_final_paris_total_difficulty()
261    }
262
263    fn next_block_base_fee(&self, _parent: &TempoHeader, _target_timestamp: u64) -> Option<u64> {
264        Some(TEMPO_BASE_FEE)
265    }
266}
267
268impl EthereumHardforks for TempoChainSpec {
269    fn ethereum_fork_activation(&self, fork: EthereumHardfork) -> ForkCondition {
270        self.inner.ethereum_fork_activation(fork)
271    }
272}
273
274impl EthExecutorSpec for TempoChainSpec {
275    fn deposit_contract_address(&self) -> Option<Address> {
276        self.inner.deposit_contract_address()
277    }
278}
279
280impl TempoHardforks for TempoChainSpec {
281    fn tempo_fork_activation(&self, fork: TempoHardfork) -> ForkCondition {
282        self.fork(fork)
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use crate::hardfork::{TempoHardfork, TempoHardforks};
289    use reth_chainspec::{EthereumHardfork, ForkCondition, Hardforks};
290    use reth_cli::chainspec::ChainSpecParser as _;
291    use serde_json::json;
292
293    #[test]
294    fn can_load_testnet() {
295        let _ = super::TempoChainSpecParser::parse("testnet")
296            .expect("the testnet chainspec must always be well formed");
297    }
298
299    #[test]
300    fn can_load_dev() {
301        let _ = super::TempoChainSpecParser::parse("dev")
302            .expect("the dev chainspec must always be well formed");
303    }
304
305    #[test]
306    fn test_tempo_chainspec_has_tempo_hardforks() {
307        let chainspec = super::TempoChainSpecParser::parse("testnet")
308            .expect("the testnet chainspec must always be well formed");
309
310        // Adagio should be active at genesis (timestamp 0)
311        assert!(chainspec.is_adagio_active_at_timestamp(0));
312    }
313
314    #[test]
315    fn test_tempo_chainspec_implements_tempo_hardforks_trait() {
316        let chainspec = super::TempoChainSpecParser::parse("testnet")
317            .expect("the testnet chainspec must always be well formed");
318
319        // Should be able to query Tempo hardfork activation through trait
320        let activation = chainspec.tempo_fork_activation(TempoHardfork::Adagio);
321        assert_eq!(activation, ForkCondition::Timestamp(0));
322
323        // Should be able to use convenience method through trait
324        assert!(chainspec.is_adagio_active_at_timestamp(0));
325        assert!(chainspec.is_adagio_active_at_timestamp(1000));
326    }
327
328    #[test]
329    fn test_tempo_hardforks_in_inner_hardforks() {
330        let chainspec = super::TempoChainSpecParser::parse("testnet")
331            .expect("the testnet chainspec must always be well formed");
332
333        // Tempo hardforks should be queryable from inner.hardforks via Hardforks trait
334        let activation = chainspec.fork(TempoHardfork::Adagio);
335        assert_eq!(activation, ForkCondition::Timestamp(0));
336
337        // Verify Adagio appears in forks iterator
338        let has_adagio = chainspec
339            .forks_iter()
340            .any(|(fork, _)| fork.name() == "Adagio");
341        assert!(has_adagio, "Adagio hardfork should be in inner.hardforks");
342    }
343
344    #[test]
345    fn test_parse_tempo_hardforks_from_genesis_extra_fields() {
346        // Create a genesis with Tempo hardfork timestamps as extra fields in config
347        // (non-standard fields automatically go into extra_fields)
348        let genesis_json = json!({
349            "config": {
350                "chainId": 1337,
351                "homesteadBlock": 0,
352                "eip150Block": 0,
353                "eip155Block": 0,
354                "eip158Block": 0,
355                "byzantiumBlock": 0,
356                "constantinopleBlock": 0,
357                "petersburgBlock": 0,
358                "istanbulBlock": 0,
359                "berlinBlock": 0,
360                "londonBlock": 0,
361                "mergeNetsplitBlock": 0,
362                "terminalTotalDifficulty": 0,
363                "terminalTotalDifficultyPassed": true,
364                "shanghaiTime": 0,
365                "cancunTime": 0,
366                "adagioTime": 1000,
367                "moderatoTime": 2000,
368                "allegrettoTime": 3000,
369                "allegroModeratoTime": 4000,
370            },
371            "alloc": {}
372        });
373
374        let genesis: alloy_genesis::Genesis =
375            serde_json::from_value(genesis_json).expect("genesis should be valid");
376
377        let chainspec = super::TempoChainSpec::from_genesis(genesis);
378
379        // Test Adagio activation
380        let activation = chainspec.fork(TempoHardfork::Adagio);
381        assert_eq!(
382            activation,
383            ForkCondition::Timestamp(1000),
384            "Adagio should be activated at the parsed timestamp from extra_fields"
385        );
386
387        assert!(
388            !chainspec.is_adagio_active_at_timestamp(0),
389            "Adagio should not be active before its activation timestamp"
390        );
391        assert!(
392            chainspec.is_adagio_active_at_timestamp(1000),
393            "Adagio should be active at its activation timestamp"
394        );
395        assert!(
396            chainspec.is_adagio_active_at_timestamp(2000),
397            "Adagio should be active after its activation timestamp"
398        );
399
400        // Test Moderato activation
401        let activation = chainspec.fork(TempoHardfork::Moderato);
402        assert_eq!(
403            activation,
404            ForkCondition::Timestamp(2000),
405            "Moderato should be activated at the parsed timestamp from extra_fields"
406        );
407
408        assert!(
409            !chainspec.is_moderato_active_at_timestamp(0),
410            "Moderato should not be active before its activation timestamp"
411        );
412        assert!(
413            !chainspec.is_moderato_active_at_timestamp(1000),
414            "Moderato should not be active at Adagio's activation timestamp"
415        );
416        assert!(
417            chainspec.is_moderato_active_at_timestamp(2000),
418            "Moderato should be active at its activation timestamp"
419        );
420        assert!(
421            chainspec.is_moderato_active_at_timestamp(3000),
422            "Moderato should be active after its activation timestamp"
423        );
424
425        // Test Allegretto activation
426        let activation = chainspec.fork(TempoHardfork::Allegretto);
427        assert_eq!(
428            activation,
429            ForkCondition::Timestamp(3000),
430            "Allegretto should be activated at the parsed timestamp from extra_fields"
431        );
432
433        assert!(
434            !chainspec.is_allegretto_active_at_timestamp(0),
435            "Allegretto should not be active before its activation timestamp"
436        );
437        assert!(
438            !chainspec.is_allegretto_active_at_timestamp(1000),
439            "Allegretto should not be active at Adagio's activation timestamp"
440        );
441        assert!(
442            !chainspec.is_allegretto_active_at_timestamp(2000),
443            "Allegretto should not be active at Moderato's activation timestamp"
444        );
445        assert!(
446            chainspec.is_allegretto_active_at_timestamp(3000),
447            "Allegretto should be active at its activation timestamp"
448        );
449        assert!(
450            chainspec.is_allegretto_active_at_timestamp(4000),
451            "Allegretto should be active after its activation timestamp"
452        );
453
454        // Test AllegroModerato activation
455        let activation = chainspec.fork(TempoHardfork::AllegroModerato);
456        assert_eq!(
457            activation,
458            ForkCondition::Timestamp(4000),
459            "AllegroModerato should be activated at the parsed timestamp from extra_fields"
460        );
461
462        assert!(
463            !chainspec.is_allegro_moderato_active_at_timestamp(0),
464            "AllegroModerato should not be active before its activation timestamp"
465        );
466        assert!(
467            !chainspec.is_allegro_moderato_active_at_timestamp(1000),
468            "AllegroModerato should not be active at Adagio's activation timestamp"
469        );
470        assert!(
471            !chainspec.is_allegro_moderato_active_at_timestamp(2000),
472            "AllegroModerato should not be active at Moderato's activation timestamp"
473        );
474        assert!(
475            !chainspec.is_allegro_moderato_active_at_timestamp(3000),
476            "AllegroModerato should not be active at Allegretto's activation timestamp"
477        );
478        assert!(
479            chainspec.is_allegro_moderato_active_at_timestamp(4000),
480            "AllegroModerato should be active at its activation timestamp"
481        );
482        assert!(
483            chainspec.is_allegro_moderato_active_at_timestamp(5000),
484            "AllegroModerato should be active after its activation timestamp"
485        );
486    }
487
488    #[test]
489    fn test_tempo_hardforks_are_ordered_correctly() {
490        // Create a genesis where Adagio should appear between Shanghai (time 0) and Cancun (time 2000)
491        let genesis_json = json!({
492            "config": {
493                "chainId": 1337,
494                "homesteadBlock": 0,
495                "eip150Block": 0,
496                "eip155Block": 0,
497                "eip158Block": 0,
498                "byzantiumBlock": 0,
499                "constantinopleBlock": 0,
500                "petersburgBlock": 0,
501                "istanbulBlock": 0,
502                "berlinBlock": 0,
503                "londonBlock": 0,
504                "mergeNetsplitBlock": 0,
505                "terminalTotalDifficulty": 0,
506                "terminalTotalDifficultyPassed": true,
507                "shanghaiTime": 0,
508                "cancunTime": 2000,
509                "adagioTime": 1000,
510            },
511            "alloc": {}
512        });
513
514        let genesis: alloy_genesis::Genesis =
515            serde_json::from_value(genesis_json).expect("genesis should be valid");
516
517        let chainspec = super::TempoChainSpec::from_genesis(genesis);
518
519        // Collect forks in order
520        let forks: Vec<_> = chainspec.inner.hardforks.forks_iter().collect();
521
522        // Find positions of Shanghai, Adagio, and Cancun
523        let shanghai_pos = forks
524            .iter()
525            .position(|(f, _)| f.name() == EthereumHardfork::Shanghai.name());
526        let adagio_pos = forks
527            .iter()
528            .position(|(f, _)| f.name() == TempoHardfork::Adagio.name());
529        let cancun_pos = forks
530            .iter()
531            .position(|(f, _)| f.name() == EthereumHardfork::Cancun.name());
532
533        assert!(shanghai_pos.is_some(), "Shanghai should be present");
534        assert!(adagio_pos.is_some(), "Adagio should be present");
535        assert!(cancun_pos.is_some(), "Cancun should be present");
536
537        // Verify ordering: Shanghai (0) < Adagio (1000) < Cancun (2000)
538        assert!(
539            shanghai_pos.unwrap() < adagio_pos.unwrap(),
540            "Shanghai (time 0) should come before Adagio (time 1000), but got positions {} and {}",
541            shanghai_pos.unwrap(),
542            adagio_pos.unwrap()
543        );
544        assert!(
545            adagio_pos.unwrap() < cancun_pos.unwrap(),
546            "Adagio (time 1000) should come before Cancun (time 2000), but got positions {} and {}",
547            adagio_pos.unwrap(),
548            cancun_pos.unwrap()
549        );
550    }
551
552    #[test]
553    fn test_tempo_hardfork_at() {
554        // Create a genesis with specific timestamps for each hardfork
555        let genesis_json = json!({
556            "config": {
557                "chainId": 1337,
558                "homesteadBlock": 0,
559                "eip150Block": 0,
560                "eip155Block": 0,
561                "eip158Block": 0,
562                "byzantiumBlock": 0,
563                "constantinopleBlock": 0,
564                "petersburgBlock": 0,
565                "istanbulBlock": 0,
566                "berlinBlock": 0,
567                "londonBlock": 0,
568                "mergeNetsplitBlock": 0,
569                "terminalTotalDifficulty": 0,
570                "terminalTotalDifficultyPassed": true,
571                "shanghaiTime": 0,
572                "cancunTime": 0,
573                "adagioTime": 1000,
574                "moderatoTime": 2000,
575                "allegrettoTime": 3000,
576                "allegroModeratoTime": 4000
577            },
578            "alloc": {}
579        });
580
581        let genesis: alloy_genesis::Genesis =
582            serde_json::from_value(genesis_json).expect("genesis should be valid");
583
584        let chainspec = super::TempoChainSpec::from_genesis(genesis);
585
586        // Before Adagio activation - should return Adagio (it's the baseline)
587        assert_eq!(
588            chainspec.tempo_hardfork_at(0),
589            TempoHardfork::Adagio,
590            "Should return Adagio at timestamp 0"
591        );
592
593        // At Adagio time
594        assert_eq!(
595            chainspec.tempo_hardfork_at(1000),
596            TempoHardfork::Adagio,
597            "Should return Adagio at its activation time"
598        );
599
600        // Between Adagio and Moderato
601        assert_eq!(
602            chainspec.tempo_hardfork_at(1500),
603            TempoHardfork::Adagio,
604            "Should return Adagio between Adagio and Moderato activation"
605        );
606
607        // At Moderato time
608        assert_eq!(
609            chainspec.tempo_hardfork_at(2000),
610            TempoHardfork::Moderato,
611            "Should return Moderato at its activation time"
612        );
613
614        // Between Moderato and Allegretto
615        assert_eq!(
616            chainspec.tempo_hardfork_at(2500),
617            TempoHardfork::Moderato,
618            "Should return Moderato between Moderato and Allegretto activation"
619        );
620
621        // At Allegretto time
622        assert_eq!(
623            chainspec.tempo_hardfork_at(3000),
624            TempoHardfork::Allegretto,
625            "Should return Allegretto at its activation time"
626        );
627
628        // Between Allegretto and AllegroModerato
629        assert_eq!(
630            chainspec.tempo_hardfork_at(3500),
631            TempoHardfork::Allegretto,
632            "Should return Allegretto between Allegretto and AllegroModerato activation"
633        );
634
635        // At AllegroModerato time
636        assert_eq!(
637            chainspec.tempo_hardfork_at(4000),
638            TempoHardfork::AllegroModerato,
639            "Should return AllegroModerato at its activation time"
640        );
641
642        // After AllegroModerato
643        assert_eq!(
644            chainspec.tempo_hardfork_at(5000),
645            TempoHardfork::AllegroModerato,
646            "Should return AllegroModerato after its activation time"
647        );
648    }
649}