Skip to main content

anvil/eth/backend/
db.rs

1//! Helper types for working with [revm](foundry_evm::revm)
2
3use std::{
4    collections::BTreeMap,
5    fmt::{self, Debug},
6    path::Path,
7};
8
9use alloy_consensus::{BlockBody, Header};
10use alloy_eips::eip4895::Withdrawals;
11use alloy_primitives::{
12    Address, B256, Bytes, U256, keccak256,
13    map::{AddressMap, HashMap},
14};
15use alloy_rpc_types::BlockId;
16use anvil_core::eth::{
17    block::Block,
18    transaction::{MaybeImpersonatedTransaction, TransactionInfo},
19};
20use foundry_common::errors::FsPathError;
21use foundry_evm::backend::{
22    BlockchainDb, DatabaseError, DatabaseResult, MemDb, RevertStateSnapshotAction, StateSnapshot,
23};
24use foundry_primitives::{FoundryReceiptEnvelope, FoundryTxEnvelope};
25use revm::{
26    Database, DatabaseCommit,
27    bytecode::Bytecode,
28    context::BlockEnv,
29    context_interface::block::BlobExcessGasAndPrice,
30    database::{CacheDB, DatabaseRef, DbAccount},
31    primitives::{KECCAK_EMPTY, eip4844::BLOB_BASE_FEE_UPDATE_FRACTION_PRAGUE},
32    state::AccountInfo,
33};
34use serde::{
35    Deserialize, Deserializer, Serialize,
36    de::{Error as DeError, MapAccess, Visitor},
37};
38use serde_json::Value;
39
40use crate::mem::storage::MinedTransaction;
41
42/// Helper trait get access to the full state data of the database
43pub trait MaybeFullDatabase: DatabaseRef<Error = DatabaseError> + Debug {
44    fn maybe_as_full_db(&self) -> Option<&AddressMap<DbAccount>> {
45        None
46    }
47
48    /// Clear the state and move it into a new `StateSnapshot`.
49    fn clear_into_state_snapshot(&mut self) -> StateSnapshot;
50
51    /// Read the state snapshot.
52    ///
53    /// This clones all the states and returns a new `StateSnapshot`.
54    fn read_as_state_snapshot(&self) -> StateSnapshot;
55
56    /// Clears the entire database
57    fn clear(&mut self);
58
59    /// Reverses `clear_into_snapshot` by initializing the db's state with the state snapshot.
60    fn init_from_state_snapshot(&mut self, state_snapshot: StateSnapshot);
61}
62
63impl<'a, T: 'a + MaybeFullDatabase + ?Sized> MaybeFullDatabase for &'a T
64where
65    &'a T: DatabaseRef<Error = DatabaseError>,
66{
67    fn maybe_as_full_db(&self) -> Option<&AddressMap<DbAccount>> {
68        T::maybe_as_full_db(self)
69    }
70
71    fn clear_into_state_snapshot(&mut self) -> StateSnapshot {
72        unreachable!("never called for DatabaseRef")
73    }
74
75    fn read_as_state_snapshot(&self) -> StateSnapshot {
76        unreachable!("never called for DatabaseRef")
77    }
78
79    fn clear(&mut self) {}
80
81    fn init_from_state_snapshot(&mut self, _state_snapshot: StateSnapshot) {}
82}
83
84/// Helper trait to reset the DB if it's forked
85pub trait MaybeForkedDatabase {
86    fn maybe_reset(&mut self, _url: Option<String>, block_number: BlockId) -> Result<(), String>;
87
88    fn maybe_flush_cache(&self) -> Result<(), String>;
89
90    fn maybe_inner(&self) -> Result<&BlockchainDb, String>;
91}
92
93/// This bundles all required revm traits
94pub trait Db:
95    DatabaseRef<Error = DatabaseError>
96    + Database<Error = DatabaseError>
97    + DatabaseCommit
98    + MaybeFullDatabase
99    + MaybeForkedDatabase
100    + fmt::Debug
101    + Send
102    + Sync
103{
104    /// Inserts an account
105    fn insert_account(&mut self, address: Address, account: AccountInfo);
106
107    /// Sets the nonce of the given address
108    fn set_nonce(&mut self, address: Address, nonce: u64) -> DatabaseResult<()> {
109        let mut info = self.basic(address)?.unwrap_or_default();
110        info.nonce = nonce;
111        self.insert_account(address, info);
112        Ok(())
113    }
114
115    /// Sets the balance of the given address
116    fn set_balance(&mut self, address: Address, balance: U256) -> DatabaseResult<()> {
117        let mut info = self.basic(address)?.unwrap_or_default();
118        info.balance = balance;
119        self.insert_account(address, info);
120        Ok(())
121    }
122
123    /// Sets the code of the given address
124    fn set_code(&mut self, address: Address, code: Bytes) -> DatabaseResult<()> {
125        let mut info = self.basic(address)?.unwrap_or_default();
126        let code_hash = if code.as_ref().is_empty() {
127            KECCAK_EMPTY
128        } else {
129            B256::from_slice(&keccak256(code.as_ref())[..])
130        };
131        info.code_hash = code_hash;
132        info.code = Some(Bytecode::new_raw(alloy_primitives::Bytes(code.0)));
133        self.insert_account(address, info);
134        Ok(())
135    }
136
137    /// Sets the storage value at the given slot for the address
138    fn set_storage_at(&mut self, address: Address, slot: B256, val: B256) -> DatabaseResult<()>;
139
140    /// inserts a blockhash for the given number
141    fn insert_block_hash(&mut self, number: U256, hash: B256);
142
143    /// Write all chain data to serialized bytes buffer
144    fn dump_state(
145        &self,
146        at: BlockEnv,
147        best_number: u64,
148        blocks: Vec<SerializableBlock>,
149        transactions: Vec<SerializableTransaction>,
150        historical_states: Option<SerializableHistoricalStates>,
151    ) -> DatabaseResult<Option<SerializableState>>;
152
153    /// Deserialize and add all chain data to the backend storage
154    fn load_state(&mut self, state: SerializableState) -> DatabaseResult<bool> {
155        for (addr, account) in state.accounts.into_iter() {
156            let old_account_nonce = DatabaseRef::basic_ref(self, addr)
157                .ok()
158                .and_then(|acc| acc.map(|acc| acc.nonce))
159                .unwrap_or_default();
160            // use max nonce in case account is imported multiple times with difference
161            // nonces to prevent collisions
162            let nonce = std::cmp::max(old_account_nonce, account.nonce);
163
164            self.insert_account(
165                addr,
166                AccountInfo {
167                    balance: account.balance,
168                    code_hash: KECCAK_EMPTY, // will be set automatically
169                    code: if account.code.0.is_empty() {
170                        None
171                    } else {
172                        Some(Bytecode::new_raw(alloy_primitives::Bytes(account.code.0)))
173                    },
174                    nonce,
175                    account_id: None,
176                },
177            );
178
179            for (k, v) in account.storage.into_iter() {
180                self.set_storage_at(addr, k, v)?;
181            }
182        }
183        Ok(true)
184    }
185
186    /// Creates a new state snapshot.
187    fn snapshot_state(&mut self) -> U256;
188
189    /// Reverts a state snapshot.
190    ///
191    /// Returns `true` if the state snapshot was reverted.
192    fn revert_state(&mut self, state_snapshot: U256, action: RevertStateSnapshotAction) -> bool;
193
194    /// Returns the state root if possible to compute
195    fn maybe_state_root(&self) -> Option<B256> {
196        None
197    }
198
199    /// Returns the current, standalone state of the Db
200    fn current_state(&self) -> StateDb;
201}
202
203/// Convenience impl only used to use any `Db` on the fly as the db layer for revm's CacheDB
204/// This is useful to create blocks without actually writing to the `Db`, but rather in the cache of
205/// the `CacheDB` see also
206/// [Backend::pending_block()](crate::eth::backend::mem::Backend::pending_block())
207impl<T: DatabaseRef<Error = DatabaseError> + Send + Sync + Clone + fmt::Debug> Db for CacheDB<T> {
208    fn insert_account(&mut self, address: Address, account: AccountInfo) {
209        self.insert_account_info(address, account)
210    }
211
212    fn set_storage_at(&mut self, address: Address, slot: B256, val: B256) -> DatabaseResult<()> {
213        self.insert_account_storage(address, slot.into(), val.into())
214    }
215
216    fn insert_block_hash(&mut self, number: U256, hash: B256) {
217        self.cache.block_hashes.insert(number, hash);
218    }
219
220    fn dump_state(
221        &self,
222        _at: BlockEnv,
223        _best_number: u64,
224        _blocks: Vec<SerializableBlock>,
225        _transaction: Vec<SerializableTransaction>,
226        _historical_states: Option<SerializableHistoricalStates>,
227    ) -> DatabaseResult<Option<SerializableState>> {
228        Ok(None)
229    }
230
231    fn snapshot_state(&mut self) -> U256 {
232        U256::ZERO
233    }
234
235    fn revert_state(&mut self, _state_snapshot: U256, _action: RevertStateSnapshotAction) -> bool {
236        false
237    }
238
239    fn current_state(&self) -> StateDb {
240        StateDb::new(MemDb::default())
241    }
242}
243
244impl<T: DatabaseRef<Error = DatabaseError> + Debug> MaybeFullDatabase for CacheDB<T> {
245    fn maybe_as_full_db(&self) -> Option<&AddressMap<DbAccount>> {
246        Some(&self.cache.accounts)
247    }
248
249    fn clear_into_state_snapshot(&mut self) -> StateSnapshot {
250        let db_accounts = std::mem::take(&mut self.cache.accounts);
251        let mut accounts = HashMap::default();
252        let mut account_storage = HashMap::default();
253
254        for (addr, mut acc) in db_accounts {
255            account_storage.insert(addr, std::mem::take(&mut acc.storage));
256            let mut info = acc.info;
257            info.code = self.cache.contracts.remove(&info.code_hash);
258            accounts.insert(addr, info);
259        }
260        let block_hashes = std::mem::take(&mut self.cache.block_hashes);
261        StateSnapshot { accounts, storage: account_storage, block_hashes }
262    }
263
264    fn read_as_state_snapshot(&self) -> StateSnapshot {
265        let mut accounts = HashMap::default();
266        let mut account_storage = HashMap::default();
267
268        for (addr, acc) in &self.cache.accounts {
269            account_storage.insert(*addr, acc.storage.clone());
270            let mut info = acc.info.clone();
271            info.code = self.cache.contracts.get(&info.code_hash).cloned();
272            accounts.insert(*addr, info);
273        }
274
275        let block_hashes = self.cache.block_hashes.clone();
276        StateSnapshot { accounts, storage: account_storage, block_hashes }
277    }
278
279    fn clear(&mut self) {
280        self.clear_into_state_snapshot();
281    }
282
283    fn init_from_state_snapshot(&mut self, state_snapshot: StateSnapshot) {
284        let StateSnapshot { accounts, mut storage, block_hashes } = state_snapshot;
285
286        for (addr, mut acc) in accounts {
287            if let Some(code) = acc.code.take() {
288                self.cache.contracts.insert(acc.code_hash, code);
289            }
290            self.cache.accounts.insert(
291                addr,
292                DbAccount {
293                    info: acc,
294                    storage: storage.remove(&addr).unwrap_or_default(),
295                    ..Default::default()
296                },
297            );
298        }
299        self.cache.block_hashes = block_hashes;
300    }
301}
302
303impl<T: DatabaseRef<Error = DatabaseError>> MaybeForkedDatabase for CacheDB<T> {
304    fn maybe_reset(&mut self, _url: Option<String>, _block_number: BlockId) -> Result<(), String> {
305        Err("not supported".to_string())
306    }
307
308    fn maybe_flush_cache(&self) -> Result<(), String> {
309        Err("not supported".to_string())
310    }
311
312    fn maybe_inner(&self) -> Result<&BlockchainDb, String> {
313        Err("not supported".to_string())
314    }
315}
316
317/// Represents a state at certain point
318#[derive(Debug)]
319pub struct StateDb(pub(crate) Box<dyn MaybeFullDatabase + Send + Sync>);
320
321impl StateDb {
322    pub fn new(db: impl MaybeFullDatabase + Send + Sync + 'static) -> Self {
323        Self(Box::new(db))
324    }
325
326    pub fn serialize_state(&mut self) -> StateSnapshot {
327        // Using read_as_snapshot makes sures we don't clear the historical state from the current
328        // instance.
329        self.read_as_state_snapshot()
330    }
331}
332
333impl DatabaseRef for StateDb {
334    type Error = DatabaseError;
335    fn basic_ref(&self, address: Address) -> DatabaseResult<Option<AccountInfo>> {
336        self.0.basic_ref(address)
337    }
338
339    fn code_by_hash_ref(&self, code_hash: B256) -> DatabaseResult<Bytecode> {
340        self.0.code_by_hash_ref(code_hash)
341    }
342
343    fn storage_ref(&self, address: Address, index: U256) -> DatabaseResult<U256> {
344        self.0.storage_ref(address, index)
345    }
346
347    fn block_hash_ref(&self, number: u64) -> DatabaseResult<B256> {
348        self.0.block_hash_ref(number)
349    }
350}
351
352impl MaybeFullDatabase for StateDb {
353    fn maybe_as_full_db(&self) -> Option<&AddressMap<DbAccount>> {
354        self.0.maybe_as_full_db()
355    }
356
357    fn clear_into_state_snapshot(&mut self) -> StateSnapshot {
358        self.0.clear_into_state_snapshot()
359    }
360
361    fn read_as_state_snapshot(&self) -> StateSnapshot {
362        self.0.read_as_state_snapshot()
363    }
364
365    fn clear(&mut self) {
366        self.0.clear()
367    }
368
369    fn init_from_state_snapshot(&mut self, state_snapshot: StateSnapshot) {
370        self.0.init_from_state_snapshot(state_snapshot)
371    }
372}
373
374/// Legacy block environment from before v1.3.
375#[derive(Debug, Deserialize)]
376#[serde(rename_all = "snake_case")]
377pub struct LegacyBlockEnv {
378    pub number: Option<StringOrU64>,
379    #[serde(alias = "coinbase")]
380    pub beneficiary: Option<Address>,
381    pub timestamp: Option<StringOrU64>,
382    pub gas_limit: Option<StringOrU64>,
383    pub basefee: Option<StringOrU64>,
384    pub difficulty: Option<StringOrU64>,
385    pub prevrandao: Option<B256>,
386    pub blob_excess_gas_and_price: Option<LegacyBlobExcessGasAndPrice>,
387}
388
389/// Legacy blob excess gas and price structure from before v1.3.
390#[derive(Debug, Deserialize)]
391pub struct LegacyBlobExcessGasAndPrice {
392    pub excess_blob_gas: u64,
393    pub blob_gasprice: u64,
394}
395
396/// Legacy string or u64 type from before v1.3.
397#[derive(Debug, Deserialize)]
398#[serde(untagged)]
399pub enum StringOrU64 {
400    Hex(String),
401    Dec(u64),
402}
403
404impl StringOrU64 {
405    pub fn to_u64(&self) -> Option<u64> {
406        match self {
407            Self::Dec(n) => Some(*n),
408            Self::Hex(s) => s.strip_prefix("0x").and_then(|s| u64::from_str_radix(s, 16).ok()),
409        }
410    }
411
412    pub fn to_u256(&self) -> Option<U256> {
413        match self {
414            Self::Dec(n) => Some(U256::from(*n)),
415            Self::Hex(s) => s.strip_prefix("0x").and_then(|s| U256::from_str_radix(s, 16).ok()),
416        }
417    }
418}
419
420/// Converts a `LegacyBlockEnv` to a `BlockEnv`, handling the conversion of legacy fields.
421impl TryFrom<LegacyBlockEnv> for BlockEnv {
422    type Error = &'static str;
423
424    fn try_from(legacy: LegacyBlockEnv) -> Result<Self, Self::Error> {
425        Ok(Self {
426            number: legacy.number.and_then(|v| v.to_u256()).unwrap_or(U256::ZERO),
427            beneficiary: legacy.beneficiary.unwrap_or(Address::ZERO),
428            timestamp: legacy.timestamp.and_then(|v| v.to_u256()).unwrap_or(U256::ONE),
429            gas_limit: legacy.gas_limit.and_then(|v| v.to_u64()).unwrap_or(u64::MAX),
430            basefee: legacy.basefee.and_then(|v| v.to_u64()).unwrap_or(0),
431            difficulty: legacy.difficulty.and_then(|v| v.to_u256()).unwrap_or(U256::ZERO),
432            prevrandao: legacy.prevrandao.or(Some(B256::ZERO)),
433            blob_excess_gas_and_price: legacy
434                .blob_excess_gas_and_price
435                .map(|v| BlobExcessGasAndPrice::new(v.excess_blob_gas, v.blob_gasprice))
436                .or_else(|| {
437                    Some(BlobExcessGasAndPrice::new(0, BLOB_BASE_FEE_UPDATE_FRACTION_PRAGUE))
438                }),
439        })
440    }
441}
442
443/// Custom deserializer for `BlockEnv` that handles both v1.2 and v1.3+ formats.
444fn deserialize_block_env_compat<'de, D>(deserializer: D) -> Result<Option<BlockEnv>, D::Error>
445where
446    D: Deserializer<'de>,
447{
448    let value: Option<Value> = Option::deserialize(deserializer)?;
449    let Some(value) = value else {
450        return Ok(None);
451    };
452
453    if let Ok(env) = BlockEnv::deserialize(&value) {
454        return Ok(Some(env));
455    }
456
457    let legacy: LegacyBlockEnv = serde_json::from_value(value).map_err(|e| {
458        D::Error::custom(format!("Legacy deserialization of `BlockEnv` failed: {e}"))
459    })?;
460
461    Ok(Some(BlockEnv::try_from(legacy).map_err(D::Error::custom)?))
462}
463
464/// Custom deserializer for `best_block_number` that handles both v1.2 and v1.3+ formats.
465fn deserialize_best_block_number_compat<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
466where
467    D: Deserializer<'de>,
468{
469    let value: Option<Value> = Option::deserialize(deserializer)?;
470    let Some(value) = value else {
471        return Ok(None);
472    };
473
474    let number = match value {
475        Value::Number(n) => n.as_u64(),
476        Value::String(s) => {
477            if let Some(s) = s.strip_prefix("0x") {
478                u64::from_str_radix(s, 16).ok()
479            } else {
480                s.parse().ok()
481            }
482        }
483        _ => None,
484    };
485
486    Ok(number)
487}
488
489#[derive(Clone, Debug, Default, Serialize, Deserialize)]
490pub struct SerializableState {
491    /// The block number of the state
492    ///
493    /// Note: This is an Option for backwards compatibility: <https://github.com/foundry-rs/foundry/issues/5460>
494    #[serde(deserialize_with = "deserialize_block_env_compat")]
495    pub block: Option<BlockEnv>,
496    pub accounts: BTreeMap<Address, SerializableAccountRecord>,
497    /// The best block number of the state, can be different from block number (Arbitrum chain).
498    #[serde(deserialize_with = "deserialize_best_block_number_compat")]
499    pub best_block_number: Option<u64>,
500    #[serde(default)]
501    pub blocks: Vec<SerializableBlock>,
502    #[serde(default)]
503    pub transactions: Vec<SerializableTransaction>,
504    /// Historical states of accounts and storage at particular block hashes.
505    ///
506    /// Note: This is an Option for backwards compatibility.
507    #[serde(default)]
508    pub historical_states: Option<SerializableHistoricalStates>,
509}
510
511impl SerializableState {
512    /// Loads the `Genesis` object from the given json file path
513    pub fn load(path: impl AsRef<Path>) -> Result<Self, FsPathError> {
514        let path = path.as_ref();
515        if path.is_dir() {
516            foundry_common::fs::read_json_file(&path.join("state.json"))
517        } else {
518            foundry_common::fs::read_json_file(path)
519        }
520    }
521
522    /// This is used as the clap `value_parser` implementation
523    #[cfg(feature = "cmd")]
524    pub(crate) fn parse(path: &str) -> Result<Self, String> {
525        Self::load(path).map_err(|err| err.to_string())
526    }
527}
528
529#[derive(Clone, Debug, Serialize, Deserialize)]
530pub struct SerializableAccountRecord {
531    pub nonce: u64,
532    pub balance: U256,
533    pub code: Bytes,
534
535    #[serde(deserialize_with = "deserialize_btree")]
536    pub storage: BTreeMap<B256, B256>,
537}
538
539fn deserialize_btree<'de, D>(deserializer: D) -> Result<BTreeMap<B256, B256>, D::Error>
540where
541    D: Deserializer<'de>,
542{
543    struct BTreeVisitor;
544
545    impl<'de> Visitor<'de> for BTreeVisitor {
546        type Value = BTreeMap<B256, B256>;
547
548        fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
549            formatter.write_str("a mapping of hex encoded storage slots to hex encoded state data")
550        }
551
552        fn visit_map<M>(self, mut mapping: M) -> Result<BTreeMap<B256, B256>, M::Error>
553        where
554            M: MapAccess<'de>,
555        {
556            let mut btree = BTreeMap::new();
557            while let Some((key, value)) = mapping.next_entry::<U256, U256>()? {
558                btree.insert(B256::from(key), B256::from(value));
559            }
560
561            Ok(btree)
562        }
563    }
564
565    deserializer.deserialize_map(BTreeVisitor)
566}
567
568/// Defines a backwards-compatible enum for transactions.
569/// This is essential for maintaining compatibility with state dumps
570/// created before the changes introduced in PR #8411.
571///
572/// The enum can represent either a `TypedTransaction` or a `MaybeImpersonatedTransaction`,
573/// depending on the data being deserialized. This flexibility ensures that older state
574/// dumps can still be loaded correctly, even after the changes in #8411.
575#[derive(Clone, Debug, Serialize, Deserialize)]
576#[serde(untagged)]
577pub enum SerializableTransactionType {
578    TypedTransaction(FoundryTxEnvelope),
579    MaybeImpersonatedTransaction(MaybeImpersonatedTransaction),
580}
581
582#[derive(Clone, Debug, Serialize, Deserialize)]
583pub struct SerializableBlock {
584    pub header: Header,
585    pub transactions: Vec<SerializableTransactionType>,
586    pub ommers: Vec<Header>,
587    #[serde(default)]
588    pub withdrawals: Option<Withdrawals>,
589}
590
591impl From<Block> for SerializableBlock {
592    fn from(block: Block) -> Self {
593        Self {
594            header: block.header,
595            transactions: block.body.transactions.into_iter().map(Into::into).collect(),
596            ommers: block.body.ommers.into_iter().collect(),
597            withdrawals: block.body.withdrawals,
598        }
599    }
600}
601
602impl From<SerializableBlock> for Block {
603    fn from(block: SerializableBlock) -> Self {
604        let transactions = block.transactions.into_iter().map(Into::into).collect();
605        let ommers = block.ommers;
606        let body = BlockBody { transactions, ommers, withdrawals: block.withdrawals };
607        Self::new(block.header, body)
608    }
609}
610
611impl From<MaybeImpersonatedTransaction> for SerializableTransactionType {
612    fn from(transaction: MaybeImpersonatedTransaction) -> Self {
613        Self::MaybeImpersonatedTransaction(transaction)
614    }
615}
616
617impl From<SerializableTransactionType> for MaybeImpersonatedTransaction {
618    fn from(transaction: SerializableTransactionType) -> Self {
619        match transaction {
620            SerializableTransactionType::TypedTransaction(tx) => Self::new(tx),
621            SerializableTransactionType::MaybeImpersonatedTransaction(tx) => tx,
622        }
623    }
624}
625
626#[derive(Clone, Debug, Serialize, Deserialize)]
627pub struct SerializableTransaction {
628    pub info: TransactionInfo,
629    pub receipt: FoundryReceiptEnvelope,
630    pub block_hash: B256,
631    pub block_number: u64,
632}
633
634impl From<MinedTransaction> for SerializableTransaction {
635    fn from(transaction: MinedTransaction) -> Self {
636        Self {
637            info: transaction.info,
638            receipt: transaction.receipt,
639            block_hash: transaction.block_hash,
640            block_number: transaction.block_number,
641        }
642    }
643}
644
645impl From<SerializableTransaction> for MinedTransaction {
646    fn from(transaction: SerializableTransaction) -> Self {
647        Self {
648            info: transaction.info,
649            receipt: transaction.receipt,
650            block_hash: transaction.block_hash,
651            block_number: transaction.block_number,
652        }
653    }
654}
655
656#[derive(Clone, Debug, Serialize, Deserialize, Default)]
657pub struct SerializableHistoricalStates(Vec<(B256, StateSnapshot)>);
658
659impl SerializableHistoricalStates {
660    pub const fn new(states: Vec<(B256, StateSnapshot)>) -> Self {
661        Self(states)
662    }
663}
664
665impl IntoIterator for SerializableHistoricalStates {
666    type Item = (B256, StateSnapshot);
667    type IntoIter = std::vec::IntoIter<Self::Item>;
668
669    fn into_iter(self) -> Self::IntoIter {
670        self.0.into_iter()
671    }
672}
673
674#[cfg(test)]
675mod test {
676    use super::*;
677
678    #[test]
679    fn test_deser_block() {
680        let block = r#"{
681            "header": {
682                "parentHash": "0xceb0fe420d6f14a8eeec4319515b89acbb0bb4861cad9983d529ab4b1e4af929",
683                "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
684                "miner": "0x0000000000000000000000000000000000000000",
685                "stateRoot": "0xe1423fd180478ab4fd05a7103277d64496b15eb914ecafe71eeec871b552efd1",
686                "transactionsRoot": "0x2b5598ef261e5f88e4303bb2b3986b3d5c0ebf4cd9977daebccae82a6469b988",
687                "receiptsRoot": "0xf78dfb743fbd92ade140711c8bbc542b5e307f0ab7984eff35d751969fe57efa",
688                "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
689                "difficulty": "0x0",
690                "number": "0x2",
691                "gasLimit": "0x1c9c380",
692                "gasUsed": "0x5208",
693                "timestamp": "0x66cdc823",
694                "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
695                "nonce": "0x0000000000000000",
696                "baseFeePerGas": "0x342a1c58",
697                "blobGasUsed": "0x0",
698                "excessBlobGas": "0x0",
699                "extraData": "0x"
700            },
701            "transactions": [
702                {
703                    "type": "0x2",
704                    "chainId": "0x7a69",
705                    "nonce": "0x0",
706                    "gas": "0x5209",
707                    "maxFeePerGas": "0x77359401",
708                    "maxPriorityFeePerGas": "0x1",
709                    "to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
710                    "value": "0x0",
711                    "accessList": [],
712                    "input": "0x",
713                    "r": "0x85c2794a580da137e24ccc823b45ae5cea99371ae23ee13860fcc6935f8305b0",
714                    "s": "0x41de7fa4121dab284af4453d30928241208bafa90cdb701fe9bc7054759fe3cd",
715                    "yParity": "0x0",
716                    "hash": "0x8c9b68e8947ace33028dba167354fde369ed7bbe34911b772d09b3c64b861515"
717                }
718            ],
719            "ommers": []
720        }
721        "#;
722
723        let _block: SerializableBlock = serde_json::from_str(block).unwrap();
724    }
725
726    #[test]
727    fn test_block_withdrawals_preserved() {
728        use alloy_eips::eip4895::Withdrawal;
729
730        // create a block with withdrawals (like post-Shanghai blocks)
731        let withdrawal = Withdrawal {
732            index: 42,
733            validator_index: 123,
734            address: Address::repeat_byte(1),
735            amount: 1000,
736        };
737
738        let header = Header::default();
739        let body = BlockBody {
740            transactions: vec![],
741            ommers: vec![],
742            withdrawals: Some(vec![withdrawal].into()),
743        };
744        let block = Block::new(header, body);
745
746        // convert to SerializableBlock and back
747        let serializable = SerializableBlock::from(block);
748        let restored = Block::from(serializable);
749
750        // withdrawals should be preserved
751        assert!(restored.body.withdrawals.is_some());
752        let withdrawals = restored.body.withdrawals.unwrap();
753        assert_eq!(withdrawals.len(), 1);
754        assert_eq!(withdrawals[0].index, 42);
755        assert_eq!(withdrawals[0].validator_index, 123);
756    }
757}