diff --git a/.github/actions/plan/modes.ts b/.github/actions/plan/modes.ts index 7f9b2316d..1d2caa7c6 100644 --- a/.github/actions/plan/modes.ts +++ b/.github/actions/plan/modes.ts @@ -60,6 +60,12 @@ export const code = { cargoArgs: "--locked --workspace --features try-runtime", cargoCacheKey: "try-runtime", }, + buildEvmTracing: { + name: "build with evm-tracing", + cargoCommand: "build", + cargoArgs: "--locked --workspace --features evm-tracing", + cargoCacheKey: "evm-tracing", + }, } satisfies Modes; export const build = { diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 8e270a023..3eb92b9cd 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -80,6 +80,9 @@ jobs: - suite: "try-runtime" build-args: "--features try-runtime" skip-ts-tests: true + - suite: "evm-tracing" + build-args: "--features evm-tracing" + skip-bash-tests: true fail-fast: false name: End-to-end tests / ${{ matrix.test.suite }} runs-on: ubuntu-24.04 diff --git a/Cargo.lock b/Cargo.lock index 11a8257a8..fea072426 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2303,6 +2303,97 @@ dependencies = [ "sha3 0.10.8", ] +[[package]] +name = "evm-tracer" +version = "0.1.0" +dependencies = [ + "evm", + "evm-gasometer", + "evm-runtime", + "evm-tracing-events", + "evm-tracing-host-api", + "parity-scale-codec", + "sp-std", +] + +[[package]] +name = "evm-tracing-api" +version = "0.1.0" +dependencies = [ + "ethereum", + "sp-api", + "sp-core", + "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" +dependencies = [ + "environmental", + "evm", + "evm-gasometer", + "evm-runtime", + "parity-scale-codec", + "smallvec", + "sp-core", + "sp-runtime-interface", +] + +[[package]] +name = "evm-tracing-host-api" +version = "0.1.0" +dependencies = [ + "evm-tracing-events", + "parity-scale-codec", + "sp-runtime-interface", + "sp-std", +] + +[[package]] +name = "evm-tracing-rpc" +version = "0.1.0" +dependencies = [ + "ethereum", + "evm-tracing-api", + "evm-tracing-client", + "fc-db", + "fc-rpc", + "fc-rpc-core", + "fc-storage", + "fp-rpc", + "frame-support", + "futures", + "hex-literal", + "jsonrpsee", + "parity-scale-codec", + "sc-client-api", + "sc-utils", + "serde", + "sp-api", + "sp-block-builder", + "sp-blockchain", + "sp-core", + "sp-io", + "sp-runtime", + "substrate-prometheus-endpoint", + "tokio", + "tracing", +] + [[package]] name = "exit-future" version = "0.2.0" @@ -3496,6 +3587,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" @@ -3645,6 +3739,8 @@ dependencies = [ "clap", "crypto-utils", "crypto-utils-evm", + "evm-tracing-host-api", + "evm-tracing-rpc", "fc-cli", "fc-consensus", "fc-db", @@ -3718,6 +3814,7 @@ dependencies = [ "bioauth-flow-api", "bioauth-flow-rpc", "bioauth-keys", + "evm-tracing-rpc", "fc-db", "fc-mapping-sync", "fc-rpc", @@ -3769,6 +3866,8 @@ dependencies = [ "eip712-token-claim", "ethereum", "evm-nonces-recovery", + "evm-tracer", + "evm-tracing-api", "fp-evm", "fp-rpc", "fp-self-contained", @@ -3878,7 +3977,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite 0.2.13", - "socket2 0.4.10", + "socket2 0.5.5", "tokio", "tower-service", "tracing", @@ -5696,7 +5795,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ - "proc-macro-crate 1.1.3", + "proc-macro-crate 3.1.0", "proc-macro2", "quote", "syn 2.0.106", @@ -9423,9 +9522,9 @@ checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7" [[package]] name = "smallvec" -version = "1.13.1" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "snap" @@ -11238,7 +11337,7 @@ checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" dependencies = [ "cfg-if", "digest 0.10.7", - "rand 0.7.3", + "rand 0.8.5", "static_assertions", ] diff --git a/Cargo.toml b/Cargo.toml index 4bd25a221..cbf302bb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,9 +20,12 @@ bytes = { version = "1", default-features = false } chrono = { version = "0.4", default-features = false } clap = { version = "4", default-features = false } ed25519-dalek = { version = "2", default-features = false } +environmental = { version = "1.1", default-features = false } ethereum = { version = "0.14", default-features = false } ethers-core = { version = "2.0.14", default-features = false } evm = { git = "https://github.com/rust-blockchain/evm", rev = "b7b82c7e1fc57b7449d6dfa6826600de37cc1e65", default-features = false } +evm-gasometer = { git = "https://github.com/rust-blockchain/evm", rev = "b7b82c7e1fc57b7449d6dfa6826600de37cc1e65", default-features = false } +evm-runtime = { git = "https://github.com/rust-blockchain/evm", rev = "b7b82c7e1fc57b7449d6dfa6826600de37cc1e65", default-features = false } fdlimit = { version = "0.2", default-features = false } futures = { version = "0.3", default-features = false } getrandom = { version = "0.3", default-features = false } @@ -55,6 +58,7 @@ serde = { version = "1", default-features = false } serde_json = { version = "1", default-features = false } sha3 = { version = "0.10", default-features = false } similar-asserts = { version = "1", default-features = false } +smallvec = { version = "1.13", default-features = false } static_assertions = { version = "1", default-features = false } syn = { version = "2", default-features = false } thiserror = { version = "1.0.69", default-features = false } @@ -132,6 +136,7 @@ sp-keystore = { git = "https://github.com/humanode-network/substrate", tag = "lo sp-offchain = { git = "https://github.com/humanode-network/substrate", tag = "locked/polkadot-v0.9.43-2025-03-22", default-features = false } sp-panic-handler = { git = "https://github.com/humanode-network/substrate", tag = "locked/polkadot-v0.9.43-2025-03-22", default-features = false } sp-runtime = { git = "https://github.com/humanode-network/substrate", tag = "locked/polkadot-v0.9.43-2025-03-22", default-features = false } +sp-runtime-interface = { git = "https://github.com/humanode-network/substrate", tag = "locked/polkadot-v0.9.43-2025-03-22", default-features = false } sp-session = { git = "https://github.com/humanode-network/substrate", tag = "locked/polkadot-v0.9.43-2025-03-22", default-features = false } sp-staking = { git = "https://github.com/humanode-network/substrate", tag = "locked/polkadot-v0.9.43-2025-03-22", default-features = false } sp-std = { git = "https://github.com/humanode-network/substrate", tag = "locked/polkadot-v0.9.43-2025-03-22", default-features = false } @@ -140,6 +145,7 @@ sp-tracing = { git = "https://github.com/humanode-network/substrate", tag = "loc sp-transaction-pool = { git = "https://github.com/humanode-network/substrate", tag = "locked/polkadot-v0.9.43-2025-03-22", default-features = false } sp-version = { git = "https://github.com/humanode-network/substrate", tag = "locked/polkadot-v0.9.43-2025-03-22", default-features = false } substrate-frame-rpc-system = { git = "https://github.com/humanode-network/substrate", tag = "locked/polkadot-v0.9.43-2025-03-22", default-features = false } +substrate-prometheus-endpoint = { git = "https://github.com/humanode-network/substrate", tag = "locked/polkadot-v0.9.43-2025-03-22", default-features = false } substrate-wasm-builder = { git = "https://github.com/humanode-network/substrate", tag = "locked/polkadot-v0.9.43-2025-03-22", default-features = false } try-runtime-cli = { git = "https://github.com/humanode-network/substrate", tag = "locked/polkadot-v0.9.43-2025-03-22", default-features = false } diff --git a/crates/evm-tracer/Cargo.toml b/crates/evm-tracer/Cargo.toml new file mode 100644 index 000000000..4321f0974 --- /dev/null +++ b/crates/evm-tracer/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "evm-tracer" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +evm-tracing-events = { path = "../evm-tracing-events", default-features = false, features = ["evm-tracing"] } +evm-tracing-host-api = { path = "../evm-tracing-host-api", default-features = false } + +codec = { workspace = true, features = ["derive"] } +evm = { workspace = true, features = ["tracing"] } +evm-gasometer = { workspace = true, features = ["tracing"] } +evm-runtime = { workspace = true, features = ["tracing"] } +sp-std = { workspace = true } + +[features] +default = ["std"] +std = [ + "codec/std", + "evm-gasometer/std", + "evm-runtime/std", + "evm-tracing-events/std", + "evm-tracing-host-api/std", + "evm/std", + "sp-std/std", +] diff --git a/crates/evm-tracer/src/lib.rs b/crates/evm-tracer/src/lib.rs new file mode 100644 index 000000000..644f603a4 --- /dev/null +++ b/crates/evm-tracer/src/lib.rs @@ -0,0 +1,98 @@ +//! Substrate EVM tracer. +//! +//! Enables tracing the EVM opcode execution and proxies EVM messages to the host functions. + +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::Encode; +use evm::tracing::{using as evm_using, EventListener as EvmListener}; +use evm_gasometer::tracing::{using as gasometer_using, EventListener as GasometerListener}; +use evm_runtime::tracing::{using as runtime_using, EventListener as RuntimeListener}; +use evm_tracing_events::{EvmEvent, GasometerEvent, RuntimeEvent, StepEventFilter}; +use sp_std::{cell::RefCell, rc::Rc}; + +/// Listener proxy. +struct ListenerProxy(pub Rc>); + +impl GasometerListener for ListenerProxy { + fn event(&mut self, event: evm_gasometer::tracing::Event) { + self.0.borrow_mut().event(event); + } +} + +impl RuntimeListener for ListenerProxy { + fn event(&mut self, event: evm_runtime::tracing::Event) { + self.0.borrow_mut().event(event); + } +} + +impl EvmListener for ListenerProxy { + fn event(&mut self, event: evm::tracing::Event) { + self.0.borrow_mut().event(event); + } +} + +/// EVM tracer. +pub struct EvmTracer { + /// Step event filter. + step_event_filter: StepEventFilter, +} + +impl Default for EvmTracer { + fn default() -> Self { + Self { + step_event_filter: evm_tracing_host_api::externalities::step_event_filter(), + } + } +} + +impl EvmTracer { + /// Setup event listeners and execute provided closure. + /// + /// Consume the tracer and return it alongside the return value of + /// the closure. + pub fn trace R>(self, f: F) { + let wrapped = Rc::new(RefCell::new(self)); + + let mut gasometer = ListenerProxy(Rc::clone(&wrapped)); + let mut runtime = ListenerProxy(Rc::clone(&wrapped)); + let mut evm = ListenerProxy(Rc::clone(&wrapped)); + + // Each line wraps the previous `f` into a `using` call. + // Listening to new events results in adding one new line. + // Order is irrelevant when registering listeners. + let f = || runtime_using(&mut runtime, f); + let f = || gasometer_using(&mut gasometer, f); + let f = || evm_using(&mut evm, f); + f(); + } + + /// Emit new call stack. + pub fn emit_new() { + evm_tracing_host_api::externalities::call_list_new(); + } +} + +impl EvmListener for EvmTracer { + fn event(&mut self, event: evm::tracing::Event) { + let event: EvmEvent = event.into(); + let message = event.encode(); + evm_tracing_host_api::externalities::evm_event(message); + } +} + +impl GasometerListener for EvmTracer { + fn event(&mut self, event: evm_gasometer::tracing::Event) { + let event: GasometerEvent = event.into(); + let message = event.encode(); + evm_tracing_host_api::externalities::gasometer_event(message); + } +} + +impl RuntimeListener for EvmTracer { + fn event(&mut self, event: evm_runtime::tracing::Event) { + let event = RuntimeEvent::from_evm_event(event, self.step_event_filter); + let message = event.encode(); + evm_tracing_host_api::externalities::runtime_event(message); + } +} diff --git a/crates/evm-tracing-api/Cargo.toml b/crates/evm-tracing-api/Cargo.toml new file mode 100644 index 000000000..f1a0ca7ad --- /dev/null +++ b/crates/evm-tracing-api/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "evm-tracing-api" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +ethereum = { workspace = true, features = ["with-codec"] } +sp-api = { workspace = true } +sp-core = { workspace = true } +sp-runtime = { workspace = true } + +[features] +default = ["std"] +std = [ + "ethereum/std", + "sp-api/std", + "sp-core/std", + "sp-runtime/std", +] diff --git a/crates/evm-tracing-api/src/lib.rs b/crates/evm-tracing-api/src/lib.rs new file mode 100644 index 000000000..4e5923f55 --- /dev/null +++ b/crates/evm-tracing-api/src/lib.rs @@ -0,0 +1,41 @@ +//! The runtime API for the EVM tracing logic. + +#![cfg_attr(not(feature = "std"), no_std)] + +use ethereum::TransactionV2 as Transaction; +use sp_core::{sp_std::vec::Vec, H160, H256, U256}; + +sp_api::decl_runtime_apis! { + /// Runtime API for the EVM tracing logic. + pub trait EvmTracingApi { + /// Trace transaction. + fn trace_transaction( + extrinsics: Vec, + transaction: &Transaction, + header: &Block::Header, + ) -> Result<(), sp_runtime::DispatchError>; + + /// Trace block. + fn trace_block( + extrinsics: Vec, + known_transactions: Vec, + header: &Block::Header, + ) -> Result<(), sp_runtime::DispatchError>; + + /// Trace call execution. + // Allow too many arguments to pass them in the way used at EVM runner call. + #[allow(clippy::too_many_arguments)] + fn trace_call( + header: &Block::Header, + from: H160, + to: H160, + data: Vec, + value: U256, + gas_limit: U256, + max_fee_per_gas: Option, + max_priority_fee_per_gas: Option, + nonce: Option, + access_list: Option)>>, + ) -> Result<(), sp_runtime::DispatchError>; + } +} 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..9515663be --- /dev/null +++ b/crates/evm-tracing-client/src/formatters/call_tracer.rs @@ -0,0 +1,256 @@ +//! Call tracer formatter implementation. + +use evm_tracing_events::MarshalledOpcode; +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 => MarshalledOpcode::from("CALL"), + CallType::CallCode => MarshalledOpcode::from("CALLCODE"), + CallType::DelegateCall => { + MarshalledOpcode::from("DELEGATECALL") + } + CallType::StaticCall => MarshalledOpcode::from("STATICCALL"), + }, + 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: MarshalledOpcode::from("CREATE"), + }, + BlockscoutCallInner::SelfDestruct { balance, to } => { + CallTracerInner::SelfDestruct { + value: balance, + to, + call_type: MarshalledOpcode::from("SELFDESTRUCT"), + } + } + }, + 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..5e0538cdc --- /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, MarshalledOpcode, + }; + 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: MarshalledOpcode::default(), + 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..7afe61518 --- /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, + MarshalledOpcode, 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: MarshalledOpcode, + /// 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..a85f21e1e --- /dev/null +++ b/crates/evm-tracing-client/src/serialization.rs @@ -0,0 +1,99 @@ +//! Serialization functions for various types and formats. + +use evm_tracing_events::MarshalledOpcode; +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: &MarshalledOpcode, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str(&opcode.to_string()) +} + +/// 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..0e01b1582 --- /dev/null +++ b/crates/evm-tracing-client/src/types/call_tracer.rs @@ -0,0 +1,89 @@ +//! Call tracer explicitly types. + +use codec::{Decode, Encode}; +use evm_tracing_events::MarshalledOpcode; +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: MarshalledOpcode, + /// 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: MarshalledOpcode, + /// 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: MarshalledOpcode, + /// 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..63952fcab --- /dev/null +++ b/crates/evm-tracing-client/src/types/mod.rs @@ -0,0 +1,155 @@ +//! EVM tracing types. + +extern crate alloc; + +use codec::{Decode, Encode}; +use evm_tracing_events::MarshalledOpcode; +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: MarshalledOpcode) -> Option { + match &opcode.to_string()[..] { + "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..ada84a6b3 --- /dev/null +++ b/crates/evm-tracing-client/src/types/single.rs @@ -0,0 +1,94 @@ +//! Single transaction related types. + +use codec::{Decode, Encode}; +use evm_tracing_events::MarshalledOpcode; +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: MarshalledOpcode, + /// 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/crates/evm-tracing-events/Cargo.toml b/crates/evm-tracing-events/Cargo.toml new file mode 100644 index 000000000..2bd8f14b4 --- /dev/null +++ b/crates/evm-tracing-events/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "evm-tracing-events" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +codec = { workspace = true } +environmental = { workspace = true } +evm = { workspace = true, features = ["with-codec"] } +evm-gasometer = { workspace = true } +evm-runtime = { workspace = true } +smallvec = { workspace = true } +sp-core = { workspace = true } +sp-runtime-interface = { workspace = true } + +[features] +default = ["std"] +evm-tracing = [ + "evm-gasometer/tracing", + "evm-runtime/tracing", + "evm/tracing", +] +std = [ + "codec/std", + "environmental/std", + "evm-gasometer/std", + "evm-runtime/std", + "evm/std", + "sp-core/std", + "sp-runtime-interface/std", +] diff --git a/crates/evm-tracing-events/src/evm.rs b/crates/evm-tracing-events/src/evm.rs new file mode 100644 index 000000000..be7c91a62 --- /dev/null +++ b/crates/evm-tracing-events/src/evm.rs @@ -0,0 +1,284 @@ +//! EVM explicitly events definitions. + +use codec::{Decode, Encode}; +use evm::ExitReason; +use sp_core::{sp_std::vec::Vec, H160, H256, U256}; + +use crate::Context; + +/// EVM transfer. +#[derive(Clone, Debug, Encode, Decode, PartialEq, Eq)] +pub struct Transfer { + /// Source address. + pub source: H160, + /// Target address. + pub target: H160, + /// Transfer value. + pub value: U256, +} + +impl From for Transfer { + fn from(transfer: evm_runtime::Transfer) -> Self { + Self { + source: transfer.source, + target: transfer.target, + value: transfer.value, + } + } +} + +/// EVM create scheme. +#[derive(Clone, Copy, Eq, PartialEq, Debug, Encode, Decode)] +pub enum CreateScheme { + /// Legacy create scheme of `CREATE`. + Legacy { + /// Caller of the create. + caller: H160, + }, + /// Create scheme of `CREATE2`. + Create2 { + /// Caller of the create. + caller: H160, + /// Code hash. + code_hash: H256, + /// Salt. + salt: H256, + }, + /// Create at a fixed location. + Fixed(H160), +} + +impl From for CreateScheme { + fn from(create_scheme: evm_runtime::CreateScheme) -> Self { + match create_scheme { + evm_runtime::CreateScheme::Legacy { caller } => Self::Legacy { caller }, + evm_runtime::CreateScheme::Create2 { + caller, + code_hash, + salt, + } => Self::Create2 { + caller, + code_hash, + salt, + }, + evm_runtime::CreateScheme::Fixed(address) => Self::Fixed(address), + } + } +} + +/// EVM event. +#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)] +pub enum EvmEvent { + /// Call. + Call { + /// Code address. + code_address: H160, + /// Transfer. + transfer: Option, + /// Input. + input: Vec, + /// Target gas. + target_gas: Option, + /// Is static flag. + is_static: bool, + /// Context. + context: Context, + }, + /// Create. + Create { + /// Caller. + caller: H160, + /// Address. + address: H160, + /// Scheme. + scheme: CreateScheme, + /// Value. + value: U256, + /// Init code. + init_code: Vec, + /// Target gas. + target_gas: Option, + }, + /// Suicide. + Suicide { + /// Address. + address: H160, + /// Target. + target: H160, + /// Balance. + balance: U256, + }, + /// Exit. + Exit { + /// Reason. + reason: ExitReason, + /// Return value. + return_value: Vec, + }, + /// Transact call. + TransactCall { + /// Caller. + caller: H160, + /// Address. + address: H160, + /// Value. + value: U256, + /// Data. + data: Vec, + /// Gas limit. + gas_limit: u64, + }, + /// Transact create. + TransactCreate { + /// Caller. + caller: H160, + /// Value. + value: U256, + /// Init code. + init_code: Vec, + /// Gas limit. + gas_limit: u64, + /// Address. + address: H160, + }, + /// Transact create2. + TransactCreate2 { + /// Caller. + caller: H160, + /// Value. + value: U256, + /// Init code. + init_code: Vec, + /// Salt. + salt: H256, + /// Gas limit. + gas_limit: u64, + /// Address. + address: H160, + }, + /// Precompile subcall. + PrecompileSubcall { + /// Code address. + code_address: H160, + /// Transfer. + transfer: Option, + /// Input. + input: Vec, + /// Target. + target_gas: Option, + /// Is static flag. + is_static: bool, + /// Context. + context: Context, + }, +} + +#[cfg(feature = "evm-tracing")] +impl<'a> From> for EvmEvent { + fn from(event: evm::tracing::Event<'a>) -> Self { + match event { + evm::tracing::Event::Call { + code_address, + transfer, + input, + target_gas, + is_static, + context, + } => Self::Call { + code_address, + transfer: transfer.as_ref().map(|transfer| transfer.clone().into()), + input: input.to_vec(), + target_gas, + is_static, + context: context.clone().into(), + }, + evm::tracing::Event::Create { + caller, + address, + scheme, + value, + init_code, + target_gas, + } => Self::Create { + caller, + address, + scheme: scheme.into(), + value, + init_code: init_code.to_vec(), + target_gas, + }, + evm::tracing::Event::Suicide { + address, + target, + balance, + } => Self::Suicide { + address, + target, + balance, + }, + evm::tracing::Event::Exit { + reason, + return_value, + } => Self::Exit { + reason: reason.clone(), + return_value: return_value.to_vec(), + }, + evm::tracing::Event::TransactCall { + caller, + address, + value, + data, + gas_limit, + } => Self::TransactCall { + caller, + address, + value, + data: data.to_vec(), + gas_limit, + }, + evm::tracing::Event::TransactCreate { + caller, + value, + init_code, + gas_limit, + address, + } => Self::TransactCreate { + caller, + value, + init_code: init_code.to_vec(), + gas_limit, + address, + }, + evm::tracing::Event::TransactCreate2 { + caller, + value, + init_code, + salt, + gas_limit, + address, + } => Self::TransactCreate2 { + caller, + value, + init_code: init_code.to_vec(), + salt, + gas_limit, + address, + }, + evm::tracing::Event::PrecompileSubcall { + code_address, + transfer, + input, + target_gas, + is_static, + context, + } => Self::PrecompileSubcall { + code_address, + transfer: transfer.as_ref().map(|transfer| transfer.clone().into()), + input: input.to_vec(), + target_gas, + is_static, + context: context.clone().into(), + }, + } + } +} diff --git a/crates/evm-tracing-events/src/gasometer.rs b/crates/evm-tracing-events/src/gasometer.rs new file mode 100644 index 000000000..b98badadf --- /dev/null +++ b/crates/evm-tracing-events/src/gasometer.rs @@ -0,0 +1,126 @@ +//! EVM gasometer events definitions. + +use codec::{Decode, Encode}; + +/// Snapshot. +#[derive(Debug, Default, Copy, Clone, Encode, Decode, PartialEq, Eq)] +pub struct Snapshot { + /// Gas limit. + pub gas_limit: u64, + /// Memory gas. + pub memory_gas: u64, + /// Used gas. + pub used_gas: u64, + /// Refunded gas. + pub refunded_gas: i64, +} + +impl Snapshot { + /// Calculate gas. + pub fn gas(&self) -> u64 { + self.gas_limit + .saturating_sub(self.used_gas) + .saturating_sub(self.memory_gas) + } +} + +#[cfg(feature = "evm-tracing")] +impl From> for Snapshot { + fn from(snapshot: Option) -> Self { + if let Some(snapshot) = snapshot { + Self { + gas_limit: snapshot.gas_limit, + memory_gas: snapshot.memory_gas, + used_gas: snapshot.used_gas, + refunded_gas: snapshot.refunded_gas, + } + } else { + Default::default() + } + } +} + +/// EVM gasometer event. +#[derive(Debug, Copy, Clone, Encode, Decode, PartialEq, Eq)] +pub enum GasometerEvent { + /// Record cost. + RecordCost { + /// Cost. + cost: u64, + /// Snapshot. + snapshot: Snapshot, + }, + /// Record refund. + RecordRefund { + /// Refund. + refund: i64, + /// Snapshot. + snapshot: Snapshot, + }, + /// Record stipend. + RecordStipend { + /// Stipend. + stipend: u64, + /// Snapshot. + snapshot: Snapshot, + }, + /// Record dynamic cost. + RecordDynamicCost { + /// Gas cost. + gas_cost: u64, + /// Memory gas. + memory_gas: u64, + /// Gas refunded. + gas_refund: i64, + /// Snapshot. + snapshot: Snapshot, + }, + /// Record transaction. + RecordTransaction { + /// Cost. + cost: u64, + /// Snapshot. + snapshot: Snapshot, + }, +} + +#[cfg(feature = "evm-tracing")] +impl From for GasometerEvent { + fn from(event: evm_gasometer::tracing::Event) -> Self { + match event { + evm_gasometer::tracing::Event::RecordCost { cost, snapshot } => Self::RecordCost { + cost, + snapshot: snapshot.into(), + }, + evm_gasometer::tracing::Event::RecordRefund { refund, snapshot } => { + Self::RecordRefund { + refund, + snapshot: snapshot.into(), + } + } + evm_gasometer::tracing::Event::RecordStipend { stipend, snapshot } => { + Self::RecordStipend { + stipend, + snapshot: snapshot.into(), + } + } + evm_gasometer::tracing::Event::RecordDynamicCost { + gas_cost, + memory_gas, + gas_refund, + snapshot, + } => Self::RecordDynamicCost { + gas_cost, + memory_gas, + gas_refund, + snapshot: snapshot.into(), + }, + evm_gasometer::tracing::Event::RecordTransaction { cost, snapshot } => { + Self::RecordTransaction { + cost, + snapshot: snapshot.into(), + } + } + } + } +} diff --git a/crates/evm-tracing-events/src/lib.rs b/crates/evm-tracing-events/src/lib.rs new file mode 100644 index 000000000..df562b3ba --- /dev/null +++ b/crates/evm-tracing-events/src/lib.rs @@ -0,0 +1,103 @@ +//! EVM tracing events related primitives. + +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::{Decode, Encode}; +use sp_core::{H160, U256}; +use sp_runtime_interface::pass_by::PassByCodec; + +pub mod evm; +pub mod gasometer; +mod marshalled_opcode; +pub mod runtime; + +pub use gasometer::GasometerEvent; +pub use marshalled_opcode::MarshalledOpcode; +pub use runtime::RuntimeEvent; + +pub use self::evm::EvmEvent; + +environmental::environmental!(listener: dyn Listener + 'static); + +/// Run closure with provided listener. +pub fn using R>(l: &mut (dyn Listener + 'static), f: F) -> R { + listener::using(l, f) +} + +/// Allow to configure which data of the step event +/// we want to keep or discard. Not discarding the data requires cloning the data +/// in the runtime which have a significant cost for each step. +#[derive(Clone, Copy, Eq, PartialEq, Debug, Encode, Decode, Default, PassByCodec)] +pub struct StepEventFilter { + /// Enabling stack flag. + pub enable_stack: bool, + /// Enabling memory flag. + pub enable_memory: bool, +} + +/// EVM tracing events. +#[derive(Clone, Eq, PartialEq, Debug, Encode, Decode)] +pub enum Event { + /// EVM explicit event. + Evm(evm::EvmEvent), + /// EVM gasometer event. + Gasometer(gasometer::GasometerEvent), + /// EVM runtime event. + Runtime(runtime::RuntimeEvent), + /// An event used to create a new `CallList`. + CallListNew(), +} + +impl Event { + /// Access the global reference and call it's `event` method, passing the `Event` itself as + /// argument. + /// + /// This only works if we are `using` a global reference to a `Listener` implementor. + pub fn emit(self) { + listener::with(|listener| listener.event(self)); + } +} + +/// Main trait to proxy emitted messages. +/// Used 2 times : +/// - Inside the runtime to proxy the events through the host functions +/// - Inside the client to forward those events to the client listener. +pub trait Listener { + /// Proxy emitted event. + fn event(&mut self, event: Event); + + /// Allow the runtime to know which data should be discarded and not cloned. + /// WARNING: It is only called once when the runtime tracing is instantiated to avoid + /// performing many ext calls. + fn step_event_filter(&self) -> StepEventFilter; +} + +/// Allow the tracing module in the runtime to know how to filter Step event +/// content, as cloning the entire data is expensive and most of the time +/// not necessary. +pub fn step_event_filter() -> Option { + let mut filter = None; + listener::with(|listener| filter = Some(listener.step_event_filter())); + filter +} + +/// EVM context of the runtime. +#[derive(Clone, Debug, Encode, Decode, PartialEq, Eq)] +pub struct Context { + /// Execution address. + pub address: H160, + /// Caller of the EVM. + pub caller: H160, + /// Apparent value of the EVM. + pub apparent_value: U256, +} + +impl From for Context { + fn from(context: evm_runtime::Context) -> Self { + Self { + address: context.address, + caller: context.caller, + apparent_value: context.apparent_value, + } + } +} diff --git a/crates/evm-tracing-events/src/marshalled_opcode.rs b/crates/evm-tracing-events/src/marshalled_opcode.rs new file mode 100644 index 000000000..3fd731595 --- /dev/null +++ b/crates/evm-tracing-events/src/marshalled_opcode.rs @@ -0,0 +1,86 @@ +//! Marshalled opcode definition and implementations. + +extern crate alloc; + +use codec::{Decode, Encode}; +use smallvec::SmallVec; +use sp_core::sp_std::{borrow::Cow, vec::Vec}; + +use crate::runtime::opcode_known_name; + +/// Marshalled opcode. +/// +/// 8 cause the longest is 13 and the epmeric estimate of max length +/// for the most popular ones in 7-ish. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct MarshalledOpcode(SmallVec<[u8; 8]>); + +impl From<&evm::Opcode> for MarshalledOpcode { + fn from(opcode: &evm::Opcode) -> Self { + let opcode = match opcode_known_name(opcode) { + Some(known) => known.to_uppercase(), + None => alloc::format!("UNKNOWN({})", opcode.as_u8()), + }; + + MarshalledOpcode(SmallVec::from_slice(opcode.as_bytes())) + } +} + +impl From<&'static str> for MarshalledOpcode { + fn from(value: &'static str) -> Self { + MarshalledOpcode(SmallVec::from_slice(value.as_bytes())) + } +} + +impl Encode for MarshalledOpcode { + fn encode(&self) -> Vec { + Cow::Borrowed(&self.0.as_slice()).encode() + } +} + +impl Decode for MarshalledOpcode { + fn decode(input: &mut I) -> Result { + let bytes = Cow::decode(input)?; + Ok(MarshalledOpcode(SmallVec::from_slice(&bytes))) + } +} + +impl core::fmt::Display for MarshalledOpcode { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + f, + "{}", + sp_core::sp_std::str::from_utf8(self.0.as_slice()).map_err(|_| core::fmt::Error)? + ) + } +} + +#[cfg(test)] +mod tests { + use smallvec::smallvec; + + use super::*; + + #[test] + fn encode_decode_works() { + let test_cases = [ + MarshalledOpcode(smallvec![0x11]), + MarshalledOpcode(smallvec![0x11, 0x22]), + MarshalledOpcode(smallvec![0x11, 0x22, 0x33]), + MarshalledOpcode(SmallVec::from_vec(vec![0x11; 13])), + ]; + + for opcode in test_cases { + let encoded = opcode.encode(); + assert_eq!(MarshalledOpcode::decode(&mut &encoded[..]).unwrap(), opcode); + } + } + + #[test] + fn display_works() { + assert_eq!( + MarshalledOpcode::from(&evm::Opcode::CREATE).to_string(), + "CREATE" + ); + } +} diff --git a/crates/evm-tracing-events/src/runtime.rs b/crates/evm-tracing-events/src/runtime.rs new file mode 100644 index 000000000..6d3d18cae --- /dev/null +++ b/crates/evm-tracing-events/src/runtime.rs @@ -0,0 +1,334 @@ +//! EVM runtime events definitions. + +use codec::{Decode, Encode}; +pub use evm::{ExitError, ExitReason, ExitSucceed}; +use sp_core::{sp_std::vec::Vec, H160, H256, U256}; + +#[cfg(feature = "evm-tracing")] +use crate::StepEventFilter; +use crate::{Context, MarshalledOpcode}; + +/// EVM stack. +#[derive(Clone, Debug, Encode, Decode, PartialEq, Eq)] +pub struct Stack { + /// Data. + pub data: Vec, + /// Limit. + pub limit: u64, +} + +impl From<&evm::Stack> for Stack { + fn from(stack: &evm::Stack) -> Self { + Self { + data: stack.data().clone(), + limit: stack.limit() as u64, + } + } +} + +/// EVM memory. +#[derive(Clone, Debug, Encode, Decode, PartialEq, Eq)] +pub struct Memory { + /// Data. + pub data: Vec, + /// Effective length. + pub effective_len: U256, + /// Limit. + pub limit: u64, +} + +impl From<&evm::Memory> for Memory { + fn from(memory: &evm::Memory) -> Self { + Self { + data: memory.data().clone(), + effective_len: memory.effective_len(), + limit: memory.limit() as u64, + } + } +} + +/// Capture represents the result of execution. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Encode, Decode)] +pub enum Capture { + /// The machine has exited. It cannot be executed again. + Exit(E), + /// The machine has trapped. It is waiting for external information, and can + /// be executed again. + Trap(T), +} + +/// A type alias representing trap data. Should hold the marshalled `Opcode`. +pub type Trap = MarshalledOpcode; + +/// EVM runtime event. +#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)] +pub enum RuntimeEvent { + /// Step. + Step { + /// Context. + context: Context, + /// Opcode. Needs to be marshalled in the runtime no matter what. + opcode: MarshalledOpcode, + /// Position. + position: Result, + /// Stack. + stack: Option, + /// Memory. + memory: Option, + }, + /// Step result. + StepResult { + /// Result. + result: Result<(), Capture>, + /// Return value. + return_value: Vec, + }, + /// Storage load. + SLoad { + /// Address. + address: H160, + /// Index. + index: H256, + /// Value. + value: H256, + }, + /// Storage store. + SStore { + /// Address. + address: H160, + /// Index. + index: H256, + /// Value. + value: H256, + }, +} + +#[cfg(feature = "evm-tracing")] +impl RuntimeEvent { + /// Obtain `RuntimeEvent` from [`evm_runtime::tracing::Event`] based on provided + /// step event filter. + pub fn from_evm_event(event: evm_runtime::tracing::Event<'_>, filter: StepEventFilter) -> Self { + match event { + evm_runtime::tracing::Event::Step { + context, + opcode, + position, + stack, + memory, + } => Self::Step { + context: context.clone().into(), + opcode: (&opcode).into(), + position: match position { + Ok(position) => Ok(*position as u64), + Err(e) => Err(e.clone()), + }, + stack: if filter.enable_stack { + Some(stack.into()) + } else { + None + }, + memory: if filter.enable_memory { + Some(memory.into()) + } else { + None + }, + }, + evm_runtime::tracing::Event::StepResult { + result, + return_value, + } => Self::StepResult { + result: match result { + Ok(_) => Ok(()), + Err(capture) => match capture { + evm::Capture::Exit(e) => Err(Capture::Exit(e.clone())), + evm::Capture::Trap(t) => Err(Capture::Trap(t.into())), + }, + }, + return_value: return_value.to_vec(), + }, + evm_runtime::tracing::Event::SLoad { + address, + index, + value, + } => Self::SLoad { + address, + index, + value, + }, + evm_runtime::tracing::Event::SStore { + address, + index, + value, + } => Self::SStore { + address, + index, + value, + }, + } + } +} + +/// Check whether it's a known opcode or not. In case it's a known one, +/// return the name of the opcode then. +pub fn opcode_known_name(opcode: &evm::Opcode) -> Option<&'static str> { + Some(match opcode.as_u8() { + 0 => "Stop", + 1 => "Add", + 2 => "Mul", + 3 => "Sub", + 4 => "Div", + 5 => "SDiv", + 6 => "Mod", + 7 => "SMod", + 8 => "AddMod", + 9 => "MulMod", + 10 => "Exp", + 11 => "SignExtend", + 16 => "Lt", + 17 => "Gt", + 18 => "Slt", + 19 => "Sgt", + 20 => "Eq", + 21 => "IsZero", + 22 => "And", + 23 => "Or", + 24 => "Xor", + 25 => "Not", + 26 => "Byte", + 27 => "Shl", + 28 => "Shr", + 29 => "Sar", + 32 => "Keccak256", + 48 => "Address", + 49 => "Balance", + 50 => "Origin", + 51 => "Caller", + 52 => "CallValue", + 53 => "CallDataLoad", + 54 => "CallDataSize", + 55 => "CallDataCopy", + 56 => "CodeSize", + 57 => "CodeCopy", + 58 => "GasPrice", + 59 => "ExtCodeSize", + 60 => "ExtCodeCopy", + 61 => "ReturnDataSize", + 62 => "ReturnDataCopy", + 63 => "ExtCodeHash", + 64 => "BlockHash", + 65 => "Coinbase", + 66 => "Timestamp", + 67 => "Number", + 68 => "Difficulty", + 69 => "GasLimit", + 70 => "ChainId", + 80 => "Pop", + 81 => "MLoad", + 82 => "MStore", + 83 => "MStore8", + 84 => "SLoad", + 85 => "SStore", + 86 => "Jump", + 87 => "JumpI", + 88 => "GetPc", + 89 => "MSize", + 90 => "Gas", + 91 => "JumpDest", + 92 => "TLoad", + 93 => "TStore", + 94 => "MCopy", + 96 => "Push1", + 97 => "Push2", + 98 => "Push3", + 99 => "Push4", + 100 => "Push5", + 101 => "Push6", + 102 => "Push7", + 103 => "Push8", + 104 => "Push9", + 105 => "Push10", + 106 => "Push11", + 107 => "Push12", + 108 => "Push13", + 109 => "Push14", + 110 => "Push15", + 111 => "Push16", + 112 => "Push17", + 113 => "Push18", + 114 => "Push19", + 115 => "Push20", + 116 => "Push21", + 117 => "Push22", + 118 => "Push23", + 119 => "Push24", + 120 => "Push25", + 121 => "Push26", + 122 => "Push27", + 123 => "Push28", + 124 => "Push29", + 125 => "Push30", + 126 => "Push31", + 127 => "Push32", + 128 => "Dup1", + 129 => "Dup2", + 130 => "Dup3", + 131 => "Dup4", + 132 => "Dup5", + 133 => "Dup6", + 134 => "Dup7", + 135 => "Dup8", + 136 => "Dup9", + 137 => "Dup10", + 138 => "Dup11", + 139 => "Dup12", + 140 => "Dup13", + 141 => "Dup14", + 142 => "Dup15", + 143 => "Dup16", + 144 => "Swap1", + 145 => "Swap2", + 146 => "Swap3", + 147 => "Swap4", + 148 => "Swap5", + 149 => "Swap6", + 150 => "Swap7", + 151 => "Swap8", + 152 => "Swap9", + 153 => "Swap10", + 154 => "Swap11", + 155 => "Swap12", + 156 => "Swap13", + 157 => "Swap14", + 158 => "Swap15", + 159 => "Swap16", + 160 => "Log0", + 161 => "Log1", + 162 => "Log2", + 163 => "Log3", + 164 => "Log4", + 176 => "JumpTo", + 177 => "JumpIf", + 178 => "JumpSub", + 180 => "JumpSubv", + 181 => "BeginSub", + 182 => "BeginData", + 184 => "ReturnSub", + 185 => "PutLocal", + 186 => "GetLocal", + 225 => "SLoadBytes", + 226 => "SStoreBytes", + 227 => "SSize", + 240 => "Create", + 241 => "Call", + 242 => "CallCode", + 243 => "Return", + 244 => "DelegateCall", + 245 => "Create2", + 250 => "StaticCall", + 252 => "TxExecGas", + 253 => "Revert", + 254 => "Invalid", + 255 => "SelfDestruct", + _ => return None, + }) +} diff --git a/crates/evm-tracing-host-api/Cargo.toml b/crates/evm-tracing-host-api/Cargo.toml new file mode 100644 index 000000000..da1cd3d19 --- /dev/null +++ b/crates/evm-tracing-host-api/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "evm-tracing-host-api" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +evm-tracing-events = { path = "../evm-tracing-events", default-features = false } + +codec = { workspace = true } +sp-runtime-interface = { workspace = true } +sp-std = { workspace = true } + +[features] +default = ["std"] +std = [ + "codec/std", + "evm-tracing-events/std", + "sp-runtime-interface/std", + "sp-std/std", +] diff --git a/crates/evm-tracing-host-api/src/lib.rs b/crates/evm-tracing-host-api/src/lib.rs new file mode 100644 index 000000000..448755ffc --- /dev/null +++ b/crates/evm-tracing-host-api/src/lib.rs @@ -0,0 +1,54 @@ +//! Environmental-aware externalities for EVM tracing in Wasm runtime. This enables +//! capturing the - potentially large - trace output data in the host and keep +//! a low memory footprint in `--execution=wasm`. +//! +//! - The original trace Runtime Api call is wrapped `using` environmental (thread local). +//! - Arguments are scale-encoded known types in the host. +//! - Host functions will decode the input and emit an event `with` environmental. + +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::Decode; +use evm_tracing_events::{Event, EvmEvent, GasometerEvent, RuntimeEvent, StepEventFilter}; +use sp_runtime_interface::runtime_interface; +use sp_std::vec::Vec; + +/// EVM tracing runtime interface. +#[runtime_interface] +pub trait Externalities { + /// An `EvmEvent` proxied by the runtime to this host function. + /// EVM -> runtime -> host. + fn evm_event(&mut self, event: Vec) { + if let Ok(event) = EvmEvent::decode(&mut &event[..]) { + Event::Evm(event).emit(); + } + } + + /// A `GasometerEvent` proxied by the runtime to this host function. + /// EVM gasometer -> runtime -> host. + fn gasometer_event(&mut self, event: Vec) { + if let Ok(event) = GasometerEvent::decode(&mut &event[..]) { + Event::Gasometer(event).emit(); + } + } + + /// A `RuntimeEvent` proxied by the runtime to this host function. + /// EVM runtime -> runtime -> host. + fn runtime_event(&mut self, event: Vec) { + if let Ok(event) = RuntimeEvent::decode(&mut &event[..]) { + Event::Runtime(event).emit(); + } + } + + /// Allow the tracing module in the runtime to know how to filter Step event + /// content, as cloning the entire data is expensive and most of the time + /// not necessary. + fn step_event_filter(&self) -> StepEventFilter { + evm_tracing_events::step_event_filter().unwrap_or_default() + } + + /// An event to create a new `CallList` (currently a new transaction when tracing a block). + fn call_list_new(&mut self) { + Event::CallListNew().emit(); + } +} diff --git a/crates/evm-tracing-rpc/Cargo.toml b/crates/evm-tracing-rpc/Cargo.toml new file mode 100644 index 000000000..0128fcfa2 --- /dev/null +++ b/crates/evm-tracing-rpc/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "evm-tracing-rpc" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +evm-tracing-api = { path = "../evm-tracing-api" } +evm-tracing-client = { path = "../evm-tracing-client" } + +codec = { workspace = true } +ethereum = { workspace = true } +fc-db = { workspace = true } +fc-rpc = { workspace = true } +fc-rpc-core = { workspace = true } +fc-storage = { workspace = true } +fp-rpc = { workspace = true } +frame-support = { workspace = true } +futures = { workspace = true } +hex-literal = { workspace = true } +jsonrpsee = { workspace = true, features = ["server", "macros"] } +sc-client-api = { workspace = true } +sc-utils = { workspace = true } +serde = { workspace = true, features = ["derive"] } +sp-api = { workspace = true } +sp-block-builder = { workspace = true } +sp-blockchain = { workspace = true } +sp-core = { workspace = true, features = ["std"] } +sp-io = { workspace = true } +sp-runtime = { workspace = true } +substrate-prometheus-endpoint = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } diff --git a/crates/evm-tracing-rpc/src/debug/core.rs b/crates/evm-tracing-rpc/src/debug/core.rs new file mode 100644 index 000000000..74feb20e2 --- /dev/null +++ b/crates/evm-tracing-rpc/src/debug/core.rs @@ -0,0 +1,83 @@ +//! Core. + +use ethereum::AccessListItem; +use evm_tracing_client::types::{block, single}; +use fc_rpc_core::types::Bytes; +use jsonrpsee::{core::RpcResult, proc_macros::rpc}; +use serde::Deserialize; +use sp_core::{H160, H256, U256}; + +use crate::types::RequestBlockId; + +/// Trace params. +#[derive(Clone, Eq, PartialEq, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TraceParams { + /// Disable storage flag. + pub disable_storage: Option, + /// Disable memory flag. + pub disable_memory: Option, + /// Disable stack flag. + pub disable_stack: Option, + /// Javascript tracer (we just check if it's Blockscout tracer string). + pub tracer: Option, + /// Timeout. + pub timeout: Option, +} + +/// Trace call params. +#[derive(Debug, Clone, Default, Eq, PartialEq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TraceCallParams { + /// Sender. + pub from: Option, + /// Recipient. + pub to: H160, + /// Gas price. + pub gas_price: Option, + /// Max `BaseFeePerGas` the user is willing to pay. + pub max_fee_per_gas: Option, + /// The miner's tip. + pub max_priority_fee_per_gas: Option, + /// Gas. + pub gas: Option, + /// Value of transaction in wei. + pub value: Option, + /// Additional data sent with transaction. + pub data: Option, + /// Nonce. + pub nonce: Option, + /// EIP-2930 access list. + pub access_list: Option>, + /// EIP-2718 type. + #[serde(rename = "type")] + pub transaction_type: Option, +} + +#[rpc(server)] +pub trait Debug { + /// Trace transaction. + #[method(name = "debug_traceTransaction")] + async fn trace_transaction( + &self, + transaction_hash: H256, + params: Option, + ) -> RpcResult; + + /// Trace call. + #[method(name = "debug_traceCall")] + async fn trace_call( + &self, + call_params: TraceCallParams, + id: RequestBlockId, + params: Option, + ) -> RpcResult; + + /// Trace block. + #[method(name = "debug_traceBlockByNumber", aliases = ["debug_traceBlockByHash"])] + async fn trace_block( + &self, + id: RequestBlockId, + params: Option, + ) -> RpcResult>; +} diff --git a/crates/evm-tracing-rpc/src/debug/handler.rs b/crates/evm-tracing-rpc/src/debug/handler.rs new file mode 100644 index 000000000..9ec616dca --- /dev/null +++ b/crates/evm-tracing-rpc/src/debug/handler.rs @@ -0,0 +1,787 @@ +//! Debug handler implementation. + +use std::{collections::BTreeMap, future::Future, marker::PhantomData, sync::Arc}; + +use evm_tracing_api::EvmTracingApi; +use evm_tracing_client::{ + formatters::ResponseFormatter, + types::{ + block::BlockTransactionTrace, + call_tracer::CallTracerInner, + single::{self, TransactionTrace}, + }, +}; +use fc_rpc::{frontier_backend_client, internal_err, OverrideHandle}; +use fp_rpc::EthereumRuntimeRPCApi; +use futures::StreamExt; +use jsonrpsee::core::RpcResult; +use sc_client_api::backend::{Backend, StateBackend, StorageProvider}; +use sp_api::{ApiExt, ProvideRuntimeApi}; +use sp_block_builder::BlockBuilder; +use sp_blockchain::{ + Backend as BlockchainBackend, Error as BlockChainError, HeaderBackend, HeaderMetadata, +}; +use sp_core::{H160, H256}; +use sp_runtime::{ + generic::BlockId, + traits::{BlakeTwo256, Block as BlockT, Header as HeaderT, UniqueSaturatedInto}, +}; +use tokio::sync::Semaphore; + +use super::{ + core::{TraceCallParams, TraceParams}, + DebugRequester, RequesterInput, Response, +}; +use crate::types::{RequestBlockId, RequestBlockTag, TracerInput, TracerResponse}; + +/// Debug handler. +pub struct DebugHandler(PhantomData<(B, C, BE)>); + +impl DebugHandler +where + BE: Backend + 'static, + BE::State: StateBackend, + C: ProvideRuntimeApi, + C: StorageProvider, + C: HeaderMetadata + HeaderBackend, + C: Send + Sync + 'static, + B: BlockT + Send + Sync + 'static, + C::Api: BlockBuilder, + C::Api: EvmTracingApi, + C::Api: EthereumRuntimeRPCApi, + C::Api: ApiExt, +{ + /// Task spawned at service level that listens for messages on the rpc channel and spawns + /// blocking tasks using a permit pool. + pub fn task( + client: Arc, + backend: Arc, + frontier_backend: Arc + Send + Sync>, + permit_pool: Arc, + overrides: Arc>, + raw_max_memory_usage: usize, + ) -> (impl Future, DebugRequester) { + let (tx, mut rx): (DebugRequester, _) = + sc_utils::mpsc::tracing_unbounded("debug-requester", 100_000); + + let fut = async move { + loop { + match rx.next().await { + Some(( + (RequesterInput::Transaction(transaction_hash), params), + response_tx, + )) => { + let client = Arc::clone(&client); + let backend = Arc::clone(&backend); + let frontier_backend = Arc::clone(&frontier_backend); + let permit_pool = Arc::clone(&permit_pool); + let overrides = Arc::clone(&overrides); + + tokio::task::spawn(async move { + let _ = response_tx.send( + async { + let _permit = permit_pool.acquire().await; + tokio::task::spawn_blocking(move || { + Self::handle_transaction_request( + client, + backend, + frontier_backend, + transaction_hash, + params, + overrides, + raw_max_memory_usage, + ) + }) + .await + .map_err(|e| { + internal_err(format!( + "Internal error on spawned task : {:?}", + e + )) + })? + } + .await, + ); + }); + } + Some(( + (RequesterInput::Call((request_block_id, call_params)), params), + response_tx, + )) => { + let client = Arc::clone(&client); + let frontier_backend = Arc::clone(&frontier_backend); + let permit_pool = Arc::clone(&permit_pool); + + tokio::task::spawn(async move { + let _ = response_tx.send( + async { + let _permit = permit_pool.acquire().await; + tokio::task::spawn_blocking(move || { + Self::handle_call_request( + client, + frontier_backend, + request_block_id, + call_params, + params, + raw_max_memory_usage, + ) + }) + .await + .map_err(|e| { + internal_err(format!( + "Internal error on spawned task : {:?}", + e + )) + })? + } + .await, + ); + }); + } + Some(((RequesterInput::Block(request_block_id), params), response_tx)) => { + let client = Arc::clone(&client); + let backend = Arc::clone(&backend); + let frontier_backend = Arc::clone(&frontier_backend); + let permit_pool = Arc::clone(&permit_pool); + let overrides = Arc::clone(&overrides); + + tokio::task::spawn(async move { + let _ = response_tx.send( + async { + let _permit = permit_pool.acquire().await; + + tokio::task::spawn_blocking(move || { + Self::handle_block_request( + client, + backend, + frontier_backend, + request_block_id, + params, + overrides, + ) + }) + .await + .map_err(|e| { + internal_err(format!( + "Internal error on spawned task : {:?}", + e + )) + })? + } + .await, + ); + }); + } + _ => {} + } + } + }; + (fut, tx) + } + + /// Handle params. + fn handle_params(params: Option) -> RpcResult<(TracerInput, single::TraceType)> { + // Set trace input and type + match params { + Some(TraceParams { + tracer: Some(tracer), + .. + }) => { + /// Blockscout related js code hash. + const BLOCKSCOUT_JS_CODE_HASH: [u8; 16] = + hex_literal::hex!("94d9f08796f91eb13a2e82a6066882f7"); + /// Blockscout V2 related js code hash. + const BLOCKSCOUT_JS_CODE_HASH_V2: [u8; 16] = + hex_literal::hex!("89db13694675692951673a1e6e18ff02"); + let hash = sp_io::hashing::twox_128(tracer.as_bytes()); + let tracer = + if hash == BLOCKSCOUT_JS_CODE_HASH || hash == BLOCKSCOUT_JS_CODE_HASH_V2 { + Some(TracerInput::Blockscout) + } else if tracer == "callTracer" { + Some(TracerInput::CallTracer) + } else { + None + }; + if let Some(tracer) = tracer { + Ok((tracer, single::TraceType::CallList)) + } else { + Err(internal_err(format!( + "javascript based tracing is not available (hash :{:?})", + hash + ))) + } + } + Some(params) => Ok(( + TracerInput::None, + single::TraceType::Raw { + disable_storage: params.disable_storage.unwrap_or(false), + disable_memory: params.disable_memory.unwrap_or(false), + disable_stack: params.disable_stack.unwrap_or(false), + }, + )), + _ => Ok(( + TracerInput::None, + single::TraceType::Raw { + disable_storage: false, + disable_memory: false, + disable_stack: false, + }, + )), + } + } + + /// Handle block request. + fn handle_block_request( + client: Arc, + backend: Arc, + frontier_backend: Arc + Send + Sync>, + request_block_id: RequestBlockId, + params: Option, + overrides: Arc>, + ) -> RpcResult { + let (tracer_input, trace_type) = Self::handle_params(params)?; + + let reference_id: BlockId = match request_block_id { + RequestBlockId::Number(n) => Ok(BlockId::Number(n.unique_saturated_into())), + RequestBlockId::Tag(RequestBlockTag::Latest) => { + Ok(BlockId::Number(client.info().best_number)) + } + RequestBlockId::Tag(RequestBlockTag::Earliest) => { + Ok(BlockId::Number(0u32.unique_saturated_into())) + } + RequestBlockId::Tag(RequestBlockTag::Pending) => { + Err(internal_err("'pending' blocks are not supported")) + } + RequestBlockId::Hash(eth_hash) => { + match futures::executor::block_on(frontier_backend_client::load_hash::( + client.as_ref(), + frontier_backend.as_ref(), + eth_hash, + )) { + Ok(Some(hash)) => Ok(BlockId::Hash(hash)), + Ok(_) => Err(internal_err("Block hash not found".to_string())), + Err(e) => Err(e), + } + } + }?; + + // Get `ApiRef`. This handle allows to keep changes between txs in an internal buffer. + let api = client.runtime_api(); + + // Get Blockchain backend + let blockchain = backend.blockchain(); + // Get the header I want to work with. + let Ok(hash) = client.expect_block_hash_from_id(&reference_id) else { + return Err(internal_err("Block header not found")); + }; + let header = match client.header(hash) { + Ok(Some(h)) => h, + _ => return Err(internal_err("Block header not found")), + }; + + // Get parent blockid. + let parent_block_hash = *header.parent_hash(); + + let statuses = overrides + .fallback + .current_transaction_statuses(hash) + .unwrap_or_default(); + + /// Partial ethereum transaction data to check if a trace match an ethereum transaction. + struct EthTxPartial { + /// Transaction hash. + transaction_hash: H256, + /// From address. + from: H160, + /// To address. + to: Option, + } + + // Known ethereum transaction hashes. + let eth_transactions_by_index: BTreeMap = statuses + .iter() + .map(|status| { + ( + status.transaction_index, + EthTxPartial { + transaction_hash: status.transaction_hash, + from: status.from, + to: status.to, + }, + ) + }) + .collect(); + + let eth_tx_hashes: Vec<_> = eth_transactions_by_index + .values() + .map(|tx| tx.transaction_hash) + .collect(); + + // If there are no ethereum transactions in the block return empty trace right away. + if eth_tx_hashes.is_empty() { + return Ok(Response::Block(vec![])); + } + + // Get block extrinsics. + let exts = blockchain + .body(hash) + .map_err(|e| internal_err(format!("Fail to read blockchain db: {:?}", e)))? + .unwrap_or_default(); + + // Trace the block. + let f = || -> RpcResult<_> { + let result = api.trace_block(parent_block_hash, exts, eth_tx_hashes, &header); + + result + .map_err(|e| { + internal_err(format!( + "Blockchain error when replaying block {} : {:?}", + reference_id, e + )) + })? + .map_err(|e| { + internal_err(format!( + "Internal runtime error when replaying block {} : {:?}", + reference_id, e + )) + })?; + + Ok(TracerResponse::Block) + }; + + // Offset to account for old buggy transactions that are in trace not in the ethereum block. + let mut tx_position_offset = 0; + + match trace_type { + single::TraceType::CallList => { + let mut proxy = evm_tracing_client::listeners::call_list::Listener::default(); + proxy.using(f)?; + proxy.finish_transaction(); + let response = match tracer_input { + TracerInput::CallTracer => { + let result = + evm_tracing_client::formatters::call_tracer::Formatter::format(proxy) + .ok_or("Trace result is empty.") + .map_err(|e| internal_err(format!("{:?}", e)))? + .into_iter() + .filter_map(|mut trace: BlockTransactionTrace| { + if let Some(EthTxPartial { + transaction_hash, + from, + to, + }) = eth_transactions_by_index.get( + &(trace.tx_position.checked_sub(tx_position_offset)) + .expect("valid operation; qed"), + ) { + // Verify that the trace matches the ethereum transaction. + let (trace_from, trace_to) = match trace.result { + TransactionTrace::Raw { .. } + | TransactionTrace::CallList(_) => { + (Default::default(), None) + } + TransactionTrace::CallListNested(ref call) => { + match call { + single::Call::Blockscout(_) => { + (Default::default(), None) + } + single::Call::CallTracer(call) => ( + call.from, + match call.inner { + CallTracerInner::Call { + to, .. + } => Some(to), + CallTracerInner::Create { .. } + | CallTracerInner::SelfDestruct { + .. + } => None, + }, + ), + } + } + }; + if trace_from == *from && trace_to == *to { + trace.tx_hash = *transaction_hash; + Some(trace) + } else { + // If the trace does not match the ethereum transaction + // it means that the trace is about a buggy transaction that is not in the block + // we need to offset the tx_position. + tx_position_offset = tx_position_offset + .checked_add(1) + .expect("valid operation; qed"); + + None + } + } else { + // If the transaction is not in the ethereum block + // it should not appear in the block trace. + tx_position_offset = tx_position_offset + .checked_add(1) + .expect("valid operation; qed"); + + None + } + }) + .collect::>(); + + let n_txs = eth_transactions_by_index.len(); + let n_traces = result.len(); + if n_txs != n_traces { + frame_support::log::warn!( + "The traces in block {:?} don't match with the number of ethereum transactions. (txs: {}, traces: {})", + request_block_id, + n_txs, + n_traces + ); + } + + Ok(result) + } + _ => Err(internal_err( + "Bug: failed to resolve the tracer format.".to_string(), + )), + }?; + + Ok(Response::Block(response)) + } + _ => Err(internal_err( + "debug_traceBlock functions currently only support callList mode (enabled + by providing `{{'tracer': 'callTracer'}}` in the request)." + .to_string(), + )), + } + } + + /// Replays a transaction in the Runtime at a given block height. + /// + /// In order to successfully reproduce the result of the original transaction we need a correct + /// state to replay over. + /// + /// Substrate allows to apply extrinsics in the Runtime and thus creating an overlaid state. + /// These overlaid changes will live in-memory for the lifetime of the `ApiRef`. + fn handle_transaction_request( + client: Arc, + backend: Arc, + frontier_backend: Arc + Send + Sync>, + transaction_hash: H256, + params: Option, + overrides: Arc>, + raw_max_memory_usage: usize, + ) -> RpcResult { + let (tracer_input, trace_type) = Self::handle_params(params)?; + + let (hash, index) = + match futures::executor::block_on(frontier_backend_client::load_transactions::( + client.as_ref(), + frontier_backend.as_ref(), + transaction_hash, + false, + )) { + Ok(Some((hash, index))) => (hash, index as usize), + Ok(None) => return Err(internal_err("Transaction hash not found".to_string())), + Err(e) => return Err(e), + }; + + let reference_id = + match futures::executor::block_on(frontier_backend_client::load_hash::( + client.as_ref(), + frontier_backend.as_ref(), + hash, + )) { + Ok(Some(hash)) => BlockId::Hash(hash), + Ok(_) => return Err(internal_err("Block hash not found".to_string())), + Err(e) => return Err(e), + }; + // Get `ApiRef`. This handle allow to keep changes between txs in an internal buffer. + let api = client.runtime_api(); + + // Get blockchain backend. + let blockchain = backend.blockchain(); + // Get the header I want to work with. + let Ok(reference_hash) = client.expect_block_hash_from_id(&reference_id) else { + return Err(internal_err("Block header not found")); + }; + let header = match client.header(reference_hash) { + Ok(Some(h)) => h, + _ => return Err(internal_err("Block header not found")), + }; + // Get parent blockid. + let parent_block_hash = *header.parent_hash(); + + // Get block extrinsics. + let exts = blockchain + .body(reference_hash) + .map_err(|e| internal_err(format!("Fail to read blockchain db: {:?}", e)))? + .unwrap_or_default(); + + let reference_block = overrides + .schemas + .get(&fc_storage::onchain_storage_schema( + client.as_ref(), + reference_hash, + )) + .unwrap_or(&overrides.fallback) + .current_block(reference_hash); + + // Get the actual ethereum transaction. + if let Some(block) = reference_block { + let transactions = block.transactions; + if let Some(transaction) = transactions.get(index) { + let f = || -> RpcResult<_> { + let result = + api.trace_transaction(parent_block_hash, exts, transaction, &header); + + result + .map_err(|e| internal_err(format!("Runtime api access error: {:?}", e)))? + .map_err(|e| internal_err(format!("DispatchError: {:?}", e)))?; + + Ok(TracerResponse::Single) + }; + + return match trace_type { + single::TraceType::Raw { + disable_storage, + disable_memory, + disable_stack, + } => { + let mut proxy = evm_tracing_client::listeners::raw::Listener::new( + disable_storage, + disable_memory, + disable_stack, + raw_max_memory_usage, + ); + proxy.using(f)?; + Ok(Response::Single( + evm_tracing_client::formatters::raw::Formatter::format(proxy).ok_or( + internal_err( + "replayed transaction generated too much data. \ + try disabling memory or storage?", + ), + )?, + )) + } + single::TraceType::CallList => { + let mut proxy = + evm_tracing_client::listeners::call_list::Listener::default(); + proxy.using(f)?; + proxy.finish_transaction(); + let response = match tracer_input { + TracerInput::Blockscout => { + evm_tracing_client::formatters::blockscout::Formatter::format(proxy) + .ok_or("Trace result is empty.") + .map_err(|e| internal_err(format!("{:?}", e))) + } + TracerInput::CallTracer => { + let mut res = + evm_tracing_client::formatters::call_tracer::Formatter::format( + proxy, + ) + .ok_or("Trace result is empty.") + .map_err(|e| internal_err(format!("{:?}", e)))?; + Ok(res.pop().expect("Trace result is empty.").result) + } + _ => Err(internal_err( + "Bug: failed to resolve the tracer format.".to_string(), + )), + }?; + Ok(Response::Single(response)) + } + not_supported => Err(internal_err(format!( + "Bug: `handle_transaction_request` does not support {:?}.", + not_supported + ))), + }; + } + } + Err(internal_err("Runtime block call failed".to_string())) + } + + /// Handle call request. + fn handle_call_request( + client: Arc, + frontier_backend: Arc + Send + Sync>, + request_block_id: RequestBlockId, + call_params: TraceCallParams, + trace_params: Option, + raw_max_memory_usage: usize, + ) -> RpcResult { + let (tracer_input, trace_type) = Self::handle_params(trace_params)?; + + let reference_id: BlockId = match request_block_id { + RequestBlockId::Number(n) => Ok(BlockId::Number(n.unique_saturated_into())), + RequestBlockId::Tag(RequestBlockTag::Latest) => { + Ok(BlockId::Number(client.info().best_number)) + } + RequestBlockId::Tag(RequestBlockTag::Earliest) => { + Ok(BlockId::Number(0u32.unique_saturated_into())) + } + RequestBlockId::Tag(RequestBlockTag::Pending) => { + Err(internal_err("'pending' blocks are not supported")) + } + RequestBlockId::Hash(eth_hash) => { + match futures::executor::block_on(frontier_backend_client::load_hash::( + client.as_ref(), + frontier_backend.as_ref(), + eth_hash, + )) { + Ok(Some(hash)) => Ok(BlockId::Hash(hash)), + Ok(_) => Err(internal_err("Block hash not found".to_string())), + Err(e) => Err(e), + } + } + }?; + + let api = client.runtime_api(); + + // Get the header I want to work with. + let Ok(hash) = client.expect_block_hash_from_id(&reference_id) else { + return Err(internal_err("Block header not found")); + }; + let header = match client.header(hash) { + Ok(Some(h)) => h, + _ => return Err(internal_err("Block header not found")), + }; + // Get parent blockid. + let parent_block_hash = *header.parent_hash(); + + let TraceCallParams { + from, + to, + gas_price, + max_fee_per_gas, + max_priority_fee_per_gas, + gas, + value, + data, + nonce, + access_list, + .. + } = call_params; + + let (max_fee_per_gas, max_priority_fee_per_gas) = + match (gas_price, max_fee_per_gas, max_priority_fee_per_gas) { + (gas_price, None, None) => { + // Legacy request, all default to gas price. A zero-set gas price is None. + let gas_price = if gas_price.unwrap_or_default().is_zero() { + None + } else { + gas_price + }; + (gas_price, gas_price) + } + (_, max_fee, max_priority) => { + // EIP-1559: A zero-set max fee is None. + let max_fee = if max_fee.unwrap_or_default().is_zero() { + None + } else { + max_fee + }; + // Ensure `max_priority_fee_per_gas` is less or equal to `max_fee_per_gas`. + if let Some(max_priority) = max_priority { + let max_fee = max_fee.unwrap_or_default(); + if max_priority > max_fee { + return Err(internal_err( + "Invalid input: `max_priority_fee_per_gas` greater than `max_fee_per_gas`", + )); + } + } + (max_fee, max_priority) + } + }; + + let gas_limit = match gas { + Some(amount) => amount, + None => { + if let Some(block) = api + .current_block(parent_block_hash) + .map_err(|err| internal_err(format!("runtime error: {:?}", err)))? + { + block.header.gas_limit + } else { + return Err(internal_err( + "block unavailable, cannot query gas limit".to_string(), + )); + } + } + }; + let data = data.map(|d| d.0).unwrap_or_default(); + + let access_list = access_list.unwrap_or_default(); + + let f = || -> RpcResult<_> { + api.trace_call( + parent_block_hash, + &header, + from.unwrap_or_default(), + to, + data, + value.unwrap_or_default(), + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + nonce, + Some( + access_list + .into_iter() + .map(|item| (item.address, item.storage_keys)) + .collect(), + ), + ) + .map_err(|e| internal_err(format!("Runtime api access error: {:?}", e)))? + .map_err(|e| internal_err(format!("DispatchError: {:?}", e)))?; + + Ok(TracerResponse::Single) + }; + + match trace_type { + single::TraceType::Raw { + disable_storage, + disable_memory, + disable_stack, + } => { + let mut proxy = evm_tracing_client::listeners::raw::Listener::new( + disable_storage, + disable_memory, + disable_stack, + raw_max_memory_usage, + ); + proxy.using(f)?; + Ok(Response::Single( + evm_tracing_client::formatters::raw::Formatter::format(proxy).ok_or( + internal_err( + "replayed transaction generated too much data. \ + try disabling memory or storage?", + ), + )?, + )) + } + single::TraceType::CallList => { + let mut proxy = evm_tracing_client::listeners::call_list::Listener::default(); + proxy.using(f)?; + proxy.finish_transaction(); + let response = match tracer_input { + TracerInput::Blockscout => { + evm_tracing_client::formatters::blockscout::Formatter::format(proxy) + .ok_or("Trace result is empty.") + .map_err(|e| internal_err(format!("{:?}", e))) + } + TracerInput::CallTracer => { + let mut res = + evm_tracing_client::formatters::call_tracer::Formatter::format(proxy) + .ok_or("Trace result is empty.") + .map_err(|e| internal_err(format!("{:?}", e)))?; + Ok(res.pop().expect("Trace result is empty.").result) + } + _ => Err(internal_err( + "Bug: failed to resolve the tracer format.".to_string(), + )), + }?; + Ok(Response::Single(response)) + } + not_supported => Err(internal_err(format!( + "Bug: `handle_call_request` does not support {:?}.", + not_supported + ))), + } + } +} diff --git a/crates/evm-tracing-rpc/src/debug/mod.rs b/crates/evm-tracing-rpc/src/debug/mod.rs new file mode 100644 index 000000000..5ae6db908 --- /dev/null +++ b/crates/evm-tracing-rpc/src/debug/mod.rs @@ -0,0 +1,136 @@ +//! Debug related implementation. + +use core::{DebugServer, TraceCallParams, TraceParams}; + +use evm_tracing_client::types::{ + block::{self, BlockTransactionTrace}, + single, +}; +use fc_rpc::internal_err; +use jsonrpsee::core::{async_trait, RpcResult}; +use sc_utils::mpsc::TracingUnboundedSender; +use sp_core::H256; +use tokio::sync::oneshot; + +pub mod core; +mod handler; + +pub use handler::DebugHandler; + +use crate::types::RequestBlockId; + +/// Requester input. +#[allow(clippy::large_enum_variant)] +pub enum RequesterInput { + /// Call. + Call((RequestBlockId, TraceCallParams)), + /// Transaction. + Transaction(H256), + /// Block. + Block(RequestBlockId), +} + +/// Response. +pub enum Response { + /// Single transaction data. + Single(single::TransactionTrace), + /// Block data. + Block(Vec), +} + +/// Responder type alias. +pub type Responder = oneshot::Sender>; +/// Debug requester type alias. +pub type DebugRequester = + TracingUnboundedSender<((RequesterInput, Option), Responder)>; + +/// Debug. +pub struct Debug { + /// Requester. + pub requester: DebugRequester, +} + +#[async_trait] +impl DebugServer for Debug { + async fn trace_transaction( + &self, + transaction_hash: H256, + params: Option, + ) -> RpcResult { + let requester = self.requester.clone(); + + let (tx, rx) = oneshot::channel(); + // Send a message from the rpc handler to the service level task. + requester + .unbounded_send(((RequesterInput::Transaction(transaction_hash), params), tx)) + .map_err(|err| { + internal_err(format!( + "failed to send request to debug service : {:?}", + err + )) + })?; + + // Receive a message from the service level task and send the rpc response. + rx.await + .map_err(|err| internal_err(format!("debug service dropped the channel : {:?}", err)))? + .map(|res| match res { + Response::Single(res) => res, + _ => unreachable!(), + }) + } + + async fn trace_block( + &self, + id: RequestBlockId, + params: Option, + ) -> RpcResult> { + let requester = self.requester.clone(); + + let (tx, rx) = oneshot::channel(); + // Send a message from the rpc handler to the service level task. + requester + .unbounded_send(((RequesterInput::Block(id), params), tx)) + .map_err(|err| { + internal_err(format!( + "failed to send request to debug service : {:?}", + err + )) + })?; + + // Receive a message from the service level task and send the rpc response. + rx.await + .map_err(|err| internal_err(format!("debug service dropped the channel : {:?}", err)))? + .map(|res| match res { + Response::Block(res) => res, + _ => unreachable!(), + }) + } + + async fn trace_call( + &self, + call_params: TraceCallParams, + id: RequestBlockId, + params: Option, + ) -> RpcResult { + let requester = self.requester.clone(); + + let (tx, rx) = oneshot::channel(); + // Send a message from the rpc handler to the service level task. + requester + .unbounded_send(((RequesterInput::Call((id, call_params)), params), tx)) + .map_err(|err| { + internal_err(format!( + "failed to send request to debug service : {:?}", + err + )) + })?; + + // Receive a message from the service level task and send the rpc response. + rx.await + .map_err(|err| internal_err(format!("debug service dropped the channel : {:?}", err)))? + .map(|res| match res { + Response::Single(res) => res, + _ => unreachable!(), + }) + } +} diff --git a/crates/evm-tracing-rpc/src/lib.rs b/crates/evm-tracing-rpc/src/lib.rs new file mode 100644 index 000000000..ea975fda6 --- /dev/null +++ b/crates/evm-tracing-rpc/src/lib.rs @@ -0,0 +1,5 @@ +//! RPC interface for the EVM tracing logic. + +pub mod debug; +pub mod trace; +pub mod types; diff --git a/crates/evm-tracing-rpc/src/trace/cache_requester.rs b/crates/evm-tracing-rpc/src/trace/cache_requester.rs new file mode 100644 index 000000000..50eeb39c0 --- /dev/null +++ b/crates/evm-tracing-rpc/src/trace/cache_requester.rs @@ -0,0 +1,121 @@ +//! Cache requester. + +use sc_utils::mpsc::TracingUnboundedSender; +use sp_core::H256; +use tokio::sync::oneshot; +use tracing::instrument; + +use super::TxsTraceRes; + +/// An opaque batch ID. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct CacheBatchId(pub u64); + +/// Requests the cache task can accept. +pub enum CacheRequest { + /// Request to start caching the provided range of blocks. + /// The task will add to blocks to its pool and immediately return a new batch ID. + StartBatch { + /// Returns the ID of the batch for cancellation. + sender: oneshot::Sender, + /// List of block hash to trace. + blocks: Vec, + }, + /// Fetch the traces for given block hash. + /// The task will answer only when it has processed this block. + GetTraces { + /// Returns the array of traces or an error. + sender: oneshot::Sender, + /// Hash of the block. + block: H256, + }, + /// Notify the cache that it can stop the batch with that ID. Any block contained only in + /// this batch and still not started will be discarded. + StopBatch { + /// Batch identifier. + batch_id: CacheBatchId, + }, +} + +/// Allows to interact with the cache task. +#[derive(Clone)] +pub struct CacheRequester(pub TracingUnboundedSender); + +impl CacheRequester { + /// Request to start caching the provided range of blocks. + /// The task will add to blocks to its pool and immediately return the batch ID. + #[instrument(skip(self))] + pub async fn start_batch(&self, blocks: Vec) -> Result { + let (response_tx, response_rx) = oneshot::channel(); + let sender = self.0.clone(); + + sender + .unbounded_send(CacheRequest::StartBatch { + sender: response_tx, + blocks, + }) + .map_err(|e| { + format!( + "Failed to send request to the trace cache task. Error : {:?}", + e + ) + })?; + + response_rx.await.map_err(|e| { + format!( + "Trace cache task closed the response channel. Error : {:?}", + e + ) + }) + } + + /// Fetch the traces for given block hash. + /// The task will answer only when it has processed this block. + /// The block should be part of a batch first. If no batch has requested the block it will + /// return an error. + #[instrument(skip(self))] + pub async fn get_traces(&self, block: H256) -> TxsTraceRes { + let (response_tx, response_rx) = oneshot::channel(); + let sender = self.0.clone(); + + sender + .unbounded_send(CacheRequest::GetTraces { + sender: response_tx, + block, + }) + .map_err(|e| { + format!( + "Failed to send request to the trace cache task. Error : {:?}", + e + ) + })?; + + response_rx + .await + .map_err(|e| { + format!( + "Trace cache task closed the response channel. Error : {:?}", + e + ) + })? + .map_err(|e| format!("Failed to replay block. Error : {:?}", e)) + } + + /// Notify the cache that it can stop the batch with that ID. Any block contained only in + /// this batch and still in the waiting pool will be discarded. + #[instrument(skip(self))] + pub async fn stop_batch(&self, batch_id: CacheBatchId) { + let sender = self.0.clone(); + + // Here we don't care if the request has been accepted or refused, the caller can't + // do anything with it. + let _ = sender + .unbounded_send(CacheRequest::StopBatch { batch_id }) + .map_err(|e| { + format!( + "Failed to send request to the trace cache task. Error : {:?}", + e + ) + }); + } +} diff --git a/crates/evm-tracing-rpc/src/trace/cache_task.rs b/crates/evm-tracing-rpc/src/trace/cache_task.rs new file mode 100644 index 000000000..237310c23 --- /dev/null +++ b/crates/evm-tracing-rpc/src/trace/cache_task.rs @@ -0,0 +1,643 @@ +//! Cache task. + +use std::future::Future; +use std::{collections::BTreeMap, marker::PhantomData, sync::Arc, time::Duration}; + +use evm_tracing_api::EvmTracingApi; +use evm_tracing_client::{ + formatters::ResponseFormatter, + types::block::{self, TransactionTrace}, +}; +use fc_rpc::OverrideHandle; +use fp_rpc::EthereumRuntimeRPCApi; +use futures::stream::FuturesUnordered; +use futures::{select, FutureExt, StreamExt}; +use sc_client_api::{Backend, StateBackend, StorageProvider}; +use sp_api::{ApiExt, ProvideRuntimeApi}; +use sp_block_builder::BlockBuilder; +use sp_blockchain::{ + Backend as BlockchainBackend, Error as BlockChainError, HeaderBackend, HeaderMetadata, +}; +use sp_core::H256; +use sp_runtime::traits::{BlakeTwo256, Block as BlockT, Header as HeaderT}; +use substrate_prometheus_endpoint::{ + register, Counter, PrometheusError, Registry as PrometheusRegistry, U64, +}; +use tokio::{ + sync::{mpsc, oneshot, Semaphore}, + time::sleep, +}; +use tracing::{instrument, Instrument}; + +use super::{ + cache_requester::{CacheBatchId, CacheRequester}, + TxsTraceRes, +}; +use crate::{trace::cache_requester::CacheRequest, types::TracerResponse}; + +/// Data stored for each block in the cache. +/// `active_batch_count` represents the number of batches using this +/// block. It will increase immediately when a batch is created, but will be +/// decrease only after the batch ends and its expiration delay passes. +/// It allows to keep the data in the cache for following requests that would use +/// this block, which is important to handle pagination efficiently. +struct CacheBlock { + /// Active batch count. + active_batch_count: usize, + /// State. + state: CacheBlockState, +} + +/// State of a cached block. It can either be polled to be traced or cached. +enum CacheBlockState { + /// Block has been added to the pool blocks to be replayed. + /// It may be currently waiting to be replayed or being replayed. + Pooled { + /// Started flag. + started: bool, + /// Multiple requests might query the same block while it is pooled to be + /// traced. They response channel is stored here, and the result will be + /// sent in all of them when the tracing is finished. + waiting_requests: Vec>, + /// Channel used to unqueue a tracing that has not yet started. + /// A tracing will be unqueued if it has not yet been started and the last batch + /// needing this block is ended (ignoring the expiration delay). + /// It is not used directly, but dropping will wake up the receiver. + #[allow(dead_code)] + unqueue_sender: oneshot::Sender<()>, + }, + /// Tracing has been completed and the result is available. No Runtime API call + /// will be needed until this block cache is removed. + Cached { + /// Traces. + traces: TxsTraceRes, + }, +} + +/// Tracing a block is done in a separate tokio blocking task to avoid clogging the async threads. +/// For this reason a channel using this type is used by the blocking task to communicate with the +/// main cache task. +enum BlockingTaskMessage { + /// Notify the tracing for this block has started as the blocking task got a permit from + /// the semaphore. This is used to prevent the deletion of a cache entry for a block that has + /// started being traced. + Started { + /// Block hash. + block_hash: H256, + }, + /// The tracing is finished and the result is sent to the main task. + Finished { + /// Block hash. + block_hash: H256, + /// Result. + result: TxsTraceRes, + }, +} + +/// Type wrapper for the cache task, generic over the Client, Block and Backend types. +pub struct CacheTask { + /// Inner client. + client: Arc, + /// Backend. + backend: Arc, + /// Blocking permits. + blocking_permits: Arc, + /// Cached blocks. + cached_blocks: BTreeMap, + /// Batches. + batches: BTreeMap>, + /// Next batch id. + next_batch_id: u64, + /// Metrics. + metrics: Option, + /// Phantom data. + _phantom: PhantomData, +} + +impl CacheTask +where + BE: Backend + 'static, + BE::State: StateBackend, + C: ProvideRuntimeApi, + C: StorageProvider, + C: HeaderMetadata + HeaderBackend, + C: Send + Sync + 'static, + B: BlockT + Send + Sync + 'static, + B::Header: HeaderT, + C::Api: BlockBuilder, + C::Api: EvmTracingApi, + C::Api: EthereumRuntimeRPCApi, + C::Api: ApiExt, +{ + /// Create a new cache task. + /// + /// Returns a Future that needs to be added to a tokio executor, and a handle allowing to + /// send requests to the task. + pub fn create( + client: Arc, + backend: Arc, + cache_duration: Duration, + blocking_permits: Arc, + overrides: Arc>, + prometheus: Option, + ) -> (impl Future, CacheRequester) { + let (requester_tx, mut requester_rx) = + sc_utils::mpsc::tracing_unbounded("trace-filter-cache", 100_000); + + // Task running in the service. + let task = async move { + // The following variables are polled by the select! macro, and thus cannot be + // part of Self without introducing borrowing issues. + let mut batch_expirations = FuturesUnordered::new(); + let (blocking_tx, mut blocking_rx) = + mpsc::channel(blocking_permits.available_permits().saturating_mul(2)); + let metrics = if let Some(registry) = prometheus { + match Metrics::register(®istry) { + Ok(metrics) => Some(metrics), + Err(err) => { + frame_support::log::error!(target: "tracing", "Failed to register metrics {err:?}"); + None + } + } + } else { + None + }; + // Contains the inner state of the cache task, excluding the pooled futures/channels. + // Having this object allows to refactor each event into its own function, simplifying + // the main loop. + let mut inner = Self { + client, + backend, + blocking_permits, + cached_blocks: BTreeMap::new(), + batches: BTreeMap::new(), + next_batch_id: 0, + metrics, + _phantom: Default::default(), + }; + + // Main event loop. This loop must not contain any direct .await, as we want to + // react to events as fast as possible. + loop { + select! { + request = requester_rx.next() => { + match request { + None => break, + Some(CacheRequest::StartBatch {sender, blocks}) + => inner.request_start_batch(&blocking_tx, sender, blocks, Arc::clone(&overrides)), + Some(CacheRequest::GetTraces {sender, block}) + => inner.request_get_traces(sender, block), + Some(CacheRequest::StopBatch {batch_id}) => { + batch_expirations.push(async move { + sleep(cache_duration).await; + batch_id + }); + + inner.request_stop_batch(batch_id); + }, + } + }, + message = blocking_rx.recv().fuse() => { + match message { + None => (), + Some(BlockingTaskMessage::Started { block_hash }) + => inner.blocking_started(block_hash), + Some(BlockingTaskMessage::Finished { block_hash, result }) + => inner.blocking_finished(block_hash, result), + } + }, + batch_id = batch_expirations.next() => { + match batch_id { + None => (), + Some(batch_id) => inner.expired_batch(batch_id), + } + } + } + } + } + .instrument(tracing::debug_span!("trace_filter_cache")); + + (task, CacheRequester(requester_tx)) + } + + /// Handle the creation of a batch. + /// Will start the tracing process for blocks that are not already in the cache. + #[instrument(skip(self, blocking_tx, sender, blocks, overrides))] + fn request_start_batch( + &mut self, + blocking_tx: &mpsc::Sender, + sender: oneshot::Sender, + blocks: Vec, + overrides: Arc>, + ) { + tracing::trace!("Starting batch {}", self.next_batch_id); + self.batches.insert(self.next_batch_id, blocks.clone()); + + for block in blocks { + // The block is already in the cache, awesome! + if let Some(block_cache) = self.cached_blocks.get_mut(&block) { + block_cache.active_batch_count = block_cache + .active_batch_count + .checked_add(1) + .expect("valid operation; qed"); + tracing::trace!( + "Cache hit for block {}, now used by {} batches.", + block, + block_cache.active_batch_count + ); + } + // Otherwise we need to queue this block for tracing. + else { + tracing::trace!("Cache miss for block {}, pooling it for tracing.", block); + + let blocking_permits = Arc::clone(&self.blocking_permits); + let (unqueue_sender, unqueue_receiver) = oneshot::channel(); + let client = Arc::clone(&self.client); + let backend = Arc::clone(&self.backend); + let blocking_tx = blocking_tx.clone(); + let overrides = Arc::clone(&overrides); + + // Spawn all block caching asynchronously. + // It will wait to obtain a permit, then spawn a blocking task. + // When the blocking task returns its result, it is sent + // thought a channel to the main task loop. + tokio::spawn( + async move { + tracing::trace!("Waiting for blocking permit or task cancellation"); + let _permit = select!( + _ = unqueue_receiver.fuse() => { + tracing::trace!("Tracing of the block has been cancelled."); + return; + }, + permit = blocking_permits.acquire().fuse() => permit, + ); + + // Warn the main task that block tracing as started, and + // this block cache entry should not be removed. + let _ = blocking_tx + .send(BlockingTaskMessage::Started { block_hash: block }) + .await; + + tracing::trace!("Start block tracing in a blocking task."); + + // Perform block tracing in a tokio blocking task. + let result = async { + tokio::task::spawn_blocking(move || { + Self::cache_block(client, backend, block, overrides) + }) + .await + .map_err(|e| { + format!("Tracing Substrate block {} panicked : {:?}", block, e) + })? + } + .await + .map_err(|e| e.to_string()); + + tracing::trace!("Block tracing finished, sending result to main task."); + + // Send a response to the main task. + let _ = blocking_tx + .send(BlockingTaskMessage::Finished { + block_hash: block, + result, + }) + .await; + } + .instrument(tracing::trace_span!("Block tracing", block = %block)), + ); + + // Insert the block in the cache. + self.cached_blocks.insert( + block, + CacheBlock { + active_batch_count: 1, + state: CacheBlockState::Pooled { + started: false, + waiting_requests: vec![], + unqueue_sender, + }, + }, + ); + } + } + + // Respond with the batch ID. + let _ = sender.send(CacheBatchId(self.next_batch_id)); + + // Increase batch ID for the next request. + self.next_batch_id = self.next_batch_id.overflowing_add(1).0; + } + + /// Handle a request to get the traces of the provided block. + /// - If the result is stored in the cache, it sends it immediately. + /// - If the block is currently being pooled, it is added to this block cache waiting list, + /// and all requests concerning this block will be satisfied when the tracing for this block + /// is finished. + /// - If this block is missing from the cache, it means no batch asked for it. All requested + /// blocks should be contained in a batch beforehand, and thus an error is returned. + #[instrument(skip(self))] + fn request_get_traces(&mut self, sender: oneshot::Sender, block: H256) { + if let Some(block_cache) = self.cached_blocks.get_mut(&block) { + match &mut block_cache.state { + CacheBlockState::Pooled { + ref mut waiting_requests, + .. + } => { + tracing::warn!( + "A request asked a pooled block ({}), adding it to the list of waiting requests.", + block + ); + waiting_requests.push(sender); + if let Some(metrics) = &self.metrics { + metrics.tracing_cache_misses.inc(); + } + } + CacheBlockState::Cached { traces, .. } => { + tracing::warn!( + "A request asked a cached block ({}), sending the traces directly.", + block + ); + let _ = sender.send(traces.clone()); + if let Some(metrics) = &self.metrics { + metrics.tracing_cache_hits.inc(); + } + } + } + } else { + tracing::warn!( + "An RPC request asked to get a block ({}) which was not batched.", + block + ); + let _ = sender.send(Err(format!( + "RPC request asked a block ({}) that was not batched", + block + ))); + } + } + + /// Handle a request to stop a batch. + /// For all blocks that needed to be traced, are only in this batch and not yet started, their + /// tracing is cancelled to save CPU-time and avoid attacks requesting large amount of blocks. + /// This batch data is not yet removed however. Instead a expiration delay timer is started + /// after which the data will indeed be cleared. (the code for that is in the main loop code + /// as it involved an unnamable type :C) + #[instrument(skip(self))] + fn request_stop_batch(&mut self, batch_id: CacheBatchId) { + tracing::trace!("Stopping batch {}", batch_id.0); + if let Some(blocks) = self.batches.get(&batch_id.0) { + for block in blocks { + let mut remove = false; + + // We remove early the block cache if this batch is the last + // pooling this block. + if let Some(block_cache) = self.cached_blocks.get_mut(block) { + if block_cache.active_batch_count == 1 + && matches!( + block_cache.state, + CacheBlockState::Pooled { started: false, .. } + ) + { + remove = true; + } + } + + if remove { + tracing::trace!("Pooled block {} is no longer requested.", block); + // Remove block from the cache. Drops the value, + // closing all the channels contained in it. + let _ = self.cached_blocks.remove(block); + } + } + } + } + + /// A tracing blocking task notifies it got a permit and is starting the tracing. + /// This started status is stored to avoid removing this block entry. + #[instrument(skip(self))] + fn blocking_started(&mut self, block_hash: H256) { + if let Some(block_cache) = self.cached_blocks.get_mut(&block_hash) { + if let CacheBlockState::Pooled { + ref mut started, .. + } = block_cache.state + { + *started = true; + } + } + } + + /// A tracing blocking task notifies it has finished the tracing and provide the result. + #[instrument(skip(self, result))] + fn blocking_finished(&mut self, block_hash: H256, result: TxsTraceRes) { + // In some cases it might be possible to receive traces of a block + // that has no entry in the cache because it was removed of the pool + // and received a permit concurrently. We just ignore it. + if let Some(block_cache) = self.cached_blocks.get_mut(&block_hash) { + if let CacheBlockState::Pooled { + ref mut waiting_requests, + .. + } = block_cache.state + { + tracing::trace!( + "A new block ({}) has been traced, adding it to the cache and responding to {} waiting requests.", + block_hash, + waiting_requests.len() + ); + // Send result in waiting channels. + while let Some(channel) = waiting_requests.pop() { + let _ = channel.send(result.clone()); + } + + // Update cache entry. + block_cache.state = CacheBlockState::Cached { traces: result }; + } + } + } + + /// A batch expiration delay timer has completed. It performs the cache cleaning for blocks + /// not longer used by other batches. + #[instrument(skip(self))] + fn expired_batch(&mut self, batch_id: CacheBatchId) { + if let Some(batch) = self.batches.remove(&batch_id.0) { + for block in batch { + // For each block of the batch, we remove it if it was the + // last batch containing it. + let mut remove = false; + if let Some(block_cache) = self.cached_blocks.get_mut(&block) { + block_cache.active_batch_count = block_cache + .active_batch_count + .checked_sub(1) + .expect("valid operation; qed"); + + if block_cache.active_batch_count == 0 { + remove = true; + } + } + + if remove { + let _ = self.cached_blocks.remove(&block); + } + } + } + } + + /// (In blocking task) Use the Runtime API to trace the block. + #[instrument(skip(client, backend, overrides))] + fn cache_block( + client: Arc, + backend: Arc, + substrate_hash: H256, + overrides: Arc>, + ) -> TxsTraceRes { + let api = client.runtime_api(); + let block_header = client + .header(substrate_hash) + .map_err(|e| { + format!( + "Error when fetching substrate block {} header : {:?}", + substrate_hash, e + ) + })? + .ok_or_else(|| format!("Substrate block {} don't exist", substrate_hash))?; + + let height = *block_header.number(); + let substrate_parent_hash = *block_header.parent_hash(); + + // Get Ethereum block data. + let (eth_block, eth_transactions) = match ( + overrides + .schemas + .get(&fc_storage::onchain_storage_schema( + client.as_ref(), + substrate_hash, + )) + .unwrap_or(&overrides.fallback) + .current_block(substrate_hash), + overrides + .fallback + .current_transaction_statuses(substrate_hash), + ) { + (Some(a), Some(b)) => (a, b), + _ => { + return Err(format!( + "Failed to get Ethereum block data for Substrate block {}", + substrate_hash + )) + } + }; + + let eth_block_hash = eth_block.header.hash(); + let eth_tx_hashes = eth_transactions + .iter() + .map(|t| t.transaction_hash) + .collect(); + + // Get extrinsics (containing Ethereum ones). + let extrinsics = backend + .blockchain() + .body(substrate_hash) + .map_err(|e| { + format!( + "Blockchain error when fetching extrinsics of block {} : {:?}", + height, e + ) + })? + .ok_or_else(|| format!("Could not find block {} when fetching extrinsics.", height))?; + + // Trace the block. + let f = || -> Result<_, String> { + let result = api.trace_block( + substrate_parent_hash, + extrinsics, + eth_tx_hashes, + &block_header, + ); + + result + .map_err(|e| format!("Blockchain error when replaying block {} : {:?}", height, e))? + .map_err(|e| { + tracing::warn!( + target: "tracing", + "Internal runtime error when replaying block {} : {:?}", + height, + e + ); + format!( + "Internal runtime error when replaying block {} : {:?}", + height, e + ) + })?; + + Ok(TracerResponse::Block) + }; + + let eth_transactions_by_index: BTreeMap = eth_transactions + .iter() + .map(|t| (t.transaction_index, t.transaction_hash)) + .collect(); + + let mut proxy = evm_tracing_client::listeners::call_list::Listener::default(); + proxy.using(f)?; + + let traces: Vec = + evm_tracing_client::formatters::trace_filter::Formatter::format(proxy) + .ok_or("Fail to format proxy")? + .into_iter() + .filter_map(|mut trace| { + match eth_transactions_by_index.get(&trace.transaction_position) { + Some(transaction_hash) => { + trace.block_hash = eth_block_hash; + trace.block_number = height; + trace.transaction_hash = *transaction_hash; + + // Reformat error messages. + if let block::TransactionTraceOutput::Error(ref mut error) = + trace.output + { + if error.as_slice() == b"execution reverted" { + *error = b"Reverted".to_vec(); + } + } + Some(trace) + } + None => { + frame_support::log::warn!( + target: "tracing", + "A trace in block {} does not map to any known ethereum transaction. Trace: {:?}", + height, + trace, + ); + + None + } + } + }) + .collect(); + + Ok(traces) + } +} + +/// Prometheus metrics for tracing. +#[derive(Clone)] +pub struct Metrics { + /// Tracing cache hits. + tracing_cache_hits: Counter, + /// Tracing cache misses. + tracing_cache_misses: Counter, +} + +impl Metrics { + /// Register. + pub fn register(registry: &PrometheusRegistry) -> Result { + Ok(Self { + tracing_cache_hits: register( + Counter::new("tracing_cache_hits", "Number of tracing cache hits.")?, + registry, + )?, + tracing_cache_misses: register( + Counter::new("tracing_cache_misses", "Number of tracing cache misses.")?, + registry, + )?, + }) + } +} diff --git a/crates/evm-tracing-rpc/src/trace/core.rs b/crates/evm-tracing-rpc/src/trace/core.rs new file mode 100644 index 000000000..9b5e834c9 --- /dev/null +++ b/crates/evm-tracing-rpc/src/trace/core.rs @@ -0,0 +1,33 @@ +//! Core. + +use evm_tracing_client::types::block::TransactionTrace; +use jsonrpsee::{core::RpcResult, proc_macros::rpc}; +use serde::Deserialize; +use sp_core::H160; + +use crate::types::RequestBlockId; + +/// Filter request. +#[derive(Clone, Eq, PartialEq, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FilterRequest { + /// From this block. + pub from_block: Option, + /// To this block. + pub to_block: Option, + /// Sent from these addresses. + pub from_address: Option>, + /// Sent to these addresses. + pub to_address: Option>, + /// The offset trace number. + pub after: Option, + /// Integer number of traces to display in a batch. + pub count: Option, +} + +#[rpc(server)] +pub trait Trace { + /// Filter. + #[method(name = "trace_filter")] + async fn filter(&self, filter: FilterRequest) -> RpcResult>; +} diff --git a/crates/evm-tracing-rpc/src/trace/mod.rs b/crates/evm-tracing-rpc/src/trace/mod.rs new file mode 100644 index 000000000..45a196530 --- /dev/null +++ b/crates/evm-tracing-rpc/src/trace/mod.rs @@ -0,0 +1,210 @@ +//! Trace related implementation. + +use core::{FilterRequest, TraceServer}; +use std::{marker::PhantomData, sync::Arc}; + +use cache_requester::CacheRequester; +use evm_tracing_client::types::block::{self, TransactionTrace}; +use jsonrpsee::core::{async_trait, RpcResult}; +use sp_blockchain::{Error as BlockChainError, HeaderBackend, HeaderMetadata}; +use sp_core::H256; +use sp_runtime::traits::{Block as BlockT, Header as HeaderT}; + +use crate::types::{RequestBlockId, RequestBlockTag}; + +pub mod cache_requester; +pub mod cache_task; +pub mod core; + +/// Transaction trace result alias. +type TxsTraceRes = Result, String>; + +/// RPC handler. Will communicate with a `CacheTask` through a `CacheRequester`. +pub struct Trace { + /// Inner client. + client: Arc, + /// Cache requester. + requester: CacheRequester, + /// Max count. + max_count: u32, + /// Phantom data. + _phantom: PhantomData, +} + +impl Clone for Trace { + fn clone(&self) -> Self { + Self { + client: Arc::clone(&self.client), + requester: self.requester.clone(), + max_count: self.max_count, + _phantom: PhantomData, + } + } +} + +impl Trace +where + B: BlockT + Send + Sync + 'static, + B::Header: HeaderT, + C: HeaderMetadata + HeaderBackend, + C: Send + Sync + 'static, +{ + /// Create a new RPC handler. + pub fn new(client: Arc, requester: CacheRequester, max_count: u32) -> Self { + Self { + client, + requester, + max_count, + _phantom: PhantomData, + } + } + + /// Convert an optional block ID (number or tag) to a block height. + fn block_id(&self, id: Option) -> Result { + match id { + Some(RequestBlockId::Number(n)) => Ok(n), + None | Some(RequestBlockId::Tag(RequestBlockTag::Latest)) => { + Ok(self.client.info().best_number) + } + Some(RequestBlockId::Tag(RequestBlockTag::Earliest)) => Ok(0), + Some(RequestBlockId::Tag(RequestBlockTag::Pending)) => { + Err("'pending' is not supported") + } + Some(RequestBlockId::Hash(_)) => Err("Block hash not supported"), + } + } + + /// `trace_filter` endpoint (wrapped in the trait implementation with futures compatibility). + async fn filter(self, req: FilterRequest) -> TxsTraceRes { + let from_block = self.block_id(req.from_block)?; + let to_block = self.block_id(req.to_block)?; + let block_heights = from_block..=to_block; + + let count = req.count.unwrap_or(self.max_count); + if count > self.max_count { + return Err(format!( + "count ({}) can't be greater than maximum ({})", + count, self.max_count + )); + } + + // Build a list of all the Substrate block hashes that need to be traced. + let mut block_hashes = vec![]; + for block_height in block_heights { + if block_height == 0 { + continue; // no traces for genesis block. + } + + let block_hash = self + .client + .hash(block_height) + .map_err(|e| { + format!( + "Error when fetching block {} header : {:?}", + block_height, e + ) + })? + .ok_or_else(|| format!("Block with height {} don't exist", block_height))?; + + block_hashes.push(block_hash); + } + + // Start a batch with these blocks. + let batch_id = self.requester.start_batch(block_hashes.clone()).await?; + // Fetch all the traces. It is done in another function to simplify error handling and allow + // to call the following `stop_batch` regardless of the result. This is important for the + // cache cleanup to work properly. + let res = self.fetch_traces(req, &block_hashes, count as usize).await; + // Stop the batch, allowing the cache task to remove useless non-started block traces and + // start the expiration delay. + self.requester.stop_batch(batch_id).await; + + res + } + + /// Fetch traces. + async fn fetch_traces( + &self, + req: FilterRequest, + block_hashes: &[H256], + count: usize, + ) -> TxsTraceRes { + let from_address = req.from_address.unwrap_or_default(); + let to_address = req.to_address.unwrap_or_default(); + + let mut traces = vec![]; + + for &block_hash in block_hashes { + // Request the traces of this block to the cache service. + // This will resolve quickly if the block is already cached, or wait until the block + // has finished tracing. + let block_traces = self.requester.get_traces(block_hash).await?; + + // Filter addresses. + let mut block_traces: Vec<_> = block_traces + .iter() + .filter(|trace| match trace.action { + block::TransactionTraceAction::Call { from, to, .. } => { + (from_address.is_empty() || from_address.contains(&from)) + && (to_address.is_empty() || to_address.contains(&to)) + } + block::TransactionTraceAction::Create { from, .. } => { + (from_address.is_empty() || from_address.contains(&from)) + && to_address.is_empty() + } + block::TransactionTraceAction::Suicide { address, .. } => { + (from_address.is_empty() || from_address.contains(&address)) + && to_address.is_empty() + } + }) + .cloned() + .collect(); + + // Don't insert anything if we're still before "after". + if let Some(traces_amount) = block_traces.len().checked_sub( + // usize is big enough for this overflow to be practically impossible. + req.after.unwrap_or(0).try_into().unwrap(), + ) { + // If the current Vec of traces is across the "after" marker, + // we skip some elements of it. + if let Some(skip) = block_traces.len().checked_sub(traces_amount) { + block_traces = block_traces.into_iter().skip(skip).collect(); + } + + traces.append(&mut block_traces); + + // If we go over "count" (the limit), we trim and exit the loop, + // unless we used the default maximum, in which case we return an error. + if traces_amount >= count { + if req.count.is_none() { + return Err(format!( + "the amount of traces goes over the maximum ({}), please use 'after' and 'count' in your request", + self.max_count + )); + } + + traces = traces.into_iter().take(count).collect(); + break; + } + } + } + + Ok(traces) + } +} + +#[async_trait] +impl TraceServer for Trace +where + B: BlockT + Send + Sync + 'static, + B::Header: HeaderT, + C: HeaderMetadata + HeaderBackend, + C: Send + Sync + 'static, +{ + async fn filter(&self, filter: FilterRequest) -> RpcResult> { + self.clone() + .filter(filter) + .await + .map_err(fc_rpc::internal_err) + } +} diff --git a/crates/evm-tracing-rpc/src/types.rs b/crates/evm-tracing-rpc/src/types.rs new file mode 100644 index 000000000..30583845c --- /dev/null +++ b/crates/evm-tracing-rpc/src/types.rs @@ -0,0 +1,64 @@ +//! Types definitions. + +use codec::{Decode, Encode}; +use serde::{de::Error, Deserialize, Deserializer}; +use sp_core::H256; + +/// Supported tracer input types. +#[derive(Clone, Copy, Eq, PartialEq, Debug, Encode, Decode)] +pub enum TracerInput { + /// None represents unsupported tracer type. + None, + /// Blockscout tracer type. + Blockscout, + /// Call tracer type. + CallTracer, +} + +/// Tracer response. +#[derive(Debug)] +pub enum TracerResponse { + /// Single. + Single, + /// Block. + Block, +} + +/// Request block by identifier. +#[derive(Copy, Clone, Eq, PartialEq, Debug, Deserialize)] +#[serde(rename_all = "camelCase", untagged)] +pub enum RequestBlockId { + /// By number. + Number(#[serde(deserialize_with = "deserialize_u32_0x")] u32), + /// By hash. + Hash(H256), + /// By tag. + Tag(RequestBlockTag), +} + +/// Request block by tag. +#[derive(Copy, Clone, Eq, PartialEq, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum RequestBlockTag { + /// The earliest. + Earliest, + /// The latest. + Latest, + /// The pending. + Pending, +} + +/// Deserializer used for `RequestBlockId`. +fn deserialize_u32_0x<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let buf = String::deserialize(deserializer)?; + + let parsed = match buf.strip_prefix("0x") { + Some(buf) => u32::from_str_radix(buf, 16), + None => buf.parse::(), + }; + + parsed.map_err(|e| Error::custom(format!("parsing error: {:?} from '{}'", e, buf))) +} diff --git a/crates/humanode-peer/Cargo.toml b/crates/humanode-peer/Cargo.toml index 1538df368..9daa75e65 100644 --- a/crates/humanode-peer/Cargo.toml +++ b/crates/humanode-peer/Cargo.toml @@ -16,6 +16,8 @@ bioauth-flow-rpc = { path = "../bioauth-flow-rpc" } bioauth-keys = { path = "../bioauth-keys" } crypto-utils = { path = "../crypto-utils" } crypto-utils-evm = { path = "../crypto-utils-evm" } +evm-tracing-host-api = { path = "../evm-tracing-host-api" } +evm-tracing-rpc = { path = "../evm-tracing-rpc" } humanode-rpc = { path = "../humanode-rpc" } humanode-runtime = { path = "../humanode-runtime" } keystore-bioauth-account-id = { path = "../keystore-bioauth-account-id" } diff --git a/crates/humanode-peer/src/cli/config.rs b/crates/humanode-peer/src/cli/config.rs index fcd003bda..33b6d3005 100644 --- a/crates/humanode-peer/src/cli/config.rs +++ b/crates/humanode-peer/src/cli/config.rs @@ -51,6 +51,11 @@ pub trait CliConfigurationExt: SubstrateCliConfigurationProvider { max_stored_filters: params.max_stored_filters, fee_history_limit: params.fee_history_limit, execute_gas_limit_multiplier: params.execute_gas_limit_multiplier, + tracing_mode: params.tracing_mode.clone(), + tracing_max_permits: params.tracing_max_permits, + tracing_debug_raw_max_memory_usage: params.tracing_debug_raw_max_memory_usage, + tracing_trace_max_count: params.tracing_trace_max_count, + tracing_trace_cache_duration: params.tracing_trace_cache_duration, }); let fb_params = self.frontier_backend(); diff --git a/crates/humanode-peer/src/cli/params.rs b/crates/humanode-peer/src/cli/params.rs index 211db604a..ad6cf0317 100644 --- a/crates/humanode-peer/src/cli/params.rs +++ b/crates/humanode-peer/src/cli/params.rs @@ -1,6 +1,6 @@ //! Shared CLI parameters. -use crate::configuration::FrontierBackendType; +use crate::configuration::{EthTracingMode, FrontierBackendType}; /// Possible RPC URL scheme preference options. #[derive(Debug, clap::ValueEnum, Clone)] @@ -75,6 +75,30 @@ pub struct EthereumRpcParams { /// `block.gas_limit` * `execute_gas_limit_multiplier`. #[arg(long, default_value = "10")] pub execute_gas_limit_multiplier: u64, + + /// Enable EVM tracing mode on a non-authority node. + #[arg(long, value_delimiter = ',')] + pub tracing_mode: Vec, + + /// Number of concurrent tracing tasks. Meant to be shared by both "debug" and "trace" modules. + #[arg(long, default_value = "10")] + pub tracing_max_permits: u32, + + /// Size in bytes of data a raw tracing request is allowed to use. + /// Bound the size of memory, stack and storage data. + #[arg(long, default_value = "20000000")] + pub tracing_debug_raw_max_memory_usage: usize, + + /// Maximum number of trace entries a single request of `trace_filter` is allowed to return. + /// A request asking for more or an unbounded one going over this limit will both return an + /// error. + #[arg(long, default_value = "500")] + pub tracing_trace_max_count: u32, + + /// Duration (in seconds) after which the cache of `trace_filter` for a given block will be + /// discarded. + #[arg(long, default_value = "300")] + pub tracing_trace_cache_duration: u64, } /// Shared CLI parameters used to configure Frontier backend. diff --git a/crates/humanode-peer/src/configuration.rs b/crates/humanode-peer/src/configuration.rs index cf711bed7..f6bf449a1 100644 --- a/crates/humanode-peer/src/configuration.rs +++ b/crates/humanode-peer/src/configuration.rs @@ -75,6 +75,25 @@ pub struct EthereumRpc { /// When using `eth_call/eth_estimateGas`, the maximum allowed gas limit will be /// `block.gas_limit` * `execute_gas_limit_multiplier`. pub execute_gas_limit_multiplier: u64, + + /// Enable EVM tracing mode on a non-authority node. + pub tracing_mode: Vec, + + /// Number of concurrent tracing tasks. Meant to be shared by both "debug" and "trace" modules. + pub tracing_max_permits: u32, + + /// Size in bytes of data a raw tracing request is allowed to use. + /// Bound the size of memory, stack and storage data. + pub tracing_debug_raw_max_memory_usage: usize, + + /// Maximum number of trace entries a single request of `trace_filter` is allowed to return. + /// A request asking for more or an unbounded one going over this limit will both return an + /// error. + pub tracing_trace_max_count: u32, + + /// Duration (in seconds) after which the cache of `trace_filter` for a given block will be + /// discarded. + pub tracing_trace_cache_duration: u64, } /// Frontier backend configuration parameters. @@ -105,3 +124,12 @@ pub enum FrontierBackendType { /// Sql database with custom log indexing. Sql, } + +/// Possible EVM tracing modes. +#[derive(Debug, clap::ValueEnum, Clone, PartialEq)] +pub enum EthTracingMode { + /// Debug mode. + Debug, + /// Trace mode. + Trace, +} diff --git a/crates/humanode-peer/src/service/mod.rs b/crates/humanode-peer/src/service/mod.rs index b9644c5e7..df3d4662e 100644 --- a/crates/humanode-peer/src/service/mod.rs +++ b/crates/humanode-peer/src/service/mod.rs @@ -21,7 +21,7 @@ use sc_service::{ use sc_telemetry::{Telemetry, TelemetryWorker}; use tracing::*; -use crate::configuration::Configuration; +use crate::configuration::{Configuration, EthTracingMode}; pub mod frontier; pub mod inherents; @@ -33,10 +33,13 @@ pub struct ExecutorDispatch; impl sc_executor::NativeExecutionDispatch for ExecutorDispatch { /// Only enable the benchmarking host functions when we actually want to benchmark. #[cfg(feature = "runtime-benchmarks")] - type ExtendHostFunctions = frame_benchmarking::benchmarking::HostFunctions; + type ExtendHostFunctions = ( + frame_benchmarking::benchmarking::HostFunctions, + evm_tracing_host_api::externalities::HostFunctions, + ); /// Otherwise we only use the default Substrate host functions. #[cfg(not(feature = "runtime-benchmarks"))] - type ExtendHostFunctions = (); + type ExtendHostFunctions = (evm_tracing_host_api::externalities::HostFunctions,); fn dispatch(method: &str, data: &[u8]) -> Option> { humanode_runtime::api::dispatch(method, data) @@ -277,6 +280,56 @@ pub async fn new_full(config: Configuration) -> Result, >::default()); + let eth_permit_pool = Arc::new(tokio::sync::Semaphore::new( + ethereum_rpc_config.tracing_max_permits as usize, + )); + + let eth_debug_requester = if ethereum_rpc_config + .tracing_mode + .contains(&EthTracingMode::Debug) + { + let (debug_task, debug_requester) = evm_tracing_rpc::debug::DebugHandler::task( + Arc::clone(&client), + Arc::clone(&backend), + match frontier_backend.clone() { + fc_db::Backend::KeyValue(b) => Arc::new(b), + fc_db::Backend::Sql(b) => Arc::new(b), + }, + Arc::clone(ð_permit_pool), + Arc::clone(ð_overrides), + ethereum_rpc_config.tracing_debug_raw_max_memory_usage, + ); + + task_manager + .spawn_essential_handle() + .spawn("debug", Some("eth-tracing"), debug_task); + + Some(debug_requester) + } else { + None + }; + + let eth_trace_requester = if ethereum_rpc_config + .tracing_mode + .contains(&EthTracingMode::Trace) + { + let (trace_task, trace_requester) = evm_tracing_rpc::trace::cache_task::CacheTask::create( + Arc::clone(&client), + Arc::clone(&backend), + Duration::from_secs(ethereum_rpc_config.tracing_trace_cache_duration), + Arc::clone(ð_permit_pool), + Arc::clone(ð_overrides), + prometheus_registry.clone(), + ); + + task_manager + .spawn_essential_handle() + .spawn("trace", Some("eth-tracing"), trace_task); + + Some(trace_requester) + } else { + None + }; let proposer_factory = sc_basic_authorship::ProposerFactory::new( task_manager.spawn_handle(), @@ -363,6 +416,8 @@ pub async fn new_full(config: Configuration) -> Result Result, >, >, + + /// Ethereum debug requester. + pub eth_debuq_requester: Option, + + /// Ethereum trace requester. + pub eth_trace_requester: Option<(evm_tracing_rpc::trace::cache_requester::CacheRequester, u32)>, } /// RPC subsystem dependencies. @@ -237,6 +244,8 @@ where eth_execute_gas_limit_multiplier, eth_forced_parent_hashes, eth_pubsub_notification_sinks, + eth_debuq_requester, + eth_trace_requester, } = evm; let chain_name = chain_spec.name().to_string(); @@ -346,7 +355,7 @@ where if let Some(eth_filter_pool) = eth_filter_pool { io.merge( EthFilter::new( - client, + Arc::clone(&client), eth_backend, eth_tx_pool.clone(), eth_filter_pool, @@ -360,5 +369,25 @@ where io.merge(eth_tx_pool.into_rpc())?; + if let Some(eth_debug_requester) = eth_debuq_requester { + io.merge( + evm_tracing_rpc::debug::Debug { + requester: eth_debug_requester, + } + .into_rpc(), + )?; + } + + if let Some((eth_trace_requester, eth_trace_filter_max_count)) = eth_trace_requester { + io.merge( + evm_tracing_rpc::trace::Trace::new( + client, + eth_trace_requester, + eth_trace_filter_max_count, + ) + .into_rpc(), + )?; + } + Ok(io) } diff --git a/crates/humanode-runtime/Cargo.toml b/crates/humanode-runtime/Cargo.toml index 528727410..f239afaac 100644 --- a/crates/humanode-runtime/Cargo.toml +++ b/crates/humanode-runtime/Cargo.toml @@ -17,6 +17,8 @@ eip712-account-claim = { path = "../eip712-account-claim", default-features = fa eip712-common = { path = "../eip712-common", default-features = false } eip712-token-claim = { path = "../eip712-token-claim", default-features = false } evm-nonces-recovery = { path = "../evm-nonces-recovery", default-features = false } +evm-tracer = { path = "../evm-tracer", default-features = false, optional = true } +evm-tracing-api = { path = "../evm-tracing-api", default-features = false } keystore-bioauth-account-id = { path = "../keystore-bioauth-account-id", default-features = false } pallet-balanced-currency-swap-bridges-initializer = { path = "../pallet-balanced-currency-swap-bridges-initializer", default-features = false } pallet-bioauth = { path = "../pallet-bioauth", default-features = false } @@ -112,6 +114,7 @@ sp-keystore = { workspace = true } [features] default = ["std"] +evm-tracing = ["evm-tracer"] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", @@ -154,6 +157,8 @@ std = [ "eip712-token-claim/std", "ethereum/std", "evm-nonces-recovery/std", + "evm-tracer/std", + "evm-tracing-api/std", "fp-evm/std", "fp-rpc/std", "fp-self-contained/std", diff --git a/crates/humanode-runtime/src/lib.rs b/crates/humanode-runtime/src/lib.rs index cac4a3c83..c2d5199ba 100644 --- a/crates/humanode-runtime/src/lib.rs +++ b/crates/humanode-runtime/src/lib.rs @@ -1502,6 +1502,194 @@ impl_runtime_apis! { } } + impl evm_tracing_api::EvmTracingApi for Runtime { + fn trace_transaction( + extrinsics: Vec<::Extrinsic>, + traced_transaction: &EthereumTransaction, + header: &::Header, + ) -> Result<(), sp_runtime::DispatchError> { + #[cfg(feature = "evm-tracing")] + { + Executive::initialize_block(header); + + for ext in extrinsics { + match &ext.0.function { + RuntimeCall::Ethereum(transact { transaction }) => { + let tx_hash = &transaction.hash(); + if transaction == traced_transaction { + evm_tracer::EvmTracer::default().trace(|| { + if let Err(err) = Executive::apply_extrinsic(ext) { + frame_support::log::debug!( + target: "tracing", + "Could not trace eth transaction (hash: {}): {:?}", + &tx_hash, + err + ); + } + }); + } else if let Err(err) = Executive::apply_extrinsic(ext) { + frame_support::log::debug!( + target: "tracing", + "Failed to apply eth extrinsic (hash: {}): {:?}", + &tx_hash, + err + ); + } + } + _ => { + if let Err(err) = Executive::apply_extrinsic(ext) { + frame_support::log::debug!( + target: "tracing", + "Failed to apply non-eth extrinsic: {:?}", + err + ); + } + } + }; + } + + Ok(()) + } + + #[cfg(not(feature = "evm-tracing"))] + { + let _ = extrinsics; + let _ = traced_transaction; + let _ = header; + + Err(sp_runtime::DispatchError::Other( + "Missing `evm-tracing` compile time feature flag.", + )) + } + } + + fn trace_block( + extrinsics: Vec<::Extrinsic>, + known_transactions: Vec, + header: &::Header, + ) -> Result<(), sp_runtime::DispatchError> { + #[cfg(feature = "evm-tracing")] + { + let mut config = ::config().clone(); + config.estimate = true; + + Executive::initialize_block(header); + + // Apply all extrinsics. Ethereum extrinsics are traced. + for ext in extrinsics { + match &ext.0.function { + RuntimeCall::Ethereum(transact { transaction }) => { + let tx_hash = &transaction.hash(); + if known_transactions.contains(tx_hash) { + // Each known extrinsic is a new call stack. + evm_tracer::EvmTracer::emit_new(); + evm_tracer::EvmTracer::default().trace(|| { + if let Err(err) = Executive::apply_extrinsic(ext) { + frame_support::log::debug!( + target: "tracing", + "Could not trace eth transaction (hash: {}): {:?}", + &tx_hash, + err + ); + } + }); + } else if let Err(err) = Executive::apply_extrinsic(ext) { + frame_support::log::debug!( + target: "tracing", + "Failed to apply eth extrinsic (hash: {}): {:?}", + &tx_hash, + err + ); + } + }, + _ => { + if let Err(err) = Executive::apply_extrinsic(ext) { + frame_support::log::debug!( + target: "tracing", + "Failed to apply non-eth extrinsic: {:?}", + err + ); + } + } + } + } + + Ok(()) + } + + #[cfg(not(feature = "evm-tracing"))] + { + let _ = extrinsics; + let _ = known_transactions; + let _ = header; + + Err(sp_runtime::DispatchError::Other( + "Missing `evm-tracing` compile time feature flag.", + )) + } + } + + fn trace_call( + header: &::Header, + from: H160, + to: H160, + data: Vec, + value: U256, + gas_limit: U256, + max_fee_per_gas: Option, + max_priority_fee_per_gas: Option, + nonce: Option, + access_list: Option)>>, + ) -> Result<(), sp_runtime::DispatchError> { + #[cfg(feature = "evm-tracing")] + { + Executive::initialize_block(header); + + evm_tracer::EvmTracer::default().trace(|| { + let is_transactional = false; + let validate = true; + + let _ = ::Runner::call( + from, + to, + data, + value, + gas_limit.low_u64(), + max_fee_per_gas, + max_priority_fee_per_gas, + nonce, + access_list.unwrap_or_default(), + is_transactional, + validate, + None, + None, + ::config(), + ); + }); + + Ok(()) + } + + #[cfg(not(feature = "evm-tracing"))] + { + let _ = header; + let _ = from; + let _ = to; + let _ = data; + let _ = value; + let _ = gas_limit; + let _ = max_fee_per_gas; + let _ = max_priority_fee_per_gas; + let _ = nonce; + let _ = access_list; + + Err(sp_runtime::DispatchError::Other( + "Missing `evm-tracing` compile time feature flag.", + )) + } + } + } + #[cfg(feature = "runtime-benchmarks")] impl frame_benchmarking::Benchmark for Runtime { fn benchmark_metadata(extra: bool) -> ( diff --git a/utils/checks/snapshots/features.yaml b/utils/checks/snapshots/features.yaml index b09caea38..6ab371618 100644 --- a/utils/checks/snapshots/features.yaml +++ b/utils/checks/snapshots/features.yaml @@ -802,6 +802,7 @@ - scale-info - serde - std + - tracing - with-codec - with-serde - name: evm-core 0.39.0 @@ -816,6 +817,7 @@ features: - environmental - std + - tracing - name: evm-nonces-recovery 0.1.0 features: - default @@ -824,6 +826,28 @@ features: - environmental - std + - tracing +- name: evm-tracer 0.1.0 + features: + - default + - std +- name: evm-tracing-api 0.1.0 + features: + - default + - std +- name: evm-tracing-client 0.1.0 + features: [] +- name: evm-tracing-events 0.1.0 + features: + - default + - evm-tracing + - std +- name: evm-tracing-host-api 0.1.0 + features: + - default + - std +- name: evm-tracing-rpc 0.1.0 + features: [] - name: exit-future 0.2.0 features: [] - name: expander 1.0.0 @@ -1215,6 +1239,7 @@ features: - alloc - default + - serde - std - name: hex-literal 0.4.1 features: [] @@ -1261,6 +1286,7 @@ - name: humanode-runtime 0.1.0 features: - default + - evm-tracer - frame-try-runtime - serde - std @@ -2857,7 +2883,7 @@ features: - default - std -- name: smallvec 1.13.1 +- name: smallvec 1.15.1 features: - const_generics - const_new diff --git a/utils/e2e-tests/bash/fixtures/help-output/help.stdout.txt b/utils/e2e-tests/bash/fixtures/help-output/help.stdout.txt index 8050e16c3..6dabbb0ef 100644 --- a/utils/e2e-tests/bash/fixtures/help-output/help.stdout.txt +++ b/utils/e2e-tests/bash/fixtures/help-output/help.stdout.txt @@ -585,6 +585,33 @@ Options: [default: 10] + --tracing-mode + Enable EVM tracing mode on a non-authority node + + Possible values: + - debug: Debug mode + - trace: Trace mode + + --tracing-max-permits + Number of concurrent tracing tasks. Meant to be shared by both "debug" and "trace" modules + + [default: 10] + + --tracing-debug-raw-max-memory-usage + Size in bytes of data a raw tracing request is allowed to use. Bound the size of memory, stack and storage data + + [default: 20000000] + + --tracing-trace-max-count + Maximum number of trace entries a single request of `trace_filter` is allowed to return. A request asking for more or an unbounded one going over this limit will both return an error + + [default: 500] + + --tracing-trace-cache-duration + Duration (in seconds) after which the cache of `trace_filter` for a given block will be discarded + + [default: 300] + --time-warp-revive-timestamp The time in the future when the warp is going to be started diff --git a/utils/e2e-tests/ts/lib/abis/evmTracing/callForwarder.ts b/utils/e2e-tests/ts/lib/abis/evmTracing/callForwarder.ts new file mode 100644 index 000000000..730702bb9 --- /dev/null +++ b/utils/e2e-tests/ts/lib/abis/evmTracing/callForwarder.ts @@ -0,0 +1,108 @@ +// pragma solidity >=0.8.3; +// +// contract CallForwarder { +// function call( +// address target, +// bytes memory data +// ) public returns (bool, bytes memory) { +// return target.call(data); +// } +// +// function callRange(address first, address last) public { +// require(first < last, "invalid range"); +// while (first < last) { +// first.call(""); +// first = address(uint160(first) + 1); +// } +// } +// +// function delegateCall( +// address target, +// bytes memory data +// ) public returns (bool, bytes memory) { +// return target.delegatecall(data); +// } +// } + +export default { + abi: [ + { + inputs: [ + { + internalType: "address", + name: "target", + type: "address", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + ], + name: "call", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + { + internalType: "bytes", + name: "", + type: "bytes", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "first", + type: "address", + }, + { + internalType: "address", + name: "last", + type: "address", + }, + ], + name: "callRange", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "target", + type: "address", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + ], + name: "delegateCall", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + { + internalType: "bytes", + name: "", + type: "bytes", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + ], + bytecode: + "0x6080604052348015600e575f5ffd5b5061076d8061001c5f395ff3fe608060405234801561000f575f5ffd5b506004361061003f575f3560e01c80630382943f146100435780631b8b921d1461005f57806356e7b7aa14610090575b5f5ffd5b61005d6004803603810190610058919061032e565b6100c1565b005b610079600480360381019061007491906104a8565b6101e0565b604051610087929190610596565b60405180910390f35b6100aa60048036038101906100a591906104a8565b610252565b6040516100b8929190610596565b60405180910390f35b8073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff161061012f576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101269061061e565b60405180910390fd5b5b8073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1610156101dc578173ffffffffffffffffffffffffffffffffffffffff1660405161018790610669565b5f604051808303815f865af19150503d805f81146101c0576040519150601f19603f3d011682016040523d82523d5f602084013e6101c5565b606091505b5050506001826101d591906106aa565b9150610130565b5050565b5f60608373ffffffffffffffffffffffffffffffffffffffff16836040516102089190610721565b5f604051808303815f865af19150503d805f8114610241576040519150601f19603f3d011682016040523d82523d5f602084013e610246565b606091505b50915091509250929050565b5f60608373ffffffffffffffffffffffffffffffffffffffff168360405161027a9190610721565b5f60405180830381855af49150503d805f81146102b2576040519150601f19603f3d011682016040523d82523d5f602084013e6102b7565b606091505b50915091509250929050565b5f604051905090565b5f5ffd5b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6102fd826102d4565b9050919050565b61030d816102f3565b8114610317575f5ffd5b50565b5f8135905061032881610304565b92915050565b5f5f60408385031215610344576103436102cc565b5b5f6103518582860161031a565b92505060206103628582860161031a565b9150509250929050565b5f5ffd5b5f5ffd5b5f601f19601f8301169050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b6103ba82610374565b810181811067ffffffffffffffff821117156103d9576103d8610384565b5b80604052505050565b5f6103eb6102c3565b90506103f782826103b1565b919050565b5f67ffffffffffffffff82111561041657610415610384565b5b61041f82610374565b9050602081019050919050565b828183375f83830152505050565b5f61044c610447846103fc565b6103e2565b90508281526020810184848401111561046857610467610370565b5b61047384828561042c565b509392505050565b5f82601f83011261048f5761048e61036c565b5b813561049f84826020860161043a565b91505092915050565b5f5f604083850312156104be576104bd6102cc565b5b5f6104cb8582860161031a565b925050602083013567ffffffffffffffff8111156104ec576104eb6102d0565b5b6104f88582860161047b565b9150509250929050565b5f8115159050919050565b61051681610502565b82525050565b5f81519050919050565b5f82825260208201905092915050565b5f5b83811015610553578082015181840152602081019050610538565b5f8484015250505050565b5f6105688261051c565b6105728185610526565b9350610582818560208601610536565b61058b81610374565b840191505092915050565b5f6040820190506105a95f83018561050d565b81810360208301526105bb818461055e565b90509392505050565b5f82825260208201905092915050565b7f696e76616c69642072616e6765000000000000000000000000000000000000005f82015250565b5f610608600d836105c4565b9150610613826105d4565b602082019050919050565b5f6020820190508181035f830152610635816105fc565b9050919050565b5f81905092915050565b50565b5f6106545f8361063c565b915061065f82610646565b5f82019050919050565b5f61067382610649565b9150819050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f6106b4826102d4565b91506106bf836102d4565b9250828201905073ffffffffffffffffffffffffffffffffffffffff8111156106eb576106ea61067d565b5b92915050565b5f6106fb8261051c565b610705818561063c565b9350610715818560208601610536565b80840191505092915050565b5f61072c82846106f1565b91508190509291505056fea264697066735822122061744ef3cd320f8504db2e240b6131dd71c7811dbb3c105ee2c3014e04b1635864736f6c634300081e0033", +} as const; diff --git a/utils/e2e-tests/ts/lib/abis/evmTracing/callee.ts b/utils/e2e-tests/ts/lib/abis/evmTracing/callee.ts new file mode 100644 index 000000000..3fa9b2040 --- /dev/null +++ b/utils/e2e-tests/ts/lib/abis/evmTracing/callee.ts @@ -0,0 +1,50 @@ +// pragma solidity >=0.8.3; +// +// contract TraceCallee { +// uint256 public store; +// +// function addSeven(uint256 value) external returns (uint256 result) { +// uint256 x = 7; +// store = value; +// return value + x; +// } +// } + +export default { + abi: [ + { + inputs: [ + { + internalType: "uint256", + name: "value", + type: "uint256", + }, + ], + name: "addSeven", + outputs: [ + { + internalType: "uint256", + name: "result", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "store", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + ], + bytecode: + "0x6080604052348015600e575f5ffd5b506101cb8061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610034575f3560e01c80631ba6484614610038578063975057e714610068575b5f5ffd5b610052600480360381019061004d91906100e2565b610086565b60405161005f919061011c565b60405180910390f35b6100706100a6565b60405161007d919061011c565b60405180910390f35b5f5f60079050825f81905550808361009e9190610162565b915050919050565b5f5481565b5f5ffd5b5f819050919050565b6100c1816100af565b81146100cb575f5ffd5b50565b5f813590506100dc816100b8565b92915050565b5f602082840312156100f7576100f66100ab565b5b5f610104848285016100ce565b91505092915050565b610116816100af565b82525050565b5f60208201905061012f5f83018461010d565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f61016c826100af565b9150610177836100af565b925082820190508082111561018f5761018e610135565b5b9291505056fea26469706673582212209aa1341b3eba47419a335ba344ab0e24f3ae67f3e68677260ec0784c164cd05d64736f6c634300081e0033", +} as const; diff --git a/utils/e2e-tests/ts/lib/abis/evmTracing/caller.ts b/utils/e2e-tests/ts/lib/abis/evmTracing/caller.ts new file mode 100644 index 000000000..549167f8f --- /dev/null +++ b/utils/e2e-tests/ts/lib/abis/evmTracing/caller.ts @@ -0,0 +1,49 @@ +// pragma solidity >=0.8.3; +// +// contract TraceCaller { +// TraceCallee internal callee; +// uint256 public store; +// +// function someAction(address addr, uint256 number) public { +// callee = TraceCallee(addr); +// store = callee.addSeven(number); +// } +// } + +export default { + abi: [ + { + inputs: [ + { + internalType: "address", + name: "addr", + type: "address", + }, + { + internalType: "uint256", + name: "number", + type: "uint256", + }, + ], + name: "someAction", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "store", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + ], + bytecode: + "0x6080604052348015600e575f5ffd5b506102c68061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610034575f3560e01c8063398f722314610038578063975057e714610054575b5f5ffd5b610052600480360381019061004d91906101eb565b610072565b005b61005c610154565b6040516100699190610238565b60405180910390f35b815f5f6101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16631ba64846826040518263ffffffff1660e01b815260040161010a9190610238565b6020604051808303815f875af1158015610126573d5f5f3e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061014a9190610265565b6001819055505050565b60015481565b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6101878261015e565b9050919050565b6101978161017d565b81146101a1575f5ffd5b50565b5f813590506101b28161018e565b92915050565b5f819050919050565b6101ca816101b8565b81146101d4575f5ffd5b50565b5f813590506101e5816101c1565b92915050565b5f5f604083850312156102015761020061015a565b5b5f61020e858286016101a4565b925050602061021f858286016101d7565b9150509250929050565b610232816101b8565b82525050565b5f60208201905061024b5f830184610229565b92915050565b5f8151905061025f816101c1565b92915050565b5f6020828403121561027a5761027961015a565b5b5f61028784828501610251565b9150509291505056fea264697066735822122063e98df9ee71802baca2f482bdaadad21254526b1716f2dea23f395c7d9c055e64736f6c634300081e0033", +} as const; diff --git a/utils/e2e-tests/ts/lib/abis/evmTracing/heavy.ts b/utils/e2e-tests/ts/lib/abis/evmTracing/heavy.ts new file mode 100644 index 000000000..6a4f49561 --- /dev/null +++ b/utils/e2e-tests/ts/lib/abis/evmTracing/heavy.ts @@ -0,0 +1,298 @@ +// pragma solidity >=0.8.3; +// +// contract Heavy { +// constructor(bool should_revert) { +// if (should_revert) { +// revert(); +// } +// } +// +// function call_ok() public pure {} +// +// function call_revert() public pure { +// revert(); +// } +// +// function subcalls(address target0, address target1) public pure { +// try Heavy(target0).subsubcalls(target1) {} catch {} +// try Heavy(target0).subsubcalls(target1) {} catch {} +// } +// +// function subsubcalls(address target1) public pure { +// Heavy(target1).call_ok(); +// Heavy(target1).call_revert(); +// } +// +// function heavy_steps(uint256 store_steps, uint256 op_steps) external { +// while (store_steps != 0) { +// assembly { +// sstore(store_steps, store_steps) +// } +// store_steps--; +// } +// +// while (op_steps != 0) { +// op_steps--; +// } +// } +// +// // This part is to trace Wasm memory overflow +// uint256 public a; +// uint256 public b; +// uint256 public c; +// uint256 public d; +// uint256 public e; +// uint256 public f; +// uint256 public g; +// uint256 public h; +// uint256 public i; +// uint256 public j; +// +// function set_and_loop(uint256 loops) public returns (uint256 result) { +// a = 1; +// b = 1; +// c = 1; +// d = 1; +// e = 1; +// f = 1; +// g = 1; +// h = 1; +// i = 1; +// j = 1; +// uint256 count = 0; +// while (i < loops) { +// count += 1; +// } +// return 1; +// } +// } + +export default { + abi: [ + { + inputs: [ + { + internalType: "bool", + name: "should_revert", + type: "bool", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "a", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "b", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "c", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "call_ok", + outputs: [], + stateMutability: "pure", + type: "function", + }, + { + inputs: [], + name: "call_revert", + outputs: [], + stateMutability: "pure", + type: "function", + }, + { + inputs: [], + name: "d", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "e", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "f", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "g", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "h", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "store_steps", + type: "uint256", + }, + { + internalType: "uint256", + name: "op_steps", + type: "uint256", + }, + ], + name: "heavy_steps", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "i", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "j", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "loops", + type: "uint256", + }, + ], + name: "set_and_loop", + outputs: [ + { + internalType: "uint256", + name: "result", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "target0", + type: "address", + }, + { + internalType: "address", + name: "target1", + type: "address", + }, + ], + name: "subcalls", + outputs: [], + stateMutability: "pure", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "target1", + type: "address", + }, + ], + name: "subsubcalls", + outputs: [], + stateMutability: "pure", + type: "function", + }, + ], + bytecode: + "0x6080604052348015600e575f5ffd5b506040516108503803806108508339818101604052810190602e91906070565b80156037575f5ffd5b506096565b5f5ffd5b5f8115159050919050565b6052816040565b8114605b575f5ffd5b50565b5f81519050606a81604b565b92915050565b5f602082840312156082576081603c565b5b5f608d84828501605e565b91505092915050565b6107ad806100a35f395ff3fe608060405234801561000f575f5ffd5b50600436106100fe575f3560e01c8063b582ec5f11610095578063e2179b8e11610064578063e2179b8e14610250578063e5aa3d581461026e578063f34f16101461028c578063ffae15ba146102a8576100fe565b8063b582ec5f146101ec578063b8c9d3651461020a578063c3da42b814610228578063cb30e69614610246576100fe565b80635eaf9bc1116100d15780635eaf9bc11461018c5780636422847b146101965780638a054ac2146101b2578063a885f4e3146101d0576100fe565b80630dbe671f1461010257806313128fdc1461012057806326121ff0146101505780634df7e3d01461016e575b5f5ffd5b61010a6102c6565b6040516101179190610555565b60405180910390f35b61013a6004803603810190610135919061059c565b6102cb565b6040516101479190610555565b60405180910390f35b610158610347565b6040516101659190610555565b60405180910390f35b61017661034d565b6040516101839190610555565b60405180910390f35b610194610353565b005b6101b060048036038101906101ab91906105c7565b610355565b005b6101ba610392565b6040516101c79190610555565b60405180910390f35b6101ea60048036038101906101e5919061065f565b610398565b005b6101f461044f565b6040516102019190610555565b60405180910390f35b610212610455565b60405161021f9190610555565b60405180910390f35b61023061045b565b60405161023d9190610555565b60405180910390f35b61024e610461565b005b610258610465565b6040516102659190610555565b60405180910390f35b61027661046b565b6040516102839190610555565b60405180910390f35b6102a660048036038101906102a1919061068a565b610471565b005b6102b0610537565b6040516102bd9190610555565b60405180910390f35b5f5481565b5f60015f8190555060018081905550600160028190555060016003819055506001600481905550600160058190555060016006819055506001600781905550600160088190555060016009819055505f5f90505b82600854101561033d5760018161033691906106f5565b905061031f565b6001915050919050565b60055481565b60015481565b565b5b5f821461037357818255818061036b90610728565b925050610356565b5b5f811461038e57808061038690610728565b915050610374565b5050565b60035481565b8073ffffffffffffffffffffffffffffffffffffffff16635eaf9bc16040518163ffffffff1660e01b81526004015f6040518083038186803b1580156103dc575f5ffd5b505afa1580156103ee573d5f5f3e3d5ffd5b505050508073ffffffffffffffffffffffffffffffffffffffff1663cb30e6966040518163ffffffff1660e01b81526004015f6040518083038186803b158015610436575f5ffd5b505afa158015610448573d5f5f3e3d5ffd5b5050505050565b60095481565b60075481565b60025481565b5f5ffd5b60065481565b60085481565b8173ffffffffffffffffffffffffffffffffffffffff1663a885f4e3826040518263ffffffff1660e01b81526004016104aa919061075e565b5f6040518083038186803b1580156104c0575f5ffd5b505afa9250505080156104d1575060015b508173ffffffffffffffffffffffffffffffffffffffff1663a885f4e3826040518263ffffffff1660e01b815260040161050b919061075e565b5f6040518083038186803b158015610521575f5ffd5b505afa925050508015610532575060015b505050565b60045481565b5f819050919050565b61054f8161053d565b82525050565b5f6020820190506105685f830184610546565b92915050565b5f5ffd5b61057b8161053d565b8114610585575f5ffd5b50565b5f8135905061059681610572565b92915050565b5f602082840312156105b1576105b061056e565b5b5f6105be84828501610588565b91505092915050565b5f5f604083850312156105dd576105dc61056e565b5b5f6105ea85828601610588565b92505060206105fb85828601610588565b9150509250929050565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f61062e82610605565b9050919050565b61063e81610624565b8114610648575f5ffd5b50565b5f8135905061065981610635565b92915050565b5f602082840312156106745761067361056e565b5b5f6106818482850161064b565b91505092915050565b5f5f604083850312156106a05761069f61056e565b5b5f6106ad8582860161064b565b92505060206106be8582860161064b565b9150509250929050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f6106ff8261053d565b915061070a8361053d565b9250828201905080821115610722576107216106c8565b5b92915050565b5f6107328261053d565b91505f8203610744576107436106c8565b5b600182039050919050565b61075881610624565b82525050565b5f6020820190506107715f83018461074f565b9291505056fea26469706673582212203a48031a3a6911e44a2dfe636fa7c6f4de7214d2c0461bd2ff205366af22bb6664736f6c634300081e0033", +} as const; diff --git a/utils/e2e-tests/ts/lib/abis/evmTracing/incrementor.ts b/utils/e2e-tests/ts/lib/abis/evmTracing/incrementor.ts new file mode 100644 index 000000000..3cdb3e4f8 --- /dev/null +++ b/utils/e2e-tests/ts/lib/abis/evmTracing/incrementor.ts @@ -0,0 +1,69 @@ +// pragma solidity >=0.8.3; +// +// contract Incrementor { +// uint256 public count; +// +// constructor() { +// count = 0; +// } +// +// function incr() public { +// count = count + 1; +// } +// +// function incr(uint256 num) public returns (uint256) { +// count = count + num; +// return count; +// } +// } + +export default { + abi: [ + { + inputs: [], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "count", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "incr", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "num", + type: "uint256", + }, + ], + name: "incr", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + ], + bytecode: + "0x6080604052348015600e575f5ffd5b505f5f819055506101f1806100225f395ff3fe608060405234801561000f575f5ffd5b506004361061003f575f3560e01c806306661abd14610043578063119fbbd41461006157806321b13c481461006b575b5f5ffd5b61004b61009b565b60405161005891906100e9565b60405180910390f35b6100696100a0565b005b61008560048036038101906100809190610130565b6100b5565b60405161009291906100e9565b60405180910390f35b5f5481565b60015f546100ae9190610188565b5f81905550565b5f815f546100c39190610188565b5f819055505f549050919050565b5f819050919050565b6100e3816100d1565b82525050565b5f6020820190506100fc5f8301846100da565b92915050565b5f5ffd5b61010f816100d1565b8114610119575f5ffd5b50565b5f8135905061012a81610106565b92915050565b5f6020828403121561014557610144610102565b5b5f6101528482850161011c565b91505092915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f610192826100d1565b915061019d836100d1565b92508282019050808211156101b5576101b461015b565b5b9291505056fea2646970667358221220836c30d0514664def3debcdd06557178b40a286f683281477d734ff6edbfa46064736f6c634300081e0033", +} as const; diff --git a/utils/e2e-tests/ts/lib/abis/evmTracing/looper.ts b/utils/e2e-tests/ts/lib/abis/evmTracing/looper.ts new file mode 100644 index 000000000..fa8dc500f --- /dev/null +++ b/utils/e2e-tests/ts/lib/abis/evmTracing/looper.ts @@ -0,0 +1,57 @@ +// pragma solidity >=0.8.3; +// +// contract Looper { +// uint256 public count; +// +// function infinite() public pure { +// while (true) {} +// } +// +// function incrementalLoop(uint256 n) public { +// uint256 i = 0; +// while (i < n) { +// count = count + 1; +// i += 1; +// } +// } +// } + +export default { + abi: [ + { + inputs: [], + name: "count", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "n", + type: "uint256", + }, + ], + name: "incrementalLoop", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "infinite", + outputs: [], + stateMutability: "pure", + type: "function", + }, + ], + bytecode: + "0x6080604052348015600e575f5ffd5b506101ed8061001c5f395ff3fe608060405234801561000f575f5ffd5b506004361061003f575f3560e01c806306661abd146100435780635bec9e67146100615780636e4709f91461006b575b5f5ffd5b61004b610087565b60405161005891906100e5565b60405180910390f35b61006961008c565b005b6100856004803603810190610080919061012c565b610095565b005b5f5481565b5b600161008d57565b5f5f90505b818110156100c95760015f546100b09190610184565b5f819055506001816100c29190610184565b905061009a565b5050565b5f819050919050565b6100df816100cd565b82525050565b5f6020820190506100f85f8301846100d6565b92915050565b5f5ffd5b61010b816100cd565b8114610115575f5ffd5b50565b5f8135905061012681610102565b92915050565b5f60208284031215610141576101406100fe565b5b5f61014e84828501610118565b91505092915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f61018e826100cd565b9150610199836100cd565b92508282019050808211156101b1576101b0610157565b5b9291505056fea26469706673582212202018b089162ffbb47bf3ea8487f4a122e87b63c21811d615bec9e32546e3f58064736f6c634300081e0033", +} as const; diff --git a/utils/e2e-tests/ts/lib/abis/evmTracing/multiplyBy7.ts b/utils/e2e-tests/ts/lib/abis/evmTracing/multiplyBy7.ts new file mode 100644 index 000000000..8daa85e39 --- /dev/null +++ b/utils/e2e-tests/ts/lib/abis/evmTracing/multiplyBy7.ts @@ -0,0 +1,33 @@ +// pragma solidity >=0.8.3; +// +// contract MultiplyBy7 { +// function multiply(uint256 a) public pure returns (uint256 d) { +// return a * 7; +// } +// } + +export default { + abi: [ + { + inputs: [ + { + internalType: "uint256", + name: "a", + type: "uint256", + }, + ], + name: "multiply", + outputs: [ + { + internalType: "uint256", + name: "d", + type: "uint256", + }, + ], + stateMutability: "pure", + type: "function", + }, + ], + bytecode: + "0x6080604052348015600e575f5ffd5b506101a08061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610029575f3560e01c8063c6888fa11461002d575b5f5ffd5b610047600480360381019061004291906100a9565b61005d565b60405161005491906100e3565b60405180910390f35b5f60078261006b9190610129565b9050919050565b5f5ffd5b5f819050919050565b61008881610076565b8114610092575f5ffd5b50565b5f813590506100a38161007f565b92915050565b5f602082840312156100be576100bd610072565b5b5f6100cb84828501610095565b91505092915050565b6100dd81610076565b82525050565b5f6020820190506100f65f8301846100d4565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f61013382610076565b915061013e83610076565b925082820261014c81610076565b91508282048414831517610163576101626100fc565b5b509291505056fea26469706673582212204f32ea69d8b2260c7fffb56c84905516c1c665b20b41698aaea81eb3df977c0a64736f6c634300081e0033", +} as const; diff --git a/utils/e2e-tests/ts/lib/helpers/blockscout/README.md b/utils/e2e-tests/ts/lib/helpers/blockscout/README.md new file mode 100644 index 000000000..bbc8939ca --- /dev/null +++ b/utils/e2e-tests/ts/lib/helpers/blockscout/README.md @@ -0,0 +1,8 @@ +# Blockscout test helpers + +The directory contains a list of fixtures containing blockscout tracer code. + +## Source data + +- [tracer_v2](tracer_v2.min.json) - [source code](https://github.com/moonbeam-foundation/moonbeam/blob/d47da4eee70f60bd835c39f9e6098e1e4e0c9347/test/helpers/tracer/blockscout_tracer_v2.min.json) +- [tracer](tracer.min.json) - [source code](https://github.com/moonbeam-foundation/moonbeam/blob/d47da4eee70f60bd835c39f9e6098e1e4e0c9347/test/helpers/tracer/blockscout_tracer.min.json) diff --git a/utils/e2e-tests/ts/lib/helpers/blockscout/tracer.min.json b/utils/e2e-tests/ts/lib/helpers/blockscout/tracer.min.json new file mode 100644 index 000000000..e81fe875d --- /dev/null +++ b/utils/e2e-tests/ts/lib/helpers/blockscout/tracer.min.json @@ -0,0 +1,3 @@ +{ + "body": "// tracer allows Geth's `debug_traceTransaction` to mimic the output of Parity's `trace_replayTransaction`\n{\n // The call stack of the EVM execution.\n callStack: [{}],\n\n // step is invoked for every opcode that the VM executes.\n step(log, db) {\n // Capture any errors immediately\n const error = log.getError();\n\n if (error !== undefined) {\n this.fault(log, db);\n } else {\n this.success(log, db);\n }\n },\n\n // fault is invoked when the actual execution of an opcode fails.\n fault(log, db) {\n // If the topmost call already reverted, don't handle the additional fault again\n if (this.topCall().error === undefined) {\n this.putError(log);\n }\n },\n\n putError(log) {\n if (this.callStack.length > 1) {\n this.putErrorInTopCall(log);\n } else {\n this.putErrorInBottomCall(log);\n }\n },\n\n putErrorInTopCall(log) {\n // Pop off the just failed call\n const call = this.callStack.pop();\n this.putErrorInCall(log, call);\n this.pushChildCall(call);\n },\n\n putErrorInBottomCall(log) {\n const call = this.bottomCall();\n this.putErrorInCall(log, call);\n },\n\n putErrorInCall(log, call) {\n call.error = log.getError();\n\n // Consume all available gas and clean any leftovers\n if (call.gasBigInt !== undefined) {\n call.gasUsedBigInt = call.gasBigInt;\n }\n\n delete call.outputOffset;\n delete call.outputLength;\n },\n\n topCall() {\n return this.callStack[this.callStack.length - 1];\n },\n\n bottomCall() {\n return this.callStack[0];\n },\n\n pushChildCall(childCall) {\n const topCall = this.topCall();\n\n if (topCall.calls === undefined) {\n topCall.calls = [];\n }\n\n topCall.calls.push(childCall);\n },\n\n pushGasToTopCall(log) {\n const topCall = this.topCall();\n\n if (topCall.gasBigInt === undefined) {\n topCall.gasBigInt = log.getGas();\n }\n topCall.gasUsedBigInt = topCall.gasBigInt - log.getGas() - log.getCost();\n },\n\n success(log, db) {\n const op = log.op.toString();\n\n this.beforeOp(log, db);\n\n switch (op) {\n case 'CREATE':\n this.createOp(log);\n break;\n case 'CREATE2':\n this.create2Op(log);\n break;\n case 'SELFDESTRUCT':\n this.selfDestructOp(log, db);\n break;\n case 'CALL':\n case 'CALLCODE':\n case 'DELEGATECALL':\n case 'STATICCALL':\n this.callOp(log, op);\n break;\n case 'REVERT':\n this.revertOp();\n break;\n }\n },\n\n beforeOp(log, db) {\n /**\n * Depths\n * 0 - `ctx`. Never shows up in `log.getDepth()`\n * 1 - first level of `log.getDepth()`\n *\n * callStack indexes\n *\n * 0 - pseudo-call stand-in for `ctx` in initializer (`callStack: [{}]`)\n * 1 - first callOp inside of `ctx`\n */\n const logDepth = log.getDepth();\n const callStackDepth = this.callStack.length;\n\n if (logDepth < callStackDepth) {\n // Pop off the last call and get the execution results\n const call = this.callStack.pop();\n\n const ret = log.stack.peek(0);\n\n if (!ret.equals(0)) {\n if (call.type === 'create' || call.type === 'create2') {\n call.createdContractAddressHash = toHex(toAddress(ret.toString(16)));\n call.createdContractCode = toHex(db.getCode(toAddress(ret.toString(16))));\n } else {\n call.output = toHex(log.memory.slice(call.outputOffset, call.outputOffset + call.outputLength));\n }\n } else if (call.error === undefined) {\n call.error = 'internal failure';\n }\n\n delete call.outputOffset;\n delete call.outputLength;\n\n this.pushChildCall(call);\n }\n else {\n this.pushGasToTopCall(log);\n }\n },\n\n createOp(log) {\n const inputOffset = log.stack.peek(1).valueOf();\n const inputLength = log.stack.peek(2).valueOf();\n const inputEnd = inputOffset + inputLength;\n const stackValue = log.stack.peek(0);\n\n const call = {\n type: 'create',\n from: toHex(log.contract.getAddress()),\n init: toHex(log.memory.slice(inputOffset, inputEnd)),\n valueBigInt: bigInt(stackValue.toString(10))\n };\n this.callStack.push(call);\n },\n\n create2Op(log) {\n const inputOffset = log.stack.peek(1).valueOf();\n const inputLength = log.stack.peek(2).valueOf();\n const inputEnd = inputOffset + inputLength;\n const stackValue = log.stack.peek(0);\n\n const call = {\n type: 'create2',\n from: toHex(log.contract.getAddress()),\n init: toHex(log.memory.slice(inputOffset, inputEnd)),\n valueBigInt: bigInt(stackValue.toString(10))\n };\n this.callStack.push(call);\n },\n\n selfDestructOp(log, db) {\n const contractAddress = log.contract.getAddress();\n\n this.pushChildCall({\n type: 'selfdestruct',\n from: toHex(contractAddress),\n to: toHex(toAddress(log.stack.peek(0).toString(16))),\n gasBigInt: log.getGas(),\n gasUsedBigInt: log.getCost(),\n valueBigInt: db.getBalance(contractAddress)\n });\n },\n\n callOp(log, op) {\n const to = toAddress(log.stack.peek(1).toString(16));\n\n // Skip any pre-compile invocations, those are just fancy opcodes\n if (!isPrecompiled(to)) {\n this.callCustomOp(log, op, to);\n }\n },\n\n callCustomOp(log, op, to) {\n const stackOffset = (op === 'DELEGATECALL' || op === 'STATICCALL' ? 0 : 1);\n\n const inputOffset = log.stack.peek(2 + stackOffset).valueOf();\n const inputLength = log.stack.peek(3 + stackOffset).valueOf();\n const inputEnd = inputOffset + inputLength;\n\n const call = {\n type: 'call',\n callType: op.toLowerCase(),\n from: toHex(log.contract.getAddress()),\n to: toHex(to),\n input: toHex(log.memory.slice(inputOffset, inputEnd)),\n outputOffset: log.stack.peek(4 + stackOffset).valueOf(),\n outputLength: log.stack.peek(5 + stackOffset).valueOf()\n };\n\n switch (op) {\n case 'CALL':\n case 'CALLCODE':\n call.valueBigInt = bigInt(log.stack.peek(2));\n break;\n case 'DELEGATECALL':\n // value inherited from scope during call sequencing\n break;\n case 'STATICCALL':\n // by definition static calls transfer no value\n call.valueBigInt = bigInt.zero;\n break;\n default:\n throw \"Unknown custom call op \" + op;\n }\n\n this.callStack.push(call);\n },\n\n revertOp() {\n this.topCall().error = 'execution reverted';\n },\n\n // result is invoked when all the opcodes have been iterated over and returns\n // the final result of the tracing.\n result(ctx, db) {\n const result = this.ctxToResult(ctx, db);\n const filtered = this.filterNotUndefined(result);\n const callSequence = this.sequence(filtered, [], filtered.valueBigInt, []).callSequence;\n return this.encodeCallSequence(callSequence);\n },\n\n ctxToResult(ctx, db) {\n var result;\n\n switch (ctx.type) {\n case 'CALL':\n result = this.ctxToCall(ctx);\n break;\n case 'CREATE':\n result = this.ctxToCreate(ctx, db);\n break;\n case 'CREATE2':\n result = this.ctxToCreate2(ctx, db);\n break;\n }\n\n return result;\n },\n\n ctxToCall(ctx) {\n const result = {\n type: 'call',\n callType: 'call',\n from: toHex(ctx.from),\n to: toHex(ctx.to),\n valueBigInt: bigInt(ctx.value.toString(10)),\n gasBigInt: bigInt(ctx.gas),\n gasUsedBigInt: bigInt(ctx.gasUsed),\n input: toHex(ctx.input)\n };\n\n this.putBottomChildCalls(result);\n this.putErrorOrOutput(result, ctx);\n\n return result;\n },\n\n putErrorOrOutput(result, ctx) {\n const error = this.error(ctx);\n\n if (error !== undefined) {\n result.error = error;\n } else {\n result.output = toHex(ctx.output);\n }\n },\n\n ctxToCreate(ctx, db) {\n const result = {\n type: 'create',\n from: toHex(ctx.from),\n init: toHex(ctx.input),\n valueBigInt: bigInt(ctx.value.toString(10)),\n gasBigInt: bigInt(ctx.gas),\n gasUsedBigInt: bigInt(ctx.gasUsed)\n };\n\n this.putBottomChildCalls(result);\n this.putErrorOrCreatedContract(result, ctx, db);\n\n return result;\n },\n\n ctxToCreate2(ctx, db) {\n const result = {\n type: 'create2',\n from: toHex(ctx.from),\n init: toHex(ctx.input),\n valueBigInt: bigInt(ctx.value.toString(10)),\n gasBigInt: bigInt(ctx.gas),\n gasUsedBigInt: bigInt(ctx.gasUsed)\n };\n\n this.putBottomChildCalls(result);\n this.putErrorOrCreatedContract(result, ctx, db);\n\n return result;\n },\n\n putBottomChildCalls(result) {\n const bottomCall = this.bottomCall();\n const bottomChildCalls = bottomCall.calls;\n\n if (bottomChildCalls !== undefined) {\n result.calls = bottomChildCalls;\n }\n },\n\n putErrorOrCreatedContract(result, ctx, db) {\n const error = this.error(ctx);\n\n if (error !== undefined) {\n result.error = error\n } else {\n result.createdContractAddressHash = toHex(ctx.to);\n result.createdContractCode = toHex(db.getCode(ctx.to));\n }\n },\n\n error(ctx) {\n var error;\n\n const bottomCall = this.bottomCall();\n const bottomCallError = bottomCall.error;\n\n if (bottomCallError !== undefined) {\n error = bottomCallError;\n } else {\n const ctxError = ctx.error;\n\n if (ctxError !== undefined) {\n error = ctxError;\n }\n }\n\n return error;\n },\n\n filterNotUndefined(call) {\n for (var key in call) {\n if (call[key] === undefined) {\n delete call[key];\n }\n }\n\n if (call.calls !== undefined) {\n for (var i = 0; i < call.calls.length; i++) {\n call.calls[i] = this.filterNotUndefined(call.calls[i]);\n }\n }\n\n return call;\n },\n\n // sequence converts the finalized calls from a call tree to a call sequence\n sequence(call, callSequence, availableValueBigInt, traceAddress) {\n const subcalls = call.calls;\n delete call.calls;\n\n call.traceAddress = traceAddress;\n\n if (call.type === 'call' && call.callType === 'delegatecall') {\n call.valueBigInt = availableValueBigInt;\n }\n\n var newCallSequence = callSequence.concat([call]);\n\n if (subcalls !== undefined) {\n for (var i = 0; i < subcalls.length; i++) {\n const nestedSequenced = this.sequence(\n subcalls[i],\n newCallSequence,\n call.valueBigInt,\n traceAddress.concat([i])\n );\n newCallSequence = nestedSequenced.callSequence;\n }\n }\n\n return {\n callSequence: newCallSequence\n };\n },\n\n encodeCallSequence(calls) {\n for (var i = 0; i < calls.length; i++) {\n this.encodeCall(calls[i]);\n }\n\n return calls;\n },\n\n encodeCall(call) {\n this.putValue(call);\n this.putGas(call);\n this.putGasUsed(call);\n\n return call;\n },\n\n putValue(call) {\n const valueBigInt = call.valueBigInt;\n delete call.valueBigInt;\n\n call.value = '0x' + valueBigInt.toString(16);\n },\n\n putGas(call) {\n const gasBigInt = call.gasBigInt;\n delete call.gasBigInt;\n\n if (gasBigInt === undefined) {\n gasBigInt = bigInt.zero;\n }\n\n call.gas = '0x' + gasBigInt.toString(16);\n },\n\n putGasUsed(call) {\n const gasUsedBigInt = call.gasUsedBigInt;\n delete call.gasUsedBigInt;\n\n if (gasUsedBigInt === undefined) {\n gasUsedBigInt = bigInt.zero;\n }\n\n call.gasUsed = '0x' + gasUsedBigInt.toString(16);\n }\n}\n" +} diff --git a/utils/e2e-tests/ts/lib/helpers/blockscout/tracer_v2.min.json b/utils/e2e-tests/ts/lib/helpers/blockscout/tracer_v2.min.json new file mode 100644 index 000000000..41e5e3b9a --- /dev/null +++ b/utils/e2e-tests/ts/lib/helpers/blockscout/tracer_v2.min.json @@ -0,0 +1,3 @@ +{ + "body": "// tracer allows Geth's `debug_traceTransaction` to mimic the output of Parity's `trace_replayTransaction`\n{\n // The call stack of the EVM execution.\n callStack: [{}],\n\n // step is invoked for every opcode that the VM executes.\n step(log, db) {\n // Capture any errors immediately\n const error = log.getError();\n\n if (error !== undefined) {\n this.fault(log, db);\n } else {\n this.success(log, db);\n }\n },\n\n // fault is invoked when the actual execution of an opcode fails.\n fault(log, db) {\n // If the topmost call already reverted, don't handle the additional fault again\n if (this.topCall().error === undefined) {\n this.putError(log);\n }\n },\n\n putError(log) {\n if (this.callStack.length > 1) {\n this.putErrorInTopCall(log);\n } else {\n this.putErrorInBottomCall(log);\n }\n },\n\n putErrorInTopCall(log) {\n // Pop off the just failed call\n const call = this.callStack.pop();\n this.putErrorInCall(log, call);\n this.pushChildCall(call);\n },\n\n putErrorInBottomCall(log) {\n const call = this.bottomCall();\n this.putErrorInCall(log, call);\n },\n\n putErrorInCall(log, call) {\n call.error = log.getError();\n\n // Consume all available gas and clean any leftovers\n if (call.gasBigInt !== undefined) {\n call.gasUsedBigInt = call.gasBigInt;\n }\n\n delete call.outputOffset;\n delete call.outputLength;\n },\n\n topCall() {\n return this.callStack[this.callStack.length - 1];\n },\n\n bottomCall() {\n return this.callStack[0];\n },\n\n pushChildCall(childCall) {\n const topCall = this.topCall();\n\n if (topCall.calls === undefined) {\n topCall.calls = [];\n }\n\n topCall.calls.push(childCall);\n },\n\n pushGasToTopCall(log) {\n const topCall = this.topCall();\n\n if (topCall.gasBigInt === undefined) {\n topCall.gasBigInt = log.getGas();\n }\n topCall.gasUsedBigInt = topCall.gasBigInt - log.getGas() - log.getCost();\n },\n\n success(log, db) {\n const op = log.op.toString();\n\n this.beforeOp(log, db);\n\n switch (op) {\n case 'CREATE':\n this.createOp(log);\n break;\n case 'CREATE2':\n this.create2Op(log);\n break;\n case 'SELFDESTRUCT':\n this.selfDestructOp(log, db);\n break;\n case 'CALL':\n case 'CALLCODE':\n case 'DELEGATECALL':\n case 'STATICCALL':\n this.callOp(log, op);\n break;\n case 'REVERT':\n this.revertOp();\n break;\n }\n },\n\n beforeOp(log, db) {\n /**\n * Depths\n * 0 - `ctx`. Never shows up in `log.getDepth()`\n * 1 - first level of `log.getDepth()`\n *\n * callStack indexes\n *\n * 0 - pseudo-call stand-in for `ctx` in initializer (`callStack: [{}]`)\n * 1 - first callOp inside of `ctx`\n */\n const logDepth = log.getDepth();\n const callStackDepth = this.callStack.length;\n\n if (logDepth < callStackDepth) {\n // Pop off the last call and get the execution results\n const call = this.callStack.pop();\n\n const ret = log.stack.peek(0);\n\n if (!ret.equals(0)) {\n if (call.type === 'create' || call.type === 'create2') {\n call.createdContractAddressHash = toHex(toAddress(ret.toString(16)));\n call.createdContractCode = toHex(db.getCode(toAddress(ret.toString(16))));\n } else {\n call.output = toHex(log.memory.slice(call.outputOffset, call.outputOffset + call.outputLength));\n }\n } else if (call.error === undefined) {\n call.error = 'internal failure';\n }\n\n delete call.outputOffset;\n delete call.outputLength;\n\n this.pushChildCall(call);\n }\n else {\n this.pushGasToTopCall(log);\n }\n },\n\n createOp(log) {\n const inputOffset = log.stack.peek(1).valueOf();\n const inputLength = log.stack.peek(2).valueOf();\n const inputEnd = inputOffset + inputLength;\n const stackValue = log.stack.peek(0);\n\n const call = {\n type: 'create',\n from: toHex(log.contract.getAddress()),\n init: toHex(log.memory.slice(inputOffset, inputEnd)),\n valueBigInt: bigInt(stackValue.toString(10))\n };\n this.callStack.push(call);\n },\n\n create2Op(log) {\n const inputOffset = log.stack.peek(1).valueOf();\n const inputLength = log.stack.peek(2).valueOf();\n const inputEnd = inputOffset + inputLength;\n const stackValue = log.stack.peek(0);\n\n const call = {\n type: 'create2',\n from: toHex(log.contract.getAddress()),\n init: toHex(log.memory.slice(inputOffset, inputEnd)),\n valueBigInt: bigInt(stackValue.toString(10))\n };\n this.callStack.push(call);\n },\n\n selfDestructOp(log, db) {\n const contractAddress = log.contract.getAddress();\n\n this.pushChildCall({\n type: 'selfdestruct',\n from: toHex(contractAddress),\n to: toHex(toAddress(log.stack.peek(0).toString(16))),\n gasBigInt: log.getGas(),\n gasUsedBigInt: log.getCost(),\n valueBigInt: db.getBalance(contractAddress)\n });\n },\n\n callOp(log, op) {\n const to = toAddress(log.stack.peek(1).toString(16));\n\n // Skip any pre-compile invocations, those are just fancy opcodes\n if (!isPrecompiled(to)) {\n this.callCustomOp(log, op, to);\n }\n },\n\n callCustomOp(log, op, to) {\n const stackOffset = (op === 'DELEGATECALL' || op === 'STATICCALL' ? 0 : 1);\n\n const inputOffset = log.stack.peek(2 + stackOffset).valueOf();\n const inputLength = log.stack.peek(3 + stackOffset).valueOf();\n const inputEnd = inputOffset + inputLength;\n\n const call = {\n type: 'call',\n callType: op.toLowerCase(),\n from: toHex(log.contract.getAddress()),\n to: toHex(to),\n input: toHex(log.memory.slice(inputOffset, inputEnd)),\n outputOffset: log.stack.peek(4 + stackOffset).valueOf(),\n outputLength: log.stack.peek(5 + stackOffset).valueOf()\n };\n\n switch (op) {\n case 'CALL':\n case 'CALLCODE':\n call.valueBigInt = bigInt(log.stack.peek(2));\n break;\n case 'DELEGATECALL':\n // value inherited from scope during call sequencing\n break;\n case 'STATICCALL':\n // by definition static calls transfer no value\n call.valueBigInt = bigInt.zero;\n break;\n default:\n throw 'Unknown custom call op ' + op;\n }\n\n this.callStack.push(call);\n },\n\n revertOp() {\n this.topCall().error = 'execution reverted';\n },\n\n // result is invoked when all the opcodes have been iterated over and returns\n // the final result of the tracing.\n result(ctx, db) {\n const result = this.ctxToResult(ctx, db);\n const filtered = this.filterNotUndefined(result);\n const callSequence = this.sequence(filtered, [], filtered.valueBigInt, []).callSequence;\n return this.encodeCallSequence(callSequence);\n },\n\n ctxToResult(ctx, db) {\n var result;\n\n switch (ctx.type) {\n case 'CALL':\n result = this.ctxToCall(ctx);\n break;\n case 'CREATE':\n result = this.ctxToCreate(ctx, db);\n break;\n case 'CREATE2':\n result = this.ctxToCreate2(ctx, db);\n break;\n }\n\n return result;\n },\n\n ctxToCall(ctx) {\n const result = {\n type: 'call',\n callType: 'call',\n from: toHex(ctx.from),\n to: toHex(ctx.to),\n valueBigInt: bigInt(ctx.value.toString(10)),\n gasBigInt: bigInt(ctx.gas),\n gasUsedBigInt: bigInt(ctx.gasUsed),\n input: toHex(ctx.input)\n };\n\n this.putBottomChildCalls(result);\n this.putErrorOrOutput(result, ctx);\n\n return result;\n },\n\n putErrorOrOutput(result, ctx) {\n const error = this.error(ctx);\n\n if (error !== undefined) {\n result.error = error;\n } else {\n result.output = toHex(ctx.output);\n }\n },\n\n ctxToCreate(ctx, db) {\n const result = {\n type: 'create',\n from: toHex(ctx.from),\n init: toHex(ctx.input),\n valueBigInt: bigInt(ctx.value.toString(10)),\n gasBigInt: bigInt(ctx.gas),\n gasUsedBigInt: bigInt(ctx.gasUsed)\n };\n\n this.putBottomChildCalls(result);\n this.putErrorOrCreatedContract(result, ctx, db);\n\n return result;\n },\n\n ctxToCreate2(ctx, db) {\n const result = {\n type: 'create2',\n from: toHex(ctx.from),\n init: toHex(ctx.input),\n valueBigInt: bigInt(ctx.value.toString(10)),\n gasBigInt: bigInt(ctx.gas),\n gasUsedBigInt: bigInt(ctx.gasUsed)\n };\n\n this.putBottomChildCalls(result);\n this.putErrorOrCreatedContract(result, ctx, db);\n\n return result;\n },\n\n putBottomChildCalls(result) {\n const bottomCall = this.bottomCall();\n const bottomChildCalls = bottomCall.calls;\n\n if (bottomChildCalls !== undefined) {\n result.calls = bottomChildCalls;\n }\n },\n\n putErrorOrCreatedContract(result, ctx, db) {\n const error = this.error(ctx);\n\n if (error !== undefined) {\n result.error = error\n } else {\n result.createdContractAddressHash = toHex(ctx.to);\n if (toHex(ctx.input) != '0x') {\n result.createdContractCode = toHex(db.getCode(ctx.to));\n } else {\n result.createdContractCode = '0x';\n }\n }\n },\n\n error(ctx) {\n var error;\n\n const bottomCall = this.bottomCall();\n const bottomCallError = bottomCall.error;\n\n if (bottomCallError !== undefined) {\n error = bottomCallError;\n } else {\n const ctxError = ctx.error;\n\n if (ctxError !== undefined) {\n error = ctxError;\n }\n }\n\n return error;\n },\n\n filterNotUndefined(call) {\n for (var key in call) {\n if (call[key] === undefined) {\n delete call[key];\n }\n }\n\n if (call.calls !== undefined) {\n for (var i = 0; i < call.calls.length; i++) {\n call.calls[i] = this.filterNotUndefined(call.calls[i]);\n }\n }\n\n return call;\n },\n\n // sequence converts the finalized calls from a call tree to a call sequence\n sequence(call, callSequence, availableValueBigInt, traceAddress) {\n const subcalls = call.calls;\n delete call.calls;\n\n call.traceAddress = traceAddress;\n\n if (call.type === 'call' && call.callType === 'delegatecall') {\n call.valueBigInt = availableValueBigInt;\n }\n\n var newCallSequence = callSequence.concat([call]);\n\n if (subcalls !== undefined) {\n for (var i = 0; i < subcalls.length; i++) {\n const nestedSequenced = this.sequence(\n subcalls[i],\n newCallSequence,\n call.valueBigInt,\n traceAddress.concat([i])\n );\n newCallSequence = nestedSequenced.callSequence;\n }\n }\n\n return {\n callSequence: newCallSequence\n };\n },\n\n encodeCallSequence(calls) {\n for (var i = 0; i < calls.length; i++) {\n this.encodeCall(calls[i]);\n }\n\n return calls;\n },\n\n encodeCall(call) {\n this.putValue(call);\n this.putGas(call);\n this.putGasUsed(call);\n\n return call;\n },\n\n putValue(call) {\n const valueBigInt = call.valueBigInt;\n delete call.valueBigInt;\n\n call.value = '0x' + valueBigInt.toString(16);\n },\n\n putGas(call) {\n const gasBigInt = call.gasBigInt;\n delete call.gasBigInt;\n\n if (gasBigInt === undefined) {\n gasBigInt = bigInt.zero;\n }\n\n call.gas = '0x' + gasBigInt.toString(16);\n },\n\n putGasUsed(call) {\n const gasUsedBigInt = call.gasUsedBigInt;\n delete call.gasUsedBigInt;\n\n if (gasUsedBigInt === undefined) {\n gasUsedBigInt = bigInt.zero;\n }\n\n call.gasUsed = '0x' + gasUsedBigInt.toString(16);\n }\n}\n" +} diff --git a/utils/e2e-tests/ts/lib/rpcUtils.ts b/utils/e2e-tests/ts/lib/rpcUtils.ts new file mode 100644 index 000000000..a668b4f2a --- /dev/null +++ b/utils/e2e-tests/ts/lib/rpcUtils.ts @@ -0,0 +1,36 @@ +//! Common RPC utils. + +interface JsonRpcResponse { + result?: any; + error?: { + code: number; + message: string; + }; +} + +export async function customRpcRequest( + endpoint: string, + method: string, + params: any[] = [], +): Promise { + const data = { + jsonrpc: "2.0", + id: 1, + method, + params, + }; + + const response = await fetch(endpoint, { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }); + + const responseData = (await response.json()) as JsonRpcResponse; + + if (responseData.error) { + throw new Error(responseData.error.message); + } + + return responseData.result; +} diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceBlockByGeneral.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceBlockByGeneral.ts new file mode 100644 index 000000000..5006be99f --- /dev/null +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceBlockByGeneral.ts @@ -0,0 +1,92 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { RunNodeState, runNode } from "../../lib/node"; +import * as eth from "../../lib/ethViem"; +import { beforeEachWithCleanup } from "../../lib/lifecycle"; +import callee from "../../lib/abis/evmTracing/callee"; +import caller from "../../lib/abis/evmTracing/caller"; +import { encodeFunctionData } from "viem"; +import { customRpcRequest } from "../../lib/rpcUtils"; + +describe("`debug_traceBlockByNumber` and `debug_traceBlockByHash` tests to verify general logic", () => { + let node: RunNodeState; + let publicClient: eth.PublicClientWebSocket; + let devClients: eth.DevClientsWebSocket; + beforeEachWithCleanup(async (cleanup) => { + node = runNode( + { + args: ["--tracing-mode=debug", "--dev", "--tmp"], + }, + cleanup.push, + ); + + await node.waitForBoot; + + publicClient = eth.publicClientFromNodeWebSocket(node, cleanup.push); + devClients = eth.devClientsFromNodeWebSocket(node, cleanup.push); + }, 60 * 1000); + + let calleeAddress: `0x${string}`; + let callerAddress: `0x${string}`; + + beforeEach(async () => { + const [alice, _] = devClients; + + const deployCalleeContractTxHash = await alice.deployContract({ + abi: callee.abi, + bytecode: callee.bytecode, + }); + const deployCalleeContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployCalleeContractTxHash, + timeout: 18_000, + }); + calleeAddress = deployCalleeContractTxReceipt.contractAddress!; + + const deployCallerContractTxHash = await alice.deployContract({ + abi: caller.abi, + bytecode: caller.bytecode, + }); + const deployCallerContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployCallerContractTxHash, + timeout: 18_000, + }); + callerAddress = deployCallerContractTxReceipt.contractAddress!; + }); + + it("should trace block by number and hash", async () => { + const [alice, _] = devClients; + + const txHash = await alice.sendTransaction({ + to: callerAddress, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "someAction", + args: [calleeAddress, 7n], + }), + }); + const txReceipt = await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + const blockNumberHex = txReceipt.blockNumber.toString(16); + const blockHash = txReceipt.blockHash; + + const responseByNumber = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceBlockByNumber", + [blockNumberHex, { tracer: "callTracer" }], + ); + + expect(responseByNumber.length).to.equal(1); + expect(txHash).to.equal(responseByNumber[0].txHash); + + const responseByHash = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceBlockByHash", + [blockHash, { tracer: "callTracer" }], + ); + + expect(responseByHash.length).to.equal(1); + expect(txHash).to.equal(responseByHash[0].txHash); + }); +}); diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceCallGeneral.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceCallGeneral.ts new file mode 100644 index 000000000..c1dce6d36 --- /dev/null +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceCallGeneral.ts @@ -0,0 +1,93 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { RunNodeState, runNode } from "../../lib/node"; +import * as eth from "../../lib/ethViem"; +import { beforeEachWithCleanup } from "../../lib/lifecycle"; +import callee from "../../lib/abis/evmTracing/callee"; +import caller from "../../lib/abis/evmTracing/caller"; +import { encodeFunctionData } from "viem"; +import { customRpcRequest } from "../../lib/rpcUtils"; + +describe("`debug_traceCall` tests to verify general logic", () => { + let node: RunNodeState; + let publicClient: eth.PublicClientWebSocket; + let devClients: eth.DevClientsWebSocket; + beforeEachWithCleanup(async (cleanup) => { + node = runNode( + { + args: ["--tracing-mode=debug", "--dev", "--tmp"], + }, + cleanup.push, + ); + + await node.waitForBoot; + + publicClient = eth.publicClientFromNodeWebSocket(node, cleanup.push); + devClients = eth.devClientsFromNodeWebSocket(node, cleanup.push); + }, 60 * 1000); + + let calleeAddress: `0x${string}`; + let callerAddress: `0x${string}`; + + beforeEach(async () => { + const [alice, _] = devClients; + + const deployCalleeContractTxHash = await alice.deployContract({ + abi: callee.abi, + bytecode: callee.bytecode, + }); + const deployCalleeContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployCalleeContractTxHash, + timeout: 18_000, + }); + calleeAddress = deployCalleeContractTxReceipt.contractAddress!; + + const deployCallerContractTxHash = await alice.deployContract({ + abi: caller.abi, + bytecode: caller.bytecode, + }); + const deployCallerContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployCallerContractTxHash, + timeout: 18_000, + }); + callerAddress = deployCallerContractTxReceipt.contractAddress!; + }); + + it("should trace nested contract calls", async () => { + const [alice, bob] = devClients; + + const dummyTx = await alice.sendTransaction({ + to: bob.account.address, + value: 1000n, + }); + await publicClient.waitForTransactionReceipt({ hash: dummyTx }); + + const callParams = { + to: callerAddress, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "someAction", + args: [calleeAddress, 7n], + }), + }; + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceCall", + [callParams, "latest"], + ); + + const logs: any[] = []; + response.structLogs.forEach((item: any, index: number) => { + if (item.op === "RETURN") { + logs.push(item); + logs.push(response.structLogs[index + 1]); + } + }); + + expect(logs).to.be.lengthOf(2); + expect(logs[0].depth).to.be.equal(2); + expect(logs[1].depth).to.be.equal(1); + }); +}); diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionCallTracer.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionCallTracer.ts new file mode 100644 index 000000000..0876f98de --- /dev/null +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionCallTracer.ts @@ -0,0 +1,186 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { RunNodeState, runNode } from "../../lib/node"; +import * as eth from "../../lib/ethViem"; +import { beforeEachWithCleanup } from "../../lib/lifecycle"; +import callee from "../../lib/abis/evmTracing/callee"; +import caller from "../../lib/abis/evmTracing/caller"; +import { encodeFunctionData } from "viem"; +import { customRpcRequest } from "../../lib/rpcUtils"; + +describe("`debug_traceTransaction` tests to verify `callTracer` usage logic", () => { + let node: RunNodeState; + let publicClient: eth.PublicClientWebSocket; + let devClients: eth.DevClientsWebSocket; + beforeEachWithCleanup(async (cleanup) => { + node = runNode( + { + args: ["--tracing-mode=debug", "--dev", "--tmp"], + }, + cleanup.push, + ); + + await node.waitForBoot; + + publicClient = eth.publicClientFromNodeWebSocket(node, cleanup.push); + devClients = eth.devClientsFromNodeWebSocket(node, cleanup.push); + }, 60 * 1000); + + let calleeAddress: `0x${string}`; + let callerAddress: `0x${string}`; + + beforeEach(async () => { + const [alice, _] = devClients; + + const deployCalleeContractTxHash = await alice.deployContract({ + abi: callee.abi, + bytecode: callee.bytecode, + }); + const deployCalleeContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployCalleeContractTxHash, + timeout: 18_000, + }); + calleeAddress = deployCalleeContractTxReceipt.contractAddress!; + + const deployCallerContractTxHash = await alice.deployContract({ + abi: caller.abi, + bytecode: caller.bytecode, + }); + const deployCallerContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployCallerContractTxHash, + timeout: 18_000, + }); + callerAddress = deployCallerContractTxReceipt.contractAddress!; + }); + + it("should format as request (Call)", async () => { + const [alice, _] = devClients; + + const txHash = await alice.sendTransaction({ + to: callerAddress, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "someAction", + args: [calleeAddress, 7n], + }), + }); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [txHash, { tracer: "callTracer" }], + ); + + expect(Object.keys(response).sort()).to.deep.equal([ + "calls", + "from", + "gas", + "gasUsed", + "input", + "output", + "to", + "type", + "value", + ]); + expect(response.type).to.be.equal("CALL"); + const calls = response.calls; + expect(calls.length).to.be.eq(1); + const nested_call = calls[0]; + expect(response.to).to.be.equal(nested_call.from); + expect(nested_call.type).to.be.equal("CALL"); + }); + + it("should format as request (Create)", async () => { + const [alice, _] = devClients; + + const txHash = await alice.deployContract({ + abi: callee.abi, + bytecode: callee.bytecode, + }); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [txHash, { tracer: "callTracer" }], + ); + + expect(Object.keys(response).sort()).to.deep.equal([ + "from", + "gas", + "gasUsed", + "input", + "output", + "to", + "type", + "value", + ]); + + expect(response.type).to.be.equal("CREATE"); + }); + + it("should trace block by number and hash", async () => { + const [alice, _] = devClients; + + const txHash = await alice.sendTransaction({ + to: callerAddress, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "someAction", + args: [calleeAddress, 7n], + }), + }); + const txReceipt = await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + const blockNumberHex = txReceipt.blockNumber.toString(16); + const blockHash = txReceipt.blockHash; + + // Trace block by number. + const responseByNumber = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceBlockByNumber", + [blockNumberHex, { tracer: "callTracer" }], + ); + + expect(responseByNumber.length).to.be.equal(1); + expect(responseByNumber[0]["txHash"]).to.be.equal( + txReceipt.transactionHash, + ); + expect(responseByNumber[0]["result"].calls.length).to.be.equal(1); + expect(Object.keys(responseByNumber[0]["result"]).sort()).to.deep.equal([ + "calls", + "from", + "gas", + "gasUsed", + "input", + "output", + "to", + "type", + "value", + ]); + + // Trace block by hash (actually the rpc method is an alias of `debug_traceBlockByNumber`). + const responseByHash = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceBlockByNumber", + [blockHash, { tracer: "callTracer" }], + ); + expect(responseByHash.length).to.be.equal(1); + expect(responseByHash[0]["txHash"]).to.be.equal(txReceipt.transactionHash); + expect(responseByHash[0]["result"].calls.length).to.be.equal(1); + expect(Object.keys(responseByHash[0]["result"]).sort()).to.deep.equal([ + "calls", + "from", + "gas", + "gasUsed", + "input", + "output", + "to", + "type", + "value", + ]); + }); +}); diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionGeneral.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionGeneral.ts new file mode 100644 index 000000000..97657403c --- /dev/null +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionGeneral.ts @@ -0,0 +1,448 @@ +import { describe, expect, it } from "vitest"; +import { RunNodeState, runNode } from "../../lib/node"; +import * as eth from "../../lib/ethViem"; +import { beforeEachWithCleanup } from "../../lib/lifecycle"; +import callee from "../../lib/abis/evmTracing/callee"; +import caller from "../../lib/abis/evmTracing/caller"; +import looper from "../../lib/abis/evmTracing/looper"; +import heavy from "../../lib/abis/evmTracing/heavy"; +import evmSwap from "../../lib/abis/evmSwap"; +import incrementor from "../../lib/abis/evmTracing/incrementor"; +import BS_TRACER from "../../lib/helpers/blockscout/tracer.min.json"; +import BS_TRACER_V2 from "../../lib/helpers/blockscout/tracer_v2.min.json"; +import { encodeFunctionData, hexToNumber } from "viem"; +import { customRpcRequest } from "../../lib/rpcUtils"; + +const evmToNativeSwapPrecompileAddress = + "0x0000000000000000000000000000000000000900"; + +describe("`debug_traceTransaction` tests to verify general logic", () => { + let node: RunNodeState; + let publicClient: eth.PublicClientWebSocket; + let devClients: eth.DevClientsWebSocket; + beforeEachWithCleanup(async (cleanup) => { + node = runNode( + { + args: ["--tracing-mode=debug", "--dev", "--tmp"], + }, + cleanup.push, + ); + + await node.waitForBoot; + + publicClient = eth.publicClientFromNodeWebSocket(node, cleanup.push); + devClients = eth.devClientsFromNodeWebSocket(node, cleanup.push); + }, 60 * 1000); + + it("should trace nested contract calls", async () => { + const [alice, _] = devClients; + + const deployCalleeContractTxHash = await alice.deployContract({ + abi: callee.abi, + bytecode: callee.bytecode, + }); + + const deployCalleeContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployCalleeContractTxHash, + timeout: 18_000, + }); + + const calleeAddress = deployCalleeContractTxReceipt.contractAddress!; + + const deployCallerContractTxHash = await alice.deployContract({ + abi: caller.abi, + bytecode: caller.bytecode, + }); + + const deployCallerContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployCallerContractTxHash, + timeout: 18_000, + }); + + const callerAddress = deployCallerContractTxReceipt.contractAddress!; + + const txHash = await alice.sendTransaction({ + to: callerAddress, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "someAction", + args: [calleeAddress, 7n], + }), + }); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [txHash], + ); + + const logs: any[] = []; + response.structLogs.forEach((item: any, index: number) => { + if (item.op === "RETURN") { + logs.push(item); + logs.push(response.structLogs[index + 1]); + } + }); + + expect(logs).to.be.lengthOf(2); + expect(logs[0].depth).to.be.equal(2); + expect(logs[1].depth).to.be.equal(1); + }); + + it("should trace correctly transfers", async () => { + const [alice, bob] = devClients; + + const txHash = await alice.sendTransaction({ + to: bob.account.address, + value: 1_000_000n, + }); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [txHash], + ); + + expect(response.gas).to.be.eq("0x5208"); // 21_000 gas for a transfer. + }); + + it("should use optional disable parameters", async () => { + const [alice, bob] = devClients; + + const txHash = await alice.sendTransaction({ + to: bob.account.address, + value: 1_000_000n, + }); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [ + txHash, + { disableMemory: true, disableStack: true, disableStorage: true }, + ], + ); + + const logs: any[] = []; + for (const log of response.structLogs) { + const hasStorage = Object.hasOwn(log, "storage"); + const hasMemory = Object.hasOwn(log, "memory"); + const hasStack = Object.hasOwn(log, "stack"); + if (hasStorage || hasMemory || hasStack) { + logs.push(log); + } + } + expect(logs.length).to.be.equal(0); + }); + + it("should trace correctly out of gas transaction execution", async () => { + const [alice, _] = devClients; + + const deployLooperContractTxHash = await alice.deployContract({ + abi: looper.abi, + bytecode: looper.bytecode, + }); + const deployLooperContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployLooperContractTxHash, + timeout: 18_000, + }); + const looperAddress = deployLooperContractTxReceipt.contractAddress!; + + const txHash = await alice.sendTransaction({ + to: looperAddress, + gas: 1_000_000n, + data: "0x5bec9e67", + gasLimit: "0x100000", + value: 0n, + }); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [txHash, { tracer: BS_TRACER.body }], + ); + + expect(response.length).to.be.eq(1); + expect(response[0].error).to.be.equal("out of gas"); + + const responseV2 = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [txHash, { tracer: BS_TRACER_V2.body }], + ); + + expect(responseV2.length).to.be.eq(1); + expect(responseV2[0].error).to.be.equal("out of gas"); + }); + + it("should trace correctly precompiles", async () => { + const [alice, _] = devClients; + + const txHash = await alice.writeContract({ + abi: evmSwap.abi, + address: evmToNativeSwapPrecompileAddress, + functionName: "swap", + args: [ + "0x7700000000000000000000000000000000000000000000000000000000000077", + ], + value: 1_000_000n, + }); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [txHash, { tracer: BS_TRACER.body }], + ); + + expect(response.length).to.be.eq(1); + + const responseV2 = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [txHash, { tracer: BS_TRACER_V2.body }], + ); + + expect(responseV2.length).to.be.eq(1); + }); + + it("should prevent wasm memory overflow", async () => { + const [alice, _] = devClients; + + const deployHeavyContractTxHash = await alice.deployContract({ + abi: heavy.abi, + bytecode: heavy.bytecode, + args: [false], + }); + + const deployHeavyContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployHeavyContractTxHash, + timeout: 18_000, + }); + + const heavyAddress = deployHeavyContractTxReceipt.contractAddress!; + + const txHash = await alice.sendTransaction({ + to: heavyAddress, + gas: 1_000_000n, + data: encodeFunctionData({ + abi: heavy.abi, + functionName: "set_and_loop", + args: [10n], + }), + }); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + await customRpcRequest(node.meta.rpcUrlHttp, "debug_traceTransaction", [ + txHash, + ]).then( + () => { + expect.fail("trace should be reverted but it worked instead"); + }, + (error) => { + expect(error.message).to.eq( + "replayed transaction generated too much data. try disabling memory or storage?", + ); + }, + ); + }); + + it("should not trace call that would produce too big responses", async () => { + const [alice, _] = devClients; + + const deployHeavyContractTxHash = await alice.deployContract({ + abi: heavy.abi, + bytecode: heavy.bytecode, + args: [false], + }); + + const deployHeavyContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployHeavyContractTxHash, + timeout: 18_000, + }); + + const heavyAddress = deployHeavyContractTxReceipt.contractAddress!; + + const txHash = await alice.sendTransaction({ + to: heavyAddress, + gasLimit: "0x800000", + value: 0n, + data: encodeFunctionData({ + abi: heavy.abi, + functionName: "heavy_steps", + args: [100n, 1000n], + }), + }); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + await customRpcRequest(node.meta.rpcUrlHttp, "debug_traceTransaction", [ + txHash, + ]).then( + () => { + expect.fail("trace should be reverted but it worked instead"); + }, + (error) => { + expect(error.message).to.eq( + "replayed transaction generated too much data. try disabling memory or storage?", + ); + }, + ); + }); + + it("should format as request (Blockscout, BlockscoutV2)", async () => { + const [alice, _] = devClients; + + const deployCalleeContractTxHash = await alice.deployContract({ + abi: callee.abi, + bytecode: callee.bytecode, + }); + + const deployCalleeContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployCalleeContractTxHash, + timeout: 18_000, + }); + + const calleeAddress = deployCalleeContractTxReceipt.contractAddress!; + + const deployCallerContractTxHash = await alice.deployContract({ + abi: caller.abi, + bytecode: caller.bytecode, + }); + + const deployCallerContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployCallerContractTxHash, + timeout: 18_000, + }); + + const callerAddress = deployCallerContractTxReceipt.contractAddress!; + + const txHash = await alice.sendTransaction({ + to: callerAddress, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "someAction", + args: [calleeAddress, 7n], + }), + }); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [txHash, { tracer: BS_TRACER.body }], + ); + + const entries = response; + expect(entries).to.be.lengthOf(2); + const resCaller = entries[0]; + const resCallee = entries[1]; + expect(resCaller.callType).to.be.equal("call"); + expect(resCallee.type).to.be.equal("call"); + expect(resCallee.from).to.be.equal(resCaller.to); + expect(resCaller.traceAddress).to.be.empty; + expect(resCallee.traceAddress.length).to.be.eq(1); + expect(resCallee.traceAddress[0]).to.be.eq(0); + + const responseV2 = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [txHash, { tracer: BS_TRACER_V2.body }], + ); + + const entriesV2 = responseV2; + expect(entriesV2).to.be.lengthOf(2); + const resCallerV2 = entriesV2[0]; + const resCalleeV2 = entriesV2[1]; + expect(resCallerV2.callType).to.be.equal("call"); + expect(resCalleeV2.type).to.be.equal("call"); + expect(resCalleeV2.from).to.be.equal(resCallerV2.to); + expect(resCallerV2.traceAddress).to.be.empty; + expect(resCalleeV2.traceAddress.length).to.be.eq(1); + expect(resCalleeV2.traceAddress[0]).to.be.eq(0); + }); + + it("should replay over an intermediate state", async () => { + const [alice, _] = devClients; + + const deployIncrementorContractTxHash = await alice.deployContract({ + abi: incrementor.abi, + bytecode: incrementor.bytecode, + }); + + const deployIncrementorContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployIncrementorContractTxHash, + timeout: 18_000, + }); + + const incrementorAddress = + deployIncrementorContractTxReceipt.contractAddress!; + + // In our case, the total number of transactions === the max value of the incrementer. + // If we trace the last transaction of the block, should return the total number of + // transactions we executed (10). + // If we trace the 5th transaction, should return 5 and so on. + // + // So we set 5 different target txs for a single block: the 1st, 3 intermediate, and + // the last. + const totalTxs = 10; + const targets = [1, 2, 5, 8, 10]; + const txsPromises: any[] = []; + + const nonce = await publicClient.getTransactionCount({ + address: alice.account.address, + }); + + // Create 10 transactions in a block. + for (let numTxs = 0; numTxs < totalTxs; numTxs++) { + const txsPromise = alice + .sendTransaction({ + to: incrementorAddress, + data: encodeFunctionData({ + abi: incrementor.abi, + functionName: "incr", + args: [1n], + }), + gas: 100_000n, + nonce: nonce + numTxs, + }) + .then((txHash) => + publicClient.waitForTransactionReceipt({ + hash: txHash, + timeout: 18_000, + }), + ); + + txsPromises.push(txsPromise); + } + + const txsReceipts = await Promise.all(txsPromises); + + // Trace 5 target transactions on it. + for (const target of targets) { + const index = target - 1; + + const intermediateTx = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [txsReceipts[index].transactionHash], + ); + + const evmResult = hexToNumber( + ("0x" + intermediateTx.returnValue) as `0x${string}`, + ); + expect(evmResult).to.equal(target); + } + }); +}); diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionSubcalls.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionSubcalls.ts new file mode 100644 index 000000000..5fa4a0496 --- /dev/null +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionSubcalls.ts @@ -0,0 +1,125 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { RunNodeState, runNode } from "../../lib/node"; +import * as eth from "../../lib/ethViem"; +import { beforeEachWithCleanup } from "../../lib/lifecycle"; +import callForwarder from "../../lib/abis/evmTracing/callForwarder"; +import multiplyBy7 from "../../lib/abis/evmTracing/multiplyBy7"; +import { encodeFunctionData } from "viem"; +import { customRpcRequest } from "../../lib/rpcUtils"; + +describe("`debug_traceTransaction` tests to verify subcalls logic", () => { + let node: RunNodeState; + let publicClient: eth.PublicClientWebSocket; + let devClients: eth.DevClientsWebSocket; + beforeEachWithCleanup(async (cleanup) => { + node = runNode( + { + args: ["--tracing-mode=debug", "--dev", "--tmp"], + }, + cleanup.push, + ); + + await node.waitForBoot; + + publicClient = eth.publicClientFromNodeWebSocket(node, cleanup.push); + devClients = eth.devClientsFromNodeWebSocket(node, cleanup.push); + }, 60 * 1000); + + let callForwarderAddress: `0x${string}`; + let multiplyBy7Address: `0x${string}`; + + beforeEach(async () => { + const [alice, _] = devClients; + + const deployCallForwarderContractTxHash = await alice.deployContract({ + abi: callForwarder.abi, + bytecode: callForwarder.bytecode, + }); + const deployCallForwarderContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployCallForwarderContractTxHash, + timeout: 18_000, + }); + callForwarderAddress = + deployCallForwarderContractTxReceipt.contractAddress!; + + const deployMultiplyBy7ContractTxHash = await alice.deployContract({ + abi: multiplyBy7.abi, + bytecode: multiplyBy7.bytecode, + }); + const deployMultiplyBy7ContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployMultiplyBy7ContractTxHash, + timeout: 18_000, + }); + multiplyBy7Address = deployMultiplyBy7ContractTxReceipt.contractAddress!; + }); + + it("should correctly trace subcall", async () => { + const [alice, _] = devClients; + + const txHash = await alice.sendTransaction({ + to: callForwarderAddress, + data: encodeFunctionData({ + abi: callForwarder.abi, + functionName: "call", + args: [ + multiplyBy7Address, + encodeFunctionData({ + abi: multiplyBy7.abi, + functionName: "multiply", + args: [42n], + }), + ], + }), + }); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [txHash, { tracer: "callTracer" }], + ); + + expect(response.from).to.be.eq(alice.account.address.toLowerCase()); + expect(response.to).to.be.eq(callForwarderAddress.toLowerCase()); + expect(response.calls.length).to.be.eq(1); + expect(response.calls[0].from).to.be.eq(callForwarderAddress.toLowerCase()); + expect(response.calls[0].to).to.be.eq(multiplyBy7Address.toLowerCase()); + expect(response.calls[0].type).to.be.eq("CALL"); + }); + + it("should correctly trace delegatecall subcall", async () => { + const [alice, _] = devClients; + + const txHash = await alice.sendTransaction({ + to: callForwarderAddress, + data: encodeFunctionData({ + abi: callForwarder.abi, + functionName: "delegateCall", + args: [ + multiplyBy7Address, + encodeFunctionData({ + abi: multiplyBy7.abi, + functionName: "multiply", + args: [42n], + }), + ], + }), + }); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [txHash, { tracer: "callTracer" }], + ); + + expect(response.from).to.be.eq(alice.account.address.toLowerCase()); + expect(response.to).to.be.eq(callForwarderAddress.toLowerCase()); + expect(response.calls.length).to.be.eq(1); + expect(response.calls[0].from).to.be.eq(callForwarderAddress.toLowerCase()); + expect(response.calls[0].to).to.be.eq(multiplyBy7Address.toLowerCase()); + expect(response.calls[0].type).to.be.eq("DELEGATECALL"); + }); +}); diff --git a/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGeneral.ts b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGeneral.ts new file mode 100644 index 000000000..f459d6663 --- /dev/null +++ b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGeneral.ts @@ -0,0 +1,164 @@ +import { describe, expect, it } from "vitest"; +import { RunNodeState, runNode } from "../../lib/node"; +import * as eth from "../../lib/ethViem"; +import { beforeEachWithCleanup } from "../../lib/lifecycle"; +import { customRpcRequest } from "../../lib/rpcUtils"; + +describe("`trace_filter` tests to verify general logic", () => { + let node: RunNodeState; + let publicClient: eth.PublicClientWebSocket; + let devClients: eth.DevClientsWebSocket; + beforeEachWithCleanup(async (cleanup) => { + node = runNode( + { + args: ["--tracing-mode=trace", "--dev", "--tmp"], + }, + cleanup.push, + ); + + await node.waitForBoot; + + publicClient = eth.publicClientFromNodeWebSocket(node, cleanup.push); + devClients = eth.devClientsFromNodeWebSocket(node, cleanup.push); + }, 60 * 1000); + + it("should support tracing range of blocks", async () => { + const [alice, bob] = devClients; + + const firstTxHash = await alice.sendTransaction({ + to: bob.account.address, + value: 1_000_000n, + }); + const firstTxReceipt = await publicClient.waitForTransactionReceipt({ + hash: firstTxHash, + }); + + const firstBlockNumber = firstTxReceipt.blockNumber; + const firstBlockNumberHex = firstTxReceipt.blockNumber.toString(16); + + const secondTxHash = await alice.sendTransaction({ + to: bob.account.address, + value: 1_000_000n, + }); + const secondTxReceipt = await publicClient.waitForTransactionReceipt({ + hash: secondTxHash, + }); + + const secondBlockNumber = secondTxReceipt.blockNumber; + const secondBlockNumberHex = secondTxReceipt.blockNumber.toString(16); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "trace_filter", + [ + { + fromBlock: firstBlockNumberHex, + toBlock: secondBlockNumberHex, + }, + ], + ); + + expect(BigInt(response.length)).to.equal( + secondBlockNumber - firstBlockNumber + 1n, + ); + + for (const index in response.length) { + const blockNumber = response[index].blockNumber; + expect(blockNumber).to.equal(firstBlockNumber + index); + if ( + blockNumber == firstBlockNumber || + blockNumber == secondBlockNumberHex + ) { + expect(response[index].transactionPosition).to.equal(0); + } + } + }); + + it("should support filtering trace per fromAddress/toAddress", async () => { + const [alice, bob] = devClients; + + const txHash = await alice.sendTransaction({ + to: bob.account.address, + value: 1_000_000n, + }); + const txReceipt = await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + const blockNumberHex = txReceipt.blockNumber.toString(16); + + const responsePerFrom = await customRpcRequest( + node.meta.rpcUrlHttp, + "trace_filter", + [ + { + fromBlock: blockNumberHex, + toBlock: blockNumberHex, + fromAddress: [alice.account.address], + }, + ], + ); + + expect(responsePerFrom.length).to.equal(1); + expect(txHash).to.equal(responsePerFrom[0].transactionHash); + + const responsePerTo = await customRpcRequest( + node.meta.rpcUrlHttp, + "trace_filter", + [ + { + fromBlock: blockNumberHex, + toBlock: blockNumberHex, + toAddress: [bob.account.address], + }, + ], + ); + + expect(responsePerTo.length).to.equal(1); + expect(txHash).to.equal(responsePerTo[0].transactionHash); + }); + + it("should check default max 500 traces request", async () => { + const [alice, bob] = devClients; + + const txHash = await alice.sendTransaction({ + to: bob.account.address, + value: 1_000_000n, + }); + const txReceipt = await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + const blockNumberHex = txReceipt.blockNumber.toString(16); + + const responseSuccess = await customRpcRequest( + node.meta.rpcUrlHttp, + "trace_filter", + [ + { + fromBlock: blockNumberHex, + toBlock: blockNumberHex, + count: 500, + }, + ], + ); + + expect(responseSuccess.length).to.equal(1); + expect(txHash).to.equal(responseSuccess[0].transactionHash); + + await customRpcRequest(node.meta.rpcUrlHttp, "trace_filter", [ + { + fromBlock: blockNumberHex, + toBlock: blockNumberHex, + count: 501, + }, + ]).then( + () => { + expect.fail("should not succeed"); + }, + (error) => { + expect(error.message).to.eq( + "count (501) can't be greater than maximum (500)", + ); + }, + ); + }); +}); diff --git a/utils/e2e-tests/ts/tests/evm-tracing/traceFilterHeavy.ts b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterHeavy.ts new file mode 100644 index 000000000..7fe88ed3f --- /dev/null +++ b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterHeavy.ts @@ -0,0 +1,179 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { RunNodeState, runNode } from "../../lib/node"; +import * as eth from "../../lib/ethViem"; +import { beforeEachWithCleanup } from "../../lib/lifecycle"; +import heavy from "../../lib/abis/evmTracing/heavy"; +import { customRpcRequest } from "../../lib/rpcUtils"; +import { encodeFunctionData, hexToNumber } from "viem"; + +describe("`trace_filter` tests to verify some heavy logic in contracts", () => { + let node: RunNodeState; + let publicClient: eth.PublicClientWebSocket; + let devClients: eth.DevClientsWebSocket; + beforeEachWithCleanup(async (cleanup) => { + node = runNode( + { + args: ["--tracing-mode=trace", "--dev", "--tmp"], + }, + cleanup.push, + ); + + await node.waitForBoot; + + publicClient = eth.publicClientFromNodeWebSocket(node, cleanup.push); + devClients = eth.devClientsFromNodeWebSocket(node, cleanup.push); + }, 60 * 1000); + + let heavyContracts: { + address: `0x${string}`; + blockNumberHex: `0x${string}`; + txHash: `0x${string}`; + }[] = []; + + beforeEach(async () => { + const [alice, _] = devClients; + + for (let index = 0; index < 4; index++) { + let shouldRevert = false; + let gas; + + if (index == 3) { + shouldRevert = true; + gas = 150_000n; // should be increased for revert logic. + } + + const deployHeavyContractTxHash = await alice.deployContract({ + abi: heavy.abi, + bytecode: heavy.bytecode, + gas: gas, + args: [shouldRevert], + }); + + const deployHeavyContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployHeavyContractTxHash, + timeout: 18_000, + }); + + heavyContracts.push({ + address: deployHeavyContractTxReceipt.contractAddress!, + blockNumberHex: deployHeavyContractTxReceipt.blockNumber.toString( + 16, + ) as `0x${string}`, + txHash: deployHeavyContractTxReceipt.transactionHash, + }); + } + }); + + it("should be able to replay deployed contract", async () => { + const [alice, _] = devClients; + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "trace_filter", + [ + { + fromBlock: heavyContracts[0]!.blockNumberHex, + toBlock: heavyContracts[0]!.blockNumberHex, + }, + ], + ); + + expect(response.length).to.equal(1); + + expect(response[0].action).to.include({ + creationMethod: "create", + from: alice.account.address.toLocaleLowerCase(), + gas: "0x66fa5", + value: "0x0", + }); + + expect(response[0]).to.include({ + blockNumber: hexToNumber(heavyContracts[0]!.blockNumberHex), + subtraces: 0, + transactionHash: heavyContracts[0]!.txHash, + transactionPosition: 0, + type: "create", + }); + }); + + it("should be able to replay reverted contract", async () => { + const [alice, _] = devClients; + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "trace_filter", + [ + { + fromBlock: heavyContracts[3]!.blockNumberHex, + toBlock: heavyContracts[3]!.blockNumberHex, + }, + ], + ); + + expect(response.length).to.equal(1); + expect(response[0].action.creationMethod).to.equal("create"); + expect(response[0].action.from).to.equal( + alice.account.address.toLocaleLowerCase(), + ); + expect(response[0].action.gas).to.equal("0xf576"); + expect(response[0].action.init).to.be.a("string"); + expect(response[0].action.value).to.equal("0x0"); + expect(response[0].blockHash).to.be.a("string"); + expect(response[0].blockNumber).to.equal( + hexToNumber(heavyContracts[3]!.blockNumberHex), + ); + expect(response[0].result).to.equal(undefined); + expect(response[0].error).to.equal("Reverted"); + expect(response[0].subtraces).to.equal(0); + expect(response[0].traceAddress.length).to.equal(0); + expect(response[0].transactionHash).to.equal(heavyContracts[3]!.txHash); + expect(response[0].transactionPosition).to.equal(0); + expect(response[0].type).to.equal("create"); + }); + + it("should be able to trace sub-call with reverts", async () => { + const [alice, _] = devClients; + + const txHash = await alice.sendTransaction({ + to: heavyContracts[0]!.address, + data: encodeFunctionData({ + abi: heavy.abi, + functionName: "subcalls", + args: [heavyContracts[1]!.address, heavyContracts[2]!.address], + }), + gas: 1_000_000n, + }); + const txReceipt = await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + const blockNumberHex = txReceipt.blockNumber.toString(16); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "trace_filter", + [ + { + fromBlock: blockNumberHex, + toBlock: blockNumberHex, + }, + ], + ); + + expect(response.length).to.equal(7); + expect(response[0].subtraces).to.equal(2); + expect(response[0].traceAddress).to.deep.equal([]); + expect(response[1].subtraces).to.equal(2); + expect(response[1].traceAddress).to.deep.equal([0]); + expect(response[2].subtraces).to.equal(0); + expect(response[2].traceAddress).to.deep.equal([0, 0]); + expect(response[3].subtraces).to.equal(0); + expect(response[3].traceAddress).to.deep.equal([0, 1]); + expect(response[4].subtraces).to.equal(2); + expect(response[4].traceAddress).to.deep.equal([1]); + expect(response[5].subtraces).to.equal(0); + expect(response[5].traceAddress).to.deep.equal([1, 0]); + expect(response[6].subtraces).to.equal(0); + expect(response[6].traceAddress).to.deep.equal([1, 1]); + }); +}); diff --git a/utils/e2e-tests/ts/tests/evm-tracing/traceFilterUsedGas.ts b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterUsedGas.ts new file mode 100644 index 000000000..b236f3966 --- /dev/null +++ b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterUsedGas.ts @@ -0,0 +1,141 @@ +// Constants related to used gas can be different on various EVM versions. + +import { beforeEach, describe, expect, it } from "vitest"; +import { RunNodeState, runNode } from "../../lib/node"; +import * as eth from "../../lib/ethViem"; +import { beforeEachWithCleanup } from "../../lib/lifecycle"; +import looper from "../../lib/abis/evmTracing/looper"; +import { customRpcRequest } from "../../lib/rpcUtils"; +import { encodeFunctionData } from "viem"; + +describe("`trace_filter` tests to verify used gas logic", () => { + let node: RunNodeState; + let publicClient: eth.PublicClientWebSocket; + let devClients: eth.DevClientsWebSocket; + beforeEachWithCleanup(async (cleanup) => { + node = runNode( + { + args: ["--tracing-mode=trace", "--dev", "--tmp"], + }, + cleanup.push, + ); + + await node.waitForBoot; + + publicClient = eth.publicClientFromNodeWebSocket(node, cleanup.push); + devClients = eth.devClientsFromNodeWebSocket(node, cleanup.push); + }, 60 * 1000); + + let looperAddress: `0x${string}`; + + beforeEach(async () => { + const [alice, _] = devClients; + + const deployLooperContractTxHash = await alice.deployContract({ + abi: looper.abi, + bytecode: looper.bytecode, + }); + const deployLooperContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployLooperContractTxHash, + timeout: 18_000, + }); + looperAddress = deployLooperContractTxReceipt.contractAddress!; + }); + + it("should return 21653 `gasUsed` for 0 loop", async () => { + const [alice, _] = devClients; + + const txHash = await alice.sendTransaction({ + to: looperAddress, + data: encodeFunctionData({ + abi: looper.abi, + functionName: "incrementalLoop", + args: [0n], + }), + }); + const txReceipt = await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + const blockNumberHex = txReceipt.blockNumber.toString(16); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "trace_filter", + [ + { + fromBlock: blockNumberHex, + toBlock: blockNumberHex, + }, + ], + ); + + expect(response[0].result).to.not.be.undefined; + expect(response[0].result.error).to.not.exist; + expect(response[0].result.gasUsed).to.equal("0x5495"); + }); + + it("should return 106265 `gasUsed` for 100 loops", async () => { + const [alice, _] = devClients; + + const txHash = await alice.sendTransaction({ + to: looperAddress, + data: encodeFunctionData({ + abi: looper.abi, + functionName: "incrementalLoop", + args: [100n], + }), + }); + const txReceipt = await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + const blockNumberHex = txReceipt.blockNumber.toString(16); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "trace_filter", + [ + { + fromBlock: blockNumberHex, + toBlock: blockNumberHex, + }, + ], + ); + + expect(response[0].result).to.not.be.undefined; + expect(response[0].result.error).to.not.exist; + expect(response[0].result.gasUsed).to.equal("0x19f19"); + }); + + it("should return 670577 `gasUsed` for 1000 loops", async () => { + const [alice, _] = devClients; + + const txHash = await alice.sendTransaction({ + to: looperAddress, + data: encodeFunctionData({ + abi: looper.abi, + functionName: "incrementalLoop", + args: [1000n], + }), + }); + const txReceipt = await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + const blockNumberHex = txReceipt.blockNumber.toString(16); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "trace_filter", + [ + { + fromBlock: blockNumberHex, + toBlock: blockNumberHex, + }, + ], + ); + + expect(response[0].result).to.not.be.undefined; + expect(response[0].result.error).to.not.exist; + expect(response[0].result.gasUsed).to.equal("0xa3b71"); + }); +}); diff --git a/utils/e2e-tests/ts/vitest.config.ts b/utils/e2e-tests/ts/vitest.config.ts index 22439d444..8329bdf9b 100644 --- a/utils/e2e-tests/ts/vitest.config.ts +++ b/utils/e2e-tests/ts/vitest.config.ts @@ -19,6 +19,7 @@ export default defineConfig({ }, }, testTimeout: 30_000, + hookTimeout: 30_000, }, // Path relative to this config file or absolute path. // Default value: "./node_modules/.vite/"