diff --git a/Cargo.lock b/Cargo.lock index 6d462c7e7e8..f285461ac86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9905,6 +9905,7 @@ dependencies = [ "revm", "serde", "serde_json", + "strum 0.27.2", "thiserror 2.0.17", "tokio", "tokio-stream", diff --git a/crates/optimism/rpc/Cargo.toml b/crates/optimism/rpc/Cargo.toml index 19526b2a24d..d9e6aa8fc1f 100644 --- a/crates/optimism/rpc/Cargo.toml +++ b/crates/optimism/rpc/Cargo.toml @@ -89,6 +89,9 @@ derive_more = { workspace = true, features = ["constructor"] } reth-metrics.workspace = true metrics.workspace = true +# enum +strum.workspace = true + [dev-dependencies] reth-optimism-chainspec.workspace = true alloy-op-hardforks.workspace = true diff --git a/crates/optimism/rpc/src/debug.rs b/crates/optimism/rpc/src/debug.rs index 195e0de181e..48dcb75dc88 100644 --- a/crates/optimism/rpc/src/debug.rs +++ b/crates/optimism/rpc/src/debug.rs @@ -1,6 +1,9 @@ //! Historical proofs RPC server implementation for `debug_` namespace. -use crate::state::OpStateProviderFactory; +use crate::{ + metrics::{DebugApiExtMetrics, DebugApis}, + state::OpStateProviderFactory, +}; use alloy_consensus::BlockHeader; use alloy_eips::{BlockId, BlockNumberOrTag}; use alloy_primitives::B256; @@ -109,6 +112,7 @@ pub struct DebugApiExtInner, semaphore: Semaphore, _attrs: PhantomData, + metrics: DebugApiExtMetrics, } impl DebugApiExtInner @@ -134,6 +138,7 @@ where task_spawner, semaphore: Semaphore::new(3), _attrs: PhantomData, + metrics: DebugApiExtMetrics::new(), } } } @@ -187,114 +192,127 @@ where parent_block_hash: B256, attributes: Attrs::RpcPayloadAttributes, ) -> RpcResult { - let _permit = self.inner.semaphore.acquire().await; - - let parent_header = self.parent_header(parent_block_hash).to_rpc_result()?; - - let (tx, rx) = oneshot::channel(); - let this = self.inner.clone(); - self.inner.task_spawner.spawn_blocking(Box::pin(async move { - let result = async { - let parent_hash = parent_header.hash(); - let attributes = Attrs::try_new(parent_hash, attributes, 3) - .map_err(PayloadBuilderError::other)?; - - let config = PayloadConfig { parent_header: Arc::new(parent_header), attributes }; - let ctx = OpPayloadBuilderCtx { - evm_config: this.evm_config.clone(), - chain_spec: this.provider.chain_spec(), - config, - cancel: Default::default(), - best_payload: Default::default(), - builder_config: Default::default(), - }; - - let state_provider = this - .state_provider_factory - .state_provider(Some(BlockId::Hash(parent_hash.into()))) - .await - .map_err(PayloadBuilderError::other)?; - - let builder = OpBuilder::new(|_| { - NoopPayloadTransactions::< - OpPooledTx2< - ::_TX, - op_alloy_consensus::OpPooledTransaction, - >, - >::default() - }); - - builder.witness(state_provider, &ctx).map_err(PayloadBuilderError::other) - }; - - let _ = tx.send(result.await); - })); - - rx.await - .map_err(|err| internal_rpc_err(err.to_string()))? - .map_err(|err| internal_rpc_err(err.to_string())) + self.inner + .metrics + .record_operation_async(DebugApis::DebugExecutePayload, async { + let _permit = self.inner.semaphore.acquire().await; + + let parent_header = self.parent_header(parent_block_hash).to_rpc_result()?; + + let (tx, rx) = oneshot::channel(); + let this = self.inner.clone(); + self.inner.task_spawner.spawn_blocking(Box::pin(async move { + let result = async { + let parent_hash = parent_header.hash(); + let attributes = Attrs::try_new(parent_hash, attributes, 3) + .map_err(PayloadBuilderError::other)?; + + let config = + PayloadConfig { parent_header: Arc::new(parent_header), attributes }; + let ctx = OpPayloadBuilderCtx { + evm_config: this.evm_config.clone(), + chain_spec: this.provider.chain_spec(), + config, + cancel: Default::default(), + best_payload: Default::default(), + builder_config: Default::default(), + }; + + let state_provider = this + .state_provider_factory + .state_provider(Some(BlockId::Hash(parent_hash.into()))) + .await + .map_err(PayloadBuilderError::other)?; + + let builder = OpBuilder::new(|_| { + NoopPayloadTransactions::< + OpPooledTx2< + ::_TX, + op_alloy_consensus::OpPooledTransaction, + >, + >::default() + }); + + builder.witness(state_provider, &ctx).map_err(PayloadBuilderError::other) + }; + + let _ = tx.send(result.await); + })); + + rx.await + .map_err(|err| internal_rpc_err(err.to_string()))? + .map_err(|err| internal_rpc_err(err.to_string())) + }) + .await } async fn execution_witness(&self, block_id: BlockNumberOrTag) -> RpcResult { - let _permit = self.inner.semaphore.acquire().await; - - let block = self - .inner - .eth_api - .recovered_block(block_id.into()) - .await? - .ok_or(EthApiError::HeaderNotFound(block_id.into()))?; + self.inner + .metrics + .record_operation_async(DebugApis::DebugExecutionWitness, async { + let _permit = self.inner.semaphore.acquire().await; - let this = self.inner.clone(); - let block_number = block.header().number(); + let block = self + .inner + .eth_api + .recovered_block(block_id.into()) + .await? + .ok_or(EthApiError::HeaderNotFound(block_id.into()))?; - let state_provider = this - .state_provider_factory - .state_provider(Some(BlockId::Number(block.parent_num_hash().number.into()))) - .await - .map_err(EthApiError::from)?; - let db = StateProviderDatabase::new(&state_provider); - let block_executor = this.eth_api.evm_config().executor(db); + let this = self.inner.clone(); + let block_number = block.header().number(); - let mut witness_record = ExecutionWitnessRecord::default(); + let state_provider = this + .state_provider_factory + .state_provider(Some(BlockId::Number(block.parent_num_hash().number.into()))) + .await + .map_err(EthApiError::from)?; + let db = StateProviderDatabase::new(&state_provider); + let block_executor = this.eth_api.evm_config().executor(db); + + let mut witness_record = ExecutionWitnessRecord::default(); + + let _ = block_executor + .execute_with_state_closure(&block, |statedb: &State<_>| { + witness_record.record_executed_state(statedb); + }) + .map_err(EthApiError::from)?; + + let ExecutionWitnessRecord { hashed_state, codes, keys, lowest_block_number } = + witness_record; + + let state = state_provider + .witness(Default::default(), hashed_state) + .map_err(EthApiError::from)?; + let mut exec_witness = + ExecutionWitness { state, codes, keys, ..Default::default() }; + + let smallest = match lowest_block_number { + Some(smallest) => smallest, + None => { + // Return only the parent header, if there were no calls to the + // BLOCKHASH opcode. + block_number.saturating_sub(1) + } + }; - let _ = block_executor - .execute_with_state_closure(&block, |statedb: &State<_>| { - witness_record.record_executed_state(statedb); + let range = smallest..block_number; + exec_witness.headers = self + .inner + .provider + .headers_range(range) + .map_err(EthApiError::from)? + .into_iter() + .map(|header| { + let mut serialized_header = Vec::new(); + header.encode(&mut serialized_header); + serialized_header.into() + }) + .collect(); + + Ok(exec_witness) }) - .map_err(EthApiError::from)?; - - let ExecutionWitnessRecord { hashed_state, codes, keys, lowest_block_number } = - witness_record; - - let state = - state_provider.witness(Default::default(), hashed_state).map_err(EthApiError::from)?; - let mut exec_witness = ExecutionWitness { state, codes, keys, ..Default::default() }; - - let smallest = match lowest_block_number { - Some(smallest) => smallest, - None => { - // Return only the parent header, if there were no calls to the - // BLOCKHASH opcode. - block_number.saturating_sub(1) - } - }; - - let range = smallest..block_number; - exec_witness.headers = self - .inner - .provider - .headers_range(range) - .map_err(EthApiError::from)? - .into_iter() - .map(|header| { - let mut serialized_header = Vec::new(); - header.encode(&mut serialized_header); - serialized_header.into() - }) - .collect(); - - Ok(exec_witness) + .await } async fn proofs_sync_status(&self) -> RpcResult { diff --git a/crates/optimism/rpc/src/metrics.rs b/crates/optimism/rpc/src/metrics.rs index 7979fe0164c..17ac94f6aa6 100644 --- a/crates/optimism/rpc/src/metrics.rs +++ b/crates/optimism/rpc/src/metrics.rs @@ -1,8 +1,11 @@ //! RPC metrics unique for OP-stack. +use alloy_primitives::map::HashMap; use core::time::Duration; use metrics::{Counter, Histogram}; use reth_metrics::Metrics; +use std::time::Instant; +use strum::{EnumCount, EnumIter, IntoEnumIterator}; /// Optimism sequencer metrics #[derive(Metrics, Clone)] @@ -36,3 +39,97 @@ pub struct EthApiExtMetrics { /// Total number of failures handling `eth_getProof` requests pub(crate) get_proof_failures: Counter, } + +/// Types of debug apis +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, EnumCount, EnumIter)] +pub enum DebugApis { + /// `DebugExecutePayload` Api + DebugExecutePayload, + /// `DebugExecutionWitness` Api + DebugExecutionWitness, +} + +impl DebugApis { + /// Returns the operation as a string for metrics labels. + pub const fn as_str(&self) -> &'static str { + match self { + Self::DebugExecutePayload => "debug_execute_payload", + Self::DebugExecutionWitness => "debug_execution_witness", + } + } +} + +/// Metrics for Debug API extension calls. +#[derive(Debug)] +pub struct DebugApiExtMetrics { + /// Per-api metrics handles + apis: HashMap, +} + +impl DebugApiExtMetrics { + /// Initializes new `DebugApiExtMetrics` + pub fn new() -> Self { + let mut apis = HashMap::default(); + for api in DebugApis::iter() { + apis.insert(api, DebugApiExtRpcMetrics::new_with_labels(&[("api", api.as_str())])); + } + Self { apis } + } + + /// Record a Debug API call async (tracks latency, requests, success, failures). + pub async fn record_operation_async(&self, api: DebugApis, f: F) -> Result + where + F: Future>, + { + if let Some(metrics) = self.apis.get(&api) { + metrics.record_async(f).await + } else { + f.await + } + } +} + +impl Default for DebugApiExtMetrics { + fn default() -> Self { + Self::new() + } +} + +/// Optimism Debug API extension metrics +#[derive(Metrics, Clone)] +#[metrics(scope = "optimism_rpc.debug_api_ext")] +pub struct DebugApiExtRpcMetrics { + /// End-to-end time to handle this API call + pub(crate) latency: Histogram, + + /// Total number of requests for this API + pub(crate) requests: Counter, + + /// Total number of successful responses for this API + pub(crate) successful_responses: Counter, + + /// Total number of failures for this API + pub(crate) failures: Counter, +} + +impl DebugApiExtRpcMetrics { + /// Record rpc api call async. + async fn record_async(&self, f: F) -> Result + where + F: Future>, + { + let start = Instant::now(); + let result = f.await; + + self.latency.record(start.elapsed().as_secs_f64()); + self.requests.increment(1); + + if result.is_ok() { + self.successful_responses.increment(1); + } else { + self.failures.increment(1); + } + + result + } +} diff --git a/etc/grafana/dashboards/op-proof-history.json b/etc/grafana/dashboards/op-proof-history.json index 8db17b3cf4c..7d295851109 100644 --- a/etc/grafana/dashboards/op-proof-history.json +++ b/etc/grafana/dashboards/op-proof-history.json @@ -1388,7 +1388,7 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "description": "API metric for rate of failed requests.", + "description": "API metric for rate of successful requests.", "fieldConfig": { "defaults": { "color": { @@ -1448,7 +1448,7 @@ "options": { "mode": "exclude", "names": [ - "op-reth" + "debug_executionWitness" ], "prefix": "All except:", "readOnly": true @@ -1473,13 +1473,13 @@ "x": 0, "y": 46 }, - "id": 16, + "id": 17, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": false + "showLegend": true }, "tooltip": { "hideZeros": false, @@ -1495,17 +1495,44 @@ "uid": "${DS_PROMETHEUS}" }, "disableTextWrap": false, - "editorMode": "builder", - "expr": "rate(reth_optimism_rpc_eth_api_ext_get_proof_failures[$__rate_interval])", + "editorMode": "code", + "expr": "rate(reth_optimism_rpc_eth_api_ext_get_proof_successful_responses[$__rate_interval])", "fullMetaSearch": false, "includeNullMetadata": false, - "legendFormat": "{{client_name}}", + "legendFormat": "eth_getProof", "range": true, "refId": "A", "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "rate(reth_optimism_rpc_debug_api_ext_successful_responses{api=\"debug_execute_payload\"}[$__rate_interval])", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "debug_executePayload", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "rate(reth_optimism_rpc_debug_api_ext_successful_responses{api=\"debug_execution_witness\"}[$__rate_interval])", + "hide": false, + "instant": false, + "legendFormat": "debug_executionWitness", + "range": true, + "refId": "C" } ], - "title": "eth_getProof - Rate of Failures", + "title": "RPC API - Rate of Success", "type": "timeseries" }, { @@ -1513,7 +1540,7 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "description": "API metric for rate of successful requests.", + "description": "API metric for rate of failed requests.", "fieldConfig": { "defaults": { "color": { @@ -1565,7 +1592,32 @@ }, "unit": "reqps" }, - "overrides": [] + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "debug_executionWitness" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": true, + "viz": true + } + } + ] + } + ] }, "gridPos": { "h": 6, @@ -1573,13 +1625,13 @@ "x": 6, "y": 46 }, - "id": 17, + "id": 24, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": false + "showLegend": true }, "tooltip": { "hideZeros": false, @@ -1595,17 +1647,44 @@ "uid": "${DS_PROMETHEUS}" }, "disableTextWrap": false, - "editorMode": "builder", - "expr": "rate(reth_optimism_rpc_eth_api_ext_get_proof_successful_responses[$__rate_interval])", + "editorMode": "code", + "expr": "rate(reth_optimism_rpc_eth_api_ext_get_proof_failures[$__rate_interval])", "fullMetaSearch": false, "includeNullMetadata": false, - "legendFormat": "{{client_name}}", + "legendFormat": "eth_getProof", "range": true, "refId": "A", "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "rate(reth_optimism_rpc_debug_api_ext_failures{api=\"debug_execute_payload\"}[$__rate_interval])", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "debug_executePayload", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "rate(reth_optimism_rpc_debug_api_ext_failures{api=\"debug_execution_witness\"}[$__rate_interval])", + "hide": false, + "instant": false, + "legendFormat": "debug_executionWitness", + "range": true, + "refId": "C" } ], - "title": "eth_getProof - Rate of Success", + "title": "RPC API - Rate of Failures", "type": "timeseries" }, { @@ -1644,7 +1723,7 @@ }, "showPoints": "auto", "showValues": false, - "spanNulls": false, + "spanNulls": true, "stacking": { "group": "A", "mode": "none" @@ -1665,7 +1744,32 @@ }, "unit": "s" }, - "overrides": [] + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "eth_getProof" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": true, + "viz": true + } + } + ] + } + ] }, "gridPos": { "h": 6, @@ -1679,12 +1783,12 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": false + "showLegend": true }, "tooltip": { "hideZeros": false, "mode": "single", - "sort": "none" + "sort": "desc" } }, "pluginVersion": "12.3.0", @@ -1695,7 +1799,7 @@ "uid": "${DS_PROMETHEUS}" }, "disableTextWrap": false, - "editorMode": "builder", + "editorMode": "code", "exemplar": false, "expr": "reth_optimism_rpc_eth_api_ext_get_proof_latency{quantile=\"1.0\"}", "format": "time_series", @@ -1707,9 +1811,35 @@ "range": true, "refId": "A", "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "reth_optimism_rpc_debug_api_ext_latency{api=\"debug_execute_payload\", quantile=\"1.0\"}", + "hide": false, + "instant": false, + "legendFormat": "debug_executePayload", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "reth_optimism_rpc_debug_api_ext_latency{api=\"debug_execution_witness\", quantile=\"1.0\"}", + "hide": false, + "instant": false, + "legendFormat": "debug_executionWitness", + "range": true, + "refId": "B" } ], - "title": "eth_getProof - Latency ", + "title": "RPC API - Latency ", "type": "timeseries" }, {