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};
15use std::{
16    collections::BTreeSet,
17    fs,
18    io::{BufWriter, Write},
19    path::{Path, PathBuf},
20    sync::Arc,
21};
22use url::Url;
23
24pub type StorageInfo = StorageKeyMap<U256>;
25
26/// A shareable Block database
27#[derive(Clone, Debug)]
28pub struct BlockchainDb<B = BlockEnv> {
29    /// Contains all the data
30    db: Arc<MemDb>,
31    /// metadata of the current config
32    meta: Arc<RwLock<BlockchainDbMeta<B>>>,
33    /// the cache that can be flushed
34    cache: Arc<JsonBlockCacheDB<B>>,
35}
36
37impl<B: ForkBlockEnv> BlockchainDb<B> {
38    /// Creates a new instance of the [BlockchainDb].
39    ///
40    /// If a `cache_path` is provided it attempts to load a previously stored [JsonBlockCacheData]
41    /// and will try to use the cached entries it holds.
42    ///
43    /// This will return a new and empty [MemDb] if
44    ///   - `cache_path` is `None`
45    ///   - the file the `cache_path` points to, does not exist
46    ///   - the file contains malformed data, or if it couldn't be read
47    ///   - the provided `meta` differs from [BlockchainDbMeta] that's stored on disk
48    pub fn new(meta: BlockchainDbMeta<B>, cache_path: Option<PathBuf>) -> Self {
49        Self::new_db(meta, cache_path, false)
50    }
51
52    /// Creates a new instance of the [BlockchainDb] and skips check when comparing meta
53    /// This is useful for offline-start mode when we don't want to fetch metadata of `block`.
54    ///
55    /// if a `cache_path` is provided it attempts to load a previously stored [JsonBlockCacheData]
56    /// and will try to use the cached entries it holds.
57    ///
58    /// This will return a new and empty [MemDb] if
59    ///   - `cache_path` is `None`
60    ///   - the file the `cache_path` points to, does not exist
61    ///   - the file contains malformed data, or if it couldn't be read
62    ///   - the provided `meta` differs from [BlockchainDbMeta] that's stored on disk
63    pub fn new_skip_check(meta: BlockchainDbMeta<B>, cache_path: Option<PathBuf>) -> Self {
64        Self::new_db(meta, cache_path, true)
65    }
66
67    fn new_db(meta: BlockchainDbMeta<B>, cache_path: Option<PathBuf>, skip_check: bool) -> Self {
68        trace!(target: "forge::cache", cache=?cache_path, "initialising blockchain db");
69        // read cache and check if metadata matches
70        let cache = cache_path
71            .as_ref()
72            .and_then(|p| {
73                JsonBlockCacheDB::load(p).ok().filter(|cache| {
74                    if skip_check {
75                        return true;
76                    }
77                    let mut existing = cache.meta().write();
78                    existing.hosts.extend(meta.hosts.clone());
79                    if meta != *existing {
80                        warn!(target: "cache", "non-matching block metadata");
81                        false
82                    } else {
83                        true
84                    }
85                })
86            })
87            .unwrap_or_else(|| JsonBlockCacheDB::new(Arc::new(RwLock::new(meta)), cache_path));
88
89        Self { db: Arc::clone(cache.db()), meta: Arc::clone(cache.meta()), cache: Arc::new(cache) }
90    }
91
92    /// Returns the map that holds the account related info
93    pub fn accounts(&self) -> &RwLock<AddressHashMap<AccountInfo>> {
94        &self.db.accounts
95    }
96
97    /// Returns the map that holds the storage related info
98    pub fn storage(&self) -> &RwLock<AddressHashMap<StorageInfo>> {
99        &self.db.storage
100    }
101
102    /// Returns the map that holds all the block hashes
103    pub fn block_hashes(&self) -> &RwLock<U256Map<B256>> {
104        &self.db.block_hashes
105    }
106
107    /// Returns the Env related metadata
108    pub const fn meta(&self) -> &Arc<RwLock<BlockchainDbMeta<B>>> {
109        &self.meta
110    }
111
112    /// Returns the inner cache
113    pub const fn cache(&self) -> &Arc<JsonBlockCacheDB<B>> {
114        &self.cache
115    }
116
117    /// Returns the underlying storage
118    pub const fn db(&self) -> &Arc<MemDb> {
119        &self.db
120    }
121}
122
123/// Marker trait for block environment types that can be used with the forking backend.
124///
125/// Automatically implemented for any `B: Serialize + DeserializeOwned + Default + Clone +
126/// PartialEq + Send + Sync + 'static`.
127pub trait ForkBlockEnv:
128    Serialize + DeserializeOwned + Default + Clone + PartialEq + Send + Sync + 'static
129{
130}
131
132impl<B: Serialize + DeserializeOwned + Default + Clone + PartialEq + Send + Sync + 'static>
133    ForkBlockEnv for B
134{
135}
136
137/// relevant identifying markers in the context of [BlockchainDb]
138#[derive(Clone, Debug, Default, Eq)]
139pub struct BlockchainDbMeta<B> {
140    /// The chain of the blockchain of the block environment
141    pub chain: Option<Chain>,
142    /// The block environment
143    pub block_env: B,
144    /// All the hosts used to connect to
145    pub hosts: BTreeSet<String>,
146}
147
148impl<B> BlockchainDbMeta<B> {
149    /// Creates a new instance
150    pub fn new(block_env: B, url: String) -> Self {
151        let host = Url::parse(&url)
152            .ok()
153            .and_then(|url| url.host().map(|host| host.to_string()))
154            .unwrap_or(url);
155
156        Self { chain: None, block_env, hosts: BTreeSet::from([host]) }
157    }
158
159    /// Infers the host from the provided url and adds it to the set of hosts
160    pub fn with_url(mut self, url: &str) -> Self {
161        let host = Url::parse(url)
162            .ok()
163            .and_then(|url| url.host().map(|host| host.to_string()))
164            .unwrap_or(url.to_string());
165        self.hosts.insert(host);
166        self
167    }
168
169    /// Sets the [Chain] of this instance
170    pub const fn set_chain(mut self, chain: Chain) -> Self {
171        self.chain = Some(chain);
172        self
173    }
174
175    /// Sets the block environment of this instance
176    pub fn set_block_env(mut self, block_env: B) -> Self {
177        self.block_env = block_env;
178        self
179    }
180}
181
182// ignore hosts to not invalidate the cache when different endpoints are used, as it's commonly the
183// case for http vs ws endpoints
184impl<B: PartialEq> PartialEq for BlockchainDbMeta<B> {
185    fn eq(&self, other: &Self) -> bool {
186        self.block_env == other.block_env
187    }
188}
189
190impl<B: Serialize> Serialize for BlockchainDbMeta<B> {
191    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
192        use serde::ser::SerializeStruct;
193
194        let field_count = if self.chain.is_some() { 3 } else { 2 };
195        let mut s = serializer.serialize_struct("BlockchainDbMeta", field_count)?;
196        if let Some(chain) = &self.chain {
197            s.serialize_field("chain", chain)?;
198        }
199        s.serialize_field("block_env", &self.block_env)?;
200        s.serialize_field("hosts", &self.hosts)?;
201        s.end()
202    }
203}
204
205/// A backwards compatible representation of a block environment type `B`.
206///
207/// This prevents deserialization errors of cache files caused by breaking changes to the
208/// block environment, for example enabling an optional feature that adds new fields.
209/// By filling in missing fields from `B::default()` we can prevent cache file issues.
210struct BlockEnvBackwardsCompat<B> {
211    inner: B,
212}
213
214impl<'de, B: DeserializeOwned + Default + Serialize> Deserialize<'de>
215    for BlockEnvBackwardsCompat<B>
216{
217    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
218        let mut value = serde_json::Value::deserialize(deserializer)?;
219
220        // we check for any missing fields here
221        if let Some(obj) = value.as_object_mut() {
222            let default_value = serde_json::to_value(B::default()).unwrap();
223            for (key, value) in default_value.as_object().unwrap() {
224                if !obj.contains_key(key) {
225                    obj.insert(key.to_string(), value.clone());
226                }
227            }
228        }
229
230        let inner: B = serde_json::from_value(value).map_err(serde::de::Error::custom)?;
231        Ok(Self { inner })
232    }
233}
234
235#[derive(Deserialize)]
236#[serde(untagged)]
237enum Hosts {
238    Multi(BTreeSet<String>),
239    Single(String),
240}
241
242impl From<Hosts> for BTreeSet<String> {
243    fn from(hosts: Hosts) -> Self {
244        match hosts {
245            Hosts::Multi(hosts) => hosts,
246            Hosts::Single(host) => Self::from([host]),
247        }
248    }
249}
250
251impl<'de, B: DeserializeOwned + Default + Serialize> Deserialize<'de> for BlockchainDbMeta<B> {
252    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
253        #[derive(Deserialize)]
254        #[serde(bound = "B: DeserializeOwned + Default + Serialize")]
255        struct Meta<B> {
256            chain: Option<Chain>,
257            block_env: BlockEnvBackwardsCompat<B>,
258            #[serde(alias = "host")]
259            hosts: Hosts,
260        }
261
262        let Meta { chain, block_env, hosts } = Meta::deserialize(deserializer)?;
263        Ok(Self { chain, block_env: block_env.inner, hosts: hosts.into() })
264    }
265}
266
267/// In Memory cache containing all fetched accounts and storage slots
268/// and their values from RPC
269#[derive(Debug, Default)]
270pub struct MemDb {
271    /// Account related data
272    pub accounts: RwLock<AddressHashMap<AccountInfo>>,
273    /// Storage related data
274    pub storage: RwLock<AddressHashMap<StorageInfo>>,
275    /// All retrieved block hashes
276    pub block_hashes: RwLock<U256Map<B256>>,
277}
278
279impl MemDb {
280    /// Clears all data stored in this db
281    pub fn clear(&self) {
282        self.accounts.write().clear();
283        self.storage.write().clear();
284        self.block_hashes.write().clear();
285    }
286
287    // Inserts the account, replacing it if it exists already
288    pub fn do_insert_account(&self, address: Address, account: AccountInfo) {
289        self.accounts.write().insert(address, account);
290    }
291
292    /// The implementation of [DatabaseCommit::commit()]
293    pub fn do_commit(&self, changes: AddressHashMap<Account>) {
294        let mut storage = self.storage.write();
295        let mut accounts = self.accounts.write();
296        for (add, mut acc) in changes {
297            if acc.is_empty() || acc.is_selfdestructed() {
298                accounts.remove(&add);
299                storage.remove(&add);
300            } else {
301                // insert account
302                if let Some(code_hash) = acc
303                    .info
304                    .code
305                    .as_ref()
306                    .filter(|code| !code.is_empty())
307                    .map(|code| code.hash_slow())
308                {
309                    acc.info.code_hash = code_hash;
310                } else if acc.info.code_hash.is_zero() {
311                    acc.info.code_hash = KECCAK_EMPTY;
312                }
313                accounts.insert(add, acc.info);
314
315                let acc_storage = storage.entry(add).or_default();
316                if acc.status.contains(AccountStatus::Created) {
317                    acc_storage.clear();
318                }
319                for (index, value) in acc.storage {
320                    if value.present_value().is_zero() {
321                        acc_storage.remove(&index);
322                    } else {
323                        acc_storage.insert(index, value.present_value());
324                    }
325                }
326                if acc_storage.is_empty() {
327                    storage.remove(&add);
328                }
329            }
330        }
331    }
332}
333
334impl Clone for MemDb {
335    fn clone(&self) -> Self {
336        Self {
337            storage: RwLock::new(self.storage.read().clone()),
338            accounts: RwLock::new(self.accounts.read().clone()),
339            block_hashes: RwLock::new(self.block_hashes.read().clone()),
340        }
341    }
342}
343
344impl DatabaseCommit for MemDb {
345    fn commit(&mut self, changes: AddressHashMap<Account>) {
346        self.do_commit(changes)
347    }
348}
349
350/// A DB that stores the cached content in a json file
351#[derive(Debug)]
352pub struct JsonBlockCacheDB<B> {
353    /// Where this cache file is stored.
354    ///
355    /// If this is a [None] then caching is disabled
356    cache_path: Option<PathBuf>,
357    /// Object that's stored in a json file
358    data: JsonBlockCacheData<B>,
359}
360
361impl<B> JsonBlockCacheDB<B> {
362    /// Creates a new instance.
363    fn new(meta: Arc<RwLock<BlockchainDbMeta<B>>>, cache_path: Option<PathBuf>) -> Self {
364        Self { cache_path, data: JsonBlockCacheData { meta, data: Arc::new(Default::default()) } }
365    }
366
367    /// Returns the [MemDb] it holds access to
368    pub const fn db(&self) -> &Arc<MemDb> {
369        &self.data.data
370    }
371
372    /// Metadata stored alongside the data
373    pub const fn meta(&self) -> &Arc<RwLock<BlockchainDbMeta<B>>> {
374        &self.data.meta
375    }
376
377    /// Returns `true` if this is a transient cache and nothing will be flushed
378    pub const fn is_transient(&self) -> bool {
379        self.cache_path.is_none()
380    }
381
382    /// Returns the cache path.
383    pub fn cache_path(&self) -> Option<&Path> {
384        self.cache_path.as_deref()
385    }
386}
387
388impl<B: ForkBlockEnv> JsonBlockCacheDB<B> {
389    /// Loads the contents of the diskmap file and returns the read object
390    ///
391    /// # Errors
392    /// This will fail if
393    ///   - the `path` does not exist
394    ///   - the format does not match [JsonBlockCacheData]
395    pub fn load(path: impl Into<PathBuf>) -> eyre::Result<Self> {
396        let path = path.into();
397        trace!(target: "cache", ?path, "reading json cache");
398        let contents = std::fs::read_to_string(&path).inspect_err(|err| {
399            warn!(?err, ?path, "Failed to read cache file");
400        })?;
401        let data = serde_json::from_str(&contents).inspect_err(|err| {
402            warn!(target: "cache", ?err, ?path, "Failed to deserialize cache data");
403        })?;
404        trace!(target: "cache", ?path, "read json cache");
405        Ok(Self { cache_path: Some(path), data })
406    }
407}
408
409impl<B: Serialize + Clone> JsonBlockCacheDB<B> {
410    /// Flushes the DB to disk if caching is enabled.
411    #[instrument(level = "warn", skip_all, fields(path = ?self.cache_path))]
412    pub fn flush(&self) {
413        let Some(path) = &self.cache_path else { return };
414        self.flush_to(path.as_path());
415    }
416
417    /// Flushes the DB to a specific file
418    pub fn flush_to(&self, cache_path: &Path) {
419        let path: &Path = cache_path;
420
421        trace!(target: "cache", "saving json cache");
422
423        if let Some(parent) = path.parent() {
424            let _ = fs::create_dir_all(parent);
425        }
426
427        let file = match fs::File::create(path) {
428            Ok(file) => file,
429            Err(e) => return warn!(target: "cache", %e, "Failed to open json cache for writing"),
430        };
431
432        let mut writer = BufWriter::new(file);
433        if let Err(e) = serde_json::to_writer(&mut writer, &self.data) {
434            return warn!(target: "cache", %e, "Failed to write to json cache");
435        }
436        if let Err(e) = writer.flush() {
437            return warn!(target: "cache", %e, "Failed to flush to json cache");
438        }
439
440        trace!(target: "cache", "saved json cache");
441    }
442}
443
444/// The Data the [JsonBlockCacheDB] can read and flush
445///
446/// This will be deserialized in a JSON object with the keys:
447/// `["meta", "accounts", "storage", "block_hashes"]`
448#[derive(Debug)]
449pub struct JsonBlockCacheData<B> {
450    pub meta: Arc<RwLock<BlockchainDbMeta<B>>>,
451    pub data: Arc<MemDb>,
452}
453
454impl<B: Serialize + Clone> Serialize for JsonBlockCacheData<B> {
455    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
456        let mut map = serializer.serialize_map(Some(4))?;
457
458        map.serialize_entry("meta", &self.meta.read().clone())?;
459        map.serialize_entry("accounts", &self.data.accounts.read().clone())?;
460        map.serialize_entry("storage", &self.data.storage.read().clone())?;
461        map.serialize_entry("block_hashes", &self.data.block_hashes.read().clone())?;
462
463        map.end()
464    }
465}
466
467impl<'de, B: DeserializeOwned + Default + Serialize> Deserialize<'de> for JsonBlockCacheData<B> {
468    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
469        #[derive(Deserialize)]
470        struct Data<B> {
471            meta: B,
472            accounts: AddressHashMap<AccountInfo>,
473            storage: AddressHashMap<StorageInfo>,
474            block_hashes: U256Map<B256>,
475        }
476
477        let Data { meta, accounts, storage, block_hashes } =
478            Data::<BlockchainDbMeta<B>>::deserialize(deserializer)?;
479
480        Ok(Self {
481            meta: Arc::new(RwLock::new(meta)),
482            data: Arc::new(MemDb {
483                accounts: RwLock::new(accounts),
484                storage: RwLock::new(storage),
485                block_hashes: RwLock::new(block_hashes),
486            }),
487        })
488    }
489}
490
491/// A type that flushes a `JsonBlockCacheDB` on drop
492///
493/// This type intentionally does not implement `Clone` since it's intended that there's only once
494/// instance that will flush the cache.
495#[derive(Debug)]
496pub struct FlushJsonBlockCacheDB<B: Serialize + Clone>(pub Arc<JsonBlockCacheDB<B>>);
497
498impl<B: Serialize + Clone> Drop for FlushJsonBlockCacheDB<B> {
499    fn drop(&mut self) {
500        trace!(target: "fork::cache", "flushing cache");
501        self.0.flush();
502        trace!(target: "fork::cache", "flushed cache");
503    }
504}
505
506#[cfg(test)]
507mod tests {
508    use super::*;
509
510    #[test]
511    fn can_deserialize_cache() {
512        let s = r#"{
513    "meta": {
514        "cfg_env": {
515            "chain_id": 1337,
516            "perf_analyse_created_bytecodes": "Analyse",
517            "limit_contract_code_size": 18446744073709551615,
518            "memory_limit": 4294967295,
519            "disable_block_gas_limit": false,
520            "disable_eip3607": false,
521            "disable_base_fee": false
522        },
523        "block_env": {
524            "number": 15547871,
525            "coinbase": "0x0000000000000000000000000000000000000000",
526            "timestamp": 1663351871,
527            "difficulty": "0x0",
528            "basefee": 12448539171,
529            "gas_limit": 30000000,
530            "prevrandao": "0x0000000000000000000000000000000000000000000000000000000000000000"
531        },
532        "hosts": [
533            "eth-mainnet.alchemyapi.io"
534        ]
535    },
536    "accounts": {
537        "0xb8ffc3cd6e7cf5a098a1c92f48009765b24088dc": {
538            "balance": "0x0",
539            "nonce": 10,
540            "code_hash": "0x3ac64c95eedf82e5d821696a12daac0e1b22c8ee18a9fd688b00cfaf14550aad",
541            "code": {
542                "LegacyAnalyzed": {
543                    "bytecode": "0x00",
544                    "original_len": 0,
545                    "jump_table": {
546                      "order": "bitvec::order::Lsb0",
547                      "head": {
548                        "width": 8,
549                        "index": 0
550                      },
551                      "bits": 1,
552                      "data": [0]
553                    }
554                }
555            }
556        }
557    },
558    "storage": {
559        "0xa354f35829ae975e850e23e9615b11da1b3dc4de": {
560            "0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564": "0x5553444320795661756c74000000000000000000000000000000000000000000",
561            "0x10": "0x37fd60ff8346",
562            "0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563": "0xb",
563            "0x6": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
564            "0x5": "0x36ff5b93162e",
565            "0x14": "0x29d635a8e000",
566            "0x11": "0x63224c73",
567            "0x2": "0x6"
568        }
569    },
570    "block_hashes": {
571        "0xed3deb": "0xbf7be3174b261ea3c377b6aba4a1e05d5fae7eee7aab5691087c20cf353e9877",
572        "0xed3de9": "0xba1c3648e0aee193e7d00dffe4e9a5e420016b4880455641085a4731c1d32eef",
573        "0xed3de8": "0x61d1491c03a9295fb13395cca18b17b4fa5c64c6b8e56ee9cc0a70c3f6cf9855",
574        "0xed3de7": "0xb54560b5baeccd18350d56a3bee4035432294dc9d2b7e02f157813e1dee3a0be",
575        "0xed3dea": "0x816f124480b9661e1631c6ec9ee39350bda79f0cbfc911f925838d88e3d02e4b"
576    }
577}"#;
578
579        let cache: JsonBlockCacheData<BlockEnv> = serde_json::from_str(s).unwrap();
580        assert_eq!(cache.data.accounts.read().len(), 1);
581        assert_eq!(cache.data.storage.read().len(), 1);
582        assert_eq!(cache.data.block_hashes.read().len(), 5);
583
584        let _s = serde_json::to_string(&cache).unwrap();
585    }
586
587    #[test]
588    fn can_deserialize_cache_post_4844() {
589        let s = r#"{
590    "meta": {
591        "cfg_env": {
592            "chain_id": 1,
593            "kzg_settings": "Default",
594            "perf_analyse_created_bytecodes": "Analyse",
595            "limit_contract_code_size": 18446744073709551615,
596            "memory_limit": 134217728,
597            "disable_block_gas_limit": false,
598            "disable_eip3607": true,
599            "disable_base_fee": false,
600            "optimism": false
601        },
602        "block_env": {
603            "number": 18651580,
604            "coinbase": "0x4838b106fce9647bdf1e7877bf73ce8b0bad5f97",
605            "timestamp": 1700950019,
606            "gas_limit": 30000000,
607            "basefee": 26886078239,
608            "difficulty": "0xc6b1a299886016dea3865689f8393b9bf4d8f4fe8c0ad25f0058b3569297c057",
609            "prevrandao": "0xc6b1a299886016dea3865689f8393b9bf4d8f4fe8c0ad25f0058b3569297c057",
610            "blob_excess_gas_and_price": {
611                "excess_blob_gas": 0,
612                "blob_gasprice": 1
613            }
614        },
615        "hosts": [
616            "eth-mainnet.alchemyapi.io"
617        ]
618    },
619    "accounts": {
620        "0x4838b106fce9647bdf1e7877bf73ce8b0bad5f97": {
621            "balance": "0x8e0c373cfcdfd0eb",
622            "nonce": 128912,
623            "code_hash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470",
624            "code": {
625                "LegacyAnalyzed": {
626                    "bytecode": "0x00",
627                    "original_len": 0,
628                    "jump_table": {
629                      "order": "bitvec::order::Lsb0",
630                      "head": {
631                        "width": 8,
632                        "index": 0
633                      },
634                      "bits": 1,
635                      "data": [0]
636                    }
637                }
638            }
639        }
640    },
641    "storage": {},
642    "block_hashes": {}
643}"#;
644
645        let cache: JsonBlockCacheData<BlockEnv> = serde_json::from_str(s).unwrap();
646        assert_eq!(cache.data.accounts.read().len(), 1);
647
648        let _s = serde_json::to_string(&cache).unwrap();
649    }
650
651    #[test]
652    fn roundtrip_meta_block_env() {
653        let meta = BlockchainDbMeta {
654            chain: Some(Chain::mainnet()),
655            block_env: BlockEnv { number: U256::from(1u64), ..Default::default() },
656            hosts: BTreeSet::from(["eth-mainnet.alchemyapi.io".to_string()]),
657        };
658        let json = serde_json::to_string(&meta).unwrap();
659        let recovered: BlockchainDbMeta<BlockEnv> = serde_json::from_str(&json).unwrap();
660        assert_eq!(meta, recovered);
661    }
662
663    #[test]
664    fn can_return_cache_path_if_set() {
665        // set
666        let cache_db = JsonBlockCacheDB::<BlockEnv>::new(
667            Arc::new(RwLock::new(BlockchainDbMeta::default())),
668            Some(PathBuf::from("/tmp/foo")),
669        );
670        assert_eq!(Some(Path::new("/tmp/foo")), cache_db.cache_path());
671
672        // unset
673        let cache_db = JsonBlockCacheDB::<BlockEnv>::new(
674            Arc::new(RwLock::new(BlockchainDbMeta::default())),
675            None,
676        );
677        assert_eq!(None, cache_db.cache_path());
678    }
679}