edb_common/
cache.rs

1// EDB - Ethereum Debugger
2// Copyright (C) 2024 Zhuo Zhang and Wuqi Zhang
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program. If not, see <https://www.gnu.org/licenses/>.
16
17//! Cache utilities.
18
19use std::{fs, marker::PhantomData, path::PathBuf, time::Duration};
20
21use alloy_chains::Chain;
22use eyre::Result;
23use serde::{de::DeserializeOwned, Deserialize, Serialize};
24use tracing::{trace, warn};
25
26/// Default cache TTL for etherscan.
27/// Set to 1 day since the source code of a contract is unlikely to change frequently.
28pub const DEFAULT_ETHERSCAN_CACHE_TTL: u64 = 86400;
29
30/// Trait for cache paths.
31pub trait CachePath {
32    /// Returns the path to edb's cache dir: `~/.edb/cache` by default.
33    fn edb_cache_dir(&self) -> Option<PathBuf>;
34
35    /// Check whether the cache is valid.
36    fn is_valid(&self) -> bool {
37        self.edb_cache_dir().is_some()
38    }
39
40    /// Returns the path to edb rpc cache dir: `<cache_root>/rpc`.
41    fn edb_rpc_cache_dir(&self) -> Option<PathBuf> {
42        Some(self.edb_cache_dir()?.join("rpc"))
43    }
44    /// Returns the path to edb chain's cache dir: `<cache_root>/rpc/<chain>`
45    fn rpc_chain_cache_dir(&self, chain_id: impl Into<Chain>) -> Option<PathBuf> {
46        Some(self.edb_rpc_cache_dir()?.join(chain_id.into().to_string()))
47    }
48
49    /// Returns the path to edb's etherscan cache dir: `<cache_root>/etherscan`.
50    fn etherscan_cache_dir(&self) -> Option<PathBuf> {
51        Some(self.edb_cache_dir()?.join("etherscan"))
52    }
53
54    /// Returns the path to edb's etherscan cache dir for `chain_id`:
55    /// `<cache_root>/etherscan/<chain>`
56    fn etherscan_chain_cache_dir(&self, chain_id: impl Into<Chain>) -> Option<PathBuf> {
57        Some(self.etherscan_cache_dir()?.join(chain_id.into().to_string()))
58    }
59
60    /// Returns the path to edb's compiler cache dir: `<cache_root>/solc`.
61    fn compiler_cache_dir(&self) -> Option<PathBuf> {
62        Some(self.edb_cache_dir()?.join("solc"))
63    }
64
65    /// Returns the path to edb's compiler cache dir for `chain_id`:
66    /// `<cache_root>/solc/<chain>`
67    fn compiler_chain_cache_dir(&self, chain_id: impl Into<Chain>) -> Option<PathBuf> {
68        Some(self.compiler_cache_dir()?.join(chain_id.into().to_string()))
69    }
70}
71
72/// Cache path for edb.
73#[derive(Debug, Default)]
74pub struct EdbCachePath {
75    root: Option<PathBuf>,
76}
77
78impl EdbCachePath {
79    /// New cache path.
80    pub fn new(root: Option<impl Into<PathBuf>>) -> Self {
81        Self {
82            root: root
83                .map(Into::into)
84                .or_else(|| dirs_next::home_dir().map(|p| p.join(".edb").join("cache"))),
85        }
86    }
87}
88
89impl CachePath for EdbCachePath {
90    fn edb_cache_dir(&self) -> Option<PathBuf> {
91        self.root.clone()
92    }
93}
94
95impl CachePath for Option<EdbCachePath> {
96    fn edb_cache_dir(&self) -> Option<PathBuf> {
97        self.as_ref()?.edb_cache_dir()
98    }
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
102struct CacheWrapper<T> {
103    pub data: T,
104    pub expires_at: u64,
105}
106
107impl<T> CacheWrapper<T> {
108    pub fn new(data: T, ttl: Option<Duration>) -> Self {
109        Self {
110            data,
111            expires_at: ttl
112                .map(|ttl| ttl.as_secs().saturating_add(chrono::Utc::now().timestamp() as u64))
113                .unwrap_or(u64::MAX),
114        }
115    }
116
117    pub fn is_expired(&self) -> bool {
118        self.expires_at < chrono::Utc::now().timestamp() as u64
119    }
120}
121
122/// Trait for cache.
123pub trait Cache {
124    /// The type of the data to be cached.
125    type Data: Serialize + DeserializeOwned;
126
127    /// Loads the cache for the given label.
128    fn load_cache(&self, label: impl Into<String>) -> Option<Self::Data>;
129
130    /// Saves the cache for the given label.
131    fn save_cache(&self, label: impl Into<String>, data: &Self::Data) -> Result<()>;
132}
133
134/// A cache manager that stores data in the file system.
135///  - `T` is the type of the data to be cached.
136///  - `cache_dir` is the directory where the cache files are stored.
137///  - `cache_ttl` is the time-to-live of the cache files. If it is `None`, the cache files will
138///    never expire.
139#[derive(Debug, Clone)]
140pub struct EdbCache<T> {
141    cache_dir: PathBuf,
142    cache_ttl: Option<Duration>,
143    phantom: PhantomData<T>,
144}
145
146impl<T> EdbCache<T>
147where
148    T: Serialize + DeserializeOwned,
149{
150    /// New cache.
151    pub fn new(
152        cache_dir: Option<impl Into<PathBuf>>,
153        cache_ttl: Option<Duration>,
154    ) -> Result<Option<Self>> {
155        if let Some(cache_dir) = cache_dir {
156            let cache_dir = cache_dir.into();
157            fs::create_dir_all(&cache_dir)?;
158            Ok(Some(Self { cache_dir, cache_ttl, phantom: PhantomData }))
159        } else {
160            Ok(None)
161        }
162    }
163
164    /// Returns the cache directory.
165    pub fn cache_dir(&self) -> &PathBuf {
166        &self.cache_dir
167    }
168
169    /// Returns the cache TTL.
170    pub fn cache_ttl(&self) -> Option<Duration> {
171        self.cache_ttl
172    }
173}
174
175impl<T> Cache for EdbCache<T>
176where
177    T: Serialize + DeserializeOwned,
178{
179    type Data = T;
180
181    fn load_cache(&self, label: impl Into<String>) -> Option<T> {
182        let cache_file = self.cache_dir.join(format!("{}.json", label.into()));
183        trace!("loading cache: {:?}", cache_file);
184        if !cache_file.exists() {
185            return None;
186        }
187
188        let content = fs::read_to_string(&cache_file).ok()?;
189        let cache: CacheWrapper<_> = if let Ok(cache) = serde_json::from_str(&content) {
190            cache
191        } else {
192            warn!("the cache file has been corrupted: {:?}", cache_file);
193            let _ = fs::remove_file(&cache_file); // we do not care about the result
194            return None;
195        };
196
197        if cache.is_expired() {
198            trace!("the cache file has expired: {:?}", cache_file);
199            let _ = fs::remove_file(&cache_file); // we do not care about the result
200            None
201        } else {
202            trace!("hit the cache: {:?}", cache_file);
203            Some(cache.data)
204        }
205    }
206
207    fn save_cache(&self, label: impl Into<String>, data: &T) -> Result<()> {
208        let cache_file = self.cache_dir.join(format!("{}.json", label.into()));
209        trace!("saving cache: {:?}", cache_file);
210
211        let cache = CacheWrapper::new(data, self.cache_ttl);
212        let content = serde_json::to_string(&cache)?;
213        fs::write(&cache_file, content)?;
214        Ok(())
215    }
216}
217
218impl<T> Cache for Option<EdbCache<T>>
219where
220    T: Serialize + DeserializeOwned,
221{
222    type Data = T;
223
224    fn load_cache(&self, label: impl Into<String>) -> Option<T> {
225        self.as_ref()?.load_cache(label)
226    }
227
228    fn save_cache(&self, label: impl Into<String>, data: &T) -> Result<()> {
229        if let Some(cache) = self {
230            cache.save_cache(label, data)
231        } else {
232            Ok(())
233        }
234    }
235}