From 8602bfd893e59602d3a31258a1786c738ff56138 Mon Sep 17 00:00:00 2001 From: Hubert Bugaj Date: Tue, 9 Jun 2026 18:37:25 +0200 Subject: [PATCH] feat: `trace_block` caching --- src/daemon/mod.rs | 6 ++- src/rpc/methods/eth.rs | 70 +++++++++++++++++++----------- src/rpc/methods/eth/trace/types.rs | 31 +++++++++---- 3 files changed, 71 insertions(+), 36 deletions(-) diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index ef3b91ecb5c3..580c801a8d50 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -407,8 +407,10 @@ async fn prefill_rpc_caches_for_tipset(state_manager: StateManager, tsk: TipsetK } } { - if let Err(e) = state_manager.execution_trace(&ts).await { - warn!("failed to call `StateManager::execution_trace` for cache warmup: {e:#}"); + // Warms both the FVM-replay cache and the parity-trace cache, + // since `eth_trace_block` calls `execution_trace` internally. + if let Err(e) = crate::rpc::eth::eth_trace_block(&state_manager, &ts).await { + warn!("failed to call `eth_trace_block` for cache warmup: {e:#}"); } } { diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 0e319b7e36da..2a647c6fa8c4 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -3475,17 +3475,17 @@ impl RpcMethod<1> for EthTraceBlock { let ts = resolver .tipset_by_block_number_or_hash(block_param, ResolveNullTipset::TakeOlder) .await?; - eth_trace_block(&ctx, &ts).await + eth_trace_block(&ctx.state_manager, &ts).await } } /// Replays a tipset and resolves every non-system transaction into a [`trace::TipsetTraceEntry`]. async fn execute_tipset_traces( - ctx: &Ctx, + state_manager: &StateManager, ts: &Tipset, ) -> Result<(StateTree, Vec), ServerError> { - let (state_root, raw_traces) = ctx.state_manager.execution_trace(ts).await?; - let state = ctx.state_manager.get_state_tree(&state_root)?; + let (state_root, raw_traces) = state_manager.execution_trace(ts).await?; + let state = state_manager.get_state_tree(&state_root)?; // Resolve every non-system message's tx hash in parallel. Each lookup is // an independent DB read; running them sequentially adds O(N) IO @@ -3493,8 +3493,8 @@ async fn execute_tipset_traces( let raw = non_system_traces_with_positions(raw_traces).collect_vec(); let mut entries: Vec = Vec::with_capacity(raw.len()); let mut join_set = tokio::task::JoinSet::new(); - let db = ctx.db(); - let eth_chain_id = ctx.chain_config().eth_chain_id; + let db = state_manager.db(); + let eth_chain_id = state_manager.chain_config().eth_chain_id; for (msg_position, invoc_result) in raw { let db = db.shallow_clone(); join_set.spawn_blocking(move || { @@ -3533,23 +3533,43 @@ fn non_system_traces_with_positions( .map(|(idx, ir)| (idx as i64, ir)) } -async fn eth_trace_block(ctx: &Ctx, ts: &Tipset) -> Result, ServerError> { - let (state, entries) = execute_tipset_traces(ctx, ts).await?; - let block_hash: EthHash = ts.key().cid()?.into(); - let mut all_traces = vec![]; +/// Builds the Parity-style block traces for `ts`, caching the result by tipset. +/// Unlike [`StateManager::execution_trace`], this also caches the tx-hash +/// lookups and parity-trace construction. +pub(crate) async fn eth_trace_block( + state_manager: &StateManager, + ts: &Tipset, +) -> Result, ServerError> { + // 64 most-recent blocks; bounded by count, not bytes (a few MiB on mainnet, + // see the `cache_eth_trace_block_size` metric). + const ETH_TRACE_BLOCK_CACHE_SIZE: NonZeroUsize = nonzero!(64usize); + static ETH_TRACE_BLOCK_CACHE: LazyLock>>> = + LazyLock::new(|| { + SizeTrackingCache::new_with_metrics("eth_trace_block", ETH_TRACE_BLOCK_CACHE_SIZE) + }); - for entry in entries { - for trace in entry.build_parity_traces(&state)? { - all_traces.push(EthBlockTrace { - trace, - block_hash, - block_number: ts.epoch(), - transaction_hash: entry.tx_hash, - transaction_position: entry.msg_position, - }); - } - } - Ok(all_traces) + let block_cid = ts.key().cid()?; + let traces = ETH_TRACE_BLOCK_CACHE + .get_or_insert_async(&CidWrapper::from(block_cid), async move { + let (state, entries) = execute_tipset_traces(state_manager, ts).await?; + let block_hash: EthHash = block_cid.into(); + let mut all_traces = vec![]; + + for entry in entries { + for trace in entry.build_parity_traces(&state)? { + all_traces.push(EthBlockTrace { + trace, + block_hash, + block_number: ts.epoch(), + transaction_hash: entry.tx_hash, + transaction_position: entry.msg_position, + }); + } + } + anyhow::Ok(Arc::new(all_traces)) + }) + .await?; + Ok(Arc::unwrap_or_clone(traces)) } pub enum EthDebugTraceTransaction {} @@ -3657,7 +3677,7 @@ async fn debug_trace_transaction( return Ok(GethTrace::PreState(frame)); } - let (state, entries) = execute_tipset_traces(&ctx, &ts).await?; + let (state, entries) = execute_tipset_traces(&ctx.state_manager, &ts).await?; let entry = entries .into_iter() .find(|e| e.tx_hash == eth_hash) @@ -3860,7 +3880,7 @@ impl RpcMethod<1> for EthTraceTransaction { .tipset_by_block_number_or_hash(eth_txn.block_number, ResolveNullTipset::TakeOlder) .await?; - let traces = eth_trace_block(&ctx, &ts) + let traces = eth_trace_block(&ctx.state_manager, &ts) .await? .into_iter() .filter(|trace| trace.transaction_hash == eth_hash) @@ -3909,7 +3929,7 @@ async fn eth_trace_replay_block_transactions( ctx: &Ctx, ts: &Tipset, ) -> Result, ServerError> { - let (state, entries) = execute_tipset_traces(ctx, ts).await?; + let (state, entries) = execute_tipset_traces(&ctx.state_manager, ts).await?; let mut all_traces = vec![]; for entry in entries { diff --git a/src/rpc/methods/eth/trace/types.rs b/src/rpc/methods/eth/trace/types.rs index a0b90842b712..502d0dcf1d51 100644 --- a/src/rpc/methods/eth/trace/types.rs +++ b/src/rpc/methods/eth/trace/types.rs @@ -13,12 +13,13 @@ use crate::rpc::eth::trace::GETH_TRACE_REVERT_ERROR; use crate::rpc::eth::trace::utils::extract_revert_reason; use crate::shim::error::ExitCode; use anyhow::{Context as _, Result, bail}; +use get_size2::GetSize; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; /// Typed error for Parity-style EVM trace entries. -#[derive(Debug, Hash, Clone, PartialEq, Eq, thiserror::Error)] +#[derive(Debug, Hash, Clone, PartialEq, Eq, thiserror::Error, GetSize)] pub enum TraceError { #[error("Reverted")] Reverted, @@ -102,7 +103,9 @@ fn parse_exit_code_display(s: &str) -> u32 { .unwrap_or(0) } -#[derive(Eq, Hash, PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[derive( + Eq, Hash, PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema, GetSize, +)] #[serde(rename_all = "camelCase")] pub struct EthCallTraceAction { pub call_type: String, @@ -113,7 +116,9 @@ pub struct EthCallTraceAction { pub input: EthBytes, } -#[derive(Eq, Hash, PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[derive( + Eq, Hash, PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema, GetSize, +)] #[serde(rename_all = "camelCase")] pub struct EthCreateTraceAction { pub from: EthAddress, @@ -122,7 +127,7 @@ pub struct EthCreateTraceAction { pub init: EthBytes, } -#[derive(Eq, Hash, PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[derive(Eq, Hash, PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema, GetSize)] #[serde(untagged)] pub enum TraceAction { Call(EthCallTraceAction), @@ -135,14 +140,18 @@ impl Default for TraceAction { } } -#[derive(Eq, Hash, PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[derive( + Eq, Hash, PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema, GetSize, +)] #[serde(rename_all = "camelCase")] pub struct EthCallTraceResult { pub gas_used: EthUint64, pub output: EthBytes, } -#[derive(Eq, Hash, PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[derive( + Eq, Hash, PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema, GetSize, +)] #[serde(rename_all = "camelCase")] pub struct EthCreateTraceResult { #[serde(skip_serializing_if = "Option::is_none")] @@ -151,7 +160,7 @@ pub struct EthCreateTraceResult { pub code: EthBytes, } -#[derive(Eq, Hash, PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[derive(Eq, Hash, PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema, GetSize)] #[serde(untagged)] pub enum TraceResult { Call(EthCallTraceResult), @@ -496,7 +505,9 @@ impl EthTraceResults { } } -#[derive(Eq, Hash, PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[derive( + Eq, Hash, PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema, GetSize, +)] #[serde(rename_all = "camelCase")] pub struct EthTrace { pub r#type: String, @@ -595,7 +606,9 @@ impl EthTrace { } } -#[derive(Eq, Hash, PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[derive( + Eq, Hash, PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema, GetSize, +)] #[serde(rename_all = "camelCase")] pub struct EthBlockTrace { #[serde(flatten)]