diff --git a/Cargo.lock b/Cargo.lock index 86cc8e044..fb7bcfe81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2310,6 +2310,52 @@ 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-events" +version = "0.1.0" +dependencies = [ + "environmental", + "evm", + "evm-gasometer", + "evm-runtime", + "parity-scale-codec", + "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 = "exit-future" version = "0.2.0" @@ -3652,6 +3698,7 @@ dependencies = [ "clap", "crypto-utils", "crypto-utils-evm", + "evm-tracing-host-api", "fc-cli", "fc-consensus", "fc-db", @@ -3776,6 +3823,8 @@ dependencies = [ "eip712-token-claim", "ethereum", "evm-nonces-recovery", + "evm-tracer", + "evm-tracing-api", "fp-evm", "fp-rpc", "fp-self-contained", diff --git a/Cargo.toml b/Cargo.toml index 4bd25a221..cd3bde127 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 } @@ -132,6 +135,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 } 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-events/Cargo.toml b/crates/evm-tracing-events/Cargo.toml new file mode 100644 index 000000000..e049bc7eb --- /dev/null +++ b/crates/evm-tracing-events/Cargo.toml @@ -0,0 +1,31 @@ +[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 } +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..fc8218000 --- /dev/null +++ b/crates/evm-tracing-events/src/lib.rs @@ -0,0 +1,101 @@ +//! 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; +pub mod runtime; + +pub use gasometer::GasometerEvent; +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/runtime.rs b/crates/evm-tracing-events/src/runtime.rs new file mode 100644 index 000000000..e8e3358a1 --- /dev/null +++ b/crates/evm-tracing-events/src/runtime.rs @@ -0,0 +1,347 @@ +//! EVM runtime events definitions. + +extern crate alloc; + +use codec::{Decode, Encode}; +#[cfg(feature = "evm-tracing")] +use evm::Opcode; +pub use evm::{ExitError, ExitReason, ExitSucceed}; +use sp_core::{sp_std::vec::Vec, H160, H256, U256}; + +use crate::Context; +#[cfg(feature = "evm-tracing")] +use crate::StepEventFilter; + +/// 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 = Vec; + +/// EVM runtime event. +#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)] +pub enum RuntimeEvent { + /// Step. + Step { + /// Context. + context: Context, + /// Opcode. + opcode: Vec, + /// 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: opcodes_string(opcode), + 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(opcodes_string(*t))), + }, + }, + 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, + }, + } + } +} + +/// Converts an `Opcode` into its name, stored in a `Vec`. +#[cfg(feature = "evm-tracing")] +pub fn opcodes_string(opcode: Opcode) -> Vec { + match opcode_str(opcode) { + Some(s) => s.into(), + None => alloc::format!("Unknown({})", opcode.0).into(), + } +} + +/// Converts an `Opcode` into its name. +#[cfg(feature = "evm-tracing")] +pub fn opcode_str(opcode: Opcode) -> Option<&'static str> { + Some(match opcode.0 { + 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/humanode-peer/Cargo.toml b/crates/humanode-peer/Cargo.toml index 1538df368..49a37eb88 100644 --- a/crates/humanode-peer/Cargo.toml +++ b/crates/humanode-peer/Cargo.toml @@ -16,6 +16,7 @@ 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" } 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/service/mod.rs b/crates/humanode-peer/src/service/mod.rs index b9644c5e7..03ca04dbc 100644 --- a/crates/humanode-peer/src/service/mod.rs +++ b/crates/humanode-peer/src/service/mod.rs @@ -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) diff --git a/crates/humanode-runtime/Cargo.toml b/crates/humanode-runtime/Cargo.toml index 7c88043f5..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,7 +114,7 @@ sp-keystore = { workspace = true } [features] default = ["std"] -evm-tracing = [] +evm-tracing = ["evm-tracer"] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", @@ -155,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 53d17af25..cc57d498e 100644 --- a/utils/checks/snapshots/features.yaml +++ b/utils/checks/snapshots/features.yaml @@ -805,6 +805,7 @@ - scale-info - serde - std + - tracing - with-codec - with-serde - name: evm-core 0.39.0 @@ -819,6 +820,7 @@ features: - environmental - std + - tracing - name: evm-nonces-recovery 0.1.0 features: - default @@ -827,6 +829,24 @@ 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-events 0.1.0 + features: + - default + - evm-tracing + - std +- name: evm-tracing-host-api 0.1.0 + features: + - default + - std - name: exit-future 0.2.0 features: [] - name: expander 1.0.0 @@ -1264,6 +1284,7 @@ - name: humanode-runtime 0.1.0 features: - default + - evm-tracer - frame-try-runtime - serde - std