chore: move revm-inspectors to a separate repo (#5992)

This commit is contained in:
DaniPopes
2024-01-09 22:33:45 +01:00
committed by GitHub
parent 0efbf893e3
commit 19f481006b
28 changed files with 40 additions and 5134 deletions

39
Cargo.lock generated
View File

@ -5692,7 +5692,6 @@ dependencies = [
"reth-provider",
"reth-prune",
"reth-revm",
"reth-revm-inspectors",
"reth-rpc",
"reth-rpc-api",
"reth-rpc-builder",
@ -5705,6 +5704,7 @@ dependencies = [
"reth-tracing",
"reth-transaction-pool",
"reth-trie",
"revm-inspectors",
"secp256k1 0.27.0",
"serde",
"serde_json",
@ -6446,29 +6446,12 @@ dependencies = [
"reth-interfaces",
"reth-primitives",
"reth-provider",
"reth-revm-inspectors",
"reth-trie",
"revm",
"revm-inspectors",
"tracing",
]
[[package]]
name = "reth-revm-inspectors"
version = "0.1.0-alpha.14"
dependencies = [
"alloy-primitives",
"alloy-rpc-trace-types",
"alloy-rpc-types",
"alloy-sol-types",
"boa_engine",
"boa_gc",
"revm",
"serde",
"serde_json",
"thiserror",
"tokio",
]
[[package]]
name = "reth-rpc"
version = "0.1.0-alpha.14"
@ -6805,6 +6788,24 @@ dependencies = [
"revm-precompile",
]
[[package]]
name = "revm-inspectors"
version = "0.1.0"
source = "git+https://github.com/paradigmxyz/evm-inspectors#4a3f82fa0d2010b3de69479b42fafe82339dae5e"
dependencies = [
"alloy-primitives",
"alloy-rpc-trace-types",
"alloy-rpc-types",
"alloy-sol-types",
"boa_engine",
"boa_gc",
"revm",
"serde",
"serde_json",
"thiserror",
"tokio",
]
[[package]]
name = "revm-interpreter"
version = "1.3.0"

View File

@ -28,7 +28,6 @@ members = [
"crates/primitives/",
"crates/prune/",
"crates/revm/",
"crates/revm/revm-inspectors/",
"crates/rpc/ipc/",
"crates/rpc/rpc/",
"crates/rpc/rpc-api/",
@ -135,7 +134,6 @@ reth-primitives = { path = "crates/primitives" }
reth-provider = { path = "crates/storage/provider" }
reth-prune = { path = "crates/prune" }
reth-revm = { path = "crates/revm" }
reth-revm-inspectors = { path = "crates/revm/revm-inspectors" }
reth-rpc = { path = "crates/rpc/rpc" }
reth-rpc-api = { path = "crates/rpc/rpc-api" }
reth-rpc-api-testing-util = { path = "crates/rpc/rpc-testing-util" }
@ -152,8 +150,14 @@ reth-transaction-pool = { path = "crates/transaction-pool" }
reth-trie = { path = "crates/trie" }
# revm
revm = { git = "https://github.com/bluealloy/revm", branch = "reth_freeze", features = ["std", "secp256k1"], default-features = false }
revm-primitives = { git = "https://github.com/bluealloy/revm", branch = "reth_freeze", features = ["std"], default-features = false }
revm = { git = "https://github.com/bluealloy/revm", branch = "reth_freeze", features = [
"std",
"secp256k1",
], default-features = false }
revm-primitives = { git = "https://github.com/bluealloy/revm", branch = "reth_freeze", features = [
"std",
], default-features = false }
revm-inspectors = { git = "https://github.com/paradigmxyz/evm-inspectors" }
# eth
alloy-primitives = "0.5"
@ -161,7 +165,9 @@ alloy-dyn-abi = "0.5"
alloy-sol-types = "0.5"
alloy-rlp = "0.3"
alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy", features = ["jsonrpsee-types"] }
alloy-rpc-trace-types = { git = "https://github.com/alloy-rs/alloy", features = ["jsonrpsee-types"] }
alloy-rpc-trace-types = { git = "https://github.com/alloy-rs/alloy", features = [
"jsonrpsee-types",
] }
ethers-core = { version = "2.0", default-features = false }
ethers-providers = { version = "2.0", default-features = false }
ethers-signers = { version = "2.0", default-features = false }
@ -196,7 +202,6 @@ once_cell = "1.17"
syn = "2.0"
ahash = "0.8.6"
# proc-macros
proc-macro2 = "1.0"
quote = "1.0"
@ -241,7 +246,6 @@ proptest = "1.4"
proptest-derive = "0.4"
serial_test = "2"
[workspace.metadata.cargo-udeps.ignore]
# ignored because this is mutually exclusive with the optimism payload builder via feature flags
normal = ["reth-ethereum-payload-builder"]

View File

@ -26,7 +26,6 @@ reth-db = { workspace = true, features = ["mdbx", "test-utils"] }
# TODO: Temporary use of the test-utils feature
reth-provider = { workspace = true, features = ["test-utils"] }
reth-revm.workspace = true
reth-revm-inspectors.workspace = true
reth-stages.workspace = true
reth-interfaces = { workspace = true, features = ["test-utils", "clap"] }
reth-transaction-pool.workspace = true
@ -60,6 +59,7 @@ reth-nippy-jar.workspace = true
# crypto
alloy-rlp.workspace = true
secp256k1 = { workspace = true, features = ["global-context", "rand-std", "recovery"] }
revm-inspectors.workspace = true
# tracing
tracing.workspace = true

View File

@ -73,7 +73,6 @@ use reth_provider::{
};
use reth_prune::PrunerBuilder;
use reth_revm::EvmProcessorFactory;
use reth_revm_inspectors::stack::Hook;
use reth_rpc_engine_api::EngineApi;
use reth_stages::{
prelude::*,
@ -89,6 +88,7 @@ use reth_transaction_pool::{
blobstore::InMemoryBlobStore, EthTransactionPool, TransactionPool,
TransactionValidationTaskExecutor,
};
use revm_inspectors::stack::Hook;
use secp256k1::SecretKey;
use std::{
net::{SocketAddr, SocketAddrV4},
@ -862,7 +862,7 @@ impl NodeConfig {
}
let (tip_tx, tip_rx) = watch::channel(B256::ZERO);
use reth_revm_inspectors::stack::InspectorStackConfig;
use revm_inspectors::stack::InspectorStackConfig;
let factory = reth_revm::EvmProcessorFactory::new(self.chain.clone());
let stack_config = InspectorStackConfig {

View File

@ -16,11 +16,11 @@ workspace = true
reth-primitives.workspace = true
reth-interfaces.workspace = true
reth-provider.workspace = true
reth-revm-inspectors.workspace = true
reth-consensus-common.workspace = true
# revm
revm.workspace = true
revm-inspectors.workspace = true
# common
tracing.workspace = true
@ -35,3 +35,4 @@ optimism = [
"reth-consensus-common/optimism",
"reth-interfaces/optimism",
]
js-tracer = ["revm-inspectors/js-tracer"]

View File

@ -1,35 +0,0 @@
[package]
name = "reth-revm-inspectors"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
description = "revm inspector implementations used by reth"
[lints]
workspace = true
[dependencies]
# eth
alloy-sol-types.workspace = true
alloy-primitives.workspace = true
alloy-rpc-types.workspace = true
alloy-rpc-trace-types.workspace = true
revm.workspace = true
serde = { workspace = true, features = ["derive"] }
thiserror = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
# js-tracing-inspector
boa_engine = { workspace = true, optional = true }
boa_gc = { workspace = true, optional = true }
tokio = { version = "1", features = ["sync"], optional = true }
[features]
default = ["js-tracer"]
js-tracer = ["boa_engine", "boa_gc", "tokio", "thiserror", "serde_json"]

View File

@ -1,99 +0,0 @@
use alloy_primitives::{Address, B256};
use alloy_rpc_types::{AccessList, AccessListItem};
use revm::{
interpreter::{opcode, Interpreter},
Database, EVMData, Inspector,
};
use std::collections::{BTreeSet, HashMap, HashSet};
/// An [Inspector] that collects touched accounts and storage slots.
///
/// This can be used to construct an [AccessList] for a transaction via `eth_createAccessList`
#[derive(Default, Debug)]
pub struct AccessListInspector {
/// All addresses that should be excluded from the final accesslist
excluded: HashSet<Address>,
/// All addresses and touched slots
access_list: HashMap<Address, BTreeSet<B256>>,
}
impl AccessListInspector {
/// Creates a new inspector instance
///
/// The `access_list` is the provided access list from the call request
pub fn new(
access_list: AccessList,
from: Address,
to: Address,
precompiles: impl IntoIterator<Item = Address>,
) -> Self {
AccessListInspector {
excluded: [from, to].into_iter().chain(precompiles).collect(),
access_list: access_list
.0
.into_iter()
.map(|v| (v.address, v.storage_keys.into_iter().collect()))
.collect(),
}
}
/// Returns list of addresses and storage keys used by the transaction. It gives you the list of
/// addresses and storage keys that were touched during execution.
pub fn into_access_list(self) -> AccessList {
let items = self.access_list.into_iter().map(|(address, slots)| AccessListItem {
address,
storage_keys: slots.into_iter().collect(),
});
AccessList(items.collect())
}
/// Returns list of addresses and storage keys used by the transaction. It gives you the list of
/// addresses and storage keys that were touched during execution.
pub fn access_list(&self) -> AccessList {
let items = self.access_list.iter().map(|(address, slots)| AccessListItem {
address: *address,
storage_keys: slots.iter().copied().collect(),
});
AccessList(items.collect())
}
}
impl<DB> Inspector<DB> for AccessListInspector
where
DB: Database,
{
fn step(&mut self, interpreter: &mut Interpreter<'_>, _data: &mut EVMData<'_, DB>) {
match interpreter.current_opcode() {
opcode::SLOAD | opcode::SSTORE => {
if let Ok(slot) = interpreter.stack().peek(0) {
let cur_contract = interpreter.contract.address;
self.access_list
.entry(cur_contract)
.or_default()
.insert(B256::from(slot.to_be_bytes()));
}
}
opcode::EXTCODECOPY |
opcode::EXTCODEHASH |
opcode::EXTCODESIZE |
opcode::BALANCE |
opcode::SELFDESTRUCT => {
if let Ok(slot) = interpreter.stack().peek(0) {
let addr = Address::from_word(B256::from(slot.to_be_bytes()));
if !self.excluded.contains(&addr) {
self.access_list.entry(addr).or_default();
}
}
}
opcode::DELEGATECALL | opcode::CALL | opcode::STATICCALL | opcode::CALLCODE => {
if let Ok(slot) = interpreter.stack().peek(1) {
let addr = Address::from_word(B256::from(slot.to_be_bytes()));
if !self.excluded.contains(&addr) {
self.access_list.entry(addr).or_default();
}
}
}
_ => (),
}
}
}

View File

@ -1,24 +0,0 @@
//! revm [Inspector](revm::Inspector) implementations, such as call tracers
//!
//! ## Feature Flags
//!
//! - `js-tracer` (default): Enables a JavaScript tracer implementation. This pulls in extra
//! dependencies (such as `boa`, `tokio` and `serde_json`).
#![doc(
html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
)]
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
/// An inspector implementation for an EIP2930 Accesslist
pub mod access_list;
/// An inspector stack abstracting the implementation details of
/// each inspector and allowing to hook on block/transaction execution,
/// used in the main RETH executor.
pub mod stack;
/// An inspector for recording traces
pub mod tracing;

View File

@ -1,178 +0,0 @@
use alloy_primitives::U256;
use revm::{
interpreter::{CallInputs, CreateInputs, Gas, InstructionResult, Interpreter},
primitives::{db::Database, Address, Bytes, B256},
EVMData, Inspector,
};
use std::{
cell::{Ref, RefCell},
rc::Rc,
};
/// An [Inspector] that is either owned by an individual [Inspector] or is shared as part of a
/// series of inspectors in a [InspectorStack](crate::stack::InspectorStack).
///
/// Caution: if the [Inspector] is _stacked_ then it _must_ be called first.
#[derive(Debug)]
pub enum MaybeOwnedInspector<INSP> {
/// Inspector is owned.
Owned(Rc<RefCell<INSP>>),
/// Inspector is shared and part of a stack
Stacked(Rc<RefCell<INSP>>),
}
impl<INSP> MaybeOwnedInspector<INSP> {
/// Create a new _owned_ instance
pub fn new_owned(inspector: INSP) -> Self {
MaybeOwnedInspector::Owned(Rc::new(RefCell::new(inspector)))
}
/// Creates a [MaybeOwnedInspector::Stacked] clone of this type.
pub fn clone_stacked(&self) -> Self {
match self {
MaybeOwnedInspector::Owned(gas) | MaybeOwnedInspector::Stacked(gas) => {
MaybeOwnedInspector::Stacked(Rc::clone(gas))
}
}
}
/// Returns a reference to the inspector.
pub fn as_ref(&self) -> Ref<'_, INSP> {
match self {
MaybeOwnedInspector::Owned(insp) => insp.borrow(),
MaybeOwnedInspector::Stacked(insp) => insp.borrow(),
}
}
}
impl<INSP: Default> MaybeOwnedInspector<INSP> {
/// Create a new _owned_ instance
pub fn owned() -> Self {
Self::new_owned(Default::default())
}
}
impl<INSP: Default> Default for MaybeOwnedInspector<INSP> {
fn default() -> Self {
Self::owned()
}
}
impl<INSP> Clone for MaybeOwnedInspector<INSP> {
fn clone(&self) -> Self {
self.clone_stacked()
}
}
impl<INSP, DB> Inspector<DB> for MaybeOwnedInspector<INSP>
where
DB: Database,
INSP: Inspector<DB>,
{
fn initialize_interp(&mut self, interp: &mut Interpreter<'_>, data: &mut EVMData<'_, DB>) {
match self {
MaybeOwnedInspector::Owned(insp) => insp.borrow_mut().initialize_interp(interp, data),
MaybeOwnedInspector::Stacked(_) => {}
}
}
fn step(&mut self, interp: &mut Interpreter<'_>, data: &mut EVMData<'_, DB>) {
match self {
MaybeOwnedInspector::Owned(insp) => insp.borrow_mut().step(interp, data),
MaybeOwnedInspector::Stacked(_) => {}
}
}
fn log(
&mut self,
evm_data: &mut EVMData<'_, DB>,
address: &Address,
topics: &[B256],
data: &Bytes,
) {
match self {
MaybeOwnedInspector::Owned(insp) => {
return insp.borrow_mut().log(evm_data, address, topics, data)
}
MaybeOwnedInspector::Stacked(_) => {}
}
}
fn step_end(&mut self, interp: &mut Interpreter<'_>, data: &mut EVMData<'_, DB>) {
match self {
MaybeOwnedInspector::Owned(insp) => insp.borrow_mut().step_end(interp, data),
MaybeOwnedInspector::Stacked(_) => {}
}
}
fn call(
&mut self,
data: &mut EVMData<'_, DB>,
inputs: &mut CallInputs,
) -> (InstructionResult, Gas, Bytes) {
match self {
MaybeOwnedInspector::Owned(insp) => return insp.borrow_mut().call(data, inputs),
MaybeOwnedInspector::Stacked(_) => {}
}
(InstructionResult::Continue, Gas::new(0), Bytes::new())
}
fn call_end(
&mut self,
data: &mut EVMData<'_, DB>,
inputs: &CallInputs,
remaining_gas: Gas,
ret: InstructionResult,
out: Bytes,
) -> (InstructionResult, Gas, Bytes) {
match self {
MaybeOwnedInspector::Owned(insp) => {
return insp.borrow_mut().call_end(data, inputs, remaining_gas, ret, out)
}
MaybeOwnedInspector::Stacked(_) => {}
}
(ret, remaining_gas, out)
}
fn create(
&mut self,
data: &mut EVMData<'_, DB>,
inputs: &mut CreateInputs,
) -> (InstructionResult, Option<Address>, Gas, Bytes) {
match self {
MaybeOwnedInspector::Owned(insp) => return insp.borrow_mut().create(data, inputs),
MaybeOwnedInspector::Stacked(_) => {}
}
(InstructionResult::Continue, None, Gas::new(0), Bytes::default())
}
fn create_end(
&mut self,
data: &mut EVMData<'_, DB>,
inputs: &CreateInputs,
ret: InstructionResult,
address: Option<Address>,
remaining_gas: Gas,
out: Bytes,
) -> (InstructionResult, Option<Address>, Gas, Bytes) {
match self {
MaybeOwnedInspector::Owned(insp) => {
return insp.borrow_mut().create_end(data, inputs, ret, address, remaining_gas, out)
}
MaybeOwnedInspector::Stacked(_) => {}
}
(ret, address, remaining_gas, out)
}
fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) {
match self {
MaybeOwnedInspector::Owned(insp) => {
return insp.borrow_mut().selfdestruct(contract, target, value)
}
MaybeOwnedInspector::Stacked(_) => {}
}
}
}

View File

@ -1,215 +0,0 @@
use alloy_primitives::{Address, Bytes, B256, U256};
use revm::{
inspectors::CustomPrintTracer,
interpreter::{CallInputs, CreateInputs, Gas, InstructionResult, Interpreter},
primitives::Env,
Database, EVMData, Inspector,
};
use std::fmt::Debug;
/// A wrapped [Inspector] that can be reused in the stack
mod maybe_owned;
pub use maybe_owned::MaybeOwnedInspector;
/// One can hook on inspector execution in 3 ways:
/// - Block: Hook on block execution
/// - BlockWithIndex: Hook on block execution transaction index
/// - Transaction: Hook on a specific transaction hash
#[derive(Debug, Default, Clone)]
pub enum Hook {
#[default]
/// No hook.
None,
/// Hook on a specific block.
Block(u64),
/// Hook on a specific transaction hash.
Transaction(B256),
/// Hooks on every transaction in a block.
All,
}
/// An inspector that calls multiple inspectors in sequence.
///
/// If a call to an inspector returns a value other than [InstructionResult::Continue] (or
/// equivalent) the remaining inspectors are not called.
#[derive(Default, Clone)]
pub struct InspectorStack {
/// An inspector that prints the opcode traces to the console.
pub custom_print_tracer: Option<CustomPrintTracer>,
/// The provided hook
pub hook: Hook,
}
impl Debug for InspectorStack {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InspectorStack")
.field("custom_print_tracer", &self.custom_print_tracer.is_some())
.field("hook", &self.hook)
.finish()
}
}
impl InspectorStack {
/// Create a new inspector stack.
pub fn new(config: InspectorStackConfig) -> Self {
let mut stack = InspectorStack { hook: config.hook, ..Default::default() };
if config.use_printer_tracer {
stack.custom_print_tracer = Some(CustomPrintTracer::default());
}
stack
}
/// Check if the inspector should be used.
pub fn should_inspect(&self, env: &Env, tx_hash: B256) -> bool {
match self.hook {
Hook::None => false,
Hook::Block(block) => env.block.number.to::<u64>() == block,
Hook::Transaction(hash) => hash == tx_hash,
Hook::All => true,
}
}
}
/// Configuration for the inspectors.
#[derive(Debug, Default)]
pub struct InspectorStackConfig {
/// Enable revm inspector printer.
/// In execution this will print opcode level traces directly to console.
pub use_printer_tracer: bool,
/// Hook on a specific block or transaction.
pub hook: Hook,
}
/// Helper macro to call the same method on multiple inspectors without resorting to dynamic
/// dispatch
#[macro_export]
macro_rules! call_inspectors {
($id:ident, [ $($inspector:expr),+ ], $call:block) => {
$({
if let Some($id) = $inspector {
$call;
}
})+
}
}
impl<DB> Inspector<DB> for InspectorStack
where
DB: Database,
{
fn initialize_interp(&mut self, interpreter: &mut Interpreter<'_>, data: &mut EVMData<'_, DB>) {
call_inspectors!(inspector, [&mut self.custom_print_tracer], {
inspector.initialize_interp(interpreter, data);
});
}
fn step(&mut self, interpreter: &mut Interpreter<'_>, data: &mut EVMData<'_, DB>) {
call_inspectors!(inspector, [&mut self.custom_print_tracer], {
inspector.step(interpreter, data);
});
}
fn log(
&mut self,
evm_data: &mut EVMData<'_, DB>,
address: &Address,
topics: &[B256],
data: &Bytes,
) {
call_inspectors!(inspector, [&mut self.custom_print_tracer], {
inspector.log(evm_data, address, topics, data);
});
}
fn step_end(&mut self, interpreter: &mut Interpreter<'_>, data: &mut EVMData<'_, DB>) {
call_inspectors!(inspector, [&mut self.custom_print_tracer], {
inspector.step_end(interpreter, data);
});
}
fn call(
&mut self,
data: &mut EVMData<'_, DB>,
inputs: &mut CallInputs,
) -> (InstructionResult, Gas, Bytes) {
call_inspectors!(inspector, [&mut self.custom_print_tracer], {
let (status, gas, retdata) = inspector.call(data, inputs);
// Allow inspectors to exit early
if status != InstructionResult::Continue {
return (status, gas, retdata)
}
});
(InstructionResult::Continue, Gas::new(inputs.gas_limit), Bytes::new())
}
fn call_end(
&mut self,
data: &mut EVMData<'_, DB>,
inputs: &CallInputs,
remaining_gas: Gas,
ret: InstructionResult,
out: Bytes,
) -> (InstructionResult, Gas, Bytes) {
call_inspectors!(inspector, [&mut self.custom_print_tracer], {
let (new_ret, new_gas, new_out) =
inspector.call_end(data, inputs, remaining_gas, ret, out.clone());
// If the inspector returns a different ret or a revert with a non-empty message,
// we assume it wants to tell us something
if new_ret != ret || (new_ret == InstructionResult::Revert && new_out != out) {
return (new_ret, new_gas, new_out)
}
});
(ret, remaining_gas, out)
}
fn create(
&mut self,
data: &mut EVMData<'_, DB>,
inputs: &mut CreateInputs,
) -> (InstructionResult, Option<Address>, Gas, Bytes) {
call_inspectors!(inspector, [&mut self.custom_print_tracer], {
let (status, addr, gas, retdata) = inspector.create(data, inputs);
// Allow inspectors to exit early
if status != InstructionResult::Continue {
return (status, addr, gas, retdata)
}
});
(InstructionResult::Continue, None, Gas::new(inputs.gas_limit), Bytes::new())
}
fn create_end(
&mut self,
data: &mut EVMData<'_, DB>,
inputs: &CreateInputs,
ret: InstructionResult,
address: Option<Address>,
remaining_gas: Gas,
out: Bytes,
) -> (InstructionResult, Option<Address>, Gas, Bytes) {
call_inspectors!(inspector, [&mut self.custom_print_tracer], {
let (new_ret, new_address, new_gas, new_retdata) =
inspector.create_end(data, inputs, ret, address, remaining_gas, out.clone());
if new_ret != ret {
return (new_ret, new_address, new_gas, new_retdata)
}
});
(ret, address, remaining_gas, out)
}
fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) {
call_inspectors!(inspector, [&mut self.custom_print_tracer], {
Inspector::<DB>::selfdestruct(inspector, contract, target, value);
});
}
}

View File

@ -1,92 +0,0 @@
use crate::tracing::types::{CallTrace, CallTraceNode, LogCallOrder};
/// An arena of recorded traces.
///
/// This type will be populated via the [TracingInspector](crate::tracing::TracingInspector).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CallTraceArena {
/// The arena of recorded trace nodes
pub(crate) arena: Vec<CallTraceNode>,
}
impl CallTraceArena {
/// Pushes a new trace into the arena, returning the trace ID
///
/// This appends a new trace to the arena, and also inserts a new entry in the node's parent
/// node children set if `attach_to_parent` is `true`. E.g. if calls to precompiles should
/// not be included in the call graph this should be called with [PushTraceKind::PushOnly].
pub(crate) fn push_trace(
&mut self,
mut entry: usize,
kind: PushTraceKind,
new_trace: CallTrace,
) -> usize {
loop {
match new_trace.depth {
// The entry node, just update it
0 => {
self.arena[0].trace = new_trace;
return 0
}
// We found the parent node, add the new trace as a child
_ if self.arena[entry].trace.depth == new_trace.depth - 1 => {
let id = self.arena.len();
let node = CallTraceNode {
parent: Some(entry),
trace: new_trace,
idx: id,
..Default::default()
};
self.arena.push(node);
// also track the child in the parent node
if kind.is_attach_to_parent() {
let parent = &mut self.arena[entry];
let trace_location = parent.children.len();
parent.ordering.push(LogCallOrder::Call(trace_location));
parent.children.push(id);
}
return id
}
_ => {
// We haven't found the parent node, go deeper
entry = *self.arena[entry].children.last().expect("Disconnected trace");
}
}
}
}
/// Returns the nodes in the arena
pub fn nodes(&self) -> &[CallTraceNode] {
&self.arena
}
/// Consumes the arena and returns the nodes
pub fn into_nodes(self) -> Vec<CallTraceNode> {
self.arena
}
}
/// How to push a trace into the arena
pub(crate) enum PushTraceKind {
/// This will _only_ push the trace into the arena.
PushOnly,
/// This will push the trace into the arena, and also insert a new entry in the node's parent
/// node children set.
PushAndAttachToParent,
}
impl PushTraceKind {
#[inline]
fn is_attach_to_parent(&self) -> bool {
matches!(self, PushTraceKind::PushAndAttachToParent)
}
}
impl Default for CallTraceArena {
fn default() -> Self {
// The first node is the root node
CallTraceArena { arena: vec![Default::default()] }
}
}

