foundry_evm_traces/identifier/
signatures.rs

1use alloy_json_abi::{Error, Event, Function};
2use alloy_primitives::{hex, map::HashSet};
3use foundry_common::{
4    abi::{get_error, get_event, get_func},
5    fs,
6    selectors::{OpenChainClient, SelectorType},
7};
8use serde::{Deserialize, Serialize};
9use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
10use tokio::sync::RwLock;
11
12pub type SingleSignaturesIdentifier = Arc<RwLock<SignaturesIdentifier>>;
13
14#[derive(Debug, Default, Serialize, Deserialize)]
15pub struct CachedSignatures {
16    pub errors: BTreeMap<String, String>,
17    pub events: BTreeMap<String, String>,
18    pub functions: BTreeMap<String, String>,
19}
20
21impl CachedSignatures {
22    #[instrument(target = "evm::traces")]
23    pub fn load(cache_path: PathBuf) -> Self {
24        let path = cache_path.join("signatures");
25        if path.is_file() {
26            fs::read_json_file(&path)
27                .map_err(
28                    |err| warn!(target: "evm::traces", ?path, ?err, "failed to read cache file"),
29                )
30                .unwrap_or_default()
31        } else {
32            if let Err(err) = std::fs::create_dir_all(cache_path) {
33                warn!(target: "evm::traces", "could not create signatures cache dir: {:?}", err);
34            }
35            Self::default()
36        }
37    }
38}
39/// An identifier that tries to identify functions and events using signatures found at
40/// `https://openchain.xyz` or a local cache.
41#[derive(Debug)]
42pub struct SignaturesIdentifier {
43    /// Cached selectors for functions, events and custom errors.
44    cached: CachedSignatures,
45    /// Location where to save `CachedSignatures`.
46    cached_path: Option<PathBuf>,
47    /// Selectors that were unavailable during the session.
48    unavailable: HashSet<String>,
49    /// The OpenChain client to fetch signatures from.
50    client: Option<OpenChainClient>,
51}
52
53impl SignaturesIdentifier {
54    #[instrument(target = "evm::traces")]
55    pub fn new(
56        cache_path: Option<PathBuf>,
57        offline: bool,
58    ) -> eyre::Result<SingleSignaturesIdentifier> {
59        let client = if !offline { Some(OpenChainClient::new()?) } else { None };
60
61        let identifier = if let Some(cache_path) = cache_path {
62            let path = cache_path.join("signatures");
63            trace!(target: "evm::traces", ?path, "reading signature cache");
64            let cached = CachedSignatures::load(cache_path);
65            Self { cached, cached_path: Some(path), unavailable: HashSet::default(), client }
66        } else {
67            Self {
68                cached: Default::default(),
69                cached_path: None,
70                unavailable: HashSet::default(),
71                client,
72            }
73        };
74
75        Ok(Arc::new(RwLock::new(identifier)))
76    }
77
78    #[instrument(target = "evm::traces", skip(self))]
79    pub fn save(&self) {
80        if let Some(cached_path) = &self.cached_path {
81            if let Some(parent) = cached_path.parent() {
82                if let Err(err) = std::fs::create_dir_all(parent) {
83                    warn!(target: "evm::traces", ?parent, ?err, "failed to create cache");
84                }
85            }
86            if let Err(err) = fs::write_json_file(cached_path, &self.cached) {
87                warn!(target: "evm::traces", ?cached_path, ?err, "failed to flush signature cache");
88            } else {
89                trace!(target: "evm::traces", ?cached_path, "flushed signature cache")
90            }
91        }
92    }
93}
94
95impl SignaturesIdentifier {
96    async fn identify<T>(
97        &mut self,
98        selector_type: SelectorType,
99        identifiers: impl IntoIterator<Item = impl AsRef<[u8]>>,
100        get_type: impl Fn(&str) -> eyre::Result<T>,
101    ) -> Vec<Option<T>> {
102        let cache = match selector_type {
103            SelectorType::Function => &mut self.cached.functions,
104            SelectorType::Event => &mut self.cached.events,
105            SelectorType::Error => &mut self.cached.errors,
106        };
107
108        let hex_identifiers: Vec<String> =
109            identifiers.into_iter().map(hex::encode_prefixed).collect();
110
111        if let Some(client) = &self.client {
112            let query: Vec<_> = hex_identifiers
113                .iter()
114                .filter(|v| !cache.contains_key(v.as_str()))
115                .filter(|v| !self.unavailable.contains(v.as_str()))
116                .collect();
117
118            if let Ok(res) = client.decode_selectors(selector_type, query.clone()).await {
119                for (hex_id, selector_result) in query.into_iter().zip(res.into_iter()) {
120                    let mut found = false;
121                    if let Some(decoded_results) = selector_result {
122                        if let Some(decoded_result) = decoded_results.into_iter().next() {
123                            cache.insert(hex_id.clone(), decoded_result);
124                            found = true;
125                        }
126                    }
127                    if !found {
128                        self.unavailable.insert(hex_id.clone());
129                    }
130                }
131            }
132        }
133
134        hex_identifiers.iter().map(|v| cache.get(v).and_then(|v| get_type(v).ok())).collect()
135    }
136
137    /// Identifies `Function`s from its cache or `https://api.openchain.xyz`
138    pub async fn identify_functions(
139        &mut self,
140        identifiers: impl IntoIterator<Item = impl AsRef<[u8]>>,
141    ) -> Vec<Option<Function>> {
142        self.identify(SelectorType::Function, identifiers, get_func).await
143    }
144
145    /// Identifies `Function` from its cache or `https://api.openchain.xyz`
146    pub async fn identify_function(&mut self, identifier: &[u8]) -> Option<Function> {
147        self.identify_functions(&[identifier]).await.pop().unwrap()
148    }
149
150    /// Identifies `Event`s from its cache or `https://api.openchain.xyz`
151    pub async fn identify_events(
152        &mut self,
153        identifiers: impl IntoIterator<Item = impl AsRef<[u8]>>,
154    ) -> Vec<Option<Event>> {
155        self.identify(SelectorType::Event, identifiers, get_event).await
156    }
157
158    /// Identifies `Event` from its cache or `https://api.openchain.xyz`
159    pub async fn identify_event(&mut self, identifier: &[u8]) -> Option<Event> {
160        self.identify_events(&[identifier]).await.pop().unwrap()
161    }
162
163    /// Identifies `Error`s from its cache or `https://api.openchain.xyz`.
164    pub async fn identify_errors(
165        &mut self,
166        identifiers: impl IntoIterator<Item = impl AsRef<[u8]>>,
167    ) -> Vec<Option<Error>> {
168        self.identify(SelectorType::Error, identifiers, get_error).await
169    }
170
171    /// Identifies `Error` from its cache or `https://api.openchain.xyz`.
172    pub async fn identify_error(&mut self, identifier: &[u8]) -> Option<Error> {
173        self.identify_errors(&[identifier]).await.pop().unwrap()
174    }
175}
176
177impl Drop for SignaturesIdentifier {
178    fn drop(&mut self) {
179        self.save();
180    }
181}