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}