Skip to main content

anvil/eth/backend/mem/
tempo.rs

1//! Tempo consensus owned by the canonical in-memory chain.
2//!
3//! Mining checks consensus metadata before committing execution and signs the final header in the
4//! same storage write guard that publishes its block. Cache commits and state-root computation do
5//! not hold the storage lock, so readers can still access the published chain.
6//!
7//! Imports that change canonical history start an unsigned signing segment. Database snapshots
8//! remain usable, but reverting across that import boundary starts a new segment at the restored
9//! height. Snapshots within the current segment restore consensus exactly.
10//!
11//! All live consensus mutations, including rotation RPCs, must acquire the database write
12//! lock before the storage lock. Mining retains that database guard from preparation through
13//! publication, preventing snapshots, rewinds, resets, imports, or rotations from changing the
14//! prepared height or key schedule. Only initialization before the backend is shared and private
15//! staged copies are exempt; publishing a staged copy still requires the database write lock.
16//! Consensus readers acquire the database read lock before storage.
17//!
18//! Historical queries can use the trusted fork provider at or below its pinned height. They
19//! snapshot the provider, root, and known canonical hash together with local history, then release
20//! all guards before network I/O. Upstream blocks must match that canonical hash when known.
21//! An overlapping reset can complete while the request returns the old snapshot;
22//! upstream responses are never cached into the replacement chain. Upstream committees remain
23//! independent of the local development identity. Height-bounded forwarding does not establish
24//! an authenticated committee or ancestry chain from upstream history to locally signed blocks.
25//!
26//! Locally mined fork headers seal the same partial account and storage trie used by proofs.
27//! Its snapshots live in a separate bounded history cache, sharing the execution history's cache
28//! settings but preserving local trie membership through disk round trips. Only a canonical local
29//! certificate can select a snapshot. Rewinds remove orphaned proof states. Resets clear proof
30//! history before a replacement signing history starts.
31
32use 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/// Local certificates, key schedule, and consensus snapshots for the current signing history.
62#[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    /// Immutable local tries sealed by locally mined fork blocks, with bounded retention.
70    pub(super) proof_states: Arc<RwLock<InMemoryBlockStates>>,
71    /// Incremental trie of the latest sealed account set, updated by diffing the next seal.
72    pub(super) proof_root: Arc<Mutex<(StateRootCache, AddressMap<DbAccount>)>>,
73}
74
75/// Consensus and the sealed head trie retained by an explicit EVM snapshot.
76#[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    /// Resets local trust at an unsigned chain head and clears saved consensus histories.
97    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    /// Restores saved consensus, or starts unsigned if an import discarded that history.
117    ///
118    /// The caller supplies the saved database height, which may differ from the imported head,
119    /// and restores the returned proof state after releasing the chain storage lock.
120    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    /// Rewinds signed history or starts a new unsigned segment below the imported anchor.
134    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            // Exact snapshots retain their own controller and anchor across ordinary rollbacks.
140            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    /// Checks every fallible consensus input before execution is committed.
153    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    /// Signs a prepared header while its block is published under the same storage write guard.
164    ///
165    /// The caller must retain the database write guard from preparation through publication and
166    /// only install the execution state root in the prepared header, using the same hardfork.
167    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    /// Retains the latest complete body independently of historical transaction pruning.
174    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    /// Returns the currently active public development committee.
189    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    /// Returns a local certificate, or pinned fork history when no local certificate exists.
198    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    /// Returns local latest, falling back to the pinned upstream height before local mining.
208    ///
209    /// Standalone unsigned history returns `None`; upstream errors remain errors.
210    pub async fn tempo_latest_finalization(
211        &self,
212    ) -> Result<Option<TempoCertifiedBlock>, BlockchainError> {
213        self.tempo_certified_block(None).await
214    }
215
216    /// Schedules a development identity change without racing canonical mining or rewinds.
217    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            // Capture one fork snapshot while reset is excluded; release guards before network I/O.
265            (
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    /// Proves against the immutable local trie committed by a locally certified fork block.
297    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        // Canonical selection uses the database guard; release it before loading and hashing a
321        // historical trie so mining and other database writers are not blocked by proof work.
322        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        // Loading a disk state mutates the cache, so the write guard is required only for the
330        // load. Clone the loaded account map once, then release the exclusive guard before the
331        // potentially expensive trie construction.
332        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        // The exact snapshot retains the pending key and derivation counter.
552        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        // The same process retains execution history even when the dump omits it.
747        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        // Importing the saved head again creates another unsigned signing segment.
755        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        // Restoring the snapshot restores anchor 5, so height 4 is an unsigned ancestor again.
794        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            // Imported state belongs to pending execution and the next certified block.
906            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        // Raw backend snapshots use the database guard; RPC snapshots also hold the mining guard.
968        // Queue the snapshot first, then stage the import before either can mutate the database.
969        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        // Restoring a pre-import snapshot uses its database height, not the current anchor.
1002        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        // The execution snapshot includes an unmined edit, while the sealed trie does not.
1133        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}