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    /// Adds a block to storage and returns its hash.
448    pub fn insert_block(&mut self, block: Block) -> B256 {
449        let block_hash = block.header.hash_slow();
450        let block_number = block.header.number();
451        self.blocks.insert(block_hash, block);
452        self.hashes.insert(block_number, block_hash);
453
454        // Update genesis_hash if we are loading the genesis block, so that
455        // Finalized/Safe/Earliest block tag lookups return the correct hash. The genesis
456        // number can be non-zero when configured via `--block-number`.
457        // See: https://github.com/foundry-rs/foundry/issues/12645
458        if block_number == self.genesis_number {
459            self.genesis_hash = block_hash;
460        }
461
462        block_hash
463    }
464
465    /// Deserialize and add all blocks data to the backend storage
466    pub fn load_blocks(&mut self, serializable_blocks: Vec<SerializableBlock>) {
467        for serializable_block in serializable_blocks {
468            let block: Block = serializable_block.into();
469            self.insert_block(block);
470        }
471    }
472
473    /// Returns the hash for [BlockNumberOrTag]
474    pub fn hash(&self, number: BlockNumberOrTag, slots_in_an_epoch: u64) -> Option<B256> {
475        match number {
476            BlockNumberOrTag::Latest => Some(self.best_hash),
477            BlockNumberOrTag::Earliest => Some(self.genesis_hash),
478            BlockNumberOrTag::Pending => None,
479            BlockNumberOrTag::Number(num) => self.hashes.get(&num).copied(),
480            BlockNumberOrTag::Safe => {
481                if self.best_number > slots_in_an_epoch {
482                    self.hashes.get(&(self.best_number - slots_in_an_epoch)).copied()
483                } else {
484                    Some(self.genesis_hash)
485                }
486            }
487            BlockNumberOrTag::Finalized => {
488                if self.best_number > slots_in_an_epoch * 2 {
489                    self.hashes.get(&(self.best_number - slots_in_an_epoch * 2)).copied()
490                } else {
491                    Some(self.genesis_hash)
492                }
493            }
494        }
495    }
496}
497
498impl<N: Network<ReceiptEnvelope = FoundryReceiptEnvelope>> BlockchainStorage<N> {
499    pub fn serialized_transactions(&self) -> Vec<SerializableTransaction> {
500        self.transactions.values().map(|tx: &MinedTransaction<N>| tx.clone().into()).collect()
501    }
502
503    /// Deserialize and add all transactions data to the backend storage
504    pub fn load_transactions(&mut self, serializable_transactions: Vec<SerializableTransaction>) {
505        for serializable_transaction in serializable_transactions {
506            let transaction: MinedTransaction<N> = serializable_transaction.into();
507            self.transactions.insert(transaction.info.transaction_hash, transaction);
508        }
509    }
510}
511
512/// A simple in-memory blockchain
513#[derive(Clone, Debug)]
514pub struct Blockchain<N: Network> {
515    /// underlying storage that supports concurrent reads
516    pub storage: Arc<RwLock<BlockchainStorage<N>>>,
517}
518
519impl<N: Network> Blockchain<N> {
520    /// Creates a new storage with a genesis block
521    pub fn new(
522        evm_env: &EvmEnv,
523        base_fee: Option<u64>,
524        timestamp: u64,
525        genesis_number: u64,
526        is_tempo: bool,
527    ) -> Self {
528        Self {
529            storage: Arc::new(RwLock::new(BlockchainStorage::new(
530                evm_env,
531                base_fee,
532                timestamp,
533                genesis_number,
534                is_tempo,
535            ))),
536        }
537    }
538
539    pub fn forked(block_number: u64, block_hash: B256, total_difficulty: U256) -> Self {
540        Self {
541            storage: Arc::new(RwLock::new(BlockchainStorage::forked(
542                block_number,
543                block_hash,
544                total_difficulty,
545            ))),
546        }
547    }
548
549    /// returns the header hash of given block
550    pub fn hash(&self, id: BlockId, slots_in_an_epoch: u64) -> Option<B256> {
551        match id {
552            BlockId::Hash(h) => Some(h.block_hash),
553            BlockId::Number(num) => self.storage.read().hash(num, slots_in_an_epoch),
554        }
555    }
556
557    pub fn get_block_by_hash(&self, hash: &B256) -> Option<Block> {
558        self.storage.read().blocks.get(hash).cloned()
559    }
560
561    pub fn get_transaction_by_hash(&self, hash: &B256) -> Option<MinedTransaction<N>> {
562        self.storage.read().transactions.get(hash).cloned()
563    }
564
565    /// Returns the total number of blocks
566    pub fn blocks_count(&self) -> usize {
567        self.storage.read().blocks.len()
568    }
569}
570
571/// Represents the outcome of mining a new block
572pub struct MinedBlockOutcome<T> {
573    /// The block that was mined
574    pub block_number: u64,
575    /// All transactions included in the block
576    pub included: Vec<Arc<PoolTransaction<T>>>,
577    /// All transactions that were attempted to be included but were invalid at the time of
578    /// execution
579    pub invalid: Vec<Arc<PoolTransaction<T>>>,
580    /// Transactions skipped because they're not yet valid (e.g., valid_after in the future).
581    /// These remain in the pool and should be retried later.
582    pub not_yet_valid: Vec<Arc<PoolTransaction<T>>>,
583}
584
585impl<T> Clone for MinedBlockOutcome<T> {
586    fn clone(&self) -> Self {
587        Self {
588            block_number: self.block_number,
589            included: self.included.clone(),
590            invalid: self.invalid.clone(),
591            not_yet_valid: self.not_yet_valid.clone(),
592        }
593    }
594}
595
596impl<T> fmt::Debug for MinedBlockOutcome<T> {
597    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
598        f.debug_struct("MinedBlockOutcome")
599            .field("block_number", &self.block_number)
600            .field("included", &self.included.len())
601            .field("invalid", &self.invalid.len())
602            .field("not_yet_valid", &self.not_yet_valid.len())
603            .finish()
604    }
605}
606
607/// Container type for a mined transaction
608#[derive(Clone, Debug)]
609pub struct MinedTransaction<N: Network> {
610    pub info: TransactionInfo,
611    pub receipt: N::ReceiptEnvelope,
612    pub block_hash: B256,
613    pub block_number: u64,
614}
615
616impl<N: Network> MinedTransaction<N> {
617    /// Returns the traces of the transaction for `trace_transaction`
618    pub fn parity_traces(&self) -> Vec<LocalizedTransactionTrace> {
619        ParityTraceBuilder::new(
620            self.info.traces.clone(),
621            None,
622            TracingInspectorConfig::default_parity(),
623        )
624        .into_localized_transaction_traces(RethTransactionInfo {
625            hash: Some(self.info.transaction_hash),
626            index: Some(self.info.transaction_index),
627            block_hash: Some(self.block_hash),
628            block_number: Some(self.block_number),
629            base_fee: None,
630            block_timestamp: None,
631        })
632    }
633
634    pub fn ots_internal_operations(&self) -> Vec<InternalOperation> {
635        self.info
636            .traces
637            .iter()
638            .filter_map(|node| {
639                let r#type = match node.trace.kind {
640                    _ if node.is_selfdestruct() => OperationType::OpSelfDestruct,
641                    CallKind::Call if !node.trace.value.is_zero() => OperationType::OpTransfer,
642                    CallKind::Create => OperationType::OpCreate,
643                    CallKind::Create2 => OperationType::OpCreate2,
644                    _ => return None,
645                };
646                let (from, to, value) = if node.is_selfdestruct() {
647                    (
648                        node.trace.address,
649                        node.trace.selfdestruct_refund_target.unwrap_or_default(),
650                        node.trace.selfdestruct_transferred_value.unwrap_or_default(),
651                    )
652                } else {
653                    (node.trace.caller, node.trace.address, node.trace.value)
654                };
655                Some(InternalOperation { r#type, from, to, value })
656            })
657            .collect()
658    }
659}
660
661/// Intermediary Anvil representation of a receipt
662#[derive(Clone, Debug)]
663pub struct MinedTransactionReceipt<N: Network> {
664    /// The actual json rpc receipt object
665    pub inner: N::ReceiptResponse,
666    /// Output data for the transaction
667    pub out: Option<Bytes>,
668}
669
670#[cfg(test)]
671mod tests {
672    use super::*;
673    use crate::eth::backend::{db::Db, mem::in_memory_db::StateRootDb};
674    use alloy_primitives::{Address, hex};
675    use alloy_rlp::Decodable;
676    use revm::{database::DatabaseRef, state::AccountInfo};
677    use tempo_primitives::TempoHeader;
678
679    #[test]
680    fn test_interval_update() {
681        let mut storage = InMemoryBlockStates::default();
682        storage.update_interval_mine_block_time(Duration::from_secs(1));
683        assert_eq!(storage.in_memory_limit, DEFAULT_HISTORY_LIMIT * 3);
684    }
685
686    #[test]
687    fn test_init_state_limits() {
688        let mut storage = InMemoryBlockStates::default();
689        assert_eq!(storage.in_memory_limit, DEFAULT_HISTORY_LIMIT);
690        assert_eq!(storage.min_in_memory_limit, MIN_HISTORY_LIMIT);
691        assert_eq!(storage.max_on_disk_limit, MAX_ON_DISK_HISTORY_LIMIT);
692
693        storage = storage.memory_only();
694        assert!(storage.is_memory_only());
695
696        storage = InMemoryBlockStates::new(1, 0);
697        assert!(storage.is_memory_only());
698        assert_eq!(storage.in_memory_limit, 1);
699        assert_eq!(storage.min_in_memory_limit, 1);
700        assert_eq!(storage.max_on_disk_limit, 0);
701
702        storage = InMemoryBlockStates::new(1, 2);
703        assert!(!storage.is_memory_only());
704        assert_eq!(storage.in_memory_limit, 1);
705        assert_eq!(storage.min_in_memory_limit, 1);
706        assert_eq!(storage.max_on_disk_limit, 2);
707
708        storage = InMemoryBlockStates::new(0, 0);
709        assert!(storage.is_memory_only());
710        assert_eq!(storage.in_memory_limit, 1);
711        assert_eq!(storage.min_in_memory_limit, 1);
712        assert_eq!(storage.max_on_disk_limit, 0);
713
714        storage.set_cache_limit(0);
715        assert_eq!(storage.in_memory_limit, 1);
716        assert_eq!(storage.min_in_memory_limit, 1);
717    }
718
719    #[tokio::test(flavor = "multi_thread")]
720    async fn can_read_write_cached_state() {
721        let mut storage = InMemoryBlockStates::new(1, MAX_ON_DISK_HISTORY_LIMIT);
722        let one = B256::from(U256::from(1));
723        let two = B256::from(U256::from(2));
724
725        let mut state = MemDb::default();
726        let addr = Address::random();
727        let info = AccountInfo::from_balance(U256::from(1337));
728        state.insert_account(addr, info);
729        storage.insert(one, StateDb::new(state));
730        storage.insert(two, StateDb::new(MemDb::default()));
731
732        // wait for files to be flushed
733        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
734
735        assert_eq!(storage.on_disk_states.len(), 1);
736        assert!(storage.on_disk_states.contains_key(&one));
737
738        let loaded = storage.get_on_disk_state(&one).unwrap();
739
740        let acc = loaded.basic_ref(addr).unwrap().unwrap();
741        assert_eq!(acc.balance, U256::from(1337u64));
742    }
743
744    #[test]
745    fn persistent_states_do_not_use_disk_cache() {
746        let mut storage = InMemoryBlockStates::new(1, MAX_ON_DISK_HISTORY_LIMIT);
747        let one = B256::from(U256::from(1));
748        let two = B256::from(U256::from(2));
749        let address = Address::random();
750        let mut db = StateRootDb::default();
751
752        db.insert_account(address, AccountInfo::from_balance(U256::from(1)));
753        storage.insert(one, db.current_state());
754        db.set_balance(address, U256::from(2)).unwrap();
755        storage.insert(two, db.current_state());
756
757        assert!(storage.disk_cache.temp_dir.is_none());
758        assert!(storage.on_disk_states.get(&one).unwrap().is_persistent());
759        assert_eq!(
760            storage.get_on_disk_state(&one).unwrap().basic_ref(address).unwrap().unwrap().balance,
761            U256::from(1)
762        );
763        storage.remove_block_states(&[one]);
764        assert!(storage.disk_cache.temp_dir.is_none());
765    }
766
767    #[tokio::test(flavor = "multi_thread")]
768    async fn can_decrease_state_cache_size() {
769        let limit = 15;
770        let mut storage = InMemoryBlockStates::new(limit, MAX_ON_DISK_HISTORY_LIMIT);
771
772        let num_states = 30;
773        for idx in 0..num_states {
774            let mut state = MemDb::default();
775            let hash = B256::from(U256::from(idx));
776            let addr = Address::from_word(hash);
777            let balance = (idx * 2) as u64;
778            let info = AccountInfo::from_balance(U256::from(balance));
779            state.insert_account(addr, info);
780            storage.insert(hash, StateDb::new(state));
781        }
782
783        // wait for files to be flushed
784        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
785
786        let on_disk_states_len = num_states - storage.min_in_memory_limit;
787
788        assert_eq!(storage.on_disk_states.len(), on_disk_states_len);
789        assert_eq!(storage.present.len(), storage.min_in_memory_limit);
790
791        for idx in 0..num_states {
792            let hash = B256::from(U256::from(idx));
793            let addr = Address::from_word(hash);
794
795            let loaded = if idx < on_disk_states_len {
796                storage.get_on_disk_state(&hash).unwrap()
797            } else {
798                storage.get_state(&hash).unwrap()
799            };
800
801            let acc = loaded.basic_ref(addr).unwrap().unwrap();
802            let balance = (idx * 2) as u64;
803            assert_eq!(acc.balance, U256::from(balance));
804        }
805    }
806
807    #[test]
808    fn test_remove_block_states_on_rollback() {
809        let mut storage = InMemoryBlockStates::new(10, MAX_ON_DISK_HISTORY_LIMIT);
810
811        // Insert 5 states
812        let hashes: Vec<B256> = (0..5)
813            .map(|i| {
814                let hash = B256::from(U256::from(i));
815                let mut state = MemDb::default();
816                let addr = Address::from_word(hash);
817                state.insert_account(addr, AccountInfo::from_balance(U256::from(i * 100)));
818                storage.insert(hash, StateDb::new(state));
819                hash
820            })
821            .collect();
822
823        assert_eq!(storage.present.len(), 5);
824
825        // Simulate rollback: remove the last 3 blocks
826        let removed_hashes = &hashes[2..];
827        storage.remove_block_states(removed_hashes);
828
829        // Only the first 2 states should remain
830        assert_eq!(storage.present.len(), 2);
831        assert!(storage.get_state(&hashes[0]).is_some());
832        assert!(storage.get_state(&hashes[1]).is_some());
833        for h in removed_hashes {
834            assert!(storage.get_state(h).is_none());
835            assert!(!storage.present.contains(h));
836        }
837    }
838
839    #[tokio::test(flavor = "multi_thread")]
840    async fn test_remove_block_states_cleans_disk_cache() {
841        // Use limit=1 to force states to disk
842        let mut storage = InMemoryBlockStates::new(1, MAX_ON_DISK_HISTORY_LIMIT);
843
844        let hash_a = B256::from(U256::from(1));
845        let hash_b = B256::from(U256::from(2));
846
847        storage.insert(hash_a, StateDb::new(MemDb::default()));
848        storage.insert(hash_b, StateDb::new(MemDb::default()));
849
850        // Wait for disk flush
851        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
852
853        assert!(storage.on_disk_states.contains_key(&hash_a));
854
855        // Remove hash_a (on disk)
856        storage.remove_block_states(&[hash_a]);
857
858        assert!(!storage.on_disk_states.contains_key(&hash_a));
859        assert!(!storage.oldest_on_disk.contains(&hash_a));
860        assert!(storage.get_on_disk_state(&hash_a).is_none());
861    }
862
863    // verifies that blocks and transactions in BlockchainStorage remain the same when dumped and
864    // reloaded
865    #[test]
866    fn test_storage_dump_reload_cycle() {
867        let mut dump_storage = BlockchainStorage::<FoundryNetwork>::empty();
868
869        let header = Header { gas_limit: 123456, ..Default::default() };
870        let bytes_first = &mut &hex::decode("f86b02843b9aca00830186a094d3e8763675e4c425df46cc3b5c0f6cbdac39604687038d7ea4c68000802ba00eb96ca19e8a77102767a41fc85a36afd5c61ccb09911cec5d3e86e193d9c5aea03a456401896b1b6055311536bf00a718568c744d8c1f9df59879e8350220ca18").unwrap()[..];
871        let tx: MaybeImpersonatedTransaction<FoundryTxEnvelope> =
872            FoundryTxEnvelope::decode(&mut &bytes_first[..]).unwrap().into();
873        let block = create_block(header.clone().into(), vec![tx.clone()]);
874        let block_hash = block.header.hash_slow();
875        dump_storage.blocks.insert(block_hash, block);
876
877        let serialized_blocks = dump_storage.serialized_blocks();
878        let serialized_transactions = dump_storage.serialized_transactions();
879
880        let mut load_storage = BlockchainStorage::<FoundryNetwork>::empty();
881
882        load_storage.load_blocks(serialized_blocks);
883        load_storage.load_transactions(serialized_transactions);
884
885        let loaded_block = load_storage.blocks.get(&block_hash).unwrap();
886        assert_eq!(loaded_block.header.gas_limit(), header.gas_limit());
887        let loaded_tx = loaded_block.body.transactions.first().unwrap();
888        assert_eq!(loaded_tx, &tx);
889    }
890
891    #[test]
892    fn test_tempo_storage_dump_reload_cycle() {
893        let mut dump_storage = BlockchainStorage::<FoundryNetwork>::empty();
894        let header = TempoHeader {
895            general_gas_limit: 30_000_000,
896            shared_gas_limit: 1_000_000,
897            timestamp_millis_part: 123,
898            inner: Header { number: 7, gas_limit: 30_000_000, timestamp: 42, ..Default::default() },
899            consensus_context: None,
900        };
901        let block = create_block(
902            header.into(),
903            Vec::<MaybeImpersonatedTransaction<FoundryTxEnvelope>>::new(),
904        );
905        let expected_header = block.header.clone();
906        let block_hash = block.header.hash_slow();
907        dump_storage.blocks.insert(block_hash, block);
908
909        let serialized = serde_json::to_string(&dump_storage.serialized_blocks()).unwrap();
910        let blocks: Vec<SerializableBlock> = serde_json::from_str(&serialized).unwrap();
911        let mut load_storage = BlockchainStorage::<FoundryNetwork>::empty();
912        load_storage.load_blocks(blocks);
913
914        let loaded_block = load_storage.blocks.get(&block_hash).unwrap();
915        assert_eq!(loaded_block.header, expected_header);
916        assert_eq!(loaded_block.header.as_tempo().unwrap().shared_gas_limit, 1_000_000);
917        assert_eq!(load_storage.hashes.get(&7), Some(&block_hash));
918    }
919
920    // Regression test for https://github.com/foundry-rs/foundry/issues/12645:
921    // when a non-zero genesis number is configured (e.g. `--block-number 73 --load-state ...`),
922    // `load_blocks` must set `genesis_hash` to the loaded block matching `genesis_number`,
923    // not the hardcoded block 0.
924    #[test]
925    fn test_load_blocks_sets_genesis_hash_with_non_zero_genesis_number() {
926        const GENESIS_NUMBER: u64 = 73;
927
928        // Build a serialized block at the configured genesis number.
929        let header = Header { number: GENESIS_NUMBER, gas_limit: 123456, ..Default::default() };
930        let block = create_block(
931            header.into(),
932            Vec::<MaybeImpersonatedTransaction<FoundryTxEnvelope>>::new(),
933        );
934        let block_hash = block.header.hash_slow();
935        let serialized_blocks: Vec<SerializableBlock> = vec![block.into()];
936
937        // Simulate a fresh storage started with `--block-number 73`: the dummy block created by
938        // `new()` is hash X, and `genesis_number` is 73. Loading a state snapshot whose genesis
939        // is also 73 must rewrite `genesis_hash` to the loaded block's hash.
940        let mut load_storage = BlockchainStorage::<FoundryNetwork>::empty();
941        load_storage.genesis_number = GENESIS_NUMBER;
942        let dummy_genesis_hash = B256::repeat_byte(0xab);
943        load_storage.genesis_hash = dummy_genesis_hash;
944
945        load_storage.load_blocks(serialized_blocks);
946
947        assert_eq!(load_storage.genesis_hash, block_hash);
948        assert_ne!(load_storage.genesis_hash, dummy_genesis_hash);
949
950        // Sanity check: with the old hardcoded `block_number == 0` logic, `genesis_hash` would
951        // never be updated when no block 0 is present, so the dummy hash would leak through.
952        let mut sanity_storage = BlockchainStorage::<FoundryNetwork>::empty();
953        sanity_storage.genesis_number = 0;
954        sanity_storage.genesis_hash = dummy_genesis_hash;
955
956        let header_only_73 =
957            Header { number: GENESIS_NUMBER, gas_limit: 123456, ..Default::default() };
958        let block_73 = create_block(
959            header_only_73.into(),
960            Vec::<MaybeImpersonatedTransaction<FoundryTxEnvelope>>::new(),
961        );
962        sanity_storage.load_blocks(vec![block_73.into()]);
963        assert_eq!(sanity_storage.genesis_hash, dummy_genesis_hash);
964    }
965}