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