From faa6b9d09f2e077cfe3b2792c6cb07071ce3630a Mon Sep 17 00:00:00 2001 From: init4samwise Date: Mon, 16 Feb 2026 14:55:24 +0000 Subject: [PATCH] feat(rpc): add lock-free cache for gas oracle tip suggestions Add GasOracleCache struct that uses atomics to cache the tip cap value per block. This avoids redundant cold storage reads when multiple requests for gas price or max priority fee come in for the same block. - Add GasOracleCache struct in config/mod.rs with atomic block/tip fields - Add gas_cache field to StorageRpcCtxInner and accessor to StorageRpcCtx - Modify suggest_tip_cap to accept cache parameter and check/update it - Pass ctx.gas_cache() to suggest_tip_cap calls in endpoints Closes ENG-1898 --- crates/rpc/src/config/ctx.rs | 10 +++++- crates/rpc/src/config/gas_oracle.rs | 18 +++++++++-- crates/rpc/src/config/mod.rs | 47 +++++++++++++++++++++++++++++ crates/rpc/src/eth/endpoints.rs | 4 +-- 4 files changed, 74 insertions(+), 5 deletions(-) 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()) };