mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 10:59:55 +00:00
chore: move revm-inspectors to a separate repo (#5992)
This commit is contained in:
39
Cargo.lock
generated
39
Cargo.lock
generated
@ -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"
|
||||
|
||||
18
Cargo.toml
18
Cargo.toml
@ -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"]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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"]
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -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(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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()] }
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -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 ¤t.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,
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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, *};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user