Skip to main content

tempo_chainspec/
hardfork.rs

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