diff --git a/crates/rpc/src/config/ctx.rs b/crates/rpc/src/config/ctx.rs index 46f31a6..cfb5b92 100644 --- a/crates/rpc/src/config/ctx.rs +++ b/crates/rpc/src/config/ctx.rs @@ -2,7 +2,7 @@ use crate::{ config::{ - StorageRpcConfig, + GasOracleCache, StorageRpcConfig, resolve::{BlockTags, ResolveError}, }, eth::EthError, @@ -71,6 +71,7 @@ struct StorageRpcCtxInner { tracing_semaphore: Arc, filter_manager: FilterManager, sub_manager: SubscriptionManager, + gas_cache: GasOracleCache, } impl StorageRpcCtx { @@ -90,6 +91,7 @@ impl StorageRpcCtx { let tracing_semaphore = Arc::new(Semaphore::new(config.max_tracing_requests)); let filter_manager = FilterManager::new(config.stale_filter_ttl, config.stale_filter_ttl); let sub_manager = SubscriptionManager::new(notif_sender, config.stale_filter_ttl); + let gas_cache = GasOracleCache::new(); Self { inner: Arc::new(StorageRpcCtxInner { storage, @@ -100,6 +102,7 @@ impl StorageRpcCtx { tracing_semaphore, filter_manager, sub_manager, + gas_cache, }), } } @@ -165,6 +168,11 @@ impl StorageRpcCtx { &self.inner.sub_manager } + /// Access the gas oracle cache. + pub fn gas_cache(&self) -> &GasOracleCache { + &self.inner.gas_cache + } + /// Resolve a [`BlockNumberOrTag`] to a block number. /// /// This is synchronous — no cold storage lookup is needed. diff --git a/crates/rpc/src/config/gas_oracle.rs b/crates/rpc/src/config/gas_oracle.rs index 76b1872..742e6a6 100644 --- a/crates/rpc/src/config/gas_oracle.rs +++ b/crates/rpc/src/config/gas_oracle.rs @@ -9,7 +9,7 @@ use alloy::{consensus::Transaction, primitives::U256}; use signet_cold::{ColdStorageError, ColdStorageReadHandle, HeaderSpecifier}; -use crate::config::StorageRpcConfig; +use crate::config::{GasOracleCache, StorageRpcConfig}; /// Suggest a tip cap based on recent transaction tips. /// @@ -20,11 +20,20 @@ use crate::config::StorageRpcConfig; /// /// Returns `default_gas_price` (default 1 Gwei) when no qualifying /// transactions are found. +/// +/// Uses the provided `cache` to avoid redundant cold storage reads +/// when the tip has already been computed for the current block. pub(crate) async fn suggest_tip_cap( cold: &ColdStorageReadHandle, latest: u64, config: &StorageRpcConfig, + cache: &GasOracleCache, ) -> Result { + // Check cache first - return early on hit + if let Some(cached_tip) = cache.get(latest) { + return Ok(cached_tip); + } + let block_count = config.gas_oracle_block_count.min(latest + 1); let start = latest.saturating_sub(block_count - 1); @@ -64,7 +73,9 @@ pub(crate) async fn suggest_tip_cap( } if all_tips.is_empty() { - return Ok(config.default_gas_price.map_or(U256::ZERO, U256::from)); + let default_price = config.default_gas_price.map_or(U256::ZERO, U256::from); + cache.set(latest, default_price); + return Ok(default_price); } all_tips.sort_unstable(); @@ -78,5 +89,8 @@ pub(crate) async fn suggest_tip_cap( price = price.min(U256::from(max)); } + // Store result in cache before returning + cache.set(latest, price); + Ok(price) } diff --git a/crates/rpc/src/config/mod.rs b/crates/rpc/src/config/mod.rs index 8a99258..891a859 100644 --- a/crates/rpc/src/config/mod.rs +++ b/crates/rpc/src/config/mod.rs @@ -4,6 +4,10 @@ //! that wraps [`signet_storage::UnifiedStorage`], gas oracle helpers, //! and block tag / block ID resolution logic. +use std::sync::atomic::{AtomicU64, Ordering}; + +use alloy::primitives::U256; + mod rpc_config; pub use rpc_config::StorageRpcConfig; @@ -15,3 +19,46 @@ pub(crate) mod gas_oracle; pub(crate) mod resolve; pub use resolve::{BlockTags, SyncStatus}; + +/// Lock-free cache for gas oracle tip suggestions. +/// Invalidates automatically when block number changes. +#[derive(Debug)] +pub struct GasOracleCache { + /// Block number this cached value corresponds to + block: AtomicU64, + /// Cached tip value + tip: AtomicU64, +} + +impl GasOracleCache { + /// Create a new cache with no valid cached value. + pub const fn new() -> Self { + Self { + block: AtomicU64::new(u64::MAX), // sentinel - no valid cache + tip: AtomicU64::new(0), + } + } + + /// Returns cached tip if still valid for current block + pub fn get(&self, current_block: u64) -> Option { + let cached_block = self.block.load(Ordering::Acquire); + if cached_block == current_block { + Some(U256::from(self.tip.load(Ordering::Acquire))) + } else { + None + } + } + + /// Update cache for new block + pub fn set(&self, block: u64, tip: U256) { + let tip_u64: u64 = tip.try_into().unwrap_or(u64::MAX); + self.tip.store(tip_u64, Ordering::Release); + self.block.store(block, Ordering::Release); + } +} + +impl Default for GasOracleCache { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/rpc/src/eth/endpoints.rs b/crates/rpc/src/eth/endpoints.rs index e54fd9d..fc59d82 100644 --- a/crates/rpc/src/eth/endpoints.rs +++ b/crates/rpc/src/eth/endpoints.rs @@ -107,7 +107,7 @@ where let latest = ctx.tags().latest(); let cold = ctx.cold(); - let tip = gas_oracle::suggest_tip_cap(&cold, latest, ctx.config()) + let tip = gas_oracle::suggest_tip_cap(&cold, latest, ctx.config(), ctx.gas_cache()) .await .map_err(|e| e.to_string())?; @@ -135,7 +135,7 @@ where { let task = async move { let latest = ctx.tags().latest(); - gas_oracle::suggest_tip_cap(&ctx.cold(), latest, ctx.config()) + gas_oracle::suggest_tip_cap(&ctx.cold(), latest, ctx.config(), ctx.gas_cache()) .await .map_err(|e| e.to_string()) };