feat(rpc): impl parity trace_transaction (#1765)

This commit is contained in:
Matthias Seitz
2023-03-15 15:03:42 +01:00
committed by GitHub
parent bba61c0b61
commit 0c434e7916
12 changed files with 349 additions and 130 deletions

View File

@@ -1,11 +1,4 @@
use crate::tracing::types::{CallTrace, CallTraceNode, LogCallOrder};
use reth_primitives::{Address, JsonU256, H256, U256};
use reth_rpc_types::trace::{
geth::{DefaultFrame, GethDebugTracingOptions, StructLog},
parity::{ActionType, TransactionTrace},
};
use revm::interpreter::{opcode, InstructionResult};
use std::collections::{BTreeMap, HashMap};
/// An arena of recorded traces.
///
@@ -49,112 +42,4 @@ impl CallTraceArena {
),
}
}
/// Returns the traces of the transaction for `trace_transaction`
pub fn parity_traces(&self) -> Vec<TransactionTrace> {
let traces = Vec::with_capacity(self.arena.len());
for (_idx, node) in self.arena.iter().cloned().enumerate() {
let _action = node.parity_action();
let _result = node.parity_result();
let _action_type = if node.status() == InstructionResult::SelfDestruct {
ActionType::Selfdestruct
} else {
node.kind().into()
};
todo!()
// let trace = TransactionTrace {
// action,
// result: Some(result),
// trace_address: self.info.trace_address(idx),
// subtraces: node.children.len(),
// };
// traces.push(trace)
}
traces
}
/// Recursively fill in the geth trace by going through the traces
///
/// TODO rewrite this iteratively
fn add_to_geth_trace(
&self,
storage: &mut HashMap<Address, BTreeMap<H256, H256>>,
trace_node: &CallTraceNode,
struct_logs: &mut Vec<StructLog>,
opts: &GethDebugTracingOptions,
) {
let mut child_id = 0;
// Iterate over the steps inside the given trace
for step in trace_node.trace.steps.iter() {
let mut log: StructLog = step.into();
// Fill in memory and storage depending on the options
if !opts.disable_storage.unwrap_or_default() {
let contract_storage = storage.entry(step.contract).or_default();
if let Some((key, value)) = step.state_diff {
contract_storage.insert(key.into(), value.into());
log.storage = Some(contract_storage.clone());
}
}
if opts.disable_stack.unwrap_or_default() {
log.stack = None;
}
if !opts.enable_memory.unwrap_or_default() {
log.memory = None;
}
// Add step to geth trace
struct_logs.push(log);
// If the opcode is a call, the descend into child trace
match step.op.u8() {
opcode::CREATE |
opcode::CREATE2 |
opcode::DELEGATECALL |
opcode::CALL |
opcode::STATICCALL |
opcode::CALLCODE => {
self.add_to_geth_trace(
storage,
&self.arena[trace_node.children[child_id]],
struct_logs,
opts,
);
child_id += 1;
}
_ => {}
}
}
}
/// Generate a geth-style trace e.g. for `debug_traceTransaction`
pub fn geth_traces(
&self,
// TODO(mattsse): This should be the total gas used, or gas used by last CallTrace?
receipt_gas_used: U256,
opts: GethDebugTracingOptions,
) -> DefaultFrame {
if self.arena.is_empty() {
return Default::default()
}
// Fetch top-level trace
let main_trace_node = &self.arena[0];
let main_trace = &main_trace_node.trace;
let mut struct_logs = Vec::new();
let mut storage = HashMap::new();
self.add_to_geth_trace(&mut storage, main_trace_node, &mut struct_logs, &opts);
DefaultFrame {
// If the top-level trace succeeded, then it was a success
failed: !main_trace.success,
gas: JsonU256(receipt_gas_used),
return_value: main_trace.output.clone().into(),
struct_logs,
}
}
}

View File

