diff --git a/Cargo.lock b/Cargo.lock index e84a7c7d..36ebf6f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1723,6 +1723,7 @@ name = "heimdall-core" version = "0.6.5" dependencies = [ "async-convert", + "async-recursion", "backtrace", "clap", "clap-verbosity-flag", diff --git a/cli/src/main.rs b/cli/src/main.rs index 246cb3e3..08f9cb0e 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -349,7 +349,34 @@ async fn main() -> Result<(), Box> { cmd.transpose_api_key = Some(configuration.transpose_api_key); } - inspect(cmd).await?; + // if the user has passed an output filename, override the default filename + let mut filename = "decoded_trace.json".to_string(); + let given_name = cmd.name.as_str(); + + if !given_name.is_empty() { + filename = format!("{}-{}", given_name, filename); + } + + let inspect_result = inspect(cmd.clone()).await?; + + if cmd.output == "print" { + let mut output_str = String::new(); + + if let Some(decoded_trace) = inspect_result.decoded_trace { + output_str.push_str(&format!( + "Decoded Trace:\n\n{}\n", + serde_json::to_string_pretty(&decoded_trace).unwrap() + )); + } + + print_with_less(&output_str).await?; + } else if let Some(decoded_trace) = inspect_result.decoded_trace { + // write decoded trace with serde + let output_path = + build_output_path(&cmd.output, &cmd.target, &cmd.rpc_url, &filename).await?; + + write_file(&output_path, &serde_json::to_string_pretty(&decoded_trace).unwrap()); + } } Subcommands::Config(cmd) => { diff --git a/cli/src/output.rs b/cli/src/output.rs index 1eac0dc0..d4d12657 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -1,6 +1,9 @@ use std::{env, io::Write}; -use heimdall_common::{constants::ADDRESS_REGEX, ether::rpc}; +use heimdall_common::{ + constants::{ADDRESS_REGEX, TRANSACTION_HASH_REGEX}, + ether::rpc, +}; /// build a standardized output path for the given parameters. follows the following cases: /// - if `output` is `print`, return `None` @@ -19,7 +22,7 @@ pub async fn build_output_path( // get the current working directory let cwd = env::current_dir()?.into_os_string().into_string().unwrap(); - if ADDRESS_REGEX.is_match(target)? { + if ADDRESS_REGEX.is_match(target)? || TRANSACTION_HASH_REGEX.is_match(target)? { let chain_id = rpc::chain_id(rpc_url).await?; return Ok(format!("{}/output/{}/{}/{}", cwd, chain_id, target, filename)); } else { diff --git a/common/src/ether/rpc.rs b/common/src/ether/rpc.rs index 7d0a8f17..20216adf 100644 --- a/common/src/ether/rpc.rs +++ b/common/src/ether/rpc.rs @@ -1,13 +1,15 @@ -use std::{str::FromStr, time::Duration}; - use crate::{debug_max, utils::io::logging::Logger}; use backoff::ExponentialBackoff; use ethers::{ core::types::Address, providers::{Http, Middleware, Provider}, - types::{BlockTrace, StateDiff, TraceType, Transaction, H256}, + types::{ + BlockNumber::{self}, + BlockTrace, Filter, FilterBlockOption, StateDiff, TraceType, Transaction, H256, + }, }; use heimdall_cache::{read_cache, store_cache}; +use std::{str::FromStr, time::Duration}; /// Get the chainId of the provided RPC URL /// @@ -395,6 +397,70 @@ pub async fn get_trace( .map_err(|_| Box::from("failed to get trace")) } +/// Get all logs for the given block number +/// +/// ```no_run +/// use heimdall_common::ether::rpc::get_logs; +/// +/// // let logs = get_logs(1, "https://eth.llamarpc.com").await; +/// // assert!(logs.is_ok()); +/// ``` +pub async fn get_block_logs( + block_number: u64, + rpc_url: &str, +) -> Result, Box> { + backoff::future::retry( + ExponentialBackoff { + max_elapsed_time: Some(Duration::from_secs(10)), + ..ExponentialBackoff::default() + }, + || async { + // create new logger + let logger = Logger::default(); + + debug_max!(&format!("fetching logs from node for block: '{}' .", &block_number)); + + // create new provider + let provider = match Provider::::try_from(rpc_url) { + Ok(provider) => provider, + Err(_) => { + logger.error(&format!("failed to connect to RPC provider '{}' .", &rpc_url)); + return Err(backoff::Error::Permanent(())) + } + }; + + // fetch the logs for the block + let logs = match provider + .get_logs(&Filter { + block_option: FilterBlockOption::Range { + from_block: Some(BlockNumber::from(block_number)), + to_block: Some(BlockNumber::from(block_number)), + }, + address: None, + topics: [None, None, None, None], + }) + .await + { + Ok(logs) => logs, + Err(e) => { + logger.error(&format!( + "failed to fetch logs for block '{}' . does your RPC provider support it?", + &block_number + )); + logger.error(&format!("error: '{e}' .")); + return Err(backoff::Error::Permanent(())) + } + }; + + debug_max!("fetched logs for block '{}' .", &block_number); + + Ok(logs) + }, + ) + .await + .map_err(|_| Box::from("failed to get logs")) +} + // TODO: add tests #[cfg(test)] pub mod tests {} diff --git a/core/Cargo.toml b/core/Cargo.toml index 6f63cff9..4e090ea1 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -8,6 +8,7 @@ readme = "README.md" version = "0.6.5" [dependencies] +async-recursion = "1.0.5" thiserror = "1.0.50" backtrace = "0.3" clap = {version = "3.1.18", features = ["derive"]} diff --git a/core/src/inspect/core/logs.rs b/core/src/inspect/core/logs.rs new file mode 100644 index 00000000..dcf6eafb --- /dev/null +++ b/core/src/inspect/core/logs.rs @@ -0,0 +1,83 @@ +// TODO: impl decodedlog for log + +use async_convert::{async_trait, TryFrom}; +use ethers::types::{Address, Bytes, Log, H256, U256, U64}; +use serde::{Deserialize, Serialize}; + +/// Represents a decoded log +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd)] +pub struct DecodedLog { + /// H160. the contract that emitted the log + pub address: Address, + + /// topics: Array of 0 to 4 32 Bytes of indexed log arguments. + /// (In solidity: The first topic is the hash of the signature of the event + /// (e.g. `Deposit(address,bytes32,uint256)`), except you declared the event + /// with the anonymous specifier.) + pub topics: Vec, + + /// Data + pub data: Bytes, + + /// Block Hash + #[serde(rename = "blockHash")] + #[serde(skip_serializing_if = "Option::is_none")] + pub block_hash: Option, + + /// Block Number + #[serde(rename = "blockNumber")] + #[serde(skip_serializing_if = "Option::is_none")] + pub block_number: Option, + + /// Transaction Hash + #[serde(rename = "transactionHash")] + #[serde(skip_serializing_if = "Option::is_none")] + pub transaction_hash: Option, + + /// Transaction Index + #[serde(rename = "transactionIndex")] + #[serde(skip_serializing_if = "Option::is_none")] + pub transaction_index: Option, + + /// Integer of the log index position in the block. None if it's a pending log. + #[serde(rename = "logIndex")] + #[serde(skip_serializing_if = "Option::is_none")] + pub log_index: Option, + + /// Integer of the transactions index position log was created from. + /// None when it's a pending log. + #[serde(rename = "transactionLogIndex")] + #[serde(skip_serializing_if = "Option::is_none")] + pub transaction_log_index: Option, + + /// Log Type + #[serde(rename = "logType")] + #[serde(skip_serializing_if = "Option::is_none")] + pub log_type: Option, + + /// True when the log was removed, due to a chain reorganization. + /// false if it's a valid log. + #[serde(skip_serializing_if = "Option::is_none")] + pub removed: Option, +} + +#[async_trait] +impl TryFrom for DecodedLog { + type Error = crate::error::Error; + + async fn try_from(value: Log) -> Result { + Ok(Self { + address: value.address, + topics: value.topics, + data: value.data, + block_hash: value.block_hash, + block_number: value.block_number, + transaction_hash: value.transaction_hash, + transaction_index: value.transaction_index, + log_index: value.log_index, + transaction_log_index: value.transaction_log_index, + log_type: value.log_type, + removed: value.removed, + }) + } +} diff --git a/core/src/inspect/core/mod.rs b/core/src/inspect/core/mod.rs index ed583ff9..3f256a7c 100644 --- a/core/src/inspect/core/mod.rs +++ b/core/src/inspect/core/mod.rs @@ -1,2 +1,4 @@ pub(crate) mod contracts; +pub mod logs; +pub mod storage; pub mod tracing; diff --git a/core/src/inspect/core/storage.rs b/core/src/inspect/core/storage.rs index 968dd9f1..8337712e 100644 --- a/core/src/inspect/core/storage.rs +++ b/core/src/inspect/core/storage.rs @@ -1,3 +1 @@ - - -/// Converts a raw [`StateDiff`] to a human-readable diff. +// diff --git a/core/src/inspect/core/tracing.rs b/core/src/inspect/core/tracing.rs index d4da4517..cd7720a8 100644 --- a/core/src/inspect/core/tracing.rs +++ b/core/src/inspect/core/tracing.rs @@ -1,10 +1,14 @@ -use std::collections::{HashSet, VecDeque}; +use std::{ + borrow::BorrowMut, + collections::{HashSet, VecDeque}, +}; +use async_recursion::async_recursion; use ethers::{ abi::Token, types::{ - ActionType, Address, Bytes, Call, CallResult, CallType, Create, CreateResult, Reward, - Suicide, TransactionTrace, U256, + ActionType, Address, Bytes, Call, CallResult, CallType, Create, CreateResult, + ExecutedInstruction, Reward, Suicide, TransactionTrace, VMTrace, U256, }, }; use heimdall_common::ether::signatures::ResolvedFunction; @@ -14,6 +18,8 @@ use crate::{decode::DecodeArgsBuilder, error::Error}; use async_convert::{async_trait, TryFrom}; use futures::future::try_join_all; +use super::logs::DecodedLog; + /// Decoded Trace #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] pub struct DecodedTransactionTrace { @@ -23,6 +29,7 @@ pub struct DecodedTransactionTrace { pub result: Option, pub error: Option, pub subtraces: Vec, + pub logs: Vec, } /// Decoded Action @@ -116,7 +123,6 @@ impl TryFrom> for DecodedTransactionTrace { // Iterate through the trace address, navigating through subtraces for &index in trace_address.iter().take(trace_address.len() - 1) { current_trace = current_trace.subtraces.get_mut(index).ok_or(Error::DecodeError)?; - // You might need to define this error } // Insert the decoded trace into the correct position @@ -164,6 +170,7 @@ impl TryFrom for DecodedTransactionTrace { result, error: value.error, subtraces: Vec::new(), // we will build this later + logs: Vec::new(), // we will build this later }) } } @@ -289,4 +296,53 @@ impl DecodedTransactionTrace { addresses } + + #[async_recursion] + pub async fn join_logs( + &mut self, + decoded_logs: &mut VecDeque, + vm_trace: VMTrace, + parent_address: Vec, + ) -> Result<(), Error> { + // Track the current depth using trace_address. Initialize with the trace_address of self. + let mut current_address = parent_address.clone(); + let mut relative_index = 0; + + // Iterate over vm_trace.ops + for op in vm_trace.ops { + match op.op { + // Check if the operation is one of the LOG operations + ExecutedInstruction::Known(ethers::types::Opcode::LOG0) | + ExecutedInstruction::Known(ethers::types::Opcode::LOG1) | + ExecutedInstruction::Known(ethers::types::Opcode::LOG2) | + ExecutedInstruction::Known(ethers::types::Opcode::LOG3) | + ExecutedInstruction::Known(ethers::types::Opcode::LOG4) => { + // Pop the first decoded log, this is the log that corresponds to the current + // operation + let decoded_log = decoded_logs.pop_front().ok_or(Error::DecodeError)?; + + // add the log to the correct position in the trace + let mut current_trace = self.borrow_mut(); + for &index in current_address.iter() { + current_trace = + current_trace.subtraces.get_mut(index).ok_or(Error::DecodeError)?; + } + + // push decoded log into current_trace.logs + current_trace.logs.push(decoded_log); + } + _ => {} + } + + // Handle subtraces if present + if let Some(sub) = op.sub { + current_address.push(relative_index); + let _ = &self.join_logs(decoded_logs, sub, current_address.clone()).await?; + current_address.pop(); + relative_index += 1; + } + } + + Ok(()) + } } diff --git a/core/src/inspect/mod.rs b/core/src/inspect/mod.rs index daf4a19f..68a3873c 100644 --- a/core/src/inspect/mod.rs +++ b/core/src/inspect/mod.rs @@ -1,18 +1,21 @@ mod core; +use std::collections::VecDeque; + use clap::{AppSettings, Parser}; use derive_builder::Builder; -use ethers::types::TransactionTrace; +use ethers::types::{Log, TransactionTrace, U256, U64}; +use futures::future::try_join_all; use heimdall_common::{ - ether::rpc::{get_trace, get_transaction}, + ether::rpc::{get_block_logs, get_trace, get_transaction}, utils::io::logging::Logger, }; use crate::error::Error; -use self::core::{contracts::Contracts, tracing::DecodedTransactionTrace}; +use self::core::{contracts::Contracts, logs::DecodedLog, tracing::DecodedTransactionTrace}; #[derive(Debug, Clone, Parser, Builder)] #[clap( @@ -41,6 +44,14 @@ pub struct InspectArgs { /// Your OPTIONAL Transpose.io API Key, used for labeling contract addresses. #[clap(long = "transpose-api-key", short, hide_default_value = true)] pub transpose_api_key: Option, + + /// Name for the output files. + #[clap(long, short, default_value = "", hide_default_value = true)] + pub name: String, + + /// The output directory to write the output to, or 'print' to print to the console. + #[clap(long = "output", short = 'o', default_value = "output", hide_default_value = true)] + pub output: String, } impl InspectArgsBuilder { @@ -51,6 +62,8 @@ impl InspectArgsBuilder { rpc_url: Some(String::new()), default: Some(true), transpose_api_key: None, + name: Some(String::new()), + output: Some(String::from("output")), } } } @@ -83,15 +96,37 @@ pub async fn inspect(args: InspectArgs) -> Result { }); // get calldata from RPC - let _transaction = get_transaction(&args.target, &args.rpc_url) + let transaction = get_transaction(&args.target, &args.rpc_url) .await .map_err(|e| Error::RpcError(e.to_string()))?; + let block_number = transaction.block_number.unwrap_or(U64::zero()).as_u64(); // get trace let block_trace = get_trace(&args.target, &args.rpc_url).await.map_err(|e| Error::RpcError(e.to_string()))?; - let decoded_trace = + // get logs for this transaction + let transaction_logs = get_block_logs(block_number, &args.rpc_url) + .await + .map_err(|e| Error::RpcError(e.to_string()))? + .into_iter() + .filter(|log| log.transaction_hash == Some(transaction.hash)) + .collect::>(); + + // convert Vec to Vec + let handles = + transaction_logs.into_iter().map(>::try_from); + + logger.debug(&format!("decoding {} logs", handles.len())); + + // sort logs by log index + let mut decoded_logs = try_join_all(handles).await?; + decoded_logs.sort_by(|a, b| { + a.log_index.unwrap_or(U256::zero()).cmp(&b.log_index.unwrap_or(U256::zero())) + }); + let mut decoded_logs = VecDeque::from(decoded_logs); + + let mut decoded_trace = match block_trace.trace { Some(trace) => , @@ -100,20 +135,38 @@ pub async fn inspect(args: InspectArgs) -> Result { .ok(), None => None, }; - if decoded_trace.is_none() { - logger.warn("no trace found for transaction"); - } + if let Some(decoded_trace) = decoded_trace.as_mut() { + logger.debug("resolving address contract labels"); - // get contracts client and extend with addresses from trace - let mut contracts = Contracts::new(&args); - if let Some(decoded_trace) = decoded_trace.clone() { + // get contracts client + let mut contracts = Contracts::new(&args); contracts .extend(decoded_trace.addresses(true, true).into_iter().collect()) .await .map_err(|e| Error::GenericError(e.to_string()))?; - }; - println!("{:#?}", contracts); + // extend with addresses from state diff + if let Some(state_diff) = block_trace.state_diff { + contracts + .extend(state_diff.0.keys().cloned().collect()) + .await + .map_err(|e| Error::GenericError(e.to_string()))?; + } else { + logger + .warn("no state diff found for transaction. skipping state diff label resolution"); + } + + logger.debug(&format!("joining {} decoded logs to trace", decoded_logs.len())); + + if let Some(vm_trace) = block_trace.vm_trace { + // join logs to trace + let _ = decoded_trace.join_logs(&mut decoded_logs, vm_trace, Vec::new()).await; + } else { + logger.warn("no vm trace found for transaction. skipping joining logs"); + } + } else { + logger.warn("no trace found for transaction"); + } Ok(InspectResult { decoded_trace }) }