View File

@ -1,325 +0,0 @@
//! Geth trace builder
use crate::tracing::{
types::{CallTraceNode, CallTraceStepStackItem},
utils::load_account_code,
TracingInspectorConfig,
};
use alloy_primitives::{Address, Bytes, B256, U256};
use alloy_rpc_trace_types::geth::{
AccountChangeKind, AccountState, CallConfig, CallFrame, DefaultFrame, DiffMode,
GethDefaultTracingOptions, PreStateConfig, PreStateFrame, PreStateMode, StructLog,
};
use revm::{db::DatabaseRef, primitives::ResultAndState};
use std::collections::{btree_map::Entry, BTreeMap, HashMap, VecDeque};
/// A type for creating geth style traces
#[derive(Clone, Debug)]
pub struct GethTraceBuilder {
/// Recorded trace nodes.
nodes: Vec<CallTraceNode>,
/// How the traces were recorded
_config: TracingInspectorConfig,
}
impl GethTraceBuilder {
/// Returns a new instance of the builder
pub fn new(nodes: Vec<CallTraceNode>, _config: TracingInspectorConfig) -> Self {
Self { nodes, _config }
}
/// Fill in the geth trace with all steps of the trace and its children traces in the order they
/// appear in the transaction.
fn fill_geth_trace(
&self,
main_trace_node: &CallTraceNode,
opts: &GethDefaultTracingOptions,
storage: &mut HashMap<Address, BTreeMap<B256, B256>>,
struct_logs: &mut Vec<StructLog>,
) {
// A stack with all the steps of the trace and all its children's steps.
// This is used to process the steps in the order they appear in the transactions.
// Steps are grouped by their Call Trace Node, in order to process them all in the order
// they appear in the transaction, we need to process steps of call nodes when they appear.
// When we find a call step, we push all the steps of the child trace on the stack, so they
// are processed next. The very next step is the last item on the stack
let mut step_stack = VecDeque::with_capacity(main_trace_node.trace.steps.len());
main_trace_node.push_steps_on_stack(&mut step_stack);
// Iterate over the steps inside the given trace
while let Some(CallTraceStepStackItem { trace_node, step, call_child_id }) =
step_stack.pop_back()
{
let mut log = step.convert_to_geth_struct_log(opts);
// Fill in memory and storage depending on the options
if opts.is_storage_enabled() {
let contract_storage = storage.entry(step.contract).or_default();
if let Some(change) = step.storage_change {
contract_storage.insert(change.key.into(), change.value.into());
log.storage = Some(contract_storage.clone());
}
}
if opts.is_return_data_enabled() {
log.return_data = Some(trace_node.trace.output.clone());
}
// Add step to geth trace
struct_logs.push(log);
// If the step is a call, we first push all the steps of the child trace on the stack,
// so they are processed next
if let Some(call_child_id) = call_child_id {
let child_trace = &self.nodes[call_child_id];
child_trace.push_steps_on_stack(&mut step_stack);
}
}
}
/// Generate a geth-style trace e.g. for `debug_traceTransaction`
///
/// This expects the gas used and return value for the
/// [ExecutionResult](revm::primitives::ExecutionResult) of the executed transaction.
pub fn geth_traces(
&self,
receipt_gas_used: u64,
return_value: Bytes,
opts: GethDefaultTracingOptions,
) -> DefaultFrame {
if self.nodes.is_empty() {
return Default::default()
}
// Fetch top-level trace
let main_trace_node = &self.nodes[0];
let main_trace = &main_trace_node.trace;
let mut struct_logs = Vec::new();
let mut storage = HashMap::new();
self.fill_geth_trace(main_trace_node, &opts, &mut storage, &mut struct_logs);
DefaultFrame {
// If the top-level trace succeeded, then it was a success
failed: !main_trace.success,
gas: receipt_gas_used,
return_value,
struct_logs,
}
}
/// Generate a geth-style traces for the call tracer.
///
/// This decodes all call frames from the recorded traces.
///
/// This expects the gas used and return value for the
/// [ExecutionResult](revm::primitives::ExecutionResult) of the executed transaction.
pub fn geth_call_traces(&self, opts: CallConfig, gas_used: u64) -> CallFrame {
if self.nodes.is_empty() {
return Default::default()
}
let include_logs = opts.with_log.unwrap_or_default();
// first fill up the root
let main_trace_node = &self.nodes[0];
let mut root_call_frame = main_trace_node.geth_empty_call_frame(include_logs);
root_call_frame.gas_used = U256::from(gas_used);
// selfdestructs are not recorded as individual call traces but are derived from
// the call trace and are added as additional `CallFrame` objects to the parent call
if let Some(selfdestruct) = main_trace_node.geth_selfdestruct_call_trace() {
root_call_frame.calls.push(selfdestruct);
}
if opts.only_top_call.unwrap_or_default() {
return root_call_frame
}
// fill all the call frames in the root call frame with the recorded traces.
// traces are identified by their index in the arena
// so we can populate the call frame tree by walking up the call tree
let mut call_frames = Vec::with_capacity(self.nodes.len());
call_frames.push((0, root_call_frame));
for (idx, trace) in self.nodes.iter().enumerate().skip(1) {
// selfdestructs are not recorded as individual call traces but are derived from
// the call trace and are added as additional `CallFrame` objects to the parent call
if let Some(selfdestruct) = trace.geth_selfdestruct_call_trace() {
call_frames.last_mut().expect("not empty").1.calls.push(selfdestruct);
}
call_frames.push((idx, trace.geth_empty_call_frame(include_logs)));
}
// pop the _children_ calls frame and move it to the parent
// this will roll up the child frames to their parent; this works because `child idx >
// parent idx`
loop {
let (idx, call) = call_frames.pop().expect("call frames not empty");
let node = &self.nodes[idx];
if let Some(parent) = node.parent {
let parent_frame = &mut call_frames[parent];
// we need to ensure that calls are in order they are called: the last child node is
// the last call, but since we walk up the tree, we need to always
// insert at position 0
parent_frame.1.calls.insert(0, call);
} else {
debug_assert!(call_frames.is_empty(), "only one root node has no parent");
return call
}
}
}
/// Returns the accounts necessary for transaction execution.
///
/// The prestate mode returns the accounts necessary to execute a given transaction.
/// diff_mode returns the differences between the transaction's pre and post-state.
///
/// * `state` - The state post-transaction execution.
/// * `diff_mode` - if prestate is in diff or prestate mode.
/// * `db` - The database to fetch state pre-transaction execution.
pub fn geth_prestate_traces<DB: DatabaseRef>(
&self,
ResultAndState { state, .. }: &ResultAndState,
prestate_config: PreStateConfig,
db: DB,
) -> Result<PreStateFrame, DB::Error> {
let account_diffs = state.into_iter().map(|(addr, acc)| (*addr, acc));
if prestate_config.is_default_mode() {
let mut prestate = PreStateMode::default();
// in default mode we __only__ return the touched state
for node in self.nodes.iter() {
let addr = node.trace.address;
let acc_state = match prestate.0.entry(addr) {
Entry::Vacant(entry) => {
let db_acc = db.basic_ref(addr)?.unwrap_or_default();
let code = load_account_code(&db, &db_acc);
let acc_state =
AccountState::from_account_info(db_acc.nonce, db_acc.balance, code);
entry.insert(acc_state)
}
Entry::Occupied(entry) => entry.into_mut(),
};
for (key, value) in node.touched_slots() {
match acc_state.storage.entry(key.into()) {
Entry::Vacant(entry) => {
entry.insert(value.into());
}
Entry::Occupied(_) => {
// we've already recorded this slot
}
}
}
}
// also need to check changed accounts for things like balance changes etc
for (addr, changed_acc) in account_diffs {
let acc_state = match prestate.0.entry(addr) {
Entry::Vacant(entry) => {
let db_acc = db.basic_ref(addr)?.unwrap_or_default();
let code = load_account_code(&db, &db_acc);
let acc_state =
AccountState::from_account_info(db_acc.nonce, db_acc.balance, code);
entry.insert(acc_state)
}
Entry::Occupied(entry) => {
// already recorded via touched accounts
entry.into_mut()
}
};
// in case we missed anything during the trace, we need to add the changed accounts
// storage
for (key, slot) in changed_acc.storage.iter() {
match acc_state.storage.entry((*key).into()) {
Entry::Vacant(entry) => {
entry.insert(slot.previous_or_original_value.into());
}
Entry::Occupied(_) => {
// we've already recorded this slot
}
}
}
}
Ok(PreStateFrame::Default(prestate))
} else {
let mut state_diff = DiffMode::default();
let mut account_change_kinds = HashMap::with_capacity(account_diffs.len());
for (addr, changed_acc) in account_diffs {
let db_acc = db.basic_ref(addr)?.unwrap_or_default();
let pre_code = load_account_code(&db, &db_acc);
let mut pre_state =
AccountState::from_account_info(db_acc.nonce, db_acc.balance, pre_code);
let mut post_state = AccountState::from_account_info(
changed_acc.info.nonce,
changed_acc.info.balance,
changed_acc.info.code.as_ref().map(|code| code.original_bytes()),
);
// handle storage changes
for (key, slot) in changed_acc.storage.iter().filter(|(_, slot)| slot.is_changed())
{
pre_state.storage.insert((*key).into(), slot.previous_or_original_value.into());
post_state.storage.insert((*key).into(), slot.present_value.into());
}
state_diff.pre.insert(addr, pre_state);
state_diff.post.insert(addr, post_state);
// determine the change type
let pre_change = if changed_acc.is_created() {
AccountChangeKind::Create
} else {
AccountChangeKind::Modify
};
let post_change = if changed_acc.is_selfdestructed() {
AccountChangeKind::SelfDestruct
} else {
AccountChangeKind::Modify
};
account_change_kinds.insert(addr, (pre_change, post_change));
}
// ensure we're only keeping changed entries
state_diff.retain_changed().remove_zero_storage_values();
self.diff_traces(&mut state_diff.pre, &mut state_diff.post, account_change_kinds);
Ok(PreStateFrame::Diff(state_diff))
}
}
/// Returns the difference between the pre and post state of the transaction depending on the
/// kind of changes of that account (pre,post)
fn diff_traces(
&self,
pre: &mut BTreeMap<Address, AccountState>,
post: &mut BTreeMap<Address, AccountState>,
change_type: HashMap<Address, (AccountChangeKind, AccountChangeKind)>,
) {
post.retain(|addr, post_state| {
// Don't keep destroyed accounts in the post state
if change_type.get(addr).map(|ty| ty.1.is_selfdestruct()).unwrap_or(false) {
return false
}
if let Some(pre_state) = pre.get(addr) {
// remove any unchanged account info
post_state.remove_matching_account_info(pre_state);
}
true
});
// Don't keep created accounts the pre state
pre.retain(|addr, _pre_state| {
// only keep accounts that are not created
change_type.get(addr).map(|ty| !ty.0.is_created()).unwrap_or(true)
});
}
}

View File

@ -1,10 +0,0 @@
//! Builder types for building traces
/// Geth style trace builders for `debug_` namespace
pub mod geth;
/// Parity style trace builders for `trace_` namespace
pub mod parity;
/// Walker types used for traversing various callgraphs
mod walker;

View File

