Skip to content

Commit

Permalink
feat(inspect): log decoding, joining to trace at correct addresses
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon-Becker committed Dec 11, 2023
1 parent cc7c9fd commit 806aca0
Show file tree
Hide file tree
Showing 10 changed files with 316 additions and 26 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 28 additions & 1 deletion cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,34 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
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) => {
Expand Down
7 changes: 5 additions & 2 deletions cli/src/output.rs
Original file line number Diff line number Diff line change
@@ -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`
Expand All @@ -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 {
Expand Down
72 changes: 69 additions & 3 deletions common/src/ether/rpc.rs
Original file line number Diff line number Diff line change
@@ -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
///
Expand Down Expand Up @@ -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<Vec<ethers::core::types::Log>, Box<dyn std::error::Error>> {
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::<Http>::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 {}
1 change: 1 addition & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]}
Expand Down
83 changes: 83 additions & 0 deletions core/src/inspect/core/logs.rs
Original file line number Diff line number Diff line change
@@ -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<H256>,

/// Data
pub data: Bytes,

/// Block Hash
#[serde(rename = "blockHash")]
#[serde(skip_serializing_if = "Option::is_none")]
pub block_hash: Option<H256>,

/// Block Number
#[serde(rename = "blockNumber")]
#[serde(skip_serializing_if = "Option::is_none")]
pub block_number: Option<U64>,

/// Transaction Hash
#[serde(rename = "transactionHash")]
#[serde(skip_serializing_if = "Option::is_none")]
pub transaction_hash: Option<H256>,

/// Transaction Index
#[serde(rename = "transactionIndex")]
#[serde(skip_serializing_if = "Option::is_none")]
pub transaction_index: Option<U64>,

/// 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<U256>,

/// 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<U256>,

/// Log Type
#[serde(rename = "logType")]
#[serde(skip_serializing_if = "Option::is_none")]
pub log_type: Option<String>,

/// 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<bool>,
}

#[async_trait]
impl TryFrom<Log> for DecodedLog {
type Error = crate::error::Error;

async fn try_from(value: Log) -> Result<Self, Self::Error> {
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,
})
}
}
2 changes: 2 additions & 0 deletions core/src/inspect/core/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
pub(crate) mod contracts;
pub mod logs;
pub mod storage;
pub mod tracing;
4 changes: 1 addition & 3 deletions core/src/inspect/core/storage.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@


/// Converts a raw [`StateDiff`] to a human-readable diff.
//
64 changes: 60 additions & 4 deletions core/src/inspect/core/tracing.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 {
Expand All @@ -23,6 +29,7 @@ pub struct DecodedTransactionTrace {
pub result: Option<DecodedRes>,
pub error: Option<String>,
pub subtraces: Vec<DecodedTransactionTrace>,
pub logs: Vec<DecodedLog>,
}

/// Decoded Action
Expand Down Expand Up @@ -116,7 +123,6 @@ impl TryFrom<Vec<TransactionTrace>> 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
Expand Down Expand Up @@ -164,6 +170,7 @@ impl TryFrom<TransactionTrace> for DecodedTransactionTrace {
result,
error: value.error,
subtraces: Vec::new(), // we will build this later
logs: Vec::new(), // we will build this later
})
}
}
Expand Down Expand Up @@ -289,4 +296,53 @@ impl DecodedTransactionTrace {

addresses
}

#[async_recursion]
pub async fn join_logs(
&mut self,
decoded_logs: &mut VecDeque<DecodedLog>,
vm_trace: VMTrace,
parent_address: Vec<usize>,
) -> 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(())
}
}
Loading

0 comments on commit 806aca0

Please sign in to comment.