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