stratus/eth/primitives/
log_filter.rs

1use display_json::DebugAsJson;
2
3use crate::eth::primitives::Address;
4use crate::eth::primitives::BlockNumber;
5use crate::eth::primitives::Log;
6use crate::eth::primitives::LogFilterInput;
7use crate::ext::not;
8
9#[derive(Clone, DebugAsJson, serde::Serialize, Eq, Hash, PartialEq)]
10#[cfg_attr(test, derive(serde::Deserialize, fake::Dummy))]
11#[cfg_attr(test, derive(Default))]
12pub struct LogFilter {
13    pub from_block: BlockNumber,
14    pub to_block: Option<BlockNumber>,
15    pub addresses: Vec<Address>,
16
17    /// Original payload received via RPC.
18    #[cfg_attr(not(test), serde(skip))]
19    pub original_input: LogFilterInput,
20}
21
22impl LogFilter {
23    /// Checks if a log matches the filter.
24    pub fn matches(&self, log: &Log, block_number: BlockNumber) -> bool {
25        // filter block range
26        if block_number < self.from_block {
27            return false;
28        }
29        if self.to_block.as_ref().is_some_and(|to_block| block_number > *to_block) {
30            return false;
31        }
32
33        // filter address
34        let has_addresses = not(self.addresses.is_empty());
35        if has_addresses && not(self.addresses.contains(&log.address)) {
36            return false;
37        }
38
39        let filter_topics = &self.original_input.topics;
40        let log_topics = log.topics();
41
42        // (https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getlogs)
43        // Matching rules for filtering topics in `eth_getLogs`:
44        //
45        // - `[]`: anything
46        // - `[A]`: A in first position (and anything after)
47        // - `[null, B]`: anything in first position AND B in second position (and anything after)
48        // - `[A, B]`: A in first position AND B in second position (and anything after)
49        // - `[[A, B], [A, B]]`: (A OR B) in first position AND (A OR B) in second position (and anything after)
50        //
51        // And from
52        //
53        // But it seems to leave the following unspecified:
54        //
55        // - `[[A, B, null]]`: ?
56        //   - `null` in nested array with other non-null items
57        // - `[[]]`: ?
58        //   - `[]` as an inner array alone, after, or before other elements
59        //
60        // In doubt of what to do, this implementation will:
61        //
62        // - Treat `[[null]]` as `[null]`, that is, match anything for that index.
63        // - Treat `[[]]` as `[null]` (same as above), match anything for that index.
64
65        // filter field missing, set to `null` or equal to `[]`
66        if filter_topics.is_empty() {
67            return true;
68        }
69
70        for (log_topic, filter_topic) in log_topics.into_iter().zip(filter_topics) {
71            // the unspecified nested `[[]]`
72            if filter_topic.is_empty() {
73                continue; // match anything
74            }
75            // `[null]` and `[[null]]` (due to how this is deserialized)
76            if filter_topic.contains(&None) {
77                continue; // match anything
78            }
79            // `[A, ..]` ,`[[A], ..]` and `[[A, B, C, ..], ..]` (due to how this is deserialized)
80            if !filter_topic.contains(&log_topic) {
81                return false; // not included in OR filter, filtered out
82            }
83        }
84
85        true
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use std::sync::Arc;
92
93    use itertools::Itertools;
94
95    use super::*;
96    use crate::eth::primitives::Log;
97    use crate::eth::primitives::LogFilterInputTopic;
98    use crate::eth::primitives::LogTopic;
99    use crate::eth::storage::StratusStorage;
100    use crate::utils::test_utils::fake_first;
101    use crate::utils::test_utils::fake_list;
102
103    fn build_filter(addresses: Vec<Address>, topics_nested: Vec<Vec<Option<LogTopic>>>) -> LogFilter {
104        let topics_map = |topics: Vec<Option<LogTopic>>| LogFilterInputTopic(topics.into_iter().collect());
105
106        let storage = StratusStorage::new_test().unwrap();
107
108        LogFilterInput {
109            address: addresses,
110            topics: topics_nested.into_iter().map(topics_map).collect(),
111            ..LogFilterInput::default()
112        }
113        .parse(&Arc::new(storage))
114        .unwrap()
115    }
116
117    fn log_with_topics(topics: [Option<LogTopic>; 4]) -> Log {
118        let log = fake_first::<Log>();
119        Log {
120            topic0: topics[0],
121            topic1: topics[1],
122            topic2: topics[2],
123            topic3: topics[3],
124            ..log
125        }
126    }
127
128    fn log_with_address(address: Address) -> Log {
129        let log = fake_first::<Log>();
130        Log { address, ..log }
131    }
132
133    #[test]
134    fn log_filtering_by_topic() {
135        let topics = fake_list::<LogTopic>(8).into_iter().map(Some).collect_vec();
136
137        let filter = build_filter(
138            vec![],
139            vec![
140                vec![topics[1], topics[2], topics[3]],
141                vec![None],
142                vec![topics[4], topics[5]],
143                vec![topics[6], topics[7]],
144            ],
145        );
146
147        assert!(filter.matches(&log_with_topics([topics[1], None, topics[4], topics[6]]), BlockNumber::ZERO));
148        assert!(filter.matches(&log_with_topics([topics[2], None, topics[4], topics[6]]), BlockNumber::ZERO));
149        assert!(filter.matches(&log_with_topics([topics[3], None, topics[4], topics[6]]), BlockNumber::ZERO));
150        assert!(filter.matches(&log_with_topics([topics[3], None, topics[5], topics[6]]), BlockNumber::ZERO));
151        assert!(filter.matches(&log_with_topics([topics[3], topics[0], topics[5], topics[7]]), BlockNumber::ZERO));
152        assert!(filter.matches(&log_with_topics([topics[1], topics[2], topics[4], topics[6]]), BlockNumber::ZERO));
153        assert!(filter.matches(&log_with_topics([topics[2], topics[4], topics[4], topics[7]]), BlockNumber::ZERO));
154        assert!(filter.matches(&log_with_topics([topics[2], topics[7], topics[5], topics[6]]), BlockNumber::ZERO));
155
156        assert!(not(filter.matches(&log_with_topics([None, None, None, None]), BlockNumber::ZERO)));
157        assert!(not(filter.matches(&log_with_topics([topics[0], None, None, None]), BlockNumber::ZERO)));
158        assert!(not(filter.matches(&log_with_topics([None, topics[0], None, None]), BlockNumber::ZERO)));
159        assert!(not(filter.matches(&log_with_topics([None, None, topics[0], None]), BlockNumber::ZERO)));
160        assert!(not(filter.matches(&log_with_topics([None, None, None, topics[0]]), BlockNumber::ZERO)));
161        assert!(not(
162            filter.matches(&log_with_topics([topics[2], topics[2], topics[4], topics[0]]), BlockNumber::ZERO)
163        ));
164        assert!(not(filter.matches(&log_with_topics([topics[3], None, topics[5], None]), BlockNumber::ZERO)));
165        assert!(not(filter.matches(&log_with_topics([topics[2], topics[4], None, topics[6]]), BlockNumber::ZERO)));
166        assert!(not(filter.matches(&log_with_topics([None, topics[0], topics[4], topics[6]]), BlockNumber::ZERO)));
167        assert!(not(filter.matches(&log_with_topics([topics[3], topics[0], topics[4], None]), BlockNumber::ZERO)));
168
169        let filter = build_filter(vec![], vec![vec![None], vec![topics[1], topics[2]]]);
170
171        assert!(filter.matches(&log_with_topics([topics[1], topics[1], topics[1], topics[1]]), BlockNumber::ZERO));
172        assert!(filter.matches(&log_with_topics([topics[1], topics[1], topics[1], None]), BlockNumber::ZERO));
173        assert!(filter.matches(&log_with_topics([topics[1], topics[1], None, topics[1]]), BlockNumber::ZERO));
174        assert!(filter.matches(&log_with_topics([None, topics[1], topics[1], topics[1]]), BlockNumber::ZERO));
175        assert!(filter.matches(&log_with_topics([topics[0], topics[1], None, topics[2]]), BlockNumber::ZERO));
176        assert!(filter.matches(&log_with_topics([None, topics[2], None, topics[1]]), BlockNumber::ZERO));
177
178        assert!(not(filter.matches(&log_with_topics([topics[1], None, topics[1], topics[1]]), BlockNumber::ZERO)));
179        assert!(not(filter.matches(&log_with_topics([topics[1], None, None, None]), BlockNumber::ZERO)));
180        assert!(not(filter.matches(&log_with_topics([topics[1], topics[3], None, None]), BlockNumber::ZERO)));
181        assert!(not(filter.matches(&log_with_topics([None, topics[3], None, None]), BlockNumber::ZERO)));
182        assert!(not(filter.matches(&log_with_topics([None, None, None, None]), BlockNumber::ZERO)));
183    }
184
185    #[test]
186    fn log_filtering_by_address() {
187        let addresses = fake_list::<Address>(4);
188
189        let filter = build_filter(vec![addresses[1], addresses[2]], vec![]);
190
191        assert!(filter.matches(&log_with_address(addresses[1]), BlockNumber::ZERO));
192        assert!(filter.matches(&log_with_address(addresses[2]), BlockNumber::ZERO));
193
194        assert!(not(filter.matches(&log_with_address(addresses[0]), BlockNumber::ZERO)));
195        assert!(not(filter.matches(&log_with_address(addresses[3]), BlockNumber::ZERO)));
196
197        let filter = build_filter(vec![], vec![]);
198
199        assert!(filter.matches(&log_with_address(addresses[0]), BlockNumber::ZERO));
200        assert!(filter.matches(&log_with_address(addresses[1]), BlockNumber::ZERO));
201        assert!(filter.matches(&log_with_address(addresses[2]), BlockNumber::ZERO));
202        assert!(filter.matches(&log_with_address(addresses[3]), BlockNumber::ZERO));
203    }
204}