Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions e2e/test/external/e2e-json-rpc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,50 @@ describe("JSON-RPC", () => {
expect(logEntry.topics.length).to.equal(2);
});
});

// Minimal coverage for the endpoint's own logic (state loading + never-sent semantics).
// The tracer engine itself is exercised exhaustively by the debug_traceTransaction block
// above, since both endpoints share it, so tracers are not re-tested per-variant here.
describe("debug_traceCall", () => {
it("callTracer traces a contract call that was never sent (latest)", async () => {
const contract = await deployTestRevertReason();
await sendEvmMine();
await contract.waitForDeployment();

const data = contract.interface.encodeFunctionData("revertWithKnownError", []);
const call = { from: ALICE.address, to: await contract.getAddress(), data };

const trace = await sendAndGetFullResponse("debug_traceCall", [call, "latest", { tracer: "callTracer" }]);

expect(trace.data.result.from).to.eq("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266");
expect(trace.data.result.to).to.eq((await contract.getAddress()).toLowerCase());
expect(trace.data.result.input).to.eq("0x2b3d7bd2");
expect(trace.data.result.output).to.eq("0x22aa4404");
expect(trace.data.result.error).to.eq("execution reverted");
expect(trace.data.result.type).to.eq("CALL");
});

it("traces against pending state without mutating it", async () => {
const contract = await deployTestContractBalances();
await sendEvmMine();
await contract.waitForDeployment();

// trace a state-changing call against pending — exercises the from_pending_block branch
const data = contract.interface.encodeFunctionData("add", [ALICE.address, 10]);
const call = { from: ALICE.address, to: await contract.getAddress(), data };

const trace = await sendAndGetFullResponse("debug_traceCall", [call, "pending", { tracer: "callTracer" }]);

expect(trace.data.result.to).to.eq((await contract.getAddress()).toLowerCase());
expect(trace.data.result.type).to.eq("CALL");
expect(trace.data.result.error).to.be.undefined;

// the traced call must NOT have persisted: ALICE's balance is still zero
const getData = contract.interface.encodeFunctionData("get", [ALICE.address]);
const balance = await send("eth_call", [{ to: await contract.getAddress(), data: getData }, "latest"]);
expect(balance).to.eq(toPaddedHex(0, 32));
});
});
});

