From 13b012d62146ee388a60f4d78c2986f8719f050f Mon Sep 17 00:00:00 2001 From: Jon-Becker Date: Wed, 6 Dec 2023 10:30:24 -0500 Subject: [PATCH] feat(inspect): init transaction tracing (and decoding) --- Cargo.lock | 1 + cache/Cargo.toml | 1 + cache/src/lib.rs | 20 +++--- common/src/ether/rpc.rs | 69 +++++++------------- common/src/ether/signatures.rs | 9 ++- common/src/utils/io/macros.rs | 2 +- common/src/utils/strings.rs | 29 +++++++++ core/src/inspect/mod.rs | 116 +++++++++++++++++++++++++++++---- 8 files changed, 180 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a494a03c..8d94667f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1650,6 +1650,7 @@ dependencies = [ "bincode", "clap", "serde", + "serde_json", ] [[package]] diff --git a/cache/Cargo.toml b/cache/Cargo.toml index 6fe491b3..5fb992a4 100644 --- a/cache/Cargo.toml +++ b/cache/Cargo.toml @@ -11,3 +11,4 @@ keywords = ["ethereum", "web3", "decompiler", "evm", "crypto"] clap = { version = "3.1.18", features = ["derive"] } serde = { version = "1.0", features = ["derive"] } bincode = "1.3.3" +serde_json = "1.0.108" diff --git a/cache/src/lib.rs b/cache/src/lib.rs index ae8e575e..3a821270 100644 --- a/cache/src/lib.rs +++ b/cache/src/lib.rs @@ -194,13 +194,9 @@ where None => return None, }; - let binary_vec = decode_hex(&binary_string); + let binary_vec = decode_hex(&binary_string).ok()?; - if binary_vec.is_err() { - return None; - } - - let cache: Cache = match bincode::deserialize::>(&binary_vec.unwrap()) { + let cache: Cache = match bincode::deserialize::>(&binary_vec) { Ok(c) => { // check if the cache has expired, if so, delete it and return None if c.expiry < @@ -233,7 +229,11 @@ where /// store_cache("store_cache_key2", "value", Some(60 * 60 * 24)); /// ``` #[allow(deprecated)] -pub fn store_cache(key: &str, value: T, expiry: Option) +pub fn store_cache( + key: &str, + value: T, + expiry: Option, +) -> Result<(), Box> where T: Serialize, { let home = home_dir().unwrap(); @@ -247,9 +247,12 @@ where ); let cache = Cache { value, expiry }; - let encoded: Vec = bincode::serialize(&cache).unwrap(); + let encoded: Vec = bincode::serialize(&cache) + .map_err(|e| format!("Failed to serialize cache object: {:?}", e))?; let binary_string = encode_hex(encoded); write_file(cache_file.to_str().unwrap(), &binary_string); + + Ok(()) } /// Cache subcommand handler @@ -289,6 +292,7 @@ pub fn cache(args: CacheArgs) -> Result<(), Box> { } #[allow(deprecated)] +#[allow(unused_must_use)] #[cfg(test)] mod tests { use crate::{delete_cache, exists, keys, read_cache, store_cache}; diff --git a/common/src/ether/rpc.rs b/common/src/ether/rpc.rs index 731a2bc1..4f507064 100644 --- a/common/src/ether/rpc.rs +++ b/common/src/ether/rpc.rs @@ -61,7 +61,8 @@ pub async fn chain_id(rpc_url: &str) -> Result> }; // cache the results - store_cache(&cache_key, chain_id.as_u64(), None); + let _ = store_cache(&cache_key, chain_id.as_u64(), None) + .map_err(|_| logger.error(&format!("failed to cache chain id for rpc url: {:?}", &rpc_url))); debug_max!(&format!("chain_id is '{}'", &chain_id)); @@ -93,17 +94,16 @@ pub async fn get_code( let logger = Logger::default(); // get chain_id - let _chain_id = chain_id(rpc_url).await.unwrap_or(1); - - logger - .debug_max(&format!("fetching bytecode from node for contract: '{}' .", &contract_address)); + let chain_id = chain_id(rpc_url).await.unwrap_or(1); // check the cache for a matching address - if let Some(bytecode) = read_cache(&format!("contract.{}.{}", &_chain_id, &contract_address)) { + if let Some(bytecode) = read_cache(&format!("contract.{}.{}", &chain_id, &contract_address)) { logger.debug(&format!("found cached bytecode for '{}' .", &contract_address)); return Ok(bytecode) } + debug_max!("fetching bytecode from node for contract: '{}' .", &contract_address); + // make sure the RPC provider isn't empty if rpc_url.is_empty() { logger.error("reading on-chain data requires an RPC provider. Use `heimdall --help` for more information."); @@ -138,11 +138,12 @@ pub async fn get_code( }; // cache the results - store_cache( - &format!("contract.{}.{}", &_chain_id, &contract_address), + let _ = store_cache( + &format!("contract.{}.{}", &chain_id, &contract_address), bytecode_as_bytes.to_string().replacen("0x", "", 1), None, - ); + ) + .map_err(|_| logger.error(&format!("failed to cache bytecode for contract: {:?}", &contract_address))); Ok(bytecode_as_bytes.to_string()) }) @@ -193,7 +194,7 @@ pub async fn get_transaction( }; // safely unwrap the transaction hash - let transaction_hash = match H256::from_str(transaction_hash) { + let transaction_hash_hex = match H256::from_str(transaction_hash) { Ok(transaction_hash) => transaction_hash, Err(_) => { logger.error(&format!("failed to parse transaction hash '{}' .", &transaction_hash)); @@ -201,8 +202,8 @@ pub async fn get_transaction( } }; - // fetch the transaction from the node - Ok(match provider.get_transaction(transaction_hash).await { + // get the transaction + let tx = match provider.get_transaction(transaction_hash_hex).await { Ok(tx) => match tx { Some(tx) => tx, None => { @@ -214,7 +215,9 @@ pub async fn get_transaction( logger.error(&format!("failed to fetch calldata from '{}' .", &transaction_hash)); return Err(backoff::Error::Transient { err: (), retry_after: Some(Duration::from_secs(1)) }) } - }) + }; + + Ok(tx) }) .await .map_err(|_| Box::from("failed to get transaction")) @@ -295,16 +298,17 @@ pub async fn get_storage_diff( }; // write the state diff to the cache - let expiry = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() + - 60 * 60 * 24 * 7; - store_cache( + let _ = store_cache( &format!("diff.{}.{}", &chain_id, &transaction_hash), &state_diff, - Some(expiry), - ); + None, + ) + .map_err(|_| { + logger.error(&format!( + "failed to cache state diff for transaction: {:?}", + &transaction_hash + )) + }); debug_max!("fetched state diff for transaction '{}' .", &transaction_hash); @@ -336,17 +340,6 @@ pub async fn get_trace( // create new logger let logger = Logger::default(); - // get chain_id - let chain_id = chain_id(rpc_url).await.unwrap(); - - // check the cache for a matching address - if let Some(block_trace) = - read_cache(&format!("trace.{}.{}", &chain_id, &transaction_hash)) - { - debug_max!("found cached trace for transaction '{}' .", &transaction_hash); - return Ok(block_trace); - } - debug_max!(&format!( "fetching trace from node for transaction: '{}' .", &transaction_hash @@ -392,18 +385,6 @@ pub async fn get_trace( } }; - // write the trace to the cache - let expiry = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() - + 60 * 60 * 24 * 7; - store_cache( - &format!("trace.{}.{}", &chain_id, &transaction_hash), - &block_trace, - Some(expiry), - ); - debug_max!("fetched trace for transaction '{}' .", &transaction_hash); Ok(block_trace) diff --git a/common/src/ether/signatures.rs b/common/src/ether/signatures.rs index c5a5cce6..7c5cbed7 100644 --- a/common/src/ether/signatures.rs +++ b/common/src/ether/signatures.rs @@ -106,7 +106,8 @@ impl ResolveSelector for ResolvedError { } // cache the results - store_cache(&format!("selector.{selector}"), signature_list.clone(), None); + let _ = store_cache(&format!("selector.{selector}"), signature_list.clone(), None) + .map_err(|e| debug_max!("error storing signatures in cache: {}", e)); match signature_list.len() { 0 => None, @@ -184,7 +185,8 @@ impl ResolveSelector for ResolvedLog { } // cache the results - store_cache(&format!("selector.{selector}"), signature_list.clone(), None); + let _ = store_cache(&format!("selector.{selector}"), signature_list.clone(), None) + .map_err(|e| debug_max!("error storing signatures in cache: {}", e)); match signature_list.len() { 0 => None, @@ -263,7 +265,8 @@ impl ResolveSelector for ResolvedFunction { } // cache the results - store_cache(&format!("selector.{selector}"), signature_list.clone(), None); + let _ = store_cache(&format!("selector.{selector}"), signature_list.clone(), None) + .map_err(|e| debug_max!("error storing signatures in cache: {}", e)); match signature_list.len() { 0 => None, diff --git a/common/src/utils/io/macros.rs b/common/src/utils/io/macros.rs index 3f70adc7..632e4ecf 100644 --- a/common/src/utils/io/macros.rs +++ b/common/src/utils/io/macros.rs @@ -4,6 +4,6 @@ macro_rules! debug_max { $crate::utils::io::logging::Logger::default().debug_max($message); }; ($message:expr, $($arg:tt)*) => { - $crate::utils::io::logging::Logger::default().debug_max(&format!($message, $($arg)*)); + $crate::utils::io::logging::Logger::default().debug_max(&format!($message, $($arg)*)) }; } diff --git a/common/src/utils/strings.rs b/common/src/utils/strings.rs index 168093d4..81820ae6 100644 --- a/common/src/utils/strings.rs +++ b/common/src/utils/strings.rs @@ -3,6 +3,7 @@ use std::{fmt::Write, num::ParseIntError}; use ethers::{ abi::AbiEncode, prelude::{I256, U256}, + types::{Bloom, H160, H256, H64}, }; use fancy_regex::Regex; @@ -309,6 +310,34 @@ pub fn tokenize(s: &str) -> Vec { tokens } +pub trait ToLowerHex { + fn to_lower_hex(&self) -> String; +} + +impl ToLowerHex for H256 { + fn to_lower_hex(&self) -> String { + format!("{:#032x}", self) + } +} + +impl ToLowerHex for H160 { + fn to_lower_hex(&self) -> String { + format!("{:#020x}", self) + } +} + +impl ToLowerHex for H64 { + fn to_lower_hex(&self) -> String { + format!("{:#016x}", self) + } +} + +impl ToLowerHex for Bloom { + fn to_lower_hex(&self) -> String { + format!("{:#064x}", self) + } +} + #[derive(Debug, PartialEq)] pub enum TokenType { Control, diff --git a/core/src/inspect/mod.rs b/core/src/inspect/mod.rs index 336f6d00..7d332368 100644 --- a/core/src/inspect/mod.rs +++ b/core/src/inspect/mod.rs @@ -1,12 +1,15 @@ +use std::collections::HashMap; + use clap::{AppSettings, Parser}; +use clap_verbosity_flag::Verbosity; use derive_builder::Builder; use heimdall_common::{ - ether::rpc::{get_trace, get_transaction}, - utils::io::logging::Logger, + ether::rpc::get_trace, + utils::{io::logging::Logger, strings::ToLowerHex}, }; -use crate::error::Error; +use crate::{decode::DecodeArgsBuilder, error::Error}; #[derive(Debug, Clone, Parser, Builder)] #[clap( @@ -62,24 +65,115 @@ pub async fn inspect(args: InspectArgs) -> Result<(), Error> { } // get a new logger and trace - let (_logger, _trace) = Logger::new(match args.verbose.log_level() { + let (_logger, mut trace) = Logger::new(match args.verbose.log_level() { Some(level) => level.as_str(), None => "SILENT", }); // get calldata from RPC - let transaction = get_transaction(&args.target, &args.rpc_url) - .await - .map_err(|e| Error::RpcError(e.to_string()))?; + // let transaction = get_transaction(&args.target, &args.rpc_url) + // .await + // .map_err(|e| Error::RpcError(e.to_string()))?; // get trace let block_trace = get_trace(&args.target, &args.rpc_url).await.map_err(|e| Error::RpcError(e.to_string()))?; - println!("transaction: {:?}", transaction); - println!("output: {:?}", block_trace.output); - println!("trace: {:#?}", block_trace.trace); - println!("state diff: {:#?}", block_trace.state_diff); + let decode_call = trace.add_call( + 0, + line!(), + "heimdall".to_string(), + "inspect".to_string(), + vec![args.target.clone()], + "()".to_string(), + ); + + if let Some(transaction_traces) = block_trace.trace { + let mut trace_indices = HashMap::new(); + + for transaction_trace in transaction_traces { + println!("trace: {:?}", transaction_trace); + let trace_address = transaction_trace + .trace_address + .iter() + .map(|address| address.to_string()) + .collect::>() + .join("."); + let parent_address = trace_address + .split('.') + .take(trace_address.split('.').count() - 1) + .collect::>() + .join("."); + + // get trace index from parent_address + let parent_index = trace_indices.get(&parent_address).unwrap_or(&decode_call); + + // get action + match transaction_trace.action { + ethers::types::Action::Call(call) => { + // attempt to decode calldata + let calldata = call.input.to_string(); + + if !calldata.replacen("0x", "", 1).is_empty() { + let result = crate::decode::decode( + DecodeArgsBuilder::new() + .target(calldata) + .rpc_url("https://eth.llamarpc.com".to_string()) + .verbose(Verbosity::new(2, 0)) + .build() + .map_err(|_e| Error::DecodeError)?, + ) + .await?; + + // get first result + if let Some(resolved_function) = result.first() { + // convert decoded inputs Option> to Vec + let decoded_inputs = + resolved_function.decoded_inputs.clone().unwrap_or_default(); + + // get index of parent + let parent_index = trace.add_call( + *parent_index, + line!(), + call.to.to_lower_hex(), + resolved_function.name.clone(), + decoded_inputs + .iter() + .map(|token| format!("{:?}", token)) + .collect::>(), + "()".to_string(), + ); + + // add trace_address to trace_indices + trace_indices.insert(trace_address.clone(), parent_index); + trace.add_info( + parent_index, + line!(), + &format!("trace_address: {:?}", trace_address), + ); + } else { + unimplemented!(); + } + } else { + // value transfer + trace.add_call( + *parent_index, + line!(), + call.to.to_lower_hex(), + "transfer".to_string(), + vec![format!("{} wei", call.value)], + "()".to_string(), + ); + } + } + ethers::types::Action::Create(_) => todo!(), + ethers::types::Action::Suicide(_) => todo!(), + ethers::types::Action::Reward(_) => todo!(), + } + } + } + + trace.display(); Ok(()) }