From 8c61d6d80f0d41894fc254410bbc53d382678357 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Tue, 7 Jul 2026 21:00:52 +0800 Subject: [PATCH 1/4] fix: cache eth transaction receipt for better RPC perf --- src/rpc/methods/eth.rs | 64 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index b1a094aa73c9..e426b819d1b0 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -757,7 +757,7 @@ impl HasLotusJson for EthSyncingResult { } } -#[derive(PartialEq, Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] +#[derive(PartialEq, Debug, Default, Clone, Serialize, Deserialize, JsonSchema, GetSize)] #[serde(rename_all = "camelCase")] pub struct EthTxReceipt { transaction_hash: EthHash, @@ -788,7 +788,7 @@ impl EthTxReceipt { } /// Represents the results of an event filter execution. -#[derive(PartialEq, Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] +#[derive(PartialEq, Debug, Default, Clone, Serialize, Deserialize, JsonSchema, GetSize)] #[serde(rename_all = "camelCase")] pub struct EthLog { /// The address of the actor that produced the event log. @@ -2995,6 +2995,62 @@ impl RpcMethod<2> for FilecoinAddressToEthAddress { } } +async fn get_eth_transaction_receipt_with_cache( + ctx: Ctx, + tx_hash: EthHash, + limit: Option, + cancellation_token: &CancellationToken, +) -> Result, ServerError> { + const CACHE_SIZE: NonZeroUsize = nonzero!(1024usize); // ~1.25MiB on mainnet + static CACHE: LazyLock> = + LazyLock::new(|| SizeTrackingCache::new_with_metrics("eth_transaction", CACHE_SIZE)); + + enum TmpError { + NotFound, + Error(ServerError), + } + + // Do not update cache when not found by returning an error + match CACHE + .get_or_insert_async(&tx_hash, { + let ctx = ctx.shallow_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) + } + }) + .await + { + Ok(r) if let Some(limit) = limit => { + // Use `>` instead of `>=` here to match the calculation in `message_search.rs` + // ``` + // let lookback_max_epoch = match look_back_limit { + // // No search: limit = 0 means search 0 epochs + // Some(0) => return Ok(None), + // // Limited search: calculate the inclusive lower bound, clamped to genesis + // // Example: limit=5 at epoch=1000 → min_epoch=996, searches [996,1000] = 5 epochs + // // Example: limit=2000 at epoch=1000 → min_epoch=0, searches [0,1000] = 1001 epochs (all available) + // Some(limit) if limit > 0 => (current_epoch - limit + 1).max(0), + // // Search all the way to genesis (epoch 0) + // _ => 0, + // }; + // ``` + if r.block_number.0 + limit > ctx.chain_store().heaviest_tipset().epoch() { + Ok(Some(r)) + } else { + // Cache hit but beyond the lookback limit + Ok(None) + } + } + Ok(r) => Ok(Some(r)), // `limit` is `None` + Err(TmpError::NotFound) => Ok(None), + Err(TmpError::Error(e)) => Err(e), + } +} + async fn get_eth_transaction_receipt( ctx: Ctx, tx_hash: EthHash, @@ -3079,7 +3135,7 @@ impl RpcMethod<1> for EthGetTransactionReceipt { ) -> Result { let cancellation_token = CancellationToken::new(); let _drop_guard = cancellation_token.drop_guard_ref(); - get_eth_transaction_receipt(ctx, tx_hash, None, &cancellation_token).await + get_eth_transaction_receipt_with_cache(ctx, tx_hash, None, &cancellation_token).await } } @@ -3101,7 +3157,7 @@ impl RpcMethod<2> for EthGetTransactionReceiptLimited { ) -> Result { let cancellation_token = CancellationToken::new(); let _drop_guard = cancellation_token.drop_guard_ref(); - get_eth_transaction_receipt(ctx, tx_hash, Some(limit), &cancellation_token).await + get_eth_transaction_receipt_with_cache(ctx, tx_hash, Some(limit), &cancellation_token).await } } From d17beece370e47c692fa9e9fbc17fec8206ba7bb Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Tue, 7 Jul 2026 21:08:55 +0800 Subject: [PATCH 2/4] Update src/rpc/methods/eth.rs Co-authored-by: Hubert --- src/rpc/methods/eth.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index e426b819d1b0..4ba66682f2eb 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -3003,7 +3003,7 @@ async fn get_eth_transaction_receipt_with_cache( ) -> Result, ServerError> { const CACHE_SIZE: NonZeroUsize = nonzero!(1024usize); // ~1.25MiB on mainnet static CACHE: LazyLock> = - LazyLock::new(|| SizeTrackingCache::new_with_metrics("eth_transaction", CACHE_SIZE)); + LazyLock::new(|| SizeTrackingCache::new_with_metrics("eth_transaction_receipt", CACHE_SIZE)); enum TmpError { NotFound, From 89d0621e22e9b5df1631f82a7aedd07f67e85a79 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Tue, 7 Jul 2026 21:21:38 +0800 Subject: [PATCH 3/4] make `max_lookback_epoch_inclusive` a function --- src/rpc/methods/eth.rs | 24 +++++++------------- src/state_manager/message_search.rs | 35 +++++++++++++++++++---------- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index e426b819d1b0..3a32ba7b9fee 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -3024,28 +3024,20 @@ async fn get_eth_transaction_receipt_with_cache( }) .await { - Ok(r) if let Some(limit) = limit => { - // Use `>` instead of `>=` here to match the calculation in `message_search.rs` - // ``` - // let lookback_max_epoch = match look_back_limit { - // // No search: limit = 0 means search 0 epochs - // Some(0) => return Ok(None), - // // Limited search: calculate the inclusive lower bound, clamped to genesis - // // Example: limit=5 at epoch=1000 → min_epoch=996, searches [996,1000] = 5 epochs - // // Example: limit=2000 at epoch=1000 → min_epoch=0, searches [0,1000] = 1001 epochs (all available) - // Some(limit) if limit > 0 => (current_epoch - limit + 1).max(0), - // // Search all the way to genesis (epoch 0) - // _ => 0, - // }; - // ``` - if r.block_number.0 + limit > ctx.chain_store().heaviest_tipset().epoch() { + 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) } } - Ok(r) => Ok(Some(r)), // `limit` is `None` Err(TmpError::NotFound) => Ok(None), Err(TmpError::Error(e)) => Err(e), } diff --git a/src/state_manager/message_search.rs b/src/state_manager/message_search.rs index 4c67bf517035..3f8a323f04e0 100644 --- a/src/state_manager/message_search.rs +++ b/src/state_manager/message_search.rs @@ -126,34 +126,45 @@ impl StateManager { &self, current: Tipset, message: &ChainMessage, - look_back_limit: Option, + look_back_limit: Option, allow_replaced: Option, cancellation_token: &CancellationToken, ) -> Result, Error> { let current_epoch = current.epoch(); let allow_replaced = allow_replaced.unwrap_or(true); - // Calculate the max lookback epoch (inclusive lower bound) for the search. - let lookback_max_epoch = match look_back_limit { - // No search: limit = 0 means search 0 epochs - Some(0) => return Ok(None), - // Limited search: calculate the inclusive lower bound, clamped to genesis - // Example: limit=5 at epoch=1000 → min_epoch=996, searches [996,1000] = 5 epochs - // Example: limit=2000 at epoch=1000 → min_epoch=0, searches [0,1000] = 1001 epochs (all available) - Some(limit) if limit > 0 => (current_epoch - limit + 1).max(0), - // Search all the way to genesis (epoch 0) - _ => 0, + let Some(max_lookback_epoch_inclusive) = + Self::max_lookback_epoch_inclusive(current_epoch, look_back_limit) + else { + return Ok(None); }; self.check_search_blocking( current, message, - lookback_max_epoch, + max_lookback_epoch_inclusive, allow_replaced, cancellation_token, ) } + //. Calculates the max lookback epoch (inclusive lower bound) for the search. + pub fn max_lookback_epoch_inclusive( + current_epoch: ChainEpoch, + look_back_limit: Option, + ) -> Option { + match look_back_limit { + // No search: limit = 0 means search 0 epochs + Some(0) => None, + // Limited search: calculate the inclusive lower bound, clamped to genesis + // Example: limit=5 at epoch=1000 → min_epoch=996, searches [996,1000] = 5 epochs + // Example: limit=2000 at epoch=1000 → min_epoch=0, searches [0,1000] = 1001 epochs (all available) + Some(limit) if limit > 0 => Some((current_epoch - limit + 1).max(0)), + // Search all the way to genesis (epoch 0) + _ => Some(0), + } + } + /// Returns a message receipt from a given tipset and message CID. pub fn get_receipt_blocking( &self, From ccd1e8647e1b07fdab91d42d457697268867ca9d Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Tue, 7 Jul 2026 21:22:11 +0800 Subject: [PATCH 4/4] fmt --- src/rpc/methods/eth.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index e224451d5008..21a7a95d7bdc 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -3002,8 +3002,9 @@ async fn get_eth_transaction_receipt_with_cache( cancellation_token: &CancellationToken, ) -> Result, ServerError> { const CACHE_SIZE: NonZeroUsize = nonzero!(1024usize); // ~1.25MiB on mainnet - static CACHE: LazyLock> = - LazyLock::new(|| SizeTrackingCache::new_with_metrics("eth_transaction_receipt", CACHE_SIZE)); + static CACHE: LazyLock> = LazyLock::new(|| { + SizeTrackingCache::new_with_metrics("eth_transaction_receipt", CACHE_SIZE) + }); enum TmpError { NotFound,