Skip to main content

tempo_chainspec/
spec.rs

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