diff --git a/crates/edr_napi/index.d.ts b/crates/edr_napi/index.d.ts index 30fe695b3..9b769076a 100644 --- a/crates/edr_napi/index.d.ts +++ b/crates/edr_napi/index.d.ts @@ -458,6 +458,7 @@ export class Exit { getReason(): string } export type VMTracer = VmTracer +/** N-API bindings for the Rust port of `VMTracer` from Hardhat. */ export class VmTracer { constructor() /** Observes a trace, collecting information about the execution of the EVM. */ diff --git a/crates/edr_napi/src/trace/message_trace.rs b/crates/edr_napi/src/trace/message_trace.rs index f6fedcee0..87353b48a 100644 --- a/crates/edr_napi/src/trace/message_trace.rs +++ b/crates/edr_napi/src/trace/message_trace.rs @@ -1,10 +1,9 @@ -//! Bridging type for the `MessageTrace` interface in Hardhat. +//! Bridging type for the existing `MessageTrace` interface in Hardhat. use napi::{ bindgen_prelude::{BigInt, Either3, Either4, Uint8Array, Undefined}, Either, }; - use napi_derive::napi; use serde_json::Value; @@ -70,7 +69,32 @@ pub struct CallMessageTrace { pub code_address: Uint8Array, } -/// Converts the Rust representation of a `MessageTrace` to the N-API representation. +/// Converts [`edr_solidity::message_trace::MessageTraceStep`] to the N-API +/// representation. +/// +/// # Panics +/// This function will panic if the value is mutably borrowed. +pub fn message_trace_step_to_napi( + value: edr_solidity::message_trace::MessageTraceStep, +) -> Either4 { + match value { + edr_solidity::message_trace::MessageTraceStep::Evm(step) => { + Either4::A(EvmStep { pc: step.pc as u32 }) + } + edr_solidity::message_trace::MessageTraceStep::Message(msg) => { + // Immediately drop the borrow lock as it may be + let owned = msg.borrow().clone(); + match message_trace_to_napi(owned) { + Either3::A(precompile) => Either4::B(precompile), + Either3::B(create) => Either4::C(create), + Either3::C(call) => Either4::D(call), + } + } + } +} + +/// Converts the Rust representation of a `MessageTrace` to the N-API +/// representation. pub fn message_trace_to_napi( value: edr_solidity::message_trace::MessageTrace, ) -> Either3 { @@ -105,18 +129,7 @@ pub fn message_trace_to_napi( .base .steps .into_iter() - .map(|step| match step { - edr_solidity::message_trace::MessageTraceStep::Evm(step) => { - Either4::A(EvmStep { pc: step.pc as u32 }) - } - edr_solidity::message_trace::MessageTraceStep::Message(msg) => { - match message_trace_to_napi(msg) { - Either3::A(precompile) => Either4::B(precompile), - Either3::B(create) => Either4::C(create), - Either3::C(call) => Either4::D(call), - } - } - }) + .map(message_trace_step_to_napi) .collect(), // NOTE: We specifically use None as that will be later filled on the JS side bytecode: None, @@ -142,18 +155,7 @@ pub fn message_trace_to_napi( .base .steps .into_iter() - .map(|step| match step { - edr_solidity::message_trace::MessageTraceStep::Evm(step) => { - Either4::A(EvmStep { pc: step.pc as u32 }) - } - edr_solidity::message_trace::MessageTraceStep::Message(msg) => { - match message_trace_to_napi(msg) { - Either3::A(precompile) => Either4::B(precompile), - Either3::B(create) => Either4::C(create), - Either3::C(call) => Either4::D(call), - } - } - }) + .map(message_trace_step_to_napi) .collect(), // NOTE: We specifically use None as that will be later filled on the JS side bytecode: None, diff --git a/crates/edr_napi/src/trace/vm_tracer.rs b/crates/edr_napi/src/trace/vm_tracer.rs index d0d9f4c83..2d84d9452 100644 --- a/crates/edr_napi/src/trace/vm_tracer.rs +++ b/crates/edr_napi/src/trace/vm_tracer.rs @@ -1,52 +1,49 @@ +//! N-API bindings for the Rust port of `VMTracer` from Hardhat. + use napi::{ bindgen_prelude::{Either3, Either4, Undefined}, Either, JsError, }; use napi_derive::napi; -use crate::trace::message_trace::{ - message_trace_to_napi, CallMessageTrace, CreateMessageTrace, PrecompileMessageTrace, +use crate::trace::{ + message_trace::{ + message_trace_to_napi, CallMessageTrace, CreateMessageTrace, PrecompileMessageTrace, + }, + RawTrace, }; -use crate::trace::RawTrace; +/// N-API bindings for the Rust port of `VMTracer` from Hardhat. #[napi] -pub struct VMTracer(edr_solidity::vm_tracer::VMTracer); +pub struct VMTracer(edr_solidity::vm_tracer::VmTracer); #[napi] impl VMTracer { #[napi(constructor)] pub fn new() -> napi::Result { - Ok(Self(edr_solidity::vm_tracer::VMTracer::new())) + Ok(Self(edr_solidity::vm_tracer::VmTracer::new())) } /// Observes a trace, collecting information about the execution of the EVM. #[napi] pub fn observe(&mut self, trace: &RawTrace) { - for msg in &trace.inner.messages { - match msg.clone() { - edr_evm::trace::TraceMessage::Before(before) => { - self.0.add_before_message(before); - } - edr_evm::trace::TraceMessage::Step(step) => { - self.0.add_step(step); - } - edr_evm::trace::TraceMessage::After(after) => { - self.0.add_after_message(after.execution_result); - } - } - } + self.0.observe(&trace.inner); } // Explicitly return undefined as `Option` by default returns `null` in JS - // and the null/undefined checks we use there are strict + // and the null/undefined checks we use in JS are strict #[napi] pub fn get_last_top_level_message_trace( &self, ) -> Either4 { match self .0 - .get_last_top_level_message_trace() - .cloned() + .get_last_top_level_message_trace_ref() + .map(|x| { + x.try_borrow() + .expect("cannot be executed concurrently with `VMTracer::observe`") + .clone() + }) .map(message_trace_to_napi) { Some(Either3::A(precompile)) => Either4::A(precompile), @@ -57,7 +54,7 @@ impl VMTracer { } // Explicitly return undefined as `Option` by default returns `null` in JS - // and the null/undefined checks we use there are strict + // and the null/undefined checks we use in JS are strict #[napi] pub fn get_last_error(&self) -> Either { match self.0.get_last_error() { diff --git a/crates/edr_solidity/src/exit.rs b/crates/edr_solidity/src/exit.rs index c9e52c7dc..fc59e1f81 100644 --- a/crates/edr_solidity/src/exit.rs +++ b/crates/edr_solidity/src/exit.rs @@ -1,21 +1,31 @@ //! Naive rewrite of `hardhat-network/provider/vm/exit.ts` from Hardhat. -//! Used together with `VMTracer`. +//! Used together with `VmTracer`. use std::fmt; use edr_evm::HaltReason; -use edr_evm::SuccessReason; -#[derive(Clone, Copy)] +/// Represents the exit code of the EVM. Naive Rust port of the `ExitCode` from +/// Hardhat. +#[derive(Clone, Copy, Debug)] pub enum ExitCode { + /// Execution was successful. Success = 0, + /// Execution was reverted. Revert, + /// Execution ran out of gas. OutOfGas, + /// Execution encountered an internal error. InternalError, + /// Execution encountered an invalid opcode. InvalidOpcode, + /// Execution encountered a stack underflow. StackUnderflow, + /// Create init code size exceeds limit (runtime). CodesizeExceedsMaximum, + /// Create collision. CreateCollision, + /// Static state change. StaticStateChange, } @@ -39,6 +49,7 @@ impl TryFrom for ExitCode { } impl ExitCode { + /// Whether the exit code represents an error. pub fn is_error(&self) -> bool { !matches!(self, ExitCode::Success) } @@ -60,16 +71,7 @@ impl fmt::Display for ExitCode { } } -impl From for ExitCode { - fn from(reason: SuccessReason) -> Self { - match reason { - SuccessReason::Stop => Self::Success, - SuccessReason::Return => Self::Success, - SuccessReason::SelfDestruct => Self::Success, - } - } -} - +#[allow(clippy::fallible_impl_from)] // naively ported for now impl From for ExitCode { fn from(halt: HaltReason) -> Self { match halt { diff --git a/crates/edr_solidity/src/lib.rs b/crates/edr_solidity/src/lib.rs index 90219c9a3..494e34841 100644 --- a/crates/edr_solidity/src/lib.rs +++ b/crates/edr_solidity/src/lib.rs @@ -10,6 +10,6 @@ pub mod contracts_identifier; mod opcodes; -pub mod message_trace; pub mod exit; +pub mod message_trace; pub mod vm_tracer; diff --git a/crates/edr_solidity/src/message_trace.rs b/crates/edr_solidity/src/message_trace.rs index 69046adaf..bd4c3dc2d 100644 --- a/crates/edr_solidity/src/message_trace.rs +++ b/crates/edr_solidity/src/message_trace.rs @@ -1,15 +1,25 @@ +//! Naive Rust port of the `MessageTrace` et al. from Hardhat. + +use std::{cell::RefCell, rc::Rc}; + use edr_eth::{Address, Bytes, U256}; use crate::{contracts_identifier::Bytecode, exit::ExitCode}; -#[derive(Clone)] +/// Represents a message trace. Naive Rust port of the `MessageTrace` from +/// Hardhat. +#[derive(Clone, Debug)] pub enum MessageTrace { + /// Represents a create message trace. Create(CreateMessageTrace), + /// Represents a call message trace. Call(CallMessageTrace), + /// Represents a precompile message trace. Precompile(PrecompileMessageTrace), } impl MessageTrace { + /// Returns a reference to the the common fields of the message trace. pub fn base(&mut self) -> &mut BaseMessageTrace { match self { MessageTrace::Create(create) => &mut create.base.base, @@ -19,106 +29,87 @@ impl MessageTrace { } } -pub enum EvmMessageTrace { - Create(CreateMessageTrace), - Call(CallMessageTrace), -} - -pub enum DecodedMessageTrace { - Create(DecodedCreateMessageTrace), - Call(DecodedCallMessageTrace), -} - -#[derive(Clone)] +/// Represents the common fields of a message trace. +#[derive(Clone, Debug)] pub struct BaseMessageTrace { + /// Value of the message. pub value: U256, + /// Return data buffer. pub return_data: Bytes, + /// EMV exit code. pub exit: ExitCode, + /// How much gas was used. pub gas_used: u64, + /// Depth of the message. pub depth: usize, } -#[derive(Clone)] +/// Represents a precompile message trace. +#[derive(Clone, Debug)] pub struct PrecompileMessageTrace { + /// Common fields of the message trace. pub base: BaseMessageTrace, + /// Precompile number. pub precompile: u32, + /// Calldata buffer pub calldata: Bytes, } -#[derive(Clone)] +/// Represents a base EVM message trace. +#[derive(Clone, Debug)] pub struct BaseEvmMessageTrace { + /// Common fields of the message trace. pub base: BaseMessageTrace, + /// Code of the contract that is being executed. pub code: Bytes, + /// Children message traces. pub steps: Vec, + /// Resolved metadata of the contract that is being executed. + /// Filled in the JS side by `ContractsIdentifier`. pub bytecode: Option, - // The following is just an optimization: When processing this traces it's useful to know ahead of - // time how many subtraces there are. + // The following is just an optimization: When processing this traces it's useful to know ahead + // of time how many subtraces there are. + /// Number of subtraces. Used to speed up the processing of the traces in + /// JS. pub number_of_subtraces: u32, } -#[derive(Clone)] +/// Represents a create message trace. +#[derive(Clone, Debug)] pub struct CreateMessageTrace { + /// Common fields pub base: BaseEvmMessageTrace, + /// Address of the deployed contract. pub deployed_contract: Option, } -#[derive(Clone)] +/// Represents a call message trace. +#[derive(Clone, Debug)] pub struct CallMessageTrace { + /// Common fields pub base: BaseEvmMessageTrace, + /// Calldata buffer pub calldata: Bytes, + /// Address of the contract that is being executed. pub address: Address, + /// Address of the code that is being executed. pub code_address: Address, } -pub struct DecodedCreateMessageTrace { - pub base: CreateMessageTrace, - pub bytecode: Bytecode, -} - -pub struct DecodedCallMessageTrace { - pub base: CallMessageTrace, - pub bytecode: Bytecode, -} - -pub fn is_precompile_trace(trace: &MessageTrace) -> bool { - matches!(trace, MessageTrace::Precompile(_)) -} - -pub fn is_create_trace(trace: &MessageTrace) -> bool { - matches!(trace, MessageTrace::Create(_)) && !is_call_trace(trace) -} - -pub fn is_decoded_create_trace(trace: &MessageTrace) -> bool { - if let MessageTrace::Create(create_trace) = trace { - create_trace.base.bytecode.is_some() - } else { - false - } -} - -pub fn is_call_trace(trace: &MessageTrace) -> bool { - matches!(trace, MessageTrace::Call(_)) -} - -pub fn is_decoded_call_trace(trace: &MessageTrace) -> bool { - if let MessageTrace::Call(call_trace) = trace { - call_trace.base.bytecode.is_some() - } else { - false - } -} - -pub fn is_evm_step(step: &MessageTraceStep) -> bool { - matches!(step, MessageTraceStep::Evm(_)) -} - -#[derive(Clone)] +/// Represents a message trace step. Naive Rust port of the `MessageTraceStep` +/// from Hardhat. +#[derive(Clone, Debug)] pub enum MessageTraceStep { - Message(MessageTrace), + /// [`MessageTrace`] variant. + // It's both read and written to (updated) by the `VmTracer`. + Message(Rc>), + /// [`EvmStep`] variant. Evm(EvmStep), } -#[derive(Clone)] +/// Minimal EVM step that contains only PC (program counter). +#[derive(Clone, Debug)] pub struct EvmStep { + /// Program counter pub pc: u64, } diff --git a/crates/edr_solidity/src/vm_tracer.rs b/crates/edr_solidity/src/vm_tracer.rs index ff45900d7..7071dd23e 100644 --- a/crates/edr_solidity/src/vm_tracer.rs +++ b/crates/edr_solidity/src/vm_tracer.rs @@ -1,7 +1,13 @@ -//! Naive Rust port of the `VMTracer` fromHardhat +//! Naive Rust port of the `VmTracer` from Hardhat. -use edr_eth::{Bytes, U256}; -use edr_evm::{alloy_primitives::U160, ExecutionResult}; +use std::{cell::RefCell, rc::Rc}; + +use edr_eth::Bytes; +use edr_evm::{ + alloy_primitives::U160, + trace::{BeforeMessage, Step}, + ExecutionResult, +}; use crate::{ exit::ExitCode, @@ -10,40 +16,80 @@ use crate::{ MessageTrace, MessageTraceStep, PrecompileMessageTrace, }, }; -use edr_evm::trace::{BeforeMessage, Step}; -pub struct VMTracer { - pub tracing_steps: Vec, - message_traces: Vec, +type MessageTraceRefCell = Rc>; + +/// Naive Rust port of the `VmTracer` from Hardhat. +pub struct VmTracer { + tracing_steps: Vec, + message_traces: Vec, last_error: Option<&'static str>, max_precompile_number: u64, } -impl VMTracer { - pub fn new() -> Self { - // TODO: temporarily hardcoded to remove the need of using ethereumjs' common and evm here +impl Default for VmTracer { + fn default() -> Self { + // TODO: temporarily hardcoded to remove the need of using ethereumjs' common + // and evm here let max_precompile_number = 10; - VMTracer { + VmTracer { tracing_steps: vec![], message_traces: vec![], last_error: None, max_precompile_number, } } +} + +impl VmTracer { + /// Creates a new [`VmTracer`]. + pub fn new() -> Self { + Self::default() + } + + /// Returns a reference to the last top-level message trace. + /// # Panics + /// This function panics if executed concurrently with [`Self::observe`]. + pub fn get_last_top_level_message_trace(&self) -> Option { + self.message_traces + .last() + .map(|x| RefCell::borrow(x).clone()) + } - pub fn get_last_top_level_message_trace(&self) -> Option<&MessageTrace> { + /// Returns a reference to the last top-level message trace. + /// The reference is only being mutated for the duration of + /// [`Self::observe`] call. + pub fn get_last_top_level_message_trace_ref(&self) -> Option<&MessageTraceRefCell> { self.message_traces.first() } + /// Retrieves the last error that occurred during the tracing process. pub fn get_last_error(&self) -> Option<&'static str> { self.last_error } + /// Observes a trace, collecting information about the execution of the EVM. + pub fn observe(&mut self, trace: &edr_evm::trace::Trace) { + for msg in &trace.messages { + match msg.clone() { + edr_evm::trace::TraceMessage::Before(before) => { + self.add_before_message(before); + } + edr_evm::trace::TraceMessage::Step(step) => { + self.add_step(step); + } + edr_evm::trace::TraceMessage::After(after) => { + self.add_after_message(after.execution_result); + } + } + } + } + fn should_keep_tracing(&self) -> bool { self.last_error.is_none() } - pub fn add_before_message(&mut self, message: BeforeMessage) { + fn add_before_message(&mut self, message: BeforeMessage) { if !self.should_keep_tracing() { return; } @@ -77,7 +123,8 @@ impl VMTracer { trace = MessageTrace::Create(create_trace); } else { - // TODO: Make this nicer and make sure the U160 logic/precompile comparison logic works + // TODO: Make this nicer and make sure the U160 logic/precompile comparison + // logic works let to = message.to.unwrap(); let to_as_bigint = U160::from_be_bytes(**to); @@ -137,75 +184,78 @@ impl VMTracer { } } - if let Some(parent_trace) = self.message_traces.last_mut() { - match parent_trace { + // We need to share it so that adding steps when processing via stack + // also updates the inner elements + let trace = Rc::new(RefCell::new(trace)); + + if let Some(parent_ref) = self.message_traces.last_mut() { + let mut parent_trace = parent_ref.borrow_mut(); + let parent_trace = match &mut *parent_trace { MessageTrace::Precompile(_) => { self.last_error = Some("This should not happen: message execution started while a precompile was executing"); return; } - MessageTrace::Create(parent_trace) => { - parent_trace - .base - .steps - .push(MessageTraceStep::Message(trace.clone())); - parent_trace.base.number_of_subtraces += 1; - } - MessageTrace::Call(parent_trace) => { - parent_trace - .base - .steps - .push(MessageTraceStep::Message(trace.clone())); - parent_trace.base.number_of_subtraces += 1; - } - } + MessageTrace::Create(create) => &mut create.base, + MessageTrace::Call(call) => &mut call.base, + }; + + parent_trace + .steps + .push(MessageTraceStep::Message(Rc::clone(&trace))); + parent_trace.number_of_subtraces += 1; } self.message_traces.push(trace); } - pub fn add_step(&mut self, step: Step) { + fn add_step(&mut self, step: Step) { if !self.should_keep_tracing() { return; } - if let Some(trace) = self.message_traces.last_mut() { - let steps = match trace { + if let Some(parent_ref) = self.message_traces.last_mut() { + let mut parent_trace = parent_ref.borrow_mut(); + let parent_trace = match &mut *parent_trace { MessageTrace::Precompile(_) => { self.last_error = Some( "This should not happen: step event fired while a precompile was executing", ); return; } - MessageTrace::Create(parent_trace) => &mut parent_trace.base.steps, - MessageTrace::Call(parent_trace) => &mut parent_trace.base.steps, + MessageTrace::Create(create) => &mut create.base, + MessageTrace::Call(call) => &mut call.base, }; - steps.push(MessageTraceStep::Evm(EvmStep { pc: step.pc })); + parent_trace + .steps + .push(MessageTraceStep::Evm(EvmStep { pc: step.pc })); } self.tracing_steps.push(step); } - pub fn add_after_message(&mut self, result: ExecutionResult) { + fn add_after_message(&mut self, result: ExecutionResult) { if !self.should_keep_tracing() { return; } if let Some(trace) = self.message_traces.last_mut() { + let mut trace = trace.borrow_mut(); + trace.base().gas_used = result.gas_used(); match result { - ExecutionResult::Success { reason, output, .. } => { - trace.base().exit = ExitCode::from(reason); + ExecutionResult::Success { output, .. } => { + trace.base().exit = ExitCode::Success; trace.base().return_data = output.data().clone(); - if let MessageTrace::Create(trace) = trace { + if let MessageTrace::Create(trace) = &mut *trace { let address = output .address() // The original code asserted this directly .expect("address should be defined in create trace"); - trace.deployed_contract = Some(Bytes::copy_from_slice(&address.0 .0)); + trace.deployed_contract = Some(address.as_slice().to_vec().into()); } } ExecutionResult::Halt { reason, .. } => { @@ -217,10 +267,10 @@ impl VMTracer { trace.base().return_data = output; } } + } - if self.message_traces.len() > 1 { - self.message_traces.pop(); - } + if self.message_traces.len() > 1 { + self.message_traces.pop(); } } }