@@ -0,0 +1,104 @@
//! Geth trace builder
use crate::tracing::{types::CallTraceNode, TraceInspectorConfig};
use reth_primitives::{Address, JsonU256, H256, U256};
use reth_rpc_types::trace::geth::*;
use revm::interpreter::opcode;
use std::collections::{BTreeMap, HashMap};
/// A type for creating geth style traces
#[derive(Clone, Debug)]
pub struct GethTraceBuilder {
/// Recorded trace nodes.
nodes: Vec<CallTraceNode>,
/// How the traces were recorded
_config: TraceInspectorConfig,
}
impl GethTraceBuilder {
/// Returns a new instance of the builder
pub(crate) fn new(nodes: Vec<CallTraceNode>, _config: TraceInspectorConfig) -> Self {
Self { nodes, _config }
}
/// Recursively fill in the geth trace by going through the traces
///
/// TODO rewrite this iteratively
fn add_to_geth_trace(
&self,
storage: &mut HashMap<Address, BTreeMap<H256, H256>>,
trace_node: &CallTraceNode,
struct_logs: &mut Vec<StructLog>,
opts: &GethDebugTracingOptions,
) {
let mut child_id = 0;
// Iterate over the steps inside the given trace
for step in trace_node.trace.steps.iter() {
let mut log: StructLog = step.into();
// Fill in memory and storage depending on the options
if !opts.disable_storage.unwrap_or_default() {
let contract_storage = storage.entry(step.contract).or_default();
if let Some((key, value)) = step.state_diff {
contract_storage.insert(key.into(), value.into());
log.storage = Some(contract_storage.clone());
}
}
if opts.disable_stack.unwrap_or_default() {
log.stack = None;
}
if !opts.enable_memory.unwrap_or_default() {
log.memory = None;
}
// Add step to geth trace
struct_logs.push(log);
// If the opcode is a call, the descend into child trace
match step.op.u8() {
opcode::CREATE |
opcode::CREATE2 |
opcode::DELEGATECALL |
opcode::CALL |
opcode::STATICCALL |
opcode::CALLCODE => {
self.add_to_geth_trace(
storage,
&self.nodes[trace_node.children[child_id]],
struct_logs,
opts,
);
child_id += 1;
}
_ => {}
}
}
}
/// Generate a geth-style trace e.g. for `debug_traceTransaction`
pub fn geth_traces(
&self,
// TODO(mattsse): This should be the total gas used, or gas used by last CallTrace?
receipt_gas_used: U256,
opts: GethDebugTracingOptions,
) -> DefaultFrame {
if self.nodes.is_empty() {
return Default::default()
}
// Fetch top-level trace
let main_trace_node = &self.nodes[0];
let main_trace = &main_trace_node.trace;
let mut struct_logs = Vec::new();
let mut storage = HashMap::new();
self.add_to_geth_trace(&mut storage, main_trace_node, &mut struct_logs, &opts);
DefaultFrame {
// If the top-level trace succeeded, then it was a success
failed: !main_trace.success,
gas: JsonU256(receipt_gas_used),
return_value: main_trace.output.clone().into(),
struct_logs,
}
}
}

View File

@@ -0,0 +1,4 @@
//! Builder types for building traces
pub(crate) mod geth;
pub(crate) mod parity;

View File

