1use super::{
33 Backend,
34 state::StateRootCache,
35 storage::{BlockchainStorage, InMemoryBlockStates},
36};
37use crate::eth::{
38 backend::{
39 db::MaybeFullDatabase,
40 tempo_consensus::{TempoConsensus, TempoConsensusIdentity, TempoFinalization},
41 },
42 error::BlockchainError,
43 tempo_consensus::TempoCertifiedBlock,
44};
45use alloy_consensus::{BlockHeader, Sealable, proofs::calculate_transaction_root};
46use alloy_network::Network;
47use alloy_primitives::{
48 Address, B256, U256,
49 map::{AddressMap, HashMap},
50};
51use alloy_provider::Provider;
52use alloy_rpc_types::EIP1186AccountProofResponse as AccountProof;
53use anvil_core::eth::block::{Block, canonical_block};
54use anvil_rpc::error::{ErrorCode, RpcError};
55use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
56use revm::database::DbAccount;
57use std::{fmt, num::NonZeroU64, sync::Arc};
58use tempo_hardfork::TempoHardfork;
59use tempo_primitives::TempoHeader;
60
61#[derive(Clone)]
63pub(crate) struct TempoChain {
64 seed: B256,
65 epoch_length: NonZeroU64,
66 consensus: TempoConsensus,
67 latest_block: Option<Block>,
68 snapshots: HashMap<U256, TempoSnapshot>,
69 pub(super) proof_states: Arc<RwLock<InMemoryBlockStates>>,
71 pub(super) proof_root: Arc<Mutex<(StateRootCache, AddressMap<DbAccount>)>>,
73}
74
75#[derive(Clone)]
77struct TempoSnapshot {
78 consensus: TempoConsensus,
79 latest_block: Option<Block>,
80 proof_state: Option<Arc<AddressMap<DbAccount>>>,
81}
82
83impl TempoChain {
84 pub(super) fn new(seed: B256, epoch_length: NonZeroU64, height: u64) -> Self {
85 Self {
86 seed,
87 epoch_length,
88 consensus: TempoConsensus::at_height(seed, epoch_length, height),
89 latest_block: None,
90 snapshots: Default::default(),
91 proof_states: Default::default(),
92 proof_root: Default::default(),
93 }
94 }
95
96 pub(super) fn reanchor(&mut self, height: u64) {
98 self.consensus = TempoConsensus::at_height(self.seed, self.epoch_length, height);
99 self.latest_block = None;
100 self.proof_states.write().clear();
101 *self.proof_root.lock() = Default::default();
102 self.snapshots.clear();
103 }
104
105 pub(super) fn snapshot(&mut self, id: U256, proof_state: Option<Arc<AddressMap<DbAccount>>>) {
106 self.snapshots.insert(
107 id,
108 TempoSnapshot {
109 consensus: self.consensus.clone(),
110 latest_block: self.latest_block.clone(),
111 proof_state,
112 },
113 );
114 }
115
116 pub(super) fn restore(&mut self, id: U256, height: u64) -> Option<Arc<AddressMap<DbAccount>>> {
121 let TempoSnapshot { consensus, latest_block, proof_state } =
122 self.snapshots.remove(&id).unwrap_or_else(|| TempoSnapshot {
123 consensus: TempoConsensus::at_height(self.seed, self.epoch_length, height),
124 latest_block: None,
125 proof_state: None,
126 });
127 self.consensus = consensus;
128 self.latest_block = latest_block;
129 self.snapshots.retain(|snapshot_id, _| *snapshot_id < id);
130 proof_state
131 }
132
133 pub(super) fn unwind(&mut self, block: &Block) -> Result<(), BlockchainError> {
135 let height = block.header.number();
136 let is_current =
137 self.consensus.latest().is_some_and(|(current_height, _)| current_height == height);
138 if height < self.consensus.anchor_height() {
139 self.consensus = TempoConsensus::at_height(self.seed, self.epoch_length, height);
141 } else {
142 self.consensus
143 .unwind(height)
144 .map_err(|error| BlockchainError::Internal(error.to_string()))?;
145 }
146 if !is_current {
147 self.latest_block = self.consensus.latest().map(|_| block.clone());
148 }
149 Ok(())
150 }
151
152 pub(super) fn prepare(
154 &self,
155 header: &mut TempoHeader,
156 hardfork: TempoHardfork,
157 ) -> Result<(), BlockchainError> {
158 self.consensus
159 .prepare_header(header, hardfork)
160 .map_err(|error| BlockchainError::Internal(error.to_string()))
161 }
162
163 pub(super) fn finalize(&mut self, header: &TempoHeader, hardfork: TempoHardfork) {
168 self.consensus.finalize(header, hardfork).expect(
169 "the database write lock preserves the prepared height and committee; metadata and hardfork are unchanged",
170 );
171 }
172
173 pub(super) fn publish(&mut self, block: &Block) {
175 self.latest_block = Some(block.clone());
176 }
177}
178
179impl fmt::Debug for TempoChain {
180 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181 f.debug_struct("TempoChain")
182 .field("identity", &self.consensus.identity())
183 .finish_non_exhaustive()
184 }
185}
186
187impl<N: Network> Backend<N> {
188 pub async fn tempo_consensus_identity(
190 &self,
191 ) -> Result<TempoConsensusIdentity, BlockchainError> {
192 let _db = self.db.read().await;
193 let storage = self.blockchain.storage.read();
194 Ok(tempo_chain(&storage)?.consensus.identity())
195 }
196
197 pub async fn tempo_finalization(
199 &self,
200 height: Option<u64>,
201 ) -> Result<TempoCertifiedBlock, BlockchainError> {
202 self.tempo_certified_block(height).await?.ok_or_else(|| {
203 consensus_unavailable(if height.is_some() { "certificate" } else { "certifications" })
204 })
205 }
206
207 pub async fn tempo_latest_finalization(
211 &self,
212 ) -> Result<Option<TempoCertifiedBlock>, BlockchainError> {
213 self.tempo_certified_block(None).await
214 }
215
216 pub async fn tempo_rotate_consensus_identity(
218 &self,
219 ) -> Result<TempoConsensusIdentity, BlockchainError> {
220 let _mining = self.mining.lock().await;
221 let _db = self.db.write().await;
222 let mut storage = self.blockchain.storage.write();
223 storage
224 .tempo
225 .as_mut()
226 .ok_or_else(tempo_unavailable)?
227 .consensus
228 .rotate()
229 .map_err(|error| BlockchainError::Internal(error.to_string()))
230 }
231
232 async fn tempo_certified_block(
233 &self,
234 height: Option<u64>,
235 ) -> Result<Option<TempoCertifiedBlock>, BlockchainError> {
236 let (provider, height, root, hash, canonical_hash) = {
237 let _db = self.db.read().await;
238 let storage = self.blockchain.storage.read();
239 let tempo = tempo_chain(&storage)?;
240 let local = match height {
241 Some(height) => tempo
242 .consensus
243 .finalization(height)
244 .map(|finalization| {
245 let block = storage
246 .hashes
247 .get(&height)
248 .and_then(|hash| storage.blocks.get(hash))
249 .ok_or_else(|| consensus_unavailable("block"))?;
250 certified_block(finalization, block)
251 })
252 .transpose()?,
253 None => latest_finalization(tempo)?,
254 };
255 if let Some(local) = local {
256 return local.try_into().map(Some);
257 }
258 let Some(fork) = self.get_fork() else { return Ok(None) };
259 let config = fork.config.read();
260 let height = height.unwrap_or(config.block_number);
261 if height > config.block_number {
262 return Ok(None);
263 }
264 (
266 config.provider.clone(),
267 height,
268 config.block_number,
269 config.block_hash,
270 storage.hashes.get(&height).copied(),
271 )
272 };
273 let certified = provider
274 .raw_request::<_, TempoCertifiedBlock>(
275 "consensus_getFinalization".into(),
276 (serde_json::json!({ "height": height }),),
277 )
278 .await?;
279 if certified.block.header.number() != height
280 || certified.block.header.hash_slow() != certified.finalization.digest
281 || (height == root && certified.finalization.digest != hash)
282 || canonical_hash.is_some_and(|hash| certified.finalization.digest != hash)
283 || tempo_evm::consensus::validate_body_against_header(
284 &certified.block.body,
285 &certified.block.header,
286 )
287 .is_err()
288 {
289 return Err(consensus_unavailable(
290 "upstream finalization does not match the pinned fork history",
291 ));
292 }
293 Ok(Some(certified))
294 }
295
296 pub(super) async fn prove_tempo_account_at(
298 &self,
299 address: Address,
300 keys: &[B256],
301 height: u64,
302 ) -> Result<Option<AccountProof>, BlockchainError> {
303 let db = self.db.read().await;
304 if self.get_fork().is_none() {
305 return Ok(None);
306 }
307 let (proof_states, hash) = {
308 let storage = self.blockchain.storage.read();
309 let Some(tempo) = &storage.tempo else { return Ok(None) };
310 let Some(finalization) = tempo.consensus.finalization(height) else { return Ok(None) };
311 let hash =
312 storage.hashes.get(&height).copied().ok_or(BlockchainError::DataUnavailable)?;
313 if finalization.digest != hash {
314 return Err(BlockchainError::DataUnavailable);
315 }
316 (Arc::clone(&tempo.proof_states), hash)
317 };
318 drop(db);
319
320 let states = proof_states.upgradable_read();
323 if let Some(state) = states.get_state(&hash) {
324 let accounts = state.maybe_as_full_db().ok_or(BlockchainError::DataUnavailable)?;
325 return Ok(Some(super::prove_account(accounts, address, keys)));
326 }
327
328 let mut states = RwLockUpgradableReadGuard::upgrade(states);
329 let accounts = states
333 .get_on_disk_state(&hash)
334 .and_then(|state| state.maybe_as_full_db())
335 .ok_or(BlockchainError::DataUnavailable)?
336 .clone();
337 drop(states);
338 Ok(Some(super::prove_account(&accounts, address, keys)))
339 }
340}
341
342fn tempo_chain<N: Network>(storage: &BlockchainStorage<N>) -> Result<&TempoChain, BlockchainError> {
343 storage.tempo.as_ref().ok_or_else(tempo_unavailable)
344}
345
346fn latest_finalization(
347 tempo: &TempoChain,
348) -> Result<Option<(TempoFinalization, Block)>, BlockchainError> {
349 let Some((_, finalization)) = tempo.consensus.latest() else { return Ok(None) };
350 let block = tempo.latest_block.as_ref().ok_or_else(|| consensus_unavailable("block"))?;
351 certified_block(finalization, block).map(Some)
352}
353
354fn certified_block(
355 finalization: &TempoFinalization,
356 block: &Block,
357) -> Result<(TempoFinalization, Block), BlockchainError> {
358 let block = canonical_block(block.clone());
359 if block.header.hash_slow() != finalization.digest
360 || calculate_transaction_root(&block.body.transactions) != block.header.transactions_root()
361 || block.body.calculate_ommers_root() != block.header.ommers_hash()
362 || block.body.calculate_withdrawals_root() != block.header.withdrawals_root()
363 {
364 return Err(consensus_unavailable("block"));
365 }
366 Ok((finalization.clone(), block))
367}
368
369fn consensus_unavailable(content: &str) -> BlockchainError {
370 RpcError {
371 code: ErrorCode::from(503),
372 message: "the consensus subservice was not available, but the request can be retried later"
373 .into(),
374 data: Some(serde_json::Value::String(content.to_owned())),
375 }
376 .into()
377}
378
379fn tempo_unavailable() -> BlockchainError {
380 RpcError::invalid_params("Tempo consensus is only available on the Tempo network").into()
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386 use crate::{
387 NodeConfig,
388 eth::{
389 api::EthApi,
390 backend::{
391 db::{SerializableState, StateDb},
392 mem::MiningCommitHook,
393 },
394 },
395 spawn,
396 };
397 use alloy_primitives::keccak256;
398 use alloy_rpc_types::{BlockId, BlockNumberOrTag, TransactionRequest, anvil::Forking};
399 use alloy_serde::WithOtherFields;
400 use alloy_trie::{Nibbles, TrieAccount, proof::verify_proof};
401 use anvil_core::types::ReorgOptions;
402 use foundry_evm::{
403 backend::MemDb,
404 hardfork::{EthereumHardfork, FoundryHardfork},
405 };
406 use foundry_primitives::{FoundryHeader, FoundryNetwork};
407 use revm::database::AccountState;
408 use std::{
409 sync::atomic::{AtomicBool, Ordering},
410 time::Duration,
411 };
412 use tokio::{sync::Notify, time::timeout};
413
414 fn consensus(api: &EthApi<FoundryNetwork>) -> TempoConsensus {
415 api.backend.blockchain.storage.read().tempo.as_ref().unwrap().consensus.clone()
416 }
417
418 fn assert_certified_head(api: &EthApi<FoundryNetwork>) {
419 let consensus = consensus(api);
420 let (height, certificate) = consensus.latest().unwrap();
421 let block = api.backend.get_block(height).unwrap();
422 assert_eq!(height, api.backend.best_number());
423 assert_eq!(certificate.digest, block.header.hash_slow());
424 assert_eq!(certificate.digest, api.backend.best_hash());
425 }
426
427 #[tokio::test(flavor = "multi_thread")]
428 async fn tempo_snapshot_waits_for_complete_block_publication() {
429 let (api, _handle) = spawn(NodeConfig::test_tempo().with_no_mining(true)).await;
430 let hook =
431 MiningCommitHook { reached: Arc::new(Notify::new()), resume: Arc::new(Notify::new()) };
432 *api.backend.mining_commit_hook.lock() = Some(hook.clone());
433 let mining_api = api.clone();
434 let mining = tokio::spawn(async move { mining_api.mine_one().await });
435 timeout(Duration::from_secs(10), hook.reached.notified()).await.unwrap();
436 assert_eq!(api.backend.best_number(), 0);
437 assert!(consensus(&api).latest().is_none());
438 assert!(api.backend.get_block(1).is_none());
439 let mut snapshot = Box::pin(api.evm_snapshot());
440 assert!(futures::poll!(snapshot.as_mut()).is_pending());
441 let mut certificate = Box::pin(api.backend.tempo_finalization(None));
442 let mut identity = Box::pin(api.backend.tempo_consensus_identity());
443 let mut latest = Box::pin(api.backend.tempo_latest_finalization());
444 let mut rotation = Box::pin(api.backend.tempo_rotate_consensus_identity());
445 assert!(futures::poll!(certificate.as_mut()).is_pending());
446 assert!(futures::poll!(identity.as_mut()).is_pending());
447 assert!(futures::poll!(latest.as_mut()).is_pending());
448 assert!(futures::poll!(rotation.as_mut()).is_pending());
449 hook.resume.notify_one();
450 let snapshot = timeout(Duration::from_secs(10), async {
451 mining.await.unwrap().unwrap();
452 let (snapshot, certificate, identity, latest, rotation) =
453 tokio::join!(snapshot, certificate, identity, latest, rotation);
454 let certificate = certificate.unwrap();
455 assert_eq!(certificate.block.header.number(), 1);
456 assert_eq!(certificate.block.header.hash_slow(), certificate.finalization.digest);
457 assert_eq!(api.backend.best_hash(), certificate.finalization.digest);
458 assert_eq!(latest.unwrap().unwrap().finalization, certificate.finalization);
459 identity.unwrap();
460 rotation.unwrap();
461 snapshot.unwrap()
462 })
463 .await
464 .unwrap();
465 *api.backend.mining_commit_hook.lock() = None;
466 assert_certified_head(&api);
467 api.mine_one().await.unwrap();
468 assert!(api.evm_revert(snapshot).await.unwrap());
469 assert_eq!(api.backend.best_number(), 1);
470 assert_certified_head(&api);
471 }
472
473 #[tokio::test]
474 async fn tempo_state_root_keeps_published_storage_readable() {
475 let (api, _handle) = spawn(NodeConfig::test_tempo().with_no_mining(true)).await;
476 api.mine_one().await.unwrap();
477 let storage = Arc::clone(&api.backend.blockchain.storage);
478 let reached = Arc::new(AtomicBool::new(false));
479 let observed = Arc::clone(&reached);
480 *api.backend.mining_state_root_hook.lock() = Some(Box::new(move || {
481 let storage = storage.try_read().expect("state-root computation must not lock storage");
482 let (height, certificate) = storage.tempo.as_ref().unwrap().consensus.latest().unwrap();
483 assert_eq!(storage.best_number, 1);
484 assert_eq!(height, storage.best_number);
485 assert_eq!(certificate.digest, storage.best_hash);
486 assert!(storage.blocks.contains_key(&certificate.digest));
487 observed.store(true, Ordering::Relaxed);
488 }));
489 api.mine_one().await.unwrap();
490 assert!(reached.load(Ordering::Relaxed));
491 assert_certified_head(&api);
492 }
493
494 #[tokio::test]
495 async fn tempo_rollback_publishes_consensus_and_head_together() {
496 let (api, _handle) = spawn(NodeConfig::test_tempo().with_no_mining(true)).await;
497 api.anvil_mine(Some(U256::from(2)), None).await.unwrap();
498 let storage = Arc::clone(&api.backend.blockchain.storage);
499 let reached = Arc::new(AtomicBool::new(false));
500 let observed = Arc::clone(&reached);
501 *api.backend.rollback_unwind_hook.lock() = Some(Box::new(move || {
502 assert!(storage.try_read().is_none(), "a partial rollback must not be visible");
503 observed.store(true, Ordering::Relaxed);
504 }));
505 api.anvil_rollback(Some(1)).await.unwrap();
506 assert!(reached.load(Ordering::Relaxed));
507 assert_eq!(api.backend.best_number(), 1);
508 assert!(consensus(&api).finalization(2).is_none());
509 api.anvil_rollback(Some(0)).await.unwrap();
510 assert_certified_head(&api);
511 }
512
513 #[tokio::test(flavor = "multi_thread")]
514 async fn rejected_consensus_preparation_does_not_commit_execution() {
515 let (api, handle) = spawn(NodeConfig::test_tempo().with_no_mining(true)).await;
516 let sender = handle.dev_wallets().next().unwrap().address();
517 queue_transfer(&api, sender).await;
518 let previous_hash = api.backend.best_hash();
519 let previous_root = api.backend.db.read().await.maybe_state_root();
520 {
521 let _db = api.backend.db.write().await;
522 api.backend.blockchain.storage.write().tempo.as_mut().unwrap().reanchor(5);
523 }
524 let error = api.mine_one().await.unwrap_err();
525 assert!(matches!(error, BlockchainError::Internal(message)
526 if message == "expected Tempo consensus height 6, got 1"));
527 assert_eq!(api.backend.best_number(), 0);
528 assert_eq!(api.backend.best_hash(), previous_hash);
529 assert_eq!(api.backend.db.read().await.maybe_state_root(), previous_root);
530 assert!(consensus(&api).latest().is_none());
531 {
532 let _db = api.backend.db.write().await;
533 api.backend.blockchain.storage.write().tempo.as_mut().unwrap().reanchor(0);
534 }
535 api.mine_one().await.unwrap();
536 assert_eq!(api.backend.get_block(1).unwrap().body.transactions.len(), 1);
537 assert_certified_head(&api);
538 }
539
540 #[tokio::test]
541 async fn tempo_snapshot_restores_pending_committee_and_counter() {
542 let (api, _handle) = spawn(tempo_config(3)).await;
543 api.mine_one().await.unwrap();
544 let first = consensus(&api).finalization(1).unwrap().digest;
545 let scheduled = rotate_consensus(&api).await;
546 let snapshot = api.evm_snapshot().await.unwrap();
547 let replaced = rotate_consensus(&api).await;
548 assert_ne!(scheduled, replaced);
549 assert!(api.evm_revert(snapshot).await.unwrap());
550 assert_eq!(consensus(&api).finalization(1).unwrap().digest, first);
551 let mut restored = consensus(&api);
553 assert_eq!(restored.rotate().unwrap(), replaced);
554 api.mine_one().await.unwrap();
555 api.mine_one().await.unwrap();
556 assert_eq!(consensus(&api).identity(), scheduled);
557 assert_certified_head(&api);
558 }
559
560 #[tokio::test]
561 async fn tempo_reorg_and_reset_preserve_lifecycle() {
562 let (api, _handle) = spawn(tempo_config(3)).await;
563 let initial = consensus(&api).identity();
564 api.anvil_mine(Some(U256::from(3)), None).await.unwrap();
565 let first = consensus(&api).finalization(1).unwrap().digest;
566 let previous = consensus(&api).finalization(3).unwrap().digest;
567 api.anvil_set_coinbase(Address::repeat_byte(0x33)).await.unwrap();
568 api.anvil_reorg(ReorgOptions { depth: 2, tx_block_pairs: Vec::new() }).await.unwrap();
569 assert_eq!(consensus(&api).finalization(1).unwrap().digest, first);
570 assert_ne!(consensus(&api).finalization(3).unwrap().digest, previous);
571 assert_certified_head(&api);
572 let stale = api.evm_snapshot().await.unwrap();
573 api.anvil_reset(None).await.unwrap();
574 assert!(api.backend.list_state_snapshots().is_empty());
575 assert!(!api.evm_revert(stale).await.unwrap());
576 let fresh = api.evm_snapshot().await.unwrap();
577 assert_eq!(fresh, stale);
578 assert!(consensus(&api).latest().is_none());
579 assert_eq!(consensus(&api).identity(), initial);
580 api.mine_one().await.unwrap();
581 assert_certified_head(&api);
582 assert!(api.evm_revert(fresh).await.unwrap());
583 assert_eq!(api.backend.best_number(), 0);
584 assert!(consensus(&api).latest().is_none());
585 api.mine_one().await.unwrap();
586 assert_certified_head(&api);
587 }
588
589 #[tokio::test]
590 async fn cross_import_snapshot_restores_state_and_resumes_mining() {
591 for tempo in [false, true] {
592 for (saved_height, imported_height) in [(2, 1), (1, 2)] {
593 let config = if tempo { NodeConfig::test_tempo() } else { NodeConfig::test() };
594 let (api, handle) = spawn(config.with_no_mining(true)).await;
595 let account = Address::repeat_byte(0x22);
596 let sender = handle.dev_wallets().next().unwrap().address();
597 api.anvil_set_balance(account, U256::from(2)).await.unwrap();
598 let mut state = None;
599 let mut saved = None;
600 for height in 1..=2 {
601 api.mine_one().await.unwrap();
602 if height == imported_height {
603 state = Some(api.anvil_dump_state(None).await.unwrap());
604 }
605 if height == saved_height {
606 api.anvil_set_balance(account, U256::ONE).await.unwrap();
607 let transaction = queue_transfer(&api, sender).await;
608 saved = Some((
609 api.evm_snapshot().await.unwrap(),
610 api.backend.best_hash(),
611 transaction,
612 ));
613 api.anvil_set_balance(account, U256::from(2)).await.unwrap();
614 api.anvil_drop_all_transactions().await.unwrap();
615 }
616 }
617 api.mine_one().await.unwrap();
618 let (snapshot, saved_hash, transaction) = saved.unwrap();
619
620 assert!(api.anvil_load_state(state.unwrap()).await.unwrap());
621 assert_eq!(api.backend.best_number(), imported_height);
622 assert_eq!(api.balance(account, None).await.unwrap(), U256::from(2));
623 assert!(api.transaction_by_hash(transaction).await.unwrap().is_none());
624 assert!(api.evm_revert(snapshot).await.unwrap());
625 assert_eq!(api.backend.best_number(), saved_height);
626 assert_eq!(api.backend.best_hash(), saved_hash);
627 assert_eq!(api.balance(account, None).await.unwrap(), U256::ONE);
628 assert!(api.transaction_by_hash(transaction).await.unwrap().is_some());
629 if tempo {
630 assert!(consensus(&api).latest().is_none());
631 }
632 api.mine_one().await.unwrap();
633 assert_eq!(api.backend.best_number(), saved_height + 1);
634 let block = api.backend.get_block(saved_height + 1).unwrap();
635 assert_eq!(block.header.parent_hash(), saved_hash);
636 assert_eq!(block.body.transactions.len(), 1);
637 if tempo {
638 assert_certified_head(&api);
639 }
640 }
641 }
642 }
643
644 #[tokio::test]
645 async fn tempo_pending_and_native_projection_follow_executed_hardfork() {
646 for hardfork in [
647 TempoHardfork::T3.into(),
648 TempoHardfork::T4.into(),
649 FoundryHardfork::Ethereum(EthereumHardfork::Prague),
650 ] {
651 let (api, _handle) =
652 spawn(NodeConfig::test_tempo().with_no_mining(true).with_hardfork(Some(hardfork)))
653 .await;
654 for _ in 0..2 {
655 api.block_by_number(BlockNumberOrTag::Pending).await.unwrap().unwrap();
656 api.header_by_number(BlockNumberOrTag::Pending).await.unwrap().unwrap();
657 }
658 assert!(consensus(&api).latest().is_none());
659 api.mine_one().await.unwrap();
660 assert_certified_head(&api);
661 let block = api.backend.get_block(1).unwrap();
662 let native = block.header.as_tempo().unwrap();
663 assert_eq!(native.consensus_context.is_some(), api.backend.tempo_hardfork().is_t4());
664 for projected in [
665 api.block_by_number(1.into()).await.unwrap().unwrap(),
666 api.backend.block_by_hash(block.header.hash_slow()).await.unwrap().unwrap(),
667 ] {
668 let projected: TempoHeader =
669 serde_json::from_value(serde_json::to_value(projected).unwrap()).unwrap();
670 assert_eq!(&projected, native);
671 }
672 }
673 let (api, _handle) = spawn(NodeConfig::test().with_no_mining(true)).await;
674 api.mine_one().await.unwrap();
675 assert!(api.backend.blockchain.storage.read().tempo.is_none());
676 }
677
678 #[tokio::test(flavor = "multi_thread")]
679 async fn tempo_fork_startup_and_reset_sign_only_local_successors() {
680 let config = tempo_config(3);
681 let (source_api, source) = spawn(config.clone()).await;
682 source_api.anvil_mine(Some(U256::from(4)), None).await.unwrap();
683 let (api, _handle) = spawn(
684 config
685 .with_eth_rpc_url(Some(source.http_endpoint()))
686 .with_fork_block_number(Some(2u64))
687 .with_tempo_consensus_seed(Some(B256::repeat_byte(0x42))),
688 )
689 .await;
690 assert!(consensus(&api).latest().is_none());
691 api.mine_one().await.unwrap();
692 assert_certified_head(&api);
693 assert_eq!(
694 api.backend.get_block(3).unwrap().header.parent_hash(),
695 source_api.backend.get_block(2).unwrap().header.hash_slow()
696 );
697 assert_eq!(consensus(&api).latest().unwrap().1.epoch, 1);
698 let stale = api.evm_snapshot().await.unwrap();
699 api.anvil_reset(Some(Forking {
700 json_rpc_url: Some(source.http_endpoint()),
701 block_number: Some(4),
702 }))
703 .await
704 .unwrap();
705 assert!(api.backend.list_state_snapshots().is_empty());
706 assert!(!api.evm_revert(stale).await.unwrap());
707 let fresh = api.evm_snapshot().await.unwrap();
708 assert_eq!(fresh, stale);
709 assert!(consensus(&api).latest().is_none());
710 assert_eq!(consensus(&api).identity().from_epoch, 1);
711 api.mine_one().await.unwrap();
712 assert_certified_head(&api);
713 assert_eq!(api.backend.best_number(), 5);
714
715 let sender = source.dev_wallets().next().unwrap().address();
716 let hash = queue_transfer(&source_api, sender).await;
717 source_api.mine_one().await.unwrap();
718 let (replay, _replay_handle) = spawn(
719 NodeConfig::test_tempo()
720 .with_no_mining(true)
721 .with_eth_rpc_url(Some(source.http_endpoint()))
722 .with_fork_transaction_hash(Some(hash)),
723 )
724 .await;
725 assert_eq!(replay.backend.best_number(), 5);
726 assert!(consensus(&replay).latest().is_none());
727 replay.mine_one().await.unwrap();
728 assert_eq!(replay.backend.best_number(), 6);
729 assert_certified_head(&replay);
730 }
731
732 #[tokio::test]
733 async fn tempo_changed_head_import_can_rollback_and_reorg_below_anchor() {
734 let (api, _handle) = spawn(tempo_config(3)).await;
735 api.anvil_mine(Some(U256::from(4)), None).await.unwrap();
736 let imported_head = api.backend.best_hash();
737 let ancestor = api.backend.get_block(2).unwrap().header.hash_slow();
738 let state = api.backend.serialized_state(false).await.unwrap();
739 assert!(state.historical_states.is_none());
740 api.mine_one().await.unwrap();
741 assert_ne!(api.backend.best_hash(), imported_head);
742 api.backend.load_state(state.clone()).await.unwrap();
743 assert_eq!(api.backend.best_hash(), imported_head);
744 assert!(consensus(&api).latest().is_none());
745
746 api.anvil_rollback(Some(2)).await.unwrap();
748 assert_eq!(api.backend.best_number(), 2);
749 assert_eq!(api.backend.best_hash(), ancestor);
750 assert!(consensus(&api).latest().is_none());
751 api.mine_one().await.unwrap();
752 assert_certified_head(&api);
753
754 api.backend.load_state(state).await.unwrap();
756 assert_eq!(api.backend.best_hash(), imported_head);
757 assert!(consensus(&api).latest().is_none());
758 api.anvil_reorg(ReorgOptions { depth: 2, tx_block_pairs: Vec::new() }).await.unwrap();
759 assert_eq!(api.backend.best_number(), 4);
760 assert!(consensus(&api).finalization(2).is_none());
761 let replacement = consensus(&api).finalization(3).unwrap().clone();
762 assert_eq!(replacement.digest, api.backend.get_block(3).unwrap().header.hash_slow());
763 assert_certified_head(&api);
764 }
765
766 #[test]
767 fn tempo_reanchor_clears_proof_states() {
768 let mut chain = TempoChain::new(B256::ZERO, NonZeroU64::new(3).unwrap(), 0);
769 let hash = B256::repeat_byte(0x42);
770 chain.proof_states.write().insert(hash, StateDb::new(MemDb::default()));
771 assert!(chain.proof_states.read().get_state(&hash).is_some());
772
773 chain.reanchor(3);
774
775 assert!(chain.proof_states.read().get_state(&hash).is_none());
776 }
777
778 #[test]
779 fn tempo_cross_anchor_rollback_preserves_exact_snapshot_anchor() {
780 let mut chain = TempoChain::new(B256::ZERO, NonZeroU64::new(3).unwrap(), 5);
781 chain.consensus.rotate().unwrap();
782 let next_rotation = chain.consensus.clone().rotate().unwrap();
783 chain.unwind(&unsigned_tempo_block(5)).unwrap();
784 assert_eq!(chain.consensus.clone().rotate().unwrap(), next_rotation);
785 chain.snapshot(U256::ONE, None);
786 let mut expected = chain.consensus.clone();
787
788 chain.unwind(&unsigned_tempo_block(3)).unwrap();
789 assert!(chain.consensus.latest().is_none());
790 let _ = chain.restore(U256::ONE, 5);
791 assert_eq!(chain.consensus.rotate().unwrap(), expected.rotate().unwrap());
792
793 chain.unwind(&unsigned_tempo_block(4)).unwrap();
795 assert!(chain.consensus.latest().is_none());
796 let mut header = TempoHeader::default();
797 header.inner.number = 5;
798 chain.prepare(&mut header, TempoHardfork::T4).unwrap();
799 chain.finalize(&header, TempoHardfork::T4);
800 assert_eq!(chain.consensus.latest().unwrap().0, 5);
801 }
802
803 #[test]
804 fn tempo_rollback_above_anchor_preserves_pruning_error() {
805 let mut chain = TempoChain::new(B256::ZERO, NonZeroU64::new(3).unwrap(), 5);
806 for height in 6..=7 {
807 let mut header = TempoHeader::default();
808 header.inner.number = height;
809 chain.prepare(&mut header, TempoHardfork::T4).unwrap();
810 chain.finalize(&header, TempoHardfork::T4);
811 }
812 chain.consensus.prune(7).unwrap();
813 let retained = chain.consensus.finalization(7).unwrap().clone();
814 let identity = chain.consensus.identity();
815
816 let error = chain.unwind(&unsigned_tempo_block(6)).unwrap_err();
817 assert!(matches!(error, BlockchainError::Internal(message)
818 if message == "cannot rewind Tempo consensus from 7 to 6 with earliest retained height 7"));
819 assert_eq!(chain.consensus.latest(), Some((7, &retained)));
820 assert_eq!(chain.consensus.identity(), identity);
821 chain.unwind(&unsigned_tempo_block(7)).unwrap();
822 assert_eq!(chain.consensus.latest(), Some((7, &retained)));
823 }
824
825 #[tokio::test]
826 async fn tempo_unchanged_history_import_preserves_consensus_and_snapshots() {
827 for include_history in [false, true] {
828 let (api, _handle) = spawn(tempo_config(4)).await;
829 api.anvil_mine(Some(U256::from(2)), None).await.unwrap();
830 let first = consensus(&api).finalization(1).unwrap().clone();
831 let head = consensus(&api).finalization(2).unwrap().clone();
832 let account = Address::repeat_byte(0x22);
833 api.anvil_set_balance(account, U256::ONE).await.unwrap();
834 let scheduled = rotate_consensus(&api).await;
835 let snapshot = api.evm_snapshot().await.unwrap();
836 let next_rotation = consensus(&api).rotate().unwrap();
837 let mut state = api.backend.serialized_state(false).await.unwrap();
838 let mut invalid = state.clone();
839 invalid.best_block_number = Some(99);
840 let error = api.backend.load_state(invalid).await.unwrap_err();
841 assert!(matches!(error, BlockchainError::RpcError(error)
842 if error.message == "Best hash not found for best number 99"));
843 assert_eq!(consensus(&api).finalization(2), Some(&head));
844 state.accounts.get_mut(&account).unwrap().balance = U256::from(2);
845 if !include_history {
846 state = SerializableState { accounts: state.accounts, ..Default::default() };
847 }
848
849 api.backend.load_state(state).await.unwrap();
850 assert_eq!(consensus(&api).finalization(1), Some(&first));
851 assert_eq!(consensus(&api).finalization(2), Some(&head));
852 assert_eq!(consensus(&api).rotate().unwrap(), next_rotation);
853 assert_eq!(api.balance(account, None).await.unwrap(), U256::from(2));
854 assert_certified_head(&api);
855 api.anvil_mine(Some(U256::from(2)), None).await.unwrap();
856 assert_eq!(consensus(&api).identity(), scheduled);
857 assert!(api.evm_revert(snapshot).await.unwrap());
858 assert_eq!(api.balance(account, None).await.unwrap(), U256::ONE);
859 assert_eq!(consensus(&api).finalization(1), Some(&first));
860 assert_eq!(consensus(&api).finalization(2), Some(&head));
861 assert_eq!(consensus(&api).rotate().unwrap(), next_rotation);
862 assert_certified_head(&api);
863 }
864 }
865
866 #[tokio::test(flavor = "multi_thread")]
867 async fn tempo_fork_unchanged_history_import_preserves_certified_proofs() {
868 let config = NodeConfig::test_tempo().with_no_mining(true);
869 let (source_api, source) = spawn(config.clone()).await;
870 source_api.mine_one().await.unwrap();
871 for include_history in [false, true] {
872 let (api, _handle) = spawn(tempo_fork_config(config.clone(), &source)).await;
873 let account = Address::repeat_byte(0x23);
874 let keys = vec![B256::ZERO];
875 api.anvil_set_balance(account, U256::ONE).await.unwrap();
876 api.anvil_set_storage_at(account, U256::ZERO, B256::with_last_byte(1)).await.unwrap();
877 api.mine_one().await.unwrap();
878
879 let first = consensus(&api).finalization(2).unwrap().clone();
880 let original =
881 api.get_proof(account, keys.clone(), Some(BlockId::number(2))).await.unwrap();
882 assert_eq!(original.balance, U256::ONE);
883 assert_eq!(original.storage_proof[0].value, U256::ONE);
884 api.mine_one().await.unwrap();
885 let head = consensus(&api).finalization(3).unwrap().clone();
886 let head_proof = api.get_proof(account, keys.clone(), None).await.unwrap();
887 let snapshot = api.evm_snapshot().await.unwrap();
888 let mut state = api.backend.serialized_state(false).await.unwrap();
889 let imported = state.accounts.get_mut(&account).unwrap();
890 imported.balance = U256::from(2);
891 imported.storage.insert(B256::ZERO, B256::with_last_byte(2));
892 if !include_history {
893 state = SerializableState { accounts: state.accounts, ..Default::default() };
894 }
895
896 api.backend.load_state(state).await.unwrap();
897 assert_eq!(consensus(&api).finalization(2), Some(&first));
898 assert_eq!(consensus(&api).finalization(3), Some(&head));
899 assert_eq!(api.get_proof(account, keys.clone(), None).await.unwrap(), head_proof);
900 assert_eq!(
901 api.get_proof(account, keys.clone(), Some(BlockId::number(2))).await.unwrap(),
902 original
903 );
904
905 assert_eq!(
907 api.balance(account, Some(BlockId::pending())).await.unwrap(),
908 U256::from(2)
909 );
910 assert_eq!(
911 api.storage_at(account, U256::ZERO, Some(BlockId::pending())).await.unwrap(),
912 B256::with_last_byte(2)
913 );
914 api.mine_one().await.unwrap();
915 let updated = api.get_proof(account, keys.clone(), None).await.unwrap();
916 assert_eq!(updated.balance, U256::from(2));
917 assert_eq!(updated.storage_proof[0].value, U256::from(2));
918 assert_certified_head(&api);
919
920 assert!(api.evm_revert(snapshot).await.unwrap());
921 assert_eq!(api.balance(account, None).await.unwrap(), U256::ONE);
922 assert_eq!(consensus(&api).finalization(2), Some(&first));
923 assert_eq!(consensus(&api).finalization(3), Some(&head));
924 assert_eq!(api.get_proof(account, keys.clone(), None).await.unwrap(), head_proof);
925 assert_eq!(
926 api.get_proof(account, keys, Some(BlockId::number(2))).await.unwrap(),
927 original
928 );
929 }
930 }
931
932 #[tokio::test]
933 async fn tempo_cross_import_snapshot_does_not_restore_changed_ancestor_certificate() {
934 let (api, _handle) = spawn(NodeConfig::test_tempo().with_no_mining(true)).await;
935 api.anvil_mine(Some(U256::from(2)), None).await.unwrap();
936 let head = api.backend.best_hash();
937 let signed_ancestor = consensus(&api).finalization(1).unwrap().digest;
938 let snapshot = api.evm_snapshot().await.unwrap();
939 let mut state = api.backend.serialized_state(false).await.unwrap();
940 let ancestor = state.blocks.iter_mut().find(|block| block.header.number() == 1).unwrap();
941 let FoundryHeader::Tempo(header) = &mut ancestor.header else {
942 panic!("expected Tempo header");
943 };
944 header.inner.timestamp += 1;
945 let replacement = ancestor.header.hash_slow();
946 assert_ne!(replacement, signed_ancestor);
947
948 api.backend.load_state(state).await.unwrap();
949 assert_eq!(api.backend.best_hash(), head);
950 assert_eq!(api.backend.get_block(1).unwrap().header.hash_slow(), replacement);
951 assert!(consensus(&api).latest().is_none());
952 assert!(api.evm_revert(snapshot).await.unwrap());
953 assert_eq!(api.backend.best_hash(), head);
954 assert_eq!(api.backend.get_block(1).unwrap().header.hash_slow(), replacement);
955 assert!(consensus(&api).latest().is_none());
956 assert!(consensus(&api).finalization(1).is_none());
957 api.mine_one().await.unwrap();
958 assert_certified_head(&api);
959 }
960
961 #[tokio::test]
962 async fn tempo_import_preserves_backend_snapshot_created_after_staging() {
963 let (api, _handle) = spawn(NodeConfig::test_tempo().with_no_mining(true)).await;
964 api.mine_one().await.unwrap();
965 let db = api.backend.db.write().await;
966
967 let mut snapshot = Box::pin(api.backend.create_state_snapshot());
970 assert!(futures::poll!(snapshot.as_mut()).is_pending());
971 let mut import = Box::pin(api.backend.load_state(SerializableState::default()));
972 assert!(futures::poll!(import.as_mut()).is_pending());
973 drop(db);
974
975 let snapshot = timeout(Duration::from_secs(10), snapshot).await.unwrap();
976 assert!(timeout(Duration::from_secs(10), import).await.unwrap().unwrap());
977 assert!(
978 timeout(Duration::from_secs(10), api.backend.revert_state_snapshot(snapshot))
979 .await
980 .unwrap()
981 .unwrap()
982 );
983 assert_certified_head(&api);
984 }
985
986 #[test]
987 fn tempo_cross_import_snapshots_preserve_revert_order() {
988 let mut chain = TempoChain::new(B256::ZERO, NonZeroU64::new(3).unwrap(), 0);
989 chain.snapshot(U256::ONE, None);
990 chain.reanchor(3);
991 assert!(chain.snapshots.is_empty());
992 chain.snapshot(U256::from(2), None);
993 chain.snapshot(U256::from(3), None);
994 let expected = chain.consensus.clone().rotate().unwrap();
995 chain.consensus.rotate().unwrap();
996 chain.snapshot(U256::from(4), None);
997 let _ = chain.restore(U256::from(3), 3);
998 assert_eq!(chain.consensus.rotate().unwrap(), expected);
999 assert_eq!(chain.snapshots.keys().copied().collect::<Vec<_>>(), vec![U256::from(2)]);
1000
1001 let _ = chain.restore(U256::ONE, 1);
1003 assert!(chain.snapshots.is_empty());
1004 assert_eq!(chain.consensus.anchor_height(), 1);
1005 assert!(chain.consensus.latest().is_none());
1006 }
1007
1008 fn tempo_config(epoch_length: u64) -> NodeConfig {
1009 NodeConfig::test_tempo()
1010 .with_no_mining(true)
1011 .with_tempo_epoch_length(NonZeroU64::new(epoch_length).unwrap())
1012 }
1013
1014 async fn queue_transfer(api: &EthApi<FoundryNetwork>, sender: Address) -> B256 {
1015 api.send_transaction(WithOtherFields::new(
1016 TransactionRequest::default().from(sender).to(Address::repeat_byte(0x11)),
1017 ))
1018 .await
1019 .unwrap()
1020 }
1021
1022 async fn rotate_consensus(api: &EthApi<FoundryNetwork>) -> TempoConsensusIdentity {
1023 api.anvil_tempo_rotate_consensus_identity().await.unwrap()
1024 }
1025
1026 fn unsigned_tempo_block(height: u64) -> Block {
1027 let mut header = TempoHeader::default();
1028 header.inner.number = height;
1029 Block::new(header.into(), Default::default())
1030 }
1031
1032 #[tokio::test(flavor = "multi_thread")]
1033 async fn tempo_fork_proof_state_survives_disk_roundtrip() {
1034 let config = NodeConfig::test_tempo()
1035 .with_no_mining(true)
1036 .with_no_storage_caching(true)
1037 .with_max_persisted_states(Some(4usize));
1038 let (source_api, source) = spawn(config.clone()).await;
1039 source_api.mine_one().await.unwrap();
1040 let (api, _handle) = spawn(tempo_fork_config(config, &source)).await;
1041 let proof_states = api.backend.tempo_proof_states().unwrap();
1042 let address = Address::repeat_byte(0x31);
1043 let absent = Address::repeat_byte(0x32);
1044 let keys = [B256::ZERO, B256::with_last_byte(1)];
1045 api.anvil_set_balance(address, U256::from(42)).await.unwrap();
1046 api.anvil_set_code(address, alloy_primitives::bytes!("60005460005260206000f3"))
1047 .await
1048 .unwrap();
1049 api.anvil_set_storage_at(address, U256::ZERO, B256::with_last_byte(7)).await.unwrap();
1050 api.backend
1051 .db
1052 .write()
1053 .await
1054 .maybe_as_full_db_mut()
1055 .unwrap()
1056 .insert(absent, DbAccount::new_not_existing());
1057 api.mine_one().await.unwrap();
1058 let certified = api.backend.tempo_finalization(Some(2)).await.unwrap();
1059 let root = certified.block.header.state_root();
1060 let hash = certified.finalization.digest;
1061 let original =
1062 api.backend.prove_tempo_account_at(address, &keys, 2).await.unwrap().unwrap();
1063 let excluded = api.backend.prove_tempo_account_at(absent, &keys, 2).await.unwrap().unwrap();
1064 assert_eq!(original.balance, U256::from(42));
1065 assert_eq!(original.storage_proof[0].value, U256::from(7));
1066 assert_eq!(original.storage_proof[1].value, U256::ZERO);
1067
1068 for spilled in [false, true] {
1069 if spilled {
1070 api.anvil_set_storage_at(address, U256::ZERO, B256::with_last_byte(9))
1071 .await
1072 .unwrap();
1073 api.mine_one().await.unwrap();
1074 let mut states = proof_states.write();
1075 states.set_cache_limit(1);
1076 states.insert(B256::repeat_byte(0x99), StateDb::new(MemDb::default()));
1077 assert!(states.get_state(&hash).is_none());
1078 let restored = states.get_on_disk_state(&hash).unwrap();
1079 assert_eq!(
1080 restored.maybe_as_full_db().unwrap()[&absent].account_state,
1081 AccountState::NotExisting,
1082 );
1083 }
1084 let proof =
1085 api.backend.prove_tempo_account_at(address, &keys, 2).await.unwrap().unwrap();
1086 assert_eq!(proof, original);
1087 assert_eq!(keccak256(&proof.account_proof[0]), root);
1088 let account =
1089 TrieAccount::new(proof.nonce, proof.balance, proof.storage_hash, proof.code_hash);
1090 verify_proof(
1091 root,
1092 Nibbles::unpack(keccak256(address)),
1093 Some(alloy_rlp::encode(account)),
1094 &proof.account_proof,
1095 )
1096 .unwrap();
1097 for storage in &proof.storage_proof {
1098 verify_proof(
1099 proof.storage_hash,
1100 Nibbles::unpack(keccak256(storage.key.as_b256())),
1101 (!storage.value.is_zero()).then(|| alloy_rlp::encode(storage.value)),
1102 &storage.proof,
1103 )
1104 .unwrap();
1105 }
1106 let proof =
1107 api.backend.prove_tempo_account_at(absent, &keys, 2).await.unwrap().unwrap();
1108 assert_eq!(proof, excluded);
1109 verify_proof(root, Nibbles::unpack(keccak256(absent)), None, &proof.account_proof)
1110 .unwrap();
1111 let retained = api.backend.tempo_finalization(Some(2)).await.unwrap();
1112 assert_eq!(retained.finalization, certified.finalization);
1113 assert_eq!(retained.block.header.state_root(), root);
1114 }
1115 }
1116
1117 #[tokio::test(flavor = "multi_thread")]
1118 async fn tempo_fork_snapshot_restores_evicted_proof_state() {
1119 let config = NodeConfig::test_tempo().with_no_mining(true).set_pruned_history(Some(None));
1120 let (source_api, source) = spawn(config.clone()).await;
1121 source_api.mine_one().await.unwrap();
1122 let (api, handle) =
1123 spawn(tempo_fork_config(config.with_no_storage_caching(true), &source)).await;
1124 let provider = handle.http_provider();
1125 let address = Address::repeat_byte(0x33);
1126 api.anvil_set_storage_at(address, U256::ZERO, B256::with_last_byte(1)).await.unwrap();
1127 api.mine_one().await.unwrap();
1128 let certified = api.backend.tempo_finalization(Some(2)).await.unwrap();
1129 let original = provider.get_proof(address, vec![B256::ZERO]).await.unwrap();
1130 assert_eq!(original.storage_proof[0].value, U256::ONE);
1131
1132 api.anvil_set_storage_at(address, U256::ZERO, B256::with_last_byte(9)).await.unwrap();
1134 let snapshot = api.evm_snapshot().await.unwrap();
1135 api.mine_one().await.unwrap();
1136 assert!(api.evm_revert(snapshot).await.unwrap());
1137 assert_eq!(
1138 api.backend.tempo_finalization(Some(2)).await.unwrap().finalization,
1139 certified.finalization
1140 );
1141 assert_eq!(provider.get_proof(address, vec![B256::ZERO]).await.unwrap(), original);
1142 assert_eq!(provider.get_storage_at(address, U256::ZERO).await.unwrap(), U256::from(9));
1143 }
1144
1145 fn tempo_fork_config(config: NodeConfig, source: &crate::NodeHandle) -> NodeConfig {
1146 config.with_eth_rpc_url(Some(source.http_endpoint())).with_fork_block_number(Some(1u64))
1147 }
1148}