@ -1,633 +0,0 @@
use super::walker::CallTraceNodeWalkerBF;
use crate::tracing::{
types::{CallTraceNode, CallTraceStep},
utils::load_account_code,
TracingInspectorConfig,
};
use alloy_primitives::{Address, U64};
use alloy_rpc_trace_types::parity::*;
use alloy_rpc_types::TransactionInfo;
use revm::{
db::DatabaseRef,
interpreter::{
opcode::{self, spec_opcode_gas},
OpCode,
},
primitives::{Account, ExecutionResult, ResultAndState, SpecId, KECCAK_EMPTY},
};
use std::collections::{HashSet, VecDeque};
/// A type for creating parity style traces
///
/// Note: Parity style traces always ignore calls to precompiles.
#[derive(Clone, Debug)]
pub struct ParityTraceBuilder {
/// Recorded trace nodes
nodes: Vec<CallTraceNode>,
/// The spec id of the EVM.
spec_id: Option<SpecId>,
/// How the traces were recorded
_config: TracingInspectorConfig,
}
impl ParityTraceBuilder {
/// Returns a new instance of the builder
pub fn new(
nodes: Vec<CallTraceNode>,
spec_id: Option<SpecId>,
_config: TracingInspectorConfig,
) -> Self {
Self { nodes, spec_id, _config }
}
/// Returns a list of all addresses that appeared as callers.
pub fn callers(&self) -> HashSet<Address> {
self.nodes.iter().map(|node| node.trace.caller).collect()
}
/// Manually the gas used of the root trace.
///
/// The root trace's gasUsed should mirror the actual gas used by the transaction.
///
/// This allows setting it manually by consuming the execution result's gas for example.
#[inline]
pub fn set_transaction_gas_used(&mut self, gas_used: u64) {
if let Some(node) = self.nodes.first_mut() {
node.trace.gas_used = gas_used;
}
}
/// Convenience function for [ParityTraceBuilder::set_transaction_gas_used] that consumes the
/// type.
#[inline]
pub fn with_transaction_gas_used(mut self, gas_used: u64) -> Self {
self.set_transaction_gas_used(gas_used);
self
}
/// Returns the trace addresses of all call nodes in the set
///
/// Each entry in the returned vector represents the [Self::trace_address] of the corresponding
/// node in the nodes set.
///
/// CAUTION: This also includes precompiles, which have an empty trace address.
fn trace_addresses(&self) -> Vec<Vec<usize>> {
let mut all_addresses = Vec::with_capacity(self.nodes.len());
for idx in 0..self.nodes.len() {
all_addresses.push(self.trace_address(idx));
}
all_addresses
}
/// Returns the `traceAddress` of the node in the arena
///
/// The `traceAddress` field of all returned traces, gives the exact location in the call trace
/// [index in root, index in first CALL, index in second CALL, …].
///
/// # Panics
///
/// if the `idx` does not belong to a node
///
/// Note: if the call node of `idx` is a precompile, the returned trace address will be empty.
fn trace_address(&self, idx: usize) -> Vec<usize> {
if idx == 0 {
// root call has empty traceAddress
return vec![]
}
let mut graph = vec![];
let mut node = &self.nodes[idx];
if node.is_precompile() {
return graph
}
while let Some(parent) = node.parent {
// the index of the child call in the arena
let child_idx = node.idx;
node = &self.nodes[parent];
// find the index of the child call in the parent node
let call_idx = node
.children
.iter()
.position(|child| *child == child_idx)
.expect("non precompile child call exists in parent");
graph.push(call_idx);
}
graph.reverse();
graph
}
/// Returns an iterator over all nodes to trace
///
/// This excludes nodes that represent calls to precompiles.
fn iter_traceable_nodes(&self) -> impl Iterator<Item = &CallTraceNode> {
self.nodes.iter().filter(|node| !node.is_precompile())
}
/// Returns an iterator over all recorded traces for `trace_transaction`
pub fn into_localized_transaction_traces_iter(
self,
info: TransactionInfo,
) -> impl Iterator<Item = LocalizedTransactionTrace> {
self.into_transaction_traces_iter().map(move |trace| {
let TransactionInfo { hash, index, block_hash, block_number, .. } = info;
LocalizedTransactionTrace {
trace,
transaction_position: index,
transaction_hash: hash,
block_number,
block_hash,
}
})
}
/// Returns all recorded traces for `trace_transaction`
pub fn into_localized_transaction_traces(
self,
info: TransactionInfo,
) -> Vec<LocalizedTransactionTrace> {
self.into_localized_transaction_traces_iter(info).collect()
}
/// Consumes the inspector and returns the trace results according to the configured trace
/// types.
///
/// Warning: If `trace_types` contains [TraceType::StateDiff] the returned [StateDiff] will not
/// be filled. Use [ParityTraceBuilder::into_trace_results_with_state] or
/// [populate_state_diff] to populate the balance and nonce changes for the [StateDiff]
/// using the [DatabaseRef].
pub fn into_trace_results(
self,
res: &ExecutionResult,
trace_types: &HashSet<TraceType>,
) -> TraceResults {
let gas_used = res.gas_used();
let output = res.output().cloned().unwrap_or_default();
let (trace, vm_trace, state_diff) = self.into_trace_type_traces(trace_types);
let mut trace =
TraceResults { output, trace: trace.unwrap_or_default(), vm_trace, state_diff };
// we're setting the gas used of the root trace explicitly to the gas used of the execution
// result
trace.set_root_trace_gas_used(gas_used);
trace
}
/// Consumes the inspector and returns the trace results according to the configured trace
/// types.
///
/// This also takes the [DatabaseRef] to populate the balance and nonce changes for the
/// [StateDiff].
///
/// Note: this is considered a convenience method that takes the state map of
/// [ResultAndState] after inspecting a transaction
/// with the [TracingInspector](crate::tracing::TracingInspector).
pub fn into_trace_results_with_state<DB: DatabaseRef>(
self,
res: &ResultAndState,
trace_types: &HashSet<TraceType>,
db: DB,
) -> Result<TraceResults, DB::Error> {
let ResultAndState { ref result, ref state } = res;
let breadth_first_addresses = if trace_types.contains(&TraceType::VmTrace) {
CallTraceNodeWalkerBF::new(&self.nodes)
.map(|node| node.trace.address)
.collect::<Vec<_>>()
} else {
vec![]
};
let mut trace_res = self.into_trace_results(result, trace_types);
// check the state diff case
if let Some(ref mut state_diff) = trace_res.state_diff {
populate_state_diff(state_diff, &db, state.iter())?;
}
// check the vm trace case
if let Some(ref mut vm_trace) = trace_res.vm_trace {
populate_vm_trace_bytecodes(&db, vm_trace, breadth_first_addresses)?;
}
Ok(trace_res)
}
/// Returns the tracing types that are configured in the set.
///
/// Warning: if [TraceType::StateDiff] is provided this does __not__ fill the state diff, since
/// this requires access to the account diffs.
///
/// See [Self::into_trace_results_with_state] and [populate_state_diff].
pub fn into_trace_type_traces(
self,
trace_types: &HashSet<TraceType>,
) -> (Option<Vec<TransactionTrace>>, Option<VmTrace>, Option<StateDiff>) {
if trace_types.is_empty() || self.nodes.is_empty() {
return (None, None, None)
}
let with_traces = trace_types.contains(&TraceType::Trace);
let with_diff = trace_types.contains(&TraceType::StateDiff);
let vm_trace =
if trace_types.contains(&TraceType::VmTrace) { Some(self.vm_trace()) } else { None };
let mut traces = Vec::with_capacity(if with_traces { self.nodes.len() } else { 0 });
for node in self.iter_traceable_nodes() {
let trace_address = self.trace_address(node.idx);
if with_traces {
let trace = node.parity_transaction_trace(trace_address);
traces.push(trace);
// check if the trace node is a selfdestruct
if node.is_selfdestruct() {
// selfdestructs are not recorded as individual call traces but are derived from
// the call trace and are added as additional `TransactionTrace` objects in the
// trace array
let addr = {
let last = traces.last_mut().expect("exists");
let mut addr = last.trace_address.clone();
addr.push(last.subtraces);
// need to account for the additional selfdestruct trace
last.subtraces += 1;
addr
};
if let Some(trace) = node.parity_selfdestruct_trace(addr) {
traces.push(trace);
}
}
}
}
let traces = with_traces.then_some(traces);
let diff = with_diff.then_some(StateDiff::default());
(traces, vm_trace, diff)
}
/// Returns an iterator over all recorded traces for `trace_transaction`
pub fn into_transaction_traces_iter(self) -> impl Iterator<Item = TransactionTrace> {
let trace_addresses = self.trace_addresses();
TransactionTraceIter {
next_selfdestruct: None,
iter: self
.nodes
.into_iter()
.zip(trace_addresses)
.filter(|(node, _)| !node.is_precompile())
.map(|(node, trace_address)| (node.parity_transaction_trace(trace_address), node)),
}
}
/// Returns the raw traces of the transaction
pub fn into_transaction_traces(self) -> Vec<TransactionTrace> {
self.into_transaction_traces_iter().collect()
}
/// Returns the last recorded step
#[inline]
fn last_step(&self) -> Option<&CallTraceStep> {
self.nodes.last().and_then(|node| node.trace.steps.last())
}
/// Returns true if the last recorded step is a STOP
#[inline]
fn is_last_step_stop_op(&self) -> bool {
self.last_step().map(|step| step.is_stop()).unwrap_or(false)
}
/// Creates a VM trace by walking over `CallTraceNode`s
///
/// does not have the code fields filled in
pub fn vm_trace(&self) -> VmTrace {
self.nodes.first().map(|node| self.make_vm_trace(node)).unwrap_or_default()
}
/// Returns a VM trace without the code filled in
///
/// Iteratively creates a VM trace by traversing the recorded nodes in the arena
fn make_vm_trace(&self, start: &CallTraceNode) -> VmTrace {
let mut child_idx_stack = Vec::with_capacity(self.nodes.len());
let mut sub_stack = VecDeque::with_capacity(self.nodes.len());
let mut current = start;
let mut child_idx: usize = 0;
// finds the deepest nested calls of each call frame and fills them up bottom to top
let instructions = 'outer: loop {
match current.children.get(child_idx) {
Some(child) => {
child_idx_stack.push(child_idx + 1);
child_idx = 0;
current = self.nodes.get(*child).expect("there should be a child");
}
None => {
let mut instructions = Vec::with_capacity(current.trace.steps.len());
for step in &current.trace.steps {
let maybe_sub_call = if step.is_calllike_op() {
sub_stack.pop_front().flatten()
} else {
None
};
if step.is_stop() && instructions.is_empty() && self.is_last_step_stop_op()
{
// This is a special case where there's a single STOP which is
// "optimised away", transfers for example
break 'outer instructions
}
instructions.push(self.make_instruction(step, maybe_sub_call));
}
match current.parent {
Some(parent) => {
sub_stack.push_back(Some(VmTrace {
code: Default::default(),
ops: instructions,
}));
child_idx = child_idx_stack.pop().expect("there should be a child idx");
current = self.nodes.get(parent).expect("there should be a parent");
}
None => break instructions,
}
}
}
};
VmTrace { code: Default::default(), ops: instructions }
}
/// Creates a VM instruction from a [CallTraceStep] and a [VmTrace] for the subcall if there is
/// one
fn make_instruction(
&self,
step: &CallTraceStep,
maybe_sub_call: Option<VmTrace>,
) -> VmInstruction {
let maybe_storage = step.storage_change.map(|storage_change| StorageDelta {
key: storage_change.key,
val: storage_change.value,
});
let maybe_memory = if step.memory.is_empty() {
None
} else {
Some(MemoryDelta {
off: step.memory_size,
data: step.memory.as_bytes().to_vec().into(),
})
};
let maybe_execution = Some(VmExecutedOperation {
used: step.gas_remaining,
push: step.push_stack.clone().unwrap_or_default(),
mem: maybe_memory,
store: maybe_storage,
});
let cost = self
.spec_id
.and_then(|spec_id| {
spec_opcode_gas(spec_id).get(step.op.get() as usize).map(|op| op.get_gas())
})
.unwrap_or_default();
VmInstruction {
pc: step.pc,
cost: cost as u64,
ex: maybe_execution,
sub: maybe_sub_call,
op: Some(step.op.to_string()),
idx: None,
}
}
}
/// An iterator for [TransactionTrace]s
struct TransactionTraceIter<Iter> {
iter: Iter,
next_selfdestruct: Option<TransactionTrace>,
}
impl<Iter> Iterator for TransactionTraceIter<Iter>
where
Iter: Iterator<Item = (TransactionTrace, CallTraceNode)>,
{
type Item = TransactionTrace;
fn next(&mut self) -> Option<Self::Item> {
if let Some(selfdestruct) = self.next_selfdestruct.take() {
return Some(selfdestruct)
}
let (mut trace, node) = self.iter.next()?;
if node.is_selfdestruct() {
// since selfdestructs are emitted as additional trace, increase the trace count
let mut addr = trace.trace_address.clone();
addr.push(trace.subtraces);
// need to account for the additional selfdestruct trace
trace.subtraces += 1;
self.next_selfdestruct = node.parity_selfdestruct_trace(addr);
}
Some(trace)
}
}
/// addresses are presorted via breadth first walk thru [CallTraceNode]s, this can be done by a
/// walker in [crate::tracing::builder::walker]
///
/// iteratively fill the [VmTrace] code fields
pub(crate) fn populate_vm_trace_bytecodes<DB, I>(
db: DB,
trace: &mut VmTrace,
breadth_first_addresses: I,
) -> Result<(), DB::Error>
where
DB: DatabaseRef,
I: IntoIterator<Item = Address>,
{
let mut stack: VecDeque<&mut VmTrace> = VecDeque::new();
stack.push_back(trace);
let mut addrs = breadth_first_addresses.into_iter();
while let Some(curr_ref) = stack.pop_front() {
for op in curr_ref.ops.iter_mut() {
if let Some(sub) = op.sub.as_mut() {
stack.push_back(sub);
}
}
let addr = addrs.next().expect("there should be an address");
let db_acc = db.basic_ref(addr)?.unwrap_or_default();
let code_hash = if db_acc.code_hash != KECCAK_EMPTY { db_acc.code_hash } else { continue };
curr_ref.code = db.code_by_hash_ref(code_hash)?.original_bytes();
}
Ok(())
}
/// Loops over all state accounts in the accounts diff that contains all accounts that are included
/// in the [ExecutionResult] state map and compares the balance and nonce against what's in the
/// `db`, which should point to the beginning of the transaction.
///
/// It's expected that `DB` is a revm [Database](revm::db::Database) which at this point already
/// contains all the accounts that are in the state map and never has to fetch them from disk.
pub fn populate_state_diff<'a, DB, I>(
state_diff: &mut StateDiff,
db: DB,
account_diffs: I,
) -> Result<(), DB::Error>
where
I: IntoIterator<Item = (&'a Address, &'a Account)>,
DB: DatabaseRef,
{
for (addr, changed_acc) in account_diffs.into_iter() {
// if the account was selfdestructed and created during the transaction, we can ignore it
if changed_acc.is_selfdestructed() && changed_acc.is_created() {
continue
}
let addr = *addr;
let entry = state_diff.entry(addr).or_default();
// we check if this account was created during the transaction
if changed_acc.is_created() || changed_acc.is_loaded_as_not_existing() {
entry.balance = Delta::Added(changed_acc.info.balance);
entry.nonce = Delta::Added(U64::from(changed_acc.info.nonce));
// accounts without code are marked as added
let account_code = load_account_code(&db, &changed_acc.info).unwrap_or_default();
entry.code = Delta::Added(account_code);
// new storage values are marked as added,
// however we're filtering changed here to avoid adding entries for the zero value
for (key, slot) in changed_acc.storage.iter().filter(|(_, slot)| slot.is_changed()) {
entry.storage.insert((*key).into(), Delta::Added(slot.present_value.into()));
}
} else {
// account already exists, we need to fetch the account from the db
let db_acc = db.basic_ref(addr)?.unwrap_or_default();
// update _changed_ storage values
for (key, slot) in changed_acc.storage.iter().filter(|(_, slot)| slot.is_changed()) {
entry.storage.insert(
(*key).into(),
Delta::changed(
slot.previous_or_original_value.into(),
slot.present_value.into(),
),
);
}
// check if the account was changed at all
if entry.storage.is_empty() &&
db_acc == changed_acc.info &&
!changed_acc.is_selfdestructed()
{
// clear the entry if the account was not changed
state_diff.remove(&addr);
continue
}
entry.balance = if db_acc.balance == changed_acc.info.balance {
Delta::Unchanged
} else {
Delta::Changed(ChangedType { from: db_acc.balance, to: changed_acc.info.balance })
};
// this is relevant for the caller and contracts
entry.nonce = if db_acc.nonce == changed_acc.info.nonce {
Delta::Unchanged
} else {
Delta::Changed(ChangedType {
from: U64::from(db_acc.nonce),
to: U64::from(changed_acc.info.nonce),
})
};
}
}
Ok(())
}
/// Returns the number of items pushed on the stack by a given opcode.
/// This used to determine how many stack etries to put in the `push` element
/// in a parity vmTrace.
/// The value is obvious for most opcodes, but SWAP* and DUP* are a bit weird,
/// and we handle those as they are handled in parity vmtraces.
/// For reference: <https://github.com/ledgerwatch/erigon/blob/9b74cf0384385817459f88250d1d9c459a18eab1/turbo/jsonrpc/trace_adhoc.go#L451>
pub(crate) fn stack_push_count(step_op: OpCode) -> usize {
let step_op = step_op.get();
match step_op {
opcode::PUSH0..=opcode::PUSH32 => 1,
opcode::SWAP1..=opcode::SWAP16 => (step_op - opcode::SWAP1) as usize + 2,
opcode::DUP1..=opcode::DUP16 => (step_op - opcode::DUP1) as usize + 2,
opcode::CALLDATALOAD |
opcode::SLOAD |
opcode::MLOAD |
opcode::CALLDATASIZE |
opcode::LT |
opcode::GT |
opcode::DIV |
opcode::SDIV |
opcode::SAR |
opcode::AND |
opcode::EQ |
opcode::CALLVALUE |
opcode::ISZERO |
opcode::ADD |
opcode::EXP |
opcode::CALLER |
opcode::KECCAK256 |
opcode::SUB |
opcode::ADDRESS |
opcode::GAS |
opcode::MUL |
opcode::RETURNDATASIZE |
opcode::NOT |
opcode::SHR |
opcode::SHL |
opcode::EXTCODESIZE |
opcode::SLT |
opcode::OR |
opcode::NUMBER |
opcode::PC |
opcode::TIMESTAMP |
opcode::BALANCE |
opcode::SELFBALANCE |
opcode::MULMOD |
opcode::ADDMOD |
opcode::BASEFEE |
opcode::BLOCKHASH |
opcode::BYTE |
opcode::XOR |
opcode::ORIGIN |
opcode::CODESIZE |
opcode::MOD |
opcode::SIGNEXTEND |
opcode::GASLIMIT |
opcode::DIFFICULTY |
opcode::SGT |
opcode::GASPRICE |
opcode::MSIZE |
opcode::EXTCODEHASH |
opcode::SMOD |
opcode::CHAINID |
opcode::COINBASE => 1,
_ => 0,
}
}

View File

@ -1,39 +0,0 @@
use crate::tracing::types::CallTraceNode;
use std::collections::VecDeque;
/// Traverses Reths internal tracing structure breadth-first
///
/// This is a lazy iterator
pub(crate) struct CallTraceNodeWalkerBF<'trace> {
/// the entire arena
nodes: &'trace Vec<CallTraceNode>,
/// holds indexes of nodes to visit as we traverse
queue: VecDeque<usize>,
}
impl<'trace> CallTraceNodeWalkerBF<'trace> {
pub(crate) fn new(nodes: &'trace Vec<CallTraceNode>) -> Self {
let mut queue = VecDeque::with_capacity(nodes.len());
queue.push_back(0);
Self { nodes, queue }
}
}
impl<'trace> Iterator for CallTraceNodeWalkerBF<'trace> {
type Item = &'trace CallTraceNode;
fn next(&mut self) -> Option<Self::Item> {
match self.queue.pop_front() {
Some(idx) => {
let curr = self.nodes.get(idx).expect("there should be a node");
self.queue.extend(curr.children.iter());
Some(curr)
}
None => None,
}
}
}

View File

