1use 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
42pub trait MaybeFullDatabase: DatabaseRef<Error = DatabaseError> + Debug {
44 fn maybe_as_full_db(&self) -> Option<&AddressMap<DbAccount>> {
45 None
46 }
47
48 fn clear_into_state_snapshot(&mut self) -> StateSnapshot;
50
51 fn read_as_state_snapshot(&self) -> StateSnapshot;
55
56 fn clear(&mut self);
58
59 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
84pub 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
93pub 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 fn insert_account(&mut self, address: Address, account: AccountInfo);
106
107 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 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 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 fn set_storage_at(&mut self, address: Address, slot: B256, val: B256) -> DatabaseResult<()>;
139
140 fn insert_block_hash(&mut self, number: U256, hash: B256);
142
143 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 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 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, 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 fn snapshot_state(&mut self) -> U256;
188
189 fn revert_state(&mut self, state_snapshot: U256, action: RevertStateSnapshotAction) -> bool;
193
194 fn maybe_state_root(&self) -> Option<B256> {
196 None
197 }
198
199 fn current_state(&self) -> StateDb;
201}
202
203impl<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#[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 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#[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#[derive(Debug, Deserialize)]
391pub struct LegacyBlobExcessGasAndPrice {
392 pub excess_blob_gas: u64,
393 pub blob_gasprice: u64,
394}
395
396#[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
420impl 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
443fn 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
464fn 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 #[serde(deserialize_with = "deserialize_block_env_compat")]
495 pub block: Option<BlockEnv>,
496 pub accounts: BTreeMap<Address, SerializableAccountRecord>,
497 #[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 #[serde(default)]
508 pub historical_states: Option<SerializableHistoricalStates>,
509}
510
511impl SerializableState {
512 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 #[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#[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 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 let serializable = SerializableBlock::from(block);
748 let restored = Block::from(serializable);
749
750 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}