Skip to main content

reth_rpc/eth/
filter.rs

1//! `eth_` `Filter` RPC handler implementation
2
3use alloy_consensus::BlockHeader;
4use alloy_eips::BlockNumberOrTag;
5use alloy_primitives::{Sealable, TxHash};
6use alloy_rpc_types_eth::{
7    error::EthRpcErrorCode, Filter, FilterBlockOption, FilterChanges, FilterId,
8    PendingTransactionFilterKind,
9};
10use async_trait::async_trait;
11use futures::{
12    future::TryFutureExt,
13    stream::{FuturesOrdered, StreamExt},
14    Future,
15};
16use itertools::Itertools;
17use jsonrpsee::{core::RpcResult, server::IdProvider};
18use reth_errors::ProviderError;
19use reth_primitives_traits::{NodePrimitives, SealedHeader};
20use reth_rpc_eth_api::{
21    helpers::{EthBlocks, LoadReceipt},
22    EngineEthFilter, EthApiTypes, EthFilterApiServer, FullEthApiTypes, QueryLimits, RpcConvert,
23    RpcLog, RpcNodeCoreExt, RpcTransaction,
24};
25use reth_rpc_eth_types::{
26    logs_utils::{self, append_matching_block_logs, ProviderOrBlock},
27    EthApiError, EthFilterConfig, EthStateCache, EthSubscriptionIdProvider,
28};
29use reth_rpc_server_types::{result::rpc_error_with_code, ToRpcResult};
30use reth_storage_api::{
31    BlockHashReader, BlockIdReader, BlockNumReader, BlockReader, HeaderProvider, ProviderBlock,
32    ProviderReceipt, ReceiptProvider,
33};
34use reth_tasks::Runtime;
35use reth_transaction_pool::{NewSubpoolTransactionStream, PoolTransaction, TransactionPool};
36use std::{
37    collections::{HashMap, VecDeque},
38    fmt,
39    iter::{Peekable, StepBy},
40    ops::RangeInclusive,
41    pin::Pin,
42    sync::Arc,
43    time::{Duration, Instant},
44};
45use tokio::{
46    sync::{mpsc::Receiver, oneshot, Mutex},
47    time::MissedTickBehavior,
48};
49use tracing::{debug, error, trace};
50
51impl<Eth> EngineEthFilter<RpcLog<Eth::NetworkTypes>> for EthFilter<Eth>
52where
53    Eth: FullEthApiTypes + RpcNodeCoreExt<Provider: BlockIdReader> + LoadReceipt + EthBlocks,
54{
55    /// Returns logs matching given filter object, no query limits
56    fn logs(
57        &self,
58        filter: Filter,
59        limits: QueryLimits,
60    ) -> impl Future<Output = RpcResult<Vec<RpcLog<Eth::NetworkTypes>>>> + Send {
61        trace!(target: "rpc::eth", "Serving eth_getLogs");
62        self.logs_for_filter(filter, limits).map_err(|e| e.into())
63    }
64}
65
66/// Threshold for deciding between cached and range mode processing
67const CACHED_MODE_BLOCK_THRESHOLD: u64 = 250;
68
69/// Threshold for bloom filter matches that triggers reduced caching
70const HIGH_BLOOM_MATCH_THRESHOLD: usize = 20;
71
72/// Threshold for bloom filter matches that triggers moderately reduced caching
73const MODERATE_BLOOM_MATCH_THRESHOLD: usize = 10;
74
75/// Minimum block count to apply bloom filter match adjustments
76const BLOOM_ADJUSTMENT_MIN_BLOCKS: u64 = 100;
77
78/// The maximum number of headers we read at once when handling a range filter.
79const MAX_HEADERS_RANGE: u64 = 1_000; // with ~530bytes per header this is ~500kb
80
81// Cached mode is only reachable for ranges that fit into a single header window, which is what
82// keeps the mode decision independent of how a range is split.
83const _: () = assert!(CACHED_MODE_BLOCK_THRESHOLD <= MAX_HEADERS_RANGE);
84
85/// Minimum number of bloom matching blocks in a header window for fetching their receipts in
86/// parallel
87const PARALLEL_PROCESSING_THRESHOLD: usize = 1000;
88
89/// Default concurrency for parallel processing
90const DEFAULT_PARALLEL_CONCURRENCY: usize = 4;
91
92/// Maximum number of blocks whose receipts the parallel fetching holds in memory at once
93const MAX_PARALLEL_BATCH_SIZE: usize = 256;
94
95/// `Eth` filter RPC implementation.
96///
97/// This type handles `eth_` rpc requests related to filters (`eth_getLogs`).
98pub struct EthFilter<Eth: EthApiTypes> {
99    /// All nested fields bundled together
100    inner: Arc<EthFilterInner<Eth>>,
101}
102
103impl<Eth> Clone for EthFilter<Eth>
104where
105    Eth: EthApiTypes,
106{
107    fn clone(&self) -> Self {
108        Self { inner: self.inner.clone() }
109    }
110}
111
112impl<Eth> EthFilter<Eth>
113where
114    Eth: EthApiTypes + 'static,
115{
116    /// Creates a new, shareable instance.
117    ///
118    /// This uses the given pool to get notified about new transactions, the provider to interact
119    /// with the blockchain, the cache to fetch cacheable data, like the logs.
120    ///
121    /// See also [`EthFilterConfig`].
122    ///
123    /// This also spawns a task that periodically clears stale filters.
124    ///
125    /// # Create a new instance with [`EthApi`](crate::EthApi)
126    ///
127    /// ```no_run
128    /// use reth_evm_ethereum::EthEvmConfig;
129    /// use reth_network_api::noop::NoopNetwork;
130    /// use reth_provider::noop::NoopProvider;
131    /// use reth_rpc::{EthApi, EthFilter};
132    /// use reth_tasks::Runtime;
133    /// use reth_transaction_pool::noop::NoopTransactionPool;
134    /// let eth_api = EthApi::builder(
135    ///     NoopProvider::default(),
136    ///     NoopTransactionPool::default(),
137    ///     NoopNetwork::default(),
138    ///     EthEvmConfig::mainnet(),
139    /// )
140    /// .build();
141    /// let filter = EthFilter::new(eth_api, Default::default(), Runtime::test());
142    /// ```
143    pub fn new(eth_api: Eth, config: EthFilterConfig, task_spawner: Runtime) -> Self {
144        let EthFilterConfig { max_blocks_per_filter, max_logs_per_response, stale_filter_ttl } =
145            config;
146        let inner = EthFilterInner {
147            eth_api,
148            active_filters: ActiveFilters::new(),
149            id_provider: Arc::new(EthSubscriptionIdProvider::default()),
150            max_headers_range: MAX_HEADERS_RANGE,
151            task_spawner,
152            stale_filter_ttl,
153            query_limits: QueryLimits { max_blocks_per_filter, max_logs_per_response },
154        };
155
156        let eth_filter = Self { inner: Arc::new(inner) };
157
158        let this = eth_filter.clone();
159        eth_filter.inner.task_spawner.spawn_critical_task(
160            "eth-filters_stale-filters-clean",
161            async move {
162                this.watch_and_clear_stale_filters().await;
163            },
164        );
165
166        eth_filter
167    }
168
169    /// Returns all currently active filters
170    pub fn active_filters(&self) -> &ActiveFilters<RpcTransaction<Eth::NetworkTypes>> {
171        &self.inner.active_filters
172    }
173
174    /// Endless future that [`Self::clear_stale_filters`] every `stale_filter_ttl` interval.
175    /// Nonetheless, this endless future frees the thread at every await point.
176    async fn watch_and_clear_stale_filters(&self) {
177        let mut interval = tokio::time::interval_at(
178            tokio::time::Instant::now() + self.inner.stale_filter_ttl,
179            self.inner.stale_filter_ttl,
180        );
181        interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
182        loop {
183            interval.tick().await;
184            self.clear_stale_filters(Instant::now()).await;
185        }
186    }
187
188    /// Clears all filters that have not been polled for longer than the configured
189    /// `stale_filter_ttl` at the given instant.
190    pub async fn clear_stale_filters(&self, now: Instant) {
191        trace!(target: "rpc::eth", "clear stale filters");
192        let mut filters = self.active_filters().inner.lock().await;
193        filters.retain(|id, filter| {
194            let is_valid = (now - filter.last_poll_timestamp) < self.inner.stale_filter_ttl;
195
196            if !is_valid {
197                trace!(target: "rpc::eth", "evict filter with id: {:?}", id);
198            }
199
200            is_valid
201        });
202        filters.shrink_to_fit();
203    }
204}
205
206impl<Eth> EthFilter<Eth>
207where
208    Eth: FullEthApiTypes<Provider: BlockReader + BlockIdReader> + LoadReceipt + EthBlocks,
209{
210    /// Access the underlying provider.
211    fn provider(&self) -> &Eth::Provider {
212        self.inner.eth_api.provider()
213    }
214
215    /// Access the underlying pool.
216    fn pool(&self) -> &Eth::Pool {
217        self.inner.eth_api.pool()
218    }
219
220    /// Returns all the filter changes for the given id, if any
221    pub async fn filter_changes(
222        &self,
223        id: FilterId,
224    ) -> Result<
225        FilterChanges<RpcTransaction<Eth::NetworkTypes>, RpcLog<Eth::NetworkTypes>>,
226        EthFilterError,
227    > {
228        let info = self.provider().chain_info()?;
229        let best_number = info.best_number;
230
231        // start_block is the block from which we should start fetching changes, the next block from
232        // the last time changes were polled, in other words the best block at last poll + 1
233        let (start_block, kind) = {
234            let mut filters = self.inner.active_filters.inner.lock().await;
235            let filter = filters.get_mut(&id).ok_or(EthFilterError::FilterNotFound(id))?;
236
237            if filter.block > best_number {
238                // no new blocks since the last poll
239                return Ok(FilterChanges::Empty)
240            }
241
242            // update filter
243            // we fetch all changes from [filter.block..best_block], so we advance the filter's
244            // block to `best_block +1`, the next from which we should start fetching changes again
245            let mut block = best_number + 1;
246            std::mem::swap(&mut filter.block, &mut block);
247            filter.last_poll_timestamp = Instant::now();
248
249            (block, filter.kind.clone())
250        };
251
252        match kind {
253            FilterKind::PendingTransaction(filter) => Ok(match filter.drain().await {
254                FilterChanges::Empty => FilterChanges::Empty,
255                FilterChanges::Hashes(hashes) => FilterChanges::Hashes(hashes),
256                FilterChanges::Transactions(transactions) => {
257                    FilterChanges::Transactions(transactions)
258                }
259                FilterChanges::Logs(_) => unreachable!("pending transaction filter returned logs"),
260            }),
261            FilterKind::Block => {
262                // Note: we need to fetch the block hashes from inclusive range
263                // [start_block..best_block]
264                let end_block = best_number + 1;
265                let block_hashes =
266                    self.provider().canonical_hashes_range(start_block, end_block).map_err(
267                        |_| EthApiError::HeaderRangeNotFound(start_block.into(), end_block.into()),
268                    )?;
269                Ok(FilterChanges::Hashes(block_hashes))
270            }
271            FilterKind::Log(filter) => {
272                let (from_block_number, to_block_number) = match filter.block_option {
273                    FilterBlockOption::Range { from_block, to_block } => {
274                        let from = from_block
275                            .map(|num| self.provider().convert_block_number(num))
276                            .transpose()?
277                            .flatten();
278                        let to = to_block
279                            .map(|num| self.provider().convert_block_number(num))
280                            .transpose()?
281                            .flatten();
282                        logs_utils::get_filter_block_range(from, to, start_block, info)?
283                    }
284                    FilterBlockOption::AtBlockHash(block_hash) => {
285                        // blockHash is equivalent to fromBlock = toBlock = the block number with
286                        // hash blockHash
287                        // get_logs_in_block_range is inclusive
288                        let block_number = self
289                            .provider()
290                            .block_number(block_hash)?
291                            .ok_or(ProviderError::HeaderNotFound(block_hash.into()))?;
292                        (block_number, block_number)
293                    }
294                };
295                let logs = self
296                    .inner
297                    .clone()
298                    .get_logs_in_block_range(
299                        *filter,
300                        from_block_number,
301                        to_block_number,
302                        self.inner.query_limits,
303                    )
304                    .await?;
305                Ok(FilterChanges::Logs(logs))
306            }
307        }
308    }
309
310    /// Returns an array of all logs matching filter with given id.
311    ///
312    /// Returns an error if no matching log filter exists.
313    ///
314    /// Handler for `eth_getFilterLogs`
315    pub async fn filter_logs(
316        &self,
317        id: FilterId,
318    ) -> Result<Vec<RpcLog<Eth::NetworkTypes>>, EthFilterError> {
319        let filter = {
320            let mut filters = self.inner.active_filters.inner.lock().await;
321            let filter =
322                filters.get_mut(&id).ok_or_else(|| EthFilterError::FilterNotFound(id.clone()))?;
323            if let FilterKind::Log(ref inner_filter) = filter.kind {
324                filter.last_poll_timestamp = Instant::now();
325                *inner_filter.clone()
326            } else {
327                // Not a log filter
328                return Err(EthFilterError::FilterNotFound(id))
329            }
330        };
331
332        self.logs_for_filter(filter, self.inner.query_limits).await
333    }
334
335    /// Returns logs matching given filter object.
336    async fn logs_for_filter(
337        &self,
338        filter: Filter,
339        limits: QueryLimits,
340    ) -> Result<Vec<RpcLog<Eth::NetworkTypes>>, EthFilterError> {
341        self.inner.clone().logs_for_filter(filter, limits).await
342    }
343}
344
345impl<Eth> EthFilterApiServer<RpcTransaction<Eth::NetworkTypes>, RpcLog<Eth::NetworkTypes>>
346    for EthFilter<Eth>
347where
348    Eth: FullEthApiTypes + LoadReceipt + EthBlocks,
349{
350    /// Handler for `eth_newFilter`
351    async fn new_filter(&self, filter: Filter) -> RpcResult<FilterId> {
352        trace!(target: "rpc::eth", "Serving eth_newFilter");
353        self.inner
354            .install_filter(FilterKind::<RpcTransaction<Eth::NetworkTypes>>::Log(Box::new(filter)))
355            .await
356    }
357
358    /// Handler for `eth_newBlockFilter`
359    async fn new_block_filter(&self) -> RpcResult<FilterId> {
360        trace!(target: "rpc::eth", "Serving eth_newBlockFilter");
361        self.inner.install_filter(FilterKind::<RpcTransaction<Eth::NetworkTypes>>::Block).await
362    }
363
364    /// Handler for `eth_newPendingTransactionFilter`
365    async fn new_pending_transaction_filter(
366        &self,
367        kind: Option<PendingTransactionFilterKind>,
368    ) -> RpcResult<FilterId> {
369        trace!(target: "rpc::eth", "Serving eth_newPendingTransactionFilter");
370
371        let transaction_kind = match kind.unwrap_or_default() {
372            PendingTransactionFilterKind::Hashes => {
373                let receiver = self.pool().pending_transactions_listener();
374                let pending_txs_receiver = PendingTransactionsReceiver::new(receiver);
375                FilterKind::PendingTransaction(PendingTransactionKind::Hashes(pending_txs_receiver))
376            }
377            PendingTransactionFilterKind::Full => {
378                let stream = self.pool().new_pending_pool_transactions_listener();
379                let full_txs_receiver = FullTransactionsReceiver::new(
380                    stream,
381                    dyn_clone::clone(self.inner.eth_api.converter()),
382                );
383                FilterKind::PendingTransaction(PendingTransactionKind::FullTransaction(Arc::new(
384                    full_txs_receiver,
385                )))
386            }
387        };
388
389        // Install the filter and propagate any errors
390        self.inner.install_filter(transaction_kind).await
391    }
392
393    /// Handler for `eth_getFilterChanges`
394    async fn filter_changes(
395        &self,
396        id: FilterId,
397    ) -> RpcResult<FilterChanges<RpcTransaction<Eth::NetworkTypes>, RpcLog<Eth::NetworkTypes>>>
398    {
399        trace!(target: "rpc::eth", "Serving eth_getFilterChanges");
400        Ok(Self::filter_changes(self, id).await?)
401    }
402
403    /// Returns an array of all logs matching filter with given id.
404    ///
405    /// Returns an error if no matching log filter exists.
406    ///
407    /// Handler for `eth_getFilterLogs`
408    async fn filter_logs(&self, id: FilterId) -> RpcResult<Vec<RpcLog<Eth::NetworkTypes>>> {
409        trace!(target: "rpc::eth", "Serving eth_getFilterLogs");
410        Ok(Self::filter_logs(self, id).await?)
411    }
412
413    /// Handler for `eth_uninstallFilter`
414    async fn uninstall_filter(&self, id: FilterId) -> RpcResult<bool> {
415        trace!(target: "rpc::eth", "Serving eth_uninstallFilter");
416        let mut filters = self.inner.active_filters.inner.lock().await;
417        if filters.remove(&id).is_some() {
418            trace!(target: "rpc::eth::filter", ?id, "uninstalled filter");
419            Ok(true)
420        } else {
421            Ok(false)
422        }
423    }
424
425    /// Returns logs matching given filter object.
426    ///
427    /// Handler for `eth_getLogs`
428    async fn logs(&self, filter: Filter) -> RpcResult<Vec<RpcLog<Eth::NetworkTypes>>> {
429        trace!(target: "rpc::eth", "Serving eth_getLogs");
430        Ok(self.logs_for_filter(filter, self.inner.query_limits).await?)
431    }
432}
433
434impl<Eth> std::fmt::Debug for EthFilter<Eth>
435where
436    Eth: EthApiTypes,
437{
438    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
439        f.debug_struct("EthFilter").finish_non_exhaustive()
440    }
441}
442
443/// Container type `EthFilter`
444#[derive(Debug)]
445struct EthFilterInner<Eth: EthApiTypes> {
446    /// Inner `eth` API implementation.
447    eth_api: Eth,
448    /// All currently installed filters.
449    active_filters: ActiveFilters<RpcTransaction<Eth::NetworkTypes>>,
450    /// Provides ids to identify filters
451    id_provider: Arc<dyn IdProvider>,
452    /// limits for logs queries
453    query_limits: QueryLimits,
454    /// maximum number of headers to read at once for range filter
455    max_headers_range: u64,
456    /// The type that can spawn tasks.
457    task_spawner: Runtime,
458    /// Duration since the last filter poll, after which the filter is considered stale
459    stale_filter_ttl: Duration,
460}
461
462impl<Eth> EthFilterInner<Eth>
463where
464    Eth: RpcNodeCoreExt<Provider: BlockIdReader, Pool: TransactionPool>
465        + EthApiTypes<NetworkTypes: reth_rpc_eth_api::types::RpcTypes>
466        + LoadReceipt
467        + EthBlocks,
468{
469    /// Access the underlying provider.
470    fn provider(&self) -> &Eth::Provider {
471        self.eth_api.provider()
472    }
473
474    /// Access the underlying [`EthStateCache`].
475    fn eth_cache(&self) -> &EthStateCache<Eth::Primitives> {
476        self.eth_api.cache()
477    }
478
479    /// Returns logs matching given filter object.
480    async fn logs_for_filter(
481        self: Arc<Self>,
482        filter: Filter,
483        limits: QueryLimits,
484    ) -> Result<Vec<RpcLog<Eth::NetworkTypes>>, EthFilterError> {
485        match filter.block_option {
486            FilterBlockOption::AtBlockHash(block_hash) => {
487                // First try to get cached block and receipts, as it's likely they're already cached
488                let Some((receipts, maybe_block)) =
489                    self.eth_cache().get_receipts_and_maybe_block(block_hash).await?
490                else {
491                    // the block itself may still exist with its receipts pruned
492                    return Err(match self.provider().block_number(block_hash)? {
493                        Some(number) => {
494                            let earliest_available = self.provider().earliest_block_number()?;
495                            if number < earliest_available {
496                                EthApiError::PrunedHistoryUnavailable {
497                                    requested: number,
498                                    earliest_available,
499                                }
500                                .into()
501                            } else {
502                                EthFilterError::ReceiptsUnavailable(number)
503                            }
504                        }
505                        None => ProviderError::HeaderNotFound(block_hash.into()).into(),
506                    })
507                };
508
509                let header = if let Some(block) = &maybe_block {
510                    block.clone_sealed_header()
511                } else {
512                    let header = self
513                        .provider()
514                        .header_by_hash_or_number(block_hash.into())?
515                        .ok_or_else(|| ProviderError::HeaderNotFound(block_hash.into()))?;
516                    SealedHeader::new(header, block_hash)
517                };
518
519                // Check if the block has been pruned (EIP-4444)
520                let earliest_block = self.provider().earliest_block_number()?;
521                if header.number() < earliest_block {
522                    return Err(EthApiError::PrunedHistoryUnavailable {
523                        requested: header.number(),
524                        earliest_available: earliest_block,
525                    }
526                    .into());
527                }
528
529                if !filter.matches_bloom(header.logs_bloom()) {
530                    return Ok(Vec::new())
531                }
532
533                let mut all_logs = Vec::new();
534                append_matching_block_logs(
535                    &mut all_logs,
536                    self.eth_api.converter(),
537                    maybe_block
538                        .map(ProviderOrBlock::Block)
539                        .unwrap_or_else(|| ProviderOrBlock::Provider(self.provider())),
540                    &filter,
541                    &header,
542                    &receipts,
543                    false,
544                )?;
545                Ok(all_logs)
546            }
547            FilterBlockOption::Range { from_block, to_block } => {
548                // Handle special case where from block is pending
549                if from_block.is_some_and(|b| b.is_pending()) {
550                    let to_block = to_block.unwrap_or(BlockNumberOrTag::Pending);
551                    if !(to_block.is_pending() || to_block.is_number()) {
552                        // always empty range
553                        return Ok(Vec::new());
554                    }
555                    // Try to get pending block and receipts
556                    if let Ok(Some(pending_block)) = self.eth_api.local_pending_block().await {
557                        if let BlockNumberOrTag::Number(to_block) = to_block &&
558                            to_block < pending_block.block.number()
559                        {
560                            // this block range is empty based on the user input
561                            return Ok(Vec::new());
562                        }
563
564                        let info = self.provider().chain_info()?;
565                        if pending_block.block.number() > info.best_number {
566                            // only consider the pending block if it is ahead of the chain
567                            let mut all_logs = Vec::new();
568                            let header = pending_block.block.clone_sealed_header();
569                            append_matching_block_logs(
570                                &mut all_logs,
571                                self.eth_api.converter(),
572                                ProviderOrBlock::<Eth::Provider>::Block(pending_block.block),
573                                &filter,
574                                &header,
575                                &pending_block.receipts,
576                                false, // removed = false for pending blocks
577                            )?;
578                            return Ok(all_logs)
579                        }
580                    }
581                }
582
583                let info = self.provider().chain_info()?;
584                let start_block = info.best_number;
585                // Without a pending block to serve, a `pending` bound resolves to the head on both
586                // ends instead of to whatever payload the engine currently holds
587                let from = from_block
588                    .filter(|num| !num.is_pending())
589                    .map(|num| self.provider().convert_block_number(num))
590                    .transpose()?
591                    .flatten();
592                let to = to_block
593                    .filter(|num| !num.is_pending())
594                    .map(|num| self.provider().convert_block_number(num))
595                    .transpose()?
596                    .flatten();
597
598                // Return error if toBlock exceeds current head
599                if let Some(t) = to &&
600                    t > info.best_number
601                {
602                    return Err(EthFilterError::BlockRangeExceedsHead {
603                        requested: t,
604                        head: info.best_number,
605                    });
606                }
607
608                let (from_block_number, to_block_number) =
609                    logs_utils::get_filter_block_range(from, to, start_block, info)?;
610
611                // Check if the requested range overlaps with pruned history (EIP-4444)
612                let earliest_block = self.provider().earliest_block_number()?;
613                if from_block_number < earliest_block {
614                    return Err(EthApiError::PrunedHistoryUnavailable {
615                        requested: from_block_number,
616                        earliest_available: earliest_block,
617                    }
618                    .into());
619                }
620
621                self.get_logs_in_block_range(filter, from_block_number, to_block_number, limits)
622                    .await
623            }
624        }
625    }
626
627    /// Installs a new filter and returns the new identifier.
628    async fn install_filter(
629        &self,
630        kind: FilterKind<RpcTransaction<Eth::NetworkTypes>>,
631    ) -> RpcResult<FilterId> {
632        let last_poll_block_number = self.provider().best_block_number().to_rpc_result()?;
633        let subscription_id = self.id_provider.next_id();
634
635        let id = match subscription_id {
636            jsonrpsee_types::SubscriptionId::Num(n) => FilterId::Num(n),
637            jsonrpsee_types::SubscriptionId::Str(s) => FilterId::Str(s.into_owned()),
638        };
639        let mut filters = self.active_filters.inner.lock().await;
640        filters.insert(
641            id.clone(),
642            ActiveFilter {
643                block: last_poll_block_number,
644                last_poll_timestamp: Instant::now(),
645                kind,
646            },
647        );
648        Ok(id)
649    }
650
651    /// Returns all logs in the given _inclusive_ range that match the filter
652    ///
653    /// Returns an error if:
654    ///  - underlying database error
655    ///  - amount of matches exceeds configured limit
656    async fn get_logs_in_block_range(
657        self: Arc<Self>,
658        filter: Filter,
659        from_block: u64,
660        to_block: u64,
661        limits: QueryLimits,
662    ) -> Result<Vec<RpcLog<Eth::NetworkTypes>>, EthFilterError> {
663        trace!(target: "rpc::eth::filter", from=from_block, to=to_block, ?filter, "finding logs in range");
664
665        // perform boundary checks first
666        if to_block < from_block {
667            return Err(EthFilterError::InvalidBlockRangeParams)
668        }
669
670        if let Some(max_blocks_per_filter) =
671            limits.max_blocks_per_filter.filter(|limit| to_block - from_block > *limit)
672        {
673            return Err(EthFilterError::QueryExceedsMaxBlocks(max_blocks_per_filter))
674        }
675
676        // The scan occupies a blocking thread until it completes, so it shares the budget for
677        // blocking IO requests with `eth_call` and friends instead of pinning an unbounded number
678        // of pool threads.
679        let permit = self
680            .eth_api
681            .acquire_owned_blocking_io()
682            .await
683            .map_err(|_| EthFilterError::InternalError)?;
684
685        let (mut tx, rx) = oneshot::channel();
686        let this = self.clone();
687        self.task_spawner.spawn_blocking_task(async move {
688            let _permit = permit;
689            let fut = this.get_logs_in_block_range_inner(&filter, from_block, to_block, limits);
690            tokio::pin!(fut);
691            let res = tokio::select! {
692                // Range scans perform blocking reads before their first yield.
693                biased;
694                _ = tx.closed() => None,
695                res = &mut fut => Some(res),
696            };
697            if let Some(res) = res {
698                let _ = tx.send(res);
699            }
700        });
701
702        rx.await.map_err(|_| EthFilterError::InternalError)?
703    }
704
705    /// Returns all logs in the given _inclusive_ range that match the filter
706    ///
707    /// Note: This function uses a mix of blocking db operations for fetching indices and header
708    /// ranges and utilizes the rpc cache for optimistically fetching receipts and blocks.
709    /// This function is considered blocking and should thus be spawned on a blocking task.
710    ///
711    /// Returns an error if:
712    ///  - underlying database error
713    async fn get_logs_in_block_range_inner(
714        self: Arc<Self>,
715        filter: &Filter,
716        from_block: u64,
717        to_block: u64,
718        limits: QueryLimits,
719    ) -> Result<Vec<RpcLog<Eth::NetworkTypes>>, EthFilterError> {
720        let mut all_logs = Vec::new();
721
722        // get current chain tip to determine processing mode
723        let chain_tip = self.provider().best_block_number()?;
724
725        // Scan the range window by window so that receipts are fetched while headers are still
726        // being read: the log limit can end the query after the first window, memory is bounded by
727        // one window, and a cancelled query stops at the next window.
728        for (from, to) in
729            BlockRangeInclusiveIter::new(from_block..=to_block, self.max_headers_range)
730        {
731            // reading headers is blocking, this gives the cancellation check a chance to run
732            tokio::task::yield_now().await;
733
734            // collect the headers of this window that match the bloom filter
735            let mut matching_headers = Vec::new();
736            let headers = self.provider().headers_range(from..=to)?;
737
738            let mut headers_iter = headers.into_iter().peekable();
739
740            while let Some(header) = headers_iter.next() {
741                if !filter.matches_bloom(header.logs_bloom()) {
742                    continue
743                }
744
745                let current_number = header.number();
746
747                let block_hash = match headers_iter.peek() {
748                    Some(next_header) if next_header.number() == current_number + 1 => {
749                        // Headers are consecutive, use the more efficient parent_hash
750                        next_header.parent_hash()
751                    }
752                    _ => {
753                        // Headers not consecutive or last header, calculate hash
754                        header.hash_slow()
755                    }
756                };
757
758                matching_headers.push(SealedHeader::new(header, block_hash));
759            }
760
761            // initialize the appropriate range mode based on collected headers
762            let mut range_mode = RangeMode::new(
763                self.clone(),
764                matching_headers,
765                from_block,
766                to_block,
767                self.max_headers_range,
768                chain_tip,
769            );
770
771            // iterate through the range mode to get receipts and blocks
772            while let Some(ReceiptBlockResult { receipts, recovered_block, header }) =
773                range_mode.next().await?
774            {
775                let num_hash = header.num_hash();
776                append_matching_block_logs(
777                    &mut all_logs,
778                    self.eth_api.converter(),
779                    recovered_block
780                        .map(ProviderOrBlock::Block)
781                        .unwrap_or_else(|| ProviderOrBlock::Provider(self.provider())),
782                    filter,
783                    &header,
784                    &receipts,
785                    false,
786                )?;
787
788                // size check but only if range is multiple blocks, so we always return all
789                // logs of a single block
790                let is_multi_block_range = from_block != to_block;
791                if let Some(max_logs_per_response) = limits.max_logs_per_response &&
792                    is_multi_block_range &&
793                    all_logs.len() > max_logs_per_response
794                {
795                    let retry_to_block = if num_hash.number == from_block {
796                        from_block
797                    } else {
798                        num_hash.number - 1
799                    };
800
801                    debug!(
802                        target: "rpc::eth::filter",
803                        logs_found = all_logs.len(),
804                        max_logs_per_response,
805                        from_block,
806                        to_block = retry_to_block,
807                        "Query exceeded max logs per response limit"
808                    );
809                    return Err(EthFilterError::QueryExceedsMaxResults {
810                        max_logs: max_logs_per_response,
811                        from_block,
812                        to_block: retry_to_block,
813                    });
814                }
815            }
816        }
817
818        Ok(all_logs)
819    }
820}
821
822/// All active filters
823#[derive(Debug, Clone, Default)]
824pub struct ActiveFilters<T> {
825    inner: Arc<Mutex<HashMap<FilterId, ActiveFilter<T>>>>,
826}
827
828impl<T> ActiveFilters<T> {
829    /// Returns an empty instance.
830    pub fn new() -> Self {
831        Self { inner: Arc::new(Mutex::new(HashMap::default())) }
832    }
833
834    /// Returns `true` if a filter with the given id exists.
835    pub async fn contains(&self, id: &FilterId) -> bool {
836        self.inner.lock().await.contains_key(id)
837    }
838
839    /// Returns the number of currently active filters.
840    pub async fn len(&self) -> usize {
841        self.inner.lock().await.len()
842    }
843
844    /// Returns `true` if there are no active filters.
845    pub async fn is_empty(&self) -> bool {
846        self.inner.lock().await.is_empty()
847    }
848
849    /// Returns all active filter ids.
850    pub async fn ids(&self) -> Vec<FilterId> {
851        self.inner.lock().await.keys().cloned().collect()
852    }
853}
854
855/// An installed filter
856#[derive(Debug)]
857struct ActiveFilter<T> {
858    /// At which block the filter was polled last.
859    block: u64,
860    /// Last time this filter was polled.
861    last_poll_timestamp: Instant,
862    /// What kind of filter it is.
863    kind: FilterKind<T>,
864}
865
866/// A receiver for pending transactions that returns all new transactions since the last poll.
867#[derive(Debug, Clone)]
868struct PendingTransactionsReceiver {
869    txs_receiver: Arc<Mutex<Receiver<TxHash>>>,
870}
871
872impl PendingTransactionsReceiver {
873    fn new(receiver: Receiver<TxHash>) -> Self {
874        Self { txs_receiver: Arc::new(Mutex::new(receiver)) }
875    }
876
877    /// Returns all new pending transactions received since the last poll.
878    async fn drain<T>(&self) -> FilterChanges<T> {
879        let mut pending_txs = Vec::new();
880        let mut prepared_stream = self.txs_receiver.lock().await;
881
882        while let Ok(tx_hash) = prepared_stream.try_recv() {
883            pending_txs.push(tx_hash);
884        }
885
886        // Convert the vector of hashes into FilterChanges::Hashes
887        FilterChanges::Hashes(pending_txs)
888    }
889}
890
891/// A structure to manage and provide access to a stream of full transaction details.
892#[derive(Debug, Clone)]
893struct FullTransactionsReceiver<T: PoolTransaction, TxCompat> {
894    txs_stream: Arc<Mutex<NewSubpoolTransactionStream<T>>>,
895    converter: TxCompat,
896}
897
898impl<T, TxCompat> FullTransactionsReceiver<T, TxCompat>
899where
900    T: PoolTransaction + 'static,
901    TxCompat: RpcConvert<Primitives: NodePrimitives<SignedTx = T::Consensus>>,
902{
903    /// Creates a new `FullTransactionsReceiver` encapsulating the provided transaction stream.
904    fn new(stream: NewSubpoolTransactionStream<T>, converter: TxCompat) -> Self {
905        Self { txs_stream: Arc::new(Mutex::new(stream)), converter }
906    }
907
908    /// Returns all new pending transactions received since the last poll.
909    async fn drain(&self) -> FilterChanges<RpcTransaction<TxCompat::Network>> {
910        let mut pending_txs = Vec::new();
911        let mut prepared_stream = self.txs_stream.lock().await;
912
913        while let Ok(tx) = prepared_stream.try_recv() {
914            match self.converter.fill_pending(tx.transaction.to_consensus()) {
915                Ok(tx) => pending_txs.push(tx),
916                Err(err) => {
917                    error!(target: "rpc",
918                        %err,
919                        "Failed to fill txn with block context"
920                    );
921                }
922            }
923        }
924        FilterChanges::Transactions(pending_txs)
925    }
926}
927
928/// Helper trait for [`FullTransactionsReceiver`] to erase the `Transaction` type.
929#[async_trait]
930trait FullTransactionsFilter<T>: fmt::Debug + Send + Sync + Unpin + 'static {
931    async fn drain(&self) -> FilterChanges<T>;
932}
933
934#[async_trait]
935impl<T, TxCompat> FullTransactionsFilter<RpcTransaction<TxCompat::Network>>
936    for FullTransactionsReceiver<T, TxCompat>
937where
938    T: PoolTransaction + 'static,
939    TxCompat: RpcConvert<Primitives: NodePrimitives<SignedTx = T::Consensus>>,
940{
941    async fn drain(&self) -> FilterChanges<RpcTransaction<TxCompat::Network>> {
942        Self::drain(self).await
943    }
944}
945
946/// Represents the kind of pending transaction data that can be retrieved.
947///
948/// This enum differentiates between two kinds of pending transaction data:
949/// - Just the transaction hashes.
950/// - Full transaction details.
951#[derive(Debug, Clone)]
952enum PendingTransactionKind<T> {
953    Hashes(PendingTransactionsReceiver),
954    FullTransaction(Arc<dyn FullTransactionsFilter<T>>),
955}
956
957impl<T: 'static> PendingTransactionKind<T> {
958    async fn drain(&self) -> FilterChanges<T> {
959        match self {
960            Self::Hashes(receiver) => receiver.drain().await,
961            Self::FullTransaction(receiver) => receiver.drain().await,
962        }
963    }
964}
965
966#[derive(Clone, Debug)]
967enum FilterKind<T> {
968    Log(Box<Filter>),
969    Block,
970    PendingTransaction(PendingTransactionKind<T>),
971}
972
973/// An iterator that yields _inclusive_ block ranges of a given step size
974#[derive(Debug)]
975struct BlockRangeInclusiveIter {
976    iter: StepBy<RangeInclusive<u64>>,
977    step: u64,
978    end: u64,
979}
980
981impl BlockRangeInclusiveIter {
982    fn new(range: RangeInclusive<u64>, step: u64) -> Self {
983        Self { end: *range.end(), iter: range.step_by(step as usize + 1), step }
984    }
985}
986
987impl Iterator for BlockRangeInclusiveIter {
988    type Item = (u64, u64);
989
990    fn next(&mut self) -> Option<Self::Item> {
991        let start = self.iter.next()?;
992        let end = (start + self.step).min(self.end);
993        if start > end {
994            return None
995        }
996        Some((start, end))
997    }
998}
999
1000/// Errors that can occur in the handler implementation
1001#[derive(Debug, thiserror::Error)]
1002pub enum EthFilterError {
1003    /// Filter not found.
1004    #[error("filter not found")]
1005    FilterNotFound(FilterId),
1006    /// Invalid block range.
1007    #[error("invalid block range params")]
1008    InvalidBlockRangeParams,
1009    /// Block range extends beyond current head.
1010    #[error("block range extends beyond current head block: requested {requested}, head {head}")]
1011    BlockRangeExceedsHead {
1012        /// The requested `toBlock` number
1013        requested: u64,
1014        /// The current head block number
1015        head: u64,
1016    },
1017    /// Query scope is too broad.
1018    #[error("query exceeds max block range {0}")]
1019    QueryExceedsMaxBlocks(u64),
1020    /// Receipts of a block the filter matched are gone, most likely pruned.
1021    #[error("pruned history unavailable")]
1022    ReceiptsUnavailable(u64),
1023    /// Query result is too large.
1024    #[error("query exceeds max results {max_logs}, retry with the range {from_block}-{to_block}")]
1025    QueryExceedsMaxResults {
1026        /// Maximum number of logs allowed per response
1027        max_logs: usize,
1028        /// Start block of the suggested retry range
1029        from_block: u64,
1030        /// End block of the suggested retry range (last successfully processed block)
1031        to_block: u64,
1032    },
1033    /// Error serving request in `eth_` namespace.
1034    #[error(transparent)]
1035    EthAPIError(#[from] EthApiError),
1036    /// Error thrown when a spawned task failed to deliver a response.
1037    #[error("internal filter error")]
1038    InternalError,
1039}
1040
1041impl From<EthFilterError> for jsonrpsee::types::error::ErrorObject<'static> {
1042    fn from(err: EthFilterError) -> Self {
1043        match err {
1044            // geth and Nethermind answer -32000 for unknown filter ids
1045            EthFilterError::FilterNotFound(_) => rpc_error_with_code(
1046                jsonrpsee::types::error::CALL_EXECUTION_FAILED_CODE,
1047                "filter not found",
1048            ),
1049            err @ EthFilterError::InternalError => {
1050                rpc_error_with_code(jsonrpsee::types::error::INTERNAL_ERROR_CODE, err.to_string())
1051            }
1052            EthFilterError::EthAPIError(err) => err.into(),
1053            err @ EthFilterError::ReceiptsUnavailable(_) => {
1054                rpc_error_with_code(EthRpcErrorCode::PrunedHistory.code(), err.to_string())
1055            }
1056            err @ (EthFilterError::InvalidBlockRangeParams |
1057            EthFilterError::QueryExceedsMaxBlocks(_) |
1058            EthFilterError::QueryExceedsMaxResults { .. } |
1059            EthFilterError::BlockRangeExceedsHead { .. }) => {
1060                rpc_error_with_code(jsonrpsee::types::error::INVALID_PARAMS_CODE, err.to_string())
1061            }
1062        }
1063    }
1064}
1065
1066impl From<ProviderError> for EthFilterError {
1067    fn from(err: ProviderError) -> Self {
1068        Self::EthAPIError(err.into())
1069    }
1070}
1071
1072impl From<logs_utils::FilterBlockRangeError> for EthFilterError {
1073    fn from(err: logs_utils::FilterBlockRangeError) -> Self {
1074        match err {
1075            logs_utils::FilterBlockRangeError::InvalidBlockRange => Self::InvalidBlockRangeParams,
1076            logs_utils::FilterBlockRangeError::BlockRangeExceedsHead { requested, head } => {
1077                Self::BlockRangeExceedsHead { requested, head }
1078            }
1079        }
1080    }
1081}
1082
1083/// Helper type for the common pattern of returning receipts, block and the original header that is
1084/// a match for the filter.
1085struct ReceiptBlockResult<P>
1086where
1087    P: BlockReader,
1088{
1089    /// We always need the entire receipts for the matching block.
1090    receipts: Arc<Vec<ProviderReceipt<P>>>,
1091    /// Block can be optional and we can fetch it lazily when needed.
1092    recovered_block: Option<Arc<reth_primitives_traits::RecoveredBlock<ProviderBlock<P>>>>,
1093    /// The header of the block.
1094    header: SealedHeader<<P as HeaderProvider>::Header>,
1095}
1096
1097/// Represents different modes for processing block ranges when filtering logs
1098enum RangeMode<
1099    Eth: RpcNodeCoreExt<Provider: BlockIdReader, Pool: TransactionPool> + LoadReceipt + EthBlocks,
1100> {
1101    /// Use cache-based processing for recent blocks
1102    Cached(CachedMode<Eth>),
1103    /// Use range-based processing for older blocks
1104    Range(RangeBlockMode<Eth>),
1105}
1106
1107impl<
1108        Eth: RpcNodeCoreExt<Provider: BlockIdReader, Pool: TransactionPool> + LoadReceipt + EthBlocks,
1109    > RangeMode<Eth>
1110{
1111    /// Creates a new `RangeMode`.
1112    fn new(
1113        filter_inner: Arc<EthFilterInner<Eth>>,
1114        sealed_headers: Vec<SealedHeader<<Eth::Provider as HeaderProvider>::Header>>,
1115        from_block: u64,
1116        to_block: u64,
1117        max_headers_range: u64,
1118        chain_tip: u64,
1119    ) -> Self {
1120        let block_count = to_block - from_block + 1;
1121        let distance_from_tip = chain_tip.saturating_sub(to_block);
1122
1123        // Determine if we should use cached mode based on range characteristics
1124        let use_cached_mode =
1125            Self::should_use_cached_mode(&sealed_headers, block_count, distance_from_tip);
1126
1127        // Fetching receipts in parallel is only worth the extra tasks when most of the window has
1128        // them to read; the sequential path serves the rest from the receipt cache where it can
1129        let parallel = sealed_headers.len() >= PARALLEL_PROCESSING_THRESHOLD;
1130
1131        if use_cached_mode && !sealed_headers.is_empty() {
1132            Self::Cached(CachedMode { filter_inner, headers_iter: sealed_headers.into_iter() })
1133        } else {
1134            Self::Range(RangeBlockMode {
1135                filter_inner,
1136                iter: sealed_headers.into_iter().peekable(),
1137                next: VecDeque::new(),
1138                max_range: (max_headers_range as usize).min(MAX_PARALLEL_BATCH_SIZE),
1139                parallel,
1140                pending_tasks: FuturesOrdered::new(),
1141            })
1142        }
1143    }
1144
1145    /// Determines whether to use cached mode based on bloom filter matches and range size
1146    const fn should_use_cached_mode(
1147        headers: &[SealedHeader<<Eth::Provider as HeaderProvider>::Header>],
1148        block_count: u64,
1149        distance_from_tip: u64,
1150    ) -> bool {
1151        // Headers are already filtered by bloom, so count equals length
1152        let bloom_matches = headers.len();
1153
1154        // Calculate adjusted threshold based on bloom matches
1155        let adjusted_threshold = Self::calculate_adjusted_threshold(block_count, bloom_matches);
1156
1157        block_count <= adjusted_threshold && distance_from_tip <= adjusted_threshold
1158    }
1159
1160    /// Calculates the adjusted cache threshold based on bloom filter matches
1161    const fn calculate_adjusted_threshold(block_count: u64, bloom_matches: usize) -> u64 {
1162        // Only apply adjustments for larger ranges
1163        if block_count <= BLOOM_ADJUSTMENT_MIN_BLOCKS {
1164            return CACHED_MODE_BLOCK_THRESHOLD;
1165        }
1166
1167        match bloom_matches {
1168            n if n > HIGH_BLOOM_MATCH_THRESHOLD => CACHED_MODE_BLOCK_THRESHOLD / 2,
1169            n if n > MODERATE_BLOOM_MATCH_THRESHOLD => (CACHED_MODE_BLOCK_THRESHOLD * 3) / 4,
1170            _ => CACHED_MODE_BLOCK_THRESHOLD,
1171        }
1172    }
1173
1174    /// Gets the next (receipts, `maybe_block`, header, `block_hash`) tuple.
1175    async fn next(&mut self) -> Result<Option<ReceiptBlockResult<Eth::Provider>>, EthFilterError> {
1176        match self {
1177            Self::Cached(cached) => cached.next().await,
1178            Self::Range(range) => range.next().await,
1179        }
1180    }
1181}
1182
1183/// Mode for processing blocks using cache optimization for recent blocks
1184struct CachedMode<
1185    Eth: RpcNodeCoreExt<Provider: BlockIdReader, Pool: TransactionPool> + LoadReceipt + EthBlocks,
1186> {
1187    filter_inner: Arc<EthFilterInner<Eth>>,
1188    headers_iter: std::vec::IntoIter<SealedHeader<<Eth::Provider as HeaderProvider>::Header>>,
1189}
1190
1191impl<
1192        Eth: RpcNodeCoreExt<Provider: BlockIdReader, Pool: TransactionPool> + LoadReceipt + EthBlocks,
1193    > CachedMode<Eth>
1194{
1195    async fn next(&mut self) -> Result<Option<ReceiptBlockResult<Eth::Provider>>, EthFilterError> {
1196        let Some(header) = self.headers_iter.next() else { return Ok(None) };
1197
1198        // Use get_receipts_and_maybe_block which has automatic fallback to provider
1199        let Some((receipts, maybe_block)) =
1200            self.filter_inner.eth_cache().get_receipts_and_maybe_block(header.hash()).await?
1201        else {
1202            return Err(EthFilterError::ReceiptsUnavailable(header.number()))
1203        };
1204
1205        Ok(Some(ReceiptBlockResult { receipts, recovered_block: maybe_block, header }))
1206    }
1207}
1208
1209/// Type alias for parallel receipt fetching task futures used in `RangeBlockMode`
1210type ReceiptFetchFuture<P> =
1211    Pin<Box<dyn Future<Output = Result<Vec<ReceiptBlockResult<P>>, EthFilterError>> + Send>>;
1212
1213/// Mode for processing blocks using range queries for older blocks
1214struct RangeBlockMode<
1215    Eth: RpcNodeCoreExt<Provider: BlockIdReader, Pool: TransactionPool> + LoadReceipt + EthBlocks,
1216> {
1217    filter_inner: Arc<EthFilterInner<Eth>>,
1218    iter: Peekable<std::vec::IntoIter<SealedHeader<<Eth::Provider as HeaderProvider>::Header>>>,
1219    next: VecDeque<ReceiptBlockResult<Eth::Provider>>,
1220    /// Maximum number of consecutive blocks fetched by one batch of parallel tasks
1221    max_range: usize,
1222    /// Whether receipts are fetched in parallel batches of consecutive blocks
1223    parallel: bool,
1224    // Stream of ongoing receipt fetching tasks
1225    pending_tasks: FuturesOrdered<ReceiptFetchFuture<Eth::Provider>>,
1226}
1227
1228impl<
1229        Eth: RpcNodeCoreExt<Provider: BlockIdReader, Pool: TransactionPool> + LoadReceipt + EthBlocks,
1230    > RangeBlockMode<Eth>
1231{
1232    async fn next(&mut self) -> Result<Option<ReceiptBlockResult<Eth::Provider>>, EthFilterError> {
1233        loop {
1234            // First, try to return any already processed result from buffer
1235            if let Some(result) = self.next.pop_front() {
1236                return Ok(Some(result));
1237            }
1238
1239            // Try to get a completed task result if there are pending tasks
1240            if let Some(task_result) = self.pending_tasks.next().await {
1241                self.next.extend(task_result?);
1242                continue;
1243            }
1244
1245            // No pending tasks - try to generate more work
1246            let Some(next_header) = self.iter.next() else {
1247                // No more headers to process
1248                return Ok(None);
1249            };
1250
1251            if !self.parallel {
1252                // Process the block on its own so that only one block's receipts are held
1253                if let Some(result) = self.process_small_range(vec![next_header]).await? {
1254                    return Ok(Some(result));
1255                }
1256                // Continue loop to check for more work
1257                continue;
1258            }
1259
1260            let mut range_headers = Vec::with_capacity(self.max_range.min(self.iter.len() + 1));
1261            range_headers.push(next_header);
1262
1263            // Collect consecutive blocks up to max_range size
1264            while range_headers.len() < self.max_range {
1265                let Some(peeked) = self.iter.peek() else { break };
1266                let Some(last_header) = range_headers.last() else { break };
1267
1268                let expected_next = last_header.number() + 1;
1269                if peeked.number() != expected_next {
1270                    trace!(
1271                        target: "rpc::eth::filter",
1272                        last_block = last_header.number(),
1273                        next_block = peeked.number(),
1274                        expected = expected_next,
1275                        range_size = range_headers.len(),
1276                        "Non-consecutive block detected, stopping range collection"
1277                    );
1278                    break; // Non-consecutive block, stop here
1279                }
1280
1281                let Some(next_header) = self.iter.next() else { break };
1282                range_headers.push(next_header);
1283            }
1284
1285            self.spawn_parallel_tasks(range_headers);
1286            // Continue loop to await the spawned tasks
1287        }
1288    }
1289
1290    /// Process a small range of headers sequentially
1291    ///
1292    /// This is used for ranges below [`PARALLEL_PROCESSING_THRESHOLD`], one block at a time.
1293    async fn process_small_range(
1294        &mut self,
1295        range_headers: Vec<SealedHeader<<Eth::Provider as HeaderProvider>::Header>>,
1296    ) -> Result<Option<ReceiptBlockResult<Eth::Provider>>, EthFilterError> {
1297        // Process each header individually to avoid queuing for all receipts
1298        for header in range_headers {
1299            // First check if already cached to avoid unnecessary provider calls
1300            let (maybe_block, maybe_receipts) = self
1301                .filter_inner
1302                .eth_cache()
1303                .maybe_cached_block_and_receipts(header.hash())
1304                .await?;
1305
1306            let receipts = match maybe_receipts {
1307                Some(receipts) => receipts,
1308                None => {
1309                    // Not cached - fetch directly from provider
1310                    match self.filter_inner.provider().receipts_by_block(header.hash().into())? {
1311                        Some(receipts) => Arc::new(receipts),
1312                        None => return Err(EthFilterError::ReceiptsUnavailable(header.number())),
1313                    }
1314                }
1315            };
1316
1317            if !receipts.is_empty() {
1318                self.next.push_back(ReceiptBlockResult {
1319                    receipts,
1320                    recovered_block: maybe_block,
1321                    header,
1322                });
1323            }
1324        }
1325
1326        Ok(self.next.pop_front())
1327    }
1328
1329    /// Spawn parallel tasks for processing a large range of headers
1330    ///
1331    /// This is used for ranges of at least [`PARALLEL_PROCESSING_THRESHOLD`] blocks.
1332    fn spawn_parallel_tasks(
1333        &mut self,
1334        range_headers: Vec<SealedHeader<<Eth::Provider as HeaderProvider>::Header>>,
1335    ) {
1336        // Split headers into chunks
1337        let chunk_size = range_headers.len().div_ceil(DEFAULT_PARALLEL_CONCURRENCY).max(1);
1338        let header_chunks = range_headers
1339            .into_iter()
1340            .chunks(chunk_size)
1341            .into_iter()
1342            .map(|chunk| chunk.collect::<Vec<_>>())
1343            .collect::<Vec<_>>();
1344
1345        // Spawn each chunk as a separate task directly into the FuturesOrdered stream
1346        for chunk_headers in header_chunks {
1347            let filter_inner = self.filter_inner.clone();
1348            let fetch = move || Self::fetch_chunk_receipts(&filter_inner, chunk_headers);
1349
1350            // A parallel task occupies an additional blocking thread, so it needs its own share
1351            // of the blocking IO budget. Without one the chunk is fetched on this task instead.
1352            let chunk_task: ReceiptFetchFuture<Eth::Provider> = match self
1353                .filter_inner
1354                .eth_api
1355                .blocking_io_task_guard()
1356                .clone()
1357                .try_acquire_owned()
1358            {
1359                Ok(permit) => Box::pin(async move {
1360                    let chunk_task = reth_tasks::spawn_blocking(move || {
1361                        let _permit = permit;
1362                        fetch()
1363                    });
1364
1365                    // Await the blocking task and handle the result
1366                    match chunk_task.await {
1367                        Ok(chunk_results) => chunk_results,
1368                        Err(join_err) => {
1369                            trace!(target: "rpc::eth::filter", error = ?join_err, "Task join error");
1370                            Err(EthFilterError::InternalError)
1371                        }
1372                    }
1373                }),
1374                Err(_) => Box::pin(async move { fetch() }),
1375            };
1376
1377            self.pending_tasks.push_back(chunk_task);
1378        }
1379    }
1380
1381    /// Fetches the receipts of the given blocks from the provider.
1382    fn fetch_chunk_receipts(
1383        filter_inner: &EthFilterInner<Eth>,
1384        chunk_headers: Vec<SealedHeader<<Eth::Provider as HeaderProvider>::Header>>,
1385    ) -> Result<Vec<ReceiptBlockResult<Eth::Provider>>, EthFilterError> {
1386        let mut chunk_results = Vec::with_capacity(chunk_headers.len());
1387
1388        for header in chunk_headers {
1389            // Fetch directly from provider - RangeMode is used for older blocks
1390            // unlikely to be cached
1391            let receipts = match filter_inner.provider().receipts_by_block(header.hash().into())? {
1392                Some(receipts) => Arc::new(receipts),
1393                None => return Err(EthFilterError::ReceiptsUnavailable(header.number())),
1394            };
1395
1396            if !receipts.is_empty() {
1397                chunk_results.push(ReceiptBlockResult { receipts, recovered_block: None, header });
1398            }
1399        }
1400
1401        Ok(chunk_results)
1402    }
1403}
1404
1405#[cfg(test)]
1406mod tests {
1407    use super::*;
1408    use crate::{eth::EthApi, EthApiBuilder};
1409    use alloy_consensus::TxLegacy;
1410    use alloy_network::Ethereum;
1411    use alloy_primitives::{Bloom, FixedBytes};
1412    use rand::Rng;
1413    use reth_chainspec::{ChainSpec, ChainSpecProvider};
1414    use reth_db_api::models::StoredBlockBodyIndices;
1415    use reth_ethereum_primitives::TxType;
1416    use reth_evm_ethereum::EthEvmConfig;
1417    use reth_network_api::noop::NoopNetwork;
1418    use reth_provider::test_utils::MockEthProvider;
1419    use reth_rpc_convert::RpcConverter;
1420    use reth_rpc_eth_api::{helpers::SpawnBlocking, node::RpcNodeCoreAdapter};
1421    use reth_rpc_eth_types::receipt::EthReceiptConverter;
1422    use reth_tasks::Runtime;
1423    use reth_testing_utils::generators;
1424    use reth_transaction_pool::test_utils::{testing_pool, TestPool};
1425    use std::{collections::VecDeque, sync::Arc};
1426
1427    #[test]
1428    fn receipts_unavailable_error_matches_geth() {
1429        let err: jsonrpsee::types::error::ErrorObject<'static> =
1430            EthFilterError::ReceiptsUnavailable(100).into();
1431        assert_eq!(err.code(), 4444);
1432        assert_eq!(err.message(), "pruned history unavailable");
1433    }
1434
1435    #[test]
1436    fn test_block_range_iter() {
1437        let mut rng = generators::rng();
1438
1439        let start = rng.random::<u32>() as u64;
1440        let end = start.saturating_add(rng.random::<u32>() as u64);
1441        let step = rng.random::<u16>() as u64;
1442        let range = start..=end;
1443        let mut iter = BlockRangeInclusiveIter::new(range.clone(), step);
1444        let (from, mut end) = iter.next().unwrap();
1445        assert_eq!(from, start);
1446        assert_eq!(end, (from + step).min(*range.end()));
1447
1448        for (next_from, next_end) in iter {
1449            // ensure range starts with previous end + 1
1450            assert_eq!(next_from, end + 1);
1451            end = next_end;
1452        }
1453
1454        assert_eq!(end, *range.end());
1455    }
1456
1457    // Helper function to create a test EthApi instance
1458    #[expect(clippy::type_complexity)]
1459    fn build_test_eth_api(
1460        provider: MockEthProvider,
1461    ) -> EthApi<
1462        RpcNodeCoreAdapter<MockEthProvider, TestPool, NoopNetwork, EthEvmConfig>,
1463        RpcConverter<Ethereum, EthEvmConfig, EthReceiptConverter<ChainSpec>>,
1464    > {
1465        EthApiBuilder::new(
1466            provider.clone(),
1467            testing_pool(),
1468            NoopNetwork::default(),
1469            EthEvmConfig::new(provider.chain_spec()),
1470        )
1471        .build()
1472    }
1473
1474    #[tokio::test]
1475    async fn test_logs_for_filter_from_block_beyond_head() {
1476        let provider = MockEthProvider::default();
1477        provider.add_header(FixedBytes::random(), alloy_consensus::Header::default());
1478        let eth_api = build_test_eth_api(provider);
1479
1480        let eth_filter =
1481            super::EthFilter::new(eth_api, EthFilterConfig::default(), Runtime::test());
1482
1483        let filter = Filter::new().from_block(100u64).to_block(BlockNumberOrTag::Latest);
1484        let result = eth_filter.inner.clone().logs_for_filter(filter, QueryLimits::default()).await;
1485        assert!(matches!(result, Err(EthFilterError::InvalidBlockRangeParams)), "{result:?}");
1486    }
1487
1488    #[tokio::test]
1489    async fn test_range_block_mode_empty_range() {
1490        let provider = MockEthProvider::default();
1491        let eth_api = build_test_eth_api(provider);
1492
1493        let eth_filter =
1494            super::EthFilter::new(eth_api, EthFilterConfig::default(), Runtime::test());
1495        let filter_inner = eth_filter.inner;
1496
1497        let headers = vec![];
1498        let max_range = 100;
1499
1500        let mut range_mode = RangeBlockMode {
1501            filter_inner,
1502            iter: headers.into_iter().peekable(),
1503            next: VecDeque::new(),
1504            max_range,
1505            parallel: false,
1506
1507            pending_tasks: FuturesOrdered::new(),
1508        };
1509
1510        let result = range_mode.next().await;
1511        assert!(result.is_ok());
1512        assert!(result.unwrap().is_none());
1513    }
1514
1515    #[tokio::test]
1516    async fn test_range_block_mode_queued_results_priority() {
1517        let provider = MockEthProvider::default();
1518
1519        let headers = vec![
1520            SealedHeader::new(
1521                alloy_consensus::Header { number: 100, ..Default::default() },
1522                FixedBytes::random(),
1523            ),
1524            SealedHeader::new(
1525                alloy_consensus::Header { number: 101, ..Default::default() },
1526                FixedBytes::random(),
1527            ),
1528        ];
1529        for header in &headers {
1530            provider.add_header(header.hash(), header.header().clone());
1531            provider.add_receipts(header.number(), vec![]);
1532        }
1533
1534        let eth_api = build_test_eth_api(provider);
1535
1536        let eth_filter =
1537            super::EthFilter::new(eth_api, EthFilterConfig::default(), Runtime::test());
1538        let filter_inner = eth_filter.inner;
1539
1540        // create specific mock results to test ordering
1541        let expected_block_hash_1 = FixedBytes::from([1u8; 32]);
1542        let expected_block_hash_2 = FixedBytes::from([2u8; 32]);
1543
1544        // create mock receipts to test receipt handling
1545        let mock_receipt_1 = reth_ethereum_primitives::Receipt {
1546            tx_type: TxType::Legacy,
1547            cumulative_gas_used: 100_000,
1548            logs: vec![],
1549            success: true,
1550        };
1551        let mock_receipt_2 = reth_ethereum_primitives::Receipt {
1552            tx_type: TxType::Eip1559,
1553            cumulative_gas_used: 200_000,
1554            logs: vec![],
1555            success: true,
1556        };
1557        let mock_receipt_3 = reth_ethereum_primitives::Receipt {
1558            tx_type: TxType::Eip2930,
1559            cumulative_gas_used: 150_000,
1560            logs: vec![],
1561            success: false, // Different success status
1562        };
1563
1564        let mock_result_1 = ReceiptBlockResult {
1565            receipts: Arc::new(vec![mock_receipt_1.clone(), mock_receipt_2.clone()]),
1566            recovered_block: None,
1567            header: SealedHeader::new(
1568                alloy_consensus::Header { number: 42, ..Default::default() },
1569                expected_block_hash_1,
1570            ),
1571        };
1572
1573        let mock_result_2 = ReceiptBlockResult {
1574            receipts: Arc::new(vec![mock_receipt_3.clone()]),
1575            recovered_block: None,
1576            header: SealedHeader::new(
1577                alloy_consensus::Header { number: 43, ..Default::default() },
1578                expected_block_hash_2,
1579            ),
1580        };
1581
1582        let mut range_mode = RangeBlockMode {
1583            filter_inner,
1584            iter: headers.into_iter().peekable(),
1585            next: VecDeque::from([mock_result_1, mock_result_2]), // Queue two results
1586            max_range: 100,
1587            parallel: false,
1588
1589            pending_tasks: FuturesOrdered::new(),
1590        };
1591
1592        // first call should return the first queued result (FIFO order)
1593        let result1 = range_mode.next().await;
1594        assert!(result1.is_ok());
1595        let receipt_result1 = result1.unwrap().unwrap();
1596        assert_eq!(receipt_result1.header.hash(), expected_block_hash_1);
1597        assert_eq!(receipt_result1.header.number, 42);
1598
1599        // verify receipts
1600        assert_eq!(receipt_result1.receipts.len(), 2);
1601        assert_eq!(receipt_result1.receipts[0].tx_type, mock_receipt_1.tx_type);
1602        assert_eq!(
1603            receipt_result1.receipts[0].cumulative_gas_used,
1604            mock_receipt_1.cumulative_gas_used
1605        );
1606        assert_eq!(receipt_result1.receipts[0].success, mock_receipt_1.success);
1607        assert_eq!(receipt_result1.receipts[1].tx_type, mock_receipt_2.tx_type);
1608        assert_eq!(
1609            receipt_result1.receipts[1].cumulative_gas_used,
1610            mock_receipt_2.cumulative_gas_used
1611        );
1612        assert_eq!(receipt_result1.receipts[1].success, mock_receipt_2.success);
1613
1614        // second call should return the second queued result
1615        let result2 = range_mode.next().await;
1616        assert!(result2.is_ok());
1617        let receipt_result2 = result2.unwrap().unwrap();
1618        assert_eq!(receipt_result2.header.hash(), expected_block_hash_2);
1619        assert_eq!(receipt_result2.header.number, 43);
1620
1621        // verify receipts
1622        assert_eq!(receipt_result2.receipts.len(), 1);
1623        assert_eq!(receipt_result2.receipts[0].tx_type, mock_receipt_3.tx_type);
1624        assert_eq!(
1625            receipt_result2.receipts[0].cumulative_gas_used,
1626            mock_receipt_3.cumulative_gas_used
1627        );
1628        assert_eq!(receipt_result2.receipts[0].success, mock_receipt_3.success);
1629
1630        // queue should now be empty
1631        assert!(range_mode.next.is_empty());
1632
1633        let result3 = range_mode.next().await;
1634        assert!(result3.is_ok());
1635    }
1636
1637    #[tokio::test]
1638    async fn test_range_block_mode_single_block_missing_receipts() {
1639        let provider = MockEthProvider::default();
1640        let eth_api = build_test_eth_api(provider);
1641
1642        let eth_filter =
1643            super::EthFilter::new(eth_api, EthFilterConfig::default(), Runtime::test());
1644        let filter_inner = eth_filter.inner;
1645
1646        let headers = vec![SealedHeader::new(
1647            alloy_consensus::Header { number: 100, ..Default::default() },
1648            FixedBytes::random(),
1649        )];
1650
1651        let mut range_mode = RangeBlockMode {
1652            filter_inner,
1653            iter: headers.into_iter().peekable(),
1654            next: VecDeque::new(),
1655            max_range: 100,
1656            parallel: false,
1657
1658            pending_tasks: FuturesOrdered::new(),
1659        };
1660
1661        // a block whose header matched the filter but whose receipts are gone must not be
1662        // silently skipped
1663        let Err(err) = range_mode.next().await else { panic!("missing receipts must be an error") };
1664        assert!(matches!(err, EthFilterError::ReceiptsUnavailable(100)), "{err:?}");
1665    }
1666
1667    #[tokio::test]
1668    async fn test_range_block_mode_provider_receipts() {
1669        let provider = MockEthProvider::default();
1670
1671        let header_1 = alloy_consensus::Header { number: 100, ..Default::default() };
1672        let header_2 = alloy_consensus::Header { number: 101, ..Default::default() };
1673        let header_3 = alloy_consensus::Header { number: 102, ..Default::default() };
1674
1675        let block_hash_1 = FixedBytes::random();
1676        let block_hash_2 = FixedBytes::random();
1677        let block_hash_3 = FixedBytes::random();
1678
1679        provider.add_header(block_hash_1, header_1.clone());
1680        provider.add_header(block_hash_2, header_2.clone());
1681        provider.add_header(block_hash_3, header_3.clone());
1682
1683        // create mock receipts to test provider fetching with mock logs
1684        let mock_log = alloy_primitives::Log {
1685            address: alloy_primitives::Address::ZERO,
1686            data: alloy_primitives::LogData::new_unchecked(vec![], alloy_primitives::Bytes::new()),
1687        };
1688
1689        let receipt_100_1 = reth_ethereum_primitives::Receipt {
1690            tx_type: TxType::Legacy,
1691            cumulative_gas_used: 21_000,
1692            logs: vec![mock_log.clone()],
1693            success: true,
1694        };
1695        let receipt_100_2 = reth_ethereum_primitives::Receipt {
1696            tx_type: TxType::Eip1559,
1697            cumulative_gas_used: 42_000,
1698            logs: vec![mock_log.clone()],
1699            success: true,
1700        };
1701        let receipt_101_1 = reth_ethereum_primitives::Receipt {
1702            tx_type: TxType::Eip2930,
1703            cumulative_gas_used: 30_000,
1704            logs: vec![mock_log.clone()],
1705            success: false,
1706        };
1707
1708        provider.add_receipts(100, vec![receipt_100_1.clone(), receipt_100_2.clone()]);
1709        provider.add_receipts(101, vec![receipt_101_1.clone()]);
1710        // a block without transactions, which a provider reports as an empty list
1711        provider.add_receipts(102, vec![]);
1712
1713        let eth_api = build_test_eth_api(provider);
1714
1715        let eth_filter =
1716            super::EthFilter::new(eth_api, EthFilterConfig::default(), Runtime::test());
1717        let filter_inner = eth_filter.inner;
1718
1719        let headers = vec![
1720            SealedHeader::new(header_1, block_hash_1),
1721            SealedHeader::new(header_2, block_hash_2),
1722            SealedHeader::new(header_3, block_hash_3),
1723        ];
1724
1725        let mut range_mode = RangeBlockMode {
1726            filter_inner,
1727            iter: headers.into_iter().peekable(),
1728            next: VecDeque::new(),
1729            max_range: 3, // include the 3 blocks in the first queried results
1730            parallel: false,
1731
1732            pending_tasks: FuturesOrdered::new(),
1733        };
1734
1735        // first call should fetch receipts from provider and return first block with receipts
1736        let result = range_mode.next().await;
1737        assert!(result.is_ok());
1738        let receipt_result = result.unwrap().unwrap();
1739
1740        assert_eq!(receipt_result.header.hash(), block_hash_1);
1741        assert_eq!(receipt_result.header.number, 100);
1742        assert_eq!(receipt_result.receipts.len(), 2);
1743
1744        // verify receipts
1745        assert_eq!(receipt_result.receipts[0].tx_type, receipt_100_1.tx_type);
1746        assert_eq!(
1747            receipt_result.receipts[0].cumulative_gas_used,
1748            receipt_100_1.cumulative_gas_used
1749        );
1750        assert_eq!(receipt_result.receipts[0].success, receipt_100_1.success);
1751
1752        assert_eq!(receipt_result.receipts[1].tx_type, receipt_100_2.tx_type);
1753        assert_eq!(
1754            receipt_result.receipts[1].cumulative_gas_used,
1755            receipt_100_2.cumulative_gas_used
1756        );
1757        assert_eq!(receipt_result.receipts[1].success, receipt_100_2.success);
1758
1759        // second call should return the second block with receipts
1760        let result2 = range_mode.next().await;
1761        assert!(result2.is_ok());
1762        let receipt_result2 = result2.unwrap().unwrap();
1763
1764        assert_eq!(receipt_result2.header.hash(), block_hash_2);
1765        assert_eq!(receipt_result2.header.number, 101);
1766        assert_eq!(receipt_result2.receipts.len(), 1);
1767
1768        // verify receipts
1769        assert_eq!(receipt_result2.receipts[0].tx_type, receipt_101_1.tx_type);
1770        assert_eq!(
1771            receipt_result2.receipts[0].cumulative_gas_used,
1772            receipt_101_1.cumulative_gas_used
1773        );
1774        assert_eq!(receipt_result2.receipts[0].success, receipt_101_1.success);
1775
1776        // third call should return None since no more blocks with receipts
1777        let result3 = range_mode.next().await;
1778        assert!(result3.is_ok());
1779        assert!(result3.unwrap().is_none());
1780    }
1781
1782    #[tokio::test]
1783    async fn test_range_block_mode_iterator_exhaustion() {
1784        let provider = MockEthProvider::default();
1785
1786        let header_100 = alloy_consensus::Header { number: 100, ..Default::default() };
1787        let header_101 = alloy_consensus::Header { number: 101, ..Default::default() };
1788
1789        let block_hash_100 = FixedBytes::random();
1790        let block_hash_101 = FixedBytes::random();
1791
1792        // Associate headers with hashes first
1793        provider.add_header(block_hash_100, header_100.clone());
1794        provider.add_header(block_hash_101, header_101.clone());
1795
1796        // Add mock receipts so headers are actually processed
1797        let mock_receipt = reth_ethereum_primitives::Receipt {
1798            tx_type: TxType::Legacy,
1799            cumulative_gas_used: 21_000,
1800            logs: vec![],
1801            success: true,
1802        };
1803        provider.add_receipts(100, vec![mock_receipt.clone()]);
1804        provider.add_receipts(101, vec![mock_receipt.clone()]);
1805
1806        let eth_api = build_test_eth_api(provider);
1807
1808        let eth_filter =
1809            super::EthFilter::new(eth_api, EthFilterConfig::default(), Runtime::test());
1810        let filter_inner = eth_filter.inner;
1811
1812        let headers = vec![
1813            SealedHeader::new(header_100, block_hash_100),
1814            SealedHeader::new(header_101, block_hash_101),
1815        ];
1816
1817        let mut range_mode = RangeBlockMode {
1818            filter_inner,
1819            iter: headers.into_iter().peekable(),
1820            next: VecDeque::new(),
1821            max_range: 1,
1822            parallel: false,
1823
1824            pending_tasks: FuturesOrdered::new(),
1825        };
1826
1827        let result1 = range_mode.next().await;
1828        assert!(result1.is_ok());
1829        assert!(result1.unwrap().is_some()); // Should have processed block 100
1830
1831        assert!(range_mode.iter.peek().is_some()); // Should still have block 101
1832
1833        let result2 = range_mode.next().await;
1834        assert!(result2.is_ok());
1835        assert!(result2.unwrap().is_some()); // Should have processed block 101
1836
1837        // now iterator should be exhausted
1838        assert!(range_mode.iter.peek().is_none());
1839
1840        // further calls should return None
1841        let result3 = range_mode.next().await;
1842        assert!(result3.is_ok());
1843        assert!(result3.unwrap().is_none());
1844    }
1845
1846    #[tokio::test]
1847    async fn test_cached_mode_with_mock_receipts() {
1848        // create test data
1849        let test_hash = FixedBytes::from([42u8; 32]);
1850        let test_block_number = 100u64;
1851        let test_header = SealedHeader::new(
1852            alloy_consensus::Header {
1853                number: test_block_number,
1854                gas_used: 50_000,
1855                ..Default::default()
1856            },
1857            test_hash,
1858        );
1859
1860        // add a mock receipt to the provider with a mock log
1861        let mock_log = alloy_primitives::Log {
1862            address: alloy_primitives::Address::ZERO,
1863            data: alloy_primitives::LogData::new_unchecked(vec![], alloy_primitives::Bytes::new()),
1864        };
1865
1866        let mock_receipt = reth_ethereum_primitives::Receipt {
1867            tx_type: TxType::Legacy,
1868            cumulative_gas_used: 21_000,
1869            logs: vec![mock_log],
1870            success: true,
1871        };
1872
1873        let provider = MockEthProvider::default();
1874        provider.add_header(test_hash, test_header.header().clone());
1875        provider.add_receipts(test_block_number, vec![mock_receipt.clone()]);
1876
1877        let eth_api = build_test_eth_api(provider);
1878        let eth_filter =
1879            super::EthFilter::new(eth_api, EthFilterConfig::default(), Runtime::test());
1880        let filter_inner = eth_filter.inner;
1881
1882        let headers = vec![test_header.clone()];
1883
1884        let mut cached_mode = CachedMode { filter_inner, headers_iter: headers.into_iter() };
1885
1886        // should find the receipt from provider fallback (cache will be empty)
1887        let result = cached_mode.next().await.expect("next should succeed");
1888        let receipt_block_result = result.expect("should have receipt result");
1889        assert_eq!(receipt_block_result.header.hash(), test_hash);
1890        assert_eq!(receipt_block_result.header.number, test_block_number);
1891        assert_eq!(receipt_block_result.receipts.len(), 1);
1892        assert_eq!(receipt_block_result.receipts[0].tx_type, mock_receipt.tx_type);
1893        assert_eq!(
1894            receipt_block_result.receipts[0].cumulative_gas_used,
1895            mock_receipt.cumulative_gas_used
1896        );
1897        assert_eq!(receipt_block_result.receipts[0].success, mock_receipt.success);
1898
1899        // iterator should be exhausted
1900        let result2 = cached_mode.next().await;
1901        assert!(result2.is_ok());
1902        assert!(result2.unwrap().is_none());
1903    }
1904
1905    #[tokio::test]
1906    async fn test_cached_mode_empty_headers() {
1907        let provider = MockEthProvider::default();
1908        let eth_api = build_test_eth_api(provider);
1909
1910        let eth_filter =
1911            super::EthFilter::new(eth_api, EthFilterConfig::default(), Runtime::test());
1912        let filter_inner = eth_filter.inner;
1913
1914        let headers: Vec<SealedHeader<alloy_consensus::Header>> = vec![];
1915
1916        let mut cached_mode = CachedMode { filter_inner, headers_iter: headers.into_iter() };
1917
1918        // should immediately return None for empty headers
1919        let result = cached_mode.next().await.expect("next should succeed");
1920        assert!(result.is_none());
1921    }
1922
1923    #[tokio::test]
1924    async fn test_log_limit_retry_range_excludes_overflow_block() {
1925        let provider = MockEthProvider::default();
1926
1927        use reth_ethereum_primitives::{TransactionSigned, TxType};
1928
1929        let tx_inner = TxLegacy {
1930            chain_id: Some(1),
1931            nonce: 0,
1932            gas_price: 21_000,
1933            gas_limit: 21_000,
1934            to: alloy_primitives::TxKind::Call(alloy_primitives::Address::ZERO),
1935            value: alloy_primitives::U256::ZERO,
1936            input: alloy_primitives::Bytes::new(),
1937        };
1938        let signature = alloy_primitives::Signature::test_signature();
1939        let tx = TransactionSigned::new_unhashed(tx_inner.into(), signature);
1940
1941        let mock_log = alloy_primitives::Log {
1942            address: alloy_primitives::Address::ZERO,
1943            data: alloy_primitives::LogData::new_unchecked(vec![], alloy_primitives::Bytes::new()),
1944        };
1945
1946        let receipt = reth_ethereum_primitives::Receipt {
1947            tx_type: TxType::Legacy,
1948            cumulative_gas_used: 21_000,
1949            logs: vec![mock_log],
1950            success: true,
1951        };
1952
1953        let mut prev_hash = alloy_primitives::B256::default();
1954        for (idx, block_number) in (100u64..=102).enumerate() {
1955            let header = alloy_consensus::Header {
1956                number: block_number,
1957                parent_hash: prev_hash,
1958                logs_bloom: Bloom::repeat_byte(1u8),
1959                ..Default::default()
1960            };
1961            let hash = header.hash_slow();
1962            prev_hash = hash;
1963
1964            let block = reth_ethereum_primitives::Block {
1965                header,
1966                body: reth_ethereum_primitives::BlockBody {
1967                    transactions: vec![tx.clone()],
1968                    ..Default::default()
1969                },
1970            };
1971            provider.add_block(hash, block);
1972            provider.add_receipts(block_number, vec![receipt.clone()]);
1973            provider.add_block_body_indices(
1974                block_number,
1975                StoredBlockBodyIndices { first_tx_num: idx as u64, tx_count: 1 },
1976            );
1977        }
1978
1979        let eth_api = build_test_eth_api(provider);
1980        let eth_filter = EthFilter::new(eth_api, EthFilterConfig::default(), Runtime::test());
1981        let err = eth_filter
1982            .inner
1983            .clone()
1984            .get_logs_in_block_range(
1985                Filter::default(),
1986                100,
1987                102,
1988                QueryLimits { max_blocks_per_filter: None, max_logs_per_response: Some(2) },
1989            )
1990            .await
1991            .expect_err("range should exceed max logs");
1992
1993        let EthFilterError::QueryExceedsMaxResults { max_logs, from_block, to_block } = err else {
1994            panic!("unexpected error: {err:?}");
1995        };
1996
1997        assert_eq!(max_logs, 2);
1998        assert_eq!(from_block, 100);
1999        assert_eq!(to_block, 101);
2000    }
2001
2002    #[tokio::test]
2003    async fn test_non_consecutive_headers_after_bloom_filter() {
2004        let provider = MockEthProvider::default();
2005
2006        // Create 4 headers where only blocks 100 and 102 will match bloom filter
2007        let mut expected_hashes = vec![];
2008        let mut prev_hash = alloy_primitives::B256::default();
2009
2010        // Create a transaction for blocks that will have receipts
2011        use reth_ethereum_primitives::{TransactionSigned, TxType};
2012
2013        let tx_inner = TxLegacy {
2014            chain_id: Some(1),
2015            nonce: 0,
2016            gas_price: 21_000,
2017            gas_limit: 21_000,
2018            to: alloy_primitives::TxKind::Call(alloy_primitives::Address::ZERO),
2019            value: alloy_primitives::U256::ZERO,
2020            input: alloy_primitives::Bytes::new(),
2021        };
2022        let signature = alloy_primitives::Signature::test_signature();
2023        let tx = TransactionSigned::new_unhashed(tx_inner.into(), signature);
2024
2025        for i in 100u64..=103 {
2026            let header = alloy_consensus::Header {
2027                number: i,
2028                parent_hash: prev_hash,
2029                // Set bloom to match filter only for blocks 100 and 102
2030                logs_bloom: if i == 100 || i == 102 {
2031                    Bloom::repeat_byte(1u8)
2032                } else {
2033                    alloy_primitives::Bloom::default()
2034                },
2035                ..Default::default()
2036            };
2037
2038            let hash = header.hash_slow();
2039            expected_hashes.push(hash);
2040            prev_hash = hash;
2041
2042            // Add transaction to blocks that will have receipts (100 and 102)
2043            let transactions = if i == 100 || i == 102 { vec![tx.clone()] } else { vec![] };
2044
2045            let block = reth_ethereum_primitives::Block {
2046                header,
2047                body: reth_ethereum_primitives::BlockBody { transactions, ..Default::default() },
2048            };
2049            provider.add_block(hash, block);
2050        }
2051
2052        // Add receipts with logs only to blocks that match bloom
2053        let mock_log = alloy_primitives::Log {
2054            address: alloy_primitives::Address::ZERO,
2055            data: alloy_primitives::LogData::new_unchecked(vec![], alloy_primitives::Bytes::new()),
2056        };
2057
2058        let receipt = reth_ethereum_primitives::Receipt {
2059            tx_type: TxType::Legacy,
2060            cumulative_gas_used: 21_000,
2061            logs: vec![mock_log],
2062            success: true,
2063        };
2064
2065        provider.add_receipts(100, vec![receipt.clone()]);
2066        provider.add_receipts(101, vec![]);
2067        provider.add_receipts(102, vec![receipt.clone()]);
2068        provider.add_receipts(103, vec![]);
2069
2070        // Add block body indices for each block so receipts can be fetched
2071        provider
2072            .add_block_body_indices(100, StoredBlockBodyIndices { first_tx_num: 0, tx_count: 1 });
2073        provider
2074            .add_block_body_indices(101, StoredBlockBodyIndices { first_tx_num: 1, tx_count: 0 });
2075        provider
2076            .add_block_body_indices(102, StoredBlockBodyIndices { first_tx_num: 1, tx_count: 1 });
2077        provider
2078            .add_block_body_indices(103, StoredBlockBodyIndices { first_tx_num: 2, tx_count: 0 });
2079
2080        let eth_api = build_test_eth_api(provider);
2081        let eth_filter = EthFilter::new(eth_api, EthFilterConfig::default(), Runtime::test());
2082
2083        // Use default filter which will match any non-empty bloom
2084        let filter = Filter::default();
2085
2086        // Get logs in the range - this will trigger the bloom filtering
2087        let logs = eth_filter
2088            .inner
2089            .clone()
2090            .get_logs_in_block_range(filter, 100, 103, QueryLimits::default())
2091            .await
2092            .expect("should succeed");
2093
2094        // We should get logs from blocks 100 and 102 only (bloom filtered)
2095        assert_eq!(logs.len(), 2);
2096
2097        assert_eq!(logs[0].block_number, Some(100));
2098        assert_eq!(logs[1].block_number, Some(102));
2099
2100        // Each block hash should be the hash of its own header, not derived from any other header
2101        assert_eq!(logs[0].block_hash, Some(expected_hashes[0])); // block 100
2102        assert_eq!(logs[1].block_hash, Some(expected_hashes[2])); // block 102
2103    }
2104
2105    #[tokio::test]
2106    async fn test_range_scan_waits_for_blocking_io_permit() {
2107        let provider = MockEthProvider::default();
2108        let header = alloy_consensus::Header::default();
2109        provider.add_header(header.hash_slow(), header);
2110        provider.add_receipts(0, vec![]);
2111        let eth_api = build_test_eth_api(provider);
2112
2113        // take every permit so the scan has to wait for one
2114        let guard = eth_api.blocking_io_task_guard().clone();
2115        let permits =
2116            guard.clone().acquire_many_owned(guard.available_permits() as u32).await.unwrap();
2117
2118        let eth_filter = EthFilter::new(eth_api, EthFilterConfig::default(), Runtime::test());
2119        let scan = eth_filter.inner.clone().get_logs_in_block_range(
2120            Filter::default(),
2121            0,
2122            0,
2123            QueryLimits::default(),
2124        );
2125        tokio::pin!(scan);
2126        assert!(
2127            tokio::time::timeout(Duration::from_millis(100), &mut scan).await.is_err(),
2128            "scan must wait for a blocking IO permit"
2129        );
2130
2131        drop(permits);
2132        assert!(scan.await.unwrap().is_empty());
2133    }
2134
2135    #[tokio::test]
2136    async fn test_range_scan_across_windows() {
2137        use alloy_primitives::{Address, Bloom, Bytes, Log, LogData, Signature};
2138        use reth_ethereum_primitives::{Block, BlockBody, Receipt, TransactionSigned};
2139
2140        let provider = MockEthProvider::default();
2141        let tx = TransactionSigned::new_unhashed(
2142            TxLegacy {
2143                chain_id: Some(1),
2144                gas_price: 21_000,
2145                gas_limit: 21_000,
2146                ..Default::default()
2147            }
2148            .into(),
2149            Signature::test_signature(),
2150        );
2151        let receipt = Receipt {
2152            tx_type: TxType::Legacy,
2153            cumulative_gas_used: 21_000,
2154            logs: vec![Log {
2155                address: Address::ZERO,
2156                data: LogData::new_unchecked(vec![], Bytes::new()),
2157            }],
2158            success: true,
2159        };
2160
2161        // an empty filter matches every header, so every window is fetched in parallel while
2162        // only three blocks have logs to return
2163        let matching_blocks = [5u64, 1_200, 2_400];
2164        let mut parent_hash = FixedBytes::default();
2165        for number in 0..=2_500u64 {
2166            let matches = matching_blocks.contains(&number);
2167            let header = alloy_consensus::Header {
2168                number,
2169                parent_hash,
2170                logs_bloom: if matches { Bloom::repeat_byte(1u8) } else { Bloom::default() },
2171                ..Default::default()
2172            };
2173            parent_hash = header.hash_slow();
2174            let transactions = if matches { vec![tx.clone()] } else { Vec::new() };
2175            provider.add_block(
2176                parent_hash,
2177                Block { header, body: BlockBody { transactions, ..Default::default() } },
2178            );
2179            if matches {
2180                let tx_num = matching_blocks.iter().position(|b| *b == number).unwrap() as u64;
2181                provider.add_receipts(number, vec![receipt.clone()]);
2182                provider.add_block_body_indices(
2183                    number,
2184                    StoredBlockBodyIndices { first_tx_num: tx_num, tx_count: 1 },
2185                );
2186            } else {
2187                provider.add_receipts(number, vec![]);
2188            }
2189        }
2190
2191        let eth_filter = EthFilter::new(
2192            build_test_eth_api(provider),
2193            EthFilterConfig::default(),
2194            Runtime::test(),
2195        );
2196        let logs = eth_filter
2197            .inner
2198            .clone()
2199            .get_logs_in_block_range(Filter::default(), 0, 2_500, QueryLimits::default())
2200            .await
2201            .unwrap();
2202        let blocks = logs.iter().map(|log| log.block_number.unwrap()).collect::<Vec<_>>();
2203        assert_eq!(blocks, matching_blocks);
2204
2205        // the log limit ends the scan in the window that exceeds it and names the last block that
2206        // fit as the range to retry with
2207        let err = eth_filter
2208            .inner
2209            .clone()
2210            .get_logs_in_block_range(
2211                Filter::default(),
2212                0,
2213                2_500,
2214                QueryLimits { max_blocks_per_filter: None, max_logs_per_response: Some(1) },
2215            )
2216            .await
2217            .unwrap_err();
2218        assert!(
2219            matches!(
2220                err,
2221                EthFilterError::QueryExceedsMaxResults {
2222                    max_logs: 1,
2223                    from_block: 0,
2224                    to_block: 1_199
2225                }
2226            ),
2227            "{err:?}"
2228        );
2229    }
2230
2231    #[tokio::test]
2232    async fn test_logs_for_filter_pending_to_block_ends_at_head() {
2233        let provider = MockEthProvider::default();
2234        let header = alloy_consensus::Header { number: 2, ..Default::default() };
2235        let hash = header.hash_slow();
2236        provider.add_header(hash, header);
2237        provider.add_receipts(2, vec![]);
2238        // the engine holds a payload that is not canonical yet
2239        provider.set_pending_block_num_hash(Some(alloy_eips::BlockNumHash::new(3, hash)));
2240
2241        let eth_filter = EthFilter::new(
2242            build_test_eth_api(provider),
2243            EthFilterConfig::default(),
2244            Runtime::test(),
2245        );
2246        for filter in [
2247            Filter::new().from_block(0u64).to_block(BlockNumberOrTag::Pending),
2248            Filter::new().select(BlockNumberOrTag::Pending..),
2249        ] {
2250            let logs = eth_filter
2251                .inner
2252                .clone()
2253                .logs_for_filter(filter, QueryLimits::default())
2254                .await
2255                .unwrap();
2256            assert!(logs.is_empty());
2257        }
2258    }
2259
2260    #[tokio::test]
2261    async fn test_logs_for_filter_over_pruned_receipts() {
2262        let provider = MockEthProvider::default();
2263        let mut parent_hash = FixedBytes::default();
2264        for number in 0..=2u64 {
2265            let header = alloy_consensus::Header {
2266                number,
2267                parent_hash,
2268                logs_bloom: Bloom::repeat_byte(1u8),
2269                ..Default::default()
2270            };
2271            parent_hash = header.hash_slow();
2272            provider.add_block(
2273                parent_hash,
2274                reth_ethereum_primitives::Block { header, body: Default::default() },
2275            );
2276            // the receipts of block 1 were pruned
2277            if number != 1 {
2278                provider.add_receipts(number, vec![]);
2279            }
2280        }
2281
2282        let eth_filter = EthFilter::new(
2283            build_test_eth_api(provider),
2284            EthFilterConfig::default(),
2285            Runtime::test(),
2286        );
2287
2288        // a range that reaches the pruned block is rejected instead of served incompletely
2289        let err = eth_filter
2290            .inner
2291            .clone()
2292            .logs_for_filter(Filter::new().from_block(0u64).to_block(2u64), QueryLimits::default())
2293            .await
2294            .unwrap_err();
2295        assert!(matches!(err, EthFilterError::ReceiptsUnavailable(1)), "{err:?}");
2296
2297        // a range that avoids it is served
2298        let logs = eth_filter
2299            .inner
2300            .clone()
2301            .logs_for_filter(Filter::new().from_block(2u64).to_block(2u64), QueryLimits::default())
2302            .await
2303            .unwrap();
2304        assert!(logs.is_empty());
2305    }
2306}