1use crate::{
2 bootnodes::{andantino_nodes, moderato_nodes, presto_nodes},
3 hardfork::{TempoHardfork, TempoHardforks},
4};
5use alloc::{boxed::Box, sync::Arc, vec::Vec};
6use alloy_eips::eip7840::BlobParams;
7use alloy_evm::{
8 eth::spec::EthExecutorSpec,
9 revm::interpreter::gas::{
10 COLD_SLOAD_COST as COLD_SLOAD, SSTORE_SET, WARM_SSTORE_RESET,
11 WARM_STORAGE_READ_COST as WARM_SLOAD,
12 },
13};
14use alloy_genesis::Genesis;
15use alloy_primitives::{Address, B256, U256};
16use once_cell as _;
17#[cfg(not(feature = "std"))]
18use once_cell::sync::Lazy as LazyLock;
19use reth_chainspec::{
20 BaseFeeParams, Chain, ChainSpec, DepositContract, DisplayHardforks, EthChainSpec,
21 EthereumHardfork, EthereumHardforks, ForkCondition, ForkFilter, ForkId, Hardfork, Hardforks,
22 Head,
23};
24use reth_network_peers::NodeRecord;
25#[cfg(feature = "std")]
26use std::sync::LazyLock;
27use tempo_primitives::TempoHeader;
28
29pub const TEMPO_T0_BASE_FEE: u64 = 10_000_000_000;
34
35pub const TEMPO_T1_BASE_FEE: u64 = 20_000_000_000;
45
46pub const TEMPO_T1_GENERAL_GAS_LIMIT: u64 = 30_000_000;
51
52pub const TEMPO_T1_TX_GAS_LIMIT_CAP: u64 = 30_000_000;
57
58pub const SYSTEM_TX_COUNT: usize = 1;
60pub const SYSTEM_TX_ADDRESSES: [Address; SYSTEM_TX_COUNT] = [Address::ZERO];
61
62pub const TEMPO_T1_EXISTING_NONCE_KEY_GAS: u64 = COLD_SLOAD + WARM_SSTORE_RESET;
64pub const TEMPO_T2_EXISTING_NONCE_KEY_GAS: u64 = TEMPO_T1_EXISTING_NONCE_KEY_GAS + 2 * WARM_SLOAD;
66
67pub const TEMPO_T1_NEW_NONCE_KEY_GAS: u64 = COLD_SLOAD + SSTORE_SET;
69pub const TEMPO_T2_NEW_NONCE_KEY_GAS: u64 = TEMPO_T1_NEW_NONCE_KEY_GAS + 2 * WARM_SLOAD;
71
72#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
74#[serde(rename_all = "camelCase")]
75pub struct TempoGenesisInfo {
76 #[serde(skip_serializing_if = "Option::is_none")]
78 epoch_length: Option<u64>,
79 #[serde(skip_serializing_if = "Option::is_none")]
81 t0_time: Option<u64>,
82 #[serde(skip_serializing_if = "Option::is_none")]
84 t1_time: Option<u64>,
85 #[serde(skip_serializing_if = "Option::is_none")]
87 t1a_time: Option<u64>,
88 #[serde(skip_serializing_if = "Option::is_none")]
90 t1b_time: Option<u64>,
91 #[serde(skip_serializing_if = "Option::is_none")]
93 t1c_time: Option<u64>,
94 #[serde(skip_serializing_if = "Option::is_none")]
96 t2_time: Option<u64>,
97}
98
99impl TempoGenesisInfo {
100 fn extract_from(genesis: &Genesis) -> Self {
102 genesis
103 .config
104 .extra_fields
105 .deserialize_as::<Self>()
106 .unwrap_or_default()
107 }
108
109 pub fn epoch_length(&self) -> Option<u64> {
110 self.epoch_length
111 }
112
113 pub fn fork_time(&self, fork: TempoHardfork) -> Option<u64> {
115 match fork {
116 TempoHardfork::Genesis => Some(0),
117 TempoHardfork::T0 => self.t0_time,
118 TempoHardfork::T1 => self.t1_time,
119 TempoHardfork::T1A => self.t1a_time,
120 TempoHardfork::T1B => self.t1b_time,
121 TempoHardfork::T1C => self.t1c_time,
122 TempoHardfork::T2 => self.t2_time,
123 }
124 }
125}
126
127#[derive(Debug, Clone, Default)]
129pub struct TempoChainSpecParser;
130
131pub const SUPPORTED_CHAINS: &[&str] = &["mainnet", "moderato", "testnet"];
133
134#[cfg(feature = "cli")]
139pub fn chain_value_parser(s: &str) -> eyre::Result<Arc<TempoChainSpec>> {
140 Ok(match s {
141 "mainnet" => PRESTO.clone(),
142 "testnet" => ANDANTINO.clone(),
143 "moderato" => MODERATO.clone(),
144 "dev" => DEV.clone(),
145 _ => TempoChainSpec::from_genesis(reth_cli::chainspec::parse_genesis(s)?).into(),
146 })
147}
148
149#[cfg(feature = "cli")]
150impl reth_cli::chainspec::ChainSpecParser for TempoChainSpecParser {
151 type ChainSpec = TempoChainSpec;
152
153 const SUPPORTED_CHAINS: &'static [&'static str] = SUPPORTED_CHAINS;
154
155 fn parse(s: &str) -> eyre::Result<Arc<Self::ChainSpec>> {
156 chain_value_parser(s)
157 }
158}
159
160pub static ANDANTINO: LazyLock<Arc<TempoChainSpec>> = LazyLock::new(|| {
161 let genesis: Genesis = serde_json::from_str(include_str!("./genesis/andantino.json"))
162 .expect("`./genesis/andantino.json` must be present and deserializable");
163 TempoChainSpec::from_genesis(genesis)
164 .with_default_follow_url("wss://rpc.testnet.tempo.xyz")
165 .into()
166});
167
168pub static MODERATO: LazyLock<Arc<TempoChainSpec>> = LazyLock::new(|| {
169 let genesis: Genesis = serde_json::from_str(include_str!("./genesis/moderato.json"))
170 .expect("`./genesis/moderato.json` must be present and deserializable");
171 TempoChainSpec::from_genesis(genesis)
172 .with_default_follow_url("wss://rpc.moderato.tempo.xyz")
173 .into()
174});
175
176pub static PRESTO: LazyLock<Arc<TempoChainSpec>> = LazyLock::new(|| {
177 let genesis: Genesis = serde_json::from_str(include_str!("./genesis/presto.json"))
178 .expect("`./genesis/presto.json` must be present and deserializable");
179 TempoChainSpec::from_genesis(genesis)
180 .with_default_follow_url("wss://rpc.presto.tempo.xyz")
181 .into()
182});
183
184pub static DEV: LazyLock<Arc<TempoChainSpec>> = LazyLock::new(|| {
188 let genesis: Genesis = serde_json::from_str(include_str!("./genesis/dev.json"))
189 .expect("`./genesis/dev.json` must be present and deserializable");
190 TempoChainSpec::from_genesis(genesis).into()
191});
192
193#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct TempoChainSpec {
196 pub inner: ChainSpec<TempoHeader>,
198 pub info: TempoGenesisInfo,
199 pub default_follow_url: Option<&'static str>,
201}
202
203impl TempoChainSpec {
204 pub fn default_follow_url(&self) -> Option<&'static str> {
206 self.default_follow_url
207 }
208
209 pub fn from_genesis(genesis: Genesis) -> Self {
211 let info = TempoGenesisInfo::extract_from(&genesis);
213
214 let mut base_spec = ChainSpec::from_genesis(genesis);
216
217 let tempo_forks = TempoHardfork::VARIANTS.iter().filter_map(|&fork| {
218 info.fork_time(fork)
219 .map(|time| (fork, ForkCondition::Timestamp(time)))
220 });
221 base_spec.hardforks.extend(tempo_forks);
222
223 Self {
224 inner: base_spec.map_header(|inner| TempoHeader {
225 general_gas_limit: 0,
226 timestamp_millis_part: inner.timestamp % 1000,
227 shared_gas_limit: 0,
228 inner,
229 }),
230 info,
231 default_follow_url: None,
232 }
233 }
234
235 pub fn with_default_follow_url(mut self, url: &'static str) -> Self {
237 self.default_follow_url = Some(url);
238 self
239 }
240
241 pub fn mainnet() -> Self {
243 PRESTO.as_ref().clone()
244 }
245}
246
247impl From<ChainSpec> for TempoChainSpec {
250 fn from(spec: ChainSpec) -> Self {
251 Self {
252 inner: spec.map_header(|inner| TempoHeader {
253 general_gas_limit: 0,
254 timestamp_millis_part: inner.timestamp % 1000,
255 inner,
256 shared_gas_limit: 0,
257 }),
258 info: TempoGenesisInfo::default(),
259 default_follow_url: None,
260 }
261 }
262}
263
264impl Hardforks for TempoChainSpec {
265 fn fork<H: Hardfork>(&self, fork: H) -> ForkCondition {
266 self.inner.fork(fork)
267 }
268
269 fn forks_iter(&self) -> impl Iterator<Item = (&dyn Hardfork, ForkCondition)> {
270 self.inner.forks_iter()
271 }
272
273 fn fork_id(&self, head: &Head) -> ForkId {
274 self.inner.fork_id(head)
275 }
276
277 fn latest_fork_id(&self) -> ForkId {
278 self.inner.latest_fork_id()
279 }
280
281 fn fork_filter(&self, head: Head) -> ForkFilter {
282 self.inner.fork_filter(head)
283 }
284}
285
286impl EthChainSpec for TempoChainSpec {
287 type Header = TempoHeader;
288
289 fn chain(&self) -> Chain {
290 self.inner.chain()
291 }
292
293 fn base_fee_params_at_timestamp(&self, timestamp: u64) -> BaseFeeParams {
294 self.inner.base_fee_params_at_timestamp(timestamp)
295 }
296
297 fn blob_params_at_timestamp(&self, timestamp: u64) -> Option<BlobParams> {
298 self.inner.blob_params_at_timestamp(timestamp)
299 }
300
301 fn deposit_contract(&self) -> Option<&DepositContract> {
302 self.inner.deposit_contract()
303 }
304
305 fn genesis_hash(&self) -> B256 {
306 self.inner.genesis_hash()
307 }
308
309 fn prune_delete_limit(&self) -> usize {
310 self.inner.prune_delete_limit()
311 }
312
313 fn display_hardforks(&self) -> Box<dyn core::fmt::Display> {
314 let tempo_forks = self.inner.hardforks.forks_iter().filter(|(fork, _)| {
316 !EthereumHardfork::VARIANTS
317 .iter()
318 .any(|h| h.name() == (*fork).name())
319 });
320
321 Box::new(DisplayHardforks::new(tempo_forks))
322 }
323
324 fn genesis_header(&self) -> &Self::Header {
325 self.inner.genesis_header()
326 }
327
328 fn genesis(&self) -> &Genesis {
329 self.inner.genesis()
330 }
331
332 fn bootnodes(&self) -> Option<Vec<NodeRecord>> {
333 match self.inner.chain_id() {
334 4217 => Some(presto_nodes()),
335 42429 => Some(andantino_nodes()),
336 42431 => Some(moderato_nodes()),
337 _ => self.inner.bootnodes(),
338 }
339 }
340
341 fn final_paris_total_difficulty(&self) -> Option<U256> {
342 self.inner.get_final_paris_total_difficulty()
343 }
344
345 fn next_block_base_fee(&self, _parent: &TempoHeader, target_timestamp: u64) -> Option<u64> {
346 Some(self.tempo_hardfork_at(target_timestamp).base_fee())
347 }
348}
349
350impl EthereumHardforks for TempoChainSpec {
351 fn ethereum_fork_activation(&self, fork: EthereumHardfork) -> ForkCondition {
352 self.inner.ethereum_fork_activation(fork)
353 }
354}
355
356impl EthExecutorSpec for TempoChainSpec {
357 fn deposit_contract_address(&self) -> Option<Address> {
358 self.inner.deposit_contract_address()
359 }
360}
361
362impl TempoHardforks for TempoChainSpec {
363 fn tempo_fork_activation(&self, fork: TempoHardfork) -> ForkCondition {
364 self.fork(fork)
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use crate::hardfork::{TempoHardfork, TempoHardforks};
371 use reth_chainspec::{ForkCondition, Hardforks};
372 use reth_cli::chainspec::ChainSpecParser as _;
373
374 #[test]
375 fn can_load_testnet() {
376 let _ = super::TempoChainSpecParser::parse("testnet")
377 .expect("the testnet chainspec must always be well formed");
378 }
379
380 #[test]
381 fn can_load_dev() {
382 let _ = super::TempoChainSpecParser::parse("dev")
383 .expect("the dev chainspec must always be well formed");
384 }
385
386 #[test]
387 fn test_tempo_chainspec_has_tempo_hardforks() {
388 let chainspec = super::TempoChainSpecParser::parse("mainnet")
389 .expect("the mainnet chainspec must always be well formed");
390
391 let activation = chainspec.tempo_fork_activation(TempoHardfork::Genesis);
393 assert_eq!(activation, ForkCondition::Timestamp(0));
394
395 let activation = chainspec.tempo_fork_activation(TempoHardfork::T0);
397 assert_eq!(activation, ForkCondition::Timestamp(0));
398 }
399
400 #[test]
401 fn test_tempo_chainspec_implements_tempo_hardforks_trait() {
402 let chainspec = super::TempoChainSpecParser::parse("mainnet")
403 .expect("the mainnet chainspec must always be well formed");
404
405 let activation = chainspec.tempo_fork_activation(TempoHardfork::T0);
407 assert_eq!(activation, ForkCondition::Timestamp(0));
408 }
409
410 #[test]
411 fn test_tempo_hardforks_in_inner_hardforks() {
412 let chainspec = super::TempoChainSpecParser::parse("mainnet")
413 .expect("the mainnet chainspec must always be well formed");
414
415 let activation = chainspec.fork(TempoHardfork::T0);
417 assert_eq!(activation, ForkCondition::Timestamp(0));
418
419 let has_genesis = chainspec
421 .forks_iter()
422 .any(|(fork, _)| fork.name() == "Genesis");
423 assert!(has_genesis, "Genesis hardfork should be in inner.hardforks");
424 }
425
426 #[test]
427 fn test_from_genesis_with_hardforks_at_zero() {
428 use alloy_genesis::Genesis;
429
430 let mut config = serde_json::Map::new();
432 config.insert("chainId".into(), 1234.into());
433 for &fork in TempoHardfork::VARIANTS {
434 if fork != TempoHardfork::Genesis {
435 let key = format!("{}Time", fork.name().to_lowercase());
436 config.insert(key, 0.into());
437 }
438 }
439 let json = serde_json::json!({ "config": config, "alloc": {} });
440 let genesis: Genesis = serde_json::from_value(json).unwrap();
441 let chainspec = super::TempoChainSpec::from_genesis(genesis);
442
443 for &fork in TempoHardfork::VARIANTS {
445 assert!(
446 chainspec.tempo_fork_activation(fork).active_at_timestamp(0),
447 "{fork:?} should be active at timestamp 0"
448 );
449 assert!(
450 chainspec
451 .tempo_fork_activation(fork)
452 .active_at_timestamp(1000),
453 "{fork:?} should be active at timestamp 1000"
454 );
455 }
456
457 let latest = *TempoHardfork::VARIANTS.last().unwrap();
459 assert_eq!(chainspec.tempo_hardfork_at(0), latest);
460 assert_eq!(chainspec.tempo_hardfork_at(1000), latest);
461 assert_eq!(chainspec.tempo_hardfork_at(u64::MAX), latest);
462 }
463
464 mod tempo_hardfork_at {
465 use super::*;
466
467 #[test]
468 fn mainnet() {
469 let cs = super::super::TempoChainSpecParser::parse("mainnet")
470 .expect("the mainnet chainspec must always be well formed");
471
472 assert_eq!(cs.tempo_hardfork_at(0), TempoHardfork::T0);
474 assert_eq!(cs.tempo_hardfork_at(1000), TempoHardfork::T0);
475 assert_eq!(cs.tempo_hardfork_at(1770908399), TempoHardfork::T0);
476
477 assert!(cs.is_t1_active_at_timestamp(1770908400));
479 assert!(cs.is_t1a_active_at_timestamp(1770908400));
480 assert_eq!(cs.tempo_hardfork_at(1770908400), TempoHardfork::T1A);
481 assert_eq!(cs.tempo_hardfork_at(1770908401), TempoHardfork::T1A);
482
483 assert!(!cs.is_t1b_active_at_timestamp(1771858799));
485 assert_eq!(cs.tempo_hardfork_at(1771858799), TempoHardfork::T1A);
486
487 assert!(cs.is_t1b_active_at_timestamp(1771858800));
489 assert_eq!(cs.tempo_hardfork_at(1771858800), TempoHardfork::T1B);
490
491 assert!(!cs.is_t1c_active_at_timestamp(1773327599));
493 assert_eq!(cs.tempo_hardfork_at(1773327599), TempoHardfork::T1B);
494
495 assert!(cs.is_t1c_active_at_timestamp(1773327600));
497 assert_eq!(cs.tempo_hardfork_at(1773327600), TempoHardfork::T1C);
498
499 assert!(cs.is_t1c_active_at_timestamp(u64::MAX));
501 assert!(!cs.is_t2_active_at_timestamp(u64::MAX));
502 assert_eq!(cs.tempo_hardfork_at(u64::MAX), TempoHardfork::T1C);
503 }
504
505 #[test]
506 fn moderato() {
507 let cs = super::super::TempoChainSpecParser::parse("moderato")
508 .expect("the moderato chainspec must always be well formed");
509
510 assert_eq!(cs.tempo_hardfork_at(0), TempoHardfork::Genesis);
512 assert_eq!(cs.tempo_hardfork_at(1770303599), TempoHardfork::Genesis);
513
514 assert_eq!(cs.tempo_hardfork_at(1770303600), TempoHardfork::T1);
516 assert_eq!(cs.tempo_hardfork_at(1770303601), TempoHardfork::T1);
517
518 assert_eq!(cs.tempo_hardfork_at(1771858799), TempoHardfork::T1);
520
521 assert!(cs.is_t1a_active_at_timestamp(1771858800));
523 assert!(cs.is_t1b_active_at_timestamp(1771858800));
524 assert_eq!(cs.tempo_hardfork_at(1771858800), TempoHardfork::T1B);
525
526 assert!(!cs.is_t1c_active_at_timestamp(1773068399));
528 assert_eq!(cs.tempo_hardfork_at(1773068399), TempoHardfork::T1B);
529
530 assert!(cs.is_t1c_active_at_timestamp(1773068400));
532 assert_eq!(cs.tempo_hardfork_at(1773068400), TempoHardfork::T1C);
533
534 assert!(cs.is_t1c_active_at_timestamp(u64::MAX));
536 assert!(!cs.is_t2_active_at_timestamp(u64::MAX));
537 assert_eq!(cs.tempo_hardfork_at(u64::MAX), TempoHardfork::T1C);
538 }
539
540 #[test]
541 fn testnet() {
542 let cs = super::super::TempoChainSpecParser::parse("testnet")
543 .expect("the testnet chainspec must always be well formed");
544
545 assert_eq!(cs.tempo_hardfork_at(0), TempoHardfork::Genesis);
547 assert_eq!(cs.tempo_hardfork_at(1000), TempoHardfork::Genesis);
548 assert_eq!(cs.tempo_hardfork_at(u64::MAX), TempoHardfork::Genesis);
549 }
550 }
551}