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
73 changes: 72 additions & 1 deletion crates/blockchain/tracing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ use std::{
time::Duration,
};

use ethrex_common::{H256, tracing::CallTrace, types::Block};
use ethrex_common::{
H256,
tracing::{CallTrace, PrestateResult},
types::Block,
};
use ethrex_storage::Store;
use ethrex_vm::{Evm, EvmError};

Expand Down Expand Up @@ -82,6 +86,73 @@ impl Blockchain {
Ok(call_traces)
}

/// Outputs the prestate trace for the given transaction.
/// If `diff_mode` is true, returns both pre and post state; otherwise returns only pre state.
/// May need to re-execute blocks in order to rebuild the transaction's prestate, up to the amount given by `reexec`.
pub async fn trace_transaction_prestate(
&self,
tx_hash: H256,
reexec: u32,
timeout: Duration,
diff_mode: bool,
) -> Result<PrestateResult, 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 the block until the transaction we want to trace
vm.rerun_block(&block, Some(tx_index))?;
// Trace the transaction
timeout_trace_operation(timeout, move || {
vm.trace_tx_prestate(&block, tx_index, diff_mode)
})
.await
}

/// Outputs the prestate trace for each transaction in the block along with the transaction's hash.
/// If `diff_mode` is true, returns both pre and post state per tx; otherwise returns only pre state.
/// May need to re-execute blocks in order to rebuild the block's prestate, up to the amount given by `reexec`.
/// Returns prestate traces from oldest to newest transaction.
pub async fn trace_block_prestate(
&self,
block: Block,
reexec: u32,
timeout: Duration,
diff_mode: bool,
) -> Result<Vec<(H256, PrestateResult)>, ChainError> {
let mut vm = self
.rebuild_parent_state(block.header.parent_hash, reexec)
.await?;
// Run system calls but stop before tx 0
vm.rerun_block(&block, Some(0))?;
// Trace each transaction sequentially — state accumulates between calls
// We need to do this in order to pass ownership of block & evm to a blocking process without cloning
let vm = Arc::new(Mutex::new(vm));
let block = Arc::new(block);
let mut traces = vec![];
for index in 0..block.body.transactions.len() {
let block = block.clone();
let vm = vm.clone();
let tx_hash = block.as_ref().body.transactions[index].hash();
let result = timeout_trace_operation(timeout, move || {
vm.lock()
.map_err(|_| EvmError::Custom("Unexpected Runtime Error".to_string()))?
.trace_tx_prestate(block.as_ref(), index, diff_mode)
})
.await?;
traces.push((tx_hash, result));
}
Ok(traces)
}

/// 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
44 changes: 44 additions & 0 deletions crates/common/tracing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use bytes::Bytes;
use ethereum_types::H256;
use ethereum_types::{Address, U256};
use serde::Serialize;
use std::collections::HashMap;

/// Collection of traces of each call frame as defined in geth's `callTracer` output
/// https://geth.ethereum.org/docs/developers/evm-tracing/built-in-tracers#call-tracer
Expand Down Expand Up @@ -65,3 +66,46 @@ pub struct CallLog {
pub data: Bytes,
pub position: u64,
}

/// Account state as captured by the prestateTracer.
/// Matches Geth's prestateTracer output format.
/// https://geth.ethereum.org/docs/developers/evm-tracing/built-in-tracers#prestate-tracer
#[derive(Debug, Serialize, Default, Clone)]
pub struct PrestateAccountState {
pub balance: U256,
#[serde(default, skip_serializing_if = "is_zero_nonce")]
pub nonce: u64,
#[serde(
default,
skip_serializing_if = "Bytes::is_empty",
with = "crate::serde_utils::bytes"
)]
pub code: Bytes,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub storage: HashMap<H256, H256>,
}
Comment on lines +74 to +86
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 balance always serialized even for zero-balance accounts

Geth omits balance when it is zero (the field simply isn't emitted). The struct currently has no skip_serializing_if guard on balance, so zero-balance accounts will always include "balance":"0x0" in the output. Low impact, but diverges slightly from Geth's wire format. Consider:

Suggested change
pub struct PrestateAccountState {
pub balance: U256,
#[serde(default, skip_serializing_if = "is_zero_nonce")]
pub nonce: u64,
#[serde(
default,
skip_serializing_if = "Bytes::is_empty",
with = "crate::serde_utils::bytes"
)]
pub code: Bytes,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub storage: HashMap<H256, H256>,
}
#[serde(skip_serializing_if = "U256::is_zero")]
pub balance: U256,
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/common/tracing.rs
Line: 74-86

Comment:
**`balance` always serialized even for zero-balance accounts**