@ -1,225 +0,0 @@
use alloy_rpc_trace_types::{geth::GethDefaultTracingOptions, parity::TraceType};
use std::collections::HashSet;
/// Gives guidance to the [TracingInspector](crate::tracing::TracingInspector).
///
/// Use [TracingInspectorConfig::default_parity] or [TracingInspectorConfig::default_geth] to get
/// the default configs for specific styles of traces.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct TracingInspectorConfig {
/// Whether to record every individual opcode level step.
pub record_steps: bool,
/// Whether to record individual memory snapshots.
pub record_memory_snapshots: bool,
/// Whether to record individual stack snapshots.
pub record_stack_snapshots: StackSnapshotType,
/// Whether to record state diffs.
pub record_state_diff: bool,
/// Whether to ignore precompile calls.
pub exclude_precompile_calls: bool,
/// Whether to record individual return data
pub record_call_return_data: bool,
/// Whether to record logs
pub record_logs: bool,
}
impl TracingInspectorConfig {
/// Returns a config with everything enabled.
pub const fn all() -> Self {
Self {
record_steps: true,
record_memory_snapshots: true,
record_stack_snapshots: StackSnapshotType::Full,
record_state_diff: false,
exclude_precompile_calls: false,
record_call_return_data: false,
record_logs: true,
}
}
/// Returns a config for parity style traces.
///
/// This config does _not_ record opcode level traces and is suited for `trace_transaction`
pub const fn default_parity() -> Self {
Self {
record_steps: false,
record_memory_snapshots: false,
record_stack_snapshots: StackSnapshotType::None,
record_state_diff: false,
exclude_precompile_calls: true,
record_call_return_data: false,
record_logs: false,
}
}
/// Returns a config for geth style traces.
///
/// This config does _not_ record opcode level traces and is suited for `debug_traceTransaction`
pub const fn default_geth() -> Self {
Self {
record_steps: true,
record_memory_snapshots: true,
record_stack_snapshots: StackSnapshotType::Full,
record_state_diff: true,
exclude_precompile_calls: false,
record_call_return_data: false,
record_logs: false,
}
}
/// Returns the [TracingInspectorConfig] depending on the enabled [TraceType]s
///
/// Note: the parity statediffs can be populated entirely via the execution result, so we don't
/// need statediff recording
#[inline]
pub fn from_parity_config(trace_types: &HashSet<TraceType>) -> Self {
let needs_vm_trace = trace_types.contains(&TraceType::VmTrace);
let snap_type =
if needs_vm_trace { StackSnapshotType::Pushes } else { StackSnapshotType::None };
TracingInspectorConfig::default_parity()
.set_steps(needs_vm_trace)
.set_stack_snapshots(snap_type)
.set_memory_snapshots(needs_vm_trace)
}
/// Returns a config for geth style traces based on the given [GethDefaultTracingOptions].
#[inline]
pub fn from_geth_config(config: &GethDefaultTracingOptions) -> Self {
Self {
record_memory_snapshots: config.enable_memory.unwrap_or_default(),
record_stack_snapshots: if config.disable_stack.unwrap_or_default() {
StackSnapshotType::None
} else {
StackSnapshotType::Full
},
record_state_diff: !config.disable_storage.unwrap_or_default(),
..Self::default_geth()
}
}
/// Configure whether calls to precompiles should be ignored.
///
/// If set to `true`, calls to precompiles without value transfers will be ignored.
pub fn set_exclude_precompile_calls(mut self, exclude_precompile_calls: bool) -> Self {
self.exclude_precompile_calls = exclude_precompile_calls;
self
}
/// Configure whether individual opcode level steps should be recorded
pub fn set_steps(mut self, record_steps: bool) -> Self {
self.record_steps = record_steps;
self
}
/// Configure whether the tracer should record memory snapshots
pub fn set_memory_snapshots(mut self, record_memory_snapshots: bool) -> Self {
self.record_memory_snapshots = record_memory_snapshots;
self
}
/// Configure how the tracer should record stack snapshots
pub fn set_stack_snapshots(mut self, record_stack_snapshots: StackSnapshotType) -> Self {
self.record_stack_snapshots = record_stack_snapshots;
self
}
/// Sets state diff recording to true.
pub fn with_state_diffs(self) -> Self {
self.set_steps_and_state_diffs(true)
}
/// Configure whether the tracer should record state diffs
pub fn set_state_diffs(mut self, record_state_diff: bool) -> Self {
self.record_state_diff = record_state_diff;
self
}
/// Configure whether the tracer should record steps and state diffs.
///
/// This is a convenience method for setting both [TracingInspectorConfig::set_steps] and
/// [TracingInspectorConfig::set_state_diffs] since tracking state diffs requires steps tracing.
pub fn set_steps_and_state_diffs(mut self, steps_and_diffs: bool) -> Self {
self.record_steps = steps_and_diffs;
self.record_state_diff = steps_and_diffs;
self
}
/// Configure whether the tracer should record logs
pub fn set_record_logs(mut self, record_logs: bool) -> Self {
self.record_logs = record_logs;
self
}
}
/// How much of the stack to record. Nothing, just the items pushed, or the full stack
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum StackSnapshotType {
/// Don't record stack snapshots
None,
/// Record only the items pushed to the stack
Pushes,
/// Record the full stack
Full,
}
impl StackSnapshotType {
/// Returns true if this is the [StackSnapshotType::Full] variant
#[inline]
pub fn is_full(self) -> bool {
matches!(self, Self::Full)
}
/// Returns true if this is the [StackSnapshotType::Pushes] variant
#[inline]
pub fn is_pushes(self) -> bool {
matches!(self, Self::Pushes)
}
}
/// What kind of tracing style this is.
///
/// This affects things like error messages.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub(crate) enum TraceStyle {
/// Parity style tracer
Parity,
/// Geth style tracer
#[allow(dead_code)]
Geth,
}
impl TraceStyle {
/// Returns true if this is a parity style tracer.
pub(crate) const fn is_parity(self) -> bool {
matches!(self, Self::Parity)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parity_config() {
let mut s = HashSet::new();
s.insert(TraceType::StateDiff);
let config = TracingInspectorConfig::from_parity_config(&s);
// not required
assert!(!config.record_steps);
assert!(!config.record_state_diff);
let mut s = HashSet::new();
s.insert(TraceType::VmTrace);
let config = TracingInspectorConfig::from_parity_config(&s);
assert!(config.record_steps);
assert!(!config.record_state_diff);
let mut s = HashSet::new();
s.insert(TraceType::VmTrace);
s.insert(TraceType::StateDiff);
let config = TracingInspectorConfig::from_parity_config(&s);
assert!(config.record_steps);
// not required for StateDiff
assert!(!config.record_state_diff);
}
}

View File

@ -1,78 +0,0 @@
//! Fourbyte tracing inspector
//!
//! Solidity contract functions are addressed using the first four byte of the Keccak-256 hash of
//! their signature. Therefore when calling the function of a contract, the caller must send this
//! function selector as well as the ABI-encoded arguments as call data.
//!
//! The 4byteTracer collects the function selectors of every function executed in the lifetime of a
//! transaction, along with the size of the supplied call data. The result is a map of
//! SELECTOR-CALLDATASIZE to number of occurrences entries, where the keys are SELECTOR-CALLDATASIZE
//! and the values are number of occurrences of this key. For example:
//!
//! ```json
//! {
//! "0x27dc297e-128": 1,
//! "0x38cc4831-0": 2,
//! "0x524f3889-96": 1,
//! "0xadf59f99-288": 1,
//! "0xc281d19e-0": 1
//! }
//! ```
//!
//! See also <https://geth.ethereum.org/docs/developers/evm-tracing/built-in-tracers>
use alloy_primitives::{hex, Bytes, Selector};
use alloy_rpc_trace_types::geth::FourByteFrame;
use revm::{
interpreter::{CallInputs, Gas, InstructionResult},
Database, EVMData, Inspector,
};
use std::collections::HashMap;
/// Fourbyte tracing inspector that records all function selectors and their calldata sizes.
#[derive(Debug, Clone, Default)]
pub struct FourByteInspector {
/// The map of SELECTOR to number of occurrences entries
inner: HashMap<(Selector, usize), u64>,
}
impl FourByteInspector {
/// Returns the map of SELECTOR to number of occurrences entries
pub fn inner(&self) -> &HashMap<(Selector, usize), u64> {
&self.inner
}
}
impl<DB> Inspector<DB> for FourByteInspector
where
DB: Database,
{
fn call(
&mut self,
_data: &mut EVMData<'_, DB>,
call: &mut CallInputs,
) -> (InstructionResult, Gas, Bytes) {
if call.input.len() >= 4 {
let selector = Selector::try_from(&call.input[..4]).expect("input is at least 4 bytes");
let calldata_size = call.input[4..].len();
*self.inner.entry((selector, calldata_size)).or_default() += 1;
}
(InstructionResult::Continue, Gas::new(0), Bytes::new())
}
}
impl From<FourByteInspector> for FourByteFrame {
fn from(value: FourByteInspector) -> Self {
FourByteFrame(
value
.inner
.into_iter()
.map(|((selector, calldata_size), count)| {
let key = format!("0x{}-{}", hex::encode(&selector[..]), calldata_size);
(key, count)
})
.collect(),
)
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,936 +0,0 @@
//! Type bindings for js tracing inspector
use crate::tracing::{
js::{
builtins::{
address_to_buf, bytes_to_address, bytes_to_hash, from_buf, to_bigint, to_buf,
to_buf_value,
},
JsDbRequest,
},
types::CallKind,
};
use alloy_primitives::{Address, Bytes, B256, U256};
use boa_engine::{
native_function::NativeFunction,
object::{builtins::JsArrayBuffer, FunctionObjectBuilder},
Context, JsArgs, JsError, JsNativeError, JsObject, JsResult, JsValue,
};
use boa_gc::{empty_trace, Finalize, Trace};
use revm::{
interpreter::{
opcode::{PUSH0, PUSH32},
OpCode, SharedMemory, Stack,
},
primitives::{AccountInfo, State, KECCAK_EMPTY},
};
use std::{cell::RefCell, rc::Rc, sync::mpsc::channel};
use tokio::sync::mpsc;
/// A macro that creates a native function that returns via [JsValue::from]
macro_rules! js_value_getter {
($value:ident, $ctx:ident) => {
FunctionObjectBuilder::new(
$ctx,
NativeFunction::from_copy_closure(move |_this, _args, _ctx| Ok(JsValue::from($value))),
)
.length(0)
.build()
};
}
/// A macro that creates a native function that returns a captured JsValue
macro_rules! js_value_capture_getter {
($value:ident, $ctx:ident) => {
FunctionObjectBuilder::new(
$ctx,
NativeFunction::from_copy_closure_with_captures(
move |_this, _args, input, _ctx| Ok(JsValue::from(input.clone())),
$value,
),
)
.length(0)
.build()
};
}
/// A reference to a value that can be garbagae collected, but will not give access to the value if
/// it has been dropped.
///
/// This is used to allow the JS tracer functions to access values at a certain point during
/// inspection by ref without having to clone them and capture them in the js object.
///
/// JS tracer functions get access to evm internals via objects or function arguments, for example
/// `function step(log,evm)` where log has an object `stack` that has a function `peek(number)` that
/// returns a value from the stack.
///
/// These functions could get garbage collected, however the data accessed by the function is
/// supposed to be ephemeral and only valid for the duration of the function call.
///
/// This type supports garbage collection of (rust) references and prevents access to the value if
/// it has been dropped.
#[derive(Debug, Clone)]
pub(crate) struct GuardedNullableGcRef<Val: 'static> {
/// The lifetime is a lie to make it possible to use a reference in boa which requires 'static
inner: Rc<RefCell<Option<&'static Val>>>,
}
impl<Val: 'static> GuardedNullableGcRef<Val> {
/// Creates a garbage collectible reference to the given reference.
///
/// SAFETY; the caller must ensure that the guard is dropped before the value is dropped.
pub(crate) fn new(val: &Val) -> (Self, RefGuard<'_, Val>) {
let inner = Rc::new(RefCell::new(Some(val)));
let guard = RefGuard { inner: Rc::clone(&inner) };
// SAFETY: guard enforces that the value is removed from the refcell before it is dropped
let this = Self { inner: unsafe { std::mem::transmute(inner) } };
(this, guard)
}
/// Executes the given closure with a reference to the inner value if it is still present.
pub(crate) fn with_inner<F, R>(&self, f: F) -> Option<R>
where
F: FnOnce(&Val) -> R,
{
self.inner.borrow().map(f)
}
}
impl<Val: 'static> Finalize for GuardedNullableGcRef<Val> {}
unsafe impl<Val: 'static> Trace for GuardedNullableGcRef<Val> {
empty_trace!();
}
/// Guard the inner references, once this value is dropped the inner reference is also removed.
///
/// This type guarantees that it never outlives the wrapped reference.
#[derive(Debug)]
pub(crate) struct RefGuard<'a, Val> {
inner: Rc<RefCell<Option<&'a Val>>>,
}
impl<'a, Val> Drop for RefGuard<'a, Val> {
fn drop(&mut self) {
self.inner.borrow_mut().take();
}
}
/// The Log object that is passed to the javascript inspector.
#[derive(Debug)]
pub(crate) struct StepLog {
/// Stack before step execution
pub(crate) stack: StackRef,
/// Opcode to be executed
pub(crate) op: OpObj,
/// All allocated memory in a step
pub(crate) memory: MemoryRef,
/// Program counter before step execution
pub(crate) pc: u64,
/// Remaining gas before step execution
pub(crate) gas_remaining: u64,
/// Gas cost of step execution
pub(crate) cost: u64,
/// Call depth
pub(crate) depth: u64,
/// Gas refund counter before step execution
pub(crate) refund: u64,
/// returns information about the error if one occurred, otherwise returns undefined
pub(crate) error: Option<String>,
/// The contract object available to the js inspector
pub(crate) contract: Contract,
}
impl StepLog {
/// Converts the contract object into a js object
///
/// Caution: this expects a global property `bigint` to be present.
pub(crate) fn into_js_object(self, context: &mut Context<'_>) -> JsResult<JsObject> {
let Self {
stack,
op,
memory,
pc,
gas_remaining: gas,
cost,
depth,
refund,
error,
contract,
} = self;
let obj = JsObject::default();
// fields
let op = op.into_js_object(context)?;
let memory = memory.into_js_object(context)?;
let stack = stack.into_js_object(context)?;
let contract = contract.into_js_object(context)?;
obj.set("op", op, false, context)?;
obj.set("memory", memory, false, context)?;
obj.set("stack", stack, false, context)?;
obj.set("contract", contract, false, context)?;
// methods
let error =
if let Some(error) = error { JsValue::from(error) } else { JsValue::undefined() };
let get_error = js_value_capture_getter!(error, context);
let get_pc = js_value_getter!(pc, context);
let get_gas = js_value_getter!(gas, context);
let get_cost = js_value_getter!(cost, context);
let get_refund = js_value_getter!(refund, context);
let get_depth = js_value_getter!(depth, context);
obj.set("getPc", get_pc, false, context)?;
obj.set("getError", get_error, false, context)?;
obj.set("getGas", get_gas, false, context)?;
obj.set("getCost", get_cost, false, context)?;
obj.set("getDepth", get_depth, false, context)?;
obj.set("getRefund", get_refund, false, context)?;
Ok(obj)
}
}
/// Represents the memory object
#[derive(Debug, Clone)]
pub(crate) struct MemoryRef(pub(crate) GuardedNullableGcRef<SharedMemory>);
impl MemoryRef {
/// Creates a new stack reference
pub(crate) fn new(mem: &SharedMemory) -> (Self, RefGuard<'_, SharedMemory>) {
let (inner, guard) = GuardedNullableGcRef::new(mem);
(MemoryRef(inner), guard)
}
fn len(&self) -> usize {
self.0.with_inner(|mem| mem.len()).unwrap_or_default()
}
pub(crate) fn into_js_object(self, context: &mut Context<'_>) -> JsResult<JsObject> {
let obj = JsObject::default();
let len = self.len();
let length = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure(move |_this, _args, _ctx| {
Ok(JsValue::from(len as u64))
}),
)
.length(0)
.build();
// slice returns the requested range of memory as a byte slice.
let slice = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
move |_this, args, memory, ctx| {
let start = args.get_or_undefined(0).to_number(ctx)?;
let end = args.get_or_undefined(1).to_number(ctx)?;
if end < start || start < 0. || (end as usize) < memory.len() {
return Err(JsError::from_native(JsNativeError::typ().with_message(
format!(
"tracer accessed out of bound memory: offset {start}, end {end}"
),
)))
}
let start = start as usize;
let end = end as usize;
let size = end - start;
let slice = memory
.0
.with_inner(|mem| mem.slice(start, size).to_vec())
.unwrap_or_default();
to_buf_value(slice, ctx)
},
self.clone(),
),
)
.length(2)
.build();
let get_uint = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
move |_this, args, memory, ctx| {
let offset_f64 = args.get_or_undefined(0).to_number(ctx)?;
let len = memory.len();
let offset = offset_f64 as usize;
if len < offset+32 || offset_f64 < 0. {
return Err(JsError::from_native(
JsNativeError::typ().with_message(format!("tracer accessed out of bound memory: available {len}, offset {offset}, size 32"))
));
}
let slice = memory.0.with_inner(|mem| mem.slice(offset, 32).to_vec()).unwrap_or_default();
to_buf_value(slice, ctx)
},
self
),
)
.length(1)
.build();
obj.set("slice", slice, false, context)?;
obj.set("getUint", get_uint, false, context)?;
obj.set("length", length, false, context)?;
Ok(obj)
}
}
impl Finalize for MemoryRef {}
unsafe impl Trace for MemoryRef {
empty_trace!();
}
/// Represents the state object
#[derive(Debug, Clone)]
pub(crate) struct StateRef(pub(crate) GuardedNullableGcRef<State>);
impl StateRef {
/// Creates a new stack reference
pub(crate) fn new(state: &State) -> (Self, RefGuard<'_, State>) {
let (inner, guard) = GuardedNullableGcRef::new(state);
(StateRef(inner), guard)
}
fn get_account(&self, address: &Address) -> Option<AccountInfo> {
self.0.with_inner(|state| state.get(address).map(|acc| acc.info.clone()))?
}
}
impl Finalize for StateRef {}
unsafe impl Trace for StateRef {
empty_trace!();
}
/// Represents the opcode object
#[derive(Debug)]
pub(crate) struct OpObj(pub(crate) u8);
impl OpObj {
pub(crate) fn into_js_object(self, context: &mut Context<'_>) -> JsResult<JsObject> {
let obj = JsObject::default();
let value = self.0;
let is_push = (PUSH0..=PUSH32).contains(&value);
let to_number = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure(move |_this, _args, _ctx| Ok(JsValue::from(value))),
)
.length(0)
.build();
let is_push = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure(move |_this, _args, _ctx| Ok(JsValue::from(is_push))),
)
.length(0)
.build();
let to_string = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure(move |_this, _args, _ctx| {
let op = OpCode::new(value)
.or_else(|| {
// if the opcode is invalid, we'll use the invalid opcode to represent it
// because this is invoked before the opcode is
// executed, the evm will eventually return a `Halt`
// with invalid/unknown opcode as result
let invalid_opcode = 0xfe;
OpCode::new(invalid_opcode)
})
.expect("is valid opcode;");
let s = op.to_string();
Ok(JsValue::from(s))
}),
)
.length(0)
.build();
obj.set("toNumber", to_number, false, context)?;
obj.set("toString", to_string, false, context)?;
obj.set("isPush", is_push, false, context)?;
Ok(obj)
}
}
impl From<u8> for OpObj {
fn from(op: u8) -> Self {
Self(op)
}
}
/// Represents the stack object
#[derive(Debug)]
pub(crate) struct StackRef(pub(crate) GuardedNullableGcRef<Stack>);
impl StackRef {
/// Creates a new stack reference
pub(crate) fn new(stack: &Stack) -> (Self, RefGuard<'_, Stack>) {
let (inner, guard) = GuardedNullableGcRef::new(stack);
(StackRef(inner), guard)
}
fn peek(&self, idx: usize, ctx: &mut Context<'_>) -> JsResult<JsValue> {
self.0
.with_inner(|stack| {
let value = stack.peek(idx).map_err(|_| {
JsError::from_native(JsNativeError::typ().with_message(format!(
"tracer accessed out of bound stack: size {}, index {}",
stack.len(),
idx
)))
})?;
to_bigint(value, ctx)
})
.ok_or_else(|| {
JsError::from_native(JsNativeError::typ().with_message(format!(
"tracer accessed out of bound stack: size 0, index {}",
idx
)))
})?
}
pub(crate) fn into_js_object(self, context: &mut Context<'_>) -> JsResult<JsObject> {
let obj = JsObject::default();
let len = self.0.with_inner(|stack| stack.len()).unwrap_or_default();
let length = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure(move |_this, _args, _ctx| Ok(JsValue::from(len))),
)
.length(0)
.build();
// peek returns the nth-from-the-top element of the stack.
let peek = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
move |_this, args, stack, ctx| {
let idx_f64 = args.get_or_undefined(0).to_number(ctx)?;
let idx = idx_f64 as usize;
if len <= idx || idx_f64 < 0. {
return Err(JsError::from_native(JsNativeError::typ().with_message(
format!(
"tracer accessed out of bound stack: size {len}, index {idx_f64}"
),
)))
}
stack.peek(idx, ctx)
},
self,
),
)
.length(1)
.build();
obj.set("length", length, false, context)?;
obj.set("peek", peek, false, context)?;
Ok(obj)
}
}
impl Finalize for StackRef {}
unsafe impl Trace for StackRef {
empty_trace!();
}
/// Represents the contract object
#[derive(Debug, Clone, Default)]
pub(crate) struct Contract {
pub(crate) caller: Address,
pub(crate) contract: Address,
pub(crate) value: U256,
pub(crate) input: Bytes,
}
impl Contract {
/// Converts the contract object into a js object
///
/// Caution: this expects a global property `bigint` to be present.
pub(crate) fn into_js_object(self, context: &mut Context<'_>) -> JsResult<JsObject> {
let Contract { caller, contract, value, input } = self;
let obj = JsObject::default();
let get_caller = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure(move |_this, _args, ctx| {
to_buf_value(caller.as_slice().to_vec(), ctx)
}),
)
.length(0)
.build();
let get_address = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure(move |_this, _args, ctx| {
to_buf_value(contract.as_slice().to_vec(), ctx)
}),
)
.length(0)
.build();
let get_value = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure(move |_this, _args, ctx| to_bigint(value, ctx)),
)
.length(0)
.build();
let input = to_buf_value(input.to_vec(), context)?;
let get_input = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
move |_this, _args, input, _ctx| Ok(input.clone()),
input,
),
)
.length(0)
.build();
obj.set("getCaller", get_caller, false, context)?;
obj.set("getAddress", get_address, false, context)?;
obj.set("getValue", get_value, false, context)?;
obj.set("getInput", get_input, false, context)?;
Ok(obj)
}
}
/// Represents the call frame object for exit functions
pub(crate) struct FrameResult {
pub(crate) gas_used: u64,
pub(crate) output: Bytes,
pub(crate) error: Option<String>,
}
impl FrameResult {
pub(crate) fn into_js_object(self, ctx: &mut Context<'_>) -> JsResult<JsObject> {
let Self { gas_used, output, error } = self;
let obj = JsObject::default();
let output = to_buf_value(output.to_vec(), ctx)?;
let get_output = FunctionObjectBuilder::new(
ctx,
NativeFunction::from_copy_closure_with_captures(
move |_this, _args, output, _ctx| Ok(output.clone()),
output,
),
)
.length(0)
.build();
let error = error.map(JsValue::from).unwrap_or_default();
let get_error = js_value_capture_getter!(error, ctx);
let get_gas_used = js_value_getter!(gas_used, ctx);
obj.set("getGasUsed", get_gas_used, false, ctx)?;
obj.set("getOutput", get_output, false, ctx)?;
obj.set("getError", get_error, false, ctx)?;
Ok(obj)
}
}
/// Represents the call frame object for enter functions
pub(crate) struct CallFrame {
pub(crate) contract: Contract,
pub(crate) kind: CallKind,
pub(crate) gas: u64,
}
impl CallFrame {
pub(crate) fn into_js_object(self, ctx: &mut Context<'_>) -> JsResult<JsObject> {
let CallFrame { contract: Contract { caller, contract, value, input }, kind, gas } = self;
let obj = JsObject::default();
let get_from = FunctionObjectBuilder::new(
ctx,
NativeFunction::from_copy_closure(move |_this, _args, ctx| {
to_buf_value(caller.as_slice().to_vec(), ctx)
}),
)
.length(0)
.build();
let get_to = FunctionObjectBuilder::new(
ctx,
NativeFunction::from_copy_closure(move |_this, _args, ctx| {
to_buf_value(contract.as_slice().to_vec(), ctx)
}),
)
.length(0)
.build();
let get_value = FunctionObjectBuilder::new(
ctx,
NativeFunction::from_copy_closure(move |_this, _args, ctx| to_bigint(value, ctx)),
)
.length(0)
.build();
let input = to_buf_value(input.to_vec(), ctx)?;
let get_input = FunctionObjectBuilder::new(
ctx,
NativeFunction::from_copy_closure_with_captures(
move |_this, _args, input, _ctx| Ok(input.clone()),
input,
),
)
.length(0)
.build();
let get_gas = js_value_getter!(gas, ctx);
let ty = kind.to_string();
let get_type = js_value_capture_getter!(ty, ctx);
obj.set("getFrom", get_from, false, ctx)?;
obj.set("getTo", get_to, false, ctx)?;
obj.set("getValue", get_value, false, ctx)?;
obj.set("getInput", get_input, false, ctx)?;
obj.set("getGas", get_gas, false, ctx)?;
obj.set("getType", get_type, false, ctx)?;
Ok(obj)
}
}
/// The `ctx` object that represents the context in which the transaction is executed.
pub(crate) struct EvmContext {
/// String, one of the two values CALL and CREATE
pub(crate) r#type: String,
/// Sender of the transaction
pub(crate) from: Address,
/// Target of the transaction
pub(crate) to: Option<Address>,
pub(crate) input: Bytes,
/// Gas limit
pub(crate) gas: u64,
/// Number, amount of gas used in executing the transaction (excludes txdata costs)
pub(crate) gas_used: u64,
/// Number, gas price configured in the transaction being executed
pub(crate) gas_price: u64,
/// Number, intrinsic gas for the transaction being executed
pub(crate) intrinsic_gas: u64,
/// big.int Amount to be transferred in wei
pub(crate) value: U256,
/// Number, block number
pub(crate) block: u64,
pub(crate) output: Bytes,
/// Number, block number
pub(crate) time: String,
pub(crate) block_hash: Option<B256>,
pub(crate) tx_index: Option<usize>,
pub(crate) tx_hash: Option<B256>,
}
impl EvmContext {
pub(crate) fn into_js_object(self, ctx: &mut Context<'_>) -> JsResult<JsObject> {
let Self {
r#type,
from,
to,
input,
gas,
gas_used,
gas_price,
intrinsic_gas,
value,
block,
output,
time,
block_hash,
tx_index,
tx_hash,
} = self;
let obj = JsObject::default();
// add properties
obj.set("type", r#type, false, ctx)?;
obj.set("from", address_to_buf(from, ctx)?, false, ctx)?;
if let Some(to) = to {
obj.set("to", address_to_buf(to, ctx)?, false, ctx)?;
} else {
obj.set("to", JsValue::null(), false, ctx)?;
}
obj.set("input", to_buf(input.to_vec(), ctx)?, false, ctx)?;
obj.set("gas", gas, false, ctx)?;
obj.set("gasUsed", gas_used, false, ctx)?;
obj.set("gasPrice", gas_price, false, ctx)?;
obj.set("intrinsicGas", intrinsic_gas, false, ctx)?;
obj.set("value", to_bigint(value, ctx)?, false, ctx)?;
obj.set("block", block, false, ctx)?;
obj.set("output", to_buf(output.to_vec(), ctx)?, false, ctx)?;
obj.set("time", time, false, ctx)?;
if let Some(block_hash) = block_hash {
obj.set("blockHash", to_buf(block_hash.as_slice().to_vec(), ctx)?, false, ctx)?;
}
if let Some(tx_index) = tx_index {
obj.set("txIndex", tx_index as u64, false, ctx)?;
}
if let Some(tx_hash) = tx_hash {
obj.set("txHash", to_buf(tx_hash.as_slice().to_vec(), ctx)?, false, ctx)?;
}
Ok(obj)
}
}
/// DB is the object that allows the js inspector to interact with the database.
#[derive(Debug, Clone)]
pub(crate) struct EvmDbRef {
state: StateRef,
to_db: mpsc::Sender<JsDbRequest>,
}
impl EvmDbRef {
/// Creates a new DB reference
pub(crate) fn new(
state: &State,
to_db: mpsc::Sender<JsDbRequest>,
) -> (Self, RefGuard<'_, State>) {
let (state, guard) = StateRef::new(state);
let this = Self { state, to_db };
(this, guard)
}
fn read_basic(&self, address: JsValue, ctx: &mut Context<'_>) -> JsResult<Option<AccountInfo>> {
let buf = from_buf(address, ctx)?;
let address = bytes_to_address(buf);
if let acc @ Some(_) = self.state.get_account(&address) {
return Ok(acc)
}
let (tx, rx) = channel();
if self.to_db.try_send(JsDbRequest::Basic { address, resp: tx }).is_err() {
return Err(JsError::from_native(
JsNativeError::error()
.with_message(format!("Failed to read address {address:?} from database",)),
))
}
match rx.recv() {
Ok(Ok(maybe_acc)) => Ok(maybe_acc),
_ => Err(JsError::from_native(
JsNativeError::error()
.with_message(format!("Failed to read address {address:?} from database",)),
)),
}
}
fn read_code(&self, address: JsValue, ctx: &mut Context<'_>) -> JsResult<JsArrayBuffer> {
let acc = self.read_basic(address, ctx)?;
let code_hash = acc.map(|acc| acc.code_hash).unwrap_or(KECCAK_EMPTY);
if code_hash == KECCAK_EMPTY {
return JsArrayBuffer::new(0, ctx)
}
let (tx, rx) = channel();
if self.to_db.try_send(JsDbRequest::Code { code_hash, resp: tx }).is_err() {
return Err(JsError::from_native(
JsNativeError::error()
.with_message(format!("Failed to read code hash {code_hash:?} from database",)),
))
}
let code = match rx.recv() {
Ok(Ok(code)) => code,
_ => {
return Err(JsError::from_native(JsNativeError::error().with_message(format!(
"Failed to read code hash {code_hash:?} from database",
))))
}
};
to_buf(code.to_vec(), ctx)
}
fn read_state(
&self,
address: JsValue,
slot: JsValue,
ctx: &mut Context<'_>,
) -> JsResult<JsArrayBuffer> {
let buf = from_buf(address, ctx)?;
let address = bytes_to_address(buf);
let buf = from_buf(slot, ctx)?;
let slot = bytes_to_hash(buf);
let (tx, rx) = channel();
if self
.to_db
.try_send(JsDbRequest::StorageAt { address, index: slot.into(), resp: tx })
.is_err()
{
return Err(JsError::from_native(JsNativeError::error().with_message(format!(
"Failed to read state for {address:?} at {slot:?} from database",
))))
}
let value = match rx.recv() {
Ok(Ok(value)) => value,
_ => {
return Err(JsError::from_native(JsNativeError::error().with_message(format!(
"Failed to read state for {address:?} at {slot:?} from database",
))))
}
};
let value: B256 = value.into();
to_buf(value.as_slice().to_vec(), ctx)
}
pub(crate) fn into_js_object(self, context: &mut Context<'_>) -> JsResult<JsObject> {
let obj = JsObject::default();
let exists = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
move |_this, args, db, ctx| {
let val = args.get_or_undefined(0).clone();
let acc = db.read_basic(val, ctx)?;
let exists = acc.is_some();
Ok(JsValue::from(exists))
},
self.clone(),
),
)
.length(1)
.build();
let get_balance = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
move |_this, args, db, ctx| {
let val = args.get_or_undefined(0).clone();
let acc = db.read_basic(val, ctx)?;
let balance = acc.map(|acc| acc.balance).unwrap_or_default();
to_bigint(balance, ctx)
},
self.clone(),
),
)
.length(1)
.build();
let get_nonce = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
move |_this, args, db, ctx| {
let val = args.get_or_undefined(0).clone();
let acc = db.read_basic(val, ctx)?;
let nonce = acc.map(|acc| acc.nonce).unwrap_or_default();
Ok(JsValue::from(nonce))
},
self.clone(),
),
)
.length(1)
.build();
let get_code = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
move |_this, args, db, ctx| {
let val = args.get_or_undefined(0).clone();
Ok(db.read_code(val, ctx)?.into())
},
self.clone(),
),
)
.length(1)
.build();
let get_state = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
move |_this, args, db, ctx| {
let addr = args.get_or_undefined(0).clone();
let slot = args.get_or_undefined(1).clone();
Ok(db.read_state(addr, slot, ctx)?.into())
},
self,
),
)
.length(2)
.build();
obj.set("getBalance", get_balance, false, context)?;
obj.set("getNonce", get_nonce, false, context)?;
obj.set("getCode", get_code, false, context)?;
obj.set("getState", get_state, false, context)?;
obj.set("exists", exists, false, context)?;
Ok(obj)
}
}
impl Finalize for EvmDbRef {}
unsafe impl Trace for EvmDbRef {
empty_trace!();
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tracing::js::builtins::BIG_INT_JS;
use boa_engine::{object::builtins::JsArrayBuffer, property::Attribute, Source};
#[test]
fn test_contract() {
let mut ctx = Context::default();
let contract = Contract {
caller: Address::ZERO,
contract: Address::ZERO,
value: U256::from(1337u64),
input: vec![0x01, 0x02, 0x03].into(),
};
let big_int = ctx.eval(Source::from_bytes(BIG_INT_JS)).unwrap();
ctx.register_global_property("bigint", big_int, Attribute::all()).unwrap();
let obj = contract.clone().into_js_object(&mut ctx).unwrap();
let s = "({
call: function(contract) { return contract.getCaller(); },
value: function(contract) { return contract.getValue(); },
input: function(contract) { return contract.getInput(); }
})";
let eval_obj = ctx.eval(Source::from_bytes(s)).unwrap();
let call = eval_obj.as_object().unwrap().get("call", &mut ctx).unwrap();
let res = call
.as_callable()
.unwrap()
.call(&JsValue::undefined(), &[obj.clone().into()], &mut ctx)
.unwrap();
assert!(res.is_object());
assert!(res.as_object().unwrap().is_array_buffer());
let call = eval_obj.as_object().unwrap().get("value", &mut ctx).unwrap();
let res = call
.as_callable()
.unwrap()
.call(&JsValue::undefined(), &[obj.clone().into()], &mut ctx)
.unwrap();
assert_eq!(
res.to_string(&mut ctx).unwrap().to_std_string().unwrap(),
contract.value.to_string()
);
let call = eval_obj.as_object().unwrap().get("input", &mut ctx).unwrap();
let res = call
.as_callable()
.unwrap()
.call(&JsValue::undefined(), &[obj.into()], &mut ctx)
.unwrap();
let buffer = JsArrayBuffer::from_object(res.as_object().unwrap().clone()).unwrap();
let input = buffer.take().unwrap();
assert_eq!(input, contract.input);
}
}

