From 4c73702d605287c39cb73a6d9a6879c5c3ead0dc Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Sat, 20 Jan 2024 08:11:12 +0100 Subject: [PATCH] feat: resolve multiple function/event selectors in one openchain.xyz request (#6863) * feat(common): send multiple selectors in one request to openchain.xyz * feat(evm/traces): resolve multiple selectors in one request * chore:touchups --------- Co-authored-by: Maxim Andreev --- crates/common/src/selectors.rs | 130 +++++++++++++----- crates/evm/traces/src/decoder/mod.rs | 25 +++- .../evm/traces/src/identifier/signatures.rs | 75 +++++----- crates/evm/traces/src/lib.rs | 2 + 4 files changed, 163 insertions(+), 69 deletions(-) diff --git a/crates/common/src/selectors.rs b/crates/common/src/selectors.rs index 576af2b9f119..13979b3874ec 100644 --- a/crates/common/src/selectors.rs +++ b/crates/common/src/selectors.rs @@ -16,8 +16,8 @@ use std::{ time::Duration, }; -static SELECTOR_DATABASE_URL: &str = "https://api.openchain.xyz/signature-database/v1/"; -static SELECTOR_IMPORT_URL: &str = "https://api.openchain.xyz/signature-database/v1/import"; +const SELECTOR_LOOKUP_URL: &str = "https://api.openchain.xyz/signature-database/v1/lookup"; +const SELECTOR_IMPORT_URL: &str = "https://api.openchain.xyz/signature-database/v1/import"; /// The standard request timeout for API requests const REQ_TIMEOUT: Duration = Duration::from_secs(15); @@ -98,13 +98,13 @@ impl SignEthClient { fn on_reqwest_err(&self, err: &reqwest::Error) { fn is_connectivity_err(err: &reqwest::Error) -> bool { if err.is_timeout() || err.is_connect() { - return true + return true; } // Error HTTP codes (5xx) are considered connectivity issues and will prompt retry if let Some(status) = err.status() { let code = status.as_u16(); if (500..600).contains(&code) { - return true + return true; } } false @@ -142,19 +142,51 @@ impl SignEthClient { selector: &str, selector_type: SelectorType, ) -> eyre::Result> { + self.decode_selectors(selector_type, std::iter::once(selector)) + .await? + .pop() // Not returning on the previous line ensures a vector with exactly 1 element + .unwrap() + .ok_or(eyre::eyre!("No signature found")) + } + + /// Decodes the given function or event selectors using https://api.openchain.xyz + pub async fn decode_selectors( + &self, + selector_type: SelectorType, + selectors: impl IntoIterator>, + ) -> eyre::Result>>> { + let selectors: Vec = selectors + .into_iter() + .map(Into::into) + .map(|s| if s.starts_with("0x") { s } else { format!("0x{s}") }) + .collect(); + + if selectors.is_empty() { + return Ok(vec![]); + } + // exit early if spurious connection self.ensure_not_spurious()?; + let expected_len = match selector_type { + SelectorType::Function => 10, // 0x + hex(4bytes) + SelectorType::Event => 66, // 0x + hex(32bytes) + }; + if let Some(s) = selectors.iter().find(|s| s.len() != expected_len) { + eyre::bail!( + "Invalid selector {s}: expected {expected_len} characters (including 0x prefix)." + ) + } + #[derive(Deserialize)] struct Decoded { name: String, - filtered: bool, } #[derive(Deserialize)] struct ApiResult { - event: HashMap>, - function: HashMap>, + event: HashMap>>, + function: HashMap>>, } #[derive(Deserialize)] @@ -165,10 +197,14 @@ impl SignEthClient { // using openchain.xyz signature database over 4byte // see https://github.com/foundry-rs/foundry/issues/1672 - let url = match selector_type { - SelectorType::Function => format!("{SELECTOR_DATABASE_URL}lookup?function={selector}"), - SelectorType::Event => format!("{SELECTOR_DATABASE_URL}lookup?event={selector}"), - }; + let url = format!( + "{SELECTOR_LOOKUP_URL}?{ltype}={selectors_str}", + ltype = match selector_type { + SelectorType::Function => "function", + SelectorType::Event => "event", + }, + selectors_str = selectors.join(",") + ); let res = self.get_text(&url).await?; let api_response = match serde_json::from_str::(&res) { @@ -187,27 +223,18 @@ impl SignEthClient { SelectorType::Event => api_response.result.event, }; - Ok(decoded - .get(selector) - .ok_or_else(|| eyre::eyre!("No signature found"))? - .iter() - .filter(|&d| !d.filtered) - .map(|d| d.name.clone()) - .collect::>()) + Ok(selectors + .into_iter() + .map(|selector| match decoded.get(&selector) { + Some(Some(r)) => Some(r.iter().map(|d| d.name.clone()).collect()), + _ => None, + }) + .collect()) } /// Fetches a function signature given the selector using https://api.openchain.xyz pub async fn decode_function_selector(&self, selector: &str) -> eyre::Result> { - let stripped_selector = selector.strip_prefix("0x").unwrap_or(selector); - let prefixed_selector = format!("0x{}", stripped_selector); - if prefixed_selector.len() != 10 { - eyre::bail!( - "Invalid selector: expected 8 characters (excluding 0x prefix), got {}.", - stripped_selector.len() - ) - } - - self.decode_selector(&prefixed_selector[..10], SelectorType::Function).await + self.decode_selector(selector, SelectorType::Function).await } /// Fetches all possible signatures and attempts to abi decode the calldata @@ -232,11 +259,7 @@ impl SignEthClient { /// Fetches an event signature given the 32 byte topic using https://api.openchain.xyz pub async fn decode_event_topic(&self, topic: &str) -> eyre::Result> { - let prefixed_topic = format!("0x{}", topic.strip_prefix("0x").unwrap_or(topic)); - if prefixed_topic.len() != 66 { - eyre::bail!("Invalid topic: expected 64 characters (excluding 0x prefix), got {} characters (including 0x prefix).", prefixed_topic.len()) - } - self.decode_selector(&prefixed_topic[..66], SelectorType::Event).await + self.decode_selector(topic, SelectorType::Event).await } /// Pretty print calldata and if available, fetch possible function signatures @@ -376,12 +399,20 @@ pub enum SelectorType { /// Decodes the given function or event selector using https://api.openchain.xyz pub async fn decode_selector( - selector: &str, selector_type: SelectorType, + selector: &str, ) -> eyre::Result> { SignEthClient::new()?.decode_selector(selector, selector_type).await } +/// Decodes the given function or event selectors using https://api.openchain.xyz +pub async fn decode_selectors( + selector_type: SelectorType, + selectors: impl IntoIterator>, +) -> eyre::Result>>> { + SignEthClient::new()?.decode_selectors(selector_type, selectors).await +} + /// Fetches a function signature given the selector https://api.openchain.xyz pub async fn decode_function_selector(selector: &str) -> eyre::Result> { SignEthClient::new()?.decode_function_selector(selector).await @@ -569,7 +600,7 @@ mod tests { .map_err(|e| { assert_eq!( e.to_string(), - "Invalid selector: expected 8 characters (excluding 0x prefix), got 6." + "Invalid selector 0xa9059c: expected 10 characters (including 0x prefix)." ) }) .map(|_| panic!("Expected fourbyte error")) @@ -685,4 +716,33 @@ mod tests { .await; assert_eq!(decoded.unwrap()[0], "canCall(address,address,bytes4)".to_string()); } + + #[tokio::test(flavor = "multi_thread")] + async fn test_decode_selectors() { + let event_topics = vec![ + "7e1db2a1cd12f0506ecd806dba508035b290666b84b096a87af2fd2a1516ede6", + "0xb7009613e63fb13fd59a2fa4c206a992c1f090a44e5d530be255aa17fed0b3dd", + ]; + let decoded = decode_selectors(SelectorType::Event, event_topics).await; + let decoded = decoded.unwrap(); + assert_eq!( + decoded, + vec![ + Some(vec!["updateAuthority(address,uint8)".to_string()]), + Some(vec!["canCall(address,address,bytes4)".to_string()]), + ] + ); + + let function_selectors = vec!["0xa9059cbb", "0x70a08231", "313ce567"]; + let decoded = decode_selectors(SelectorType::Function, function_selectors).await; + let decoded = decoded.unwrap(); + assert_eq!( + decoded, + vec![ + Some(vec!["transfer(address,uint256)".to_string()]), + Some(vec!["balanceOf(address)".to_string()]), + Some(vec!["decimals()".to_string()]), + ] + ); + } } diff --git a/crates/evm/traces/src/decoder/mod.rs b/crates/evm/traces/src/decoder/mod.rs index 29b46ad6c6be..9f430912d5a7 100644 --- a/crates/evm/traces/src/decoder/mod.rs +++ b/crates/evm/traces/src/decoder/mod.rs @@ -2,7 +2,7 @@ use crate::{ identifier::{ AddressIdentity, LocalTraceIdentifier, SingleSignaturesIdentifier, TraceIdentifier, }, - CallTrace, CallTraceArena, DecodedCallData, DecodedCallLog, DecodedCallTrace, + CallTrace, CallTraceArena, CallTraceNode, DecodedCallData, DecodedCallLog, DecodedCallTrace, }; use alloy_dyn_abi::{DecodedEvent, DynSolValue, EventExt, FunctionExt, JsonAbiExt}; use alloy_json_abi::{Event, Function, JsonAbi}; @@ -505,6 +505,29 @@ impl CallTraceDecoder { DecodedCallLog::Raw(log) } + /// Prefetches function and event signatures into the identifier cache + pub async fn prefetch_signatures(&self, nodes: &[CallTraceNode]) { + let Some(identifier) = &self.signature_identifier else { return }; + + let events_it = nodes + .iter() + .flat_map(|node| node.logs.iter().filter_map(|log| log.topics().first())) + .unique(); + identifier.write().await.identify_events(events_it).await; + + const DEFAULT_CREATE2_DEPLOYER_BYTES: [u8; 20] = DEFAULT_CREATE2_DEPLOYER.0 .0; + let funcs_it = nodes + .iter() + .filter_map(|n| match n.trace.address.0 .0 { + DEFAULT_CREATE2_DEPLOYER_BYTES => None, + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01..=0x0a] => None, + _ => n.trace.data.get(..SELECTOR_LEN), + }) + .filter(|v| !self.functions.contains_key(*v)) + .unique(); + identifier.write().await.identify_functions(funcs_it).await; + } + fn apply_label(&self, value: &DynSolValue) -> String { if let DynSolValue::Address(addr) = value { if let Some(label) = self.labels.get(addr) { diff --git a/crates/evm/traces/src/identifier/signatures.rs b/crates/evm/traces/src/identifier/signatures.rs index 7809a1c55473..46766b0a8802 100644 --- a/crates/evm/traces/src/identifier/signatures.rs +++ b/crates/evm/traces/src/identifier/signatures.rs @@ -26,7 +26,7 @@ pub struct SignaturesIdentifier { /// Location where to save `CachedSignatures` cached_path: Option, /// Selectors that were unavailable during the session. - unavailable: HashSet>, + unavailable: HashSet, /// The API client to fetch signatures from sign_eth_api: SignEthClient, /// whether traces should be decoded via `sign_eth_api` @@ -95,59 +95,68 @@ impl SignaturesIdentifier { async fn identify( &mut self, selector_type: SelectorType, - identifier: &[u8], + identifiers: impl IntoIterator>, get_type: impl Fn(&str) -> eyre::Result, - ) -> Option { - // Exit early if we have unsuccessfully queried it before. - if self.unavailable.contains(identifier) { - return None - } - - let map = match selector_type { + ) -> Vec> { + let cache = match selector_type { SelectorType::Function => &mut self.cached.functions, SelectorType::Event => &mut self.cached.events, }; - let hex_identifier = hex::encode_prefixed(identifier); + let hex_identifiers: Vec = + identifiers.into_iter().map(hex::encode_prefixed).collect(); + + if !self.offline { + let query: Vec<_> = hex_identifiers + .iter() + .filter(|v| !cache.contains_key(v.as_str())) + .filter(|v| !self.unavailable.contains(v.as_str())) + .collect(); - if !self.offline && !map.contains_key(&hex_identifier) { - if let Ok(signatures) = - self.sign_eth_api.decode_selector(&hex_identifier, selector_type).await + if let Ok(res) = self.sign_eth_api.decode_selectors(selector_type, query.clone()).await { - if let Some(signature) = signatures.into_iter().next() { - map.insert(hex_identifier.clone(), signature); + for (hex_id, selector_result) in query.into_iter().zip(res.into_iter()) { + let mut found = false; + if let Some(decoded_results) = selector_result { + if let Some(decoded_result) = decoded_results.into_iter().next() { + cache.insert(hex_id.clone(), decoded_result); + found = true; + } + } + if !found { + self.unavailable.insert(hex_id.clone()); + } } } } - if let Some(signature) = map.get(&hex_identifier) { - return get_type(signature).ok() - } - - self.unavailable.insert(identifier.to_vec()); - - None + hex_identifiers.iter().map(|v| cache.get(v).and_then(|v| get_type(v).ok())).collect() } - /// Returns `None` if in offline mode - fn ensure_not_offline(&self) -> Option<()> { - if self.offline { - None - } else { - Some(()) - } + /// Identifies `Function`s from its cache or `https://api.openchain.xyz` + pub async fn identify_functions( + &mut self, + identifiers: impl IntoIterator>, + ) -> Vec> { + self.identify(SelectorType::Function, identifiers, get_func).await } /// Identifies `Function` from its cache or `https://api.openchain.xyz` pub async fn identify_function(&mut self, identifier: &[u8]) -> Option { - self.ensure_not_offline()?; - self.identify(SelectorType::Function, identifier, get_func).await + self.identify_functions(&[identifier]).await.pop().unwrap() + } + + /// Identifies `Event`s from its cache or `https://api.openchain.xyz` + pub async fn identify_events( + &mut self, + identifiers: impl IntoIterator>, + ) -> Vec> { + self.identify(SelectorType::Event, identifiers, get_event).await } /// Identifies `Event` from its cache or `https://api.openchain.xyz` pub async fn identify_event(&mut self, identifier: &[u8]) -> Option { - self.ensure_not_offline()?; - self.identify(SelectorType::Event, identifier, get_event).await + self.identify_events(&[identifier]).await.pop().unwrap() } } diff --git a/crates/evm/traces/src/lib.rs b/crates/evm/traces/src/lib.rs index 7ad2f6bf0975..91a693656d90 100644 --- a/crates/evm/traces/src/lib.rs +++ b/crates/evm/traces/src/lib.rs @@ -71,6 +71,8 @@ pub async fn render_trace_arena( arena: &CallTraceArena, decoder: &CallTraceDecoder, ) -> Result { + decoder.prefetch_signatures(arena.nodes()).await; + fn inner<'a>( arena: &'a [CallTraceNode], decoder: &'a CallTraceDecoder,