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
55 changes: 54 additions & 1 deletion crates/blockchain/tracing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::{
use ethrex_common::{
H256,
tracing::{CallTrace, OpcodeTraceResult, PrestateResult},
types::Block,
types::{Block, BlockHeader, GenericTransaction},
};
use ethrex_storage::Store;
use ethrex_vm::tracing::OpcodeTracerConfig;
Expand Down Expand Up @@ -218,6 +218,59 @@ impl Blockchain {
Ok(traces)
}

/// Traces an arbitrary call with the callTracer at the state of the given block header.
pub async fn trace_call_calls(
&self,
header: &BlockHeader,
tx: &GenericTransaction,
timeout: Duration,
only_top_call: bool,
with_log: bool,
) -> Result<CallTrace, ChainError> {
let vm_db = StoreVmDatabase::new(self.storage.clone(), header.clone())?;
let mut vm = self.new_evm(vm_db)?;
let tx = tx.clone();
let header = header.clone();
timeout_trace_operation(timeout, move || {
vm.trace_call_calls(&header, &tx, only_top_call, with_log)
})
.await
}

/// Traces an arbitrary call with the prestateTracer at the state of the given block header.
pub async fn trace_call_prestate(
&self,
header: &BlockHeader,
tx: &GenericTransaction,
timeout: Duration,
diff_mode: bool,
include_empty: bool,
) -> Result<PrestateResult, ChainError> {
let vm_db = StoreVmDatabase::new(self.storage.clone(), header.clone())?;
let mut vm = self.new_evm(vm_db)?;
let tx = tx.clone();
let header = header.clone();
timeout_trace_operation(timeout, move || {
vm.trace_call_prestate(&header, &tx, diff_mode, include_empty)
})
.await
}

/// Traces an arbitrary call with the opcodeTracer at the state of the given block header.
pub async fn trace_call_opcodes(
&self,
header: &BlockHeader,
tx: &GenericTransaction,
timeout: Duration,
cfg: OpcodeTracerConfig,
) -> Result<OpcodeTraceResult, ChainError> {
let vm_db = StoreVmDatabase::new(self.storage.clone(), header.clone())?;
let mut vm = self.new_evm(vm_db)?;
let tx = tx.clone();
let header = header.clone();
timeout_trace_operation(timeout, move || vm.trace_call_opcodes(&header, &tx, cfg)).await
}

/// Rebuild the parent state for a block given its parent hash, returning an `Evm` instance with all changes cached
/// Will re-execute all ancestor block's which's state is not stored up to a maximum given by `reexec`
async fn rebuild_parent_state(
Expand Down
3 changes: 2 additions & 1 deletion crates/networking/rpc/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ use crate::eth::{
},
};
use crate::subscription_manager::{SubscriptionManager, SubscriptionManagerProtocol};
use crate::tracing::{TraceBlockByNumberRequest, TraceTransactionRequest};
use crate::tracing::{TraceBlockByNumberRequest, TraceCallRequest, TraceTransactionRequest};
use crate::types::transaction::SendRawTransactionRequest;
use crate::utils::{
RpcErr, RpcErrorMetadata, RpcErrorResponse, RpcNamespace, RpcRequest, RpcRequestId,
Expand Down Expand Up @@ -1155,6 +1155,7 @@ pub async fn map_debug_requests(req: &RpcRequest, context: RpcApiContext) -> Res
"debug_getBlockAccessList" => BlockAccessListRequest::call(req, context).await,
"debug_traceTransaction" => TraceTransactionRequest::call(req, context).await,
"debug_traceBlockByNumber" => TraceBlockByNumberRequest::call(req, context).await,
"debug_traceCall" => TraceCallRequest::call(req, context).await,
unknown_debug_method => Err(RpcErr::MethodNotFound(unknown_debug_method.to_owned())),
}
}
Expand Down
248 changes: 247 additions & 1 deletion crates/networking/rpc/tracing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ use ethrex_vm::tracing::OpcodeTracerConfig;
use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::{rpc::RpcHandler, types::block_identifier::BlockIdentifier, utils::RpcErr};
use ethrex_common::types::GenericTransaction;

use crate::{
rpc::RpcHandler,
types::block_identifier::{BlockIdentifier, BlockIdentifierOrHash},
utils::RpcErr,
};

/// Default max amount of blocks to re-excute if it is not given
const DEFAULT_REEXEC: u32 = 128;
Expand All @@ -26,6 +32,34 @@ pub struct TraceBlockByNumberRequest {
trace_config: TraceConfig,
}

