diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 3c33ee6cb1f..a335afbf59e 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -2952,13 +2952,28 @@ impl RpcMethod<2> for FilecoinAddressToEthAddress { } } +#[derive(Clone, Debug, GetSize)] +struct CachedReceipt { + // `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. + head_key: TipsetKey, + // A finalized receipt is immutable, so it is never invalidated. + finalized: bool, +} + +fn is_finalized(ctx: &Ctx, head_epoch: ChainEpoch, receipt_epoch: ChainEpoch) -> bool { + 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,42 +2982,56 @@ async fn get_eth_transaction_receipt_with_cache( SizeTrackingCache::new_with_metrics("eth_transaction_receipt", cache_size) }); - enum TmpError { - NotFound, + 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. + 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. + NotFoundWithinLimit, Error(ServerError), } - // Do not update cache when not found by returning an error - match CACHE + let receipt = match 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 + .map_err(Uncacheable::Error)?; + 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, head.epoch(), r.block_number.0)); + Ok::<_, Uncacheable>(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), - } + Ok(CachedReceipt { receipt, .. }) => receipt, + Err(Uncacheable::NotFoundWithinLimit) => None, + Err(Uncacheable::Error(e)) => return Err(e), + }; + + let Some(r) = receipt else { return 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(