View File

@ -1,258 +0,0 @@
//! Builtin functions
use alloy_primitives::{hex, Address, B256, U256};
use boa_engine::{
object::builtins::{JsArray, JsArrayBuffer},
property::Attribute,
Context, JsArgs, JsError, JsNativeError, JsResult, JsString, JsValue, NativeFunction, Source,
};
use boa_gc::{empty_trace, Finalize, Trace};
use std::collections::HashSet;
/// bigIntegerJS is the minified version of <https://github.com/peterolson/BigInteger.js>.
pub(crate) const BIG_INT_JS: &str = include_str!("bigint.js");
/// Registers all the builtin functions and global bigint property
///
/// Note: this does not register the `isPrecompiled` builtin, as this requires the precompile
/// addresses, see [PrecompileList::register_callable].
pub(crate) fn register_builtins(ctx: &mut Context<'_>) -> JsResult<()> {
let big_int = ctx.eval(Source::from_bytes(BIG_INT_JS.as_bytes()))?;
ctx.register_global_property("bigint", big_int, Attribute::all())?;
ctx.register_global_builtin_callable("toHex", 1, NativeFunction::from_fn_ptr(to_hex))?;
ctx.register_global_callable("toWord", 1, NativeFunction::from_fn_ptr(to_word))?;
ctx.register_global_callable("toAddress", 1, NativeFunction::from_fn_ptr(to_address))?;
ctx.register_global_callable("toContract", 2, NativeFunction::from_fn_ptr(to_contract))?;
ctx.register_global_callable("toContract2", 3, NativeFunction::from_fn_ptr(to_contract2))?;
Ok(())
}
/// Converts an array, hex string or Uint8Array to a []byte
pub(crate) fn from_buf(val: JsValue, context: &mut Context<'_>) -> JsResult<Vec<u8>> {
if let Some(obj) = val.as_object().cloned() {
if obj.is_array_buffer() {
let buf = JsArrayBuffer::from_object(obj)?;
return buf.take()
} else if obj.is_string() {
let js_string = obj.borrow().as_string().unwrap();
return hex_decode_js_string(js_string)
} else if obj.is_array() {
let array = JsArray::from_object(obj)?;
let len = array.length(context)?;
let mut buf = Vec::with_capacity(len as usize);
for i in 0..len {
let val = array.get(i, context)?;
buf.push(val.to_number(context)? as u8);
}
return Ok(buf)
}
}
Err(JsError::from_native(JsNativeError::typ().with_message("invalid buffer type")))
}
/// Create a new array buffer from the address' bytes.
pub(crate) fn address_to_buf(addr: Address, context: &mut Context<'_>) -> JsResult<JsArrayBuffer> {
to_buf(addr.0.to_vec(), context)
}
/// Create a new array buffer from byte block.
pub(crate) fn to_buf(bytes: Vec<u8>, context: &mut Context<'_>) -> JsResult<JsArrayBuffer> {
JsArrayBuffer::from_byte_block(bytes, context)
}
/// Create a new array buffer object from byte block.
pub(crate) fn to_buf_value(bytes: Vec<u8>, context: &mut Context<'_>) -> JsResult<JsValue> {
Ok(to_buf(bytes, context)?.into())
}
/// Converts a buffer type to an address.
///
/// If the buffer is larger than the address size, it will be cropped from the left
pub(crate) fn bytes_to_address(buf: Vec<u8>) -> Address {
let mut address = Address::default();
let mut buf = &buf[..];
let address_len = address.0.len();
if buf.len() > address_len {
// crop from left
buf = &buf[buf.len() - address.0.len()..];
}
let address_slice = &mut address.0[address_len - buf.len()..];
address_slice.copy_from_slice(buf);
address
}
/// Converts a buffer type to a hash.
///
/// If the buffer is larger than the hash size, it will be cropped from the left
pub(crate) fn bytes_to_hash(buf: Vec<u8>) -> B256 {
let mut hash = B256::default();
let mut buf = &buf[..];
let hash_len = hash.0.len();
if buf.len() > hash_len {
// crop from left
buf = &buf[buf.len() - hash.0.len()..];
}
let hash_slice = &mut hash.0[hash_len - buf.len()..];
hash_slice.copy_from_slice(buf);
hash
}
/// Converts a U256 to a bigint using the global bigint property
pub(crate) fn to_bigint(value: U256, ctx: &mut Context<'_>) -> JsResult<JsValue> {
let bigint = ctx.global_object().get("bigint", ctx)?;
if !bigint.is_callable() {
return Ok(JsValue::undefined())
}
bigint.as_callable().unwrap().call(
&JsValue::undefined(),
&[JsValue::from(value.to_string())],
ctx,
)
}
/// Takes three arguments: a JavaScript value that represents the sender's address, a string salt
/// value, and the initcode for the contract. Compute the address of a contract created by the
/// sender with the given salt and code hash, then converts the resulting address back into a byte
/// buffer for output.
pub(crate) fn to_contract2(
_: &JsValue,
args: &[JsValue],
ctx: &mut Context<'_>,
) -> JsResult<JsValue> {
// Extract the sender's address, salt and initcode from the arguments
let from = args.get_or_undefined(0).clone();
let salt = match args.get_or_undefined(1).to_string(ctx) {
Ok(js_string) => {
let buf = hex_decode_js_string(js_string)?;
bytes_to_hash(buf)
}
Err(_) => {
return Err(JsError::from_native(JsNativeError::typ().with_message("invalid salt type")))
}
};
let initcode = args.get_or_undefined(2).clone();
// Convert the sender's address to a byte buffer and then to an Address
let buf = from_buf(from, ctx)?;
let addr = bytes_to_address(buf);
// Convert the initcode to a byte buffer
let code_buf = from_buf(initcode, ctx)?;
// Compute the contract address
let contract_addr = addr.create2_from_code(salt, code_buf);
// Convert the contract address to a byte buffer and return it as an ArrayBuffer
to_buf_value(contract_addr.0.to_vec(), ctx)
}
/// Converts the sender's address to a byte buffer
pub(crate) fn to_contract(
_: &JsValue,
args: &[JsValue],
ctx: &mut Context<'_>,
) -> JsResult<JsValue> {
// Extract the sender's address and nonce from the arguments
let from = args.get_or_undefined(0).clone();
let nonce = args.get_or_undefined(1).to_number(ctx)? as u64;
// Convert the sender's address to a byte buffer and then to an Address
let buf = from_buf(from, ctx)?;
let addr = bytes_to_address(buf);
// Compute the contract address
let contract_addr = addr.create(nonce);
// Convert the contract address to a byte buffer and return it as an ArrayBuffer
to_buf_value(contract_addr.0.to_vec(), ctx)
}
/// Converts a buffer type to an address
pub(crate) fn to_address(
_: &JsValue,
args: &[JsValue],
ctx: &mut Context<'_>,
) -> JsResult<JsValue> {
let val = args.get_or_undefined(0).clone();
let buf = from_buf(val, ctx)?;
let address = bytes_to_address(buf);
to_buf_value(address.0.to_vec(), ctx)
}
/// Converts a buffer type to a word
pub(crate) fn to_word(_: &JsValue, args: &[JsValue], ctx: &mut Context<'_>) -> JsResult<JsValue> {
let val = args.get_or_undefined(0).clone();
let buf = from_buf(val, ctx)?;
let hash = bytes_to_hash(buf);
to_buf_value(hash.0.to_vec(), ctx)
}
/// Converts a buffer type to a hex string
pub(crate) fn to_hex(_: &JsValue, args: &[JsValue], ctx: &mut Context<'_>) -> JsResult<JsValue> {
let val = args.get_or_undefined(0).clone();
let buf = from_buf(val, ctx)?;
Ok(JsValue::from(hex::encode(buf)))
}
/// Decodes a hex decoded js-string
fn hex_decode_js_string(js_string: JsString) -> JsResult<Vec<u8>> {
match js_string.to_std_string() {
Ok(s) => match hex::decode(s.as_str()) {
Ok(data) => Ok(data),
Err(err) => Err(JsError::from_native(
JsNativeError::error().with_message(format!("invalid hex string {s}: {err}",)),
)),
},
Err(err) => Err(JsError::from_native(
JsNativeError::error()
.with_message(format!("invalid utf8 string {js_string:?}: {err}",)),
)),
}
}
/// A container for all precompile addresses used for the `isPrecompiled` global callable.
#[derive(Debug, Clone)]
pub(crate) struct PrecompileList(pub(crate) HashSet<Address>);
impl PrecompileList {
/// Registers the global callable `isPrecompiled`
pub(crate) fn register_callable(self, ctx: &mut Context<'_>) -> JsResult<()> {
let is_precompiled = NativeFunction::from_copy_closure_with_captures(
move |_this, args, precompiles, ctx| {
let val = args.get_or_undefined(0).clone();
let buf = from_buf(val, ctx)?;
let addr = bytes_to_address(buf);
Ok(precompiles.0.contains(&addr).into())
},
self,
);
ctx.register_global_callable("isPrecompiled", 1, is_precompiled)?;
Ok(())
}
}
impl Finalize for PrecompileList {}
unsafe impl Trace for PrecompileList {
empty_trace!();
}
#[cfg(test)]
mod tests {
use super::*;
use boa_engine::Source;
#[test]
fn test_install_bigint() {
let mut ctx = Context::default();
let big_int = ctx.eval(Source::from_bytes(BIG_INT_JS.as_bytes())).unwrap();
let value = JsValue::from(100);
let result =
big_int.as_callable().unwrap().call(&JsValue::undefined(), &[value], &mut ctx).unwrap();
assert_eq!(result.to_string(&mut ctx).unwrap().to_std_string().unwrap(), "100");
}
}

