Skip to main content

anvil/eth/backend/mem/
storage.rs

1//! In-memory blockchain storage
2use crate::eth::{
3    backend::{
4        db::{
5            MaybeFullDatabase, SerializableBlock, SerializableHistoricalStates,
6            SerializableTransaction, StateDb,
7        },
8        mem::cache::DiskStateCache,
9    },
10    pool::transactions::PoolTransaction,
11};
12use alloy_consensus::{BlockHeader, Header, constants::EMPTY_WITHDRAWALS};
13use alloy_eips::eip7685::EMPTY_REQUESTS_HASH;
14use alloy_evm::EvmEnv;
15use alloy_network::Network;
16use alloy_primitives::{
17    B256, Bytes, U256,
18    map::{B256HashMap, HashMap},
19};
20use alloy_rpc_types::{
21    BlockId, BlockNumberOrTag, TransactionInfo as RethTransactionInfo,
22    trace::{
23        otterscan::{InternalOperation, OperationType},
24        parity::LocalizedTransactionTrace,
25    },
26};
27use anvil_core::eth::{
28    block::{Block, create_block},
29    transaction::{MaybeImpersonatedTransaction, TransactionInfo},
30};
31use foundry_evm::{
32    backend::MemDb,
33    traces::{CallKind, ParityTraceBuilder, TracingInspectorConfig},
34};
35#[cfg(test)]
36use foundry_primitives::FoundryNetwork;
37use foundry_primitives::{FoundryHeader, FoundryReceiptEnvelope, FoundryTxEnvelope};
38use parking_lot::RwLock;
39use revm::{context::Block as RevmBlock, primitives::hardfork::SpecId};
40use std::{collections::VecDeque, fmt, path::PathBuf, sync::Arc, time::Duration};
41// use yansi::Paint;
42
43// === various limits in number of blocks ===
44
45pub const DEFAULT_HISTORY_LIMIT: usize = 500;
46const MIN_HISTORY_LIMIT: usize = 10;
47// 1hr of up-time at lowest 1s interval
48const MAX_ON_DISK_HISTORY_LIMIT: usize = 3_600;
49
50/// Represents the complete state of single block
51pub struct InMemoryBlockStates {
52    /// The states at a certain block
53    states: B256HashMap<StateDb>,
54    /// Older states in the secondary history tier.
55    ///
56    /// Structurally shared states remain here directly; other database types move their data to
57    /// disk and keep an empty state object here for loading it.
58    on_disk_states: B256HashMap<StateDb>,
59    /// How many states to store at most
60    in_memory_limit: usize,
61    /// minimum amount of states we keep in memory
62    min_in_memory_limit: usize,
63    /// maximum amount of states we keep on disk
64    ///
65    /// Limiting the states will prevent disk blow up, especially in interval mining mode
66    max_on_disk_limit: usize,
67    /// the oldest states written to disk
68    oldest_on_disk: VecDeque<B256>,
69    /// all states present, used to enforce `in_memory_limit`
70    present: VecDeque<B256>,
71    /// Stores old states on disk
72    disk_cache: DiskStateCache,
73}
74
75impl InMemoryBlockStates {
76    /// Creates a new instance with limited slots
77    pub fn new(in_memory_limit: usize, on_disk_limit: usize) -> Self {
78        let in_memory_limit = in_memory_limit.max(1);
79        Self {
80            states: Default::default(),
81            on_disk_states: Default::default(),
82            in_memory_limit,
83            min_in_memory_limit: in_memory_limit.min(MIN_HISTORY_LIMIT),
84            max_on_disk_limit: on_disk_limit,
85            oldest_on_disk: Default::default(),
86            present: Default::default(),
87            disk_cache: Default::default(),
88        }
89    }
90
91    /// Configures no disk caching
92    pub const fn memory_only(mut self) -> Self {
93        self.max_on_disk_limit = 0;
94        self
95    }
96
97    /// Configures the path on disk where the states will cached.
98    pub fn disk_path(mut self, path: PathBuf) -> Self {
99        self.disk_cache = self.disk_cache.with_path(path);
100        self
101    }
102
103    /// This modifies the `limit` what to keep stored in memory.
104    ///
105    /// This will ensure the new limit adjusts based on the block time.
106    /// The lowest blocktime is 1s which should increase the limit slightly
107    pub fn update_interval_mine_block_time(&mut self, block_time: Duration) {
108        let block_time = block_time.as_secs();
109        // for block times lower than 2s we increase the mem limit since we're mining _small_ blocks
110        // very fast
111        // this will gradually be decreased once the max limit was reached
112        if block_time <= 2 {
113            self.in_memory_limit = DEFAULT_HISTORY_LIMIT * 3;
114            self.enforce_limits();
115        }
116    }
117
118    /// Returns true if only memory caching is supported.
119    const fn is_memory_only(&self) -> bool {
120        self.max_on_disk_limit == 0
121    }
122
123    /// Inserts a new (hash -> state) pair
124    ///
125    /// When the configured limit for the number of states that can be stored in memory is reached,
126    /// the oldest state is removed.
127    ///
128    /// Database types without structural sharing gradually move snapshots to disk as the chain
129    /// grows. Structurally shared snapshots move into the secondary tier without serialization.
130    ///
131    /// When a state that was previously written to disk is requested, it is simply read from disk.
132    pub fn insert(&mut self, hash: B256, state: StateDb) {
133        if !self.is_memory_only() && self.present.len() >= self.in_memory_limit {
134            // once we hit the max limit we gradually decrease it
135            self.in_memory_limit =
136                self.in_memory_limit.saturating_sub(1).max(self.min_in_memory_limit);
137        }
138
139        self.enforce_limits();
140
141        self.states.insert(hash, state);
142        self.present.push_back(hash);
143    }
144
145    /// Enforces configured limits
146    fn enforce_limits(&mut self) {
147        // enforce memory limits
148        while self.present.len() >= self.in_memory_limit {
149            // evict the oldest block
150            if let Some((hash, mut state)) = self
151                .present
152                .pop_front()
153                .and_then(|hash| self.states.remove(&hash).map(|state| (hash, state)))
154            {
155                // only write to disk if supported
156                if !self.is_memory_only() {
157                    if state.is_persistent() {
158                        self.on_disk_states.insert(hash, state);
159                        self.oldest_on_disk.push_back(hash);
160                        continue;
161                    }
162
163                    let state_snapshot = state.0.clear_into_state_snapshot();
164                    if self.disk_cache.write(hash, &state_snapshot) {
165                        // Write succeeded, move state to on-disk tracking
166                        self.on_disk_states.insert(hash, state);
167                        self.oldest_on_disk.push_back(hash);
168                    } else {
169                        // Write failed, restore state to memory to avoid data loss
170                        state.init_from_state_snapshot(state_snapshot);
171                        self.states.insert(hash, state);
172                        self.present.push_front(hash);
173                        // Increase limit temporarily to prevent infinite retry loop
174                        self.in_memory_limit = self.in_memory_limit.saturating_add(1);
175                        break;
176                    }
177                }
178            }
179        }
180
181        // enforce on disk limit and purge the oldest state cached on disk
182        while !self.is_memory_only() && self.oldest_on_disk.len() >= self.max_on_disk_limit {
183            // evict the oldest block
184            if let Some(hash) = self.oldest_on_disk.pop_front()
185                && self.on_disk_states.remove(&hash).is_some_and(|state| !state.is_persistent())
186            {
187                self.disk_cache.remove(hash);
188            }
189        }
190    }
191
192    /// Returns the in-memory state for the given `hash` if present
193    pub fn get_state(&self, hash: &B256) -> Option<&StateDb> {
194        self.states.get(hash)
195    }
196
197    /// Returns on-disk state for the given `hash` if present
198    pub fn get_on_disk_state(&mut self, hash: &B256) -> Option<&StateDb> {
199        if let Some(state) = self.on_disk_states.get_mut(hash) {
200            if state.is_persistent() {
201                return Some(state);
202            }
203
204            let cached = self.disk_cache.read(*hash)?;
205            state.init_from_state_snapshot(cached);
206            return Some(state);
207        }
208
209        None
210    }
211
212    /// Sets the maximum number of stats we keep in memory
213    pub const fn set_cache_limit(&mut self, limit: usize) {
214        let limit = if limit == 0 { 1 } else { limit };
215        self.in_memory_limit = limit;
216        self.min_in_memory_limit =
217            if limit < MIN_HISTORY_LIMIT { limit } else { MIN_HISTORY_LIMIT };
218    }
219
220    /// Clears all entries
221    pub fn clear(&mut self) {
222        self.states.clear();
223        self.present.clear();
224        self.oldest_on_disk.clear();
225        for (hash, state) in std::mem::take(&mut self.on_disk_states) {
226            if !state.is_persistent() {
227                self.disk_cache.remove(hash);
228            }
229        }
230    }
231
232    /// Removes states for the given block hashes.
233    ///
234    /// This is used during chain rollback to clean up states for blocks that are no longer part
235    /// of the canonical chain.
236    pub fn remove_block_states(&mut self, hashes: &[B256]) {
237        for hash in hashes {
238            self.states.remove(hash);
239            if self.on_disk_states.remove(hash).is_some_and(|state| !state.is_persistent()) {
240                self.disk_cache.remove(*hash);
241            }
242        }
243        self.present.retain(|h| !hashes.contains(h));
244        self.oldest_on_disk.retain(|h| !hashes.contains(h));
245    }
246
247    /// Serialize all states to a list of serializable historical states
248    pub fn serialized_states(&mut self) -> SerializableHistoricalStates {
249        // Get in-memory states
250        let mut states = self
251            .states
252            .iter_mut()
253            .map(|(hash, state)| (*hash, state.serialize_state()))
254            .collect::<Vec<_>>();
255
256        // Get on-disk state snapshots
257        for (hash, state) in &mut self.on_disk_states {
258            if state.is_persistent() {
259                states.push((*hash, state.serialize_state()));
260            } else if let Some(state_snapshot) = self.disk_cache.read(*hash) {
261                states.push((*hash, state_snapshot));
262            }
263        }
264
265        SerializableHistoricalStates::new(states)
266    }
267
268    /// Load states from serialized data
269    pub fn load_states(&mut self, states: SerializableHistoricalStates) {
270        for (hash, state_snapshot) in states {
271            let mut state_db = StateDb::new(MemDb::default());
272            state_db.init_from_state_snapshot(state_snapshot);
273            self.insert(hash, state_db);
274        }
275    }
276}
277
278impl fmt::Debug for InMemoryBlockStates {
279    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280        f.debug_struct("InMemoryBlockStates")
281            .field("in_memory_limit", &self.in_memory_limit)
282            .field("min_in_memory_limit", &self.min_in_memory_limit)
283            .field("max_on_disk_limit", &self.max_on_disk_limit)
284            .field("oldest_on_disk", &self.oldest_on_disk)
285            .field("present", &self.present)
286            .finish_non_exhaustive()
287    }
288}
289
290impl Default for InMemoryBlockStates {
291    fn default() -> Self {
292        // enough in memory to store `DEFAULT_HISTORY_LIMIT` blocks in memory
293        Self::new(DEFAULT_HISTORY_LIMIT, MAX_ON_DISK_HISTORY_LIMIT)
294    }
295}
296
297/// Stores the blockchain data (blocks, transactions)
298#[derive(Clone, Debug)]
299pub struct BlockchainStorage<N: Network> {
300    /// all stored blocks (block hash -> block)
301    pub blocks: B256HashMap<Block>,
302    /// mapping from block number -> block hash
303    pub hashes: HashMap<u64, B256>,
304    /// The current best hash
305    pub best_hash: B256,
306    /// The current best block number
307    pub best_number: u64,
308    /// genesis hash of the chain
309    pub genesis_hash: B256,
310    /// genesis block number of the chain
311    pub genesis_number: u64,
312    /// Mapping from the transaction hash to a tuple containing the transaction as well as the
313    /// transaction receipt
314    pub transactions: B256HashMap<MinedTransaction<N>>,
315    /// The total difficulty of the chain until this block
316    pub total_difficulty: U256,
317}
318
319impl<N: Network> BlockchainStorage<N> {
320    /// Creates a new storage with a genesis block
321    pub fn new(
322        evm_env: &EvmEnv,
323        base_fee: Option<u64>,
324        timestamp: u64,
325        genesis_number: u64,
326        is_tempo: bool,
327    ) -> Self {
328        let is_shanghai = *evm_env.spec_id() >= SpecId::SHANGHAI;
329        let is_cancun = *evm_env.spec_id() >= SpecId::CANCUN;
330        let is_prague = *evm_env.spec_id() >= SpecId::PRAGUE;
331
332        // create a dummy genesis block
333        let header = Header {
334            timestamp,
335            base_fee_per_gas: base_fee,
336            gas_limit: evm_env.block_env.gas_limit,
337            beneficiary: evm_env.block_env.beneficiary,
338            difficulty: evm_env.block_env.difficulty,
339            blob_gas_used: evm_env.block_env.blob_excess_gas_and_price.as_ref().map(|_| 0),
340            excess_blob_gas: evm_env.block_env.blob_excess_gas(),
341            number: genesis_number,
342            parent_beacon_block_root: is_cancun.then_some(Default::default()),
343            withdrawals_root: is_shanghai.then_some(EMPTY_WITHDRAWALS),
344            requests_hash: is_prague.then_some(EMPTY_REQUESTS_HASH),
345            ..Default::default()
346        };
347        let block = create_block(
348            FoundryHeader::new(header, is_tempo),
349            Vec::<MaybeImpersonatedTransaction<FoundryTxEnvelope>>::new(),
350        );
351        let genesis_hash = block.header.hash_slow();
352        let best_hash = genesis_hash;
353        let best_number = genesis_number;
354
355        let mut blocks = B256HashMap::default();
356        blocks.insert(genesis_hash, block);
357
358        let mut hashes = HashMap::default();
359        hashes.insert(best_number, genesis_hash);
360        Self {
361            blocks,
362            hashes,
363            best_hash,
364            best_number,
365            genesis_hash,
366            genesis_number,
367            transactions: Default::default(),
368            total_difficulty: Default::default(),
369        }
370    }
371
372    pub fn forked(block_number: u64, block_hash: B256, total_difficulty: U256) -> Self {
373        let mut hashes = HashMap::default();
374        hashes.insert(block_number, block_hash);
375
376        Self {
377            blocks: B256HashMap::default(),
378            hashes,
379            best_hash: block_hash,
380            best_number: block_number,
381            genesis_hash: Default::default(),
382            genesis_number: 0,
383            transactions: Default::default(),
384            total_difficulty,
385        }
386    }
387
388    /// Unwind the chain state back to the given block in storage.
389    ///
390    /// The block identified by `block_number` and `block_hash` is __non-inclusive__, i.e. it will
391    /// remain in the state.
392    pub fn unwind_to(&mut self, block_number: u64, block_hash: B256) -> Vec<Block> {
393        let mut removed = vec![];
394        let best_num: u64 = self.best_number;
395        for i in (block_number + 1)..=best_num {
396            if let Some(hash) = self.hashes.get(&i).copied() {
397                // First remove the block's transactions while the mappings still exist
398                self.remove_block_transactions_by_number(i);
399
400                // Now remove the block from storage (may already be empty of txs) and drop mapping
401                if let Some(block) = self.blocks.remove(&hash) {
402                    removed.push(block);
403                }
404                self.hashes.remove(&i);
405            }
406        }
407        self.best_hash = block_hash;
408        self.best_number = block_number;
409        removed
410    }
411
412    pub fn empty() -> Self {
413        Self {
414            blocks: Default::default(),
415            hashes: Default::default(),
416            best_hash: Default::default(),
417            best_number: Default::default(),
418            genesis_hash: Default::default(),
419            genesis_number: Default::default(),
420            transactions: Default::default(),
421            total_difficulty: Default::default(),
422        }
423    }
424
425    /// Removes all stored transactions for the given block number
426    pub fn remove_block_transactions_by_number(&mut self, num: u64) {
427        if let Some(hash) = self.hashes.get(&num).copied() {
428            self.remove_block_transactions(hash);
429        }
430    }
431
432    /// Removes all stored transactions for the given block hash
433    pub fn remove_block_transactions(&mut self, block_hash: B256) {
434        if let Some(block) = self.blocks.get_mut(&block_hash) {
435            for tx in &block.body.transactions {
436                self.transactions.remove(&tx.hash());
437            }
438            block.body.transactions.clear();
439        }
440    }
441
442    /// Serialize all blocks in storage
443    pub fn serialized_blocks(&self) -> Vec<SerializableBlock> {
444        self.blocks.values().map(|block| block.clone().into()).collect()
445    }
446
447    /// Deserialize and add all blocks data to the backend storage
448    pub fn load_blocks(&mut self, serializable_blocks: Vec<SerializableBlock>) {
449        for serializable_block in &serializable_blocks {
450            let block: Block = serializable_block.clone().into();
451            let block_hash = block.header.hash_slow();
452            let block_number = block.header.number();
453            self.blocks.insert(block_hash, block);
454            self.hashes.insert(block_number, block_hash);
455
456            // Update genesis_hash if we are loading the genesis block, so that
457            // Finalized/Safe/Earliest block tag lookups return the correct hash. The genesis
458            // number can be non-zero when configured via `--block-number`.
459            // See: https://github.com/foundry-rs/foundry/issues/12645
460            if block_number == self.genesis_number {
461                self.genesis_hash = block_hash;
462            }
463        }
464    }
465
466    /// Returns the hash for [BlockNumberOrTag]
467    pub fn hash(&self, number: BlockNumberOrTag, slots_in_an_epoch: u64) -> Option<B256> {
468        match number {
469            BlockNumberOrTag::Latest => Some(self.best_hash),
470            BlockNumberOrTag::Earliest => Some(self.genesis_hash),
471            BlockNumberOrTag::Pending => None,
472            BlockNumberOrTag::Number(num) => self.hashes.get(&num).copied(),
473            BlockNumberOrTag::Safe => {
474                if self.best_number > slots_in_an_epoch {
475                    self.hashes.get(&(self.best_number - slots_in_an_epoch)).copied()
476                } else {
477                    Some(self.genesis_hash)
478                }
479            }
480            BlockNumberOrTag::Finalized => {
481                if self.best_number > slots_in_an_epoch * 2 {
482                    self.hashes.get(&(self.best_number - slots_in_an_epoch * 2)).copied()
483                } else {
484                    Some(self.genesis_hash)
485                }
486            }
487        }
488    }
489}
490
491impl<N: Network<ReceiptEnvelope = FoundryReceiptEnvelope>> BlockchainStorage<N> {
492    pub fn serialized_transactions(&self) -> Vec<SerializableTransaction> {
493        self.transactions.values().map(|tx: &MinedTransaction<N>| tx.clone().into()).collect()
494    }
495
496    /// Deserialize and add all transactions data to the backend storage
497    pub fn load_transactions(&mut self, serializable_transactions: Vec<SerializableTransaction>) {
498        for serializable_transaction in &serializable_transactions {
499            let transaction: MinedTransaction<N> = serializable_transaction.clone().into();
500            self.transactions.insert(transaction.info.transaction_hash, transaction);
501        }
502    }
503}
504
505/// A simple in-memory blockchain
506#[derive(Clone, Debug)]
507pub struct Blockchain<N: Network> {
508    /// underlying storage that supports concurrent reads
509    pub storage: Arc<RwLock<BlockchainStorage<N>>>,
510}
511
512impl<N: Network> Blockchain<N> {
513    /// Creates a new storage with a genesis block
514    pub fn new(
515        evm_env: &EvmEnv,
516        base_fee: Option<u64>,
517        timestamp: u64,
518        genesis_number: u64,
519        is_tempo: bool,
520    ) -> Self {
521        Self {
522            storage: Arc::new(RwLock::new(BlockchainStorage::new(
523                evm_env,
524                base_fee,
525                timestamp,
526                genesis_number,
527                is_tempo,
528            ))),
529        }
530    }
531
532    pub fn forked(block_number: u64, block_hash: B256, total_difficulty: U256) -> Self {
533        Self {
534            storage: Arc::new(RwLock::new(BlockchainStorage::forked(
535                block_number,
536                block_hash,
537                total_difficulty,
538            ))),
539        }
540    }
541
542    /// returns the header hash of given block
543    pub fn hash(&self, id: BlockId, slots_in_an_epoch: u64) -> Option<B256> {
544        match id {
545            BlockId::Hash(h) => Some(h.block_hash),
546            BlockId::Number(num) => self.storage.read().hash(num, slots_in_an_epoch),
547        }
548    }
549
550    pub fn get_block_by_hash(&self, hash: &B256) -> Option<Block> {
551        self.storage.read().blocks.get(hash).cloned()
552    }
553
554    pub fn get_transaction_by_hash(&self, hash: &B256) -> Option<MinedTransaction<N>> {
555        self.storage.read().transactions.get(hash).cloned()
556    }
557
558    /// Returns the total number of blocks
559    pub fn blocks_count(&self) -> usize {
560        self.storage.read().blocks.len()
561    }
562}
563
564/// Represents the outcome of mining a new block
565pub struct MinedBlockOutcome<T> {
566    /// The block that was mined
567    pub block_number: u64,
568    /// All transactions included in the block
569    pub included: Vec<Arc<PoolTransaction<T>>>,
570    /// All transactions that were attempted to be included but were invalid at the time of
571    /// execution
572    pub invalid: Vec<Arc<PoolTransaction<T>>>,
573    /// Transactions skipped because they're not yet valid (e.g., valid_after in the future).
574    /// These remain in the pool and should be retried later.
575    pub not_yet_valid: Vec<Arc<PoolTransaction<T>>>,
576}
577
578impl<T> Clone for MinedBlockOutcome<T> {
579    fn clone(&self) -> Self {
580        Self {
581            block_number: self.block_number,
582            included: self.included.clone(),
583            invalid: self.invalid.clone(),
584            not_yet_valid: self.not_yet_valid.clone(),
585        }
586    }
587}
588
589impl<T> fmt::Debug for MinedBlockOutcome<T> {
590    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
591        f.debug_struct("MinedBlockOutcome")
592            .field("block_number", &self.block_number)
593            .field("included", &self.included.len())
594            .field("invalid", &self.invalid.len())
595            .field("not_yet_valid", &self.not_yet_valid.len())
596            .finish()
597    }
598}
599
600/// Container type for a mined transaction
601#[derive(Clone, Debug)]
602pub struct MinedTransaction<N: Network> {
603    pub info: TransactionInfo,
604    pub receipt: N::ReceiptEnvelope,
605    pub block_hash: B256,
606    pub block_number: u64,
607}
608
609impl<N: Network> MinedTransaction<N> {
610    /// Returns the traces of the transaction for `trace_transaction`
611    pub fn parity_traces(&self) -> Vec<LocalizedTransactionTrace> {
612        ParityTraceBuilder::new(
613            self.info.traces.clone(),
614            None,
615            TracingInspectorConfig::default_parity(),
616        )
617        .into_localized_transaction_traces(RethTransactionInfo {
618            hash: Some(self.info.transaction_hash),
619            index: Some(self.info.transaction_index),
620            block_hash: Some(self.block_hash),
621            block_number: Some(self.block_number),
622            base_fee: None,
623            block_timestamp: None,
624        })
625    }
626
627    pub fn ots_internal_operations(&self) -> Vec<InternalOperation> {
628        self.info
629            .traces
630            .iter()
631            .filter_map(|node| {
632                let r#type = match node.trace.kind {
633                    _ if node.is_selfdestruct() => OperationType::OpSelfDestruct,
634                    CallKind::Call if !node.trace.value.is_zero() => OperationType::OpTransfer,
635                    CallKind::Create => OperationType::OpCreate,
636                    CallKind::Create2 => OperationType::OpCreate2,
637                    _ => return None,
638                };
639                let (from, to, value) = if node.is_selfdestruct() {
640                    (
641                        node.trace.address,
642                        node.trace.selfdestruct_refund_target.unwrap_or_default(),
643                        node.trace.selfdestruct_transferred_value.unwrap_or_default(),
644                    )
645                } else {
646                    (node.trace.caller, node.trace.address, node.trace.value)
647                };
648                Some(InternalOperation { r#type, from, to, value })
649            })
650            .collect()
651    }
652}
653
654/// Intermediary Anvil representation of a receipt
655#[derive(Clone, Debug)]
656pub struct MinedTransactionReceipt<N: Network> {
657    /// The actual json rpc receipt object
658    pub inner: N::ReceiptResponse,
659    /// Output data for the transaction
660    pub out: Option<Bytes>,
661}
662
663#[cfg(test)]
664mod tests {
665    use super::*;
666    use crate::eth::backend::{db::Db, mem::in_memory_db::StateRootDb};
667    use alloy_primitives::{Address, hex};
668    use alloy_rlp::Decodable;
669    use revm::{database::DatabaseRef, state::AccountInfo};
670    use tempo_primitives::TempoHeader;
671
672    #[test]
673    fn test_interval_update() {
674        let mut storage = InMemoryBlockStates::default();
675        storage.update_interval_mine_block_time(Duration::from_secs(1));
676        assert_eq!(storage.in_memory_limit, DEFAULT_HISTORY_LIMIT * 3);
677    }
678
679    #[test]
680    fn test_init_state_limits() {
681        let mut storage = InMemoryBlockStates::default();
682        assert_eq!(storage.in_memory_limit, DEFAULT_HISTORY_LIMIT);
683        assert_eq!(storage.min_in_memory_limit, MIN_HISTORY_LIMIT);
684        assert_eq!(storage.max_on_disk_limit, MAX_ON_DISK_HISTORY_LIMIT);
685
686        storage = storage.memory_only();
687        assert!(storage.is_memory_only());
688
689        storage = InMemoryBlockStates::new(1, 0);
690        assert!(storage.is_memory_only());
691        assert_eq!(storage.in_memory_limit, 1);
692        assert_eq!(storage.min_in_memory_limit, 1);
693        assert_eq!(storage.max_on_disk_limit, 0);
694
695        storage = InMemoryBlockStates::new(1, 2);
696        assert!(!storage.is_memory_only());
697        assert_eq!(storage.in_memory_limit, 1);
698        assert_eq!(storage.min_in_memory_limit, 1);
699        assert_eq!(storage.max_on_disk_limit, 2);
700
701        storage = InMemoryBlockStates::new(0, 0);
702        assert!(storage.is_memory_only());
703        assert_eq!(storage.in_memory_limit, 1);
704        assert_eq!(storage.min_in_memory_limit, 1);
705        assert_eq!(storage.max_on_disk_limit, 0);
706
707        storage.set_cache_limit(0);
708        assert_eq!(storage.in_memory_limit, 1);
709        assert_eq!(storage.min_in_memory_limit, 1);
710    }
711
712    #[tokio::test(flavor = "multi_thread")]
713    async fn can_read_write_cached_state() {
714        let mut storage = InMemoryBlockStates::new(1, MAX_ON_DISK_HISTORY_LIMIT);
715        let one = B256::from(U256::from(1));
716        let two = B256::from(U256::from(2));
717
718        let mut state = MemDb::default();
719        let addr = Address::random();
720        let info = AccountInfo::from_balance(U256::from(1337));
721        state.insert_account(addr, info);
722        storage.insert(one, StateDb::new(state));
723        storage.insert(two, StateDb::new(MemDb::default()));
724
725        // wait for files to be flushed
726        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
727
728        assert_eq!(storage.on_disk_states.len(), 1);
729        assert!(storage.on_disk_states.contains_key(&one));
730
731        let loaded = storage.get_on_disk_state(&one).unwrap();
732
733        let acc = loaded.basic_ref(addr).unwrap().unwrap();
734        assert_eq!(acc.balance, U256::from(1337u64));
735    }
736
737    #[test]
738    fn persistent_states_do_not_use_disk_cache() {
739        let mut storage = InMemoryBlockStates::new(1, MAX_ON_DISK_HISTORY_LIMIT);
740        let one = B256::from(U256::from(1));
741        let two = B256::from(U256::from(2));
742        let address = Address::random();
743        let mut db = StateRootDb::default();
744
745        db.insert_account(address, AccountInfo::from_balance(U256::from(1)));
746        storage.insert(one, db.current_state());
747        db.set_balance(address, U256::from(2)).unwrap();
748        storage.insert(two, db.current_state());
749
750        assert!(storage.disk_cache.temp_dir.is_none());
751        assert!(storage.on_disk_states.get(&one).unwrap().is_persistent());
752        assert_eq!(
753            storage.get_on_disk_state(&one).unwrap().basic_ref(address).unwrap().unwrap().balance,
754            U256::from(1)
755        );
756        storage.remove_block_states(&[one]);
757        assert!(storage.disk_cache.temp_dir.is_none());
758    }
759
760    #[tokio::test(flavor = "multi_thread")]
761    async fn can_decrease_state_cache_size() {
762        let limit = 15;
763        let mut storage = InMemoryBlockStates::new(limit, MAX_ON_DISK_HISTORY_LIMIT);
764
765        let num_states = 30;
766        for idx in 0..num_states {
767            let mut state = MemDb::default();
768            let hash = B256::from(U256::from(idx));
769            let addr = Address::from_word(hash);
770            let balance = (idx * 2) as u64;
771            let info = AccountInfo::from_balance(U256::from(balance));
772            state.insert_account(addr, info);
773            storage.insert(hash, StateDb::new(state));
774        }
775
776        // wait for files to be flushed
777        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
778
779        let on_disk_states_len = num_states - storage.min_in_memory_limit;
780
781        assert_eq!(storage.on_disk_states.len(), on_disk_states_len);
782        assert_eq!(storage.present.len(), storage.min_in_memory_limit);
783
784        for idx in 0..num_states {
785            let hash = B256::from(U256::from(idx));
786            let addr = Address::from_word(hash);
787
788            let loaded = if idx < on_disk_states_len {
789                storage.get_on_disk_state(&hash).unwrap()
790            } else {
791                storage.get_state(&hash).unwrap()
792            };
793
794            let acc = loaded.basic_ref(addr).unwrap().unwrap();
795            let balance = (idx * 2) as u64;
796            assert_eq!(acc.balance, U256::from(balance));
797        }
798    }
799
800    #[test]
801    fn test_remove_block_states_on_rollback() {
802        let mut storage = InMemoryBlockStates::new(10, MAX_ON_DISK_HISTORY_LIMIT);
803
804        // Insert 5 states
805        let hashes: Vec<B256> = (0..5)
806            .map(|i| {
807                let hash = B256::from(U256::from(i));
808                let mut state = MemDb::default();
809                let addr = Address::from_word(hash);
810                state.insert_account(addr, AccountInfo::from_balance(U256::from(i * 100)));
811                storage.insert(hash, StateDb::new(state));
812                hash
813            })
814            .collect();
815
816        assert_eq!(storage.present.len(), 5);
817
818        // Simulate rollback: remove the last 3 blocks
819        let removed_hashes = &hashes[2..];
820        storage.remove_block_states(removed_hashes);
821
822        // Only the first 2 states should remain
823        assert_eq!(storage.present.len(), 2);
824        assert!(storage.get_state(&hashes[0]).is_some());
825        assert!(storage.get_state(&hashes[1]).is_some());
826        for h in removed_hashes {
827            assert!(storage.get_state(h).is_none());
828            assert!(!storage.present.contains(h));
829        }
830    }
831
832    #[tokio::test(flavor = "multi_thread")]
833    async fn test_remove_block_states_cleans_disk_cache() {
834        // Use limit=1 to force states to disk
835        let mut storage = InMemoryBlockStates::new(1, MAX_ON_DISK_HISTORY_LIMIT);
836
837        let hash_a = B256::from(U256::from(1));
838        let hash_b = B256::from(U256::from(2));
839
840        storage.insert(hash_a, StateDb::new(MemDb::default()));
841        storage.insert(hash_b, StateDb::new(MemDb::default()));
842
843        // Wait for disk flush
844        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
845
846        assert!(storage.on_disk_states.contains_key(&hash_a));
847
848        // Remove hash_a (on disk)
849        storage.remove_block_states(&[hash_a]);
850
851        assert!(!storage.on_disk_states.contains_key(&hash_a));
852        assert!(!storage.oldest_on_disk.contains(&hash_a));
853        assert!(storage.get_on_disk_state(&hash_a).is_none());
854    }
855
856    // verifies that blocks and transactions in BlockchainStorage remain the same when dumped and
857    // reloaded
858    #[test]
859    fn test_storage_dump_reload_cycle() {
860        let mut dump_storage = BlockchainStorage::<FoundryNetwork>::empty();
861
862        let header = Header { gas_limit: 123456, ..Default::default() };
863        let bytes_first = &mut &hex::decode("f86b02843b9aca00830186a094d3e8763675e4c425df46cc3b5c0f6cbdac39604687038d7ea4c68000802ba00eb96ca19e8a77102767a41fc85a36afd5c61ccb09911cec5d3e86e193d9c5aea03a456401896b1b6055311536bf00a718568c744d8c1f9df59879e8350220ca18").unwrap()[..];
864        let tx: MaybeImpersonatedTransaction<FoundryTxEnvelope> =
865            FoundryTxEnvelope::decode(&mut &bytes_first[..]).unwrap().into();
866        let block = create_block(header.clone().into(), vec![tx.clone()]);
867        let block_hash = block.header.hash_slow();
868        dump_storage.blocks.insert(block_hash, block);
869
870        let serialized_blocks = dump_storage.serialized_blocks();
871        let serialized_transactions = dump_storage.serialized_transactions();
872
873        let mut load_storage = BlockchainStorage::<FoundryNetwork>::empty();
874
875        load_storage.load_blocks(serialized_blocks);
876        load_storage.load_transactions(serialized_transactions);
877
878        let loaded_block = load_storage.blocks.get(&block_hash).unwrap();
879        assert_eq!(loaded_block.header.gas_limit(), header.gas_limit());
880        let loaded_tx = loaded_block.body.transactions.first().unwrap();
881        assert_eq!(loaded_tx, &tx);
882    }
883
884    #[test]
885    fn test_tempo_storage_dump_reload_cycle() {
886        let mut dump_storage = BlockchainStorage::<FoundryNetwork>::empty();
887        let header = TempoHeader {
888            general_gas_limit: 30_000_000,
889            shared_gas_limit: 1_000_000,
890            timestamp_millis_part: 123,
891            inner: Header { number: 7, gas_limit: 30_000_000, timestamp: 42, ..Default::default() },
892            consensus_context: None,
893        };
894        let block = create_block(
895            header.into(),
896            Vec::<MaybeImpersonatedTransaction<FoundryTxEnvelope>>::new(),
897        );
898        let expected_header = block.header.clone();
899        let block_hash = block.header.hash_slow();
900        dump_storage.blocks.insert(block_hash, block);
901
902        let serialized = serde_json::to_string(&dump_storage.serialized_blocks()).unwrap();
903        let blocks: Vec<SerializableBlock> = serde_json::from_str(&serialized).unwrap();
904        let mut load_storage = BlockchainStorage::<FoundryNetwork>::empty();
905        load_storage.load_blocks(blocks);
906
907        let loaded_block = load_storage.blocks.get(&block_hash).unwrap();
908        assert_eq!(loaded_block.header, expected_header);
909        assert_eq!(loaded_block.header.as_tempo().unwrap().shared_gas_limit, 1_000_000);
910        assert_eq!(load_storage.hashes.get(&7), Some(&block_hash));
911    }
912
913    // Regression test for https://github.com/foundry-rs/foundry/issues/12645:
914    // when a non-zero genesis number is configured (e.g. `--block-number 73 --load-state ...`),
915    // `load_blocks` must set `genesis_hash` to the loaded block matching `genesis_number`,
916    // not the hardcoded block 0.
917    #[test]
918    fn test_load_blocks_sets_genesis_hash_with_non_zero_genesis_number() {
919        const GENESIS_NUMBER: u64 = 73;
920
921        // Build a serialized block at the configured genesis number.
922        let header = Header { number: GENESIS_NUMBER, gas_limit: 123456, ..Default::default() };
923        let block = create_block(
924            header.into(),
925            Vec::<MaybeImpersonatedTransaction<FoundryTxEnvelope>>::new(),
926        );
927        let block_hash = block.header.hash_slow();
928        let serialized_blocks: Vec<SerializableBlock> = vec![block.into()];
929
930        // Simulate a fresh storage started with `--block-number 73`: the dummy block created by
931        // `new()` is hash X, and `genesis_number` is 73. Loading a state snapshot whose genesis
932        // is also 73 must rewrite `genesis_hash` to the loaded block's hash.
933        let mut load_storage = BlockchainStorage::<FoundryNetwork>::empty();
934        load_storage.genesis_number = GENESIS_NUMBER;
935        let dummy_genesis_hash = B256::repeat_byte(0xab);
936        load_storage.genesis_hash = dummy_genesis_hash;
937
938        load_storage.load_blocks(serialized_blocks);
939
940        assert_eq!(load_storage.genesis_hash, block_hash);
941        assert_ne!(load_storage.genesis_hash, dummy_genesis_hash);
942
943        // Sanity check: with the old hardcoded `block_number == 0` logic, `genesis_hash` would
944        // never be updated when no block 0 is present, so the dummy hash would leak through.
945        let mut sanity_storage = BlockchainStorage::<FoundryNetwork>::empty();
946        sanity_storage.genesis_number = 0;
947        sanity_storage.genesis_hash = dummy_genesis_hash;
948
949        let header_only_73 =
950            Header { number: GENESIS_NUMBER, gas_limit: 123456, ..Default::default() };
951        let block_73 = create_block(
952            header_only_73.into(),
953            Vec::<MaybeImpersonatedTransaction<FoundryTxEnvelope>>::new(),
954        );
955        sanity_storage.load_blocks(vec![block_73.into()]);
956        assert_eq!(sanity_storage.genesis_hash, dummy_genesis_hash);
957    }
958}