Skip to content

Commit

Permalink
feat: resolve multiple function/event selectors in one openchain.xyz …
Browse files Browse the repository at this point in the history
…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 <[email protected]>
  • Loading branch information
mattsse and cdump committed Jan 20, 2024
1 parent 5240bba commit 4c73702
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 69 deletions.
130 changes: 95 additions & 35 deletions crates/common/src/selectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -142,19 +142,51 @@ impl SignEthClient {
selector: &str,
selector_type: SelectorType,
) -> eyre::Result<Vec<String>> {
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<Item = impl Into<String>>,
) -> eyre::Result<Vec<Option<Vec<String>>>> {
let selectors: Vec<String> = 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<String, Vec<Decoded>>,
function: HashMap<String, Vec<Decoded>>,
event: HashMap<String, Option<Vec<Decoded>>>,
function: HashMap<String, Option<Vec<Decoded>>>,
}

#[derive(Deserialize)]
Expand All @@ -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::<ApiResponse>(&res) {
Expand All @@ -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::<Vec<String>>())
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<Vec<String>> {
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
Expand All @@ -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<Vec<String>> {
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
Expand Down Expand Up @@ -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<Vec<String>> {
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<Item = impl Into<String>>,
) -> eyre::Result<Vec<Option<Vec<String>>>> {
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<Vec<String>> {
SignEthClient::new()?.decode_function_selector(selector).await
Expand Down Expand Up @@ -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"))
Expand Down Expand Up @@ -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()]),
]
);
}
}
25 changes: 24 additions & 1 deletion crates/evm/traces/src/decoder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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) {
Expand Down
75 changes: 42 additions & 33 deletions crates/evm/traces/src/identifier/signatures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub struct SignaturesIdentifier {
/// Location where to save `CachedSignatures`
cached_path: Option<PathBuf>,
/// Selectors that were unavailable during the session.
unavailable: HashSet<Vec<u8>>,
unavailable: HashSet<String>,
/// The API client to fetch signatures from
sign_eth_api: SignEthClient,
/// whether traces should be decoded via `sign_eth_api`
Expand Down Expand Up @@ -95,59 +95,68 @@ impl SignaturesIdentifier {
async fn identify<T>(
&mut self,
selector_type: SelectorType,
identifier: &[u8],
identifiers: impl IntoIterator<Item = impl AsRef<[u8]>>,
get_type: impl Fn(&str) -> eyre::Result<T>,
) -> Option<T> {
// Exit early if we have unsuccessfully queried it before.
if self.unavailable.contains(identifier) {
return None
}

let map = match selector_type {
) -> Vec<Option<T>> {
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<String> =
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<Item = impl AsRef<[u8]>>,
) -> Vec<Option<Function>> {
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<Function> {
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<Item = impl AsRef<[u8]>>,
) -> Vec<Option<Event>> {
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<Event> {
self.ensure_not_offline()?;
self.identify(SelectorType::Event, identifier, get_event).await
self.identify_events(&[identifier]).await.pop().unwrap()
}
}

Expand Down
2 changes: 2 additions & 0 deletions crates/evm/traces/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ pub async fn render_trace_arena(
arena: &CallTraceArena,
decoder: &CallTraceDecoder,
) -> Result<String, std::fmt::Error> {
decoder.prefetch_signatures(arena.nodes()).await;

fn inner<'a>(
arena: &'a [CallTraceNode],
decoder: &'a CallTraceDecoder,
Expand Down

0 comments on commit 4c73702

Please sign in to comment.