View File

@ -1,583 +0,0 @@
//! Javascript inspector
use crate::tracing::{
js::{
bindings::{
CallFrame, Contract, EvmContext, EvmDbRef, FrameResult, MemoryRef, StackRef, StepLog,
},
builtins::{register_builtins, PrecompileList},
},
types::CallKind,
};
use alloy_primitives::{Address, Bytes, B256, U256};
use boa_engine::{Context, JsError, JsObject, JsResult, JsValue, Source};
use revm::{
interpreter::{
return_revert, CallInputs, CallScheme, CreateInputs, Gas, InstructionResult, Interpreter,
},
precompile::Precompiles,
primitives::{AccountInfo, Env, ExecutionResult, Output, ResultAndState, TransactTo},
Database, EVMData, Inspector,
};
use tokio::sync::mpsc;
pub(crate) mod bindings;
pub(crate) mod builtins;
/// A javascript inspector that will delegate inspector functions to javascript functions
///
/// See also <https://geth.ethereum.org/docs/developers/evm-tracing/custom-tracer#custom-javascript-tracing>
#[derive(Debug)]
pub struct JsInspector {
ctx: Context<'static>,
/// The javascript config provided to the inspector.
_config: JsValue,
/// The evaluated object that contains the inspector functions.
obj: JsObject,
/// The javascript function that will be called when the result is requested.
result_fn: JsObject,
fault_fn: JsObject,
// EVM inspector hook functions
/// Invoked when the EVM enters a new call that is _NOT_ the top level call.
///
/// Corresponds to [Inspector::call] and [Inspector::create_end] but is also invoked on
/// [Inspector::selfdestruct].
enter_fn: Option<JsObject>,
/// Invoked when the EVM exits a call that is _NOT_ the top level call.
///
/// Corresponds to [Inspector::call_end] and [Inspector::create_end] but also invoked after
/// selfdestruct.
exit_fn: Option<JsObject>,
/// Executed before each instruction is executed.
step_fn: Option<JsObject>,
/// Keeps track of the current call stack.
call_stack: Vec<CallStackItem>,
/// sender half of a channel to communicate with the database service.
to_db_service: mpsc::Sender<JsDbRequest>,
/// Marker to track whether the precompiles have been registered.
precompiles_registered: bool,
}
impl JsInspector {
/// Creates a new inspector from a javascript code snipped that evaluates to an object with the
/// expected fields and a config object.
///
/// The object must have the following fields:
/// - `result`: a function that will be called when the result is requested.
/// - `fault`: a function that will be called when the transaction fails.
///
/// Optional functions are invoked during inspection:
/// - `setup`: a function that will be called before the inspection starts.
/// - `enter`: a function that will be called when the execution enters a new call.
/// - `exit`: a function that will be called when the execution exits a call.
/// - `step`: a function that will be called when the execution steps to the next instruction.
///
/// This also accepts a sender half of a channel to communicate with the database service so the
/// DB can be queried from inside the inspector.
pub fn new(
code: String,
config: serde_json::Value,
to_db_service: mpsc::Sender<JsDbRequest>,
) -> Result<Self, JsInspectorError> {
// Instantiate the execution context
let mut ctx = Context::default();
register_builtins(&mut ctx)?;
// evaluate the code
let code = format!("({})", code);
let obj =
ctx.eval(Source::from_bytes(code.as_bytes())).map_err(JsInspectorError::EvalCode)?;
let obj = obj.as_object().cloned().ok_or(JsInspectorError::ExpectedJsObject)?;
// ensure all the fields are callables, if present
let result_fn = obj
.get("result", &mut ctx)?
.as_object()
.cloned()
.ok_or(JsInspectorError::ResultFunctionMissing)?;
if !result_fn.is_callable() {
return Err(JsInspectorError::ResultFunctionMissing)
}
let fault_fn = obj
.get("fault", &mut ctx)?
.as_object()
.cloned()
.ok_or(JsInspectorError::FaultFunctionMissing)?;
if !result_fn.is_callable() {
return Err(JsInspectorError::FaultFunctionMissing)
}
let enter_fn = obj.get("enter", &mut ctx)?.as_object().cloned().filter(|o| o.is_callable());
let exit_fn = obj.get("exit", &mut ctx)?.as_object().cloned().filter(|o| o.is_callable());
let step_fn = obj.get("step", &mut ctx)?.as_object().cloned().filter(|o| o.is_callable());
let config =
JsValue::from_json(&config, &mut ctx).map_err(JsInspectorError::InvalidJsonConfig)?;
if let Some(setup_fn) = obj.get("setup", &mut ctx)?.as_object() {
if !setup_fn.is_callable() {
return Err(JsInspectorError::SetupFunctionNotCallable)
}
// call setup()
setup_fn
.call(&(obj.clone().into()), &[config.clone()], &mut ctx)
.map_err(JsInspectorError::SetupCallFailed)?;
}
Ok(Self {
ctx,
_config: config,
obj,
result_fn,
fault_fn,
enter_fn,
exit_fn,
step_fn,
call_stack: Default::default(),
to_db_service,
precompiles_registered: false,
})
}
/// Calls the result function and returns the result as [serde_json::Value].
///
/// Note: This is supposed to be called after the inspection has finished.
pub fn json_result(
&mut self,
res: ResultAndState,
env: &Env,
) -> Result<serde_json::Value, JsInspectorError> {
Ok(self.result(res, env)?.to_json(&mut self.ctx)?)
}
/// Calls the result function and returns the result.
pub fn result(&mut self, res: ResultAndState, env: &Env) -> Result<JsValue, JsInspectorError> {
let ResultAndState { result, state } = res;
let (db, _db_guard) = EvmDbRef::new(&state, self.to_db_service.clone());
let gas_used = result.gas_used();
let mut to = None;
let mut output_bytes = None;
match result {
ExecutionResult::Success { output, .. } => match output {
Output::Call(out) => {
output_bytes = Some(out);
}
Output::Create(out, addr) => {
to = addr;
output_bytes = Some(out);
}
},
ExecutionResult::Revert { output, .. } => {
output_bytes = Some(output);
}
ExecutionResult::Halt { .. } => {}
};
let ctx = EvmContext {
r#type: match env.tx.transact_to {
TransactTo::Call(target) => {
to = Some(target);
"CALL"
}
TransactTo::Create(_) => "CREATE",
}
.to_string(),
from: env.tx.caller,
to,
input: env.tx.data.clone(),
gas: env.tx.gas_limit,
gas_used,
gas_price: env.tx.gas_price.try_into().unwrap_or(u64::MAX),
value: env.tx.value,
block: env.block.number.try_into().unwrap_or(u64::MAX),
output: output_bytes.unwrap_or_default(),
time: env.block.timestamp.to_string(),
// TODO: fill in the following fields
intrinsic_gas: 0,
block_hash: None,
tx_index: None,
tx_hash: None,
};
let ctx = ctx.into_js_object(&mut self.ctx)?;
let db = db.into_js_object(&mut self.ctx)?;
Ok(self.result_fn.call(
&(self.obj.clone().into()),
&[ctx.into(), db.into()],
&mut self.ctx,
)?)
}
fn try_fault(&mut self, step: StepLog, db: EvmDbRef) -> JsResult<()> {
let step = step.into_js_object(&mut self.ctx)?;
let db = db.into_js_object(&mut self.ctx)?;
self.fault_fn.call(&(self.obj.clone().into()), &[step.into(), db.into()], &mut self.ctx)?;
Ok(())
}
fn try_step(&mut self, step: StepLog, db: EvmDbRef) -> JsResult<()> {
if let Some(step_fn) = &self.step_fn {
let step = step.into_js_object(&mut self.ctx)?;
let db = db.into_js_object(&mut self.ctx)?;
step_fn.call(&(self.obj.clone().into()), &[step.into(), db.into()], &mut self.ctx)?;
}
Ok(())
}
fn try_enter(&mut self, frame: CallFrame) -> JsResult<()> {
if let Some(enter_fn) = &self.enter_fn {
let frame = frame.into_js_object(&mut self.ctx)?;
enter_fn.call(&(self.obj.clone().into()), &[frame.into()], &mut self.ctx)?;
}
Ok(())
}
fn try_exit(&mut self, frame: FrameResult) -> JsResult<()> {
if let Some(exit_fn) = &self.exit_fn {
let frame = frame.into_js_object(&mut self.ctx)?;
exit_fn.call(&(self.obj.clone().into()), &[frame.into()], &mut self.ctx)?;
}
Ok(())
}
/// Returns the currently active call
///
/// Panics: if there's no call yet
#[track_caller]
fn active_call(&self) -> &CallStackItem {
self.call_stack.last().expect("call stack is empty")
}
#[inline]
fn pop_call(&mut self) {
self.call_stack.pop();
}
/// Returns true whether the active call is the root call.
#[inline]
fn is_root_call_active(&self) -> bool {
self.call_stack.len() == 1
}
/// Returns true if there's an enter function and the active call is not the root call.
#[inline]
fn can_call_enter(&self) -> bool {
self.enter_fn.is_some() && !self.is_root_call_active()
}
/// Returns true if there's an exit function and the active call is not the root call.
#[inline]
fn can_call_exit(&mut self) -> bool {
self.enter_fn.is_some() && !self.is_root_call_active()
}
/// Pushes a new call to the stack
fn push_call(
&mut self,
address: Address,
data: Bytes,
value: U256,
kind: CallKind,
caller: Address,
gas_limit: u64,
) -> &CallStackItem {
let call = CallStackItem {
contract: Contract { caller, contract: address, value, input: data },
kind,
gas_limit,
};
self.call_stack.push(call);
self.active_call()
}
/// Registers the precompiles in the JS context
fn register_precompiles(&mut self, precompiles: &Precompiles) {
if !self.precompiles_registered {
return
}
let precompiles = PrecompileList(precompiles.addresses().into_iter().copied().collect());
let _ = precompiles.register_callable(&mut self.ctx);
self.precompiles_registered = true
}
}
impl<DB> Inspector<DB> for JsInspector
where
DB: Database,
{
fn step(&mut self, interp: &mut Interpreter<'_>, data: &mut EVMData<'_, DB>) {
if self.step_fn.is_none() {
return
}
let (db, _db_guard) =
EvmDbRef::new(&data.journaled_state.state, self.to_db_service.clone());
let (stack, _stack_guard) = StackRef::new(&interp.stack);
let (memory, _memory_guard) = MemoryRef::new(interp.shared_memory);
let step = StepLog {
stack,
op: interp.current_opcode().into(),
memory,
pc: interp.program_counter() as u64,
gas_remaining: interp.gas.remaining(),
cost: interp.gas.spend(),
depth: data.journaled_state.depth(),
refund: interp.gas.refunded() as u64,
error: None,
contract: self.active_call().contract.clone(),
};
if self.try_step(step, db).is_err() {
interp.instruction_result = InstructionResult::Revert;
}
}
fn log(
&mut self,
_evm_data: &mut EVMData<'_, DB>,
_address: &Address,
_topics: &[B256],
_data: &Bytes,
) {
}
fn step_end(&mut self, interp: &mut Interpreter<'_>, data: &mut EVMData<'_, DB>) {
if self.step_fn.is_none() {
return
}
if matches!(interp.instruction_result, return_revert!()) {
let (db, _db_guard) =
EvmDbRef::new(&data.journaled_state.state, self.to_db_service.clone());
let (stack, _stack_guard) = StackRef::new(&interp.stack);
let (memory, _memory_guard) = MemoryRef::new(interp.shared_memory);
let step = StepLog {
stack,
op: interp.current_opcode().into(),
memory,
pc: interp.program_counter() as u64,
gas_remaining: interp.gas.remaining(),
cost: interp.gas.spend(),
depth: data.journaled_state.depth(),
refund: interp.gas.refunded() as u64,
error: Some(format!("{:?}", interp.instruction_result)),
contract: self.active_call().contract.clone(),
};
let _ = self.try_fault(step, db);
}
}
fn call(
&mut self,
data: &mut EVMData<'_, DB>,
inputs: &mut CallInputs,
) -> (InstructionResult, Gas, Bytes) {
self.register_precompiles(&data.precompiles);
// determine correct `from` and `to` based on the call scheme
let (from, to) = match inputs.context.scheme {
CallScheme::DelegateCall | CallScheme::CallCode => {
(inputs.context.address, inputs.context.code_address)
}
_ => (inputs.context.caller, inputs.context.address),
};
let value = inputs.transfer.value;
self.push_call(
to,
inputs.input.clone(),
value,
inputs.context.scheme.into(),
from,
inputs.gas_limit,
);
if self.can_call_enter() {
let call = self.active_call();
let frame = CallFrame {
contract: call.contract.clone(),
kind: call.kind,
gas: inputs.gas_limit,
};
if let Err(err) = self.try_enter(frame) {
return (InstructionResult::Revert, Gas::new(0), err.to_string().into())
}
}
(InstructionResult::Continue, Gas::new(0), Bytes::new())
}
fn call_end(
&mut self,
_data: &mut EVMData<'_, DB>,
_inputs: &CallInputs,
remaining_gas: Gas,
ret: InstructionResult,
out: Bytes,
) -> (InstructionResult, Gas, Bytes) {
if self.can_call_exit() {
let frame_result =
FrameResult { gas_used: remaining_gas.spend(), output: out.clone(), error: None };
if let Err(err) = self.try_exit(frame_result) {
return (InstructionResult::Revert, Gas::new(0), err.to_string().into())
}
}
self.pop_call();
(ret, remaining_gas, out)
}
fn create(
&mut self,
data: &mut EVMData<'_, DB>,
inputs: &mut CreateInputs,
) -> (InstructionResult, Option<Address>, Gas, Bytes) {
self.register_precompiles(&data.precompiles);
let _ = data.journaled_state.load_account(inputs.caller, data.db);
let nonce = data.journaled_state.account(inputs.caller).info.nonce;
let address = inputs.created_address(nonce);
self.push_call(
address,
inputs.init_code.clone(),
inputs.value,
inputs.scheme.into(),
inputs.caller,
inputs.gas_limit,
);
if self.can_call_enter() {
let call = self.active_call();
let frame =
CallFrame { contract: call.contract.clone(), kind: call.kind, gas: call.gas_limit };
if let Err(err) = self.try_enter(frame) {
return (InstructionResult::Revert, None, Gas::new(0), err.to_string().into())
}
}
(InstructionResult::Continue, None, Gas::new(inputs.gas_limit), Bytes::default())
}
fn create_end(
&mut self,
_data: &mut EVMData<'_, DB>,
_inputs: &CreateInputs,
ret: InstructionResult,
address: Option<Address>,
remaining_gas: Gas,
out: Bytes,
) -> (InstructionResult, Option<Address>, Gas, Bytes) {
if self.can_call_exit() {
let frame_result =
FrameResult { gas_used: remaining_gas.spend(), output: out.clone(), error: None };
if let Err(err) = self.try_exit(frame_result) {
return (InstructionResult::Revert, None, Gas::new(0), err.to_string().into())
}
}
self.pop_call();
(ret, address, remaining_gas, out)
}
fn selfdestruct(&mut self, _contract: Address, _target: Address, _value: U256) {
// This is exempt from the root call constraint, because selfdestruct is treated as a
// new scope that is entered and immediately exited.
if self.enter_fn.is_some() {
let call = self.active_call();
let frame =
CallFrame { contract: call.contract.clone(), kind: call.kind, gas: call.gas_limit };
let _ = self.try_enter(frame);
}
// exit with empty frame result ref <https://github.com/ethereum/go-ethereum/blob/0004c6b229b787281760b14fb9460ffd9c2496f1/core/vm/instructions.go#L829-L829>
if self.exit_fn.is_some() {
let frame_result = FrameResult { gas_used: 0, output: Bytes::new(), error: None };
let _ = self.try_exit(frame_result);
}
}
}
/// Request variants to be sent from the inspector to the database
#[derive(Debug, Clone)]
pub enum JsDbRequest {
/// Bindings for [Database::basic]
Basic {
/// The address of the account to be loaded
address: Address,
/// The response channel
resp: std::sync::mpsc::Sender<Result<Option<AccountInfo>, String>>,
},
/// Bindings for [Database::code_by_hash]
Code {
/// The code hash of the code to be loaded
code_hash: B256,
/// The response channel
resp: std::sync::mpsc::Sender<Result<Bytes, String>>,
},
/// Bindings for [Database::storage]
StorageAt {
/// The address of the account
address: Address,
/// Index of the storage slot
index: U256,
/// The response channel
resp: std::sync::mpsc::Sender<Result<U256, String>>,
},
}
/// Represents an active call
#[derive(Debug)]
struct CallStackItem {
contract: Contract,
kind: CallKind,
gas_limit: u64,
}
/// Error variants that can occur during JavaScript inspection.
#[derive(Debug, thiserror::Error)]
pub enum JsInspectorError {
/// Error originating from a JavaScript operation.
#[error(transparent)]
JsError(#[from] JsError),
/// Failure during the evaluation of JavaScript code.
#[error("failed to evaluate JS code: {0}")]
EvalCode(JsError),
/// The evaluated code is not a JavaScript object.
#[error("the evaluated code is not a JS object")]
ExpectedJsObject,
/// The trace object must expose a function named `result()`.
#[error("trace object must expose a function result()")]
ResultFunctionMissing,
/// The trace object must expose a function named `fault()`.
#[error("trace object must expose a function fault()")]
FaultFunctionMissing,
/// The setup object must be a callable function.
#[error("setup object must be a function")]
SetupFunctionNotCallable,
/// Failure during the invocation of the `setup()` function.
#[error("failed to call setup(): {0}")]
SetupCallFailed(JsError),
/// Invalid JSON configuration encountered.
#[error("invalid JSON config: {0}")]
InvalidJsonConfig(JsError),
}

View File

@ -1,563 +0,0 @@
use self::parity::stack_push_count;
use crate::tracing::{
arena::PushTraceKind,
types::{
CallKind, CallTraceNode, LogCallOrder, RecordedMemory, StorageChange, StorageChangeReason,
},
utils::gas_used,
};
use alloy_primitives::{Address, Bytes, Log, B256, U256};
pub use arena::CallTraceArena;
use revm::{
inspectors::GasInspector,
interpreter::{
opcode, return_ok, CallInputs, CallScheme, CreateInputs, Gas, InstructionResult,
Interpreter, OpCode,
},
primitives::SpecId,
Database, EVMData, Inspector, JournalEntry,
};
use types::{CallTrace, CallTraceStep};
mod arena;
mod builder;
mod config;
mod fourbyte;
mod opcount;
pub mod types;
mod utils;
pub use builder::{
geth::{self, GethTraceBuilder},
parity::{self, ParityTraceBuilder},
};
pub use config::{StackSnapshotType, TracingInspectorConfig};
pub use fourbyte::FourByteInspector;
pub use opcount::OpcodeCountInspector;
#[cfg(feature = "js-tracer")]
pub mod js;
/// An inspector that collects call traces.
///
/// This [Inspector] can be hooked into the [EVM](revm::EVM) which then calls the inspector
/// functions, such as [Inspector::call] or [Inspector::call_end].
///
/// The [TracingInspector] keeps track of everything by:
/// 1. start tracking steps/calls on [Inspector::step] and [Inspector::call]
/// 2. complete steps/calls on [Inspector::step_end] and [Inspector::call_end]
#[derive(Debug, Clone)]
pub struct TracingInspector {
/// Configures what and how the inspector records traces.
config: TracingInspectorConfig,
/// Records all call traces
traces: CallTraceArena,
/// Tracks active calls
trace_stack: Vec<usize>,
/// Tracks active steps
step_stack: Vec<StackStep>,
/// Tracks the return value of the last call
last_call_return_data: Option<Bytes>,
/// The gas inspector used to track remaining gas.
gas_inspector: GasInspector,
/// The spec id of the EVM.
///
/// This is filled during execution.
spec_id: Option<SpecId>,
}
// === impl TracingInspector ===
impl TracingInspector {
/// Returns a new instance for the given config
pub fn new(config: TracingInspectorConfig) -> Self {
Self {
config,
traces: Default::default(),
trace_stack: vec![],
step_stack: vec![],
last_call_return_data: None,
gas_inspector: Default::default(),
spec_id: None,
}
}
/// Returns the config of the inspector.
pub fn config(&self) -> &TracingInspectorConfig {
&self.config
}
/// Gets a reference to the recorded call traces.
pub fn get_traces(&self) -> &CallTraceArena {
&self.traces
}
/// Gets a mutable reference to the recorded call traces.
pub fn get_traces_mut(&mut self) -> &mut CallTraceArena {
&mut self.traces
}
/// Manually the gas used of the root trace.
///
/// This is useful if the root trace's gasUsed should mirror the actual gas used by the
/// transaction.
///
/// This allows setting it manually by consuming the execution result's gas for example.
#[inline]
pub fn set_transaction_gas_used(&mut self, gas_used: u64) {
if let Some(node) = self.traces.arena.first_mut() {
node.trace.gas_used = gas_used;
}
}
/// Convenience function for [ParityTraceBuilder::set_transaction_gas_used] that consumes the
/// type.
#[inline]
pub fn with_transaction_gas_used(mut self, gas_used: u64) -> Self {
self.set_transaction_gas_used(gas_used);
self
}
/// Consumes the Inspector and returns a [ParityTraceBuilder].
#[inline]
pub fn into_parity_builder(self) -> ParityTraceBuilder {
ParityTraceBuilder::new(self.traces.arena, self.spec_id, self.config)
}
/// Consumes the Inspector and returns a [GethTraceBuilder].
#[inline]
pub fn into_geth_builder(self) -> GethTraceBuilder {
GethTraceBuilder::new(self.traces.arena, self.config)
}
/// Returns true if we're no longer in the context of the root call.
fn is_deep(&self) -> bool {
// the root call will always be the first entry in the trace stack
!self.trace_stack.is_empty()
}
/// Returns true if this a call to a precompile contract.
///
/// Returns true if the `to` address is a precompile contract and the value is zero.
#[inline]
fn is_precompile_call<DB: Database>(
&self,
data: &EVMData<'_, DB>,
to: &Address,
value: U256,
) -> bool {
if data.precompiles.contains(to) {
// only if this is _not_ the root call
return self.is_deep() && value.is_zero()
}
false
}
/// Returns the currently active call trace.
///
/// This will be the last call trace pushed to the stack: the call we entered most recently.
#[track_caller]
#[inline]
fn active_trace(&self) -> Option<&CallTraceNode> {
self.trace_stack.last().map(|idx| &self.traces.arena[*idx])
}
/// Returns the last trace [CallTrace] index from the stack.
///
/// This will be the currently active call trace.
///
/// # Panics
///
/// If no [CallTrace] was pushed
#[track_caller]
#[inline]
fn last_trace_idx(&self) -> usize {
self.trace_stack.last().copied().expect("can't start step without starting a trace first")
}
/// _Removes_ the last trace [CallTrace] index from the stack.
///
/// # Panics
///
/// If no [CallTrace] was pushed
#[track_caller]
#[inline]
fn pop_trace_idx(&mut self) -> usize {
self.trace_stack.pop().expect("more traces were filled than started")
}
/// Starts tracking a new trace.
///
/// Invoked on [Inspector::call].
#[allow(clippy::too_many_arguments)]
fn start_trace_on_call<DB: Database>(
&mut self,
data: &EVMData<'_, DB>,
address: Address,
input_data: Bytes,
value: U256,
kind: CallKind,
caller: Address,
mut gas_limit: u64,
maybe_precompile: Option<bool>,
) {
// This will only be true if the inspector is configured to exclude precompiles and the call
// is to a precompile
let push_kind = if maybe_precompile.unwrap_or(false) {
// We don't want to track precompiles
PushTraceKind::PushOnly
} else {
PushTraceKind::PushAndAttachToParent
};
if self.trace_stack.is_empty() {
// this is the root call which should get the original gas limit of the transaction,
// because initialization costs are already subtracted from gas_limit
// For the root call this value should use the transaction's gas limit
// See <https://github.com/paradigmxyz/reth/issues/3678> and <https://github.com/ethereum/go-ethereum/pull/27029>
gas_limit = data.env.tx.gas_limit;
// we set the spec id here because we only need to do this once and this condition is
// hit exactly once
self.spec_id = Some(data.env.cfg.spec_id);
}
self.trace_stack.push(self.traces.push_trace(
0,
push_kind,
CallTrace {
depth: data.journaled_state.depth() as usize,
address,
kind,
data: input_data,
value,
status: InstructionResult::Continue,
caller,
maybe_precompile,
gas_limit,
..Default::default()
},
));
}
/// Fills the current trace with the outcome of a call.
///
/// Invoked on [Inspector::call_end].
///
/// # Panics
///
/// This expects an existing trace [Self::start_trace_on_call]
fn fill_trace_on_call_end<DB: Database>(
&mut self,
data: &EVMData<'_, DB>,
status: InstructionResult,
gas: &Gas,
output: Bytes,
created_address: Option<Address>,
) {
let trace_idx = self.pop_trace_idx();
let trace = &mut self.traces.arena[trace_idx].trace;
if trace_idx == 0 {
// this is the root call which should get the gas used of the transaction
// refunds are applied after execution, which is when the root call ends
trace.gas_used = gas_used(data.env.cfg.spec_id, gas.spend(), gas.refunded() as u64);
} else {
trace.gas_used = gas.spend();
}
trace.status = status;
trace.success = matches!(status, return_ok!());
trace.output = output.clone();
self.last_call_return_data = Some(output);
if let Some(address) = created_address {
// A new contract was created via CREATE
trace.address = address;
}
}
/// Starts tracking a step
///
/// Invoked on [Inspector::step]
///
/// # Panics
///
/// This expects an existing [CallTrace], in other words, this panics if not within the context
/// of a call.
fn start_step<DB: Database>(&mut self, interp: &Interpreter<'_>, data: &EVMData<'_, DB>) {
let trace_idx = self.last_trace_idx();
let trace = &mut self.traces.arena[trace_idx];
self.step_stack.push(StackStep { trace_idx, step_idx: trace.trace.steps.len() });
let memory = self
.config
.record_memory_snapshots
.then(|| RecordedMemory::new(interp.shared_memory.context_memory().to_vec()))
.unwrap_or_default();
let stack = if self.config.record_stack_snapshots.is_full() {
Some(interp.stack.data().clone())
} else {
None
};
let op = OpCode::new(interp.current_opcode())
.or_else(|| {
// if the opcode is invalid, we'll use the invalid opcode to represent it because
// this is invoked before the opcode is executed, the evm will eventually return a
// `Halt` with invalid/unknown opcode as result
let invalid_opcode = 0xfe;
OpCode::new(invalid_opcode)
})
.expect("is valid opcode;");
trace.trace.steps.push(CallTraceStep {
depth: data.journaled_state.depth(),
pc: interp.program_counter(),
op,
contract: interp.contract.address,
stack,
push_stack: None,
memory_size: memory.len(),
memory,
gas_remaining: self.gas_inspector.gas_remaining(),
gas_refund_counter: interp.gas.refunded() as u64,
// fields will be populated end of call
gas_cost: 0,
storage_change: None,
status: InstructionResult::Continue,
});
}
/// Fills the current trace with the output of a step.
///
/// Invoked on [Inspector::step_end].
fn fill_step_on_step_end<DB: Database>(
&mut self,
interp: &Interpreter<'_>,
data: &EVMData<'_, DB>,
) {
let StackStep { trace_idx, step_idx } =
self.step_stack.pop().expect("can't fill step without starting a step first");
let step = &mut self.traces.arena[trace_idx].trace.steps[step_idx];
if self.config.record_stack_snapshots.is_pushes() {
let num_pushed = stack_push_count(step.op);
let start = interp.stack.len() - num_pushed;
step.push_stack = Some(interp.stack.data()[start..].to_vec());
}
if self.config.record_memory_snapshots {
// resize memory so opcodes that allocated memory is correctly displayed
if interp.shared_memory.len() > step.memory.len() {
step.memory.resize(interp.shared_memory.len());
}
}
if self.config.record_state_diff {
let op = step.op.get();
let journal_entry = data
.journaled_state
.journal
.last()
// This should always work because revm initializes it as `vec![vec![]]`
// See [JournaledState::new](revm::JournaledState)
.expect("exists; initialized with vec")
.last();
step.storage_change = match (op, journal_entry) {
(
opcode::SLOAD | opcode::SSTORE,
Some(JournalEntry::StorageChange { address, key, had_value }),
) => {
// SAFETY: (Address,key) exists if part if StorageChange
let value = data.journaled_state.state[address].storage[key].present_value();
let reason = match op {
opcode::SLOAD => StorageChangeReason::SLOAD,
opcode::SSTORE => StorageChangeReason::SSTORE,
_ => unreachable!(),
};
let change = StorageChange { key: *key, value, had_value: *had_value, reason };
Some(change)
}
_ => None,
};
}
// The gas cost is the difference between the recorded gas remaining at the start of the
// step the remaining gas here, at the end of the step.
step.gas_cost = step.gas_remaining - self.gas_inspector.gas_remaining();
// set the status
step.status = interp.instruction_result;
}
}
impl<DB> Inspector<DB> for TracingInspector
where
DB: Database,
{
fn initialize_interp(&mut self, interp: &mut Interpreter<'_>, data: &mut EVMData<'_, DB>) {
self.gas_inspector.initialize_interp(interp, data)
}
fn step(&mut self, interp: &mut Interpreter<'_>, data: &mut EVMData<'_, DB>) {
if self.config.record_steps {
self.gas_inspector.step(interp, data);
self.start_step(interp, data);
}
}
fn log(
&mut self,
evm_data: &mut EVMData<'_, DB>,
address: &Address,
topics: &[B256],
data: &Bytes,
) {
self.gas_inspector.log(evm_data, address, topics, data);
let trace_idx = self.last_trace_idx();
let trace = &mut self.traces.arena[trace_idx];
if self.config.record_logs {
trace.ordering.push(LogCallOrder::Log(trace.logs.len()));
trace.logs.push(Log::new_unchecked(topics.to_vec(), data.clone()));
}
}
fn step_end(&mut self, interp: &mut Interpreter<'_>, data: &mut EVMData<'_, DB>) {
if self.config.record_steps {
self.gas_inspector.step_end(interp, data);
self.fill_step_on_step_end(interp, data);
}
}
fn call(
&mut self,
data: &mut EVMData<'_, DB>,
inputs: &mut CallInputs,
) -> (InstructionResult, Gas, Bytes) {
self.gas_inspector.call(data, inputs);
// determine correct `from` and `to` based on the call scheme
let (from, to) = match inputs.context.scheme {
CallScheme::DelegateCall | CallScheme::CallCode => {
(inputs.context.address, inputs.context.code_address)
}
_ => (inputs.context.caller, inputs.context.address),
};
let value = if matches!(inputs.context.scheme, CallScheme::DelegateCall) {
// for delegate calls we need to use the value of the top trace
if let Some(parent) = self.active_trace() {
parent.trace.value
} else {
inputs.transfer.value
}
} else {
inputs.transfer.value
};
// if calls to precompiles should be excluded, check whether this is a call to a precompile
let maybe_precompile =
self.config.exclude_precompile_calls.then(|| self.is_precompile_call(data, &to, value));
self.start_trace_on_call(
data,
to,
inputs.input.clone(),
value,
inputs.context.scheme.into(),
from,
inputs.gas_limit,
maybe_precompile,
);
(InstructionResult::Continue, Gas::new(0), Bytes::new())
}
fn call_end(
&mut self,
data: &mut EVMData<'_, DB>,
inputs: &CallInputs,
gas: Gas,
ret: InstructionResult,
out: Bytes,
) -> (InstructionResult, Gas, Bytes) {
self.gas_inspector.call_end(data, inputs, gas, ret, out.clone());
self.fill_trace_on_call_end(data, ret, &gas, out.clone(), None);
(ret, gas, out)
}
fn create(
&mut self,
data: &mut EVMData<'_, DB>,
inputs: &mut CreateInputs,
) -> (InstructionResult, Option<Address>, Gas, Bytes) {
self.gas_inspector.create(data, inputs);
let _ = data.journaled_state.load_account(inputs.caller, data.db);
let nonce = data.journaled_state.account(inputs.caller).info.nonce;
self.start_trace_on_call(
data,
inputs.created_address(nonce),
inputs.init_code.clone(),
inputs.value,
inputs.scheme.into(),
inputs.caller,
inputs.gas_limit,
Some(false),
);
(InstructionResult::Continue, None, Gas::new(inputs.gas_limit), Bytes::default())
}
/// Called when a contract has been created.
///
/// InstructionResulting anything other than the values passed to this function (`(ret,
/// remaining_gas, address, out)`) will alter the result of the create.
fn create_end(
&mut self,
data: &mut EVMData<'_, DB>,
inputs: &CreateInputs,
status: InstructionResult,
address: Option<Address>,
gas: Gas,
retdata: Bytes,
) -> (InstructionResult, Option<Address>, Gas, Bytes) {
self.gas_inspector.create_end(data, inputs, status, address, gas, retdata.clone());
// get the code of the created contract
let code = address
.and_then(|address| {
data.journaled_state
.account(address)
.info
.code
.as_ref()
.map(|code| code.bytes()[..code.len()].to_vec())
})
.unwrap_or_default();
self.fill_trace_on_call_end(data, status, &gas, code.into(), address);
(status, address, gas, retdata)
}
fn selfdestruct(&mut self, _contract: Address, target: Address, _value: U256) {
let trace_idx = self.last_trace_idx();
let trace = &mut self.traces.arena[trace_idx].trace;
trace.selfdestruct_refund_target = Some(target)
}
}
#[derive(Debug, Clone, Copy)]
struct StackStep {
trace_idx: usize,
step_idx: usize,
}

View File

@ -1,29 +0,0 @@
//! Opcount tracing inspector that simply counts all opcodes.
//!
//! See also <https://geth.ethereum.org/docs/developers/evm-tracing/built-in-tracers>
use revm::{interpreter::Interpreter, Database, EVMData, Inspector};
/// An inspector that counts all opcodes.
#[derive(Debug, Clone, Copy, Default)]
pub struct OpcodeCountInspector {
/// opcode counter
count: usize,
}
impl OpcodeCountInspector {
/// Returns the opcode counter
#[inline]
pub fn count(&self) -> usize {
self.count
}
}
impl<DB> Inspector<DB> for OpcodeCountInspector
where
DB: Database,
{
fn step(&mut self, _interp: &mut Interpreter<'_>, _data: &mut EVMData<'_, DB>) {
self.count += 1;
}
}

View File

@ -1,684 +0,0 @@
//! Types for representing call trace items.
use crate::tracing::{config::TraceStyle, utils, utils::convert_memory};
pub use alloy_primitives::Log;
use alloy_primitives::{Address, Bytes, U256, U64};
use alloy_rpc_trace_types::{
geth::{CallFrame, CallLogFrame, GethDefaultTracingOptions, StructLog},
parity::{
Action, ActionType, CallAction, CallOutput, CallType, CreateAction, CreateOutput,
SelfdestructAction, TraceOutput, TransactionTrace,
},
};
use revm::interpreter::{opcode, CallContext, CallScheme, CreateScheme, InstructionResult, OpCode};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, VecDeque};
/// A trace of a call.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CallTrace {
/// The depth of the call
pub depth: usize,
/// Whether the call was successful
pub success: bool,
/// caller of this call
pub caller: Address,
/// The destination address of the call or the address from the created contract.
///
/// In other words, this is the callee if the [CallKind::Call] or the address of the created
/// contract if [CallKind::Create].
pub address: Address,
/// Whether this is a call to a precompile
///
/// Note: This is an Option because not all tracers make use of this
pub maybe_precompile: Option<bool>,
/// Holds the target for the selfdestruct refund target if `status` is
/// [InstructionResult::SelfDestruct]
pub selfdestruct_refund_target: Option<Address>,
/// The kind of call this is
pub kind: CallKind,
/// The value transferred in the call
pub value: U256,
/// The calldata for the call, or the init code for contract creations
pub data: Bytes,
/// The return data of the call if this was not a contract creation, otherwise it is the
/// runtime bytecode of the created contract
pub output: Bytes,
/// The gas cost of the call
pub gas_used: u64,
/// The gas limit of the call
pub gas_limit: u64,
/// The status of the trace's call
pub status: InstructionResult,
/// call context of the runtime
pub call_context: Option<Box<CallContext>>,
/// Opcode-level execution steps
pub steps: Vec<CallTraceStep>,
}
impl CallTrace {
/// Returns true if the status code is an error or revert, See [InstructionResult::Revert]
#[inline]
pub fn is_error(&self) -> bool {
!self.status.is_ok()
}
/// Returns true if the status code is a revert
#[inline]
pub fn is_revert(&self) -> bool {
self.status == InstructionResult::Revert
}
/// Returns the error message if it is an erroneous result.
pub(crate) fn as_error_msg(&self, kind: TraceStyle) -> Option<String> {
// See also <https://github.com/ethereum/go-ethereum/blob/34d507215951fb3f4a5983b65e127577989a6db8/eth/tracers/native/call_flat.go#L39-L55>
self.is_error().then(|| match self.status {
InstructionResult::Revert => {
if kind.is_parity() { "Reverted" } else { "execution reverted" }.to_string()
}
InstructionResult::OutOfGas | InstructionResult::MemoryOOG => {
if kind.is_parity() { "Out of gas" } else { "out of gas" }.to_string()
}
InstructionResult::OpcodeNotFound => {
if kind.is_parity() { "Bad instruction" } else { "invalid opcode" }.to_string()
}
InstructionResult::StackOverflow => "Out of stack".to_string(),
InstructionResult::InvalidJump => {
if kind.is_parity() { "Bad jump destination" } else { "invalid jump destination" }
.to_string()
}
InstructionResult::PrecompileError => {
if kind.is_parity() { "Built-in failed" } else { "precompiled failed" }.to_string()
}
status => format!("{:?}", status),
})
}
}
impl Default for CallTrace {
fn default() -> Self {
Self {
depth: Default::default(),
success: Default::default(),
caller: Default::default(),
address: Default::default(),
selfdestruct_refund_target: None,
kind: Default::default(),
value: Default::default(),
data: Default::default(),
maybe_precompile: None,
output: Default::default(),
gas_used: Default::default(),
gas_limit: Default::default(),
status: InstructionResult::Continue,
call_context: Default::default(),
steps: Default::default(),
}
}
}
/// A node in the arena
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub struct CallTraceNode {
/// Parent node index in the arena
pub parent: Option<usize>,
/// Children node indexes in the arena
pub children: Vec<usize>,
/// This node's index in the arena
pub idx: usize,
/// The call trace
pub trace: CallTrace,
/// Recorded logs, if enabled
pub logs: Vec<Log>,
/// Ordering of child calls and logs
pub ordering: Vec<LogCallOrder>,
}
impl CallTraceNode {
/// Returns the call context's execution address
///
/// See `Inspector::call` impl of [TracingInspector](crate::tracing::TracingInspector)
pub fn execution_address(&self) -> Address {
if self.trace.kind.is_delegate() {
self.trace.caller
} else {
self.trace.address
}
}
/// Returns all storage slots touched by this trace and the value this storage.
///
/// A touched slot is either a slot that was written to or read from.
///
/// If the slot is accessed more than once, the result only includes the first time it was
/// accessed, in other words in only returns the original value of the slot.
pub fn touched_slots(&self) -> BTreeMap<U256, U256> {
let mut touched_slots = BTreeMap::new();
for change in self.trace.steps.iter().filter_map(|s| s.storage_change.as_ref()) {
match touched_slots.entry(change.key) {
std::collections::btree_map::Entry::Vacant(entry) => {
entry.insert(change.value);
}
std::collections::btree_map::Entry::Occupied(_) => {
// already touched
}
}
}
touched_slots
}
/// Pushes all steps onto the stack in reverse order
/// so that the first step is on top of the stack
pub(crate) fn push_steps_on_stack<'a>(
&'a self,
stack: &mut VecDeque<CallTraceStepStackItem<'a>>,
) {
stack.extend(self.call_step_stack().into_iter().rev());
}
/// Returns a list of all steps in this trace in the order they were executed
///
/// If the step is a call, the id of the child trace is set.
pub(crate) fn call_step_stack(&self) -> Vec<CallTraceStepStackItem<'_>> {
let mut stack = Vec::with_capacity(self.trace.steps.len());
let mut child_id = 0;
for step in self.trace.steps.iter() {
let mut item = CallTraceStepStackItem { trace_node: self, step, call_child_id: None };
// If the opcode is a call, put the child trace on the stack
if step.is_calllike_op() {
// The opcode of this step is a call but it's possible that this step resulted
// in a revert or out of gas error in which case there's no actual child call executed and recorded: <https://github.com/paradigmxyz/reth/issues/3915>
if let Some(call_id) = self.children.get(child_id).copied() {
item.call_child_id = Some(call_id);
child_id += 1;
}
}
stack.push(item);
}
stack
}
/// Returns true if this is a call to a precompile
#[inline]
pub(crate) fn is_precompile(&self) -> bool {
self.trace.maybe_precompile.unwrap_or(false)
}
/// Returns the kind of call the trace belongs to
#[inline]
pub(crate) const fn kind(&self) -> CallKind {
self.trace.kind
}
/// Returns the status of the call
#[inline]
pub(crate) const fn status(&self) -> InstructionResult {
self.trace.status
}
/// Returns true if the call was a selfdestruct
#[inline]
pub(crate) fn is_selfdestruct(&self) -> bool {
self.status() == InstructionResult::SelfDestruct
}
/// Converts this node into a parity `TransactionTrace`
pub(crate) fn parity_transaction_trace(&self, trace_address: Vec<usize>) -> TransactionTrace {
let action = self.parity_action();
let result = if self.trace.is_error() && !self.trace.is_revert() {
// if the trace is a selfdestruct or an error that is not a revert, the result is None
None
} else {
Some(self.parity_trace_output())
};
let error = self.trace.as_error_msg(TraceStyle::Parity);
TransactionTrace { action, error, result, trace_address, subtraces: self.children.len() }
}
/// Returns the `Output` for a parity trace
pub(crate) fn parity_trace_output(&self) -> TraceOutput {
match self.kind() {
CallKind::Call | CallKind::StaticCall | CallKind::CallCode | CallKind::DelegateCall => {
TraceOutput::Call(CallOutput {
gas_used: U64::from(self.trace.gas_used),
output: self.trace.output.clone(),
})
}
CallKind::Create | CallKind::Create2 => TraceOutput::Create(CreateOutput {
gas_used: U64::from(self.trace.gas_used),
code: self.trace.output.clone(),
address: self.trace.address,
}),
}
}
/// If the trace is a selfdestruct, returns the `Action` for a parity trace.
pub(crate) fn parity_selfdestruct_action(&self) -> Option<Action> {
if self.is_selfdestruct() {
Some(Action::Selfdestruct(SelfdestructAction {
address: self.trace.address,
refund_address: self.trace.selfdestruct_refund_target.unwrap_or_default(),
balance: self.trace.value,
}))
} else {
None
}
}
/// If the trace is a selfdestruct, returns the `CallFrame` for a geth call trace
pub(crate) fn geth_selfdestruct_call_trace(&self) -> Option<CallFrame> {
if self.is_selfdestruct() {
Some(CallFrame {
typ: "SELFDESTRUCT".to_string(),
from: self.trace.caller,
to: self.trace.selfdestruct_refund_target,
value: Some(self.trace.value),
..Default::default()
})
} else {
None
}
}
/// If the trace is a selfdestruct, returns the `TransactionTrace` for a parity trace.
pub(crate) fn parity_selfdestruct_trace(
&self,
trace_address: Vec<usize>,
) -> Option<TransactionTrace> {
let trace = self.parity_selfdestruct_action()?;
Some(TransactionTrace {
action: trace,
error: None,
result: None,
trace_address,
subtraces: 0,
})
}
/// Returns the `Action` for a parity trace.
///
/// Caution: This does not include the selfdestruct action, if the trace is a selfdestruct,
/// since those are handled in addition to the call action.
pub(crate) fn parity_action(&self) -> Action {
match self.kind() {
CallKind::Call | CallKind::StaticCall | CallKind::CallCode | CallKind::DelegateCall => {
Action::Call(CallAction {
from: self.trace.caller,
to: self.trace.address,
value: self.trace.value,
gas: U64::from(self.trace.gas_limit),
input: self.trace.data.clone(),
call_type: self.kind().into(),
})
}
CallKind::Create | CallKind::Create2 => Action::Create(CreateAction {
from: self.trace.caller,
value: self.trace.value,
gas: U64::from(self.trace.gas_limit),
init: self.trace.data.clone(),
}),
}
}
/// Converts this call trace into an _empty_ geth [CallFrame]
pub(crate) fn geth_empty_call_frame(&self, include_logs: bool) -> CallFrame {
let mut call_frame = CallFrame {
typ: self.trace.kind.to_string(),
from: self.trace.caller,
to: Some(self.trace.address),
value: Some(self.trace.value),
gas: U256::from(self.trace.gas_limit),
gas_used: U256::from(self.trace.gas_used),
input: self.trace.data.clone(),
output: (!self.trace.output.is_empty()).then(|| self.trace.output.clone()),
error: None,
revert_reason: None,
calls: Default::default(),
logs: Default::default(),
};
if self.trace.kind.is_static_call() {
// STATICCALL frames don't have a value
call_frame.value = None;
}
// we need to populate error and revert reason
if !self.trace.success {
call_frame.revert_reason = utils::maybe_revert_reason(self.trace.output.as_ref());
// Note: the call tracer mimics parity's trace transaction and geth maps errors to parity style error messages, <https://github.com/ethereum/go-ethereum/blob/34d507215951fb3f4a5983b65e127577989a6db8/eth/tracers/native/call_flat.go#L39-L55>
call_frame.error = self.trace.as_error_msg(TraceStyle::Parity);
}
if include_logs && !self.logs.is_empty() {
call_frame.logs = self
.logs
.iter()
.map(|log| CallLogFrame {
address: Some(self.execution_address()),
topics: Some(log.topics().to_vec()),
data: Some(log.data.clone()),
})
.collect();
}
call_frame
}
}
/// A unified representation of a call.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum CallKind {
/// Represents a regular call.
#[default]
Call,
/// Represents a static call.
StaticCall,
/// Represents a call code operation.
CallCode,
/// Represents a delegate call.
DelegateCall,
/// Represents a contract creation operation.
Create,
/// Represents a contract creation operation using the CREATE2 opcode.
Create2,
}
impl CallKind {
/// Returns true if the call is a create
#[inline]
pub fn is_any_create(&self) -> bool {
matches!(self, CallKind::Create | CallKind::Create2)
}
/// Returns true if the call is a delegate of some sorts
#[inline]
pub fn is_delegate(&self) -> bool {
matches!(self, CallKind::DelegateCall | CallKind::CallCode)
}
/// Returns true if the call is [CallKind::StaticCall].
#[inline]
pub fn is_static_call(&self) -> bool {
matches!(self, CallKind::StaticCall)
}
}
impl std::fmt::Display for CallKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CallKind::Call => {
write!(f, "CALL")
}
CallKind::StaticCall => {
write!(f, "STATICCALL")
}
CallKind::CallCode => {
write!(f, "CALLCODE")
}
CallKind::DelegateCall => {
write!(f, "DELEGATECALL")
}
CallKind::Create => {
write!(f, "CREATE")
}
CallKind::Create2 => {
write!(f, "CREATE2")
}
}
}
}
impl From<CallScheme> for CallKind {
fn from(scheme: CallScheme) -> Self {
match scheme {
CallScheme::Call => CallKind::Call,
CallScheme::StaticCall => CallKind::StaticCall,
CallScheme::CallCode => CallKind::CallCode,
CallScheme::DelegateCall => CallKind::DelegateCall,
}
}
}
impl From<CreateScheme> for CallKind {
fn from(create: CreateScheme) -> Self {
match create {
CreateScheme::Create => CallKind::Create,
CreateScheme::Create2 { .. } => CallKind::Create2,
}
}
}
impl From<CallKind> for ActionType {
fn from(kind: CallKind) -> Self {
match kind {
CallKind::Call | CallKind::StaticCall | CallKind::DelegateCall | CallKind::CallCode => {
ActionType::Call
}
CallKind::Create => ActionType::Create,
CallKind::Create2 => ActionType::Create,
}
}
}
impl From<CallKind> for CallType {
fn from(ty: CallKind) -> Self {
match ty {
CallKind::Call => CallType::Call,
CallKind::StaticCall => CallType::StaticCall,
CallKind::CallCode => CallType::CallCode,
CallKind::DelegateCall => CallType::DelegateCall,
CallKind::Create => CallType::None,
CallKind::Create2 => CallType::None,
}
}
}
pub(crate) struct CallTraceStepStackItem<'a> {
/// The trace node that contains this step
pub(crate) trace_node: &'a CallTraceNode,
/// The step that this stack item represents
pub(crate) step: &'a CallTraceStep,
/// The index of the child call in the CallArena if this step's opcode is a call
pub(crate) call_child_id: Option<usize>,
}
/// Ordering enum for calls and logs
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LogCallOrder {
/// Contains the index of the corresponding log
Log(usize),
/// Contains the index of the corresponding trace node
Call(usize),
}
/// Represents a tracked call step during execution
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CallTraceStep {
// Fields filled in `step`
/// Call depth
pub depth: u64,
/// Program counter before step execution
pub pc: usize,
/// Opcode to be executed
pub op: OpCode,
/// Current contract address
pub contract: Address,
/// Stack before step execution
pub stack: Option<Vec<U256>>,
/// The new stack items placed by this step if any
pub push_stack: Option<Vec<U256>>,
/// All allocated memory in a step
///
/// This will be empty if memory capture is disabled
pub memory: RecordedMemory,
/// Size of memory at the beginning of the step
pub memory_size: usize,
/// Remaining gas before step execution
pub gas_remaining: u64,
/// Gas refund counter before step execution
pub gas_refund_counter: u64,
// Fields filled in `step_end`
/// Gas cost of step execution
pub gas_cost: u64,
/// Change of the contract state after step execution (effect of the SLOAD/SSTORE instructions)
pub storage_change: Option<StorageChange>,
/// Final status of the step
///
/// This is set after the step was executed.
pub status: InstructionResult,
}
// === impl CallTraceStep ===
impl CallTraceStep {
/// Converts this step into a geth [StructLog]
///
/// This sets memory and stack capture based on the `opts` parameter.
pub(crate) fn convert_to_geth_struct_log(&self, opts: &GethDefaultTracingOptions) -> StructLog {
let mut log = StructLog {
depth: self.depth,
error: self.as_error(),
gas: self.gas_remaining,
gas_cost: self.gas_cost,
op: self.op.to_string(),
pc: self.pc as u64,
refund_counter: (self.gas_refund_counter > 0).then_some(self.gas_refund_counter),
// Filled, if not disabled manually
stack: None,
// Filled in `CallTraceArena::geth_trace` as a result of compounding all slot changes
return_data: None,
// Filled via trace object
storage: None,
// Only enabled if `opts.enable_memory` is true
memory: None,
// This is None in the rpc response
memory_size: None,
};
if opts.is_stack_enabled() {
log.stack = self.stack.clone();
}
if opts.is_memory_enabled() {
log.memory = Some(self.memory.memory_chunks());
}
log
}
/// Returns true if the step is a STOP opcode
#[inline]
pub(crate) fn is_stop(&self) -> bool {
matches!(self.op.get(), opcode::STOP)
}
/// Returns true if the step is a call operation, any of
/// CALL, CALLCODE, DELEGATECALL, STATICCALL, CREATE, CREATE2
#[inline]
pub(crate) fn is_calllike_op(&self) -> bool {
matches!(
self.op.get(),
opcode::CALL |
opcode::DELEGATECALL |
opcode::STATICCALL |
opcode::CREATE |
opcode::CALLCODE |
opcode::CREATE2
)
}
// Returns true if the status code is an error or revert, See [InstructionResult::Revert]
#[inline]
pub(crate) fn is_error(&self) -> bool {
self.status as u8 >= InstructionResult::Revert as u8
}
/// Returns the error message if it is an erroneous result.
#[inline]
pub(crate) fn as_error(&self) -> Option<String> {
self.is_error().then(|| format!("{:?}", self.status))
}
}
/// Represents the source of a storage change - e.g., whether it came
/// from an SSTORE or SLOAD instruction.
#[allow(clippy::upper_case_acronyms)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StorageChangeReason {
/// SLOAD opcode
SLOAD,
/// SSTORE opcode
SSTORE,
}
/// Represents a storage change during execution.
///
/// This maps to evm internals:
/// [JournalEntry::StorageChange](revm::JournalEntry::StorageChange)
///
/// It is used to track both storage change and warm load of a storage slot. For warm load in regard
/// to EIP-2929 AccessList had_value will be None.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct StorageChange {
/// key of the storage slot
pub key: U256,
/// Current value of the storage slot
pub value: U256,
/// The previous value of the storage slot, if any
pub had_value: Option<U256>,
/// How this storage was accessed
pub reason: StorageChangeReason,
}
/// Represents the memory captured during execution
///
/// This is a wrapper around the [SharedMemory](revm::interpreter::SharedMemory) context memory.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct RecordedMemory(pub(crate) Vec<u8>);
impl RecordedMemory {
#[inline]
pub(crate) fn new(mem: Vec<u8>) -> Self {
Self(mem)
}
/// Returns the memory as a byte slice
#[inline]
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
#[inline]
pub(crate) fn resize(&mut self, size: usize) {
self.0.resize(size, 0);
}
/// Returns the size of the memory
#[inline]
pub fn len(&self) -> usize {
self.0.len()
}
/// Returns whether the memory is empty
#[inline]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
/// Converts the memory into 32byte hex chunks
#[inline]
pub fn memory_chunks(&self) -> Vec<String> {
convert_memory(self.as_bytes())
}
}
impl AsRef<[u8]> for RecordedMemory {
fn as_ref(&self) -> &[u8] {
self.as_bytes()
}
}

