1use alloy_chains::Chain;
4use alloy_primitives::{Address, B256, U256, map::U256Map};
5use parking_lot::RwLock;
6use revm::{
7 DatabaseCommit,
8 context::BlockEnv,
9 primitives::{KECCAK_EMPTY, StorageKeyMap, map::AddressHashMap},
10 state::{Account, AccountInfo, AccountStatus},
11};
12use serde::{
13 Deserialize, Deserializer, Serialize, Serializer, de::DeserializeOwned, ser::SerializeMap,
14};
15#[cfg(not(feature = "zstd"))]
16use std::io::{BufWriter, Write};
17use std::{
18 collections::BTreeSet,
19 fs,
20 path::{Path, PathBuf},
21 sync::Arc,
22};
23use url::Url;
24#[cfg(feature = "zstd")]
25use zstd::{Encoder, decode_all};
26
27pub type StorageInfo = StorageKeyMap<U256>;
28
29const ZSTD_FRAME_MAGIC: [u8; 4] = [0x28, 0xB5, 0x2F, 0xFD];
31
32#[derive(Clone, Debug)]
34pub struct BlockchainDb<B = BlockEnv> {
35 db: Arc<MemDb>,
37 meta: Arc<RwLock<BlockchainDbMeta<B>>>,
39 cache: Arc<JsonBlockCacheDB<B>>,
41}
42
43impl<B: ForkBlockEnv> BlockchainDb<B> {
44 pub fn new(meta: BlockchainDbMeta<B>, cache_path: Option<PathBuf>) -> Self {
55 Self::new_db(meta, cache_path, false)
56 }
57
58 pub fn new_skip_check(meta: BlockchainDbMeta<B>, cache_path: Option<PathBuf>) -> Self {
70 Self::new_db(meta, cache_path, true)
71 }
72
73 fn new_db(meta: BlockchainDbMeta<B>, cache_path: Option<PathBuf>, skip_check: bool) -> Self {
74 trace!(target: "forge::cache", cache=?cache_path, "initialising blockchain db");
75 let cache = cache_path
77 .as_ref()
78 .and_then(|p| {
79 JsonBlockCacheDB::load(p).ok().filter(|cache| {
80 if skip_check {
81 return true;
82 }
83 let mut existing = cache.meta().write();
84 existing.hosts.extend(meta.hosts.clone());
85 if meta == *existing {
86 true
87 } else {
88 warn!(target: "cache", "non-matching block metadata");
89 false
90 }
91 })
92 })
93 .unwrap_or_else(|| JsonBlockCacheDB::new(Arc::new(RwLock::new(meta)), cache_path));
94
95 Self { db: Arc::clone(cache.db()), meta: Arc::clone(cache.meta()), cache: Arc::new(cache) }
96 }
97
98 pub fn accounts(&self) -> &RwLock<AddressHashMap<AccountInfo>> {
100 &self.db.accounts
101 }
102
103 pub fn storage(&self) -> &RwLock<AddressHashMap<StorageInfo>> {
105 &self.db.storage
106 }
107
108 pub fn block_hashes(&self) -> &RwLock<U256Map<B256>> {
110 &self.db.block_hashes
111 }
112
113 pub const fn meta(&self) -> &Arc<RwLock<BlockchainDbMeta<B>>> {
115 &self.meta
116 }
117
118 pub const fn cache(&self) -> &Arc<JsonBlockCacheDB<B>> {
120 &self.cache
121 }
122
123 pub const fn db(&self) -> &Arc<MemDb> {
125 &self.db
126 }
127}
128
129pub trait ForkBlockEnv:
134 Serialize + DeserializeOwned + Default + Clone + PartialEq + Send + Sync + 'static
135{
136}
137
138impl<B: Serialize + DeserializeOwned + Default + Clone + PartialEq + Send + Sync + 'static>
139 ForkBlockEnv for B
140{
141}
142
143#[derive(Clone, Debug, Default, Eq)]
145pub struct BlockchainDbMeta<B> {
146 pub chain: Option<Chain>,
148 pub block_env: B,
150 pub hosts: BTreeSet<String>,
152}
153
154impl<B> BlockchainDbMeta<B> {
155 pub fn new(block_env: B, url: String) -> Self {
157 let host = Url::parse(&url)
158 .ok()
159 .and_then(|url| url.host().map(|host| host.to_string()))
160 .unwrap_or(url);
161
162 Self { chain: None, block_env, hosts: BTreeSet::from([host]) }
163 }
164
165 pub fn with_url(mut self, url: &str) -> Self {
167 let host = Url::parse(url)
168 .ok()
169 .and_then(|url| url.host().map(|host| host.to_string()))
170 .unwrap_or(url.to_string());
171 self.hosts.insert(host);
172 self
173 }
174
175 pub const fn set_chain(mut self, chain: Chain) -> Self {
177 self.chain = Some(chain);
178 self
179 }
180
181 pub fn set_block_env(mut self, block_env: B) -> Self {
183 self.block_env = block_env;
184 self
185 }
186}
187
188impl<B: PartialEq> PartialEq for BlockchainDbMeta<B> {
191 fn eq(&self, other: &Self) -> bool {
192 self.block_env == other.block_env
193 }
194}
195
196impl<B: Serialize> Serialize for BlockchainDbMeta<B> {
197 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
198 use serde::ser::SerializeStruct;
199
200 let field_count = if self.chain.is_some() { 3 } else { 2 };
201 let mut s = serializer.serialize_struct("BlockchainDbMeta", field_count)?;
202 if let Some(chain) = &self.chain {
203 s.serialize_field("chain", chain)?;
204 }
205 s.serialize_field("block_env", &self.block_env)?;
206 s.serialize_field("hosts", &self.hosts)?;
207 s.end()
208 }
209}
210
211struct BlockEnvBackwardsCompat<B> {
217 inner: B,
218}
219
220impl<'de, B: DeserializeOwned + Default + Serialize> Deserialize<'de>
221 for BlockEnvBackwardsCompat<B>
222{
223 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
224 let mut value = serde_json::Value::deserialize(deserializer)?;
225
226 if let Some(obj) = value.as_object_mut() {
228 let default_value = serde_json::to_value(B::default()).unwrap();
229 for (key, value) in default_value.as_object().unwrap() {
230 if !obj.contains_key(key) {
231 obj.insert(key.clone(), value.clone());
232 }
233 }
234 }
235
236 let inner: B = serde_json::from_value(value).map_err(serde::de::Error::custom)?;
237 Ok(Self { inner })
238 }
239}
240
241#[derive(Deserialize)]
242#[serde(untagged)]
243enum Hosts {
244 Multi(BTreeSet<String>),
245 Single(String),
246}
247
248impl From<Hosts> for BTreeSet<String> {
249 fn from(hosts: Hosts) -> Self {
250 match hosts {
251 Hosts::Multi(hosts) => hosts,
252 Hosts::Single(host) => Self::from([host]),
253 }
254 }
255}
256
257impl<'de, B: DeserializeOwned + Default + Serialize> Deserialize<'de> for BlockchainDbMeta<B> {
258 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
259 #[derive(Deserialize)]
260 #[serde(bound = "B: DeserializeOwned + Default + Serialize")]
261 struct Meta<B> {
262 chain: Option<Chain>,
263 block_env: BlockEnvBackwardsCompat<B>,
264 #[serde(alias = "host")]
265 hosts: Hosts,
266 }
267
268 let Meta { chain, block_env, hosts } = Meta::deserialize(deserializer)?;
269 Ok(Self { chain, block_env: block_env.inner, hosts: hosts.into() })
270 }
271}
272
273#[derive(Debug, Default)]
276pub struct MemDb {
277 pub accounts: RwLock<AddressHashMap<AccountInfo>>,
279 pub storage: RwLock<AddressHashMap<StorageInfo>>,
281 pub block_hashes: RwLock<U256Map<B256>>,
283}
284
285impl MemDb {
286 pub fn clear(&self) {
288 self.accounts.write().clear();
289 self.storage.write().clear();
290 self.block_hashes.write().clear();
291 }
292
293 pub fn do_insert_account(&self, address: Address, account: AccountInfo) {
295 self.accounts.write().insert(address, account);
296 }
297
298 pub fn do_commit(&self, changes: AddressHashMap<Account>) {
300 let mut storage = self.storage.write();
301 let mut accounts = self.accounts.write();
302 for (add, mut acc) in changes {
303 if acc.is_empty() || acc.is_selfdestructed() {
304 accounts.remove(&add);
305 storage.remove(&add);
306 } else {
307 if let Some(code_hash) = acc
309 .info
310 .code
311 .as_ref()
312 .filter(|code| !code.is_empty())
313 .map(|code| code.hash_slow())
314 {
315 acc.info.code_hash = code_hash;
316 } else if acc.info.code_hash.is_zero() {
317 acc.info.code_hash = KECCAK_EMPTY;
318 }
319 accounts.insert(add, acc.info);
320
321 let acc_storage = storage.entry(add).or_default();
322 if acc.status.contains(AccountStatus::Created) {
323 acc_storage.clear();
324 }
325 for (index, value) in acc.storage {
326 if value.present_value().is_zero() {
327 acc_storage.remove(&index);
328 } else {
329 acc_storage.insert(index, value.present_value());
330 }
331 }
332 if acc_storage.is_empty() {
333 storage.remove(&add);
334 }
335 }
336 }
337 }
338}
339
340impl Clone for MemDb {
341 fn clone(&self) -> Self {
342 Self {
343 storage: RwLock::new(self.storage.read().clone()),
344 accounts: RwLock::new(self.accounts.read().clone()),
345 block_hashes: RwLock::new(self.block_hashes.read().clone()),
346 }
347 }
348}
349
350impl DatabaseCommit for MemDb {
351 fn commit(&mut self, changes: AddressHashMap<Account>) {
352 self.do_commit(changes)
353 }
354}
355
356#[derive(Debug)]
358pub struct JsonBlockCacheDB<B> {
359 cache_path: Option<PathBuf>,
363 data: JsonBlockCacheData<B>,
365}
366
367impl<B> JsonBlockCacheDB<B> {
368 fn new(meta: Arc<RwLock<BlockchainDbMeta<B>>>, cache_path: Option<PathBuf>) -> Self {
370 Self { cache_path, data: JsonBlockCacheData { meta, data: Arc::new(Default::default()) } }
371 }
372
373 pub const fn db(&self) -> &Arc<MemDb> {
375 &self.data.data
376 }
377
378 pub const fn meta(&self) -> &Arc<RwLock<BlockchainDbMeta<B>>> {
380 &self.data.meta
381 }
382
383 pub const fn is_transient(&self) -> bool {
385 self.cache_path.is_none()
386 }
387
388 pub fn cache_path(&self) -> Option<&Path> {
390 self.cache_path.as_deref()
391 }
392}
393
394impl<B: ForkBlockEnv> JsonBlockCacheDB<B> {
395 pub fn load(path: impl Into<PathBuf>) -> eyre::Result<Self> {
406 let path = path.into();
407 trace!(target: "cache", ?path, "reading json cache");
408 let raw = fs::read(&path).inspect_err(|err| {
409 warn!(?err, ?path, "Failed to read cache file");
410 })?;
411
412 let json_bytes = maybe_decompress(&raw, &path).inspect_err(|err| {
413 warn!(target: "cache", ?err, ?path, "Failed to decode cache file");
414 })?;
415
416 let data = serde_json::from_slice(&json_bytes).inspect_err(|err| {
417 warn!(target: "cache", ?err, ?path, "Failed to deserialize cache data");
418 })?;
419 trace!(target: "cache", ?path, "read json cache");
420 Ok(Self { cache_path: Some(path), data })
421 }
422}
423
424fn maybe_decompress(raw: &[u8], path: &Path) -> eyre::Result<Vec<u8>> {
429 if !raw.starts_with(&ZSTD_FRAME_MAGIC) {
430 return Ok(raw.to_vec());
431 }
432
433 #[cfg(feature = "zstd")]
434 {
435 trace!(target: "cache", ?path, "decompressing zstd cache");
436 let decompressed = decode_all(raw).inspect_err(|err| {
437 warn!(target: "cache", ?err, ?path, "Failed to decompress zstd cache");
438 })?;
439 Ok(decompressed)
440 }
441
442 #[cfg(not(feature = "zstd"))]
443 {
444 eyre::bail!(
445 "cache file at `{}` is zstd-compressed but the `zstd` feature is not enabled",
446 path.display()
447 );
448 }
449}
450
451impl<B: Serialize + Clone> JsonBlockCacheDB<B> {
452 #[instrument(level = "warn", skip_all, fields(path = ?self.cache_path))]
454 pub fn flush(&self) {
455 let Some(path) = &self.cache_path else { return };
456 self.flush_to(path.as_path());
457 }
458
459 pub fn flush_to(&self, cache_path: &Path) {
464 let path: &Path = cache_path;
465
466 trace!(target: "cache", "saving json cache");
467
468 if let Some(parent) = path.parent() {
469 let _ = fs::create_dir_all(parent);
470 }
471
472 let file = match fs::File::create(path) {
473 Ok(file) => file,
474 Err(e) => return warn!(target: "cache", %e, "Failed to open json cache for writing"),
475 };
476
477 #[cfg(feature = "zstd")]
478 {
479 let mut encoder = match Encoder::new(file, 3) {
481 Ok(enc) => enc,
482 Err(e) => return warn!(target: "cache", %e, "Failed to create zstd encoder"),
483 };
484 if let Err(e) = serde_json::to_writer(&mut encoder, &self.data) {
485 return warn!(target: "cache", %e, "Failed to write to zstd json cache");
486 }
487 if let Err(e) = encoder.finish() {
488 return warn!(target: "cache", %e, "Failed to finish zstd compression");
489 }
490 }
491
492 #[cfg(not(feature = "zstd"))]
493 {
494 let mut writer = BufWriter::new(file);
495 if let Err(e) = serde_json::to_writer(&mut writer, &self.data) {
496 return warn!(target: "cache", %e, "Failed to write to json cache");
497 }
498 if let Err(e) = writer.flush() {
499 return warn!(target: "cache", %e, "Failed to flush to json cache");
500 }
501 }
502
503 trace!(target: "cache", "saved json cache");
504 }
505}
506
507#[derive(Debug)]
512pub struct JsonBlockCacheData<B> {
513 pub meta: Arc<RwLock<BlockchainDbMeta<B>>>,
514 pub data: Arc<MemDb>,
515}
516
517impl<B: Serialize + Clone> Serialize for JsonBlockCacheData<B> {
518 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
519 let mut map = serializer.serialize_map(Some(4))?;
520
521 map.serialize_entry("meta", &self.meta.read().clone())?;
522 map.serialize_entry("accounts", &self.data.accounts.read().clone())?;
523 map.serialize_entry("storage", &self.data.storage.read().clone())?;
524 map.serialize_entry("block_hashes", &self.data.block_hashes.read().clone())?;
525
526 map.end()
527 }
528}
529
530impl<'de, B: DeserializeOwned + Default + Serialize> Deserialize<'de> for JsonBlockCacheData<B> {
531 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
532 #[derive(Deserialize)]
533 struct Data<B> {
534 meta: B,
535 accounts: AddressHashMap<AccountInfo>,
536 storage: AddressHashMap<StorageInfo>,
537 block_hashes: U256Map<B256>,
538 }
539
540 let Data { meta, accounts, storage, block_hashes } =
541 Data::<BlockchainDbMeta<B>>::deserialize(deserializer)?;
542
543 Ok(Self {
544 meta: Arc::new(RwLock::new(meta)),
545 data: Arc::new(MemDb {
546 accounts: RwLock::new(accounts),
547 storage: RwLock::new(storage),
548 block_hashes: RwLock::new(block_hashes),
549 }),
550 })
551 }
552}
553
554#[derive(Debug)]
559pub struct FlushJsonBlockCacheDB<B: Serialize + Clone>(pub Arc<JsonBlockCacheDB<B>>);
560
561impl<B: Serialize + Clone> Drop for FlushJsonBlockCacheDB<B> {
562 fn drop(&mut self) {
563 trace!(target: "fork::cache", "flushing cache");
564 self.0.flush();
565 trace!(target: "fork::cache", "flushed cache");
566 }
567}
568
569#[cfg(test)]
570mod tests {
571 use super::*;
572
573 #[test]
574 fn can_deserialize_cache() {
575 let s = r#"{
576 "meta": {
577 "cfg_env": {
578 "chain_id": 1337,
579 "perf_analyse_created_bytecodes": "Analyse",
580 "limit_contract_code_size": 18446744073709551615,
581 "memory_limit": 4294967295,
582 "disable_block_gas_limit": false,
583 "disable_eip3607": false,
584 "disable_base_fee": false
585 },
586 "block_env": {
587 "number": 15547871,
588 "coinbase": "0x0000000000000000000000000000000000000000",
589 "timestamp": 1663351871,
590 "difficulty": "0x0",
591 "basefee": 12448539171,
592 "gas_limit": 30000000,
593 "prevrandao": "0x0000000000000000000000000000000000000000000000000000000000000000"
594 },
595 "hosts": [
596 "eth-mainnet.alchemyapi.io"
597 ]
598 },
599 "accounts": {
600 "0xb8ffc3cd6e7cf5a098a1c92f48009765b24088dc": {
601 "balance": "0x0",
602 "nonce": 10,
603 "code_hash": "0x3ac64c95eedf82e5d821696a12daac0e1b22c8ee18a9fd688b00cfaf14550aad",
604 "code": {
605 "LegacyAnalyzed": {
606 "bytecode": "0x00",
607 "original_len": 0,
608 "jump_table": {
609 "order": "bitvec::order::Lsb0",
610 "head": {
611 "width": 8,
612 "index": 0
613 },
614 "bits": 1,
615 "data": [0]
616 }
617 }
618 }
619 }
620 },
621 "storage": {
622 "0xa354f35829ae975e850e23e9615b11da1b3dc4de": {
623 "0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564": "0x5553444320795661756c74000000000000000000000000000000000000000000",
624 "0x10": "0x37fd60ff8346",
625 "0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563": "0xb",
626 "0x6": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
627 "0x5": "0x36ff5b93162e",
628 "0x14": "0x29d635a8e000",
629 "0x11": "0x63224c73",
630 "0x2": "0x6"
631 }
632 },
633 "block_hashes": {
634 "0xed3deb": "0xbf7be3174b261ea3c377b6aba4a1e05d5fae7eee7aab5691087c20cf353e9877",
635 "0xed3de9": "0xba1c3648e0aee193e7d00dffe4e9a5e420016b4880455641085a4731c1d32eef",
636 "0xed3de8": "0x61d1491c03a9295fb13395cca18b17b4fa5c64c6b8e56ee9cc0a70c3f6cf9855",
637 "0xed3de7": "0xb54560b5baeccd18350d56a3bee4035432294dc9d2b7e02f157813e1dee3a0be",
638 "0xed3dea": "0x816f124480b9661e1631c6ec9ee39350bda79f0cbfc911f925838d88e3d02e4b"
639 }
640}"#;
641
642 let cache: JsonBlockCacheData<BlockEnv> = serde_json::from_str(s).unwrap();
643 assert_eq!(cache.data.accounts.read().len(), 1);
644 assert_eq!(cache.data.storage.read().len(), 1);
645 assert_eq!(cache.data.block_hashes.read().len(), 5);
646
647 let _s = serde_json::to_string(&cache).unwrap();
648 }
649
650 #[test]
651 fn can_deserialize_cache_post_4844() {
652 let s = r#"{
653 "meta": {
654 "cfg_env": {
655 "chain_id": 1,
656 "kzg_settings": "Default",
657 "perf_analyse_created_bytecodes": "Analyse",
658 "limit_contract_code_size": 18446744073709551615,
659 "memory_limit": 134217728,
660 "disable_block_gas_limit": false,
661 "disable_eip3607": true,
662 "disable_base_fee": false,
663 "optimism": false
664 },
665 "block_env": {
666 "number": 18651580,
667 "coinbase": "0x4838b106fce9647bdf1e7877bf73ce8b0bad5f97",
668 "timestamp": 1700950019,
669 "gas_limit": 30000000,
670 "basefee": 26886078239,
671 "difficulty": "0xc6b1a299886016dea3865689f8393b9bf4d8f4fe8c0ad25f0058b3569297c057",
672 "prevrandao": "0xc6b1a299886016dea3865689f8393b9bf4d8f4fe8c0ad25f0058b3569297c057",
673 "blob_excess_gas_and_price": {
674 "excess_blob_gas": 0,
675 "blob_gasprice": 1
676 }
677 },
678 "hosts": [
679 "eth-mainnet.alchemyapi.io"
680 ]
681 },
682 "accounts": {
683 "0x4838b106fce9647bdf1e7877bf73ce8b0bad5f97": {
684 "balance": "0x8e0c373cfcdfd0eb",
685 "nonce": 128912,
686 "code_hash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470",
687 "code": {
688 "LegacyAnalyzed": {
689 "bytecode": "0x00",
690 "original_len": 0,
691 "jump_table": {
692 "order": "bitvec::order::Lsb0",
693 "head": {
694 "width": 8,
695 "index": 0
696 },
697 "bits": 1,
698 "data": [0]
699 }
700 }
701 }
702 }
703 },
704 "storage": {},
705 "block_hashes": {}
706}"#;
707
708 let cache: JsonBlockCacheData<BlockEnv> = serde_json::from_str(s).unwrap();
709 assert_eq!(cache.data.accounts.read().len(), 1);
710
711 let _s = serde_json::to_string(&cache).unwrap();
712 }
713
714 #[test]
715 fn roundtrip_meta_block_env() {
716 let meta = BlockchainDbMeta {
717 chain: Some(Chain::mainnet()),
718 block_env: BlockEnv { number: U256::from(1u64), ..Default::default() },
719 hosts: BTreeSet::from(["eth-mainnet.alchemyapi.io".to_string()]),
720 };
721 let json = serde_json::to_string(&meta).unwrap();
722 let recovered: BlockchainDbMeta<BlockEnv> = serde_json::from_str(&json).unwrap();
723 assert_eq!(meta, recovered);
724 }
725
726 #[test]
727 fn can_return_cache_path_if_set() {
728 let cache_db = JsonBlockCacheDB::<BlockEnv>::new(
730 Arc::new(RwLock::new(BlockchainDbMeta::default())),
731 Some(PathBuf::from("/tmp/foo")),
732 );
733 assert_eq!(Some(Path::new("/tmp/foo")), cache_db.cache_path());
734
735 let cache_db = JsonBlockCacheDB::<BlockEnv>::new(
737 Arc::new(RwLock::new(BlockchainDbMeta::default())),
738 None,
739 );
740 assert_eq!(None, cache_db.cache_path());
741 }
742
743 #[test]
744 fn is_zstd_detection() {
745 assert!([0x28, 0xB5, 0x2F, 0xFD, 0x00].starts_with(&ZSTD_FRAME_MAGIC));
747 assert!(!b"{\"meta\":{}}".starts_with(&ZSTD_FRAME_MAGIC));
749 assert!(![0x28, 0xB5].starts_with(&ZSTD_FRAME_MAGIC));
751 assert!(![].starts_with(&ZSTD_FRAME_MAGIC));
753 }
754
755 #[test]
756 fn flush_and_load_roundtrip() {
757 let dir = tempfile::tempdir().unwrap();
758 let path = dir.path().join("cache.json");
759
760 let meta = BlockchainDbMeta::<BlockEnv>::default();
761 let db = BlockchainDb::new(meta, Some(path.clone()));
762
763 db.accounts().write().insert(
765 Address::ZERO,
766 AccountInfo { balance: U256::from(42u64), nonce: 1, ..Default::default() },
767 );
768
769 db.cache().flush();
770 assert!(path.exists());
771
772 let raw = fs::read(&path).unwrap();
773 #[cfg(feature = "zstd")]
774 assert!(
775 raw.starts_with(&ZSTD_FRAME_MAGIC),
776 "should be zstd-compressed with the zstd feature"
777 );
778 #[cfg(not(feature = "zstd"))]
779 assert!(!raw.starts_with(&ZSTD_FRAME_MAGIC), "should be plain JSON without zstd feature");
780
781 let loaded = JsonBlockCacheDB::<BlockEnv>::load(&path).unwrap();
783 assert_eq!(loaded.db().accounts.read().len(), 1);
784 assert_eq!(loaded.db().accounts.read()[&Address::ZERO].balance, U256::from(42u64));
785 }
786
787 #[cfg(not(feature = "zstd"))]
788 #[test]
789 fn load_errors_on_zstd_data_without_feature() {
790 let dir = tempfile::tempdir().unwrap();
791 let path = dir.path().join("cache.json.zst");
792
793 fs::write(&path, [0x28, 0xB5, 0x2F, 0xFD, 0x00]).unwrap();
794
795 let err = JsonBlockCacheDB::<BlockEnv>::load(&path).unwrap_err();
796 assert!(err.to_string().contains("zstd-compressed but the `zstd` feature is not enabled"));
797 }
798
799 #[cfg(feature = "zstd")]
800 #[test]
801 fn zstd_flush_and_load_roundtrip() {
802 let dir = tempfile::tempdir().unwrap();
803 let path = dir.path().join("cache.json.zst");
804
805 let meta = BlockchainDbMeta::<BlockEnv>::default();
806 let db = BlockchainDb::new(meta, Some(path.clone()));
807
808 db.accounts().write().insert(
809 Address::ZERO,
810 AccountInfo { balance: U256::from(100u64), nonce: 5, ..Default::default() },
811 );
812 db.storage().write().insert(Address::ZERO, Default::default());
813
814 db.cache().flush();
815 assert!(path.exists());
816
817 let raw = fs::read(&path).unwrap();
819 assert!(
820 raw.starts_with(&ZSTD_FRAME_MAGIC),
821 "should be zstd-compressed with the zstd feature"
822 );
823
824 let loaded = JsonBlockCacheDB::<BlockEnv>::load(&path).unwrap();
826 assert_eq!(loaded.db().accounts.read().len(), 1);
827 assert_eq!(loaded.db().accounts.read()[&Address::ZERO].nonce, 5);
828 }
829
830 #[cfg(feature = "zstd")]
831 #[test]
832 fn zstd_can_load_plain_json() {
833 let dir = tempfile::tempdir().unwrap();
834 let path = dir.path().join("cache.json");
835
836 let meta = BlockchainDbMeta::<BlockEnv>::default();
838 let data = JsonBlockCacheData::<BlockEnv> {
839 meta: Arc::new(RwLock::new(meta)),
840 data: Arc::new(MemDb::default()),
841 };
842 let json = serde_json::to_vec(&data).unwrap();
843 fs::write(&path, &json).unwrap();
844
845 let loaded = JsonBlockCacheDB::<BlockEnv>::load(&path).unwrap();
847 assert_eq!(loaded.db().accounts.read().len(), 0);
848 }
849}