Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: resolve multiple function/event selectors in one openchain.xyz request #6863

Merged
merged 4 commits into from
Jan 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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