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 alloy_eips::eip7825::MAX_TX_GAS_LIMIT_OSAKA;
27use alloy_evm::revm::primitives::hardfork::SpecId;
28use alloy_hardforks::hardfork;
29use reth_chainspec::{EthereumHardforks, ForkCondition};
30
31/// Single-source hardfork definition macro. Append a new variant and everything else is generated:
32///
33/// * Defines the `TempoHardfork` enum via [`hardfork!`] (including `Display`, `FromStr`,
34///   `Hardfork` trait impl, and `VARIANTS` const)
35/// * Generates `is_<fork>()` inherent methods on `TempoHardfork` — returns `true` when
36///   `*self >= Self::<Fork>`
37/// * Generates the `TempoHardforks` trait with:
38///   - `tempo_fork_activation()` (required — the only method implementors provide)
39///   - `tempo_hardfork_at()` — walks `VARIANTS` in reverse to find the latest active fork
40///   - `is_<fork>_active_at_timestamp()` — per-fork convenience helpers
41///   - `general_gas_limit_at()` — gas limit lookup by timestamp
42/// * Generates a `#[cfg(test)] mod tests` with activation, naming, trait, and serde tests
43///
44/// `Genesis` (first variant) is treated as the baseline and does not get `is_*()` methods.
45///  All subsequent variants are considered post-Genesis hardforks.
46macro_rules! tempo_hardfork {
47    (
48        $(#[$enum_meta:meta])*
49        TempoHardfork {
50            $(#[$genesis_meta:meta])* Genesis,
51            $( $(#[$meta:meta])* $variant:ident ),* $(,)?
52        }
53    ) => {
54
55        // delegate to alloy's `hardfork!` macro
56        hardfork!(
57            $(#[$enum_meta])*
58            TempoHardfork {
59                $(#[$genesis_meta])* Genesis,
60                $( $(#[$meta])* $variant ),*
61            }
62        );
63
64        impl TempoHardfork {
65            paste::paste! {
66                $(
67                    #[doc = concat!("Returns true if this hardfork is ", stringify!($variant), " or later.")]
68                    pub fn [<is_ $variant:lower>](&self) -> bool {
69                        *self >= Self::$variant
70                    }
71                )*
72            }
73        }
74
75        /// Trait for querying Tempo-specific hardfork activations.
76        pub trait TempoHardforks: EthereumHardforks {
77            /// Retrieves activation condition for a Tempo-specific hardfork.
78            fn tempo_fork_activation(&self, fork: TempoHardfork) -> ForkCondition;
79
80            /// Retrieves the Tempo hardfork active at a given timestamp.
81            fn tempo_hardfork_at(&self, timestamp: u64) -> TempoHardfork {
82                for &fork in TempoHardfork::VARIANTS.iter().rev() {
83                    if self.tempo_fork_activation(fork).active_at_timestamp(timestamp) {
84                        return fork;
85                    }
86                }
87                TempoHardfork::Genesis
88            }
89
90            paste::paste! {
91                $(
92                    #[doc = concat!("Returns true if ", stringify!($variant), " is active at the given timestamp.")]
93                    fn [<is_ $variant:lower _active_at_timestamp>](&self, timestamp: u64) -> bool {
94                        self.tempo_fork_activation(TempoHardfork::$variant)
95                            .active_at_timestamp(timestamp)
96                    }
97                )*
98            }
99
100            /// Returns the general (non-payment) gas limit for the given timestamp and block.
101            /// - T1+: fixed at 30M gas
102            /// - Pre-T1: calculated as (gas_limit - shared_gas_limit) / 2
103            fn general_gas_limit_at(&self, timestamp: u64, gas_limit: u64, shared_gas_limit: u64) -> u64 {
104                self.tempo_hardfork_at(timestamp)
105                    .general_gas_limit()
106                    .unwrap_or_else(|| (gas_limit - shared_gas_limit) / 2)
107            }
108        }
109
110        #[cfg(test)]
111        mod tests {
112            use super::*;
113            use TempoHardfork::*;
114            use reth_chainspec::Hardfork;
115
116            #[test]
117            fn test_hardfork_name() {
118                assert_eq!(Genesis.name(), "Genesis");
119                $(assert_eq!($variant.name(), stringify!($variant));)*
120            }
121
122            #[test]
123            fn test_hardfork_trait_implementation() {
124                for fork in TempoHardfork::VARIANTS {
125                    let _name: &str = Hardfork::name(fork);
126                }
127            }
128
129            #[test]
130            #[cfg(feature = "serde")]
131            fn test_tempo_hardfork_serde() {
132                for fork in TempoHardfork::VARIANTS {
133                    let json = serde_json::to_string(fork).expect("serialize");
134                    let deserialized: TempoHardfork = serde_json::from_str(&json).expect("deserialize");
135                    assert_eq!(deserialized, *fork);
136                }
137            }
138
139            paste::paste! {
140                $(
141                    #[test]
142                    fn [<test_is_ $variant:lower>]() {
143                        let idx = TempoHardfork::VARIANTS.iter().position(|v| *v == $variant)
144                            .expect(concat!(stringify!($variant), " missing from VARIANTS"));
145                        for (i, fork) in TempoHardfork::VARIANTS.iter().enumerate() {
146                            let active = TempoHardfork::[<is_ $variant:lower>](fork);
147                            if i >= idx {
148                                assert!(active, "{fork:?} should satisfy is_{}", stringify!([<$variant:lower>]));
149                            } else {
150                                assert!(!active, "{fork:?} should not satisfy is_{}", stringify!([<$variant:lower>]));
151                            }
152                        }
153                    }
154                )*
155            }
156        }
157    };
158}
159
160// -------------------------------------------------------------------------------------
161// Tempo hardfork definitions — append new variants here.
162// -------------------------------------------------------------------------------------
163tempo_hardfork! (
164    /// Tempo-specific hardforks for network upgrades.
165    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
166    #[derive(Default)]
167    TempoHardfork {
168        /// Genesis hardfork
169        Genesis,
170        #[default]
171        /// T0 hardfork (default until T1 activates on mainnet)
172        T0,
173        /// T1 hardfork - adds expiring nonce transactions
174        T1,
175        /// T1.A hardfork - removes EIP-7825 per-transaction gas limit
176        T1A,
177        /// T1.B hardfork
178        T1B,
179        /// T1.C hardfork
180        T1C,
181        /// T2 hardfork - adds compound transfer policies ([TIP-1015])
182        ///
183        /// [TIP-1015]: <https://docs.tempo.xyz/protocol/tips/tip-1015>
184        T2,
185    }
186);
187
188impl TempoHardfork {
189    /// Returns the base fee for this hardfork in attodollars.
190    ///
191    /// Attodollars are the atomic gas accounting units at 10^-18 USD precision. Individual attodollars are not representable onchain (since TIP-20 tokens only have 6 decimals), but the unit is used for gas accounting.
192    /// - Pre-T1: 10 billion attodollars per gas
193    /// - T1+: 20 billion attodollars per gas (targets ~0.1 cent per TIP-20 transfer)
194    ///
195    /// Economic conversion: ceil(basefee × gas_used / 10^12) = cost in microdollars (TIP-20 tokens)
196    pub const fn base_fee(&self) -> u64 {
197        match self {
198            Self::T1 | Self::T1A | Self::T1B | Self::T1C | Self::T2 => {
199                crate::spec::TEMPO_T1_BASE_FEE
200            }
201            Self::T0 | Self::Genesis => crate::spec::TEMPO_T0_BASE_FEE,
202        }
203    }
204
205    /// Returns the fixed general gas limit for T1+, or None for pre-T1.
206    /// - Pre-T1: None
207    /// - T1+: 30M gas (fixed)
208    pub const fn general_gas_limit(&self) -> Option<u64> {
209        match self {
210            Self::T1 | Self::T1A | Self::T1B | Self::T1C | Self::T2 => {
211                Some(crate::spec::TEMPO_T1_GENERAL_GAS_LIMIT)
212            }
213            Self::T0 | Self::Genesis => None,
214        }
215    }
216
217    /// Returns the per-transaction gas limit cap.
218    /// - Pre-T1A: EIP-7825 Osaka limit (16,777,216 gas)
219    /// - T1A+: 30M gas (allows maximum-sized contract deployments under [TIP-1000] state creation)
220    ///
221    /// [TIP-1000]: <https://docs.tempo.xyz/protocol/tips/tip-1000>
222    pub const fn tx_gas_limit_cap(&self) -> Option<u64> {
223        match self {
224            Self::T1A | Self::T1B | Self::T1C | Self::T2 => {
225                Some(crate::spec::TEMPO_T1_TX_GAS_LIMIT_CAP)
226            }
227            Self::T0 | Self::Genesis | Self::T1 => Some(MAX_TX_GAS_LIMIT_OSAKA),
228        }
229    }
230
231    /// Gas cost for using an existing 2D nonce key
232    pub const fn gas_existing_nonce_key(&self) -> u64 {
233        match self {
234            Self::Genesis | Self::T0 | Self::T1 | Self::T1A | Self::T1B | Self::T1C => {
235                crate::spec::TEMPO_T1_EXISTING_NONCE_KEY_GAS
236            }
237            Self::T2 => crate::spec::TEMPO_T2_EXISTING_NONCE_KEY_GAS,
238        }
239    }
240
241    /// Gas cost for using a new 2D nonce key
242    pub const fn gas_new_nonce_key(&self) -> u64 {
243        match self {
244            Self::Genesis | Self::T0 | Self::T1 | Self::T1A | Self::T1B | Self::T1C => {
245                crate::spec::TEMPO_T1_NEW_NONCE_KEY_GAS
246            }
247            Self::T2 => crate::spec::TEMPO_T2_NEW_NONCE_KEY_GAS,
248        }
249    }
250}
251
252impl From<TempoHardfork> for SpecId {
253    fn from(_value: TempoHardfork) -> Self {
254        Self::OSAKA
255    }
256}
257
258impl From<&TempoHardfork> for SpecId {
259    fn from(value: &TempoHardfork) -> Self {
260        Self::from(*value)
261    }
262}
263
264impl From<SpecId> for TempoHardfork {
265    fn from(_spec: SpecId) -> Self {
266        // All Tempo hardforks map to SpecId::OSAKA, so we cannot derive the hardfork from SpecId.
267        // Default to the default hardfork when converting from SpecId.
268        // The actual hardfork should be passed explicitly where needed.
269        Self::default()
270    }
271}