reth_rpc_eth_types/
logs_utils.rs

1//! Helper functions for `reth_rpc_eth_api::EthFilterApiServer` implementation.
2//!
3//! Log parsing for building filter.
4
5use alloy_consensus::TxReceipt;
6use alloy_eips::{eip2718::Encodable2718, BlockNumHash};
7use alloy_primitives::TxHash;
8use alloy_rpc_types_eth::{FilteredParams, Log};
9use reth_chainspec::ChainInfo;
10use reth_errors::ProviderError;
11use reth_primitives_traits::{BlockBody, RecoveredBlock, SignedTransaction};
12use reth_storage_api::{BlockReader, ProviderBlock};
13use std::sync::Arc;
14
15/// Returns all matching of a block's receipts when the transaction hashes are known.
16pub fn matching_block_logs_with_tx_hashes<'a, I, R>(
17    filter: &FilteredParams,
18    block_num_hash: BlockNumHash,
19    tx_hashes_and_receipts: I,
20    removed: bool,
21) -> Vec<Log>
22where
23    I: IntoIterator<Item = (TxHash, &'a R)>,
24    R: TxReceipt<Log = alloy_primitives::Log> + 'a,
25{
26    let mut all_logs = Vec::new();
27    // Tracks the index of a log in the entire block.
28    let mut log_index: u64 = 0;
29    // Iterate over transaction hashes and receipts and append matching logs.
30    for (receipt_idx, (tx_hash, receipt)) in tx_hashes_and_receipts.into_iter().enumerate() {
31        for log in receipt.logs() {
32            if log_matches_filter(block_num_hash, log, filter) {
33                let log = Log {
34                    inner: log.clone(),
35                    block_hash: Some(block_num_hash.hash),
36                    block_number: Some(block_num_hash.number),
37                    transaction_hash: Some(tx_hash),
38                    // The transaction and receipt index is always the same.
39                    transaction_index: Some(receipt_idx as u64),
40                    log_index: Some(log_index),
41                    removed,
42                    block_timestamp: None,
43                };
44                all_logs.push(log);
45            }
46            log_index += 1;
47        }
48    }
49    all_logs
50}
51
52/// Helper enum to fetch a transaction either from a block or from the provider.
53#[derive(Debug)]
54pub enum ProviderOrBlock<'a, P: BlockReader> {
55    /// Provider
56    Provider(&'a P),
57    /// [`RecoveredBlock`]
58    Block(Arc<RecoveredBlock<ProviderBlock<P>>>),
59}
60
61/// Appends all matching logs of a block's receipts.
62/// If the log matches, look up the corresponding transaction hash.
63pub fn append_matching_block_logs<P>(
64    all_logs: &mut Vec<Log>,
65    provider_or_block: ProviderOrBlock<'_, P>,
66    filter: &FilteredParams,
67    block_num_hash: BlockNumHash,
68    receipts: &[P::Receipt],
69    removed: bool,
70    block_timestamp: u64,
71) -> Result<(), ProviderError>
72where
73    P: BlockReader<Transaction: SignedTransaction>,
74{
75    // Tracks the index of a log in the entire block.
76    let mut log_index: u64 = 0;
77
78    // Lazy loaded number of the first transaction in the block.
79    // This is useful for blocks with multiple matching logs because it
80    // prevents re-querying the block body indices.
81    let mut loaded_first_tx_num = None;
82
83    // Iterate over receipts and append matching logs.
84    for (receipt_idx, receipt) in receipts.iter().enumerate() {
85        // The transaction hash of the current receipt.
86        let mut transaction_hash = None;
87
88        for log in receipt.logs() {
89            if log_matches_filter(block_num_hash, log, filter) {
90                // if this is the first match in the receipt's logs, look up the transaction hash
91                if transaction_hash.is_none() {
92                    transaction_hash = match &provider_or_block {
93                        ProviderOrBlock::Block(block) => {
94                            block.body().transactions().get(receipt_idx).map(|t| t.trie_hash())
95                        }
96                        ProviderOrBlock::Provider(provider) => {
97                            let first_tx_num = match loaded_first_tx_num {
98                                Some(num) => num,
99                                None => {
100                                    let block_body_indices = provider
101                                        .block_body_indices(block_num_hash.number)?
102                                        .ok_or(ProviderError::BlockBodyIndicesNotFound(
103                                            block_num_hash.number,
104                                        ))?;
105                                    loaded_first_tx_num = Some(block_body_indices.first_tx_num);
106                                    block_body_indices.first_tx_num
107                                }
108                            };
109
110                            // This is safe because Transactions and Receipts have the same
111                            // keys.
112                            let transaction_id = first_tx_num + receipt_idx as u64;
113                            let transaction =
114                                provider.transaction_by_id(transaction_id)?.ok_or_else(|| {
115                                    ProviderError::TransactionNotFound(transaction_id.into())
116                                })?;
117
118                            Some(transaction.trie_hash())
119                        }
120                    };
121                }
122
123                let log = Log {
124                    inner: log.clone(),
125                    block_hash: Some(block_num_hash.hash),
126                    block_number: Some(block_num_hash.number),
127                    transaction_hash,
128                    // The transaction and receipt index is always the same.
129                    transaction_index: Some(receipt_idx as u64),
130                    log_index: Some(log_index),
131                    removed,
132                    block_timestamp: Some(block_timestamp),
133                };
134                all_logs.push(log);
135            }
136            log_index += 1;
137        }
138    }
139    Ok(())
140}
141
142/// Returns true if the log matches the filter and should be included
143pub fn log_matches_filter(
144    block: BlockNumHash,
145    log: &alloy_primitives::Log,
146    params: &FilteredParams,
147) -> bool {
148    if params.filter.is_some() &&
149        (!params.filter_block_range(block.number) ||
150            !params.filter_block_hash(block.hash) ||
151            !params.filter_address(&log.address) ||
152            !params.filter_topics(log.topics()))
153    {
154        return false
155    }
156    true
157}
158
159/// Computes the block range based on the filter range and current block numbers
160pub fn get_filter_block_range(
161    from_block: Option<u64>,
162    to_block: Option<u64>,
163    start_block: u64,
164    info: ChainInfo,
165) -> (u64, u64) {
166    let mut from_block_number = start_block;
167    let mut to_block_number = info.best_number;
168
169    // if a `from_block` argument is provided then the `from_block_number` is the converted value or
170    // the start block if the converted value is larger than the start block, since `from_block`
171    // can't be a future block: `min(head, from_block)`
172    if let Some(filter_from_block) = from_block {
173        from_block_number = start_block.min(filter_from_block)
174    }
175
176    // upper end of the range is the converted `to_block` argument, restricted by the best block:
177    // `min(best_number,to_block_number)`
178    if let Some(filter_to_block) = to_block {
179        to_block_number = info.best_number.min(filter_to_block);
180    }
181
182    (from_block_number, to_block_number)
183}
184
185#[cfg(test)]
186mod tests {
187    use alloy_rpc_types_eth::Filter;
188
189    use super::*;
190
191    #[test]
192    fn test_log_range_from_and_to() {
193        let from = 14000000u64;
194        let to = 14000100u64;
195        let info = ChainInfo { best_number: 15000000, ..Default::default() };
196        let range = get_filter_block_range(Some(from), Some(to), info.best_number, info);
197        assert_eq!(range, (from, to));
198    }
199
200    #[test]
201    fn test_log_range_higher() {
202        let from = 15000001u64;
203        let to = 15000002u64;
204        let info = ChainInfo { best_number: 15000000, ..Default::default() };
205        let range = get_filter_block_range(Some(from), Some(to), info.best_number, info);
206        assert_eq!(range, (info.best_number, info.best_number));
207    }
208
209    #[test]
210    fn test_log_range_from() {
211        let from = 14000000u64;
212        let info = ChainInfo { best_number: 15000000, ..Default::default() };
213        let range = get_filter_block_range(Some(from), None, info.best_number, info);
214        assert_eq!(range, (from, info.best_number));
215    }
216
217    #[test]
218    fn test_log_range_to() {
219        let to = 14000000u64;
220        let info = ChainInfo { best_number: 15000000, ..Default::default() };
221        let range = get_filter_block_range(None, Some(to), info.best_number, info);
222        assert_eq!(range, (info.best_number, to));
223    }
224
225    #[test]
226    fn test_log_range_empty() {
227        let info = ChainInfo { best_number: 15000000, ..Default::default() };
228        let range = get_filter_block_range(None, None, info.best_number, info);
229
230        // no range given -> head
231        assert_eq!(range, (info.best_number, info.best_number));
232    }
233
234    #[test]
235    fn parse_log_from_only() {
236        let s = r#"{"fromBlock":"0xf47a42","address":["0x7de93682b9b5d80d45cd371f7a14f74d49b0914c","0x0f00392fcb466c0e4e4310d81b941e07b4d5a079","0xebf67ab8cff336d3f609127e8bbf8bd6dd93cd81"],"topics":["0x0559884fd3a460db3073b7fc896cc77986f16e378210ded43186175bf646fc5f"]}"#;
237        let filter: Filter = serde_json::from_str(s).unwrap();
238
239        assert_eq!(filter.get_from_block(), Some(16022082));
240        assert!(filter.get_to_block().is_none());
241
242        let best_number = 17229427;
243        let info = ChainInfo { best_number, ..Default::default() };
244
245        let (from_block, to_block) = filter.block_option.as_range();
246
247        let start_block = info.best_number;
248
249        let (from_block_number, to_block_number) = get_filter_block_range(
250            from_block.and_then(alloy_rpc_types_eth::BlockNumberOrTag::as_number),
251            to_block.and_then(alloy_rpc_types_eth::BlockNumberOrTag::as_number),
252            start_block,
253            info,
254        );
255        assert_eq!(from_block_number, 16022082);
256        assert_eq!(to_block_number, best_number);
257    }
258}