From eef12cf037b405ad2b465908155646fd6808c5f2 Mon Sep 17 00:00:00 2001 From: Shashank Date: Tue, 28 Jul 2026 12:18:38 +0530 Subject: [PATCH 1/3] fix cache --- src/rpc/methods/eth.rs | 86 ++++++++++++++++++++++++++++-------------- 1 file changed, 57 insertions(+), 29 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 499876d541b..1ca78553236 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -2952,13 +2952,29 @@ impl RpcMethod<2> for FilecoinAddressToEthAddress { } } +#[derive(Clone, Debug, GetSize)] +struct CachedReceipt { + // `None` is a negatively cached "not found" result. + receipt: Option, + // Head this result was computed against. Mutable entries (`None` and + // non-final receipts) are only valid while the head is unchanged. + head_key: TipsetKey, + // A finalized receipt is immutable, so it is never invalidated. + finalized: bool, +} + +fn is_finalized(ctx: &Ctx, receipt_epoch: ChainEpoch) -> bool { + let head_epoch = ctx.chain_store().heaviest_tipset().epoch(); + receipt_epoch <= head_epoch - ctx.chain_config().policy.chain_finality +} + async fn get_eth_transaction_receipt_with_cache( ctx: Ctx, tx_hash: EthHash, limit: Option, cancellation_token: &CancellationToken, ) -> Result, ServerError> { - static CACHE: LazyLock> = LazyLock::new(|| { + static CACHE: LazyLock> = LazyLock::new(|| { const DEFAULT_CACHE_SIZE: NonZeroUsize = nonzero!(10000usize); // ~12.5MiB on mainnet let cache_size = env_or_default( "FOREST_ETH_TRANSACTION_RECEIPT_CACHE_SIZE", @@ -2967,41 +2983,53 @@ async fn get_eth_transaction_receipt_with_cache( SizeTrackingCache::new_with_metrics("eth_transaction_receipt", cache_size) }); - enum TmpError { - NotFound, - Error(ServerError), + let head = ctx.chain_store().heaviest_tipset(); + let head_key = head.key().clone(); + + // Drop stale mutable entries so they are recomputed at the new head; any + // head change (advance or same-epoch reorg) yields a different key. + if CACHE + .peek(&tx_hash) + .is_some_and(|e| !e.finalized && e.head_key != head_key) + { + CACHE.remove(&tx_hash); } - // Do not update cache when not found by returning an error - match CACHE + // A genuine error bypasses the cache; both `Some` and `None` are cached. + let CachedReceipt { receipt, .. } = CACHE .get_or_insert_async(&tx_hash, { let ctx = ctx.shallow_clone(); + let head_key = head_key.clone(); async move { - let receipt = get_eth_transaction_receipt(ctx, tx_hash, limit, cancellation_token) - .await - .map_err(TmpError::Error)? - .ok_or(TmpError::NotFound)?; - Ok(receipt) + let receipt = get_eth_transaction_receipt( + ctx.shallow_clone(), + tx_hash, + limit, + cancellation_token, + ) + .await?; + let finalized = receipt + .as_ref() + .is_some_and(|r| is_finalized(&ctx, r.block_number.0)); + Ok::<_, ServerError>(CachedReceipt { + receipt, + head_key, + finalized, + }) } }) - .await - { - Ok(r) => { - let Some(max_lookback_epoch_inclusive) = StateManager::max_lookback_epoch_inclusive( - ctx.chain_store().heaviest_tipset().epoch(), - limit, - ) else { - return Ok(None); - }; - if r.block_number.0 >= max_lookback_epoch_inclusive { - Ok(Some(r)) - } else { - // Cache hit but beyond the lookback limit - Ok(None) - } - } - Err(TmpError::NotFound) => Ok(None), - Err(TmpError::Error(e)) => Err(e), + .await?; + + let Some(r) = receipt else { return Ok(None) }; + let Some(max_lookback_epoch_inclusive) = + StateManager::max_lookback_epoch_inclusive(head.epoch(), limit) + else { + return Ok(None); + }; + if r.block_number.0 >= max_lookback_epoch_inclusive { + Ok(Some(r)) + } else { + Ok(None) } } From 0af886fb4c6fa3c2bf58a339b0219831f2def4b4 Mon Sep 17 00:00:00 2001 From: Shashank Date: Wed, 29 Jul 2026 13:56:43 +0530 Subject: [PATCH 2/3] avoid cache for limited search --- src/rpc/methods/eth.rs | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 1ca78553236..ea0e016d450 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -2954,7 +2954,7 @@ impl RpcMethod<2> for FilecoinAddressToEthAddress { #[derive(Clone, Debug, GetSize)] struct CachedReceipt { - // `None` is a negatively cached "not found" result. + // `None` means "not found" by a full search. receipt: Option, // Head this result was computed against. Mutable entries (`None` and // non-final receipts) are only valid while the head is unchanged. @@ -2986,8 +2986,7 @@ async fn get_eth_transaction_receipt_with_cache( let head = ctx.chain_store().heaviest_tipset(); let head_key = head.key().clone(); - // Drop stale mutable entries so they are recomputed at the new head; any - // head change (advance or same-epoch reorg) yields a different key. + // Drop stale mutable entries so they are recomputed at the new head. if CACHE .peek(&tx_hash) .is_some_and(|e| !e.finalized && e.head_key != head_key) @@ -2995,8 +2994,13 @@ async fn get_eth_transaction_receipt_with_cache( CACHE.remove(&tx_hash); } - // A genuine error bypasses the cache; both `Some` and `None` are cached. - let CachedReceipt { receipt, .. } = CACHE + enum Uncacheable { + // A bounded search found nothing; the receipt may still exist. + NotFoundWithinLimit, + Error(ServerError), + } + + let receipt = match CACHE .get_or_insert_async(&tx_hash, { let ctx = ctx.shallow_clone(); let head_key = head_key.clone(); @@ -3007,30 +3011,32 @@ async fn get_eth_transaction_receipt_with_cache( limit, cancellation_token, ) - .await?; + .await + .map_err(Uncacheable::Error)?; + if receipt.is_none() && limit.is_some() { + return Err(Uncacheable::NotFoundWithinLimit); + } let finalized = receipt .as_ref() .is_some_and(|r| is_finalized(&ctx, r.block_number.0)); - Ok::<_, ServerError>(CachedReceipt { + Ok::<_, Uncacheable>(CachedReceipt { receipt, head_key, finalized, }) } }) - .await?; + .await + { + Ok(CachedReceipt { receipt, .. }) => receipt, + Err(Uncacheable::NotFoundWithinLimit) => None, + Err(Uncacheable::Error(e)) => return Err(e), + }; let Some(r) = receipt else { return Ok(None) }; - let Some(max_lookback_epoch_inclusive) = - StateManager::max_lookback_epoch_inclusive(head.epoch(), limit) - else { - return Ok(None); - }; - if r.block_number.0 >= max_lookback_epoch_inclusive { - Ok(Some(r)) - } else { - Ok(None) - } + let within_lookback = StateManager::max_lookback_epoch_inclusive(head.epoch(), limit) + .is_some_and(|max| r.block_number.0 >= max); + Ok(within_lookback.then_some(r)) } async fn get_eth_transaction_receipt( From 650a10d0280b586dda0d0317be2df8b545ba36e0 Mon Sep 17 00:00:00 2001 From: Shashank Date: Wed, 29 Jul 2026 19:09:00 +0530 Subject: [PATCH 3/3] cleanup --- src/rpc/methods/eth.rs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index ea0e016d450..994efed173c 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -2963,8 +2963,7 @@ struct CachedReceipt { finalized: bool, } -fn is_finalized(ctx: &Ctx, receipt_epoch: ChainEpoch) -> bool { - let head_epoch = ctx.chain_store().heaviest_tipset().epoch(); +fn is_finalized(ctx: &Ctx, head_epoch: ChainEpoch, receipt_epoch: ChainEpoch) -> bool { receipt_epoch <= head_epoch - ctx.chain_config().policy.chain_finality } @@ -2985,14 +2984,10 @@ async fn get_eth_transaction_receipt_with_cache( let head = ctx.chain_store().heaviest_tipset(); let head_key = head.key().clone(); + let head_epoch = head.epoch(); // Drop stale mutable entries so they are recomputed at the new head. - if CACHE - .peek(&tx_hash) - .is_some_and(|e| !e.finalized && e.head_key != head_key) - { - CACHE.remove(&tx_hash); - } + CACHE.remove_if(&tx_hash, |e| !e.finalized && e.head_key != head_key); enum Uncacheable { // A bounded search found nothing; the receipt may still exist. @@ -3013,12 +3008,12 @@ async fn get_eth_transaction_receipt_with_cache( ) .await .map_err(Uncacheable::Error)?; - if receipt.is_none() && limit.is_some() { + if receipt.is_none() && limit.is_some_and(|limit| limit > 0) { return Err(Uncacheable::NotFoundWithinLimit); } let finalized = receipt .as_ref() - .is_some_and(|r| is_finalized(&ctx, r.block_number.0)); + .is_some_and(|r| is_finalized(&ctx, head.epoch(), r.block_number.0)); Ok::<_, Uncacheable>(CachedReceipt { receipt, head_key, @@ -3034,7 +3029,7 @@ async fn get_eth_transaction_receipt_with_cache( }; let Some(r) = receipt else { return Ok(None) }; - let within_lookback = StateManager::max_lookback_epoch_inclusive(head.epoch(), limit) + let within_lookback = StateManager::max_lookback_epoch_inclusive(head_epoch, limit) .is_some_and(|max| r.block_number.0 >= max); Ok(within_lookback.then_some(r)) }