Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 2 additions & 0 deletions Cargo.lock

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

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,7 @@ backon = "1.5.2"
op-revm = { version = "12.0.0", default-features = false }
revm-context-interface = "10.2.0"
alloy-signer-local = "1.0.36"

# Misc
metrics = "0.24.1"
metrics-derive = "0.1"
2 changes: 2 additions & 0 deletions crates/ingress-rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ op-revm.workspace = true
revm-context-interface.workspace = true
alloy-signer-local.workspace = true
reth-optimism-evm.workspace = true
metrics.workspace = true
metrics-derive.workspace = true
1 change: 1 addition & 0 deletions crates/ingress-rpc/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod metrics;
pub mod queue;
pub mod service;
pub mod validation;
30 changes: 30 additions & 0 deletions crates/ingress-rpc/src/metrics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use metrics::Histogram;
use metrics_derive::Metrics;
use tokio::time::Duration;

/// `record_histogram` lets us record with tags.
pub fn record_histogram(rpc_latency: Duration, rpc: String) {
metrics::histogram!("tips_ingress_rpc_rpc_latency", "rpc" => rpc.clone())
.record(rpc_latency.as_secs_f64());
}

/// Metrics for the `tips_ingress_rpc` component.
/// Conventions:
/// - Durations are recorded in seconds (histograms).
/// - Counters are monotonic event counts.
/// - Gauges reflect the current value/state.
#[derive(Metrics, Clone)]
#[metrics(scope = "tips_ingress_rpc")]
pub struct Metrics {
#[metric(describe = "Duration of validate_tx")]
pub validate_tx_duration: Histogram,

#[metric(describe = "Duration of validate_bundle")]
pub validate_bundle_duration: Histogram,

#[metric(describe = "Duration of meter_bundle")]
pub meter_bundle_duration: Histogram,

#[metric(describe = "Duration of send_raw_transaction")]
pub send_raw_transaction_duration: Histogram,
}
18 changes: 18 additions & 0 deletions crates/ingress-rpc/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ use tips_core::{
AcceptedBundle, BLOCK_TIME, Bundle, BundleExtensions, BundleHash, CancelBundle,
MeterBundleResponse,
};
use tokio::time::Instant;
use tracing::{info, warn};

use crate::metrics::{Metrics, record_histogram};
use crate::queue::QueuePublisher;
use crate::validation::{AccountInfoLookup, L1BlockInfoLookup, validate_bundle, validate_tx};

Expand All @@ -43,6 +45,7 @@ pub struct IngressService<Queue, Audit> {
bundle_queue: Queue,
audit_publisher: Audit,
send_transaction_default_lifetime_seconds: u64,
metrics: Metrics,
}

impl<Queue, Audit> IngressService<Queue, Audit> {
Expand All @@ -61,6 +64,7 @@ impl<Queue, Audit> IngressService<Queue, Audit> {
bundle_queue: queue,
audit_publisher,
send_transaction_default_lifetime_seconds,
metrics: Metrics::default(),
}
}
}
Expand Down Expand Up @@ -116,6 +120,7 @@ where
}

async fn send_raw_transaction(&self, data: Bytes) -> RpcResult<B256> {
let start = Instant::now();
let transaction = self.validate_tx(&data).await?;

let expiry_timestamp = SystemTime::now()
Expand Down Expand Up @@ -175,6 +180,9 @@ where
warn!(message = "Failed to publish audit event", bundle_id = %accepted_bundle.uuid(), error = %e);
}

self.metrics
.send_raw_transaction_duration
.record(start.elapsed().as_secs_f64());
Ok(transaction.tx_hash())
}
}
Expand All @@ -185,6 +193,7 @@ where
Audit: BundleEventPublisher + Sync + Send + 'static,
{
async fn validate_tx(&self, data: &Bytes) -> RpcResult<Recovered<OpTxEnvelope>> {
let start = Instant::now();
if data.is_empty() {
return Err(EthApiError::EmptyRawTransactionData.into_rpc_err());
}
Expand All @@ -204,10 +213,14 @@ where
.await?;
validate_tx(account, &transaction, data, &mut l1_block_info).await?;

self.metrics
.validate_tx_duration
.record(start.elapsed().as_secs_f64());
Ok(transaction)
}

async fn validate_bundle(&self, bundle: &Bundle) -> RpcResult<()> {
let start = Instant::now();
if bundle.txs.is_empty() {
return Err(
EthApiError::InvalidParams("Bundle cannot have empty transactions".into())
Expand All @@ -224,19 +237,24 @@ where
}
validate_bundle(bundle, total_gas, tx_hashes)?;

self.metrics
.validate_bundle_duration
.record(start.elapsed().as_secs_f64());
Ok(())
}

/// `meter_bundle` is used to determine how long a bundle will take to execute. A bundle that
/// is within `BLOCK_TIME` will return the `MeterBundleResponse` that can be passed along
/// to the builder.
async fn meter_bundle(&self, bundle: &Bundle) -> RpcResult<MeterBundleResponse> {
let start = Instant::now();
let res: MeterBundleResponse = self
.simulation_provider
.client()
.request("base_meterBundle", (bundle,))
.await
.map_err(|e| EthApiError::InvalidParams(e.to_string()).into_rpc_err())?;
record_histogram(start.elapsed(), "base_meterBundle".to_string());

// we can save some builder payload building computation by not including bundles
// that we know will take longer than the block time to execute
Expand Down
8 changes: 8 additions & 0 deletions crates/ingress-rpc/src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ use reth_rpc_eth_types::{EthApiError, RpcInvalidTransactionError, SignError};
use std::collections::HashSet;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tips_core::Bundle;
use tokio::time::Instant;
use tracing::warn;

use crate::metrics::record_histogram;

const MAX_BUNDLE_GAS: u64 = 25_000_000;

/// Account info for a given address
Expand All @@ -33,10 +36,13 @@ pub trait AccountInfoLookup: Send + Sync {
#[async_trait]
impl AccountInfoLookup for RootProvider<Optimism> {
async fn fetch_account_info(&self, address: Address) -> RpcResult<AccountInfo> {
let start = Instant::now();
let account = self
.get_account(address)
.await
.map_err(|_| EthApiError::Signing(SignError::NoAccount))?;
record_histogram(start.elapsed(), "eth_getAccount".to_string());

Ok(AccountInfo {
balance: account.balance,
nonce: account.nonce,
Expand All @@ -55,6 +61,7 @@ pub trait L1BlockInfoLookup: Send + Sync {
#[async_trait]
impl L1BlockInfoLookup for RootProvider<Optimism> {
async fn fetch_l1_block_info(&self) -> RpcResult<L1BlockInfo> {
let start = Instant::now();
let block = self
.get_block(BlockId::Number(BlockNumberOrTag::Latest))
.full()
Expand All @@ -67,6 +74,7 @@ impl L1BlockInfoLookup for RootProvider<Optimism> {
warn!(message = "empty latest block returned");
EthApiError::InternalEthError.into_rpc_err()
})?;
record_histogram(start.elapsed(), "eth_getBlockByNumber".to_string());

let txs = block.transactions.clone();
let first_tx = txs.first_transaction().ok_or_else(|| {
Expand Down