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}