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