From c7f2a40750a53a0e5088e510b1d017a42e6d9eef Mon Sep 17 00:00:00 2001 From: Akash-Kumar-Sinha Date: Wed, 8 Apr 2026 17:03:34 +0530 Subject: [PATCH 1/2] Add status endpoint with live relayer balance and operational metrics --- src/attestation/handler.rs | 3 ++ src/main.rs | 2 + src/relayer/handler.rs | 2 + src/relayer/transaction.rs | 4 ++ src/server.rs | 4 ++ src/status/handler.rs | 34 +++++++++++++ src/status/mod.rs | 2 + src/status/status_metrics.rs | 99 ++++++++++++++++++++++++++++++++++++ 8 files changed, 150 insertions(+) create mode 100644 src/status/handler.rs create mode 100644 src/status/mod.rs create mode 100644 src/status/status_metrics.rs diff --git a/src/attestation/handler.rs b/src/attestation/handler.rs index ba748a0..da9460f 100644 --- a/src/attestation/handler.rs +++ b/src/attestation/handler.rs @@ -43,6 +43,9 @@ pub async fn attest_handler( attestation_sig = %sig, "SAS attestation issued" ); + + state.metrics.increment_attestations(); + Ok(Json(AttestResponse { success: true, attestation_tx: Some(sig), diff --git a/src/main.rs b/src/main.rs index 4ee01b8..5e65f1c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ mod listener; mod relayer; mod server; mod solana; +mod status; use std::sync::Arc; use tracing_subscriber::EnvFilter; @@ -100,6 +101,7 @@ async fn main() -> Result<(), Box> { tracker, commitment_registry, sas_attestor, + metrics: Arc::new(status::status_metrics::StatusMetrics::new()), }; let app = create_router(state, &config.cors_origins); diff --git a/src/relayer/handler.rs b/src/relayer/handler.rs index ab3a4d5..d2de4a0 100644 --- a/src/relayer/handler.rs +++ b/src/relayer/handler.rs @@ -137,6 +137,8 @@ pub async fn verify_handler( "Re-verification completed" ); + state.metrics.increment_verifications(); + Ok(Json(VerifyResponse { success: true, tx_signature: Some(outcome.signature), diff --git a/src/relayer/transaction.rs b/src/relayer/transaction.rs index d5c3ec4..b295cdf 100644 --- a/src/relayer/transaction.rs +++ b/src/relayer/transaction.rs @@ -61,4 +61,8 @@ impl RelayerTransaction { is_valid: true, }) } + + pub async fn get_balance(&self) -> Result { + self.client.get_balance().await + } } diff --git a/src/server.rs b/src/server.rs index 11093e8..ea55ab6 100644 --- a/src/server.rs +++ b/src/server.rs @@ -16,6 +16,8 @@ use crate::attestation::handler::attest_handler; use crate::attestation::sas::SasAttestor; use crate::relayer::handler::{health_handler, verify_handler}; use crate::relayer::transaction::RelayerTransaction; +use crate::status::handler::status_handler; +use crate::status::status_metrics::StatusMetrics; #[derive(Clone)] pub struct AppState { @@ -25,6 +27,7 @@ pub struct AppState { pub tracker: Arc, pub commitment_registry: Arc, pub sas_attestor: Option>, + pub metrics: Arc, } async fn auth_middleware( @@ -92,6 +95,7 @@ pub fn create_router(state: AppState, cors_origins: &[String]) -> Router { Router::new() .route("/health", get(health_handler)) + .route("/status", get(status_handler)) .merge(verify_routes) .layer(DefaultBodyLimit::max(4096)) .layer(cors) diff --git a/src/status/handler.rs b/src/status/handler.rs new file mode 100644 index 0000000..3b4eb65 --- /dev/null +++ b/src/status/handler.rs @@ -0,0 +1,34 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use axum::extract::State; +use axum::Json; +use serde::Serialize; + +use crate::error::AppError; +use crate::server::AppState; + +#[derive(Serialize)] +pub struct StatusResponse { + pub uptime_seconds: u64, + pub verifications_relayed: u64, + pub attestations_issued: u64, + pub relayer_balance_lamports: u64, + pub sas_configured: bool, +} + +pub async fn status_handler( + State(state): State, +) -> Result, AppError> { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + Ok(Json(StatusResponse { + uptime_seconds: now.saturating_sub(state.metrics.start_time()), + verifications_relayed: state.metrics.verifications_relayed(), + attestations_issued: state.metrics.attestations_issued(), + relayer_balance_lamports: state.relayer_tx.get_balance().await?, + sas_configured: state.sas_attestor.is_some(), + })) +} diff --git a/src/status/mod.rs b/src/status/mod.rs new file mode 100644 index 0000000..07a75c4 --- /dev/null +++ b/src/status/mod.rs @@ -0,0 +1,2 @@ +pub mod status_metrics; +pub mod handler; diff --git a/src/status/status_metrics.rs b/src/status/status_metrics.rs new file mode 100644 index 0000000..679d58e --- /dev/null +++ b/src/status/status_metrics.rs @@ -0,0 +1,99 @@ +use std::sync::atomic::{AtomicU64, Ordering}; + +pub struct StatusMetrics { + total_verifications_relayed: AtomicU64, + total_attestations_issued: AtomicU64, + start_time: u64, +} + +impl StatusMetrics { + pub fn new() -> Self { + Self { + total_verifications_relayed: AtomicU64::new(0), + total_attestations_issued: AtomicU64::new(0), + start_time: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + } + } + + // increase total_verifications_relayed by 1 + pub fn increment_verifications(&self) { + self.total_verifications_relayed + .fetch_add(1, Ordering::Relaxed); + } + + // increase total_attestations_issued by 1 + pub fn increment_attestations(&self) { + self.total_attestations_issued + .fetch_add(1, Ordering::Relaxed); + } + + // getters for the metrics + pub fn verifications_relayed(&self) -> u64 { + self.total_verifications_relayed.load(Ordering::Relaxed) + } + + pub fn attestations_issued(&self) -> u64 { + self.total_attestations_issued.load(Ordering::Relaxed) + } + + pub fn start_time(&self) -> u64 { + self.start_time + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn counters_start_at_zero() { + let m = StatusMetrics::new(); + assert_eq!(m.verifications_relayed(), 0); + assert_eq!(m.attestations_issued(), 0); + } + + #[test] + fn increment_verifications_counts_correctly() { + let m = StatusMetrics::new(); + m.increment_verifications(); + m.increment_verifications(); + assert_eq!(m.verifications_relayed(), 2); + assert_eq!(m.attestations_issued(), 0); + } + + #[test] + fn increment_attestations_counts_correctly() { + let m = StatusMetrics::new(); + m.increment_attestations(); + assert_eq!(m.attestations_issued(), 1); + assert_eq!(m.verifications_relayed(), 0); + } + + #[test] + fn counters_are_independent() { + let m = StatusMetrics::new(); + m.increment_verifications(); + m.increment_verifications(); + m.increment_attestations(); + assert_eq!(m.verifications_relayed(), 2); + assert_eq!(m.attestations_issued(), 1); + } + + #[test] + fn start_time_is_recent() { + let before = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + let m = StatusMetrics::new(); + let after = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + assert!(m.start_time() >= before); + assert!(m.start_time() <= after); + } +} From f8941460faede6608045ecc59ed9df59f1558492 Mon Sep 17 00:00:00 2001 From: Akash-Kumar-Sinha Date: Thu, 9 Apr 2026 16:51:37 +0530 Subject: [PATCH 2/2] cache relayer balance with 30s TTL and expose it only to authenticated API key requests --- src/status/handler.rs | 37 +++++++++++++++++++++++++++++++-- src/status/status_metrics.rs | 40 ++++++++++++++++++++++++++++++++---- 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/src/status/handler.rs b/src/status/handler.rs index 3b4eb65..decf30b 100644 --- a/src/status/handler.rs +++ b/src/status/handler.rs @@ -1,34 +1,67 @@ use std::time::{SystemTime, UNIX_EPOCH}; use axum::extract::State; +use axum::http::HeaderMap; use axum::Json; use serde::Serialize; +use subtle::ConstantTimeEq; use crate::error::AppError; use crate::server::AppState; +const BALANCE_CACHE_TTL_SECONDS: u64 = 30; + #[derive(Serialize)] pub struct StatusResponse { pub uptime_seconds: u64, pub verifications_relayed: u64, pub attestations_issued: u64, - pub relayer_balance_lamports: u64, + pub relayer_balance_lamports: Option, pub sas_configured: bool, } pub async fn status_handler( State(state): State, + headers: HeaderMap, ) -> Result, AppError> { let now = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs(); + let relayer_balance_lamports = match headers + .get("X-API-Key") + .and_then(|value| value.to_str().ok()) + { + Some(key) => { + let key_bytes = key.as_bytes(); + let is_valid = state.api_keys.iter().any(|candidate| { + candidate.len() == key_bytes.len() && candidate.as_bytes().ct_eq(key_bytes).into() + }); + + if is_valid { + let balance_fetched_at = state.metrics.balance_fetched_at(); + let cached_balance = state.metrics.cached_balance(); + + if now.saturating_sub(balance_fetched_at) < BALANCE_CACHE_TTL_SECONDS { + Some(cached_balance) + } else { + let balance = state.relayer_tx.get_balance().await?; + state.metrics.update_cached_balance(balance, now); + Some(balance) + } + } else { + None + } + } + _ => None, + }; + Ok(Json(StatusResponse { uptime_seconds: now.saturating_sub(state.metrics.start_time()), verifications_relayed: state.metrics.verifications_relayed(), attestations_issued: state.metrics.attestations_issued(), - relayer_balance_lamports: state.relayer_tx.get_balance().await?, + relayer_balance_lamports, sas_configured: state.sas_attestor.is_some(), })) } diff --git a/src/status/status_metrics.rs b/src/status/status_metrics.rs index 679d58e..4729ae6 100644 --- a/src/status/status_metrics.rs +++ b/src/status/status_metrics.rs @@ -4,6 +4,8 @@ pub struct StatusMetrics { total_verifications_relayed: AtomicU64, total_attestations_issued: AtomicU64, start_time: u64, + cached_balance: AtomicU64, + balance_fetched_at: AtomicU64, } impl StatusMetrics { @@ -13,8 +15,10 @@ impl StatusMetrics { total_attestations_issued: AtomicU64::new(0), start_time: std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .unwrap() + .unwrap_or_default() .as_secs(), + cached_balance: AtomicU64::new(0), + balance_fetched_at: AtomicU64::new(0), } } @@ -42,6 +46,19 @@ impl StatusMetrics { pub fn start_time(&self) -> u64 { self.start_time } + + pub fn cached_balance(&self) -> u64 { + self.cached_balance.load(Ordering::Relaxed) + } + + pub fn balance_fetched_at(&self) -> u64 { + self.balance_fetched_at.load(Ordering::Relaxed) + } + + pub fn update_cached_balance(&self, balance: u64, fetched_at: u64) { + self.cached_balance.store(balance, Ordering::Relaxed); + self.balance_fetched_at.store(fetched_at, Ordering::Relaxed); + } } #[cfg(test)] @@ -61,7 +78,7 @@ mod tests { m.increment_verifications(); m.increment_verifications(); assert_eq!(m.verifications_relayed(), 2); - assert_eq!(m.attestations_issued(), 0); + assert_eq!(m.attestations_issued(), 0); } #[test] @@ -86,14 +103,29 @@ mod tests { fn start_time_is_recent() { let before = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .unwrap() + .unwrap_or_default() .as_secs(); let m = StatusMetrics::new(); let after = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .unwrap() + .unwrap_or_default() .as_secs(); assert!(m.start_time() >= before); assert!(m.start_time() <= after); } + + #[test] + fn balance_cache_starts_empty() { + let m = StatusMetrics::new(); + assert_eq!(m.cached_balance(), 0); + assert_eq!(m.balance_fetched_at(), 0); + } + + #[test] + fn balance_cache_updates_together() { + let m = StatusMetrics::new(); + m.update_cached_balance(123, 456); + assert_eq!(m.cached_balance(), 123); + assert_eq!(m.balance_fetched_at(), 456); + } }