From 04068a18a98eeb54f4be2dd98bf667da05990932 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 1 Apr 2025 11:23:52 +0200 Subject: [PATCH 1/9] feat: a couple utility inspectors --- src/inspectors/mod.rs | 5 + src/inspectors/spanning.rs | 205 +++++++++++++++++++++++++++++++++++++ src/inspectors/timeout.rs | 146 ++++++++++++++++++++++++++ src/lib.rs | 3 + 4 files changed, 359 insertions(+) create mode 100644 src/inspectors/mod.rs create mode 100644 src/inspectors/spanning.rs create mode 100644 src/inspectors/timeout.rs diff --git a/src/inspectors/mod.rs b/src/inspectors/mod.rs new file mode 100644 index 0000000..fc6e5d0 --- /dev/null +++ b/src/inspectors/mod.rs @@ -0,0 +1,5 @@ +mod timeout; +pub use timeout::TimeLimit; + +mod spanning; +pub use spanning::SpanningInspector; diff --git a/src/inspectors/spanning.rs b/src/inspectors/spanning.rs new file mode 100644 index 0000000..1f81731 --- /dev/null +++ b/src/inspectors/spanning.rs @@ -0,0 +1,205 @@ +use alloy::hex; +use revm::{ + interpreter::{ + CallInputs, CallOutcome, CreateInputs, CreateOutcome, EOFCreateInputs, Interpreter, + InterpreterTypes, + }, + Inspector, +}; +use tracing::{ + debug_span, error_span, info_span, span::EnteredSpan, trace_span, warn_span, Level, Span, +}; + +macro_rules! runtime_level_span { + ($level:expr, $($args:tt)*) => {{ + match $level { + Level::TRACE => trace_span!($($args)*), + Level::DEBUG => debug_span!($($args)*), + Level::INFO => info_span!($($args)*), + Level::WARN => warn_span!($($args)*), + Level::ERROR => error_span!($($args)*), + } + }}; +} + +/// Inspector that creates spans for each call and create operation. +/// +/// This inspector is useful for tracing the execution of the EVM and +/// contextualizing information from other tracing inspectors. It uses +/// [`tracing`] to create spans for each call frame, at a specfied [`Level`], +/// and adds interpreter information to the span. +/// +/// Spans are created at the beginning of each call and create operation, +/// and closed at the end of the operation. The spans are named +/// according to the operation type (call or create) and include +/// the call depth, gas remaining, and other relevant information. +/// +/// # Note on functionality +/// +/// We assume that the EVM execution is synchronous, so [`EnteredSpan`]s will +/// not be held across await points. This means we can simply keep a +/// `Vec` to which push and from which we pop. The first span in +/// the vec will always be our root span, and 1 span will be held for each +/// active callframe. +/// +pub struct SpanningInspector { + active: Vec, + level: Level, +} + +impl core::fmt::Debug for SpanningInspector { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SpanningInspector") + .field("active", &self.active.len()) + .field("level", &self.level) + .finish() + } +} + +impl SpanningInspector { + /// Create a new `SpanningInspector` with the given tracing level. + /// Spans will be created at this level. + pub fn new(level: Level) -> Self { + Self { active: Vec::new(), level } + } + + /// Create a root span + fn root_span(&mut self, _interp: &mut Interpreter, _context: &mut Ctx) -> Span + where + Int: InterpreterTypes, + { + runtime_level_span!( + self.level, + parent: None, // this is the root span :) + "evm_execution", + ) + } + + /// Init the inspector by setting the root span. This should be called only in [`Inspector::initialize_interp`]. + fn init(&mut self, interp: &mut Interpreter, context: &mut Ctx) + where + Int: InterpreterTypes, + { + // just in case + self.active.clear(); + let span = self.root_span(interp, context).entered(); + self.active.push(span); + } + + /// Exit the active span. + fn exit_span(&mut self) { + self.active.pop(); + // If there's only 1 left, it's the root span for the trace, and all + // callframes are closed. We are now done with execution. + if self.active.len() == 1 { + self.active.pop(); + } + } + + /// Create a span for a `CALL`-family opcode. + fn span_call(&self, _context: &Ctx, inputs: &CallInputs) -> Span { + let mut selector = inputs.input.clone(); + selector.truncate(4); + runtime_level_span!( + self.level, + "call", + input_len = inputs.input.len(), + selector = hex::encode(&selector), + gas_limit = inputs.gas_limit, + bytecode_address = %inputs.bytecode_address, + target_addrses = %inputs.target_address, + caller = %inputs.caller, + value = %inputs.value.get(), + scheme = ?inputs.scheme, + ) + } + + /// Create, enter, and store a span for a `CALL`-family opcode. + fn enter_call(&mut self, context: &Ctx, inputs: &CallInputs) { + self.active.push(self.span_call(context, inputs).entered()) + } + + /// Create a span for a `CREATE`-family opcode. + fn span_create(&self, _context: &Ctx, inputs: &CreateInputs) -> Span { + runtime_level_span!( + self.level, + "create", + caller = %inputs.caller, + value = %inputs.value, + gas_limit = inputs.gas_limit, + scheme = ?inputs.scheme, + ) + } + + /// Create, enter, and store a span for a `CREATE`-family opcode. + fn enter_create(&mut self, context: &Ctx, inputs: &CreateInputs) { + self.active.push(self.span_create(context, inputs).entered()) + } + + /// Create a span for an EOF `CREATE`-family opcode. + fn span_eof_create(&self, _context: &Ctx, inputs: &EOFCreateInputs) -> Span { + runtime_level_span!( + self.level, + "eof_create", + caller = %inputs.caller, + value = %inputs.value, + gas_limit = inputs.gas_limit, + kind = ?inputs.kind, + ) + } + + /// Create, enter, and store a span for an EOF `CREATE`-family opcode. + fn enter_eof_create(&mut self, context: &Ctx, inputs: &EOFCreateInputs) { + self.active.push(self.span_eof_create(context, inputs).entered()) + } +} + +impl Inspector for SpanningInspector +where + Int: InterpreterTypes, +{ + fn initialize_interp(&mut self, interp: &mut Interpreter, context: &mut Ctx) { + self.init(interp, context); + } + + fn call(&mut self, context: &mut Ctx, inputs: &mut CallInputs) -> Option { + self.enter_call(context, inputs); + None + } + + fn call_end(&mut self, _context: &mut Ctx, _inputs: &CallInputs, _outcome: &mut CallOutcome) { + self.exit_span(); + } + + fn create(&mut self, context: &mut Ctx, inputs: &mut CreateInputs) -> Option { + self.enter_create(context, inputs); + None + } + + fn create_end( + &mut self, + _context: &mut Ctx, + _inputs: &CreateInputs, + _outcome: &mut CreateOutcome, + ) { + self.exit_span(); + } + + fn eofcreate( + &mut self, + context: &mut Ctx, + inputs: &mut EOFCreateInputs, + ) -> Option { + self.enter_eof_create(context, inputs); + None + } + + fn eofcreate_end( + &mut self, + _context: &mut Ctx, + _inputs: &EOFCreateInputs, + _outcome: &mut CreateOutcome, + ) { + self.exit_span(); + } +} diff --git a/src/inspectors/timeout.rs b/src/inspectors/timeout.rs new file mode 100644 index 0000000..5388b72 --- /dev/null +++ b/src/inspectors/timeout.rs @@ -0,0 +1,146 @@ +use revm::{ + interpreter::{ + CallInputs, CallOutcome, CreateInputs, CreateOutcome, EOFCreateInputs, Gas, + InstructionResult, Interpreter, InterpreterResult, InterpreterTypes, + }, + primitives::Bytes, + Inspector, +}; +use std::time::{Duration, Instant}; + +const CALL_TIMEOUT: CallOutcome = CallOutcome { + result: InterpreterResult { + result: InstructionResult::FatalExternalError, + output: Bytes::new(), + gas: Gas::new_spent(0), + }, + memory_offset: 0..0, +}; + +const CREATE_TIMEOUT: CreateOutcome = CreateOutcome { + result: InterpreterResult { + result: InstructionResult::FatalExternalError, + output: Bytes::new(), + gas: Gas::new_spent(0), + }, + address: None, +}; + +/// A revm [`Inspector`] that limits wallclock time spent on execution. +/// +/// This inspector will stop execution at the beginning and end of each +/// callframe after the timeout has been reached. Specifically, it will stop +/// when: +/// - any callframe is aborted (e.g. due to execeeding its gas limit) +/// - any of the following instructions are executed: +/// - `CALL` +/// - `CREATE` +/// - `RETURN` +/// - `STOP` +/// - `REVERT` +/// - any invalid opcode +/// +/// When execution is terminated by the timer, it will result in a +/// [`InstructionResult::FatalExternalError`]. +/// +/// ## Usage Note +/// +/// When the timeout is triggered, the inspector will overwrite the +/// [`CallOutcome`] or [`CreateOutcome`]. This means that if other inspectors +/// have already run, they may have inspected data that is no longer valid. +/// +/// To avoid this, run this inspector FIRST on any multi-inspector setup. I.e. +/// in a stack, this should be the OUTERMOST inspector. This ensures that +/// invalid data is not inspected, and that other inspectors do not consume +/// memory or compute resources inspecting data that is guaranteed to be +/// discarded. +#[derive(Debug, Clone, Copy)] +pub struct TimeLimit { + duration: Duration, + execution_start: Instant, +} + +impl TimeLimit { + /// Create a new [`TimeLimit`] inspector. + /// + /// The inspector will stop execution after the given duration has passed. + pub fn new(duration: Duration) -> Self { + Self { duration, execution_start: Instant::now() } + } + + /// Check if the timeout has been reached. + pub fn has_elapsed(&self) -> bool { + self.execution_start.elapsed() >= self.duration + } + + /// Set the execution start time to [`Instant::now`]. This is invoked during [`Inspector::initialize_interp`]. + pub fn reset(&mut self) { + self.execution_start = Instant::now(); + } + + /// Get the amount of time that has elapsed since execution start. + pub fn elapsed(&self) -> Duration { + self.execution_start.elapsed() + } +} + +impl Inspector for TimeLimit { + fn initialize_interp(&mut self, _interp: &mut Interpreter, _context: &mut Ctx) { + self.reset(); + } + + fn call(&mut self, _context: &mut Ctx, _inputs: &mut CallInputs) -> Option { + if self.has_elapsed() { + return Some(CALL_TIMEOUT); + } + + None + } + + fn call_end(&mut self, _context: &mut Ctx, _inputs: &CallInputs, outcome: &mut CallOutcome) { + if self.has_elapsed() { + *outcome = CALL_TIMEOUT; + } + } + + fn create(&mut self, _context: &mut Ctx, _inputs: &mut CreateInputs) -> Option { + if self.has_elapsed() { + return Some(CREATE_TIMEOUT); + } + None + } + + fn create_end( + &mut self, + _context: &mut Ctx, + _inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + if self.has_elapsed() { + *outcome = CREATE_TIMEOUT; + } + } + + fn eofcreate( + &mut self, + _context: &mut Ctx, + _inputs: &mut EOFCreateInputs, + ) -> Option { + if self.has_elapsed() { + return Some(CREATE_TIMEOUT); + } + + None + } + + fn eofcreate_end( + &mut self, + _context: &mut Ctx, + _inputs: &EOFCreateInputs, + outcome: &mut CreateOutcome, + ) { + if self.has_elapsed() { + *outcome = CREATE_TIMEOUT; + } + } +} diff --git a/src/lib.rs b/src/lib.rs index e3c3375..59a9f54 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -400,6 +400,9 @@ pub use fill::{fillers, Block, Cfg, NoopBlock, NoopCfg, Tx}; /// Type aliases for constraining revm context. pub mod helpers; +/// Utility inspectors. +pub mod inspectors; + pub mod journal; mod lifecycle; From 10c2ca4f71c15ced974ff228b85a5c161165bdae Mon Sep 17 00:00:00 2001 From: James Date: Tue, 1 Apr 2025 11:34:09 +0200 Subject: [PATCH 2/9] feat: inspector stack --- src/inspectors/mod.rs | 3 + src/inspectors/stack.rs | 131 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 src/inspectors/stack.rs diff --git a/src/inspectors/mod.rs b/src/inspectors/mod.rs index fc6e5d0..2fbaafd 100644 --- a/src/inspectors/mod.rs +++ b/src/inspectors/mod.rs @@ -3,3 +3,6 @@ pub use timeout::TimeLimit; mod spanning; pub use spanning::SpanningInspector; + +mod stack; +pub use stack::InspectorStack; diff --git a/src/inspectors/stack.rs b/src/inspectors/stack.rs new file mode 100644 index 0000000..6eba72e --- /dev/null +++ b/src/inspectors/stack.rs @@ -0,0 +1,131 @@ +use std::collections::VecDeque; + +use revm::{ + interpreter::{ + CallInputs, CallOutcome, CreateInputs, CreateOutcome, EOFCreateInputs, Interpreter, + InterpreterTypes, + }, + primitives::{Address, Log, U256}, + Inspector, +}; + +/// A stack of [`Inspector`]s. +/// +/// This is a thin wrapper around a [`VecDeque`] of inspectors. +#[derive(Default)] +pub struct InspectorStack { + inspectors: VecDeque>>, +} + +impl core::ops::Deref for InspectorStack { + type Target = VecDeque>>; + + fn deref(&self) -> &Self::Target { + &self.inspectors + } +} + +impl core::ops::DerefMut for InspectorStack { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inspectors + } +} + +impl core::fmt::Debug for InspectorStack { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("InspectorStack").field("inspectors", &self.inspectors.len()).finish() + } +} + +impl InspectorStack { + /// Instantiate a new empty inspector stack. + pub fn new() -> Self { + Self { inspectors: Default::default() } + } + + /// Instantiate a new empty stack with pre-allocated capacity. + pub fn with_capacity(cap: usize) -> Self { + Self { inspectors: VecDeque::with_capacity(cap) } + } +} + +impl Inspector for InspectorStack +where + Int: InterpreterTypes, +{ + fn initialize_interp(&mut self, interp: &mut Interpreter, context: &mut Ctx) { + self.inspectors.iter_mut().for_each(|i| i.initialize_interp(interp, context)); + } + + fn step(&mut self, interp: &mut Interpreter, context: &mut Ctx) { + self.inspectors.iter_mut().for_each(|i| i.step(interp, context)); + } + + fn step_end(&mut self, interp: &mut Interpreter, context: &mut Ctx) { + self.inspectors.iter_mut().for_each(|i| i.step_end(interp, context)); + } + + fn log(&mut self, interp: &mut Interpreter, context: &mut Ctx, log: Log) { + self.inspectors.iter_mut().for_each(|i| i.log(interp, context, log.clone())); + } + + fn call(&mut self, context: &mut Ctx, inputs: &mut CallInputs) -> Option { + for inspector in self.inspectors.iter_mut() { + let outcome = inspector.call(context, inputs); + if outcome.is_some() { + return outcome; + } + } + None + } + + fn call_end(&mut self, context: &mut Ctx, inputs: &CallInputs, outcome: &mut CallOutcome) { + self.inspectors.iter_mut().for_each(|i| i.call_end(context, inputs, outcome)) + } + + fn create(&mut self, context: &mut Ctx, inputs: &mut CreateInputs) -> Option { + for inspector in self.inspectors.iter_mut() { + let outcome = inspector.create(context, inputs); + if outcome.is_some() { + return outcome; + } + } + None + } + + fn create_end( + &mut self, + context: &mut Ctx, + inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + self.inspectors.iter_mut().for_each(|i| i.create_end(context, inputs, outcome)) + } + + fn eofcreate( + &mut self, + context: &mut Ctx, + inputs: &mut EOFCreateInputs, + ) -> Option { + for inspector in self.inspectors.iter_mut() { + let outcome = inspector.eofcreate(context, inputs); + if outcome.is_some() { + return outcome; + } + } + None + } + + fn eofcreate_end( + &mut self, + context: &mut Ctx, + inputs: &EOFCreateInputs, + outcome: &mut CreateOutcome, + ) { + self.inspectors.iter_mut().for_each(|i| i.eofcreate_end(context, inputs, outcome)) + } + + fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { + self.inspectors.iter_mut().for_each(|i| i.selfdestruct(contract, target, value)) + } +} From 83ee026866991182a4f4e2c5fbaef843b2c0c893 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 1 Apr 2025 11:37:38 +0200 Subject: [PATCH 3/9] lint: clippy --- src/inspectors/spanning.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/inspectors/spanning.rs b/src/inspectors/spanning.rs index 1f81731..46586b0 100644 --- a/src/inspectors/spanning.rs +++ b/src/inspectors/spanning.rs @@ -59,7 +59,7 @@ impl core::fmt::Debug for SpanningInspector { impl SpanningInspector { /// Create a new `SpanningInspector` with the given tracing level. /// Spans will be created at this level. - pub fn new(level: Level) -> Self { + pub const fn new(level: Level) -> Self { Self { active: Vec::new(), level } } From a98be990f037f0d20744568fe1591617186a824d Mon Sep 17 00:00:00 2001 From: James Date: Wed, 2 Apr 2025 13:07:17 +0200 Subject: [PATCH 4/9] feat: stack vs layered --- src/inspectors/layer.rs | 103 +++++++++++++++++++++++ src/inspectors/mod.rs | 33 +++++++- src/inspectors/{stack.rs => set.rs} | 35 ++++++-- src/inspectors/spanning.rs | 15 ++++ src/test_utils.rs | 121 +++++++++++++++++++++++++++- 5 files changed, 298 insertions(+), 9 deletions(-) create mode 100644 src/inspectors/layer.rs rename src/inspectors/{stack.rs => set.rs} (77%) diff --git a/src/inspectors/layer.rs b/src/inspectors/layer.rs new file mode 100644 index 0000000..1cb9883 --- /dev/null +++ b/src/inspectors/layer.rs @@ -0,0 +1,103 @@ +use revm::{ + inspector::NoOpInspector, + interpreter::{ + CallInputs, CallOutcome, CreateInputs, CreateOutcome, EOFCreateInputs, Interpreter, + InterpreterTypes, + }, + primitives::{Address, Log, U256}, + Inspector, +}; + +/// A layer in a stack of inspectors. Contains its own inspector and an +/// inner inspector. This is used to create a stack of inspectors that can +/// be used to inspect the execution of a contract. +/// +/// The current inspector will be invoked first, then the inner inspector. +/// For functions that may return values (e.g. [`Inspector::call`]), if the +/// current inspector returns a value, the inner inspector will not be invoked. +#[derive(Clone, Debug)] +pub struct Layered { + current: Outer, + inner: Inner, +} + +impl Inspector for Layered +where + Outer: Inspector, + Inner: Inspector, +{ + fn initialize_interp(&mut self, interp: &mut Interpreter, context: &mut Ctx) { + self.current.initialize_interp(interp, context); + self.inner.initialize_interp(interp, context); + } + + fn step(&mut self, interp: &mut Interpreter, context: &mut Ctx) { + self.current.step(interp, context); + self.inner.step(interp, context); + } + + fn step_end(&mut self, interp: &mut Interpreter, context: &mut Ctx) { + self.current.step_end(interp, context); + self.inner.step_end(interp, context); + } + + fn log(&mut self, interp: &mut Interpreter, context: &mut Ctx, log: Log) { + self.current.log(interp, context, log.clone()); + self.inner.log(interp, context, log); + } + + fn call(&mut self, context: &mut Ctx, inputs: &mut CallInputs) -> Option { + if let Some(outcome) = self.current.call(context, inputs) { + return Some(outcome); + } + self.inner.call(context, inputs) + } + + fn call_end(&mut self, context: &mut Ctx, inputs: &CallInputs, outcome: &mut CallOutcome) { + self.current.call_end(context, inputs, outcome); + self.inner.call_end(context, inputs, outcome); + } + + fn create(&mut self, context: &mut Ctx, inputs: &mut CreateInputs) -> Option { + if let Some(outcome) = self.current.create(context, inputs) { + return Some(outcome); + } + self.inner.create(context, inputs) + } + + fn create_end( + &mut self, + context: &mut Ctx, + inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + self.current.create_end(context, inputs, outcome); + self.inner.create_end(context, inputs, outcome); + } + + fn eofcreate( + &mut self, + context: &mut Ctx, + inputs: &mut EOFCreateInputs, + ) -> Option { + if let Some(outcome) = self.current.eofcreate(context, inputs) { + return Some(outcome); + } + self.inner.eofcreate(context, inputs) + } + + fn eofcreate_end( + &mut self, + context: &mut Ctx, + inputs: &EOFCreateInputs, + outcome: &mut CreateOutcome, + ) { + self.current.eofcreate_end(context, inputs, outcome); + self.inner.eofcreate_end(context, inputs, outcome); + } + + fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { + self.current.selfdestruct(contract, target, value); + self.inner.selfdestruct(contract, target, value); + } +} diff --git a/src/inspectors/mod.rs b/src/inspectors/mod.rs index 2fbaafd..79eff15 100644 --- a/src/inspectors/mod.rs +++ b/src/inspectors/mod.rs @@ -1,8 +1,37 @@ mod timeout; pub use timeout::TimeLimit; +mod set; +pub use set::InspectorSet; + mod spanning; pub use spanning::SpanningInspector; -mod stack; -pub use stack::InspectorStack; +mod layer; +pub use layer::Layered; + +#[cfg(test)] +mod test { + use super::*; + use crate::{test_utils::TestInspector, NoopBlock, NoopCfg}; + use revm::{database::InMemoryDB, primitives::B256}; + use std::time::Duration; + + #[test] + fn test_timeout() { + let mut stack = InspectorSet::new(); + stack.push_back(TimeLimit::new(Duration::from_millis(15))); + stack.push_back(SpanningInspector::at_info()); + stack.push_back(TestInspector::default()); + + let mut trevm = crate::TrevmBuilder::new() + .with_db(InMemoryDB::default()) + .with_insp(stack) + .build_trevm() + .unwrap() + .fill_cfg(&NoopCfg) + .fill_block(&NoopBlock); + + trevm.apply_eip4788(B256::repeat_byte(0xaa)).unwrap(); + } +} diff --git a/src/inspectors/stack.rs b/src/inspectors/set.rs similarity index 77% rename from src/inspectors/stack.rs rename to src/inspectors/set.rs index 6eba72e..48121b6 100644 --- a/src/inspectors/stack.rs +++ b/src/inspectors/set.rs @@ -13,11 +13,11 @@ use revm::{ /// /// This is a thin wrapper around a [`VecDeque`] of inspectors. #[derive(Default)] -pub struct InspectorStack { +pub struct InspectorSet { inspectors: VecDeque>>, } -impl core::ops::Deref for InspectorStack { +impl core::ops::Deref for InspectorSet { type Target = VecDeque>>; fn deref(&self) -> &Self::Target { @@ -25,19 +25,22 @@ impl core::ops::Deref for InspectorStack { } } -impl core::ops::DerefMut for InspectorStack { +impl core::ops::DerefMut for InspectorSet { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inspectors } } -impl core::fmt::Debug for InspectorStack { +impl core::fmt::Debug for InspectorSet { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("InspectorStack").field("inspectors", &self.inspectors.len()).finish() } } -impl InspectorStack { +impl InspectorSet +where + Int: InterpreterTypes, +{ /// Instantiate a new empty inspector stack. pub fn new() -> Self { Self { inspectors: Default::default() } @@ -47,9 +50,29 @@ impl InspectorStack { pub fn with_capacity(cap: usize) -> Self { Self { inspectors: VecDeque::with_capacity(cap) } } + + /// Push an inspector to the back of the stack. + pub fn push_back + 'static>(&mut self, inspector: I) { + self.inspectors.push_back(Box::new(inspector)); + } + + /// Push an inspector to the front of the stack. + pub fn push_front + 'static>(&mut self, inspector: I) { + self.inspectors.push_front(Box::new(inspector)); + } + + /// Pop an inspector from the back of the stack. + pub fn pop_back(&mut self) -> Option>> { + self.inspectors.pop_back() + } + + /// Pop an inspector from the front of the stack. + pub fn pop_front(&mut self) -> Option>> { + self.inspectors.pop_front() + } } -impl Inspector for InspectorStack +impl Inspector for InspectorSet where Int: InterpreterTypes, { diff --git a/src/inspectors/spanning.rs b/src/inspectors/spanning.rs index 46586b0..b39f06a 100644 --- a/src/inspectors/spanning.rs +++ b/src/inspectors/spanning.rs @@ -63,6 +63,21 @@ impl SpanningInspector { Self { active: Vec::new(), level } } + /// Create a new `SpanningInspector` producing spans at [`Level::TRACE`]. + pub const fn at_trace() -> Self { + Self::new(Level::TRACE) + } + + /// Create a new `SpanningInspector` producing spans at [`Level::DEBUG`]. + pub const fn at_debug() -> Self { + Self::new(Level::DEBUG) + } + + /// Create a new `SpanningInspector` producing spans at [`Level::WARN`]. + pub const fn at_info() -> Self { + Self::new(Level::INFO) + } + /// Create a root span fn root_span(&mut self, _interp: &mut Interpreter, _context: &mut Ctx) -> Span where diff --git a/src/test_utils.rs b/src/test_utils.rs index c4e7afb..0e6ce71 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -4,7 +4,11 @@ use revm::{ bytecode::Bytecode, database::{CacheDB, EmptyDB, InMemoryDB, State}, inspector::{inspectors::TracerEip3155, NoOpInspector}, - primitives::hardfork::SpecId, + interpreter::{ + CallInputs, CallOutcome, CreateInputs, CreateOutcome, EOFCreateInputs, Interpreter, + InterpreterTypes, + }, + primitives::{hardfork::SpecId, Log}, state::AccountInfo, Context, Inspector, MainBuilder, }; @@ -115,3 +119,118 @@ pub fn test_trevm_tracing() -> EvmNeedsCfg { .build_mainnet_with_inspector(TracerEip3155::new(Box::new(std::io::stdout()))), ) } + +/// Inspector that tracks whether the various inspector methods were +/// called. This is useful for testing the inspector stack. +/// +/// It is not a real inspector, and does not do anything with the +/// information it collects. +#[derive(Default, Debug, Clone, Copy)] +pub struct TestInspector { + /// Whether initialize_interp was called. + pub init_interp: bool, + /// Whether step was called. + pub step: bool, + /// Whether step_end was called. + pub step_end: bool, + /// Whether log was called. + pub log: bool, + /// Whether call was called. + pub call: bool, + /// Whether call_end was called. + pub call_end: bool, + /// Whether create was called. + pub create: bool, + /// Whether create_end was called. + pub create_end: bool, + /// Whether eofcreate was called. + pub eofcreate: bool, + /// Whether eofcreate_end was called. + pub eofcreate_end: bool, + /// Whether selfdestruct was called. + pub selfdestruct: bool, +} + +impl TestInspector { + /// Reset the inspector to its default state. + pub fn reset(&mut self) { + *self = Self::default(); + } +} + +impl Inspector for TestInspector +where + Int: InterpreterTypes, +{ + fn initialize_interp(&mut self, _interp: &mut Interpreter, _context: &mut Ctx) { + tracing::info!("initialize_interp"); + self.init_interp = true; + } + + fn step(&mut self, _interp: &mut Interpreter, _context: &mut Ctx) { + tracing::info!("step"); + self.step = true; + } + + fn step_end(&mut self, _interp: &mut Interpreter, _context: &mut Ctx) { + tracing::info!("step_end"); + self.step_end = true; + } + + fn log(&mut self, _interp: &mut Interpreter, _context: &mut Ctx, log: Log) { + tracing::info!(?log, "log"); + self.log = true; + } + + fn call(&mut self, _context: &mut Ctx, inputs: &mut CallInputs) -> Option { + tracing::info!(?inputs, "call"); + self.call = true; + None + } + + fn call_end(&mut self, _context: &mut Ctx, inputs: &CallInputs, outcome: &mut CallOutcome) { + tracing::info!(?inputs, ?outcome, "call_end"); + self.call_end = true; + } + + fn create(&mut self, _context: &mut Ctx, inputs: &mut CreateInputs) -> Option { + tracing::info!(?inputs, "create"); + self.create = true; + None + } + + fn create_end( + &mut self, + _context: &mut Ctx, + _inputs: &CreateInputs, + _outcome: &mut CreateOutcome, + ) { + tracing::info!("create_end"); + self.create_end = true; + } + + fn eofcreate( + &mut self, + _context: &mut Ctx, + _inputs: &mut EOFCreateInputs, + ) -> Option { + tracing::info!("eofcreate"); + self.eofcreate = true; + None + } + + fn eofcreate_end( + &mut self, + _context: &mut Ctx, + _inputs: &EOFCreateInputs, + _outcome: &mut CreateOutcome, + ) { + tracing::info!("eofcreate_end"); + self.eofcreate_end = true; + } + + fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { + tracing::info!(?contract, ?target, ?value, "selfdestruct"); + self.selfdestruct = true; + } +} From 9c838716cf46c078dcc936222b354899bec9dfd4 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 2 Apr 2025 13:12:51 +0200 Subject: [PATCH 5/9] fix: allow instantiating layered --- src/inspectors/layer.rs | 89 +++++++++++++++++++++++++++++++++++------ src/inspectors/mod.rs | 2 +- 2 files changed, 78 insertions(+), 13 deletions(-) diff --git a/src/inspectors/layer.rs b/src/inspectors/layer.rs index 1cb9883..e8dfc4f 100644 --- a/src/inspectors/layer.rs +++ b/src/inspectors/layer.rs @@ -8,58 +8,123 @@ use revm::{ Inspector, }; +/// Extension trait for [`Inspector`] to add convenience methods for +/// layering inspectors into [`Layered`]. +pub trait InspectorExt: Inspector +where + Int: InterpreterTypes, +{ + /// Create a new [`Layered`] inspector with the current inspector as the + /// inner inspector, `other` as the outer inspector. + fn wrap_in(self, other: Other) -> Layered + where + Self: Sized, + Other: Inspector, + { + Layered::new(other, self) + } + + /// Create a new [`Layered`] inspector with the current inspector as the + /// outer inspector, `other` as the inner inspector. + fn wrap_around(self, other: Other) -> Layered + where + Self: Sized, + Other: Inspector, + { + Layered::new(self, other) + } +} + +impl InspectorExt for T +where + T: Inspector, + Int: InterpreterTypes, +{ +} + /// A layer in a stack of inspectors. Contains its own inspector and an /// inner inspector. This is used to create a stack of inspectors that can /// be used to inspect the execution of a contract. /// +/// Use `Layered` when you need to retain type information about the inner +/// inspectors. +/// /// The current inspector will be invoked first, then the inner inspector. /// For functions that may return values (e.g. [`Inspector::call`]), if the /// current inspector returns a value, the inner inspector will not be invoked. #[derive(Clone, Debug)] pub struct Layered { - current: Outer, + outer: Outer, inner: Inner, } +impl Layered { + /// Create a new [`Layered`] inspector with the given current and inner + /// inspectors. + pub fn new(current: Outer, inner: Inner) -> Self { + Self { outer: current, inner } + } + + /// Get a reference to the current inspector. + pub fn current(&self) -> &Outer { + &self.outer + } + + /// Get a mutable reference to the current inspector. + pub fn current_mut(&mut self) -> &mut Outer { + &mut self.outer + } + + /// Get a reference to the inner inspector. + pub fn inner(&self) -> &Inner { + &self.inner + } + + /// Get a mutable reference to the inner inspector. + pub fn inner_mut(&mut self) -> &mut Inner { + &mut self.inner + } +} + impl Inspector for Layered where Outer: Inspector, Inner: Inspector, { fn initialize_interp(&mut self, interp: &mut Interpreter, context: &mut Ctx) { - self.current.initialize_interp(interp, context); + self.outer.initialize_interp(interp, context); self.inner.initialize_interp(interp, context); } fn step(&mut self, interp: &mut Interpreter, context: &mut Ctx) { - self.current.step(interp, context); + self.outer.step(interp, context); self.inner.step(interp, context); } fn step_end(&mut self, interp: &mut Interpreter, context: &mut Ctx) { - self.current.step_end(interp, context); + self.outer.step_end(interp, context); self.inner.step_end(interp, context); } fn log(&mut self, interp: &mut Interpreter, context: &mut Ctx, log: Log) { - self.current.log(interp, context, log.clone()); + self.outer.log(interp, context, log.clone()); self.inner.log(interp, context, log); } fn call(&mut self, context: &mut Ctx, inputs: &mut CallInputs) -> Option { - if let Some(outcome) = self.current.call(context, inputs) { + if let Some(outcome) = self.outer.call(context, inputs) { return Some(outcome); } self.inner.call(context, inputs) } fn call_end(&mut self, context: &mut Ctx, inputs: &CallInputs, outcome: &mut CallOutcome) { - self.current.call_end(context, inputs, outcome); + self.outer.call_end(context, inputs, outcome); self.inner.call_end(context, inputs, outcome); } fn create(&mut self, context: &mut Ctx, inputs: &mut CreateInputs) -> Option { - if let Some(outcome) = self.current.create(context, inputs) { + if let Some(outcome) = self.outer.create(context, inputs) { return Some(outcome); } self.inner.create(context, inputs) @@ -71,7 +136,7 @@ where inputs: &CreateInputs, outcome: &mut CreateOutcome, ) { - self.current.create_end(context, inputs, outcome); + self.outer.create_end(context, inputs, outcome); self.inner.create_end(context, inputs, outcome); } @@ -80,7 +145,7 @@ where context: &mut Ctx, inputs: &mut EOFCreateInputs, ) -> Option { - if let Some(outcome) = self.current.eofcreate(context, inputs) { + if let Some(outcome) = self.outer.eofcreate(context, inputs) { return Some(outcome); } self.inner.eofcreate(context, inputs) @@ -92,12 +157,12 @@ where inputs: &EOFCreateInputs, outcome: &mut CreateOutcome, ) { - self.current.eofcreate_end(context, inputs, outcome); + self.outer.eofcreate_end(context, inputs, outcome); self.inner.eofcreate_end(context, inputs, outcome); } fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { - self.current.selfdestruct(contract, target, value); + self.outer.selfdestruct(contract, target, value); self.inner.selfdestruct(contract, target, value); } } diff --git a/src/inspectors/mod.rs b/src/inspectors/mod.rs index 79eff15..1074a37 100644 --- a/src/inspectors/mod.rs +++ b/src/inspectors/mod.rs @@ -8,7 +8,7 @@ mod spanning; pub use spanning::SpanningInspector; mod layer; -pub use layer::Layered; +pub use layer::{InspectorExt, Layered}; #[cfg(test)] mod test { From 197f08897d123d853e3509a064da7815ca52487a Mon Sep 17 00:00:00 2001 From: James Date: Wed, 2 Apr 2025 14:32:26 +0200 Subject: [PATCH 6/9] fix: trace syscalls and change fail values --- src/inspectors/layer.rs | 59 ++++++++++++--------------------------- src/inspectors/mod.rs | 17 +++++------ src/inspectors/timeout.rs | 6 ++-- src/lib.rs | 4 +-- src/system/mod.rs | 12 +++++--- 5 files changed, 40 insertions(+), 58 deletions(-) diff --git a/src/inspectors/layer.rs b/src/inspectors/layer.rs index e8dfc4f..fa8d0c6 100644 --- a/src/inspectors/layer.rs +++ b/src/inspectors/layer.rs @@ -1,5 +1,4 @@ use revm::{ - inspector::NoOpInspector, interpreter::{ CallInputs, CallOutcome, CreateInputs, CreateOutcome, EOFCreateInputs, Interpreter, InterpreterTypes, @@ -8,40 +7,6 @@ use revm::{ Inspector, }; -/// Extension trait for [`Inspector`] to add convenience methods for -/// layering inspectors into [`Layered`]. -pub trait InspectorExt: Inspector -where - Int: InterpreterTypes, -{ - /// Create a new [`Layered`] inspector with the current inspector as the - /// inner inspector, `other` as the outer inspector. - fn wrap_in(self, other: Other) -> Layered - where - Self: Sized, - Other: Inspector, - { - Layered::new(other, self) - } - - /// Create a new [`Layered`] inspector with the current inspector as the - /// outer inspector, `other` as the inner inspector. - fn wrap_around(self, other: Other) -> Layered - where - Self: Sized, - Other: Inspector, - { - Layered::new(self, other) - } -} - -impl InspectorExt for T -where - T: Inspector, - Int: InterpreterTypes, -{ -} - /// A layer in a stack of inspectors. Contains its own inspector and an /// inner inspector. This is used to create a stack of inspectors that can /// be used to inspect the execution of a contract. @@ -53,7 +18,7 @@ where /// For functions that may return values (e.g. [`Inspector::call`]), if the /// current inspector returns a value, the inner inspector will not be invoked. #[derive(Clone, Debug)] -pub struct Layered { +pub struct Layered { outer: Outer, inner: Inner, } @@ -61,22 +26,34 @@ pub struct Layered { impl Layered { /// Create a new [`Layered`] inspector with the given current and inner /// inspectors. - pub fn new(current: Outer, inner: Inner) -> Self { - Self { outer: current, inner } + pub const fn new(outer: Outer, inner: Inner) -> Self { + Self { outer, inner } + } + + /// Wrap this inspector in another, creating a new [`Layered`] inspector. + /// with this as the inner inspector. + pub fn wrap_in(self, outer: Other) -> Layered { + Layered { outer, inner: self } + } + + /// Wrap this inspector around another, creating a new [`Layered`] inspector + /// with this as the outer inspector. + pub fn wrap_around(self, inner: Other) -> Layered { + Layered { outer: self, inner } } /// Get a reference to the current inspector. - pub fn current(&self) -> &Outer { + pub const fn outer(&self) -> &Outer { &self.outer } /// Get a mutable reference to the current inspector. - pub fn current_mut(&mut self) -> &mut Outer { + pub fn outer_mut(&mut self) -> &mut Outer { &mut self.outer } /// Get a reference to the inner inspector. - pub fn inner(&self) -> &Inner { + pub const fn inner(&self) -> &Inner { &self.inner } diff --git a/src/inspectors/mod.rs b/src/inspectors/mod.rs index 1074a37..15834cd 100644 --- a/src/inspectors/mod.rs +++ b/src/inspectors/mod.rs @@ -8,30 +8,31 @@ mod spanning; pub use spanning::SpanningInspector; mod layer; -pub use layer::{InspectorExt, Layered}; +pub use layer::Layered; #[cfg(test)] mod test { use super::*; use crate::{test_utils::TestInspector, NoopBlock, NoopCfg}; - use revm::{database::InMemoryDB, primitives::B256}; + use revm::{database::InMemoryDB, inspector::InspectorEvmTr, primitives::B256}; use std::time::Duration; #[test] - fn test_timeout() { - let mut stack = InspectorSet::new(); - stack.push_back(TimeLimit::new(Duration::from_millis(15))); - stack.push_back(SpanningInspector::at_info()); - stack.push_back(TestInspector::default()); + fn test() { + let inspector = + Layered::new(TimeLimit::new(Duration::from_micros(10)), SpanningInspector::at_info()) + .wrap_around(TestInspector::default()); let mut trevm = crate::TrevmBuilder::new() .with_db(InMemoryDB::default()) - .with_insp(stack) + .with_insp(inspector) .build_trevm() .unwrap() .fill_cfg(&NoopCfg) .fill_block(&NoopBlock); trevm.apply_eip4788(B256::repeat_byte(0xaa)).unwrap(); + + dbg!(trevm.inner_mut_unchecked().ctx_inspector().1); } } diff --git a/src/inspectors/timeout.rs b/src/inspectors/timeout.rs index 5388b72..395f3d4 100644 --- a/src/inspectors/timeout.rs +++ b/src/inspectors/timeout.rs @@ -10,7 +10,7 @@ use std::time::{Duration, Instant}; const CALL_TIMEOUT: CallOutcome = CallOutcome { result: InterpreterResult { - result: InstructionResult::FatalExternalError, + result: InstructionResult::CallTooDeep, output: Bytes::new(), gas: Gas::new_spent(0), }, @@ -19,7 +19,7 @@ const CALL_TIMEOUT: CallOutcome = CallOutcome { const CREATE_TIMEOUT: CreateOutcome = CreateOutcome { result: InterpreterResult { - result: InstructionResult::FatalExternalError, + result: InstructionResult::CallTooDeep, output: Bytes::new(), gas: Gas::new_spent(0), }, @@ -41,7 +41,7 @@ const CREATE_TIMEOUT: CreateOutcome = CreateOutcome { /// - any invalid opcode /// /// When execution is terminated by the timer, it will result in a -/// [`InstructionResult::FatalExternalError`]. +/// [`InstructionResult::CallTooDeep`]. /// /// ## Usage Note /// diff --git a/src/lib.rs b/src/lib.rs index 59a9f54..4674505 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -115,10 +115,10 @@ //! statistics or indices that are only available after the block is closed. //! //! ``` -//! # use revm::{database::in_memory_db::InMemoryDB}; +//! # use revm::{database::in_memory_db::InMemoryDB, inspector::NoOpInspector}; //! # use trevm::{TrevmBuilder, EvmErrored, Cfg, BlockDriver}; //! # use alloy::primitives::B256; -//! # fn t>(cfg: &C, mut driver: D) +//! # fn t>(cfg: &C, mut driver: D) //! # -> Result<(), Box> { //! let trevm = TrevmBuilder::new() //! .with_db(InMemoryDB::default()) diff --git a/src/system/mod.rs b/src/system/mod.rs index d56d3f7..721cf33 100644 --- a/src/system/mod.rs +++ b/src/system/mod.rs @@ -59,7 +59,10 @@ pub const MAX_BLOB_GAS_PER_BLOCK_CANCUN: u64 = 786_432; /// The maximum blob gas limit for a block in Prague. pub const MAX_BLOB_GAS_PER_BLOCK_PRAGUE: u64 = 1_179_648; -use crate::{helpers::Evm, EvmExtUnchecked, Tx}; +use crate::{ + helpers::{Ctx, Evm}, + EvmExtUnchecked, Tx, +}; use alloy::primitives::{Address, Bytes}; use revm::{ bytecode::Bytecode, @@ -68,7 +71,7 @@ use revm::{ Block, ContextTr, Transaction, }, primitives::KECCAK_EMPTY, - Database, DatabaseCommit, ExecuteEvm, + Database, DatabaseCommit, InspectEvm, Inspector, }; fn checked_insert_code( @@ -127,18 +130,19 @@ pub(crate) fn execute_system_tx( ) -> Result> where Db: Database + DatabaseCommit, + Insp: Inspector>, { syscall.fill_tx(evm); let limit = evm.tx().gas_limit(); let block = &mut evm.data.ctx.block; + let old_gas_limit = core::mem::replace(&mut block.gas_limit, limit); let old_base_fee = core::mem::take(&mut block.basefee); - let previous_nonce_check = std::mem::replace(&mut evm.data.ctx.cfg.disable_nonce_check, true); - let mut result = evm.replay()?; + let mut result = evm.inspect_replay()?; // Cleanup the syscall. cleanup_syscall(evm, &mut result, syscall, old_gas_limit, old_base_fee, previous_nonce_check); From e9697e6536cd56ba37047303e2c5dbe37a9e53a7 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 2 Apr 2025 14:34:50 +0200 Subject: [PATCH 7/9] lint: clippy --- src/inspectors/layer.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/inspectors/layer.rs b/src/inspectors/layer.rs index fa8d0c6..4faf0b0 100644 --- a/src/inspectors/layer.rs +++ b/src/inspectors/layer.rs @@ -32,13 +32,13 @@ impl Layered { /// Wrap this inspector in another, creating a new [`Layered`] inspector. /// with this as the inner inspector. - pub fn wrap_in(self, outer: Other) -> Layered { + pub const fn wrap_in(self, outer: Other) -> Layered { Layered { outer, inner: self } } /// Wrap this inspector around another, creating a new [`Layered`] inspector /// with this as the outer inspector. - pub fn wrap_around(self, inner: Other) -> Layered { + pub const fn wrap_around(self, inner: Other) -> Layered { Layered { outer: self, inner } } From 5dcff668c84233e7cbe8e9240b37a005c1583905 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 3 Apr 2025 07:19:57 +0200 Subject: [PATCH 8/9] chore: better docs --- src/inspectors/mod.rs | 3 ++- src/inspectors/timeout.rs | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/inspectors/mod.rs b/src/inspectors/mod.rs index 15834cd..45ed73e 100644 --- a/src/inspectors/mod.rs +++ b/src/inspectors/mod.rs @@ -33,6 +33,7 @@ mod test { trevm.apply_eip4788(B256::repeat_byte(0xaa)).unwrap(); - dbg!(trevm.inner_mut_unchecked().ctx_inspector().1); + assert!(trevm.inner_mut_unchecked().inspector().outer().outer().has_elapsed()); + } } diff --git a/src/inspectors/timeout.rs b/src/inspectors/timeout.rs index 395f3d4..087d7bf 100644 --- a/src/inspectors/timeout.rs +++ b/src/inspectors/timeout.rs @@ -41,7 +41,14 @@ const CREATE_TIMEOUT: CreateOutcome = CreateOutcome { /// - any invalid opcode /// /// When execution is terminated by the timer, it will result in a -/// [`InstructionResult::CallTooDeep`]. +/// [`InstructionResult::CallTooDeep`]. This is somewhat unintutive. `revm` +/// uses the [`InstructionResult`] enum to represent possible outcomes of a +/// opcode. It requires that the inspector's outcome is a valid +/// [`InstructionResult`], but does not provide a way to represent a custom +/// outcome. This means that the inspector must overload an existing outcome. +/// `CallTooDeep` is used here because it is effectively unreachable in normal +/// `evm` execution due to [EIP-150] call gas forwarding rules, and therefore +/// overloading it is unlikely to cause issues. /// /// ## Usage Note /// @@ -54,6 +61,8 @@ const CREATE_TIMEOUT: CreateOutcome = CreateOutcome { /// invalid data is not inspected, and that other inspectors do not consume /// memory or compute resources inspecting data that is guaranteed to be /// discarded. +/// +/// [EIP-150]: https://eips.ethereum.org/EIPS/eip-150 #[derive(Debug, Clone, Copy)] pub struct TimeLimit { duration: Duration, From dbb68d4c90d9629c17513f00c4ffa3ac26c502ae Mon Sep 17 00:00:00 2001 From: James Date: Thu, 3 Apr 2025 07:24:21 +0200 Subject: [PATCH 9/9] lint: fmt --- src/inspectors/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/inspectors/mod.rs b/src/inspectors/mod.rs index 45ed73e..7ee9cf1 100644 --- a/src/inspectors/mod.rs +++ b/src/inspectors/mod.rs @@ -34,6 +34,5 @@ mod test { trevm.apply_eip4788(B256::repeat_byte(0xaa)).unwrap(); assert!(trevm.inner_mut_unchecked().inspector().outer().outer().has_elapsed()); - } }