Skip to content
Merged
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
57 changes: 53 additions & 4 deletions src/rpc/methods/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -2995,6 +2995,55 @@ impl RpcMethod<2> for FilecoinAddressToEthAddress {
}
}

async fn get_eth_transaction_receipt_with_cache(
ctx: Ctx,
tx_hash: EthHash,
limit: Option<ChainEpoch>,
cancellation_token: &CancellationToken,
) -> Result<Option<EthTxReceipt>, ServerError> {
const CACHE_SIZE: NonZeroUsize = nonzero!(1024usize); // ~1.25MiB on mainnet
static CACHE: LazyLock<SizeTrackingCache<EthHash, EthTxReceipt>> = LazyLock::new(|| {
SizeTrackingCache::new_with_metrics("eth_transaction_receipt", 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) => {
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),
}
}

async fn get_eth_transaction_receipt(
ctx: Ctx,
tx_hash: EthHash,
Expand Down Expand Up @@ -3079,7 +3128,7 @@ impl RpcMethod<1> for EthGetTransactionReceipt {
) -> Result<Self::Ok, ServerError> {
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
}
}

Expand All @@ -3101,7 +3150,7 @@ impl RpcMethod<2> for EthGetTransactionReceiptLimited {
) -> Result<Self::Ok, ServerError> {
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
}
}

Expand Down
35 changes: 23 additions & 12 deletions src/state_manager/message_search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,34 +126,45 @@ impl StateManager {
&self,
current: Tipset,
message: &ChainMessage,
look_back_limit: Option<i64>,
look_back_limit: Option<ChainEpoch>,
allow_replaced: Option<bool>,
cancellation_token: &CancellationToken,
) -> Result<Option<(Tipset, Receipt)>, 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<ChainEpoch>,
) -> Option<ChainEpoch> {
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,
Expand Down
Loading