Skip to content

Commit

Permalink
chore(traces): add a trace identifier stack/builder (#7338)
Browse files Browse the repository at this point in the history
  • Loading branch information
DaniPopes authored Mar 7, 2024
1 parent b253d84 commit 5c3b075
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 79 deletions.
18 changes: 9 additions & 9 deletions crates/chisel/src/dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use foundry_config::{Config, RpcEndpoint};
use foundry_evm::{
decode::decode_console_logs,
traces::{
identifier::{EtherscanIdentifier, SignaturesIdentifier},
identifier::{SignaturesIdentifier, TraceIdentifiers},
render_trace_arena, CallTraceDecoder, CallTraceDecoderBuilder, TraceKind,
},
};
Expand Down Expand Up @@ -893,11 +893,6 @@ impl ChiselDispatcher {
result: &mut ChiselResult,
// known_contracts: &ContractsByArtifact,
) -> eyre::Result<CallTraceDecoder> {
let mut etherscan_identifier = EtherscanIdentifier::new(
&session_config.foundry_config,
session_config.evm_opts.get_remote_chain_id(),
)?;

let mut decoder = CallTraceDecoderBuilder::new()
.with_labels(result.labeled_addresses.clone())
.with_signature_identifier(SignaturesIdentifier::new(
Expand All @@ -906,9 +901,14 @@ impl ChiselDispatcher {
)?)
.build();

for (_, trace) in &mut result.traces {
// decoder.identify(trace, &mut local_identifier);
decoder.identify(trace, &mut etherscan_identifier);
let mut identifier = TraceIdentifiers::new().with_etherscan(
&session_config.foundry_config,
session_config.evm_opts.get_remote_chain_id(),
)?;
if !identifier.is_empty() {
for (_, trace) in &mut result.traces {
decoder.identify(trace, &mut identifier);
}
}
Ok(decoder)
}
Expand Down
12 changes: 9 additions & 3 deletions crates/cli/src/utils/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,12 +397,18 @@ pub async fn handle_traces(
.build();

let mut etherscan_identifier = EtherscanIdentifier::new(config, chain)?;
for (_, trace) in &mut result.traces {
decoder.identify(trace, &mut etherscan_identifier);
if let Some(etherscan_identifier) = &mut etherscan_identifier {
for (_, trace) in &mut result.traces {
decoder.identify(trace, etherscan_identifier);
}
}

if debug {
let sources = etherscan_identifier.get_compiled_contracts().await?;
let sources = if let Some(etherscan_identifier) = etherscan_identifier {
etherscan_identifier.get_compiled_contracts().await?
} else {
Default::default()
};
let mut debugger = Debugger::builder()
.debug_arena(&result.debug)
.decoder(&decoder)
Expand Down
19 changes: 13 additions & 6 deletions crates/evm/traces/src/decoder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ use crate::{
use alloy_dyn_abi::{DecodedEvent, DynSolValue, EventExt, FunctionExt, JsonAbiExt};
use alloy_json_abi::{Error, Event, Function, JsonAbi};
use alloy_primitives::{Address, LogData, Selector, B256};
use foundry_common::{abi::get_indexed_event, fmt::format_token, SELECTOR_LEN};
use foundry_common::{
abi::get_indexed_event, fmt::format_token, ContractsByArtifact, SELECTOR_LEN,
};
use foundry_evm_core::{
abi::{Console, HardhatConsole, Vm, HARDHAT_CONSOLE_SELECTOR_PATCHES},
constants::{
Expand Down Expand Up @@ -50,17 +52,22 @@ impl CallTraceDecoderBuilder {
self
}

/// Add known contracts to the decoder from a `LocalTraceIdentifier`.
/// Add known contracts to the decoder.
#[inline]
pub fn with_local_identifier_abis(mut self, identifier: &LocalTraceIdentifier<'_>) -> Self {
let contracts = identifier.contracts();
trace!(target: "evm::traces", len=contracts.len(), "collecting local identifier ABIs");
pub fn with_known_contracts(mut self, contracts: &ContractsByArtifact) -> Self {
trace!(target: "evm::traces", len=contracts.len(), "collecting known contract ABIs");
for (abi, _) in contracts.values() {
self.decoder.collect_abi(abi, None);
}
self
}

/// Add known contracts to the decoder from a `LocalTraceIdentifier`.
#[inline]
pub fn with_local_identifier_abis(self, identifier: &LocalTraceIdentifier<'_>) -> Self {
self.with_known_contracts(identifier.contracts())
}

/// Sets the verbosity level of the decoder.
#[inline]
pub fn with_verbosity(mut self, level: u8) -> Self {
Expand Down Expand Up @@ -225,7 +232,7 @@ impl CallTraceDecoder {
fn addresses<'a>(
&'a self,
arena: &'a CallTraceArena,
) -> impl Iterator<Item = (&'a Address, Option<&'a [u8]>)> + 'a {
) -> impl Iterator<Item = (&'a Address, Option<&'a [u8]>)> + Clone + 'a {
arena
.nodes()
.iter()
Expand Down
62 changes: 26 additions & 36 deletions crates/evm/traces/src/identifier/etherscan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,9 @@ use std::{
use tokio::time::{Duration, Interval};

/// A trace identifier that tries to identify addresses using Etherscan.
#[derive(Default)]
pub struct EtherscanIdentifier {
/// The Etherscan client
client: Option<Arc<foundry_block_explorers::Client>>,
client: Arc<foundry_block_explorers::Client>,
/// Tracks whether the API key provides was marked as invalid
///
/// After the first [EtherscanError::InvalidApiKey] this will get set to true, so we can
Expand All @@ -40,22 +39,21 @@ pub struct EtherscanIdentifier {

impl EtherscanIdentifier {
/// Creates a new Etherscan identifier with the given client
pub fn new(config: &Config, chain: Option<Chain>) -> eyre::Result<Self> {
pub fn new(config: &Config, chain: Option<Chain>) -> eyre::Result<Option<Self>> {
// In offline mode, don't use Etherscan.
if config.offline {
// offline mode, don't use etherscan
return Ok(Default::default())
}
if let Some(config) = config.get_etherscan_config_with_chain(chain)? {
trace!(target: "etherscanidentifier", chain=?config.chain, url=?config.api_url, "using etherscan identifier");
Ok(Self {
client: Some(Arc::new(config.into_client()?)),
invalid_api_key: Arc::new(Default::default()),
contracts: BTreeMap::new(),
sources: BTreeMap::new(),
})
} else {
Ok(Default::default())
return Ok(None);
}
let Some(config) = config.get_etherscan_config_with_chain(chain)? else {
return Ok(None);
};
trace!(target: "traces::etherscan", chain=?config.chain, url=?config.api_url, "using etherscan identifier");
Ok(Some(Self {
client: Arc::new(config.into_client()?),
invalid_api_key: Arc::new(AtomicBool::new(false)),
contracts: BTreeMap::new(),
sources: BTreeMap::new(),
}))
}

/// Goes over the list of contracts we have pulled from the traces, clones their source from
Expand Down Expand Up @@ -101,18 +99,13 @@ impl TraceIdentifier for EtherscanIdentifier {
{
trace!(target: "evm::traces", "identify {:?} addresses", addresses.size_hint().1);

let Some(client) = self.client.clone() else {
// no client was configured
return Vec::new()
};

if self.invalid_api_key.load(Ordering::Relaxed) {
// api key was marked as invalid
return Vec::new()
}

let mut fetcher = EtherscanFetcher::new(
client,
self.client.clone(),
Duration::from_secs(1),
5,
Arc::clone(&self.invalid_api_key),
Expand Down Expand Up @@ -191,16 +184,13 @@ impl EtherscanFetcher {

fn queue_next_reqs(&mut self) {
while self.in_progress.len() < self.concurrency {
if let Some(addr) = self.queue.pop() {
let client = Arc::clone(&self.client);
trace!(target: "etherscanidentifier", "fetching info for {:?}", addr);
self.in_progress.push(Box::pin(async move {
let res = client.contract_source_code(addr).await;
(addr, res)
}));
} else {
break
}
let Some(addr) = self.queue.pop() else { break };
let client = Arc::clone(&self.client);
self.in_progress.push(Box::pin(async move {
trace!(target: "traces::etherscan", ?addr, "fetching info");
let res = client.contract_source_code(addr).await;
(addr, res)
}));
}
}
}
Expand Down Expand Up @@ -234,24 +224,24 @@ impl Stream for EtherscanFetcher {
}
}
Err(EtherscanError::RateLimitExceeded) => {
warn!(target: "etherscanidentifier", "rate limit exceeded on attempt");
warn!(target: "traces::etherscan", "rate limit exceeded on attempt");
pin.backoff = Some(tokio::time::interval(pin.timeout));
pin.queue.push(addr);
}
Err(EtherscanError::InvalidApiKey) => {
warn!(target: "etherscanidentifier", "invalid api key");
warn!(target: "traces::etherscan", "invalid api key");
// mark key as invalid
pin.invalid_api_key.store(true, Ordering::Relaxed);
return Poll::Ready(None)
}
Err(EtherscanError::BlockedByCloudflare) => {
warn!(target: "etherscanidentifier", "blocked by cloudflare");
warn!(target: "traces::etherscan", "blocked by cloudflare");
// mark key as invalid
pin.invalid_api_key.store(true, Ordering::Relaxed);
return Poll::Ready(None)
}
Err(err) => {
warn!(target: "etherscanidentifier", "could not get etherscan info: {:?}", err);
warn!(target: "traces::etherscan", "could not get etherscan info: {:?}", err);
}
}
}
Expand Down
58 changes: 57 additions & 1 deletion crates/evm/traces/src/identifier/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use alloy_json_abi::JsonAbi;
use alloy_primitives::Address;
use foundry_common::ContractsByArtifact;
use foundry_compilers::ArtifactId;
use foundry_config::{Chain, Config};
use std::borrow::Cow;

mod local;
Expand Down Expand Up @@ -33,5 +35,59 @@ pub trait TraceIdentifier {
/// Attempts to identify an address in one or more call traces.
fn identify_addresses<'a, A>(&mut self, addresses: A) -> Vec<AddressIdentity<'_>>
where
A: Iterator<Item = (&'a Address, Option<&'a [u8]>)>;
A: Iterator<Item = (&'a Address, Option<&'a [u8]>)> + Clone;
}

/// A collection of trace identifiers.
pub struct TraceIdentifiers<'a> {
/// The local trace identifier.
pub local: Option<LocalTraceIdentifier<'a>>,
/// The optional Etherscan trace identifier.
pub etherscan: Option<EtherscanIdentifier>,
}

impl Default for TraceIdentifiers<'_> {
fn default() -> Self {
Self::new()
}
}

impl TraceIdentifier for TraceIdentifiers<'_> {
fn identify_addresses<'a, A>(&mut self, addresses: A) -> Vec<AddressIdentity<'_>>
where
A: Iterator<Item = (&'a Address, Option<&'a [u8]>)> + Clone,
{
let mut identities = Vec::new();
if let Some(local) = &mut self.local {
identities.extend(local.identify_addresses(addresses.clone()));
}
if let Some(etherscan) = &mut self.etherscan {
identities.extend(etherscan.identify_addresses(addresses));
}
identities
}
}

impl<'a> TraceIdentifiers<'a> {
/// Creates a new, empty instance.
pub const fn new() -> Self {
Self { local: None, etherscan: None }
}

/// Sets the local identifier.
pub fn with_local(mut self, known_contracts: &'a ContractsByArtifact) -> Self {
self.local = Some(LocalTraceIdentifier::new(known_contracts));
self
}

/// Sets the etherscan identifier.
pub fn with_etherscan(mut self, config: &Config, chain: Option<Chain>) -> eyre::Result<Self> {
self.etherscan = EtherscanIdentifier::new(config, chain)?;
Ok(self)
}

/// Returns `true` if there are no set identifiers.
pub fn is_empty(&self) -> bool {
self.local.is_none() && self.etherscan.is_none()
}
}
27 changes: 12 additions & 15 deletions crates/forge/bin/cmd/script/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ use forge::{
decode::decode_console_logs,
opts::EvmOpts,
traces::{
identifier::{EtherscanIdentifier, LocalTraceIdentifier, SignaturesIdentifier},
render_trace_arena, CallTraceDecoder, CallTraceDecoderBuilder, TraceKind, Traces,
identifier::SignaturesIdentifier, render_trace_arena, CallTraceDecoder,
CallTraceDecoderBuilder, TraceKind, Traces,
},
};
use forge_verify::RetryArgs;
Expand Down Expand Up @@ -42,6 +42,7 @@ use foundry_evm::{
constants::DEFAULT_CREATE2_DEPLOYER,
decode::RevertDecoder,
inspectors::cheatcodes::{BroadcastableTransaction, BroadcastableTransactions},
traces::identifier::TraceIdentifiers,
};
use foundry_wallets::MultiWalletOpts;
use futures::future;
Expand Down Expand Up @@ -198,33 +199,29 @@ impl ScriptArgs {
result: &mut ScriptResult,
known_contracts: &ContractsByArtifact,
) -> Result<CallTraceDecoder> {
let verbosity = script_config.evm_opts.verbosity;
let mut etherscan_identifier = EtherscanIdentifier::new(
&script_config.config,
script_config.evm_opts.get_remote_chain_id(),
)?;

let mut local_identifier = LocalTraceIdentifier::new(known_contracts);
let mut decoder = CallTraceDecoderBuilder::new()
.with_labels(result.labeled_addresses.clone())
.with_verbosity(verbosity)
.with_local_identifier_abis(&local_identifier)
.with_verbosity(script_config.evm_opts.verbosity)
.with_known_contracts(known_contracts)
.with_signature_identifier(SignaturesIdentifier::new(
Config::foundry_cache_dir(),
script_config.config.offline,
)?)
.build();
let mut identifier = TraceIdentifiers::new()
.with_local(known_contracts)
.with_etherscan(&script_config.config, script_config.evm_opts.get_remote_chain_id())?;

// Decoding traces using etherscan is costly as we run into rate limits,
// causing scripts to run for a very long time unnecessarily.
// Therefore, we only try and use etherscan if the user has provided an API key.
let should_use_etherscan_traces = script_config.config.etherscan_api_key.is_some();
if !should_use_etherscan_traces {
identifier.etherscan = None;
}

for (_, trace) in &mut result.traces {
decoder.identify(trace, &mut local_identifier);
if should_use_etherscan_traces {
decoder.identify(trace, &mut etherscan_identifier);
}
decoder.identify(trace, &mut identifier);
}
Ok(decoder)
}
Expand Down
Loading

0 comments on commit 5c3b075

Please sign in to comment.