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}