@@ -0,0 +1,104 @@
use crate::tracing::{types::CallTraceNode, TraceInspectorConfig};
use reth_rpc_types::{trace::parity::*, TransactionInfo};
/// A type for creating parity style traces
#[derive(Clone, Debug)]
pub struct ParityTraceBuilder {
/// Recorded trace nodes
nodes: Vec<CallTraceNode>,
/// How the traces were recorded
_config: TraceInspectorConfig,
}
impl ParityTraceBuilder {
/// Returns a new instance of the builder
pub(crate) fn new(nodes: Vec<CallTraceNode>, _config: TraceInspectorConfig) -> Self {
Self { nodes, _config }
}
/// Returns the trace addresses of all transactions in the set
fn trace_addresses(&self) -> Vec<Vec<usize>> {
let mut all_addresses = Vec::with_capacity(self.nodes.len());
for idx in 0..self.nodes.len() {
all_addresses.push(self.trace_address(idx));
}
all_addresses
}
/// Returns the `traceAddress` of the node in the arena
///
/// The `traceAddress` field of all returned traces, gives the exact location in the call trace
/// [index in root, index in first CALL, index in second CALL, …].
///
/// # Panics
///
/// if the `idx` does not belong to a node
fn trace_address(&self, idx: usize) -> Vec<usize> {
if idx == 0 {
// root call has empty traceAddress
return vec![]
}
let mut graph = vec![];
let mut node = &self.nodes[idx];
while let Some(parent) = node.parent {
// the index of the child call in the arena
let child_idx = node.idx;
node = &self.nodes[parent];
// find the index of the child call in the parent node
let call_idx = node
.children
.iter()
.position(|child| *child == child_idx)
.expect("child exists in parent");
graph.push(call_idx);
}
graph.reverse();
graph
}
/// Returns an iterator over all recorded traces for `trace_transaction`
pub fn into_localized_transaction_traces_iter(
self,
info: TransactionInfo,
) -> impl Iterator<Item = LocalizedTransactionTrace> {
self.into_transaction_traces_iter().map(move |trace| {
let TransactionInfo { hash, index, block_hash, block_number } = info;
LocalizedTransactionTrace {
trace,
transaction_position: index,
transaction_hash: hash,
block_number,
block_hash,
}
})
}
/// Returns an iterator over all recorded traces for `trace_transaction`
pub fn into_localized_transaction_traces(
self,
info: TransactionInfo,
) -> Vec<LocalizedTransactionTrace> {
self.into_localized_transaction_traces_iter(info).collect()
}
/// Returns an iterator over all recorded traces for `trace_transaction`
pub fn into_transaction_traces_iter(self) -> impl Iterator<Item = TransactionTrace> {
let trace_addresses = self.trace_addresses();
self.nodes.into_iter().zip(trace_addresses).map(|(node, trace_address)| {
let action = node.parity_action();
let output = TraceResult::parity_success(node.parity_trace_output());
TransactionTrace {
action,
result: Some(output),
trace_address,
subtraces: node.children.len(),
}
})
}
/// Returns the raw traces of the transaction
pub fn into_transaction_traces(self) -> Vec<TransactionTrace> {
self.into_transaction_traces_iter().collect()
}
}

View File

