Skip to main content

tempo_hardfork/
lib.rs

1//! Tempo-specific hardfork definitions, activation schedules, and protocol constants.
2//!
3//! This crate is the lightweight source of truth for Tempo hardfork identifiers. It intentionally
4//! does not depend on `tempo-chainspec` or Reth, so SDK crates can use [`TempoHardfork`] without
5//! pulling in chain-spec/node integration.
6//!
7//! ## Adding a New Hardfork
8//!
9//! When a new hardfork is needed (e.g., `Vivace`):
10//!
11//! ### In `tempo-hardfork`
12//! 1. Append a `Vivace` variant to `tempo_hardfork!` — automatically:
13//!    * defines the enum variant via [`hardfork!`]
14//!    * adds the variant to [`TempoHardfork::VARIANTS`]
15//!    * generates the `is_vivace()` inherent helper
16//!    * exports the variant through [`tempo_post_genesis_hardforks!`] for downstream generated APIs
17//!    * adds tests for the generated hardfork helpers
18//! 2. Update activation schedule methods/constants for the new fork.
19//! 3. Update `From<TempoHardfork> for SpecId` if the hardfork requires a different Ethereum
20//!    `SpecId`.
21//!
22//! ### In `tempo-chainspec`
23//! 4. Add `vivace_time: Option<u64>` field to `TempoGenesisInfo` if the fork is configurable in
24//!    genesis. `fork_time()` is generated through [`tempo_post_genesis_hardforks!`], so missing
25//!    fields for new hardfork variants fail at compile time.
26//!
27//! ### In genesis files and generator
28//! 5. Add `"vivaceTime": 0` to `genesis/dev.json`.
29//! 6. Add `vivace_time: Option<u64>` arg to `xtask/src/genesis_args.rs`.
30//! 7. Add insertion of `"vivaceTime"` to `chain_config.extra_fields`.
31
32#![cfg_attr(not(feature = "std"), no_std)]
33
34pub mod constants;
35
36use crate::constants::gas;
37use alloy_eips::eip7825::MAX_TX_GAS_LIMIT_OSAKA;
38#[cfg(feature = "evm")]
39use alloy_evm::revm::primitives::hardfork::SpecId;
40use alloy_hardforks::hardfork;
41
42/// Single-source hardfork definition macro. Append a new variant and everything else is generated:
43///
44/// * Defines the `TempoHardfork` enum via [`hardfork!`] (including `Display`, `FromStr`,
45///   `Hardfork` trait impl, and `VARIANTS` const)
46/// * Generates `is_<fork>()` inherent methods on `TempoHardfork` — returns `true` when
47///   `*self >= Self::<Fork>`
48/// * Generates the `TempoHardforks` trait with:
49///   - `tempo_fork_activation()` (required — the only method implementors provide)
50///   - `tempo_hardfork_at()` — walks `VARIANTS` in reverse to find the latest active fork
51///   - `is_<fork>_active_at_timestamp()` — per-fork convenience helpers
52///   - `shared_gas_limit_at()` — shared gas limit lookup by timestamp
53/// * Generates a `#[cfg(test)] mod tests` with activation, naming, trait, and serde tests
54///
55/// `Genesis` (first variant) is treated as the baseline and does not get `is_*()` methods.
56///  All subsequent variants are considered post-Genesis hardforks.
57macro_rules! tempo_hardfork {
58    (
59        $(#[$enum_meta:meta])*
60        TempoHardfork {
61            $(#[$genesis_meta:meta])* Genesis,
62            $( $(#[$meta:meta])* $variant:ident ),* $(,)?
63        }
64    ) => {
65
66        // delegate to alloy's `hardfork!` macro
67        hardfork!(
68            $(#[$enum_meta])*
69            TempoHardfork {
70                $(#[$genesis_meta])* Genesis,
71                $( $(#[$meta])* $variant ),*
72            }
73        );
74
75        impl TempoHardfork {
76            paste::paste! {
77                $(
78                    #[doc = concat!("Returns true if this hardfork is ", stringify!($variant), " or later.")]
79                    pub const fn [<is_ $variant:lower>](&self) -> bool {
80                        *self as u64 >= Self::$variant as u64
81                    }
82                )*
83            }
84        }
85
86        /// Invokes the given macro with all post-Genesis Tempo hardfork variants.
87        ///
88        /// This lets downstream crates generate per-hardfork APIs from the same variant list as
89        /// [`TempoHardfork`] without depending on Tempo's chainspec implementation.
90        #[macro_export]
91        macro_rules! tempo_post_genesis_hardforks {
92            ($callback:ident) => {
93                $callback!($($variant),*);
94            };
95        }
96
97        #[cfg(test)]
98        mod tests {
99            use super::*;
100            use TempoHardfork::*;
101            use alloy_hardforks::Hardfork;
102
103            #[test]
104            fn test_hardfork_name() {
105                assert_eq!(Genesis.name(), "Genesis");
106                $(assert_eq!($variant.name(), stringify!($variant));)*
107            }
108
109            #[test]
110            fn test_hardfork_trait_implementation() {
111                for fork in TempoHardfork::VARIANTS {
112                    let _name: &str = Hardfork::name(fork);
113                }
114            }
115
116            #[test]
117            fn test_variant_index_roundtrip() {
118                for fork in TempoHardfork::VARIANTS {
119                    assert_eq!(
120                        TempoHardfork::from_variant_index(fork.variant_index()),
121                        Some(*fork)
122                    );
123                }
124                assert_eq!(
125                    TempoHardfork::from_variant_index(TempoHardfork::VARIANTS.len() as u8),
126                    None
127                );
128            }
129
130            #[test]
131            #[cfg(feature = "serde")]
132            fn test_tempo_hardfork_serde() {
133                for fork in TempoHardfork::VARIANTS {
134                    let json = serde_json::to_string(fork).expect("serialize");
135                    let deserialized: TempoHardfork = serde_json::from_str(&json).expect("deserialize");
136                    assert_eq!(deserialized, *fork);
137                }
138            }
139
140            paste::paste! {
141                $(
142                    #[test]
143                    fn [<test_is_ $variant:lower>]() {
144                        let idx = TempoHardfork::VARIANTS.iter().position(|v| *v == $variant)
145                            .expect(concat!(stringify!($variant), " missing from VARIANTS"));
146                        for (i, fork) in TempoHardfork::VARIANTS.iter().enumerate() {
147                            let active = TempoHardfork::[<is_ $variant:lower>](fork);
148                            if i >= idx {
149                                assert!(active, "{fork:?} should satisfy is_{}", stringify!([<$variant:lower>]));
150                            } else {
151                                assert!(!active, "{fork:?} should not satisfy is_{}", stringify!([<$variant:lower>]));
152                            }
153                        }
154                    }
155                )*
156            }
157        }
158    };
159}
160
161// -------------------------------------------------------------------------------------
162// Tempo hardfork definitions — append new variants here.
163// -------------------------------------------------------------------------------------
164tempo_hardfork! (
165    /// Tempo-specific hardforks for network upgrades.
166    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
167    #[derive(Default)]
168    TempoHardfork {
169        /// Genesis hardfork.
170        Genesis,
171        #[default]
172        /// T0 hardfork.
173        T0,
174        /// T1 hardfork.
175        T1,
176        /// T1.A hardfork.
177        T1A,
178        /// T1.B hardfork.
179        T1B,
180        /// T1.C hardfork.
181        T1C,
182        /// T2 hardfork.
183        ///
184        /// See <https://docs.tempo.xyz/docs/protocol/upgrades/t2>.
185        T2,
186        /// T3 hardfork.
187        ///
188        /// See <https://docs.tempo.xyz/docs/protocol/upgrades/t3>.
189        T3,
190        /// T4 hardfork.
191        ///
192        /// See <https://docs.tempo.xyz/docs/protocol/upgrades/t4>.
193        T4,
194        /// T5 hardfork.
195        ///
196        /// See <https://docs.tempo.xyz/docs/protocol/upgrades/t5>.
197        T5,
198        /// T6 hardfork.
199        ///
200        /// See <https://docs.tempo.xyz/docs/protocol/upgrades/t6>.
201        T6,
202        /// T7 hardfork.
203        ///
204        /// See <https://docs.tempo.xyz/docs/protocol/upgrades/t7>.
205        T7,
206        /// T8 hardfork.
207        ///
208        /// See <https://docs.tempo.xyz/docs/protocol/upgrades/t8>.
209        T8,
210    }
211);
212
213impl TempoHardfork {
214    /// Returns the position of this hardfork in [`Self::VARIANTS`].
215    ///
216    /// Useful for storing the hardfork in an atomic, see [`Self::from_variant_index`].
217    pub const fn variant_index(&self) -> u8 {
218        *self as u8
219    }
220
221    /// Returns the hardfork at the given [`Self::VARIANTS`] position, see
222    /// [`Self::variant_index`].
223    ///
224    /// Returns `None` if the index is out of bounds.
225    pub const fn from_variant_index(index: u8) -> Option<Self> {
226        if (index as usize) < Self::VARIANTS.len() {
227            Some(Self::VARIANTS[index as usize])
228        } else {
229            None
230        }
231    }
232
233    /// Returns the fixed general gas limit for T1+, or None for pre-T1.
234    /// - Pre-T1: None
235    /// - T1+: 30M gas (fixed)
236    pub const fn general_gas_limit(&self) -> Option<u64> {
237        if self.is_t1() {
238            return Some(gas::TEMPO_T1_GENERAL_GAS_LIMIT);
239        }
240        None
241    }
242
243    /// Returns the shared gas limit for the given block gas limit.
244    /// - T4+: 0 gas
245    /// - Pre-T4: block_gas_limit / 10
246    pub const fn shared_gas_limit(&self, block_gas_limit: u64) -> u64 {
247        if self.is_t4() {
248            0
249        } else {
250            block_gas_limit / 10
251        }
252    }
253
254    /// Returns the per-transaction gas limit cap.
255    /// - Pre-T1A: EIP-7825 Osaka limit (16,777,216 gas)
256    /// - T1A+: 30M gas (allows maximum-sized contract deployments under [TIP-1000] state creation)
257    ///
258    /// [TIP-1000]: <https://docs.tempo.xyz/protocol/tips/tip-1000>
259    pub const fn tx_gas_limit_cap(&self) -> Option<u64> {
260        if self.is_t1a() {
261            return Some(gas::TEMPO_T1_TX_GAS_LIMIT_CAP);
262        }
263        Some(MAX_TX_GAS_LIMIT_OSAKA)
264    }
265
266    /// Gas cost for using an existing 2D nonce key
267    pub const fn gas_existing_nonce_key(&self) -> u64 {
268        if self.is_t2() {
269            return gas::TEMPO_T2_EXISTING_NONCE_KEY_GAS;
270        }
271        gas::TEMPO_T1_EXISTING_NONCE_KEY_GAS
272    }
273
274    /// Gas cost for using a new 2D nonce key
275    pub const fn gas_new_nonce_key(&self) -> u64 {
276        if self.is_t2() {
277            return gas::TEMPO_T2_NEW_NONCE_KEY_GAS;
278        }
279        gas::TEMPO_T1_NEW_NONCE_KEY_GAS
280    }
281
282    /// Returns the active hardfork at the given timestamp for the specified chain.
283    ///
284    /// Returns `None` if the chain ID is not a known Tempo chain.
285    pub const fn from_chain_and_timestamp(chain_id: u64, timestamp: u64) -> Option<Self> {
286        // Walk variants in reverse to find the latest active fork, mirroring
287        // `TempoHardforks::tempo_hardfork_at` but without needing a chainspec instance.
288        let variants = Self::VARIANTS;
289        let mut i = variants.len();
290        while i > 0 {
291            i -= 1;
292            let activation = match chain_id {
293                4217 => variants[i].mainnet_activation_timestamp(),
294                42431 => variants[i].moderato_activation_timestamp(),
295                _ => return None,
296            };
297            if let Some(ts) = activation
298                && timestamp >= ts
299            {
300                return Some(variants[i]);
301            }
302        }
303        Some(Self::Genesis)
304    }
305
306    /// Retrieves the activation block for this hardfork on mainnet.
307    pub const fn mainnet_activation_block(&self) -> Option<u64> {
308        use crate::constants::mainnet::*;
309        match self {
310            Self::Genesis => Some(MAINNET_GENESIS_BLOCK),
311            Self::T0 => Some(MAINNET_T0_BLOCK),
312            Self::T1 => Some(MAINNET_T1_BLOCK),
313            Self::T1A => Some(MAINNET_T1A_BLOCK),
314            Self::T1B => Some(MAINNET_T1B_BLOCK),
315            Self::T1C => Some(MAINNET_T1C_BLOCK),
316            Self::T2 => Some(MAINNET_T2_BLOCK),
317            Self::T3 => None, // not yet known
318            Self::T4 => None,
319            Self::T5 => None,
320            Self::T6 => None,
321            Self::T7 => None,
322            Self::T8 => None,
323        }
324    }
325
326    /// Retrieves the activation timestamp for this hardfork on mainnet.
327    pub const fn mainnet_activation_timestamp(&self) -> Option<u64> {
328        use crate::constants::mainnet::*;
329        match self {
330            Self::Genesis => Some(MAINNET_GENESIS_TIMESTAMP),
331            Self::T0 => Some(MAINNET_T0_TIMESTAMP),
332            Self::T1 => Some(MAINNET_T1_TIMESTAMP),
333            Self::T1A => Some(MAINNET_T1A_TIMESTAMP),
334            Self::T1B => Some(MAINNET_T1B_TIMESTAMP),
335            Self::T1C => Some(MAINNET_T1C_TIMESTAMP),
336            Self::T2 => Some(MAINNET_T2_TIMESTAMP),
337            Self::T3 => Some(MAINNET_T3_TIMESTAMP),
338            Self::T4 => Some(MAINNET_T4_TIMESTAMP),
339            Self::T5 => Some(MAINNET_T5_TIMESTAMP),
340            Self::T6 => Some(MAINNET_T6_TIMESTAMP),
341            Self::T7 => Some(MAINNET_T7_TIMESTAMP),
342            Self::T8 => None,
343        }
344    }
345
346    /// Retrieves the activation block for this hardfork on moderato testnet.
347    pub const fn moderato_activation_block(&self) -> Option<u64> {
348        use crate::constants::moderato::*;
349        match self {
350            Self::Genesis => Some(MODERATO_GENESIS_BLOCK),
351            Self::T0 => Some(MODERATO_T0_BLOCK),
352            Self::T1 => Some(MODERATO_T1_BLOCK),
353            Self::T1A => Some(MODERATO_T1A_BLOCK),
354            Self::T1B => Some(MODERATO_T1B_BLOCK),
355            Self::T1C => Some(MODERATO_T1C_BLOCK),
356            Self::T2 => Some(MODERATO_T2_BLOCK),
357            Self::T3 => None, // not yet known
358            Self::T4 => None,
359            Self::T5 => None,
360            Self::T6 => None,
361            Self::T7 => None,
362            Self::T8 => None,
363        }
364    }
365
366    /// Retrieves the activation timestamp for this hardfork on moderato testnet.
367    pub const fn moderato_activation_timestamp(&self) -> Option<u64> {
368        use crate::constants::moderato::*;
369        match self {
370            Self::Genesis => Some(MODERATO_GENESIS_TIMESTAMP),
371            Self::T0 => Some(MODERATO_T0_TIMESTAMP),
372            Self::T1 => Some(MODERATO_T1_TIMESTAMP),
373            Self::T1A => Some(MODERATO_T1A_TIMESTAMP),
374            Self::T1B => Some(MODERATO_T1B_TIMESTAMP),
375            Self::T1C => Some(MODERATO_T1C_TIMESTAMP),
376            Self::T2 => Some(MODERATO_T2_TIMESTAMP),
377            Self::T3 => Some(MODERATO_T3_TIMESTAMP),
378            Self::T4 => Some(MODERATO_T4_TIMESTAMP),
379            Self::T5 => Some(MODERATO_T5_TIMESTAMP),
380            Self::T6 => Some(MODERATO_T6_TIMESTAMP),
381            Self::T7 => Some(MODERATO_T7_TIMESTAMP),
382            Self::T8 => None,
383        }
384    }
385}
386
387#[cfg(feature = "evm")]
388impl From<TempoHardfork> for SpecId {
389    fn from(_value: TempoHardfork) -> Self {
390        Self::OSAKA
391    }
392}
393
394#[cfg(feature = "evm")]
395impl From<&TempoHardfork> for SpecId {
396    fn from(value: &TempoHardfork) -> Self {
397        Self::from(*value)
398    }
399}
400
401#[cfg(feature = "evm")]
402impl From<SpecId> for TempoHardfork {
403    fn from(_spec: SpecId) -> Self {
404        // All Tempo hardforks map to SpecId::OSAKA, so we cannot derive the hardfork from SpecId.
405        // Default to the default hardfork when converting from SpecId.
406        // The actual hardfork should be passed explicitly where needed.
407        Self::default()
408    }
409}