View File

@ -1,91 +0,0 @@
//! Util functions for revm related ops
use alloy_primitives::{hex, Bytes};
use alloy_sol_types::{ContractError, GenericRevertReason};
use revm::{
primitives::{SpecId, KECCAK_EMPTY},
DatabaseRef,
};
/// creates the memory data in 32byte chunks
/// see <https://github.com/ethereum/go-ethereum/blob/366d2169fbc0e0f803b68c042b77b6b480836dbc/eth/tracers/logger/logger.go#L450-L452>
#[inline]
pub(crate) fn convert_memory(data: &[u8]) -> Vec<String> {
let mut memory = Vec::with_capacity((data.len() + 31) / 32);
for idx in (0..data.len()).step_by(32) {
let len = std::cmp::min(idx + 32, data.len());
memory.push(hex::encode(&data[idx..len]));
}
memory
}
/// Get the gas used, accounting for refunds
#[inline]
pub(crate) fn gas_used(spec: SpecId, spent: u64, refunded: u64) -> u64 {
let refund_quotient = if SpecId::enabled(spec, SpecId::LONDON) { 5 } else { 2 };
spent - (refunded).min(spent / refund_quotient)
}
/// Loads the code for the given account from the account itself or the database
///
/// Returns None if the code hash is the KECCAK_EMPTY hash
#[inline]
pub(crate) fn load_account_code<DB: DatabaseRef>(
db: DB,
db_acc: &revm::primitives::AccountInfo,
) -> Option<Bytes> {
db_acc
.code
.as_ref()
.map(|code| code.original_bytes())
.or_else(|| {
if db_acc.code_hash == KECCAK_EMPTY {
None
} else {
db.code_by_hash_ref(db_acc.code_hash).ok().map(|code| code.original_bytes())
}
})
.map(Into::into)
}
/// Returns a non empty revert reason if the output is a revert/error.
#[inline]
pub(crate) fn maybe_revert_reason(output: &[u8]) -> Option<String> {
let reason = match GenericRevertReason::decode(output)? {
GenericRevertReason::ContractError(err) => {
match err {
ContractError::Revert(revert) => {
// return the raw revert reason and don't use the revert's display message
revert.reason
}
err => err.to_string(),
}
}
GenericRevertReason::RawString(err) => err,
};
if reason.is_empty() {
None
} else {
Some(reason)
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_sol_types::{GenericContractError, SolInterface};
#[test]
fn decode_empty_revert() {
let reason = GenericRevertReason::decode("".as_bytes()).map(|x| x.to_string());
assert_eq!(reason, Some("".to_string()));
}
#[test]
fn decode_revert_reason() {
let err = GenericContractError::Revert("my revert".into());
let encoded = err.abi_encode();
let reason = maybe_revert_reason(&encoded).unwrap();
assert_eq!(reason, "my revert");
}
}

View File

@ -23,7 +23,7 @@ pub mod state_change;
pub use factory::EvmProcessorFactory;
/// reexport for convenience
pub use reth_revm_inspectors::*;
pub use revm_inspectors::*;
/// Re-export everything
pub use revm::{self, *};

View File

@ -21,7 +21,7 @@ reth-provider = { workspace = true, features = ["test-utils"] }
reth-transaction-pool = { workspace = true, features = ["test-utils"] }
reth-network-api.workspace = true
reth-rpc-engine-api.workspace = true
reth-revm.workspace = true
reth-revm = { workspace = true, features = ["js-tracer"] }
reth-tasks.workspace = true
reth-consensus-common.workspace = true
reth-rpc-types-compat.workspace = true

View File

@ -18,9 +18,7 @@ multiple-versions = "warn"
wildcards = "allow"
highlight = "all"
# List of crates to deny
deny = [
{ name = "openssl" },
]
deny = [{ name = "openssl" }]
# Certain crates/versions that will be skipped when doing duplicate detection.
skip = []
# Similarly to `skip` allows you to skip certain crates during duplicate
@ -97,7 +95,7 @@ allow-git = [
# TODO: remove, see ./Cargo.toml
"https://github.com/bluealloy/revm",
"https://github.com/alloy-rs/alloy",
"https://github.com/ethereum/c-kzg-4844",
"https://github.com/paradigmxyz/evm-inspectors",
"https://github.com/sigp/discv5",
"https://github.com/stevefan1999-personal/rust-igd",
]