Skip to content
Closed
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
52 changes: 52 additions & 0 deletions crates/blockchain/tracing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,58 @@ impl Blockchain {
Ok(traces)
}

/// Re-executes the target transaction (and any preceding txs in its block) with no
/// tracer attached, then discards the result. Used by `noopTracer` so the RPC layer
/// validates that the tx exists and replays it through the EVM without paying any
/// tracing collection cost, matching the benchmarking purpose of geth's `noopTracer`.
/// May need to re-execute blocks in order to rebuild the transaction's prestate, up
/// to the amount given by `reexec`.
pub async fn noop_trace_transaction(
&self,
tx_hash: H256,
reexec: u32,
timeout: Duration,
) -> Result<(), ChainError> {
let Some((_, block_hash, tx_index)) =
self.storage.get_transaction_location(tx_hash).await?
else {
return Err(ChainError::Custom("Transaction not Found".to_string()));
};
let tx_index = tx_index as usize;
let Some(block) = self.storage.get_block_by_hash(block_hash).await? else {
return Err(ChainError::Custom("Block not Found".to_string()));
};
let mut vm = self
.rebuild_parent_state(block.header.parent_hash, reexec)
.await?;
// Run every tx up to AND including the target — no tracer attached.
let stop = tx_index.saturating_add(1);
timeout_trace_operation(timeout, move || vm.rerun_block(&block, Some(stop))).await
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Optional consistency port from the duplicate #6657. Here the target tx is folded into the replay by stopping at tx_index + 1. The sibling tracers (trace_tx_prestate, trace_tx_opcodes) instead replay [0, tx_index) and then run the target tx through a dedicated step — and #6657's noop path follows that same shape (rerun_block(Some(tx_index)) + a separate trace_tx_noop(target)).

For a noop the result is identical either way, so this is purely about keeping the module uniform:

// replay preceding txs, then run the target through the same dedicated
// step the other tracers use — keeps the tracing module's shape uniform.
vm.rerun_block(&block, Some(tx_index))?;
timeout_trace_operation(timeout, move || vm.trace_tx_noop(&block, tx_index)).await

Non-blocking; adopt only if you want parity with the prestate/opcode paths. The current Some(tx_index + 1) form is correct as-is.

}

/// Re-executes every transaction in the block with no tracer attached and returns the
/// transaction hashes in order. Used by `noopTracer` for `debug_traceBlockByNumber` so
/// the RPC layer can emit `{txHash, result: {}}` per tx without paying tracing cost.
/// May need to re-execute blocks in order to rebuild the block's prestate, up to the
/// amount given by `reexec`.
pub async fn noop_trace_block(
&self,
block: Block,
reexec: u32,
timeout: Duration,
) -> Result<Vec<H256>, ChainError> {
let mut vm = self
.rebuild_parent_state(block.header.parent_hash, reexec)
.await?;
let block = Arc::new(block);
let block_for_exec = block.clone();
timeout_trace_operation(timeout, move || {
vm.rerun_block(block_for_exec.as_ref(), None)
})
.await?;
Ok(block.body.transactions.iter().map(|tx| tx.hash()).collect())
}

/// 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
117 changes: 117 additions & 0 deletions crates/networking/rpc/tracing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ enum TracerType {
/// `structLogger` wrapper shape (`{failed, gas, returnValue, structLogs}`).
/// Selected via `"tracer": "opcodeTracer"`.
OpcodeTracer,
/// No-op tracer: re-executes the transaction(s) through the EVM with no tracer
/// attached and returns empty JSON objects. The output shape matches geth's
/// `noopTracer` (`{}` per tx), but the mechanism differs: geth runs an
/// empty-hook tracer (paying per-opcode dispatch), whereas ethrex skips tracer
/// hooks entirely, isolating raw execution cost. Any `tracerConfig` is ignored.
/// Selected via `"tracer": "noopTracer"`.
NoopTracer,
}

#[derive(Deserialize, Default)]
Expand Down Expand Up @@ -209,6 +216,17 @@ impl RpcHandler for TraceTransactionRequest {
emit,
})?)
}
TracerType::NoopTracer => {
// Like geth, noopTracer still executes the transaction (validating
// it exists and that parent state can be rebuilt) but with no tracer
// attached — so we pay only the raw execution cost, then return {}.
context
.blockchain
.noop_trace_transaction(self.tx_hash, reexec, timeout)
.await
.map_err(|err| RpcErr::Internal(err.to_string()))?;
Ok(serde_json::json!({}))
}
}
}
}
Expand Down Expand Up @@ -355,6 +373,105 @@ impl RpcHandler for TraceBlockByNumberRequest {
.collect::<Result<_, serde_json::Error>>()?;
Ok(serde_json::to_value(block_trace)?)
}
TracerType::NoopTracer => {
// Like geth, noopTracer re-executes every transaction in the block
// with no tracer attached, then emits {} per tx.
let tx_hashes = context
.blockchain
.noop_trace_block(block, reexec, timeout)
.await
.map_err(|err| RpcErr::Internal(err.to_string()))?;
let block_trace: BlockTrace<Value> = tx_hashes
.into_iter()
.map(|hash| (hash, serde_json::json!({})).into())
.collect();
Ok(serde_json::to_value(block_trace)?)
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}
}
}

#[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_with_config() {
let params = Some(vec![
json!("0x0000000000000000000000000000000000000000000000000000000000000001"),
json!({"tracer": "callTracer", "tracerConfig": {"onlyTopCall": true}}),
]);
let req = TraceTransactionRequest::parse(&params).unwrap();
assert!(matches!(req.trace_config.tracer, TracerType::CallTracer));
}

#[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());
}

// --- TracerType deserialization tests ---

#[test]
fn deserialize_tracer_type_noop() {
let t: TracerType = serde_json::from_value(json!("noopTracer")).unwrap();
assert!(matches!(t, TracerType::NoopTracer));
}

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

// --- TraceConfig deserialization tests ---

#[test]
fn deserialize_trace_config_defaults() {
let cfg: TraceConfig = serde_json::from_value(json!({})).unwrap();
assert!(matches!(cfg.tracer, TracerType::CallTracer));
assert!(cfg.timeout.is_none());
assert!(cfg.reexec.is_none());
}

// --- PrestateTracerConfig validation tests ---

#[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());
}

// --- noopTracer parse test ---

#[test]
fn parse_trace_tx_noop_tracer() {
let params = Some(vec![
json!("0x0000000000000000000000000000000000000000000000000000000000000001"),
json!({"tracer": "noopTracer"}),
]);
let req = TraceTransactionRequest::parse(&params).unwrap();
assert!(matches!(req.trace_config.tracer, TracerType::NoopTracer));
}
}
Loading
Loading