/// `debug_traceCall` — trace an arbitrary call against the state at a given
/// block, matching geth's [debug_traceCall][geth]. The call is executed in
/// memory only; nothing is persisted.
///
/// Params: `[callObject, blockIdOrHash?, traceConfig?]`. Omitting the block
/// defaults to `"latest"`. The call object follows the `eth_call` shape; the
/// trace config follows the same shape used by `debug_traceTransaction`.
///
/// **Known divergence from geth**: geth's `traceConfig` accepts
/// `stateOverrides` and `blockOverrides` for hypothetical-state debugging.
/// ethrex does not yet honour either — they are silently ignored. Pass-through
/// support requires applying overrides at the VM layer before execution.
Comment on lines +43 to +46
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The reexec field is deserialized from the caller's traceConfig but is never read in TraceCallRequest::handle. For debug_traceTransaction and debug_traceBlockByNumber, reexec controls how far back to re-execute blocks to rebuild missing trie state. That concept doesn't apply here (state comes directly from the block header's state root), so ignoring it is correct — but unlike stateOverrides/blockOverrides, this divergence is not mentioned in the doc comment, which may surprise callers who pass it expecting an effect.

Suggested change
/// **Known divergence from geth**: geth's `traceConfig` accepts
/// `stateOverrides` and `blockOverrides` for hypothetical-state debugging.
/// ethrex does not yet honour either — they are silently ignored. Pass-through
/// support requires applying overrides at the VM layer before execution.
/// **Known divergence from geth**: geth's `traceConfig` accepts
/// `stateOverrides` and `blockOverrides` for hypothetical-state debugging.
/// ethrex does not yet honour either — they are silently ignored. Pass-through
/// support requires applying overrides at the VM layer before execution.
///
/// The `reexec` field in `traceConfig` is also accepted but has no effect:
/// state is read directly from the block header's state root rather than
/// being rebuilt by re-executing ancestor blocks.
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/networking/rpc/tracing.rs
Line: 43-46

Comment:
The `reexec` field is deserialized from the caller's `traceConfig` but is never read in `TraceCallRequest::handle`. For `debug_traceTransaction` and `debug_traceBlockByNumber`, `reexec` controls how far back to re-execute blocks to rebuild missing trie state. That concept doesn't apply here (state comes directly from the block header's state root), so ignoring it is correct — but unlike `stateOverrides`/`blockOverrides`, this divergence is not mentioned in the doc comment, which may surprise callers who pass it expecting an effect.

```suggestion
/// **Known divergence from geth**: geth's `traceConfig` accepts
/// `stateOverrides` and `blockOverrides` for hypothetical-state debugging.
/// ethrex does not yet honour either — they are silently ignored. Pass-through
/// support requires applying overrides at the VM layer before execution.
///
/// The `reexec` field in `traceConfig` is also accepted but has no effect:
/// state is read directly from the block header's state root rather than
/// being rebuilt by re-executing ancestor blocks.
```

How can I resolve this? If you propose a fix, please make it concise.

///
/// The `reexec` field in `traceConfig` is also accepted but has no effect:
/// state is read directly from the block header's state root rather than
/// being rebuilt by re-executing ancestor blocks.
///
/// State is sourced from `header.state_root` (i.e. the state **after** the
/// block has been applied), matching geth. Pruned nodes that don't have the
/// state will get an "Internal: state root missing" error.
///
/// [geth]: https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-debug#debug-tracecall
pub struct TraceCallRequest {
transaction: GenericTransaction,
block: Option<BlockIdentifierOrHash>,
trace_config: TraceConfig,
}

#[derive(Deserialize, Default)]
#[serde(rename_all = "camelCase")]
struct TraceConfig {
Expand Down Expand Up @@ -358,3 +392,215 @@ impl RpcHandler for TraceBlockByNumberRequest {
}
}
}

impl RpcHandler for TraceCallRequest {
fn parse(params: &Option<Vec<serde_json::Value>>) -> Result<Self, RpcErr> {
let params = params
.as_ref()
.ok_or(RpcErr::BadParams("No params provided".to_owned()))?;
if params.is_empty() || params.len() > 3 {
return Err(RpcErr::BadParams("Expected 1 to 3 params".to_owned()));
}
let transaction: GenericTransaction = serde_json::from_value(params[0].clone())?;
let block = params
.get(1)
.map(|v| BlockIdentifierOrHash::parse(v.clone(), 1))
.transpose()?;
let trace_config = if let Some(v) = params.get(2) {
serde_json::from_value(v.clone())?
} else {
TraceConfig::default()
};
Ok(TraceCallRequest {
transaction,
block,
trace_config,
})
}

async fn handle(
&self,
context: crate::rpc::RpcApiContext,
) -> Result<serde_json::Value, crate::utils::RpcErr> {
// Omitted block parameter defaults to `latest` (matches geth).
let default_block = BlockIdentifierOrHash::Identifier(BlockIdentifier::default());
let block = self.block.as_ref().unwrap_or(&default_block);
let header = block
.resolve_block_header(&context.storage)
.await?
.ok_or_else(|| RpcErr::Internal("Block not found".to_string()))?;
let timeout = self.trace_config.timeout.unwrap_or(DEFAULT_TIMEOUT);
match self.trace_config.tracer {
TracerType::CallTracer => {
let config = if let Some(value) = &self.trace_config.tracer_config {
serde_json::from_value(value.clone())?
} else {
CallTracerConfig::default()
};
let call_trace = context
.blockchain
.trace_call_calls(
&header,
&self.transaction,
timeout,
config.only_top_call,
config.with_log,
)
.await
.map_err(|err| RpcErr::Internal(err.to_string()))?;
let top_frame = call_trace
.into_iter()
.next()
.ok_or_else(|| RpcErr::Internal("Empty call trace".to_string()))?;
Ok(serde_json::to_value(top_frame)?)
}
TracerType::PrestateTracer => {
let config: PrestateTracerConfig =
if let Some(value) = &self.trace_config.tracer_config {
serde_json::from_value(value.clone())?
} else {
PrestateTracerConfig::default()
};
config.validate()?;
let result = context
.blockchain
.trace_call_prestate(
&header,
&self.transaction,
timeout,
config.diff_mode,
config.include_empty,
)
.await
.map_err(|err| RpcErr::Internal(err.to_string()))?;
match result {
PrestateResult::Prestate(trace) => Ok(serde_json::to_value(trace)?),
PrestateResult::Diff(diff) => Ok(serde_json::to_value(diff)?),
}
}
TracerType::OpcodeTracer => {
let cfg: OpcodeTracerConfig = self
.trace_config
.tracer_config
.as_ref()
.map(|v| serde_json::from_value(v.clone()))
.transpose()?
.unwrap_or_default();
let emit = StructLoggerEmit {
mem_size: cfg.enable_memory,
return_data: cfg.enable_return_data,
refund: false,
};
let result = context
.blockchain
.trace_call_opcodes(&header, &self.transaction, timeout, cfg)
.await
.map_err(|err| RpcErr::Internal(err.to_string()))?;
Ok(serde_json::to_value(StructLoggerResult {
result: &result,
emit,
})?)
}
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::rpc::RpcHandler;
use serde_json::json;

// --- TraceTransactionRequest parse tests ---

#[test]
fn parse_trace_tx_with_hash_only() {
let params = Some(vec![json!(
"0x0000000000000000000000000000000000000000000000000000000000000001"
)]);
let req = TraceTransactionRequest::parse(&params).unwrap();
assert_eq!(req.tx_hash, H256::from_low_u64_be(1));
}

#[test]
fn parse_trace_tx_no_params() {
assert!(TraceTransactionRequest::parse(&None).is_err());
}

#[test]
fn parse_trace_tx_too_many_params() {
let params = Some(vec![json!("0x01"), json!({}), json!("extra")]);
assert!(TraceTransactionRequest::parse(&params).is_err());
}

// --- TraceCallRequest parse tests ---

#[test]
fn parse_trace_call_minimal() {
let params = Some(vec![json!({
"from": "0x0000000000000000000000000000000000000001",
"to": "0x0000000000000000000000000000000000000002"
})]);
let req = TraceCallRequest::parse(&params).unwrap();
assert!(req.block.is_none());
assert!(matches!(req.trace_config.tracer, TracerType::CallTracer));
}

#[test]
fn parse_trace_call_with_block() {
let params = Some(vec![
json!({"from": "0x0000000000000000000000000000000000000001"}),
json!("latest"),
]);
let req = TraceCallRequest::parse(&params).unwrap();
assert!(req.block.is_some());
}

#[test]
fn parse_trace_call_with_config() {
let params = Some(vec![
json!({"from": "0x0000000000000000000000000000000000000001"}),
json!("latest"),
json!({"tracer": "prestateTracer"}),
]);
let req = TraceCallRequest::parse(&params).unwrap();
assert!(matches!(
req.trace_config.tracer,
TracerType::PrestateTracer
));
}

#[test]
fn parse_trace_call_no_params() {
assert!(TraceCallRequest::parse(&None).is_err());
}

#[test]
fn parse_trace_call_empty_params() {
assert!(TraceCallRequest::parse(&Some(vec![])).is_err());
}

#[test]
fn parse_trace_call_too_many_params() {
let params = Some(vec![json!({}), json!("latest"), json!({}), json!("extra")]);
assert!(TraceCallRequest::parse(&params).is_err());
}

// --- TracerType deserialization tests ---

#[test]
fn deserialize_tracer_type_unknown_fails() {
assert!(serde_json::from_value::<TracerType>(json!("unknownTracer")).is_err());
}

// --- PrestateTracerConfig validation ---

#[test]
fn prestate_config_diff_mode_and_include_empty_is_invalid() {
let cfg = PrestateTracerConfig {
diff_mode: true,
include_empty: true,
};
assert!(cfg.validate().is_err());
}
}
Loading
Loading