diff --git a/Cargo.lock b/Cargo.lock index d4218d57a..0560072ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2326,6 +2326,19 @@ dependencies = [ "sp-runtime", ] +[[package]] +name = "evm-tracing-client" +version = "0.1.0" +dependencies = [ + "evm-tracing-events", + "frame-support", + "hex", + "parity-scale-codec", + "serde", + "sp-core", + "sp-runtime", +] + [[package]] name = "evm-tracing-events" version = "0.1.0" @@ -3542,6 +3555,9 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "hex-literal" diff --git a/crates/evm-tracing-client/Cargo.toml b/crates/evm-tracing-client/Cargo.toml new file mode 100644 index 000000000..f21f7f98c --- /dev/null +++ b/crates/evm-tracing-client/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "evm-tracing-client" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +evm-tracing-events = { path = "../evm-tracing-events" } + +codec = { workspace = true } +frame-support = { workspace = true, features = ["std"] } +hex = { workspace = true, features = ["serde"] } +serde = { workspace = true, features = ["derive"] } +sp-core = { workspace = true } +sp-runtime = { workspace = true, features = ["std"] } diff --git a/crates/evm-tracing-client/src/formatters/blockscout.rs b/crates/evm-tracing-client/src/formatters/blockscout.rs new file mode 100644 index 000000000..1a38b388d --- /dev/null +++ b/crates/evm-tracing-client/src/formatters/blockscout.rs @@ -0,0 +1,22 @@ +//! Blockscout formatter implementation. + +use crate::{ + listeners::call_list::Listener, + types::single::{Call, TransactionTrace}, +}; + +/// Blockscout formatter. +pub struct Formatter; + +impl super::ResponseFormatter for Formatter { + type Listener = Listener; + type Response = TransactionTrace; + + fn format(mut listener: Listener) -> Option { + let entry = listener.entries.pop()?; + + Some(TransactionTrace::CallList( + entry.into_values().map(Call::Blockscout).collect(), + )) + } +} diff --git a/crates/evm-tracing-client/src/formatters/call_tracer.rs b/crates/evm-tracing-client/src/formatters/call_tracer.rs new file mode 100644 index 000000000..7086106fa --- /dev/null +++ b/crates/evm-tracing-client/src/formatters/call_tracer.rs @@ -0,0 +1,253 @@ +//! Call tracer formatter implementation. + +use sp_core::sp_std::cmp::Ordering; + +use crate::{ + listeners::call_list::Listener, + types::{ + block::BlockTransactionTrace, + blockscout::BlockscoutCallInner, + call_tracer::{CallTracerCall, CallTracerInner}, + single::{Call, TransactionTrace}, + CallType, CreateResult, + }, +}; + +/// Call tracer formatter. +pub struct Formatter; + +impl super::ResponseFormatter for Formatter { + type Listener = Listener; + type Response = Vec; + + fn format(listener: Listener) -> Option> { + let mut traces = Vec::new(); + for (eth_tx_index, entry) in listener.entries.iter().enumerate() { + // Skip empty BTreeMaps pushed to `entries`. + // I.e. InvalidNonce or other pallet_evm::runner exits + if entry.is_empty() { + frame_support::log::debug!( + target: "tracing", + "Empty trace entry with transaction index {}, skipping...", eth_tx_index + ); + continue; + } + let mut result: Vec = entry + .iter() + .map(|(_, it)| { + let from = it.from; + let trace_address = it.trace_address.clone(); + let value = it.value; + let gas = it.gas; + let gas_used = it.gas_used; + let inner = it.inner.clone(); + Call::CallTracer(CallTracerCall { + from, + gas, + gas_used, + trace_address: Some(trace_address.clone()), + inner: match inner.clone() { + BlockscoutCallInner::Call { + input, + to, + res, + call_type, + } => CallTracerInner::Call { + call_type: match call_type { + CallType::Call => "CALL".as_bytes().to_vec(), + CallType::CallCode => "CALLCODE".as_bytes().to_vec(), + CallType::DelegateCall => "DELEGATECALL".as_bytes().to_vec(), + CallType::StaticCall => "STATICCALL".as_bytes().to_vec(), + }, + to, + input, + res: res.clone(), + value: Some(value), + }, + BlockscoutCallInner::Create { init, res } => CallTracerInner::Create { + input: init, + error: match res { + CreateResult::Success { .. } => None, + CreateResult::Error { ref error } => Some(error.clone()), + }, + to: match res { + CreateResult::Success { + created_contract_address_hash, + .. + } => Some(created_contract_address_hash), + CreateResult::Error { .. } => None, + }, + output: match res { + CreateResult::Success { + created_contract_code, + .. + } => Some(created_contract_code), + CreateResult::Error { .. } => None, + }, + value, + call_type: "CREATE".as_bytes().to_vec(), + }, + BlockscoutCallInner::SelfDestruct { balance, to } => { + CallTracerInner::SelfDestruct { + value: balance, + to, + call_type: "SELFDESTRUCT".as_bytes().to_vec(), + } + } + }, + calls: Vec::new(), + }) + }) + .collect(); + // Geth's `callTracer` expects a tree of nested calls and we have a stack. + // + // We iterate over the sorted stack, and push each children to it's + // parent (the item which's `trace_address` matches &T[0..T.len()-1]) until there + // is a single item on the list. + // + // The last remaining item is the context call with all it's descendants. I.e. + // + // # Input + // [] + // [0] + // [0,0] + // [0,0,0] + // [0,1] + // [0,1,0] + // [0,1,1] + // [0,1,2] + // [1] + // [1,0] + // + // # Sorted + // [0,0,0] -> pop 0 and push to [0,0] + // [0,1,0] -> pop 0 and push to [0,1] + // [0,1,1] -> pop 1 and push to [0,1] + // [0,1,2] -> pop 2 and push to [0,1] + // [0,0] -> pop 0 and push to [0] + // [0,1] -> pop 1 and push to [0] + // [1,0] -> pop 0 and push to [1] + // [0] -> pop 0 and push to root + // [1] -> pop 1 and push to root + // [] + // + // # Result + // root { + // calls: { + // 0 { 0 { 0 }, 1 { 0, 1, 2 }}, + // 1 { 0 }, + // } + // } + if result.len() > 1 { + // Sort the stack. Assume there is no `Ordering::Equal`, as we are + // sorting by index. + // + // We consider an item to be `Ordering::Less` when: + // - Is closer to the root or + // - Is greater than its sibling. + result.sort_by(|a, b| match (a, b) { + ( + Call::CallTracer(CallTracerCall { + trace_address: Some(a), + .. + }), + Call::CallTracer(CallTracerCall { + trace_address: Some(b), + .. + }), + ) => { + let a_len = a.len(); + let b_len = b.len(); + let sibling_greater_than = |a: &Vec, b: &Vec| -> bool { + for (i, a_value) in a.iter().enumerate() { + match a_value.cmp(&b[i]) { + Ordering::Greater => return true, + Ordering::Less => return false, + Ordering::Equal => continue, + } + } + + false + }; + if b_len > a_len || (a_len == b_len && sibling_greater_than(a, b)) { + Ordering::Less + } else { + Ordering::Greater + } + } + _ => unreachable!(), + }); + // Stack pop-and-push. + while result.len() > 1 { + let mut last = result + .pop() + .expect("result.len() > 1, so pop() necessarily returns an element"); + // Find the parent index. + if let Some(index) = + result + .iter() + .position(|current| match (last.clone(), current) { + ( + Call::CallTracer(CallTracerCall { + trace_address: Some(a), + .. + }), + Call::CallTracer(CallTracerCall { + trace_address: Some(b), + .. + }), + ) => { + &b[..] + == a.get( + 0..a.len().checked_sub(1).expect( + "valid operation due to the check before; qed.", + ), + ) + .expect("non-root element while traversing trace result") + } + _ => unreachable!(), + }) + { + // Remove `trace_address` from result. + if let Call::CallTracer(CallTracerCall { + ref mut trace_address, + .. + }) = last + { + *trace_address = None; + } + // Push the children to parent. + if let Some(Call::CallTracer(CallTracerCall { calls, .. })) = + result.get_mut(index) + { + calls.push(last); + } + } + } + } + // Remove `trace_address` from result. + if let Some(Call::CallTracer(CallTracerCall { trace_address, .. })) = result.get_mut(0) + { + *trace_address = None; + } + if result.len() == 1 { + traces.push(BlockTransactionTrace { + // u32 (eth tx index) is big enough for this truncation to be practically impossible. + tx_position: u32::try_from(eth_tx_index).unwrap(), + // Use default, the correct value will be set upstream + tx_hash: Default::default(), + result: TransactionTrace::CallListNested( + result + .pop() + .expect("result.len() == 1, so pop() necessarily returns this element"), + ), + }); + } + } + if traces.is_empty() { + return None; + } + + Some(traces) + } +} diff --git a/crates/evm-tracing-client/src/formatters/mod.rs b/crates/evm-tracing-client/src/formatters/mod.rs new file mode 100644 index 000000000..e1b069af0 --- /dev/null +++ b/crates/evm-tracing-client/src/formatters/mod.rs @@ -0,0 +1,20 @@ +//! Formatters implementation. + +use evm_tracing_events::Listener; +use serde::Serialize; + +pub mod blockscout; +pub mod call_tracer; +pub mod raw; +pub mod trace_filter; + +/// Response formatter. +pub trait ResponseFormatter { + /// Listener type. + type Listener: Listener; + /// Response type. + type Response: Serialize; + + /// Format. + fn format(listener: Self::Listener) -> Option; +} diff --git a/crates/evm-tracing-client/src/formatters/raw.rs b/crates/evm-tracing-client/src/formatters/raw.rs new file mode 100644 index 000000000..a8eb9f496 --- /dev/null +++ b/crates/evm-tracing-client/src/formatters/raw.rs @@ -0,0 +1,23 @@ +//! Raw formatter implementation. + +use crate::{listeners::raw::Listener, types::single::TransactionTrace}; + +/// Raw formatter. +pub struct Formatter; + +impl super::ResponseFormatter for Formatter { + type Listener = Listener; + type Response = TransactionTrace; + + fn format(listener: Listener) -> Option { + if listener.remaining_memory_usage.is_none() { + None + } else { + Some(TransactionTrace::Raw { + struct_logs: listener.struct_logs, + gas: listener.final_gas.into(), + return_value: listener.return_value, + }) + } + } +} diff --git a/crates/evm-tracing-client/src/formatters/trace_filter.rs b/crates/evm-tracing-client/src/formatters/trace_filter.rs new file mode 100644 index 000000000..8b5315cc8 --- /dev/null +++ b/crates/evm-tracing-client/src/formatters/trace_filter.rs @@ -0,0 +1,131 @@ +//! Trace filter formatter implementation. + +use sp_core::H256; + +use crate::listeners::call_list::Listener; +use crate::types::{ + block::{ + TransactionTrace, TransactionTraceAction, TransactionTraceOutput, TransactionTraceResult, + }, + blockscout::BlockscoutCallInner as CallInner, + CallResult, CreateResult, CreateType, +}; + +/// Trace filter formatter. +pub struct Formatter; + +impl super::ResponseFormatter for Formatter { + type Listener = Listener; + type Response = Vec; + + fn format(listener: Listener) -> Option> { + let mut traces = Vec::new(); + for (eth_tx_index, entry) in listener.entries.iter().enumerate() { + // Skip empty BTreeMaps pushed to `entries`. + // I.e. InvalidNonce or other pallet_evm::runner exits + if entry.is_empty() { + frame_support::log::debug!( + target: "tracing", + "Empty trace entry with transaction index {}, skipping...", eth_tx_index + ); + continue; + } + let mut tx_traces: Vec<_> = entry + .iter() + .map(|(_, trace)| match trace.inner.clone() { + CallInner::Call { + input, + to, + res, + call_type, + } => TransactionTrace { + action: TransactionTraceAction::Call { + call_type, + from: trace.from, + gas: trace.gas, + input, + to, + value: trace.value, + }, + // Can't be known here, must be inserted upstream. + block_hash: H256::default(), + // Can't be known here, must be inserted upstream. + block_number: 0, + output: match res { + CallResult::Output(output) => { + TransactionTraceOutput::Result(TransactionTraceResult::Call { + gas_used: trace.gas_used, + output, + }) + } + CallResult::Error(error) => TransactionTraceOutput::Error(error), + }, + subtraces: trace.subtraces, + trace_address: trace.trace_address.clone(), + // Can't be known here, must be inserted upstream. + transaction_hash: H256::default(), + // u32 (eth tx index) is big enough for this truncation to be practically impossible. + transaction_position: u32::try_from(eth_tx_index).unwrap(), + }, + CallInner::Create { init, res } => { + TransactionTrace { + action: TransactionTraceAction::Create { + creation_method: CreateType::Create, + from: trace.from, + gas: trace.gas, + init, + value: trace.value, + }, + // Can't be known here, must be inserted upstream. + block_hash: H256::default(), + // Can't be known here, must be inserted upstream. + block_number: 0, + output: match res { + CreateResult::Success { + created_contract_address_hash, + created_contract_code, + } => { + TransactionTraceOutput::Result(TransactionTraceResult::Create { + gas_used: trace.gas_used, + code: created_contract_code, + address: created_contract_address_hash, + }) + } + CreateResult::Error { error } => { + TransactionTraceOutput::Error(error) + } + }, + subtraces: trace.subtraces, + trace_address: trace.trace_address.clone(), + // Can't be known here, must be inserted upstream. + transaction_hash: H256::default(), + // u32 (eth tx index) is big enough for this truncation to be practically impossible. + transaction_position: u32::try_from(eth_tx_index).unwrap(), + } + } + CallInner::SelfDestruct { balance, to } => TransactionTrace { + action: TransactionTraceAction::Suicide { + address: trace.from, + balance, + refund_address: to, + }, + // Can't be known here, must be inserted upstream. + block_hash: H256::default(), + // Can't be known here, must be inserted upstream. + block_number: 0, + output: TransactionTraceOutput::Result(TransactionTraceResult::Suicide), + subtraces: trace.subtraces, + trace_address: trace.trace_address.clone(), + // Can't be known here, must be inserted upstream. + transaction_hash: H256::default(), + // u32 (eth tx index) is big enough for this truncation to be practically impossible. + transaction_position: u32::try_from(eth_tx_index).unwrap(), + }, + }) + .collect(); + + traces.append(&mut tx_traces); + } + Some(traces) + } +} diff --git a/crates/evm-tracing-client/src/lib.rs b/crates/evm-tracing-client/src/lib.rs new file mode 100644 index 000000000..2fcbf4f2a --- /dev/null +++ b/crates/evm-tracing-client/src/lib.rs @@ -0,0 +1,6 @@ +//! The client-side related implementation of EVM tracing logic. + +pub mod formatters; +pub mod listeners; +mod serialization; +pub mod types; diff --git a/crates/evm-tracing-client/src/listeners/call_list.rs b/crates/evm-tracing-client/src/listeners/call_list.rs new file mode 100644 index 000000000..2c903560b --- /dev/null +++ b/crates/evm-tracing-client/src/listeners/call_list.rs @@ -0,0 +1,1098 @@ +//! Call list listener. + +use evm_tracing_events::{ + runtime::{Capture, ExitError, ExitReason, ExitSucceed}, + Event, EvmEvent, GasometerEvent, Listener as ListenerT, RuntimeEvent, StepEventFilter, +}; +use sp_core::{sp_std::collections::btree_map::BTreeMap, H160, U256}; + +use crate::types::{ + blockscout::{BlockscoutCall as Call, BlockscoutCallInner as CallInner}, + CallResult, CallType, ContextType, CreateResult, +}; + +/// Enum of the different "modes" of tracer for multiple runtime versions and +/// the kind of EVM events that are emitted. +enum TracingVersion { + /// The first event of the transaction is `EvmEvent::TransactX`. It goes along other events + /// such as `EvmEvent::Exit`. All contexts should have clear start/end boundaries. + EarlyTransact, + /// Older version in which the events above didn't existed. + /// It means that we cannot rely on those events to perform any task, and must rely only + /// on other events. + Legacy, +} + +/// Listener. +pub struct Listener { + /// Version of the tracing. + /// Defaults to legacy, and switch to a more modern version if recently added events are + /// received. + version: TracingVersion, + /// Transaction cost that must be added to the first context cost. + transaction_cost: u64, + /// Final logs. + pub entries: Vec>, + /// Next index to use. + entries_next_index: u32, + /// Stack of contexts with data to keep between events. + context_stack: Vec, + /// Type of the next call. + /// By default is None and corresponds to the root call, which + /// can be determined using the `is_static` field of the `Call` event. + /// Then by looking at call traps events we can set this value to the correct + /// call type, to be used when the following `Call` event is received. + call_type: Option, + /// When `EvmEvent::TransactX` is received it creates its own context. However it will usually + /// be followed by an `EvmEvent::Call/Create` that will also create a context, which must be + /// prevented. It must however not be skipped if `EvmEvent::TransactX` was not received + /// (in legacy mode). + skip_next_context: bool, + /// To handle `EvmEvent::Exit` no emitted by previous runtimes versions, + /// entries are not inserted directly in `self.entries`. + /// `pending_entries`: Vec<(u32, Call)>, + /// See `RuntimeEvent::StepResult` event explanatioins. + step_result_entry: Option<(u32, Call)>, + /// When tracing a block `Event::CallListNew` is emitted before each Ethereum transaction is + /// processed. Since we use that event to **finish** the transaction, we must ignore the first + /// one. + call_list_first_transaction: bool, + /// True if only the `GasometerEvent::RecordTransaction` event has been received. + /// Allow to correctly handle transactions that cannot pay for the tx data in Legacy mode. + record_transaction_event_only: bool, +} + +/// Context. +struct Context { + /// Entries index. + entries_index: u32, + /// Context type. + context_type: ContextType, + /// From. + from: H160, + /// Trace address. + trace_address: Vec, + /// Subtraces. + subtraces: u32, + /// Value. + value: U256, + /// Gas. + gas: u64, + /// Start gas. + start_gas: Option, + /// Data. + data: Vec, + /// To. + to: H160, +} + +impl Default for Listener { + fn default() -> Self { + Self { + version: TracingVersion::Legacy, + transaction_cost: 0, + entries: vec![], + entries_next_index: 0, + context_stack: vec![], + call_type: None, + step_result_entry: None, + skip_next_context: false, + call_list_first_transaction: true, + record_transaction_event_only: false, + } + } +} + +impl Listener { + /// Run closure. + pub fn using R>(&mut self, f: F) -> R { + evm_tracing_events::using(self, f) + } + + /// Called at the end of each transaction when tracing. + /// Allow to insert the pending entries regardless of which runtime version + /// is used (with or without `EvmEvent::Exit`). + pub fn finish_transaction(&mut self) { + // remove any leftover context + let mut context_stack = vec![]; + core::mem::swap(&mut self.context_stack, &mut context_stack); + + // if there is a left over there have been an early exit. + // we generate an entry from it and discord any inner context. + if let Some(context) = context_stack.into_iter().next() { + let mut gas_used = context.start_gas.unwrap_or(0).saturating_sub(context.gas); + if context.entries_index == 0 { + gas_used = gas_used.saturating_add(self.transaction_cost); + } + + let entry = match context.context_type { + ContextType::Call(call_type) => { + let res = CallResult::Error( + b"early exit (out of gas, stack overflow, direct call to precompile, ...)" + .to_vec(), + ); + Call { + from: context.from, + trace_address: context.trace_address, + subtraces: context.subtraces, + value: context.value, + gas: context.gas.into(), + gas_used: gas_used.into(), + inner: CallInner::Call { + call_type, + to: context.to, + input: context.data, + res, + }, + } + } + ContextType::Create => { + let res = CreateResult::Error { + error: b"early exit (out of gas, stack overflow, direct call to precompile, ...)".to_vec(), + }; + + Call { + value: context.value, + trace_address: context.trace_address, + subtraces: context.subtraces, + gas: context.gas.into(), + gas_used: gas_used.into(), + from: context.from, + inner: CallInner::Create { + init: context.data, + res, + }, + } + } + }; + + self.insert_entry(context.entries_index, entry); + // Since only this context/entry is kept, we need update entries_next_index too. + self.entries_next_index = context.entries_index.saturating_add(1); + } + // However if the transaction had a too low gas limit to pay for the data cost itself, + // and `EvmEvent::Exit` is not emitted in **Legacy mode**, then it has never produced any + // context (and exited **early in the transaction**). + else if self.record_transaction_event_only { + let res = CallResult::Error( + b"transaction could not pay its own data cost (impossible to gather more info)" + .to_vec(), + ); + + let entry = Call { + from: H160::repeat_byte(0), + trace_address: vec![], + subtraces: 0, + value: 0.into(), + gas: 0.into(), + gas_used: 0.into(), + inner: CallInner::Call { + call_type: CallType::Call, + to: H160::repeat_byte(0), + input: vec![], + res, + }, + }; + + self.insert_entry(self.entries_next_index, entry); + self.entries_next_index = self.entries_next_index.saturating_add(1); + } + } + + /// Gasometer event. + pub fn gasometer_event(&mut self, event: GasometerEvent) { + match event { + GasometerEvent::RecordCost { snapshot, .. } + | GasometerEvent::RecordDynamicCost { snapshot, .. } + | GasometerEvent::RecordStipend { snapshot, .. } => { + if let Some(context) = self.context_stack.last_mut() { + if context.start_gas.is_none() { + context.start_gas = Some(snapshot.gas()); + } + context.gas = snapshot.gas(); + } + } + GasometerEvent::RecordTransaction { cost, .. } => { + self.transaction_cost = cost; + self.record_transaction_event_only = true; + } + // We ignore other kinds of message if any (new ones may be added in the future). + #[allow(unreachable_patterns)] + _ => (), + } + } + + /// Runtime event. + pub fn runtime_event(&mut self, event: RuntimeEvent) { + match event { + RuntimeEvent::StepResult { + result: Err(Capture::Trap(opcode)), + .. + } => { + if let Some(ContextType::Call(call_type)) = ContextType::from(opcode) { + self.call_type = Some(call_type) + } + } + RuntimeEvent::StepResult { + result: Err(Capture::Exit(reason)), + return_value, + } => { + if let Some((key, entry)) = self.pop_context_to_entry(reason, return_value) { + match self.version { + TracingVersion::Legacy => { + // In Legacy mode we directly insert the entry. + self.insert_entry(key, entry); + } + TracingVersion::EarlyTransact => { + // In EarlyTransact mode this context must be used if this event is + // emitted. However the context of `EvmEvent::Exit` must be used if + // `StepResult` is skipped. For that reason we store this generated + // entry in a temporary value, and deal with it in `EvmEvent::Exit` that + // will be called in all cases. + self.step_result_entry = Some((key, entry)); + } + } + } + } + // We ignore other kinds of message if any (new ones may be added in the future). + #[allow(unreachable_patterns)] + _ => (), + } + } + + /// EVM event. + pub fn evm_event(&mut self, event: EvmEvent) { + match event { + EvmEvent::TransactCall { + caller, + address, + value, + data, + .. + } => { + self.record_transaction_event_only = false; + self.version = TracingVersion::EarlyTransact; + self.context_stack.push(Context { + entries_index: self.entries_next_index, + + context_type: ContextType::Call(CallType::Call), + + from: caller, + trace_address: vec![], + subtraces: 0, + value, + + gas: 0, + start_gas: None, + + data, + to: address, + }); + + self.entries_next_index = self.entries_next_index.saturating_add(1); + self.skip_next_context = true; + } + + EvmEvent::TransactCreate { + caller, + value, + init_code, + address, + .. + } + | EvmEvent::TransactCreate2 { + caller, + value, + init_code, + address, + .. + } => { + self.record_transaction_event_only = false; + self.version = TracingVersion::EarlyTransact; + self.context_stack.push(Context { + entries_index: self.entries_next_index, + + context_type: ContextType::Create, + + from: caller, + trace_address: vec![], + subtraces: 0, + value, + + gas: 0, + start_gas: None, + + data: init_code, + to: address, + }); + + self.entries_next_index = self.entries_next_index.saturating_add(1); + self.skip_next_context = true; + } + + EvmEvent::Call { + code_address, + input, + is_static, + context, + .. + } => { + self.record_transaction_event_only = false; + + let call_type = match (self.call_type, is_static) { + (None, true) => CallType::StaticCall, + (None, false) => CallType::Call, + (Some(call_type), _) => call_type, + }; + + if !self.skip_next_context { + let trace_address = if let Some(context) = self.context_stack.last_mut() { + let mut trace_address = context.trace_address.clone(); + trace_address.push(context.subtraces); + context.subtraces = context.subtraces.saturating_add(1); + trace_address + } else { + vec![] + }; + + // For subcalls we want to have "from" always be the parent context address + // instead of `context.caller`, since the latter will not have the correct + // value inside a DelegateCall. + let from = if let Some(parent_context) = self.context_stack.last() { + parent_context.to + } else { + context.caller + }; + + self.context_stack.push(Context { + entries_index: self.entries_next_index, + + context_type: ContextType::Call(call_type), + + from, + trace_address, + subtraces: 0, + value: context.apparent_value, + + gas: 0, + start_gas: None, + + data: input.to_vec(), + to: code_address, + }); + + self.entries_next_index = self.entries_next_index.saturating_add(1); + } else { + self.skip_next_context = false; + } + } + + EvmEvent::Create { + caller, + address, + value, + init_code, + .. + } => { + self.record_transaction_event_only = false; + + if !self.skip_next_context { + let trace_address = if let Some(context) = self.context_stack.last_mut() { + let mut trace_address = context.trace_address.clone(); + trace_address.push(context.subtraces); + context.subtraces = context.subtraces.saturating_add(1); + trace_address + } else { + vec![] + }; + + self.context_stack.push(Context { + entries_index: self.entries_next_index, + + context_type: ContextType::Create, + + from: caller, + trace_address, + subtraces: 0, + value, + + gas: 0, + start_gas: None, + + data: init_code.to_vec(), + to: address, + }); + + self.entries_next_index = self.entries_next_index.saturating_add(1); + } else { + self.skip_next_context = false; + } + } + EvmEvent::Suicide { + address, + target, + balance, + } => { + let trace_address = if let Some(context) = self.context_stack.last_mut() { + let mut trace_address = context.trace_address.clone(); + trace_address.push(context.subtraces); + context.subtraces = context.subtraces.saturating_add(1); + trace_address + } else { + vec![] + }; + + self.insert_entry( + self.entries_next_index, + Call { + from: address, + trace_address, + subtraces: 0, + value: 0.into(), + gas: 0.into(), + gas_used: 0.into(), + inner: CallInner::SelfDestruct { + to: target, + balance, + }, + }, + ); + self.entries_next_index = self.entries_next_index.saturating_add(1); + } + EvmEvent::Exit { + reason, + return_value, + } => { + // We know we're in `TracingVersion::EarlyTransact` mode. + + self.record_transaction_event_only = false; + + let entry = self + .step_result_entry + .take() + .or_else(|| self.pop_context_to_entry(reason, return_value)); + + if let Some((key, entry)) = entry { + self.insert_entry(key, entry); + } + } + EvmEvent::PrecompileSubcall { .. } => { + // In a precompile subcall there is no CALL opcode result to observe, thus + // we need this new event. Precompile subcall might use non-standard call + // behavior (like batch precompile does) thus we simply consider this a call. + self.call_type = Some(CallType::Call); + } + + // We ignore other kinds of message if any (new ones may be added in the future). + #[allow(unreachable_patterns)] + _ => (), + } + } + + /// Insert entry. + fn insert_entry(&mut self, key: u32, entry: Call) { + if let Some(ref mut last) = self.entries.last_mut() { + last.insert(key, entry); + } else { + let mut btree_map = BTreeMap::new(); + btree_map.insert(key, entry); + self.entries.push(btree_map); + } + } + + /// Pop context to entry. + fn pop_context_to_entry( + &mut self, + reason: ExitReason, + return_value: Vec, + ) -> Option<(u32, Call)> { + if let Some(context) = self.context_stack.pop() { + let mut gas_used = context.start_gas.unwrap_or(0).saturating_sub(context.gas); + if context.entries_index == 0 { + gas_used = gas_used.saturating_add(self.transaction_cost); + } + + Some(( + context.entries_index, + match context.context_type { + ContextType::Call(call_type) => { + let res = match &reason { + ExitReason::Succeed(ExitSucceed::Returned) => { + CallResult::Output(return_value.to_vec()) + } + ExitReason::Succeed(_) => CallResult::Output(vec![]), + ExitReason::Error(error) => CallResult::Error(error_message(error)), + + ExitReason::Revert(_) => { + CallResult::Error(b"execution reverted".to_vec()) + } + ExitReason::Fatal(_) => CallResult::Error(vec![]), + }; + + Call { + from: context.from, + trace_address: context.trace_address, + subtraces: context.subtraces, + value: context.value, + gas: context.gas.into(), + gas_used: gas_used.into(), + inner: CallInner::Call { + call_type, + to: context.to, + input: context.data, + res, + }, + } + } + ContextType::Create => { + let res = match &reason { + ExitReason::Succeed(_) => CreateResult::Success { + created_contract_address_hash: context.to, + created_contract_code: return_value.to_vec(), + }, + ExitReason::Error(error) => CreateResult::Error { + error: error_message(error), + }, + ExitReason::Revert(_) => CreateResult::Error { + error: b"execution reverted".to_vec(), + }, + ExitReason::Fatal(_) => CreateResult::Error { error: vec![] }, + }; + + Call { + value: context.value, + trace_address: context.trace_address, + subtraces: context.subtraces, + gas: context.gas.into(), + gas_used: gas_used.into(), + from: context.from, + inner: CallInner::Create { + init: context.data, + res, + }, + } + } + }, + )) + } else { + None + } + } +} + +/// Error message. +fn error_message(error: &ExitError) -> Vec { + match error { + ExitError::StackUnderflow => "stack underflow", + ExitError::StackOverflow => "stack overflow", + ExitError::InvalidJump => "invalid jump", + ExitError::InvalidRange => "invalid range", + ExitError::DesignatedInvalid => "designated invalid", + ExitError::CallTooDeep => "call too deep", + ExitError::CreateCollision => "create collision", + ExitError::CreateContractLimit => "create contract limit", + ExitError::OutOfOffset => "out of offset", + ExitError::OutOfGas => "out of gas", + ExitError::OutOfFund => "out of funds", + ExitError::Other(err) => err, + _ => "unexpected error", + } + .as_bytes() + .to_vec() +} + +impl ListenerT for Listener { + fn event(&mut self, event: Event) { + match event { + Event::Gasometer(gasometer_event) => self.gasometer_event(gasometer_event), + Event::Runtime(runtime_event) => self.runtime_event(runtime_event), + Event::Evm(evm_event) => self.evm_event(evm_event), + Event::CallListNew() => { + if !self.call_list_first_transaction { + self.finish_transaction(); + self.skip_next_context = false; + self.entries.push(BTreeMap::new()); + } else { + self.call_list_first_transaction = false; + } + } + }; + } + + fn step_event_filter(&self) -> StepEventFilter { + StepEventFilter { + enable_memory: false, + enable_stack: false, + } + } +} + +#[cfg(test)] +#[allow(unused)] +mod tests { + use evm_tracing_events::{ + evm::CreateScheme, + gasometer::Snapshot, + runtime::{Memory, Stack}, + Context as EvmContext, + }; + use sp_core::H256; + + use super::*; + + enum TestEvmEvent { + Call, + Create, + Suicide, + Exit, + TransactCall, + TransactCreate, + TransactCreate2, + } + + enum TestRuntimeEvent { + Step, + StepResult, + SLoad, + SStore, + } + + #[allow(clippy::enum_variant_names)] + enum TestGasometerEvent { + RecordCost, + RecordRefund, + RecordStipend, + RecordDynamicCost, + RecordTransaction, + } + + fn test_context() -> EvmContext { + EvmContext { + address: H160::default(), + caller: H160::default(), + apparent_value: U256::zero(), + } + } + + fn test_create_scheme() -> CreateScheme { + CreateScheme::Legacy { + caller: H160::default(), + } + } + + fn test_stack() -> Option { + None + } + + fn test_memory() -> Option { + None + } + + fn test_snapshot() -> Snapshot { + Snapshot { + gas_limit: 0u64, + memory_gas: 0u64, + used_gas: 0u64, + refunded_gas: 0i64, + } + } + + fn test_emit_evm_event( + event_type: TestEvmEvent, + is_static: bool, + exit_reason: Option, + ) -> EvmEvent { + match event_type { + TestEvmEvent::Call => EvmEvent::Call { + code_address: H160::default(), + transfer: None, + input: Vec::new(), + target_gas: None, + is_static, + context: test_context(), + }, + TestEvmEvent::Create => EvmEvent::Create { + caller: H160::default(), + address: H160::default(), + scheme: test_create_scheme(), + value: U256::zero(), + init_code: Vec::new(), + target_gas: None, + }, + TestEvmEvent::Suicide => EvmEvent::Suicide { + address: H160::default(), + target: H160::default(), + balance: U256::zero(), + }, + TestEvmEvent::Exit => EvmEvent::Exit { + reason: exit_reason.unwrap(), + return_value: Vec::new(), + }, + TestEvmEvent::TransactCall => EvmEvent::TransactCall { + caller: H160::default(), + address: H160::default(), + value: U256::zero(), + data: Vec::new(), + gas_limit: 0u64, + }, + TestEvmEvent::TransactCreate => EvmEvent::TransactCreate { + caller: H160::default(), + value: U256::zero(), + init_code: Vec::new(), + gas_limit: 0u64, + address: H160::default(), + }, + TestEvmEvent::TransactCreate2 => EvmEvent::TransactCreate2 { + caller: H160::default(), + value: U256::zero(), + init_code: Vec::new(), + salt: H256::default(), + gas_limit: 0u64, + address: H160::default(), + }, + } + } + + fn test_emit_runtime_event(event_type: TestRuntimeEvent) -> RuntimeEvent { + match event_type { + TestRuntimeEvent::Step => RuntimeEvent::Step { + context: test_context(), + opcode: Vec::new(), + position: Ok(0u64), + stack: test_stack(), + memory: test_memory(), + }, + TestRuntimeEvent::StepResult => RuntimeEvent::StepResult { + result: Ok(()), + return_value: Vec::new(), + }, + TestRuntimeEvent::SLoad => RuntimeEvent::SLoad { + address: H160::default(), + index: H256::default(), + value: H256::default(), + }, + TestRuntimeEvent::SStore => RuntimeEvent::SStore { + address: H160::default(), + index: H256::default(), + value: H256::default(), + }, + } + } + + fn test_emit_gasometer_event(event_type: TestGasometerEvent) -> GasometerEvent { + match event_type { + TestGasometerEvent::RecordCost => GasometerEvent::RecordCost { + cost: 0u64, + snapshot: test_snapshot(), + }, + TestGasometerEvent::RecordRefund => GasometerEvent::RecordRefund { + refund: 0i64, + snapshot: test_snapshot(), + }, + TestGasometerEvent::RecordStipend => GasometerEvent::RecordStipend { + stipend: 0u64, + snapshot: test_snapshot(), + }, + TestGasometerEvent::RecordDynamicCost => GasometerEvent::RecordDynamicCost { + gas_cost: 0u64, + memory_gas: 0u64, + gas_refund: 0i64, + snapshot: test_snapshot(), + }, + TestGasometerEvent::RecordTransaction => GasometerEvent::RecordTransaction { + cost: 0u64, + snapshot: test_snapshot(), + }, + } + } + + fn do_transact_call_event(listener: &mut Listener) { + listener.evm_event(test_emit_evm_event(TestEvmEvent::TransactCall, false, None)); + } + + fn do_transact_create_event(listener: &mut Listener) { + listener.evm_event(test_emit_evm_event( + TestEvmEvent::TransactCreate, + false, + None, + )); + } + + fn do_gasometer_event(listener: &mut Listener) { + listener.gasometer_event(test_emit_gasometer_event( + TestGasometerEvent::RecordTransaction, + )); + } + + fn do_exit_event(listener: &mut Listener) { + listener.evm_event(test_emit_evm_event( + TestEvmEvent::Exit, + false, + Some(ExitReason::Error(ExitError::OutOfGas)), + )); + } + + fn do_evm_call_event(listener: &mut Listener) { + listener.evm_event(test_emit_evm_event(TestEvmEvent::Call, false, None)); + } + + fn do_evm_create_event(listener: &mut Listener) { + listener.evm_event(test_emit_evm_event(TestEvmEvent::Create, false, None)); + } + + fn do_evm_suicide_event(listener: &mut Listener) { + listener.evm_event(test_emit_evm_event(TestEvmEvent::Suicide, false, None)); + } + + fn do_runtime_step_event(listener: &mut Listener) { + listener.runtime_event(test_emit_runtime_event(TestRuntimeEvent::Step)); + } + + fn do_runtime_step_result_event(listener: &mut Listener) { + listener.runtime_event(test_emit_runtime_event(TestRuntimeEvent::StepResult)); + } + + // Call context + + // Early exit on TransactionCost. + #[test] + fn call_early_exit_tx_cost() { + let mut listener = Listener::default(); + do_transact_call_event(&mut listener); + do_gasometer_event(&mut listener); + do_exit_event(&mut listener); + listener.finish_transaction(); + assert_eq!(listener.entries.len(), 1); + assert_eq!(listener.entries[0].len(), 1); + } + + // Early exit somewhere between the first callstack event and stepping the bytecode. + // I.e. precompile call. + #[test] + fn call_early_exit_before_runtime() { + let mut listener = Listener::default(); + do_transact_call_event(&mut listener); + do_gasometer_event(&mut listener); + do_evm_call_event(&mut listener); + do_exit_event(&mut listener); + listener.finish_transaction(); + assert_eq!(listener.entries.len(), 1); + assert_eq!(listener.entries[0].len(), 1); + } + + // Exit after Step without StepResult. + #[test] + fn call_step_without_step_result() { + let mut listener = Listener::default(); + do_transact_call_event(&mut listener); + do_gasometer_event(&mut listener); + do_evm_call_event(&mut listener); + do_runtime_step_event(&mut listener); + do_exit_event(&mut listener); + listener.finish_transaction(); + assert_eq!(listener.entries.len(), 1); + assert_eq!(listener.entries[0].len(), 1); + } + + // Exit after StepResult. + #[test] + fn call_step_result() { + let mut listener = Listener::default(); + do_transact_call_event(&mut listener); + do_gasometer_event(&mut listener); + do_evm_call_event(&mut listener); + do_runtime_step_event(&mut listener); + do_runtime_step_result_event(&mut listener); + do_exit_event(&mut listener); + listener.finish_transaction(); + assert_eq!(listener.entries.len(), 1); + assert_eq!(listener.entries[0].len(), 1); + } + + // Suicide. + #[test] + fn call_suicide() { + let mut listener = Listener::default(); + do_transact_call_event(&mut listener); + do_gasometer_event(&mut listener); + do_evm_call_event(&mut listener); + do_runtime_step_event(&mut listener); + do_evm_suicide_event(&mut listener); + do_exit_event(&mut listener); + listener.finish_transaction(); + assert_eq!(listener.entries.len(), 1); + assert_eq!(listener.entries[0].len(), 2); + } + + // Create context + + // Early exit on TransactionCost. + #[test] + fn create_early_exit_tx_cost() { + let mut listener = Listener::default(); + do_transact_create_event(&mut listener); + do_gasometer_event(&mut listener); + do_exit_event(&mut listener); + listener.finish_transaction(); + assert_eq!(listener.entries.len(), 1); + assert_eq!(listener.entries[0].len(), 1); + } + + // Early exit somewhere between the first callstack event and stepping the bytecode + // I.e. precompile call.. + #[test] + fn create_early_exit_before_runtime() { + let mut listener = Listener::default(); + do_transact_create_event(&mut listener); + do_gasometer_event(&mut listener); + do_evm_create_event(&mut listener); + do_exit_event(&mut listener); + listener.finish_transaction(); + assert_eq!(listener.entries.len(), 1); + assert_eq!(listener.entries[0].len(), 1); + } + + // Exit after Step without StepResult. + #[test] + fn create_step_without_step_result() { + let mut listener = Listener::default(); + do_transact_create_event(&mut listener); + do_gasometer_event(&mut listener); + do_evm_create_event(&mut listener); + do_runtime_step_event(&mut listener); + do_exit_event(&mut listener); + listener.finish_transaction(); + assert_eq!(listener.entries.len(), 1); + assert_eq!(listener.entries[0].len(), 1); + } + + // Exit after StepResult. + #[test] + fn create_step_result() { + let mut listener = Listener::default(); + do_transact_create_event(&mut listener); + do_gasometer_event(&mut listener); + do_evm_create_event(&mut listener); + do_runtime_step_event(&mut listener); + do_runtime_step_result_event(&mut listener); + do_exit_event(&mut listener); + listener.finish_transaction(); + assert_eq!(listener.entries.len(), 1); + assert_eq!(listener.entries[0].len(), 1); + } + + // Call Context Nested + + // Nested call early exit before stepping. + #[test] + fn nested_call_early_exit_before_runtime() { + let mut listener = Listener::default(); + // Main + do_transact_call_event(&mut listener); + do_gasometer_event(&mut listener); + do_evm_call_event(&mut listener); + do_runtime_step_event(&mut listener); + do_runtime_step_result_event(&mut listener); + // Nested + do_evm_call_event(&mut listener); + do_exit_event(&mut listener); + // Main exit + do_exit_event(&mut listener); + listener.finish_transaction(); + assert_eq!(listener.entries.len(), 1); + assert_eq!(listener.entries[0].len(), 2); + } + + // Nested exit before step result. + #[test] + fn nested_call_without_step_result() { + let mut listener = Listener::default(); + // Main + do_transact_call_event(&mut listener); + do_gasometer_event(&mut listener); + do_evm_call_event(&mut listener); + do_runtime_step_event(&mut listener); + do_runtime_step_result_event(&mut listener); + // Nested + do_evm_call_event(&mut listener); + do_runtime_step_event(&mut listener); + do_exit_event(&mut listener); + // Main exit + do_exit_event(&mut listener); + listener.finish_transaction(); + assert_eq!(listener.entries.len(), 1); + assert_eq!(listener.entries[0].len(), 2); + } + + // Nested exit. + #[test] + fn nested_call_step_result() { + let depth = 5; + let mut listener = Listener::default(); + // Main + do_transact_call_event(&mut listener); + do_gasometer_event(&mut listener); + do_evm_call_event(&mut listener); + do_runtime_step_event(&mut listener); + do_runtime_step_result_event(&mut listener); + // 5 nested calls + for d in 0..depth { + do_evm_call_event(&mut listener); + do_runtime_step_event(&mut listener); + do_runtime_step_result_event(&mut listener); + do_exit_event(&mut listener); + } + // Main exit + do_exit_event(&mut listener); + listener.finish_transaction(); + assert_eq!(listener.entries.len(), 1); + assert_eq!(listener.entries[0].len(), depth + 1); + } + + // Call + Create mixed subnesting. + + #[test] + fn subnested_call_and_create_mixbag() { + let depth = 5; + let subdepth = 10; + let mut listener = Listener::default(); + // Main + do_transact_call_event(&mut listener); + do_gasometer_event(&mut listener); + do_evm_call_event(&mut listener); + do_runtime_step_event(&mut listener); + do_runtime_step_result_event(&mut listener); + // 5 nested call/creates, each with 10 nested call/creates + for d in 0..depth { + if d % 2 == 0 { + do_evm_call_event(&mut listener); + } else { + do_evm_create_event(&mut listener); + } + do_runtime_step_event(&mut listener); + do_runtime_step_result_event(&mut listener); + for s in 0..subdepth { + // Some mixed call/create and early exits. + if s % 2 == 0 { + do_evm_call_event(&mut listener); + } else { + do_evm_create_event(&mut listener); + } + if s % 3 == 0 { + do_runtime_step_event(&mut listener); + do_runtime_step_result_event(&mut listener); + } + do_exit_event(&mut listener); + } + // Nested exit + do_exit_event(&mut listener); + } + // Main exit + do_exit_event(&mut listener); + listener.finish_transaction(); + assert_eq!(listener.entries.len(), 1); + // Each nested call contains 11 elements in the callstack (main + 10 subcalls). + // There are 5 main nested calls for a total of 56 elements in the callstack: 1 main + 55 nested. + assert_eq!(listener.entries[0].len(), (depth * (subdepth + 1)) + 1); + } +} diff --git a/crates/evm-tracing-client/src/listeners/mod.rs b/crates/evm-tracing-client/src/listeners/mod.rs new file mode 100644 index 000000000..eb1328dd1 --- /dev/null +++ b/crates/evm-tracing-client/src/listeners/mod.rs @@ -0,0 +1,4 @@ +//! Listeners implementation. + +pub mod call_list; +pub mod raw; diff --git a/crates/evm-tracing-client/src/listeners/raw.rs b/crates/evm-tracing-client/src/listeners/raw.rs new file mode 100644 index 000000000..a6839907f --- /dev/null +++ b/crates/evm-tracing-client/src/listeners/raw.rs @@ -0,0 +1,340 @@ +//! Raw listener. + +use evm_tracing_events::{ + runtime::Capture, runtime::ExitReason, Event, GasometerEvent, Listener as ListenerT, + RuntimeEvent, StepEventFilter, +}; +use sp_core::{sp_std::collections::btree_map::BTreeMap, H160, H256}; + +use crate::types::{convert_memory, single::RawStepLog, ContextType}; + +/// Listener. +#[derive(Debug)] +pub struct Listener { + /// Disable storage flag. + disable_storage: bool, + /// Disable memory flag. + disable_memory: bool, + /// Disable stack flag. + disable_stack: bool, + /// New context flag. + new_context: bool, + /// Context stack. + context_stack: Vec, + /// Logs. + pub struct_logs: Vec, + /// Return value. + pub return_value: Vec, + /// Final gas. + pub final_gas: u64, + /// Remaining memory usage. + pub remaining_memory_usage: Option, +} + +/// Context +#[derive(Debug)] +struct Context { + /// Storage cache. + storage_cache: BTreeMap, + /// Address. + address: H160, + /// Current step. + current_step: Option, + /// Global storage changes. + global_storage_changes: BTreeMap>, +} + +/// Step. +#[derive(Debug)] +struct Step { + /// Current opcode. + opcode: Vec, + /// Depth of the context. + depth: usize, + /// Remaining gas. + gas: u64, + /// Gas cost of the following opcode. + gas_cost: u64, + /// Program counter position. + position: usize, + /// EVM memory copy (if not disabled). + memory: Option>, + /// EVM stack copy (if not disabled). + stack: Option>, +} + +impl Listener { + /// New listener. + pub fn new( + disable_storage: bool, + disable_memory: bool, + disable_stack: bool, + raw_max_memory_usage: usize, + ) -> Self { + Self { + disable_storage, + disable_memory, + disable_stack, + remaining_memory_usage: Some(raw_max_memory_usage), + struct_logs: vec![], + return_value: vec![], + final_gas: 0, + new_context: false, + context_stack: vec![], + } + } + + /// Run closure. + pub fn using R>(&mut self, f: F) -> R { + evm_tracing_events::using(self, f) + } + + /// Gasometer event. + pub fn gasometer_event(&mut self, event: GasometerEvent) { + match event { + GasometerEvent::RecordTransaction { cost, .. } => { + // First event of a transaction. + // Next step will be the first context. + self.new_context = true; + self.final_gas = cost; + } + GasometerEvent::RecordCost { cost, snapshot } => { + if let Some(context) = self.context_stack.last_mut() { + // Register opcode cost. (ignore costs not between Step and StepResult) + if let Some(step) = &mut context.current_step { + step.gas = snapshot.gas(); + step.gas_cost = cost; + } + + self.final_gas = snapshot.used_gas; + } + } + GasometerEvent::RecordDynamicCost { + gas_cost, snapshot, .. + } => { + if let Some(context) = self.context_stack.last_mut() { + // Register opcode cost. (ignore costs not between Step and StepResult) + if let Some(step) = &mut context.current_step { + step.gas = snapshot.gas(); + step.gas_cost = gas_cost; + } + + self.final_gas = snapshot.used_gas; + } + } + // We ignore other kinds of message if any (new ones may be added in the future). + #[allow(unreachable_patterns)] + _ => (), + } + } + + /// Runtime event. + pub fn runtime_event(&mut self, event: RuntimeEvent) { + match event { + RuntimeEvent::Step { + context, + opcode, + position, + stack, + memory, + } => { + // Create a context if needed. + if self.new_context { + self.new_context = false; + + self.context_stack.push(Context { + storage_cache: BTreeMap::new(), + address: context.address, + current_step: None, + global_storage_changes: BTreeMap::new(), + }); + } + + let depth = self.context_stack.len(); + + // Ignore steps outside of any context (shouldn't even be possible). + if let Some(context) = self.context_stack.last_mut() { + context.current_step = Some(Step { + opcode, + depth, + gas: 0, // 0 for now, will add with gas events + gas_cost: 0, // 0 for now, will add with gas events + // usize (position) is big enough for this truncation to be practically impossible. + position: usize::try_from(*position.as_ref().unwrap_or(&0)).unwrap(), + memory: if self.disable_memory { + None + } else { + let memory = memory.expect("memory data to not be filtered out"); + + self.remaining_memory_usage = self + .remaining_memory_usage + .and_then(|inner| inner.checked_sub(memory.data.len())); + + if self.remaining_memory_usage.is_none() { + return; + } + + Some(memory.data.clone()) + }, + stack: if self.disable_stack { + None + } else { + let stack = stack.expect("stack data to not be filtered out"); + + self.remaining_memory_usage = self + .remaining_memory_usage + .and_then(|inner| inner.checked_sub(stack.data.len())); + + if self.remaining_memory_usage.is_none() { + return; + } + + Some(stack.data.clone()) + }, + }); + } + } + RuntimeEvent::StepResult { + result, + return_value, + } => { + // StepResult is expected to be emitted after a step (in a context). + // Only case StepResult will occur without a Step before is in a transfer + // transaction to a non-contract address. However it will not contain any + // steps and return an empty trace, so we can ignore this edge case. + if let Some(context) = self.context_stack.last_mut() { + if let Some(current_step) = context.current_step.take() { + let Step { + opcode, + depth, + gas, + gas_cost, + position, + memory, + stack, + } = current_step; + + let memory = memory.map(convert_memory); + + let storage = if self.disable_storage { + None + } else { + self.remaining_memory_usage = + self.remaining_memory_usage.and_then(|inner| { + inner + .checked_sub(context.storage_cache.len().saturating_mul(64)) + }); + + if self.remaining_memory_usage.is_none() { + return; + } + + Some(context.storage_cache.clone()) + }; + + self.struct_logs.push(RawStepLog { + depth: depth.into(), + gas: gas.into(), + gas_cost: gas_cost.into(), + memory, + op: opcode, + pc: position.into(), + stack, + storage, + }); + } + } + + // We match on the capture to handle traps/exits. + match result { + Err(Capture::Exit(reason)) => { + // Exit = we exit the context (should always be some) + if let Some(mut context) = self.context_stack.pop() { + // If final context is exited, we store gas and return value. + if self.context_stack.is_empty() { + self.return_value = return_value.to_vec(); + } + + // If the context exited without revert we must keep track of the + // updated storage keys. + if !self.disable_storage && matches!(reason, ExitReason::Succeed(_)) { + if let Some(parent_context) = self.context_stack.last_mut() { + // Add cache to storage changes. + context + .global_storage_changes + .insert(context.address, context.storage_cache); + + // Apply storage changes to parent, either updating its cache or map of changes. + for (address, mut storage) in context.global_storage_changes { + // Same address => We update its cache (only tracked keys) + if parent_context.address == address { + for (cached_key, cached_value) in + &mut parent_context.storage_cache + { + if let Some(value) = storage.remove(cached_key) { + *cached_value = value; + } + } + } + // Otherwise, update the storage changes. + else { + parent_context + .global_storage_changes + .entry(address) + .or_insert_with(BTreeMap::new) + .append(&mut storage); + } + } + } + } + } + } + Err(Capture::Trap(opcode)) if ContextType::from(opcode.clone()).is_some() => { + self.new_context = true; + } + _ => (), + } + } + RuntimeEvent::SLoad { + address: _, + index, + value, + } + | RuntimeEvent::SStore { + address: _, + index, + value, + } => { + if let Some(context) = self.context_stack.last_mut() { + if !self.disable_storage { + context.storage_cache.insert(index, value); + } + } + } + // We ignore other kinds of messages if any (new ones may be added in the future). + #[allow(unreachable_patterns)] + _ => (), + } + } +} + +impl ListenerT for Listener { + fn event(&mut self, event: Event) { + if self.remaining_memory_usage.is_none() { + return; + } + + match event { + Event::Gasometer(e) => self.gasometer_event(e), + Event::Runtime(e) => self.runtime_event(e), + _ => {} + }; + } + + fn step_event_filter(&self) -> StepEventFilter { + StepEventFilter { + enable_memory: !self.disable_memory, + enable_stack: !self.disable_stack, + } + } +} diff --git a/crates/evm-tracing-client/src/serialization.rs b/crates/evm-tracing-client/src/serialization.rs new file mode 100644 index 000000000..97dcb79d0 --- /dev/null +++ b/crates/evm-tracing-client/src/serialization.rs @@ -0,0 +1,102 @@ +//! Serialization functions for various types and formats. + +use serde::{ + ser::{Error, SerializeSeq}, + Serializer, +}; +use sp_core::{H256, U256}; +use sp_runtime::traits::UniqueSaturatedInto; + +/// Serializes seq `H256`. +pub fn seq_h256_serialize(data: &Option>, serializer: S) -> Result +where + S: Serializer, +{ + if let Some(vec) = data { + let mut seq = serializer.serialize_seq(Some(vec.len()))?; + + for hash in vec { + seq.serialize_element(&format!("{:x}", hash))?; + } + + seq.end() + } else { + let seq = serializer.serialize_seq(Some(0))?; + seq.end() + } +} + +/// Serializes bytes 0x. +pub fn bytes_0x_serialize(bytes: &[u8], serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str(&format!("0x{}", hex::encode(bytes))) +} + +/// Serializes option bytes 0x. +pub fn option_bytes_0x_serialize( + bytes: &Option>, + serializer: S, +) -> Result +where + S: Serializer, +{ + if let Some(bytes) = bytes.as_ref() { + return bytes_0x_serialize(bytes, serializer); + } + + Err(S::Error::custom("String serialize error.")) +} + +/// Serializes opcode. +pub fn opcode_serialize(opcode: &[u8], serializer: S) -> Result +where + S: Serializer, +{ + let d = std::str::from_utf8(opcode) + .map_err(|_| S::Error::custom("Opcode serialize error."))? + .to_uppercase(); + + serializer.serialize_str(&d) +} + +/// Serializes string. +pub fn string_serialize(value: &[u8], serializer: S) -> Result +where + S: Serializer, +{ + let d = std::str::from_utf8(value) + .map_err(|_| S::Error::custom("String serialize error."))? + .to_string(); + + serializer.serialize_str(&d) +} + +/// Serializes option string. +pub fn option_string_serialize(value: &Option>, serializer: S) -> Result +where + S: Serializer, +{ + if let Some(value) = value.as_ref() { + return string_serialize(value, serializer); + } + + Err(S::Error::custom("string serialize error.")) +} + +/// Serializes `U256`. +pub fn u256_serialize(data: &U256, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_u64(UniqueSaturatedInto::::unique_saturated_into(*data)) +} + +/// Serializes `H256` 0x. +pub fn h256_0x_serialize(data: &H256, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str(&format!("0x{:x}", data)) +} diff --git a/crates/evm-tracing-client/src/types/block.rs b/crates/evm-tracing-client/src/types/block.rs new file mode 100644 index 000000000..78f76d284 --- /dev/null +++ b/crates/evm-tracing-client/src/types/block.rs @@ -0,0 +1,134 @@ +//! Block transaction related types. + +use codec::{Decode, Encode}; +use serde::Serialize; +use sp_core::{H160, H256, U256}; + +use super::{CallType, CreateType}; +use crate::serialization::*; + +/// Block transaction trace. +#[derive(Clone, Eq, PartialEq, Debug, Encode, Decode, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BlockTransactionTrace { + /// Tx hash. + #[serde(serialize_with = "h256_0x_serialize")] + pub tx_hash: H256, + /// Result. + pub result: super::single::TransactionTrace, + /// Tx position. + #[serde(skip_serializing)] + pub tx_position: u32, +} + +/// Transaction trace. +#[derive(Clone, Eq, PartialEq, Debug, Encode, Decode, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionTrace { + /// Transaction trace action. + #[serde(flatten)] + pub action: TransactionTraceAction, + /// Block hash. + #[serde(serialize_with = "h256_0x_serialize")] + pub block_hash: H256, + /// Block number. + pub block_number: u32, + /// Output. + #[serde(flatten)] + pub output: TransactionTraceOutput, + /// Subtraces. + pub subtraces: u32, + /// Trace address. + pub trace_address: Vec, + /// Transaction hash. + #[serde(serialize_with = "h256_0x_serialize")] + pub transaction_hash: H256, + /// Transaction position. + pub transaction_position: u32, +} + +/// Transaction trace action. +#[derive(Clone, Eq, PartialEq, Debug, Encode, Decode, Serialize)] +#[serde(rename_all = "camelCase", tag = "type", content = "action")] +pub enum TransactionTraceAction { + /// Call. + #[serde(rename_all = "camelCase")] + Call { + /// Call type. + call_type: CallType, + /// From. + from: H160, + /// Gas. + gas: U256, + /// Input. + #[serde(serialize_with = "bytes_0x_serialize")] + input: Vec, + /// To. + to: H160, + /// Value. + value: U256, + }, + /// Create. + #[serde(rename_all = "camelCase")] + Create { + /// Creation method. + creation_method: CreateType, + /// From. + from: H160, + /// Gas. + gas: U256, + /// Init. + #[serde(serialize_with = "bytes_0x_serialize")] + init: Vec, + /// Value. + value: U256, + }, + /// Suicide. + #[serde(rename_all = "camelCase")] + Suicide { + /// Address. + address: H160, + /// Balance. + balance: U256, + /// Refund address. + refund_address: H160, + }, +} + +/// Transaction trace output. +#[derive(Clone, Eq, PartialEq, Debug, Encode, Decode, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum TransactionTraceOutput { + /// Result. + Result(TransactionTraceResult), + /// Error. + Error(#[serde(serialize_with = "string_serialize")] Vec), +} + +/// Transaction trace result. +#[derive(Clone, Eq, PartialEq, Debug, Encode, Decode, Serialize)] +#[serde(rename_all = "camelCase", untagged)] +pub enum TransactionTraceResult { + /// Call. + #[serde(rename_all = "camelCase")] + Call { + /// Gas used. + gas_used: U256, + /// Output. + #[serde(serialize_with = "bytes_0x_serialize")] + output: Vec, + }, + /// Create. + #[serde(rename_all = "camelCase")] + Create { + /// Address. + address: H160, + /// Code. + #[serde(serialize_with = "bytes_0x_serialize")] + code: Vec, + /// Gas used. + gas_used: U256, + }, + /// Suicide. + Suicide, +} diff --git a/crates/evm-tracing-client/src/types/blockscout.rs b/crates/evm-tracing-client/src/types/blockscout.rs new file mode 100644 index 000000000..25dc6d8d1 --- /dev/null +++ b/crates/evm-tracing-client/src/types/blockscout.rs @@ -0,0 +1,69 @@ +//! Blockscout explicitly types. + +use codec::{Decode, Encode}; +use serde::Serialize; +use sp_core::{H160, U256}; + +use super::{CallResult, CallType, CreateResult}; +use crate::serialization::*; + +/// Blockcout call. +#[derive(Clone, Eq, PartialEq, Debug, Encode, Decode, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BlockscoutCall { + /// From address. + pub from: H160, + /// Indices of parent calls. + pub trace_address: Vec, + /// Number of children calls. + /// Not needed for Blockscout, but needed for `crate::block` + /// types that are build from this type. + #[serde(skip)] + pub subtraces: u32, + /// Sends funds to the (payable) function. + pub value: U256, + /// Remaining gas in the runtime. + pub gas: U256, + /// Gas used by this context. + pub gas_used: U256, + /// Inner. + #[serde(flatten)] + pub inner: BlockscoutCallInner, +} + +/// Blockscout call inner. +#[derive(Clone, Eq, PartialEq, Debug, Encode, Decode, Serialize)] +#[serde(rename_all = "lowercase", tag = "type")] +pub enum BlockscoutCallInner { + /// Call. + Call { + /// Type of call. + #[serde(rename(serialize = "callType"))] + call_type: CallType, + /// To. + to: H160, + /// Input. + #[serde(serialize_with = "bytes_0x_serialize")] + input: Vec, + /// Call result. + #[serde(flatten)] + res: CallResult, + }, + /// Create. + Create { + /// Init. + #[serde(serialize_with = "bytes_0x_serialize")] + init: Vec, + /// Create result. + #[serde(flatten)] + res: CreateResult, + }, + /// Selfdestruct. + SelfDestruct { + /// Balance. + #[serde(skip)] + balance: U256, + /// To. + to: H160, + }, +} diff --git a/crates/evm-tracing-client/src/types/call_tracer.rs b/crates/evm-tracing-client/src/types/call_tracer.rs new file mode 100644 index 000000000..a3c6c1d30 --- /dev/null +++ b/crates/evm-tracing-client/src/types/call_tracer.rs @@ -0,0 +1,88 @@ +//! Call tracer explicitly types. + +use codec::{Decode, Encode}; +use serde::Serialize; +use sp_core::{H160, U256}; + +use super::{single::Call, CallResult}; +use crate::serialization::*; + +/// Call tracer call. +#[derive(Clone, Eq, PartialEq, Debug, Encode, Decode, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CallTracerCall { + /// From address. + pub from: H160, + /// Indices of parent calls. Used to build the Etherscan nested response. + #[serde(skip_serializing_if = "Option::is_none")] + pub trace_address: Option>, + /// Remaining gas in the runtime. + pub gas: U256, + /// Gas used by this context. + pub gas_used: U256, + /// Inner. + #[serde(flatten)] + pub inner: CallTracerInner, + /// Calls. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub calls: Vec, +} + +/// Call tracer inner. +#[derive(Clone, Eq, PartialEq, Debug, Encode, Decode, Serialize)] +#[serde(untagged)] +pub enum CallTracerInner { + /// Call. + Call { + /// Call type. + #[serde(rename = "type", serialize_with = "opcode_serialize")] + call_type: Vec, + /// To. + to: H160, + /// Input. + #[serde(serialize_with = "bytes_0x_serialize")] + input: Vec, + /// Call result. + #[serde(flatten)] + res: CallResult, + /// Value. + #[serde(skip_serializing_if = "Option::is_none")] + value: Option, + }, + /// Create. + Create { + /// Call type. + #[serde(rename = "type", serialize_with = "opcode_serialize")] + call_type: Vec, + /// Input. + #[serde(serialize_with = "bytes_0x_serialize")] + input: Vec, + /// To. + #[serde(skip_serializing_if = "Option::is_none")] + to: Option, + #[serde( + skip_serializing_if = "Option::is_none", + serialize_with = "option_bytes_0x_serialize" + )] + /// Output. + output: Option>, + #[serde( + skip_serializing_if = "Option::is_none", + serialize_with = "option_string_serialize" + )] + /// Error. + error: Option>, + /// Value. + value: U256, + }, + /// Selfdestruct. + SelfDestruct { + /// Call type. + #[serde(rename = "type", serialize_with = "opcode_serialize")] + call_type: Vec, + /// To. + to: H160, + /// Value. + value: U256, + }, +} diff --git a/crates/evm-tracing-client/src/types/mod.rs b/crates/evm-tracing-client/src/types/mod.rs new file mode 100644 index 000000000..8229deac2 --- /dev/null +++ b/crates/evm-tracing-client/src/types/mod.rs @@ -0,0 +1,158 @@ +//! EVM tracing types. + +extern crate alloc; + +use codec::{Decode, Encode}; +use serde::Serialize; +use sp_core::{H160, H256}; + +pub mod block; +pub mod blockscout; +pub mod call_tracer; +pub mod single; + +use crate::serialization::*; + +/// Call result. +#[derive(Clone, Eq, PartialEq, Debug, Encode, Decode, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum CallResult { + /// Output. + Output(#[serde(serialize_with = "bytes_0x_serialize")] Vec), + /// Error. + Error(#[serde(serialize_with = "string_serialize")] Vec), +} + +/// Create result. +#[derive(Clone, Eq, PartialEq, Debug, Encode, Decode, Serialize)] +#[serde(rename_all = "camelCase", untagged)] +pub enum CreateResult { + /// Error. + Error { + /// Error bytes. + #[serde(serialize_with = "string_serialize")] + error: Vec, + }, + /// Success. + Success { + /// Created contract hash value, + #[serde(rename = "createdContractAddressHash")] + created_contract_address_hash: H160, + /// Created contract code. + #[serde(serialize_with = "bytes_0x_serialize", rename = "createdContractCode")] + created_contract_code: Vec, + }, +} + +/// Call type. +#[derive(Clone, Copy, Eq, PartialEq, Debug, Encode, Decode, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum CallType { + /// Call. + Call, + /// Call code. + CallCode, + /// Delegate call. + DelegateCall, + /// Static call. + StaticCall, +} + +/// Create type. +#[derive(Clone, Copy, Eq, PartialEq, Debug, Encode, Decode, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum CreateType { + /// Create type. + Create, +} + +/// Context type. +#[derive(Debug)] +pub enum ContextType { + /// Call type. + Call(CallType), + /// Create type. + Create, +} + +impl ContextType { + /// Obtain context type from opcode. + pub fn from(opcode: Vec) -> Option { + let opcode = match alloc::str::from_utf8(&opcode[..]) { + Ok(op) => op.to_uppercase(), + _ => return None, + }; + match &opcode[..] { + "CREATE" | "CREATE2" => Some(ContextType::Create), + "CALL" => Some(ContextType::Call(CallType::Call)), + "CALLCODE" => Some(ContextType::Call(CallType::CallCode)), + "DELEGATECALL" => Some(ContextType::Call(CallType::DelegateCall)), + "STATICCALL" => Some(ContextType::Call(CallType::StaticCall)), + _ => None, + } + } +} + +/// Memory converter. +pub fn convert_memory(memory: Vec) -> Vec { + let chunk_size = 32; + + memory + .chunks(chunk_size) + .map(|chunk| { + let mut buffer = [0u8; 32]; + buffer[chunk_size + .checked_sub(chunk.len()) + .expect("valid operation; qed")..] + .copy_from_slice(chunk); + H256::from_slice(&buffer) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::{convert_memory, H256}; + + #[test] + fn convert_memory_empty_input() { + let input = vec![]; + let output = convert_memory(input); + assert!(output.is_empty()); + } + + #[test] + fn convert_memory_muptiply_of_32_bytes() { + let input = vec![1u8; 64]; + let output = convert_memory(input); + + assert_eq!(output.len(), 2); + assert_eq!(output[0], H256::from_slice(&[1u8; 32])); + assert_eq!(output[1], H256::from_slice(&[1u8; 32])); + } + + #[test] + fn convert_memory_less_than_32_bytes() { + let input = vec![2u8; 10]; + let output = convert_memory(input); + + let mut expected_partial = [0u8; 32]; + expected_partial[22..].copy_from_slice(&[2u8; 10]); + + assert_eq!(output.len(), 1); + assert_eq!(output[0], H256::from_slice(&expected_partial)); + } + + #[test] + fn convert_memory_more_than_32_bytes() { + let input = vec![3u8; 42]; + let output = convert_memory(input); + + let mut expected_partial = [0u8; 32]; + expected_partial[22..].copy_from_slice(&[3u8; 10]); + + assert_eq!(output.len(), 2); + assert_eq!(output[0], H256::from_slice(&[3u8; 32])); + assert_eq!(output[1], H256::from_slice(&expected_partial)); + } +} diff --git a/crates/evm-tracing-client/src/types/single.rs b/crates/evm-tracing-client/src/types/single.rs new file mode 100644 index 000000000..088444f10 --- /dev/null +++ b/crates/evm-tracing-client/src/types/single.rs @@ -0,0 +1,93 @@ +//! Single transaction related types. + +use codec::{Decode, Encode}; +use serde::Serialize; +use sp_core::{sp_std::collections::btree_map::BTreeMap, H256, U256}; + +use crate::serialization::*; + +/// Call. +#[derive(Clone, Eq, PartialEq, Debug, Encode, Decode, Serialize)] +#[serde(rename_all = "camelCase", untagged)] +#[allow(clippy::large_enum_variant)] +pub enum Call { + /// Blockscout call. + Blockscout(super::blockscout::BlockscoutCall), + /// Call tracer. + CallTracer(super::call_tracer::CallTracerCall), +} + +/// Trace type. +#[derive(Clone, Copy, Eq, PartialEq, Debug, Encode, Decode)] +pub enum TraceType { + /// Classic geth with no javascript based tracing. + Raw { + /// Disable storage flag. + disable_storage: bool, + /// Disable memory flag. + disable_memory: bool, + /// Disable stack flag. + disable_stack: bool, + }, + /// List of calls and subcalls formatted with an input tracer (i.e. callTracer or Blockscout). + CallList, + /// A single block trace. Use in `debug_traceTransactionByNumber` / `traceTransactionByHash`. + Block, +} + +/// Single transaction trace. +#[derive(Clone, Eq, PartialEq, Debug, Encode, Decode, Serialize)] +#[serde(rename_all = "camelCase", untagged)] +pub enum TransactionTrace { + /// Classical output of `debug_trace`. + #[serde(rename_all = "camelCase")] + Raw { + /// Gas. + gas: U256, + /// Return value. + #[serde(with = "hex")] + return_value: Vec, + /// Logs. + struct_logs: Vec, + }, + /// Matches the formatter used by Blockscout. + CallList(Vec), + /// Used by Geth's callTracer. + CallListNested(Call), +} + +/// Raw step log. +#[derive(Clone, Eq, PartialEq, Debug, Encode, Decode, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RawStepLog { + /// Depth. + #[serde(serialize_with = "u256_serialize")] + pub depth: U256, + /// Gas. + #[serde(serialize_with = "u256_serialize")] + pub gas: U256, + /// Gas cost. + #[serde(serialize_with = "u256_serialize")] + pub gas_cost: U256, + /// Memory. + #[serde( + serialize_with = "seq_h256_serialize", + skip_serializing_if = "Option::is_none" + )] + pub memory: Option>, + /// Op. + #[serde(serialize_with = "opcode_serialize")] + pub op: Vec, + /// Pc. + #[serde(serialize_with = "u256_serialize")] + pub pc: U256, + /// Stack. + #[serde( + serialize_with = "seq_h256_serialize", + skip_serializing_if = "Option::is_none" + )] + pub stack: Option>, + /// Storage. + #[serde(skip_serializing_if = "Option::is_none")] + pub storage: Option>, +} diff --git a/utils/checks/snapshots/features.yaml b/utils/checks/snapshots/features.yaml index 7a02b92ee..790d9bdda 100644 --- a/utils/checks/snapshots/features.yaml +++ b/utils/checks/snapshots/features.yaml @@ -835,6 +835,8 @@ features: - default - std +- name: evm-tracing-client 0.1.0 + features: [] - name: evm-tracing-events 0.1.0 features: - default @@ -1235,6 +1237,7 @@ features: - alloc - default + - serde - std - name: hex-literal 0.4.1 features: []