Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
10 changes: 9 additions & 1 deletion crates/rpc/src/config/ctx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use crate::{
config::{
StorageRpcConfig,
GasOracleCache, StorageRpcConfig,
resolve::{BlockTags, ResolveError},
},
eth::EthError,
Expand Down Expand Up @@ -71,6 +71,7 @@ struct StorageRpcCtxInner<H: HotKv> {
tracing_semaphore: Arc<Semaphore>,
filter_manager: FilterManager,
sub_manager: SubscriptionManager,
gas_cache: GasOracleCache,
}

impl<H: HotKv> StorageRpcCtx<H> {
Expand All @@ -90,6 +91,7 @@ impl<H: HotKv> StorageRpcCtx<H> {
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,
Expand All @@ -100,6 +102,7 @@ impl<H: HotKv> StorageRpcCtx<H> {
tracing_semaphore,
filter_manager,
sub_manager,
gas_cache,
}),
}
}
Expand Down Expand Up @@ -165,6 +168,11 @@ impl<H: HotKv> StorageRpcCtx<H> {
&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.
Expand Down
18 changes: 16 additions & 2 deletions crates/rpc/src/config/gas_oracle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -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<U256, ColdStorageError> {
// 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);

Expand Down Expand Up @@ -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();
Expand All @@ -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)
}
47 changes: 47 additions & 0 deletions crates/rpc/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<U256> {
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()
}
}
4 changes: 2 additions & 2 deletions crates/rpc/src/eth/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())?;

Expand Down Expand Up @@ -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())
};
Expand Down