anvil_zksync_traces/identifier/
signatures.rs

1////////////////////////////////////////////////////////////////////////////////////////////////////////////
2// Attribution: File adapted from the `evm` crate for zksync usage                                        //
3//                                                                                                        //
4// Full credit goes to its authors. See the original implementation here:                                 //
5// https://github.com/foundry-rs/foundry/blob/master/crates/evm/traces/src/idenitfier/signatures.rs.      //
6//                                                                                                        //
7// Note: These methods are used under the terms of the original project's license.                        //
8////////////////////////////////////////////////////////////////////////////////////////////////////////////
9
10use crate::abi_utils::{get_error, get_event, get_func};
11use alloy::json_abi::{Error, Event, Function};
12use alloy::primitives::hex;
13use anvil_zksync_common::{
14    resolver::{SelectorType, SignEthClient},
15    utils::io::read_json_file,
16    utils::io::write_json_file,
17};
18use once_cell::sync::Lazy;
19use serde::{Deserialize, Serialize};
20use std::time::Instant;
21use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
22use tokio::sync::RwLock;
23
24/// Global `SignaturesIdentifier`` instance
25static GLOBAL_CLIENT: Lazy<SignaturesIdentifier> = Lazy::new(SignaturesIdentifier::default);
26
27#[derive(Debug, Default, Serialize, Deserialize)]
28pub struct CachedSignatures {
29    pub errors: BTreeMap<String, Option<String>>,
30    pub events: BTreeMap<String, Option<String>>,
31    pub functions: BTreeMap<String, Option<String>>,
32}
33
34impl CachedSignatures {
35    pub fn load(cache_path: PathBuf) -> Self {
36        let path = cache_path.join("signatures");
37        if path.is_file() {
38            read_json_file(&path)
39                .map_err(
40                    |err| tracing::warn!(target: "trace::signatures", ?path, ?err, "failed to read cache file"),
41                )
42                .unwrap_or_default()
43        } else {
44            if let Err(err) = std::fs::create_dir_all(cache_path) {
45                tracing::warn!(target: "trace::signatures", "could not create signatures cache dir: {:?}", err);
46            }
47            Self::default()
48        }
49    }
50
51    pub fn save(&self, cache_path: &PathBuf) {
52        if self.is_empty() {
53            // Avoid writing file if there are no signatures.
54            return;
55        }
56        if let Some(parent) = cache_path.parent() {
57            if let Err(err) = std::fs::create_dir_all(parent) {
58                tracing::warn!(target: "trace::signatures", ?parent, ?err, "failed to create cache");
59            }
60        }
61        if let Err(err) = write_json_file(cache_path, &self) {
62            tracing::warn!(target: "trace::signatures", ?cache_path, ?err, "failed to flush signature cache");
63        } else {
64            tracing::trace!(target: "trace::signatures", ?cache_path, "flushed signature cache")
65        }
66    }
67
68    pub fn is_empty(&self) -> bool {
69        self.errors.is_empty() && self.events.is_empty() && self.functions.is_empty()
70    }
71}
72
73/// An identifier that tries to identify functions and events using signatures found at
74/// `https://openchain.xyz` or a local cache.
75#[derive(Debug, Default, Clone)]
76pub struct SignaturesIdentifier {
77    inner: Arc<RwLock<SignaturesIdentifierInner>>,
78}
79
80#[derive(Debug, Default)]
81struct SignaturesIdentifierInner {
82    /// Cached selectors for functions, events and custom errors.
83    cached: CachedSignatures,
84    /// Location where to save `CachedSignatures`.
85    cached_path: Option<PathBuf>,
86    /// The OpenChain client to fetch signatures from.
87    client: Option<SignEthClient>,
88}
89
90impl SignaturesIdentifierInner {
91    fn new(cache_path: Option<PathBuf>, offline: bool) -> eyre::Result<Self> {
92        let client = if !offline {
93            Some(SignEthClient::new())
94        } else {
95            None
96        };
97
98        let self_ = if let Some(cache_path) = cache_path {
99            let path = cache_path.join("signatures");
100            tracing::trace!(target: "trace::signatures", ?path, "reading signature cache");
101            let cached = CachedSignatures::load(cache_path);
102            SignaturesIdentifierInner {
103                cached,
104                cached_path: Some(path),
105                client,
106            }
107        } else {
108            SignaturesIdentifierInner {
109                cached: Default::default(),
110                cached_path: None,
111                client,
112            }
113        };
114        Ok(self_)
115    }
116
117    fn save(&self) {
118        if let Some(cached_path) = &self.cached_path {
119            self.cached.save(cached_path);
120        }
121    }
122
123    async fn identify<T>(
124        &mut self,
125        selector_type: SelectorType,
126        identifiers: impl IntoIterator<Item = impl AsRef<[u8]>>,
127        get_type: impl Fn(&str) -> eyre::Result<T>,
128    ) -> Vec<Option<T>> {
129        let cache = match selector_type {
130            SelectorType::Function => &mut self.cached.functions,
131            SelectorType::Event => &mut self.cached.events,
132            SelectorType::Error => &mut self.cached.errors,
133        };
134
135        let hex_identifiers: Vec<String> =
136            identifiers.into_iter().map(hex::encode_prefixed).collect();
137
138        if let Some(client) = &self.client {
139            let query: Vec<_> = hex_identifiers
140                .iter()
141                .filter(|v| !cache.contains_key(v.as_str()))
142                .collect();
143
144            if !query.is_empty() {
145                let start = Instant::now();
146                let n_queries = query.len();
147                // Fetching from remote sources can easily be the slowest part of execution, so we want to track
148                // each call to the client.
149                let res = client.decode_selectors(selector_type, query.clone()).await;
150                if let Ok(res) = res {
151                    for (hex_id, selector_result) in query.into_iter().zip(res.into_iter()) {
152                        let mut found = false;
153                        if let Some(decoded_results) = selector_result {
154                            if let Some(decoded_result) = decoded_results.into_iter().next() {
155                                cache.insert(hex_id.clone(), Some(decoded_result));
156                                found = true;
157                            }
158                        }
159                        if !found {
160                            cache.insert(hex_id.clone(), None);
161                        }
162                    }
163                }
164                tracing::debug!(
165                    "Queried {} signatures from remote source in {:?}",
166                    n_queries,
167                    start.elapsed()
168                );
169            }
170        }
171
172        hex_identifiers
173            .iter()
174            .map(|v| {
175                if let Some(name) = cache.get(v) {
176                    name.as_ref().and_then(|s| get_type(s).ok())
177                } else {
178                    None
179                }
180            })
181            .collect()
182    }
183}
184
185impl SignaturesIdentifier {
186    pub fn new(cache_path: Option<PathBuf>, offline: bool) -> eyre::Result<Self> {
187        let inner = SignaturesIdentifierInner::new(cache_path, offline)?;
188        Ok(Self {
189            inner: Arc::new(RwLock::new(inner)),
190        })
191    }
192
193    pub async fn save(&self) {
194        self.inner.read().await.save();
195    }
196
197    pub async fn install(cache_path: Option<PathBuf>, offline: bool) -> eyre::Result<()> {
198        *GLOBAL_CLIENT.inner.write().await = SignaturesIdentifierInner::new(cache_path, offline)?;
199
200        Ok(())
201    }
202
203    pub fn global() -> Self {
204        GLOBAL_CLIENT.clone()
205    }
206
207    /// Identifies `Function`s from its cache or `https://api.openchain.xyz`
208    pub async fn identify_functions(
209        &self,
210        identifiers: impl IntoIterator<Item = impl AsRef<[u8]>>,
211    ) -> Vec<Option<Function>> {
212        self.inner
213            .write()
214            .await
215            .identify(SelectorType::Function, identifiers, get_func)
216            .await
217    }
218
219    /// Identifies `Function` from its cache or `https://api.openchain.xyz`
220    pub async fn identify_function(&self, identifier: &[u8]) -> Option<Function> {
221        self.identify_functions(&[identifier]).await.pop().unwrap()
222    }
223
224    /// Identifies `Event`s from its cache or `https://api.openchain.xyz`
225    pub async fn identify_events(
226        &self,
227        identifiers: impl IntoIterator<Item = impl AsRef<[u8]>>,
228    ) -> Vec<Option<Event>> {
229        self.inner
230            .write()
231            .await
232            .identify(SelectorType::Event, identifiers, get_event)
233            .await
234    }
235
236    /// Identifies `Event` from its cache or `https://api.openchain.xyz`
237    pub async fn identify_event(&self, identifier: &[u8]) -> Option<Event> {
238        self.identify_events(&[identifier]).await.pop().unwrap()
239    }
240
241    /// Identifies `Error`s from its cache or `https://api.openchain.xyz`.
242    pub async fn identify_errors(
243        &self,
244        identifiers: impl IntoIterator<Item = impl AsRef<[u8]>>,
245    ) -> Vec<Option<Error>> {
246        self.inner
247            .write()
248            .await
249            .identify(SelectorType::Error, identifiers, get_error)
250            .await
251    }
252
253    /// Identifies `Error` from its cache or `https://api.openchain.xyz`.
254    pub async fn identify_error(&self, identifier: &[u8]) -> Option<Error> {
255        self.identify_errors(&[identifier]).await.pop().unwrap()
256    }
257}
258
259#[cfg(test)]
260#[allow(clippy::needless_return)]
261mod tests {
262    use super::*;
263
264    #[tokio::test(flavor = "multi_thread")]
265    async fn can_query_signatures() {
266        let tmp = tempfile::Builder::new()
267            .prefix("sig-test")
268            .tempdir()
269            .expect("failed creating temporary dir");
270        {
271            let sigs = SignaturesIdentifier::new(Some(tmp.path().into()), false).unwrap();
272
273            assert!(sigs.inner.read().await.cached.events.is_empty());
274            assert!(sigs.inner.read().await.cached.functions.is_empty());
275
276            let func = sigs.identify_function(&[35, 184, 114, 221]).await.unwrap();
277            let event = sigs
278                .identify_event(&[
279                    39, 119, 42, 220, 99, 219, 7, 170, 231, 101, 183, 30, 178, 181, 51, 6, 79, 167,
280                    129, 189, 87, 69, 126, 27, 19, 133, 146, 216, 25, 141, 9, 89,
281                ])
282                .await
283                .unwrap();
284
285            assert_eq!(
286                func,
287                get_func("transferFrom(address,address,uint256)").unwrap()
288            );
289            assert_eq!(
290                event,
291                get_event("Transfer(address,address,uint128)").unwrap()
292            );
293
294            // Save the cache.
295            sigs.save().await;
296        }
297
298        let sigs = SignaturesIdentifier::new(Some(tmp.path().into()), false).unwrap();
299        assert_eq!(sigs.inner.read().await.cached.events.len(), 1);
300        assert_eq!(sigs.inner.read().await.cached.functions.len(), 1);
301    }
302}