Geth omits `balance` when it is zero (the field simply isn't emitted). The struct currently has no `skip_serializing_if` guard on `balance`, so zero-balance accounts will always include `"balance":"0x0"` in the output. Low impact, but diverges slightly from Geth's wire format. Consider:

```suggestion
    #[serde(skip_serializing_if = "U256::is_zero")]
    pub balance: U256,
```

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


/// Per-transaction prestate trace (non-diff mode).
/// Maps account address to its state before the transaction.
pub type PrestateTrace = HashMap<Address, PrestateAccountState>;

/// Result of a prestateTracer execution — either a plain prestate map or a diff.
#[derive(Debug, Clone)]
pub enum PrestateResult {
/// Non-diff mode: map of address → pre-tx account state.
Prestate(PrestateTrace),
/// Diff mode: pre-tx and post-tx state for all touched accounts.
Diff(PrePostState),
}

/// Per-transaction prestate trace (diff mode).
/// Contains the pre-tx and post-tx state for all touched accounts.
#[derive(Debug, Serialize, Default, Clone)]
pub struct PrePostState {
pub pre: HashMap<Address, PrestateAccountState>,
pub post: HashMap<Address, PrestateAccountState>,
}

fn is_zero_nonce(n: &u64) -> bool {
*n == 0
}
59 changes: 56 additions & 3 deletions crates/networking/rpc/tracing.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use std::time::Duration;

use ethrex_common::H256;
use ethrex_common::{serde_utils, tracing::CallTraceFrame};
use ethrex_common::{
serde_utils,
tracing::{CallTraceFrame, PrestateResult},
};
use serde::{Deserialize, Serialize};
use serde_json::Value;

Expand Down Expand Up @@ -41,6 +44,7 @@ struct TraceConfig {
enum TracerType {
#[default]
CallTracer,
PrestateTracer,
}

#[derive(Deserialize, Default)]
Expand All @@ -52,6 +56,13 @@ struct CallTracerConfig {
with_log: bool,
}

#[derive(Deserialize, Default)]
#[serde(rename_all = "camelCase")]
struct PrestateTracerConfig {
#[serde(default)]
diff_mode: bool,
}

type BlockTrace<TxTrace> = Vec<BlockTraceComponent<TxTrace>>;

#[derive(Serialize)]
Expand Down Expand Up @@ -96,7 +107,6 @@ impl RpcHandler for TraceTransactionRequest {
) -> Result<serde_json::Value, crate::utils::RpcErr> {
let reexec = self.trace_config.reexec.unwrap_or(DEFAULT_REEXEC);
let timeout = self.trace_config.timeout.unwrap_or(DEFAULT_TIMEOUT);
// This match will make more sense once we support other tracers
match self.trace_config.tracer {
TracerType::CallTracer => {
// Parse tracer config now that we know the type
Expand Down Expand Up @@ -124,6 +134,22 @@ impl RpcHandler for TraceTransactionRequest {
.ok_or(RpcErr::Internal("Empty call trace".to_string()))?;
Ok(serde_json::to_value(top_frame)?)
}
TracerType::PrestateTracer => {
let config = if let Some(value) = &self.trace_config.tracer_config {
serde_json::from_value(value.clone())?
} else {
PrestateTracerConfig::default()
};
let result = context
.blockchain
.trace_transaction_prestate(self.tx_hash, reexec, timeout, config.diff_mode)
.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)?),
}
}
}
}
}
Expand Down Expand Up @@ -166,7 +192,6 @@ impl RpcHandler for TraceBlockByNumberRequest {
.ok_or(RpcErr::Internal("Block not Found".to_string()))?;
let reexec = self.trace_config.reexec.unwrap_or(DEFAULT_REEXEC);
let timeout = self.trace_config.timeout.unwrap_or(DEFAULT_TIMEOUT);
// This match will make more sense once we support other tracers
match self.trace_config.tracer {
TracerType::CallTracer => {
// Parse tracer config now that we know the type
Expand Down Expand Up @@ -200,6 +225,34 @@ impl RpcHandler for TraceBlockByNumberRequest {
.collect::<Result<_, RpcErr>>()?;
Ok(serde_json::to_value(block_trace)?)
}
TracerType::PrestateTracer => {
let config = if let Some(value) = &self.trace_config.tracer_config {
serde_json::from_value(value.clone())?
} else {
PrestateTracerConfig::default()
};
let prestate_traces = context
.blockchain
.trace_block_prestate(block, reexec, timeout, config.diff_mode)
.await
.map_err(|err| RpcErr::Internal(err.to_string()))?;
// Each trace result is already the correct variant (Prestate or Diff)
// based on the diff_mode flag, so we serialize directly.
let block_trace: Vec<serde_json::Value> = prestate_traces
.into_iter()
.map(|(hash, result)| {
let trace_value = match result {
PrestateResult::Prestate(trace) => serde_json::to_value(trace)?,
PrestateResult::Diff(diff) => serde_json::to_value(diff)?,
};
serde_json::to_value(BlockTraceComponent {
tx_hash: hash,
result: trace_value,
})
})
.collect::<Result<_, serde_json::Error>>()?;
Ok(serde_json::to_value(block_trace)?)
}
}
}
}
Loading
Loading