Skip to content
Merged
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
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion bin/debug-trace-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ alloy-rpc-types-trace.workspace = true

# mega
mega-evm.workspace = true
salt.workspace = true

# op
op-alloy-network.workspace = true
Expand Down
13 changes: 8 additions & 5 deletions bin/debug-trace-server/src/chain_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ use stateless_core::{

use crate::{metrics, response_cache::ResponseCache, server_db::BlockStore};

/// Fetcher for the trace server: fetches blocks + witnesses, discards MPT witness,
/// converts SALT witness to [`LightWitness`].
/// Fetcher for the trace server: fetches blocks + witnesses, discards MPT witness.
///
/// Witnesses go through the zero-validation light decode (`get_witness_light`):
/// the server never verifies the proof, so the full decode's per-point
/// elliptic-curve work bought nothing.
pub struct TraceFetcher {
pub rpc_client: Arc<RpcClient>,
}
Expand All @@ -35,11 +38,11 @@ impl BlockFetcher for TraceFetcher {
// fetch instead of serializing all three round trips.
let block_hash = self.rpc_client.get_block_hash(block_number).await;
let (witness_res, block_res) = tokio::join!(
self.rpc_client.get_witness(block_number, block_hash),
self.rpc_client.get_witness_light(block_number, block_hash),
self.rpc_client.get_block(BlockId::Number(block_number.into()), true),
);
let (salt, _mpt) = witness_res;
Ok((block_res, LightWitness::from(&salt)))
let (light, _mpt) = witness_res;
Ok((block_res, light))
}

async fn latest_block_number(&self) -> Result<u64> {
Expand Down
27 changes: 13 additions & 14 deletions bin/debug-trace-server/src/data_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ use dashmap::DashMap;
use futures::{FutureExt, future::Shared};
use op_alloy_rpc_types::Transaction;
use revm::state::Bytecode;
use salt::SaltWitness;
use stateless_common::{CodeFetchError, RpcClient, RpcDeadlineExceeded, estimate_witness_size};
use stateless_common::{CodeFetchError, RpcClient, RpcDeadlineExceeded, WitnessSizeBreakdown};
use stateless_core::{
ContractStore, LightWitness, StoreResult, db::StoreError, withdrawals::MptWitness,
};
Expand Down Expand Up @@ -592,7 +591,7 @@ fn shared_to_result(
/// 2. Fetch witness and full block in parallel, each subject to the shared `deadline`. The witness
/// stage also gets a sub-deadline: `min(deadline, now + witness_timeout)`, tightened further for
/// old blocks (see `witness_deadline_for`).
/// 3. Convert SaltWitness to LightWitness.
/// 3. The witness arrives as a `LightWitness` already (zero-validation light decode).
/// 4. Extract code hashes from witness and fetch contract bytecodes (shares `deadline`).
async fn do_fetch_block_data(
rpc_client: Arc<RpcClient>,
Expand Down Expand Up @@ -637,15 +636,11 @@ async fn do_fetch_block_data(
let (block_result, block_elapsed) = block_timed;

let fetch_witness_ms = witness_elapsed.as_millis();
let (salt_witness, _mpt_witness) = witness_result?;
// Step 3: the light decode already produced a LightWitness — no conversion.
let (witness, _mpt_witness) = witness_result?;
let block = block_result?;
let fetch_full_block_ms = block_elapsed.as_millis();

// Step 3: Convert SaltWitness to LightWitness.
let start = Instant::now();
let witness = LightWitness::from(&salt_witness);
let convert_witness_ms = start.elapsed().as_millis();

// Step 4: Extract code hashes and fetch contracts.
let start = Instant::now();
let code_hashes = crate::tracing_executor::extract_code_hashes(&witness);
Expand All @@ -658,7 +653,6 @@ async fn do_fetch_block_data(

if fetch_header_ms >= SLOW_STAGE_THRESHOLD_MS ||
fetch_witness_ms >= SLOW_STAGE_THRESHOLD_MS ||
convert_witness_ms >= SLOW_STAGE_THRESHOLD_MS ||
fetch_full_block_ms >= SLOW_STAGE_THRESHOLD_MS ||
fetch_contracts_ms >= SLOW_STAGE_THRESHOLD_MS
{
Expand All @@ -669,7 +663,6 @@ async fn do_fetch_block_data(
num_contracts,
fetch_header_ms = fetch_header_ms as u64,
fetch_witness_ms = fetch_witness_ms as u64,
convert_witness_ms = convert_witness_ms as u64,
fetch_full_block_ms = fetch_full_block_ms as u64,
fetch_contracts_ms = fetch_contracts_ms as u64,
total_ms = total_ms as u64,
Expand Down Expand Up @@ -718,19 +711,25 @@ fn witness_deadline_for(

/// Fetches witness data via the deadline-aware `RpcClient` API. The `deadline` is the
/// witness stage's effective deadline (see [`witness_deadline_for`]).
///
/// Uses the zero-validation light decode: the trace server never verifies the
/// witness proof, so the full decode's per-point elliptic-curve work bought
/// nothing. The recorded size is the light lower bound (excludes the
/// never-decoded parent commitments).
async fn fetch_witness(
rpc_client: &RpcClient,
block_number: u64,
block_hash: B256,
deadline: Instant,
) -> DataProviderResult<(SaltWitness, MptWitness)> {
) -> DataProviderResult<(LightWitness, MptWitness)> {
let wg_metrics = WitnessSourceMetrics::new_for_source("witness_generator");
let start = Instant::now();

match rpc_client.get_witness_with_deadline(block_number, block_hash, Some(deadline)).await {
match rpc_client.get_witness_light_with_deadline(block_number, block_hash, Some(deadline)).await
{
Ok(w) => {
wg_metrics.record_request(true, start.elapsed().as_secs_f64());
wg_metrics.record_size(estimate_witness_size(&w.0, &w.1));
wg_metrics.record_size(WitnessSizeBreakdown::new_light(&w.0, &w.1).total());
DataSourceMetrics::new_for_source("witness_generator").record();
Ok(w)
}
Expand Down
6 changes: 1 addition & 5 deletions bin/debug-trace-server/src/tracing_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ use revm_inspectors::tracing::{
};
use stateless_core::{
chain_spec::ChainSpec,
data_types::iter_code_hashes,
evm_database::{WitnessDatabase, WitnessExternalEnv},
executor::{ValidationError, create_evm_env},
light_witness::{LightWitness, LightWitnessExecutor},
Expand All @@ -65,10 +64,7 @@ use tracing::{instrument, trace, warn};

/// Returns distinct contract code hashes referenced by the witness, sorted for stable ordering.
pub fn extract_code_hashes(witness: &LightWitness) -> Vec<B256> {
let mut code_hashes: Vec<B256> = iter_code_hashes(&witness.kvs).collect();
code_hashes.sort();
code_hashes.dedup();
code_hashes
stateless_core::collect_code_hashes(&witness.kvs)
}

// TracerKind - Unified enum for TracingInspector-based tracers
Expand Down
5 changes: 3 additions & 2 deletions crates/stateless-common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ pub use rpc_client::{
pub mod witness_encoding;
pub use witness_encoding::{
WITNESS_RESPONSE_VERSION_PREFIX, WITNESS_ZSTD_LEVEL, WitnessDecodingError,
WitnessEncodingError, decode_witness_payload, decode_witness_response, encode_witness_payload,
WitnessEncodingError, decode_witness_payload, decode_witness_payload_light,
decode_witness_response, decode_witness_response_light, encode_witness_payload,
encode_witness_response,
};
pub mod witness_size;
pub use witness_size::{WitnessSizeBreakdown, estimate_witness_size};
pub use witness_size::WitnessSizeBreakdown;

/// Default port for Prometheus metrics HTTP endpoint.
pub const DEFAULT_METRICS_PORT: u16 = 9090;
105 changes: 78 additions & 27 deletions crates/stateless-common/src/rpc_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@ use op_alloy_rpc_types::Transaction;
use revm::state::Bytecode;
use salt::SaltWitness;
use serde::{Deserialize, Serialize};
use stateless_core::withdrawals::MptWitness;
use stateless_core::{LightWitness, withdrawals::MptWitness};
use tokio::sync::Semaphore;
use tracing::{trace, warn};

use crate::{
metrics::{RpcMethod, RpcMetrics},
witness_encoding::decode_witness_response,
witness_encoding::{decode_witness_response, decode_witness_response_light},
witness_size::WitnessSizeBreakdown,
};

Expand Down Expand Up @@ -569,25 +569,77 @@ impl RpcClient {
hash: B256,
deadline: Option<Instant>,
) -> std::result::Result<(SaltWitness, MptWitness), RpcDeadlineExceeded> {
let witness = round_robin_with_backoff(
let witness = self
.witness_round_robin(number, hash, deadline, decode_witness_response, "Witness decoded")
.await?;

if let Some(ref metrics) = self.config.metrics {
metrics.on_witness_fetch(WitnessSizeBreakdown::new(&witness.0, &witness.1));
}
Ok(witness)
}

/// Zero-validation counterpart of [`Self::get_witness`] for execution-only
/// consumers: decodes just the light witness (kvs + levels, no
/// elliptic-curve work — see `stateless_core::light_witness` for the
/// safety model). Consumers that later need the full witness (e.g. to
/// assemble test fixtures) re-fetch it via [`Self::get_witness`].
///
/// The `on_witness_fetch` size metric is not recorded here — the exact
/// breakdown needs the proof's commitment count. Callers that want a size
/// signal can record `WitnessSizeBreakdown::new_light` (a documented
/// lower bound) themselves.
pub async fn get_witness_light(&self, number: u64, hash: B256) -> (LightWitness, MptWitness) {
self.get_witness_light_with_deadline(number, hash, None)
.await
.expect("None deadline cannot time out")
}

/// Deadline-aware counterpart of [`Self::get_witness_light`].
pub async fn get_witness_light_with_deadline(
&self,
number: u64,
hash: B256,
deadline: Option<Instant>,
) -> std::result::Result<(LightWitness, MptWitness), RpcDeadlineExceeded> {
self.witness_round_robin(
number,
hash,
deadline,
decode_witness_response_light,
"Witness light-decoded",
)
.await
}

/// Shared `mega_getBlockWitness` retry loop: primary-failover rounds (always start from
/// provider 0 so the primary takes all traffic while healthy; backups are touched only
/// while it is failing), each attempt one RPC round trip followed by the caller-chosen
/// `decode` (see [`fetch_witness_with`]).
async fn witness_round_robin<T: Send + 'static>(
&self,
number: u64,
hash: B256,
deadline: Option<Instant>,
decode: fn(&str) -> std::result::Result<T, crate::WitnessDecodingError>,
trace_msg: &'static str,
) -> std::result::Result<T, RpcDeadlineExceeded> {
round_robin_with_backoff(
&self.witness_providers,
&self.witness_concurrency,
&self.config.rpc_retry,
self.config.per_attempt_timeout,
// Primary-failover: always start from provider 0 so the primary takes all traffic
// while healthy. Backup endpoints are touched only while the primary is failing.
0,
RpcMethod::MegaGetBlockWitness,
self.config.metrics.as_ref(),
deadline,
|provider| Box::pin(async move { fetch_witness_raw(&provider, number, hash).await }),
|provider| {
Box::pin(async move {
fetch_witness_with(&provider, number, hash, decode, trace_msg).await
})
},
)
.await?;

if let Some(ref metrics) = self.config.metrics {
metrics.on_witness_fetch(WitnessSizeBreakdown::new(&witness.0, &witness.1));
}
Ok(witness)
.await
}

/// Reports a range of validated blocks via the dedicated report endpoint.
Expand Down Expand Up @@ -1050,15 +1102,16 @@ async fn do_get_header(
Ok(header)
}

/// Fetches and decodes witness data from a single RPC provider (one attempt, no retry).
///
/// Decodes the versioned `mega_getBlockWitness` response with
/// [`decode_witness_response`](crate::decode_witness_response).
async fn fetch_witness_raw(
/// Shared single-attempt `mega_getBlockWitness` fetch: one RPC round trip,
/// then the caller-chosen decoder on the blocking pool (zstd + bincode over a
/// multi-MB payload is CPU-bound).
async fn fetch_witness_with<T: Send + 'static>(
provider: &RootProvider,
number: u64,
hash: B256,
) -> Result<(SaltWitness, MptWitness)> {
decode: fn(&str) -> std::result::Result<T, crate::WitnessDecodingError>,
trace_msg: &'static str,
) -> Result<T> {
let keys = WitnessRequestKeys { block_number: U64::from(number), block_hash: hash };
let encoded: String = provider
.client()
Expand All @@ -1067,22 +1120,20 @@ async fn fetch_witness_raw(
.map_err(|e| eyre!("mega_getBlockWitness failed for block {number}: {e}"))?;

let decode_start = Instant::now();
let (salt_witness, mpt_witness) =
tokio::task::spawn_blocking(move || -> Result<(SaltWitness, MptWitness)> {
decode_witness_response(&encoded)
.map_err(|e| eyre!("failed to decode witness response: {e}"))
})
.await
.context("decode task panicked")??;
let result = tokio::task::spawn_blocking(move || -> Result<T> {
decode(&encoded).map_err(|e| eyre!("failed to decode witness response: {e}"))
})
.await
.context("decode task panicked")??;

trace!(
block_number = number,
%hash,
decode_ms = decode_start.elapsed().as_millis(),
"Witness decoded",
"{trace_msg}",
);

Ok((salt_witness, mpt_witness))
Ok(result)
}

/// Verifies structural integrity of a block fetched from RPC.
Expand Down
Loading
Loading