@@ -18,9 +18,11 @@ use revm::{
use types::{CallTrace, CallTraceStep};
mod arena;
mod builder;
mod config;
mod types;
mod utils;
pub use builder::{geth::GethTraceBuilder, parity::ParityTraceBuilder};
pub use config::TraceInspectorConfig;
/// An inspector that collects call traces.
@@ -60,9 +62,14 @@ impl TracingInspector {
}
}
/// Consumes the Inspector and returns the recorded.
pub fn finalize(self) -> CallTraceArena {
self.traces
/// Consumes the Inspector and returns a [ParityTraceBuilder].
pub fn into_parity_builder(self) -> ParityTraceBuilder {
ParityTraceBuilder::new(self.traces.arena, self.config)
}
/// Consumes the Inspector and returns a [GethTraceBuilder].
pub fn into_geth_builder(self) -> GethTraceBuilder {
GethTraceBuilder::new(self.traces.arena, self.config)
}
/// Configures a [GasInspector]

View File

@@ -158,7 +158,7 @@ impl CallTraceNode {
}
/// Returns the `Output` for a parity trace
pub(crate) fn parity_result(&self) -> TraceOutput {
pub(crate) fn parity_trace_output(&self) -> TraceOutput {
match self.kind() {
CallKind::Call | CallKind::StaticCall | CallKind::CallCode | CallKind::DelegateCall => {
TraceOutput::Call(CallOutput {

View File

@@ -1,7 +1,7 @@
use crate::config::revm_spec;
use reth_primitives::{
Address, ChainSpec, Head, Header, Transaction, TransactionKind, TransactionSigned, TxEip1559,
TxEip2930, TxLegacy, U256,
Address, ChainSpec, Head, Header, Transaction, TransactionKind, TransactionSignedEcRecovered,
TxEip1559, TxEip2930, TxLegacy, U256,
};
use revm::primitives::{AnalysisKind, BlockEnv, CfgEnv, SpecId, TransactTo, TxEnv};
@@ -57,8 +57,23 @@ pub fn fill_block_env(block_env: &mut BlockEnv, header: &Header, after_merge: bo
block_env.gas_limit = U256::from(header.gas_limit);
}
/// Fill transaction environment from Transaction.
pub fn fill_tx_env(tx_env: &mut TxEnv, transaction: &TransactionSigned, sender: Address) {
/// Returns a new [TxEnv] filled with the transaction's data.
pub fn tx_env_with_recovered(transaction: &TransactionSignedEcRecovered) -> TxEnv {
let mut tx_env = TxEnv::default();
fill_tx_env(&mut tx_env, transaction.as_ref(), transaction.signer());
tx_env
}
/// Fill transaction environment from [TransactionSignedEcRecovered].
pub fn fill_tx_env_with_recovered(tx_env: &mut TxEnv, transaction: &TransactionSignedEcRecovered) {
fill_tx_env(tx_env, transaction.as_ref(), transaction.signer())
}
/// Fill transaction environment from a [Transaction] and the given sender address.
pub fn fill_tx_env<T>(tx_env: &mut TxEnv, transaction: T, sender: Address)
where
T: AsRef<Transaction>,
{
tx_env.caller = sender;
match transaction.as_ref() {
Transaction::Legacy(TxLegacy {

View File

@@ -10,6 +10,20 @@ use std::collections::BTreeMap;
/// Result type for parity style transaction trace
pub type TraceResult = crate::trace::common::TraceResult<TraceOutput, String>;
// === impl TraceResult ===
impl TraceResult {
/// Wraps the result type in a [TraceResult::Success] variant
pub fn parity_success(result: TraceOutput) -> Self {
TraceResult::Success { result }
}
/// Wraps the result type in a [TraceResult::Error] variant
pub fn parity_error(error: String) -> Self {
TraceResult::Error { error }
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum TraceType {
@@ -190,10 +204,18 @@ pub struct TransactionTrace {
pub struct LocalizedTransactionTrace {
#[serde(flatten)]
pub trace: TransactionTrace,
/// Transaction index within the block, None if pending.
pub transaction_position: Option<usize>,
/// Hash of the transaction
pub transaction_hash: Option<H256>,
pub block_number: U64,
pub block_hash: H256,
/// Block number the transaction is included in, None if pending.
///
/// Note: this deviates from <https://openethereum.github.io/JSONRPC-trace-module#trace_transaction> which always returns a block number
pub block_number: Option<u64>,
/// Hash of the block, if not pending
///
/// Note: this deviates from <https://openethereum.github.io/JSONRPC-trace-module#trace_transaction> which always returns a block number
pub block_hash: Option<H256>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]

View File

@@ -0,0 +1,17 @@
//! Commonly used additional types that are not part of the JSON RPC spec but are often required
//! when working with RPC types, such as [Transaction](crate::Transaction)
use reth_primitives::{TxHash, H256};
/// Additional fields in the context of a block that contains this transaction.
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
pub struct TransactionInfo {
/// Hash of the transaction.
pub hash: Option<TxHash>,
/// Index of the transaction in the block
pub index: Option<usize>,
/// Hash of the block.
pub block_hash: Option<H256>,
/// Number of the block.
pub block_number: Option<u64>,
}

View File

@@ -1,8 +1,10 @@
mod common;
mod receipt;
mod request;
mod signature;
mod typed;
pub use common::TransactionInfo;
pub use receipt::TransactionReceipt;
pub use request::TransactionRequest;
pub use signature::Signature;

View File

@@ -10,7 +10,7 @@ use reth_primitives::{
};
use reth_provider::{providers::ChainState, BlockProvider, EvmEnvProvider, StateProviderFactory};
use reth_rlp::Decodable;
use reth_rpc_types::{Index, Transaction, TransactionRequest};
use reth_rpc_types::{Index, Transaction, TransactionInfo, TransactionRequest};
use reth_transaction_pool::{TransactionOrigin, TransactionPool};
use revm::primitives::{BlockEnv, CfgEnv};
@@ -216,6 +216,45 @@ pub enum TransactionSource {
},
}
// === impl TransactionSource ===
impl TransactionSource {
/// Consumes the type and returns the wrapped transaction.
pub fn into_recovered(self) -> TransactionSignedEcRecovered {
self.into()
}
/// Returns the transaction and block related info, if not pending
pub fn split(self) -> (TransactionSignedEcRecovered, TransactionInfo) {
match self {
TransactionSource::Pool(tx) => {
let hash = tx.hash();
(
tx,
TransactionInfo {
hash: Some(hash),
index: None,
block_hash: None,
block_number: None,
},
)
}
TransactionSource::Database { transaction, index, block_hash, block_number } => {
let hash = transaction.hash();
(
transaction,
TransactionInfo {
hash: Some(hash),
index: Some(index),
block_hash: Some(block_hash),
block_number: Some(block_number),
},
)
}
}
}
}
impl From<TransactionSource> for TransactionSignedEcRecovered {
fn from(value: TransactionSource) -> Self {
match value {

View File

@@ -1,16 +1,22 @@
use crate::{
eth::{cache::EthStateCache, EthTransactions},
eth::{cache::EthStateCache, revm_utils::inspect, EthTransactions},
result::internal_rpc_err,
};
use async_trait::async_trait;
use jsonrpsee::core::RpcResult as Result;
use reth_primitives::{BlockId, Bytes, H256};
use reth_provider::{BlockProvider, EvmEnvProvider, StateProviderFactory};
use reth_revm::{
database::{State, SubState},
env::tx_env_with_recovered,
tracing::{TraceInspectorConfig, TracingInspector},
};
use reth_rpc_api::TraceApiServer;
use reth_rpc_types::{
trace::{filter::TraceFilter, parity::*},
CallRequest, Index,
};
use revm::primitives::Env;
use std::collections::HashSet;
/// `trace` API implementation.
@@ -115,14 +121,28 @@ where
&self,
hash: H256,
) -> Result<Option<Vec<LocalizedTransactionTrace>>> {
let (_transaction, at) = match self.eth_api.transaction_by_hash_at(hash).await? {
let (transaction, at) = match self.eth_api.transaction_by_hash_at(hash).await? {
None => return Ok(None),
Some(res) => res,
};
let (_cfg, _block_env, _at) = self.eth_api.evm_env_at(at).await?;
let (cfg, block, at) = self.eth_api.evm_env_at(at).await?;
Err(internal_rpc_err("unimplemented"))
let (tx, tx_info) = transaction.split();
let traces = self.eth_api.with_state_at(at, |state| {
let tx = tx_env_with_recovered(&tx);
let env = Env { cfg, block, tx };
let db = SubState::new(State::new(state));
let mut inspector = TracingInspector::new(TraceInspectorConfig::default_parity());
inspect(db, env, &mut inspector)?;
let traces = inspector.into_parity_builder().into_localized_transaction_traces(tx_info);
Ok(traces)
})?;
Ok(Some(traces))
}
}