Skip to main content

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;
14use thiserror::Error;
15
16/// Returns all matching of a block's receipts when the transaction hashes are known.
17pub fn matching_block_logs_with_tx_hashes<'a, I, R>(
18    filter: &Filter,
19    block_num_hash: BlockNumHash,
20    block_timestamp: u64,
21    tx_hashes_and_receipts: I,
22    removed: bool,
23) -> Vec<Log>
24where
25    I: IntoIterator<Item = (TxHash, &'a R)>,
26    R: TxReceipt<Log = alloy_primitives::Log> + 'a,
27{
28    if !filter.matches_block(&block_num_hash) {
29        return vec![];
30    }
31
32    let mut all_logs = Vec::new();
33    // Tracks the index of a log in the entire block.
34    let mut log_index: u64 = 0;
35
36    // Iterate over transaction hashes and receipts and append matching logs.
37    for (receipt_idx, (tx_hash, receipt)) in tx_hashes_and_receipts.into_iter().enumerate() {
38        for log in receipt.logs() {
39            if filter.matches(log) {
40                let log = Log {
41                    inner: log.clone(),
42                    block_hash: Some(block_num_hash.hash),
43                    block_number: Some(block_num_hash.number),
44                    transaction_hash: Some(tx_hash),
45                    // The transaction and receipt index is always the same.
46                    transaction_index: Some(receipt_idx as u64),
47                    log_index: Some(log_index),
48                    removed,
49                    block_timestamp: Some(block_timestamp),
50                };
51                all_logs.push(log);
52            }
53            log_index += 1;
54        }
55    }
56    all_logs
57}
58
59/// Helper enum to fetch a transaction either from a block or from the provider.
60#[derive(Debug)]
61pub enum ProviderOrBlock<'a, P: BlockReader> {
62    /// Provider
63    Provider(&'a P),
64    /// [`RecoveredBlock`]
65    Block(Arc<RecoveredBlock<ProviderBlock<P>>>),
66}
67
68/// Appends all matching logs of a block's receipts.
69/// If the log matches, look up the corresponding transaction hash.
70pub fn append_matching_block_logs<P>(
71    all_logs: &mut Vec<Log>,
72    provider_or_block: ProviderOrBlock<'_, P>,
73    filter: &Filter,
74    block_num_hash: BlockNumHash,
75    receipts: &[P::Receipt],
76    removed: bool,
77    block_timestamp: u64,
78) -> Result<(), ProviderError>
79where
80    P: BlockReader<Transaction: SignedTransaction>,
81{
82    if !filter.matches_block(&block_num_hash) {
83        return Ok(());
84    }
85
86    // Tracks the index of a log in the entire block.
87    let mut log_index: u64 = 0;
88
89    // Lazy loaded number of the first transaction in the block.
90    // This is useful for blocks with multiple matching logs because it
91    // prevents re-querying the block body indices.
92    let mut loaded_first_tx_num = None;
93
94    // Iterate over receipts and append matching logs.
95    for (receipt_idx, receipt) in receipts.iter().enumerate() {
96        // The transaction hash of the current receipt.
97        let mut transaction_hash = None;
98
99        for log in receipt.logs() {
100            if filter.matches(log) {
101                // if this is the first match in the receipt's logs, look up the transaction hash
102                if transaction_hash.is_none() {
103                    transaction_hash = match &provider_or_block {
104                        ProviderOrBlock::Block(block) => {
105                            block.body().transactions().get(receipt_idx).map(|t| t.trie_hash())
106                        }
107                        ProviderOrBlock::Provider(provider) => {
108                            let first_tx_num = match loaded_first_tx_num {
109                                Some(num) => num,
110                                None => {
111                                    let block_body_indices = provider
112                                        .block_body_indices(block_num_hash.number)?
113                                        .ok_or(ProviderError::BlockBodyIndicesNotFound(
114                                            block_num_hash.number,
115                                        ))?;
116                                    loaded_first_tx_num = Some(block_body_indices.first_tx_num);
117                                    block_body_indices.first_tx_num
118                                }
119                            };
120
121                            // This is safe because Transactions and Receipts have the same
122                            // keys.
123                            let transaction_id = first_tx_num + receipt_idx as u64;
124                            let transaction =
125                                provider.transaction_by_id(transaction_id)?.ok_or_else(|| {
126                                    ProviderError::TransactionNotFound(transaction_id.into())
127                                })?;
128
129                            Some(transaction.trie_hash())
130                        }
131                    };
132                }
133
134                let log = Log {
135                    inner: log.clone(),
136                    block_hash: Some(block_num_hash.hash),
137                    block_number: Some(block_num_hash.number),
138                    transaction_hash,
139                    // The transaction and receipt index is always the same.
140                    transaction_index: Some(receipt_idx as u64),
141                    log_index: Some(log_index),
142                    removed,
143                    block_timestamp: Some(block_timestamp),
144                };
145                all_logs.push(log);
146            }
147            log_index += 1;
148        }
149    }
150    Ok(())
151}
152
153/// Computes the block range based on the filter range and current block numbers.
154///
155/// Returns an error for invalid ranges rather than silently clamping values.
156pub fn get_filter_block_range(
157    from_block: Option<u64>,
158    to_block: Option<u64>,
159    start_block: u64,
160    info: ChainInfo,
161) -> Result<(u64, u64), FilterBlockRangeError> {
162    let from_block_number = from_block.unwrap_or(start_block);
163    let to_block_number = to_block.unwrap_or(info.best_number);
164
165    // from > to is an invalid range
166    if from_block_number > to_block_number {
167        return Err(FilterBlockRangeError::InvalidBlockRange);
168    }
169
170    // we cannot query blocks that don't exist yet
171    if to_block_number > info.best_number {
172        return Err(FilterBlockRangeError::BlockRangeExceedsHead);
173    }
174
175    Ok((from_block_number, to_block_number))
176}
177
178/// Errors for filter block range validation.
179///
180/// See also <https://github.com/ethereum/go-ethereum/blob/master/eth/filters/filter.go#L224-L230>.
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
182pub enum FilterBlockRangeError {
183    /// `from_block > to_block`
184    #[error("invalid block range params")]
185    InvalidBlockRange,
186    /// Block range extends beyond current head
187    #[error("block range extends beyond current head block")]
188    BlockRangeExceedsHead,
189}
190
191#[cfg(test)]
192mod tests {
193    use alloy_rpc_types_eth::Filter;
194
195    use super::*;
196
197    #[test]
198    fn test_log_range_from_and_to() {
199        let from = 14000000u64;
200        let to = 14000100u64;
201        let info = ChainInfo { best_number: 15000000, ..Default::default() };
202        let range = get_filter_block_range(Some(from), Some(to), info.best_number, info).unwrap();
203        assert_eq!(range, (from, to));
204    }
205
206    #[test]
207    fn test_log_range_from() {
208        let from = 14000000u64;
209        let info = ChainInfo { best_number: 15000000, ..Default::default() };
210        let range = get_filter_block_range(Some(from), None, 0, info).unwrap();
211        assert_eq!(range, (from, info.best_number));
212    }
213
214    #[test]
215    fn test_log_range_to() {
216        let to = 14000000u64;
217        let start_block = 0u64;
218        let info = ChainInfo { best_number: 15000000, ..Default::default() };
219        let range = get_filter_block_range(None, Some(to), start_block, info).unwrap();
220        assert_eq!(range, (start_block, to));
221    }
222
223    #[test]
224    fn test_log_range_higher_error() {
225        // Range extends beyond head -> should error instead of clamping
226        let from = 15000001u64;
227        let to = 15000002u64;
228        let info = ChainInfo { best_number: 15000000, ..Default::default() };
229        let err = get_filter_block_range(Some(from), Some(to), info.best_number, info).unwrap_err();
230        assert_eq!(err, FilterBlockRangeError::BlockRangeExceedsHead);
231    }
232
233    #[test]
234    fn test_log_range_to_below_start_error() {
235        // to_block < start_block, default from -> invalid range
236        let to = 14000000u64;
237        let info = ChainInfo { best_number: 15000000, ..Default::default() };
238        let err = get_filter_block_range(None, Some(to), info.best_number, info).unwrap_err();
239        assert_eq!(err, FilterBlockRangeError::InvalidBlockRange);
240    }
241
242    #[test]
243    fn test_log_range_empty() {
244        let info = ChainInfo { best_number: 15000000, ..Default::default() };
245        let range = get_filter_block_range(None, None, info.best_number, info).unwrap();
246
247        // no range given -> head
248        assert_eq!(range, (info.best_number, info.best_number));
249    }
250
251    #[test]
252    fn test_invalid_block_range_error() {
253        let from = 100;
254        let to = 50;
255        let info = ChainInfo { best_number: 150, ..Default::default() };
256        let err = get_filter_block_range(Some(from), Some(to), 0, info).unwrap_err();
257        assert_eq!(err, FilterBlockRangeError::InvalidBlockRange);
258    }
259
260    #[test]
261    fn test_block_range_exceeds_head_error() {
262        let from = 100;
263        let to = 200;
264        let info = ChainInfo { best_number: 150, ..Default::default() };
265        let err = get_filter_block_range(Some(from), Some(to), 0, info).unwrap_err();
266        assert_eq!(err, FilterBlockRangeError::BlockRangeExceedsHead);
267    }
268
269    #[test]
270    fn parse_log_from_only() {
271        let s = r#"{"fromBlock":"0xf47a42","address":["0x7de93682b9b5d80d45cd371f7a14f74d49b0914c","0x0f00392fcb466c0e4e4310d81b941e07b4d5a079","0xebf67ab8cff336d3f609127e8bbf8bd6dd93cd81"],"topics":["0x0559884fd3a460db3073b7fc896cc77986f16e378210ded43186175bf646fc5f"]}"#;
272        let filter: Filter = serde_json::from_str(s).unwrap();
273
274        assert_eq!(filter.get_from_block(), Some(16022082));
275        assert!(filter.get_to_block().is_none());
276
277        let best_number = 17229427;
278        let info = ChainInfo { best_number, ..Default::default() };
279
280        let (from_block, to_block) = filter.block_option.as_range();
281
282        let start_block = info.best_number;
283
284        let (from_block_number, to_block_number) = get_filter_block_range(
285            from_block.and_then(alloy_rpc_types_eth::BlockNumberOrTag::as_number),
286            to_block.and_then(alloy_rpc_types_eth::BlockNumberOrTag::as_number),
287            start_block,
288            info,
289        )
290        .unwrap();
291        assert_eq!(from_block_number, 16022082);
292        assert_eq!(to_block_number, best_number);
293    }
294}