Skip to main content

foundry_fork_db/
cache.rs

1//! Cache related abstraction
2
3use 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
29/// Zstd frame magic number: `0x28B52FFD` (little-endian).
30const ZSTD_FRAME_MAGIC: [u8; 4] = [0x28, 0xB5, 0x2F, 0xFD];
31
32/// A shareable Block database
33#[derive(Clone, Debug)]
34pub struct BlockchainDb<B = BlockEnv> {
35    /// Contains all the data
36    db: Arc<MemDb>,
37    /// metadata of the current config
38    meta: Arc<RwLock<BlockchainDbMeta<B>>>,
39    /// the cache that can be flushed
40    cache: Arc<JsonBlockCacheDB<B>>,
41}
42
43impl<B: ForkBlockEnv> BlockchainDb<B> {
44    /// Creates a new instance of the [BlockchainDb].
45    ///
46    /// If a `cache_path` is provided it attempts to load a previously stored [JsonBlockCacheData]
47    /// and will try to use the cached entries it holds.
48    ///
49    /// This will return a new and empty [MemDb] if
50    ///   - `cache_path` is `None`
51    ///   - the file the `cache_path` points to, does not exist
52    ///   - the file contains malformed data, or if it couldn't be read
53    ///   - the provided `meta` differs from [BlockchainDbMeta] that's stored on disk
54    pub fn new(meta: BlockchainDbMeta<B>, cache_path: Option<PathBuf>) -> Self {
55        Self::new_db(meta, cache_path, false)
56    }
57
58    /// Creates a new instance of the [BlockchainDb] and skips check when comparing meta
59    /// This is useful for offline-start mode when we don't want to fetch metadata of `block`.
60    ///
61    /// if a `cache_path` is provided it attempts to load a previously stored [JsonBlockCacheData]
62    /// and will try to use the cached entries it holds.
63    ///
64    /// This will return a new and empty [MemDb] if
65    ///   - `cache_path` is `None`
66    ///   - the file the `cache_path` points to, does not exist
67    ///   - the file contains malformed data, or if it couldn't be read
68    ///   - the provided `meta` differs from [BlockchainDbMeta] that's stored on disk
69    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        // read cache and check if metadata matches
76        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    /// Returns the map that holds the account related info
99    pub fn accounts(&self) -> &RwLock<AddressHashMap<AccountInfo>> {
100        &self.db.accounts
101    }
102
103    /// Returns the map that holds the storage related info
104    pub fn storage(&self) -> &RwLock<AddressHashMap<StorageInfo>> {
105        &self.db.storage
106    }
107
108    /// Returns the map that holds all the block hashes
109    pub fn block_hashes(&self) -> &RwLock<U256Map<B256>> {
110        &self.db.block_hashes
111    }
112
113    /// Returns the Env related metadata
114    pub const fn meta(&self) -> &Arc<RwLock<BlockchainDbMeta<B>>> {
115        &self.meta
116    }
117
118    /// Returns the inner cache
119    pub const fn cache(&self) -> &Arc<JsonBlockCacheDB<B>> {
120        &self.cache
121    }
122
123    /// Returns the underlying storage
124    pub const fn db(&self) -> &Arc<MemDb> {
125        &self.db
126    }
127}
128
129/// Marker trait for block environment types that can be used with the forking backend.
130///
131/// Automatically implemented for any `B: Serialize + DeserializeOwned + Default + Clone +
132/// PartialEq + Send + Sync + 'static`.
133pub 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/// relevant identifying markers in the context of [BlockchainDb]
144#[derive(Clone, Debug, Default, Eq)]
145pub struct BlockchainDbMeta<B> {
146    /// The chain of the blockchain of the block environment
147    pub chain: Option<Chain>,
148    /// The block environment
149    pub block_env: B,
150    /// All the hosts used to connect to
151    pub hosts: BTreeSet<String>,
152}
153
154impl<B> BlockchainDbMeta<B> {
155    /// Creates a new instance
156    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    /// Infers the host from the provided url and adds it to the set of hosts
166    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    /// Sets the [Chain] of this instance
176    pub const fn set_chain(mut self, chain: Chain) -> Self {
177        self.chain = Some(chain);
178        self
179    }
180
181    /// Sets the block environment of this instance
182    pub fn set_block_env(mut self, block_env: B) -> Self {
183        self.block_env = block_env;
184        self
185    }
186}
187
188// ignore hosts to not invalidate the cache when different endpoints are used, as it's commonly the
189// case for http vs ws endpoints
190impl<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
211/// A backwards compatible representation of a block environment type `B`.
212///
213/// This prevents deserialization errors of cache files caused by breaking changes to the
214/// block environment, for example enabling an optional feature that adds new fields.
215/// By filling in missing fields from `B::default()` we can prevent cache file issues.
216struct 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        // we check for any missing fields here
227        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/// In Memory cache containing all fetched accounts and storage slots
274/// and their values from RPC
275#[derive(Debug, Default)]
276pub struct MemDb {
277    /// Account related data
278    pub accounts: RwLock<AddressHashMap<AccountInfo>>,
279    /// Storage related data
280    pub storage: RwLock<AddressHashMap<StorageInfo>>,
281    /// All retrieved block hashes
282    pub block_hashes: RwLock<U256Map<B256>>,
283}
284
285impl MemDb {
286    /// Clears all data stored in this db
287    pub fn clear(&self) {
288        self.accounts.write().clear();
289        self.storage.write().clear();
290        self.block_hashes.write().clear();
291    }
292
293    // Inserts the account, replacing it if it exists already
294    pub fn do_insert_account(&self, address: Address, account: AccountInfo) {
295        self.accounts.write().insert(address, account);
296    }
297
298    /// The implementation of [DatabaseCommit::commit()]
299    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                // insert account
308                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/// A DB that stores the cached content in a json file
357#[derive(Debug)]
358pub struct JsonBlockCacheDB<B> {
359    /// Where this cache file is stored.
360    ///
361    /// If this is a [None] then caching is disabled
362    cache_path: Option<PathBuf>,
363    /// Object that's stored in a json file
364    data: JsonBlockCacheData<B>,
365}
366
367impl<B> JsonBlockCacheDB<B> {
368    /// Creates a new instance.
369    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    /// Returns the [MemDb] it holds access to
374    pub const fn db(&self) -> &Arc<MemDb> {
375        &self.data.data
376    }
377
378    /// Metadata stored alongside the data
379    pub const fn meta(&self) -> &Arc<RwLock<BlockchainDbMeta<B>>> {
380        &self.data.meta
381    }
382
383    /// Returns `true` if this is a transient cache and nothing will be flushed
384    pub const fn is_transient(&self) -> bool {
385        self.cache_path.is_none()
386    }
387
388    /// Returns the cache path.
389    pub fn cache_path(&self) -> Option<&Path> {
390        self.cache_path.as_deref()
391    }
392}
393
394impl<B: ForkBlockEnv> JsonBlockCacheDB<B> {
395    /// Loads the contents of the diskmap file and returns the read object.
396    ///
397    /// When the `zstd` feature is enabled, this will transparently detect and decompress
398    /// zstd-compressed cache files (by checking for the zstd magic bytes `0x28B52FFD`).
399    /// Plain JSON files are always supported regardless of feature flags.
400    ///
401    /// # Errors
402    /// This will fail if
403    ///   - the `path` does not exist
404    ///   - the format does not match [JsonBlockCacheData]
405    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
424/// Decompresses the raw bytes if they are zstd-compressed, otherwise returns them as-is.
425///
426/// When the `zstd` feature is **not** enabled and zstd-compressed data is detected, this
427/// returns an error with a helpful message.
428fn 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    /// Flushes the DB to disk if caching is enabled.
453    #[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    /// Flushes the DB to a specific file.
460    ///
461    /// When the `zstd` feature is enabled, the cache is written as zstd-compressed JSON.
462    /// Otherwise, plain JSON is written.
463    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            // Default zstd compression level (3) offers a good balance of speed and ratio.
480            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/// The Data the [JsonBlockCacheDB] can read and flush
508///
509/// This will be deserialized in a JSON object with the keys:
510/// `["meta", "accounts", "storage", "block_hashes"]`
511#[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/// A type that flushes a `JsonBlockCacheDB` on drop
555///
556/// This type intentionally does not implement `Clone` since it's intended that there's only once
557/// instance that will flush the cache.
558#[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        // set
729        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        // unset
736        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        // zstd magic bytes
746        assert!([0x28, 0xB5, 0x2F, 0xFD, 0x00].starts_with(&ZSTD_FRAME_MAGIC));
747        // plain JSON
748        assert!(!b"{\"meta\":{}}".starts_with(&ZSTD_FRAME_MAGIC));
749        // too short
750        assert!(![0x28, 0xB5].starts_with(&ZSTD_FRAME_MAGIC));
751        // empty
752        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        // Insert some data
764        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        // Reload
782        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        // Verify on-disk bytes have zstd magic
818        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        // Reload and verify data integrity
825        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        // Write plain JSON manually
837        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        // Should load fine even with zstd feature enabled
846        let loaded = JsonBlockCacheDB::<BlockEnv>::load(&path).unwrap();
847        assert_eq!(loaded.db().accounts.read().len(), 0);
848    }
849}