describe("Call", () => {
Expand Down
228 changes: 155 additions & 73 deletions src/eth/executor/evm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use alloy_consensus::transaction::TransactionInfo;
use alloy_rpc_types_trace::geth::FourByteFrame;
use alloy_rpc_types_trace::geth::GethDebugBuiltInTracerType;
use alloy_rpc_types_trace::geth::GethDebugTracerType;
use alloy_rpc_types_trace::geth::GethDebugTracingOptions;
use alloy_rpc_types_trace::geth::GethTrace;
use alloy_rpc_types_trace::geth::NoopFrame;
use anyhow::anyhow;
Expand All @@ -19,6 +20,7 @@ use revm::InspectEvm;
use revm::context::result::ExecResultAndState;
use revm::context::result::ExecutionResult as RevmExecResult;
use revm::database::CacheDB;
use revm::primitives::hardfork::SpecId;
use revm_inspectors::tracing::FourByteInspector;
use revm_inspectors::tracing::MuxInspector;
use revm_inspectors::tracing::TracingInspector;
Expand All @@ -35,15 +37,20 @@ use crate::eth::executor::ExecutionResult;
use crate::eth::executor::ExecutorConfig;
use crate::eth::executor::TransactionExecution;
use crate::eth::executor::TransactionExecutionInput;
use crate::eth::executor::evm::types::CallExecutionInput;
use crate::eth::executor::evm::types::EvmInput;
use crate::eth::executor::evm::types::InspectorInput;
use crate::eth::executor::evm::util::EvmExt;
use crate::eth::executor::evm::util::create_evm;
use crate::eth::rpc::BlockFilter;
use crate::eth::rpc::RpcError;
use crate::eth::storage::ExecutionKind;
use crate::eth::storage::StorageError;
use crate::eth::storage::StratusStorage;
use crate::eth::types::CallInput;
use crate::eth::types::Hash;
use crate::eth::types::MinedData;
use crate::eth::types::PointInTime;
use crate::eth::types::StratusError;

pub type RevmResultAndState = ExecResultAndState<RevmExecResult>;
Expand Down Expand Up @@ -105,14 +112,24 @@ impl<Input: EvmInput> Evm<Input> {
}

impl Evm<TransactionExecutionInput> {
/// Execute a transaction using a tracer.
/// Execute a transaction or a synthetic call using a tracer.
pub fn inspect(&mut self, input: InspectorInput) -> Result<GethTrace, StratusError> {
let InspectorInput {
tx_hash,
opts,
trace_unsuccessful_only,
} = input;
let tracer_type = opts.tracer.ok_or_else(|| anyhow!("no tracer type provided"))?;
match input {
InspectorInput::Transaction {
tx_hash,
opts,
trace_unsuccessful_only,
} => self.inspect_transaction(tx_hash, opts, trace_unsuccessful_only),
InspectorInput::Call { call, point_in_time, opts } => self.inspect_call(call, point_in_time, opts),
}
}

/// Re-executes an already-mined transaction, looked up by hash, wrapped in the requested tracer.
///
/// Because Stratus only stores state at block boundaries, every transaction before the target one in the
/// same block is replayed first, to reconstruct the exact mid-block state the target transaction saw.
fn inspect_transaction(&mut self, tx_hash: Hash, opts: GethDebugTracingOptions, trace_unsuccessful_only: bool) -> Result<GethTrace, StratusError> {
let tracer_type = opts.tracer.clone().ok_or_else(|| anyhow!("no tracer type provided"))?;

if matches!(tracer_type, GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::NoopTracer)) {
return Ok(NoopFrame::default().into());
Expand Down Expand Up @@ -157,11 +174,12 @@ impl Evm<TransactionExecutionInput> {
self.evm.journaled_state.database.reset(ExecutionKind::CallPast(target));

let spec = self.evm.cfg.spec;
let chain_id: u64 = inspect_input.chain_id.unwrap_or_default().into();

let mut cache_db = CacheDB::new(&self.evm.journaled_state.database);
let mut evm = create_evm(inspect_input.chain_id.unwrap_or_default().into(), spec, &mut cache_db, self.kind);
let mut evm = create_evm(chain_id, spec, &mut cache_db, self.kind);

// Execute all transactions before target tx_hash
// Execute all transactions before target tx_hash, to reconstruct the mid-block state it saw.
for tx in block.transactions.into_iter() {
if tx.info.hash == tx_hash {
break;
Expand All @@ -174,73 +192,137 @@ impl Evm<TransactionExecutionInput> {
evm.transact_commit(tx)?;
}

let trace_result: GethTrace = match tracer_type {
GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::FourByteTracer) => {
let mut inspector = FourByteInspector::default();
let mut evm_with_inspector = evm.with_inspector(&mut inspector);
evm_with_inspector.fill_env(inspect_input);
let tx = std::mem::take(&mut evm_with_inspector.tx);
evm_with_inspector.inspect_tx(tx)?;
FourByteFrame::from(&inspector).into()
}
GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::CallTracer) => {
let call_config = opts.tracer_config.into_call_config()?;
let mut inspector = TracingInspector::new(TracingInspectorConfig::from_geth_call_config(&call_config));
let mut evm_with_inspector = evm.with_inspector(&mut inspector);
evm_with_inspector.fill_env(inspect_input);
let tx = std::mem::take(&mut evm_with_inspector.tx);
let res = evm_with_inspector.inspect_tx(tx)?;
let mut trace = inspector.geth_builder().geth_call_traces(call_config, res.result.tx_gas_used()).into();
enhance_trace_with_decoded_errors(&mut trace);
trace
}
GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::PreStateTracer) => {
let prestate_config = opts.tracer_config.into_pre_state_config()?;
let mut inspector = TracingInspector::new(TracingInspectorConfig::from_geth_prestate_config(&prestate_config));
let mut evm_with_inspector = evm.with_inspector(&mut inspector);
evm_with_inspector.fill_env(inspect_input);
let tx = std::mem::take(&mut evm_with_inspector.tx);
let res = evm_with_inspector.inspect_tx(tx)?;

inspector.geth_builder().geth_prestate_traces(&res, &prestate_config, &cache_db)?.into()
}
GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::NoopTracer) => NoopFrame::default().into(),
GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::MuxTracer) => {
let mux_config = opts.tracer_config.into_mux_config()?;
let mut inspector = MuxInspector::try_from_config(mux_config).map_err(|e| anyhow!(e))?;
let mut evm_with_inspector = evm.with_inspector(&mut inspector);
evm_with_inspector.fill_env(inspect_input);
let tx = std::mem::take(&mut evm_with_inspector.tx);
let res = evm_with_inspector.inspect_tx(tx)?;
inspector.try_into_mux_frame(&res, &cache_db, tx_info)?.into()
}
GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::FlatCallTracer) => {
let flat_call_config = opts.tracer_config.into_flat_call_config()?;
let mut inspector = TracingInspector::new(TracingInspectorConfig::from_flat_call_config(&flat_call_config));
let mut evm_with_inspector = evm.with_inspector(&mut inspector);
evm_with_inspector.fill_env(inspect_input);
let tx = std::mem::take(&mut evm_with_inspector.tx);
let res = evm_with_inspector.inspect_tx(tx)?;
inspector
.with_transaction_gas_limit(res.result.tx_gas_used())
.into_parity_builder()
.into_localized_transaction_traces(tx_info)
.into()
}
GethDebugTracerType::JsTracer(code) => {
let mut inspector = JsInspector::new(code, opts.tracer_config.into_json()).map_err(|e| anyhow!(e.to_string()))?;
let mut evm_with_inspector = evm.with_inspector(&mut inspector);
evm_with_inspector.fill_env(inspect_input);
let tx = std::mem::take(&mut evm_with_inspector.tx);
let block = std::mem::take(&mut evm_with_inspector.block);
let res = evm_with_inspector.inspect_tx(tx.clone())?;
GethTrace::JS(inspector.json_result(res, &tx, &block, &cache_db).map_err(|e| anyhow!(e.to_string()))?)
run_tracer(tracer_type, opts, spec, chain_id, self.kind, cache_db, inspect_input, tx_info)
}

/// Executes a synthetic call that was never signed or broadcast, wrapped in the requested tracer, against a
/// chosen point in time. Unlike [`Self::inspect_transaction`], there is no real position in a block to
/// reconstruct, so the call runs directly against the resolved state boundary — same as [`Self::execute`]
/// does for `eth_call`, just with a tracer attached.
fn inspect_call(&mut self, call: CallInput, point_in_time: PointInTime, opts: GethDebugTracingOptions) -> Result<GethTrace, StratusError> {
let tracer_type = opts.tracer.clone().ok_or_else(|| anyhow!("no tracer type provided"))?;

if matches!(tracer_type, GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::NoopTracer)) {
return Ok(NoopFrame::default().into());
}

let inspect_input: CallExecutionInput = match point_in_time {
PointInTime::Pending => {
let (pending_header, tx_count) = self.evm.journaled_state.database.storage.read_pending_block_header();
CallExecutionInput::from_pending_block(call, pending_header, tx_count)
}
GethDebugTracerType::BuiltInTracer(tracer) => {
return Err(anyhow!("tracer {tracer:?} is not implemented").into());
point_in_time => {
let Some(block) = self.evm.journaled_state.database.storage.read_block(point_in_time.into())? else {
return Err(RpcError::BlockFilterInvalid { filter: point_in_time.into() }.into());
};
CallExecutionInput::try_from_mined_block(call, block, point_in_time)?
}
};

Ok(trace_result)
let tx_info = TransactionInfo {
block_hash: None,
block_timestamp: Some(*inspect_input.block_timestamp),
hash: None,
index: None,
block_number: Some(inspect_input.block_number.as_u64()),
base_fee: None,
};

// point the base session at the resolved state boundary — no replay needed, this is the only execution
self.evm.journaled_state.database.reset(inspect_input.kind());

let spec = self.evm.cfg.spec;
let chain_id = self.evm.cfg.chain_id;
let cache_db = CacheDB::new(&self.evm.journaled_state.database);

run_tracer(tracer_type, opts, spec, chain_id, self.kind, cache_db, inspect_input, tx_info)
}
}

/// Runs `inspect_input` through the EVM wrapped in the inspector implied by `tracer_type`, against `cache_db`.
///
/// Shared by [`Evm::inspect_transaction`] (where `cache_db` already has the preceding transactions of the block
/// committed into it) and [`Evm::inspect_call`] (where `cache_db` is empty and reads fall straight through to
/// the underlying session).
#[allow(clippy::too_many_arguments)]
fn run_tracer<Input: EvmInput>(
tracer_type: GethDebugTracerType,
opts: GethDebugTracingOptions,
spec: SpecId,
chain_id: u64,
kind: EvmKind,
mut cache_db: CacheDB<&RevmSession>,
inspect_input: Input,
tx_info: TransactionInfo,
) -> Result<GethTrace, StratusError> {
let evm = create_evm(chain_id, spec, &mut cache_db, kind);

let trace_result: GethTrace = match tracer_type {
GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::FourByteTracer) => {
let mut inspector = FourByteInspector::default();
let mut evm_with_inspector = evm.with_inspector(&mut inspector);
inspect_input.fill_env(&mut evm_with_inspector);
let tx = std::mem::take(&mut evm_with_inspector.tx);
evm_with_inspector.inspect_tx(tx)?;
FourByteFrame::from(&inspector).into()
}
GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::CallTracer) => {
let call_config = opts.tracer_config.into_call_config()?;
let mut inspector = TracingInspector::new(TracingInspectorConfig::from_geth_call_config(&call_config));
let mut evm_with_inspector = evm.with_inspector(&mut inspector);
inspect_input.fill_env(&mut evm_with_inspector);
let tx = std::mem::take(&mut evm_with_inspector.tx);
let res = evm_with_inspector.inspect_tx(tx)?;
let mut trace = inspector.geth_builder().geth_call_traces(call_config, res.result.tx_gas_used()).into();
enhance_trace_with_decoded_errors(&mut trace);
trace
}
GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::PreStateTracer) => {
let prestate_config = opts.tracer_config.into_pre_state_config()?;
let mut inspector = TracingInspector::new(TracingInspectorConfig::from_geth_prestate_config(&prestate_config));
let mut evm_with_inspector = evm.with_inspector(&mut inspector);
inspect_input.fill_env(&mut evm_with_inspector);
let tx = std::mem::take(&mut evm_with_inspector.tx);
let res = evm_with_inspector.inspect_tx(tx)?;

inspector.geth_builder().geth_prestate_traces(&res, &prestate_config, &cache_db)?.into()
}
GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::NoopTracer) => NoopFrame::default().into(),
GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::MuxTracer) => {
let mux_config = opts.tracer_config.into_mux_config()?;
let mut inspector = MuxInspector::try_from_config(mux_config).map_err(|e| anyhow!(e))?;
let mut evm_with_inspector = evm.with_inspector(&mut inspector);
inspect_input.fill_env(&mut evm_with_inspector);
let tx = std::mem::take(&mut evm_with_inspector.tx);
let res = evm_with_inspector.inspect_tx(tx)?;
inspector.try_into_mux_frame(&res, &cache_db, tx_info)?.into()
}
GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::FlatCallTracer) => {
let flat_call_config = opts.tracer_config.into_flat_call_config()?;
let mut inspector = TracingInspector::new(TracingInspectorConfig::from_flat_call_config(&flat_call_config));
let mut evm_with_inspector = evm.with_inspector(&mut inspector);
inspect_input.fill_env(&mut evm_with_inspector);
let tx = std::mem::take(&mut evm_with_inspector.tx);
let res = evm_with_inspector.inspect_tx(tx)?;
inspector
.with_transaction_gas_limit(res.result.tx_gas_used())
.into_parity_builder()
.into_localized_transaction_traces(tx_info)
.into()
}
GethDebugTracerType::JsTracer(code) => {
let mut inspector = JsInspector::new(code, opts.tracer_config.into_json()).map_err(|e| anyhow!(e.to_string()))?;
let mut evm_with_inspector = evm.with_inspector(&mut inspector);
inspect_input.fill_env(&mut evm_with_inspector);
let tx = std::mem::take(&mut evm_with_inspector.tx);
let block = std::mem::take(&mut evm_with_inspector.block);
let res = evm_with_inspector.inspect_tx(tx.clone())?;
GethTrace::JS(inspector.json_result(res, &tx, &block, &cache_db).map_err(|e| anyhow!(e.to_string()))?)
}
GethDebugTracerType::BuiltInTracer(tracer) => {
return Err(anyhow!("tracer {tracer:?} is not implemented").into());
}
};

Ok(trace_result)
}
20 changes: 16 additions & 4 deletions src/eth/executor/evm/types/input/inspector.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
use alloy_rpc_types_trace::geth::GethDebugTracingOptions;

use crate::eth::types::CallInput;
use crate::eth::types::Hash;
use crate::eth::types::PointInTime;

pub struct InspectorInput {
pub tx_hash: Hash,
pub opts: GethDebugTracingOptions,
pub trace_unsuccessful_only: bool,
pub enum InspectorInput {
/// Traces an already-mined transaction, looked up by hash (`debug_traceTransaction`).
Transaction {
tx_hash: Hash,
opts: GethDebugTracingOptions,
trace_unsuccessful_only: bool,
},

/// Traces a synthetic call that was never signed or broadcast, against a chosen point in time (`debug_traceCall`).
Call {
call: CallInput,
point_in_time: PointInTime,
opts: GethDebugTracingOptions,
},
}
Loading