From a84472eeaaee2e0eea831a3dc56e07c4907d7fcc Mon Sep 17 00:00:00 2001 From: Nisheeth Barthwal Date: Tue, 19 Sep 2023 14:48:39 +0200 Subject: [PATCH] add tests --- Cargo.lock | 7 + Cargo.toml | 1 + src/filters.rs | 908 ++++++++++++++++++++++++++++++++++++++++++++++++- src/node.rs | 154 +++++++++ src/testing.rs | 91 +++++ 5 files changed, 1156 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9d858ba3..e14cf77a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1792,6 +1792,7 @@ dependencies = [ "jsonrpc-http-server", "lazy_static", "log", + "maplit", "once_cell", "openssl-sys", "reqwest", @@ -3454,6 +3455,12 @@ dependencies = [ "libc", ] +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + [[package]] name = "match_cfg" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index fe4d552d..0fd8384d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,3 +52,4 @@ rustc-hash = "1.1.0" [dev-dependencies] httptest = "0.15.4" tempdir = "0.3.7" +maplit = "1.0.2" \ No newline at end of file diff --git a/src/filters.rs b/src/filters.rs index d3162646..0f78e99c 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -4,19 +4,25 @@ use zksync_basic_types::{H160, H256, U256, U64}; use zksync_types::api::{BlockNumber, Log}; use zksync_web3_decl::types::FilterChanges; -#[derive(Debug, Clone)] +/// Specifies a filter type +#[derive(Debug, Clone, PartialEq)] pub enum FilterType { + /// A filter for block information Block(BlockFilter), + /// A filter for log information Log(LogFilter), + /// A filter for pending transaction information PendingTransaction(PendingTransactionFilter), } -#[derive(Debug, Default, Clone)] +/// Specifies a filter that keeps track of new blocks +#[derive(Debug, Default, Clone, PartialEq)] pub struct BlockFilter { updates: Vec, } -#[derive(Debug, Clone)] +/// Specifies a filter that keeps track of new logs +#[derive(Debug, Clone, PartialEq)] pub struct LogFilter { from_block: BlockNumber, to_block: BlockNumber, @@ -58,7 +64,7 @@ impl LogFilter { let mut matched_topic = [true; 4]; for (i, topic) in log.topics.iter().take(4).enumerate() { if let Some(topic_set) = &self.topics[i] { - if !topic_set.contains(topic) { + if !topic_set.is_empty() && !topic_set.contains(topic) { matched_topic[i] = false; } } @@ -68,13 +74,15 @@ impl LogFilter { } } -#[derive(Debug, Default, Clone)] +/// Specifies a filter that keeps track of new pending transactions +#[derive(Debug, Default, Clone, PartialEq)] pub struct PendingTransactionFilter { updates: Vec, } type Result = std::result::Result; +/// Keeps track of installed filters and their respective updates. #[derive(Debug, Default, Clone)] pub struct EthFilters { id_counter: U256, @@ -188,6 +196,7 @@ impl EthFilters { Ok(changes) } + /// Notify available filters of a newly produced block pub fn notify_new_block(&mut self, hash: H256) { self.filters .iter_mut() @@ -197,6 +206,7 @@ impl EthFilters { }) } + /// Notify available filters of a new pending transaction pub fn notify_new_pending_transaction(&mut self, hash: H256) { self.filters .iter_mut() @@ -206,6 +216,7 @@ impl EthFilters { }) } + /// Notify available filters of a new transaction log pub fn notify_new_log(&mut self, log: &Log, latest_block_number: U64) { self.filters .iter_mut() @@ -219,3 +230,890 @@ impl EthFilters { }) } } + +#[cfg(test)] +mod tests { + use crate::testing::LogBuilder; + + use super::*; + + use maplit::{hashmap, hashset}; + + #[test] + fn test_add_block_filter() { + let mut filters = EthFilters::default(); + let id = filters.add_block_filter().expect("failed adding filter"); + + assert_eq!(U256::from(1), id); + assert_eq!( + hashmap! { + U256::from(1) => FilterType::Block(BlockFilter { updates: vec![] }) + }, + filters.filters + ); + } + + #[test] + fn test_add_log_filter() { + let mut filters = EthFilters::default(); + let id = filters + .add_log_filter( + BlockNumber::Latest, + BlockNumber::Number(U64::from(10)), + vec![H160::repeat_byte(0x1)], + [ + Some(hashset! { H256::repeat_byte(0x2) }), + Some(hashset! { H256::repeat_byte(0x3), H256::repeat_byte(0x4) }), + None, + Some(hashset! {}), + ], + ) + .expect("failed adding filter"); + + assert_eq!(U256::from(1), id); + assert_eq!( + hashmap! { + U256::from(1) => FilterType::Log(LogFilter { + from_block: BlockNumber::Latest, + to_block: BlockNumber::Number(U64::from(10)), + addresses: vec![H160::repeat_byte(0x1)], + topics: [ + Some(hashset! { H256::repeat_byte(0x2) }), + Some(hashset! { H256::repeat_byte(0x3), H256::repeat_byte(0x4) }), + None, + Some(hashset! {}), + ], + updates:vec![], + }) + }, + filters.filters + ); + } + + #[test] + fn test_add_pending_transaction_filter() { + let mut filters = EthFilters::default(); + let id = filters + .add_pending_transaction_filter() + .expect("failed adding filter"); + + assert_eq!(U256::from(1), id); + assert_eq!( + hashmap! { + U256::from(1) => FilterType::PendingTransaction(PendingTransactionFilter { updates: vec![] }) + }, + filters.filters + ); + } + + #[test] + fn test_different_filters_share_incremental_identifiers() { + let mut filters = EthFilters::default(); + + let block_filter_id = filters.add_block_filter().expect("failed adding filter"); + let log_filter_id = filters + .add_log_filter( + BlockNumber::Earliest, + BlockNumber::Latest, + Default::default(), + Default::default(), + ) + .expect("failed adding filter"); + let pending_transaction_filter_id = filters + .add_pending_transaction_filter() + .expect("failed adding filter"); + + assert_eq!(U256::from(1), block_filter_id); + assert_eq!(U256::from(2), log_filter_id); + assert_eq!(U256::from(3), pending_transaction_filter_id); + } + + #[test] + fn test_remove_filter() { + let mut filters = EthFilters::default(); + let block_filter_id = filters.add_block_filter().expect("failed adding filter"); + let log_filter_id = filters + .add_log_filter( + BlockNumber::Earliest, + BlockNumber::Latest, + Default::default(), + Default::default(), + ) + .expect("failed adding filter"); + let pending_transaction_filter_id = filters + .add_pending_transaction_filter() + .expect("failed adding filter"); + + filters.remove_filter(log_filter_id); + + assert!( + filters.filters.contains_key(&block_filter_id), + "filter was erroneously removed" + ); + assert!( + filters.filters.contains_key(&pending_transaction_filter_id), + "filter was erroneously removed" + ); + assert!( + !filters.filters.contains_key(&log_filter_id), + "filter was not removed" + ); + } + + #[test] + fn test_notify_new_block_appends_updates() { + let mut filters = EthFilters::default(); + let id = filters.add_block_filter().expect("failed adding filter"); + + filters.notify_new_block(H256::repeat_byte(0x1)); + + match filters.filters.get(&id).unwrap() { + FilterType::Block(f) => { + assert_eq!(vec![H256::repeat_byte(0x1)], f.updates); + } + _ => panic!("invalid filter"), + } + } + + #[test] + fn test_notify_new_log_appends_matching_updates() { + let mut filters = EthFilters::default(); + let match_address = H160::repeat_byte(0x1); + let id = filters + .add_log_filter( + BlockNumber::Earliest, + BlockNumber::Latest, + vec![match_address], + Default::default(), + ) + .expect("failed adding filter"); + + let log = LogBuilder::new() + .set_address(match_address) + .set_block(U64::from(1)) + .build(); + filters.notify_new_log(&log, U64::from(1)); + + match filters.filters.get(&id).unwrap() { + FilterType::Log(f) => { + assert_eq!(vec![log], f.updates); + } + _ => panic!("invalid filter"), + } + } + + #[test] + fn test_notify_new_pending_transaction_appends_updates() { + let mut filters = EthFilters::default(); + let id = filters + .add_pending_transaction_filter() + .expect("failed adding filter"); + + filters.notify_new_pending_transaction(H256::repeat_byte(0x1)); + + match filters.filters.get(&id).unwrap() { + FilterType::PendingTransaction(f) => { + assert_eq!(vec![H256::repeat_byte(0x1)], f.updates); + } + _ => panic!("invalid filter"), + } + } + + #[test] + fn test_get_new_changes_block_returns_updates_and_clears_them() { + let mut filters = EthFilters::default(); + let id = filters.add_block_filter().expect("failed adding filter"); + filters.notify_new_block(H256::repeat_byte(0x1)); + + let changes = filters + .get_new_changes(id) + .expect("failed retrieving changes"); + + match changes { + FilterChanges::Hashes(result) => { + assert_eq!(vec![H256::repeat_byte(0x1)], result); + } + _ => panic!("unexpected filter changes {:?}", changes), + } + match filters.filters.get(&id).unwrap() { + FilterType::Block(f) => { + assert!(f.updates.is_empty(), "updates were not cleared"); + } + _ => panic!("invalid filter"), + } + } + + #[test] + fn test_get_new_changes_log_appends_mreturng_updates_and_clears_them() { + let mut filters = EthFilters::default(); + let match_address = H160::repeat_byte(0x1); + let id = filters + .add_log_filter( + BlockNumber::Earliest, + BlockNumber::Latest, + vec![match_address], + Default::default(), + ) + .expect("failed adding filter"); + + let log = LogBuilder::new() + .set_address(match_address) + .set_block(U64::from(1)) + .build(); + filters.notify_new_log(&log, U64::from(1)); + + let changes = filters + .get_new_changes(id) + .expect("failed retrieving changes"); + + match changes { + FilterChanges::Logs(result) => { + assert_eq!(vec![log], result); + } + _ => panic!("unexpected filter changes {:?}", changes), + } + match filters.filters.get(&id).unwrap() { + FilterType::Log(f) => { + assert!(f.updates.is_empty(), "updates were not cleared"); + } + _ => panic!("invalid filter"), + } + } + + #[test] + fn test_get_new_changes_pending_transaction_returns_updates_and_clears_them() { + let mut filters = EthFilters::default(); + let id = filters + .add_pending_transaction_filter() + .expect("failed adding filter"); + + filters.notify_new_pending_transaction(H256::repeat_byte(0x1)); + + let changes = filters + .get_new_changes(id) + .expect("failed retrieving changes"); + + match changes { + FilterChanges::Hashes(result) => { + assert_eq!(vec![H256::repeat_byte(0x1)], result); + } + _ => panic!("unexpected filter changes {:?}", changes), + } + match filters.filters.get(&id).unwrap() { + FilterType::PendingTransaction(f) => { + assert!(f.updates.is_empty(), "updates were not cleared"); + } + _ => panic!("invalid filter"), + } + } +} + +#[cfg(test)] +mod log_filter_tests { + use maplit::hashset; + + use crate::testing::LogBuilder; + + use super::*; + + #[test] + fn test_filter_from_block_earliest_accepts_all_block_numbers_lte_latest() { + let filter = LogFilter { + from_block: BlockNumber::Earliest, + to_block: BlockNumber::Latest, + addresses: Default::default(), + topics: Default::default(), + updates: Default::default(), + }; + + let latest_block_number = 2u64; + for log_block in 0..=latest_block_number { + let matched = filter.matches( + &LogBuilder::new().set_block(U64::from(log_block)).build(), + U64::from(latest_block_number), + ); + assert!( + matched, + "failed matching log for block_number {}", + log_block + ); + } + } + + #[test] + fn test_filter_from_block_latest_accepts_block_number_eq_latest() { + let filter = LogFilter { + from_block: BlockNumber::Latest, + to_block: BlockNumber::Latest, + addresses: Default::default(), + topics: Default::default(), + updates: Default::default(), + }; + + let latest_block_number = U64::from(2); + let input_block_number = U64::from(2); + let matched = filter.matches( + &LogBuilder::new().set_block(input_block_number).build(), + latest_block_number, + ); + assert!(matched); + } + + #[test] + fn test_filter_from_block_latest_rejects_block_number_lt_latest() { + let filter = LogFilter { + from_block: BlockNumber::Latest, + to_block: BlockNumber::Latest, + addresses: Default::default(), + topics: Default::default(), + updates: Default::default(), + }; + + let latest_block_number = U64::from(2); + let matched = filter.matches( + &LogBuilder::new().set_block(U64::from(1)).build(), + latest_block_number, + ); + assert!(!matched); + } + + #[test] + fn test_filter_from_block_accepts_all_block_numbers_gte_input_number() { + let input_block_number = 2u64; + let latest_block_number = 100u64; + let filter = LogFilter { + from_block: BlockNumber::Number(U64::from(input_block_number)), + to_block: BlockNumber::Latest, + addresses: Default::default(), + topics: Default::default(), + updates: Default::default(), + }; + + for log_block in input_block_number..=input_block_number + 1 { + let matched = filter.matches( + &LogBuilder::new().set_block(U64::from(log_block)).build(), + U64::from(latest_block_number), + ); + assert!( + matched, + "failed matching log for block_number {}", + log_block + ); + } + } + + #[test] + fn test_filter_from_block_rejects_block_number_lt_input_number() { + let input_block_number = 2u64; + let latest_block_number = 100u64; + let filter = LogFilter { + from_block: BlockNumber::Number(U64::from(input_block_number)), + to_block: BlockNumber::Latest, + addresses: Default::default(), + topics: Default::default(), + updates: Default::default(), + }; + + let matched = filter.matches( + &LogBuilder::new() + .set_block(U64::from(input_block_number - 1)) + .build(), + U64::from(latest_block_number), + ); + assert!(!matched); + } + + #[test] + fn test_filter_to_block_latest_accepts_block_number_lte_latest() { + let filter = LogFilter { + from_block: BlockNumber::Earliest, + to_block: BlockNumber::Latest, + addresses: Default::default(), + topics: Default::default(), + updates: Default::default(), + }; + + let latest_block_number = 2u32; + for log_block in 1..=latest_block_number { + let matched = filter.matches( + &LogBuilder::new().set_block(U64::from(log_block)).build(), + U64::from(latest_block_number), + ); + assert!( + matched, + "failed matching log for block_number {}", + log_block + ); + } + } + + #[test] + fn test_filter_to_block_accepts_all_block_numbers_lte_input_number() { + let input_block_number = 2u64; + let latest_block_number = 100u64; + let filter = LogFilter { + from_block: BlockNumber::Earliest, + to_block: BlockNumber::Number(U64::from(input_block_number)), + addresses: Default::default(), + topics: Default::default(), + updates: Default::default(), + }; + + for log_block in input_block_number - 1..=input_block_number { + let matched = filter.matches( + &LogBuilder::new().set_block(U64::from(log_block)).build(), + U64::from(latest_block_number), + ); + assert!( + matched, + "failed matching log for block_number {}", + log_block + ); + } + } + + #[test] + fn test_filter_to_block_rejects_block_number_gt_input_number() { + let input_block_number = 2u64; + let latest_block_number = 100u64; + let filter = LogFilter { + from_block: BlockNumber::Earliest, + to_block: BlockNumber::Number(U64::from(input_block_number)), + addresses: Default::default(), + topics: Default::default(), + updates: Default::default(), + }; + + let matched = filter.matches( + &LogBuilder::new() + .set_block(U64::from(input_block_number + 1)) + .build(), + U64::from(latest_block_number), + ); + assert!(!matched); + } + + #[test] + fn test_filter_from_and_to_block_rejects_block_number_left_of_range() { + let input_from_block_number = 2u64; + let input_to_block_number = 4u64; + let latest_block_number = 100u64; + let filter = LogFilter { + from_block: BlockNumber::Number(U64::from(input_from_block_number)), + to_block: BlockNumber::Number(U64::from(input_to_block_number)), + addresses: Default::default(), + topics: Default::default(), + updates: Default::default(), + }; + + let matched = filter.matches( + &LogBuilder::new() + .set_block(U64::from(input_from_block_number - 1)) + .build(), + U64::from(latest_block_number), + ); + assert!(!matched); + } + + #[test] + fn test_filter_from_and_to_block_rejects_block_number_right_of_range() { + let input_from_block_number = 2u64; + let input_to_block_number = 4u64; + let latest_block_number = 100u64; + let filter = LogFilter { + from_block: BlockNumber::Number(U64::from(input_from_block_number)), + to_block: BlockNumber::Number(U64::from(input_to_block_number)), + addresses: Default::default(), + topics: Default::default(), + updates: Default::default(), + }; + + let matched = filter.matches( + &LogBuilder::new() + .set_block(U64::from(input_to_block_number + 1)) + .build(), + U64::from(latest_block_number), + ); + assert!(!matched); + } + + #[test] + fn test_filter_from_and_to_block_accepts_block_number_inclusive_of_range() { + let input_from_block_number = 2u64; + let input_to_block_number = 4u64; + let latest_block_number = 100u64; + let filter = LogFilter { + from_block: BlockNumber::Number(U64::from(input_from_block_number)), + to_block: BlockNumber::Number(U64::from(input_to_block_number)), + addresses: Default::default(), + topics: Default::default(), + updates: Default::default(), + }; + + for log_block in input_from_block_number..=input_to_block_number { + let matched = filter.matches( + &LogBuilder::new().set_block(U64::from(log_block)).build(), + U64::from(latest_block_number), + ); + assert!( + matched, + "failed matching log for block_number {}", + log_block + ); + } + } + + #[test] + fn test_filter_address_rejects_if_address_not_in_set() { + let filter = LogFilter { + from_block: BlockNumber::Earliest, + to_block: BlockNumber::Latest, + addresses: vec![H160::repeat_byte(0xa)], + topics: Default::default(), + updates: Default::default(), + }; + + let matched = filter.matches( + &LogBuilder::new() + .set_address(H160::repeat_byte(0x1)) + .build(), + U64::from(100), + ); + assert!(!matched); + } + + #[test] + fn test_filter_address_accepts_if_address_in_set() { + let filter = LogFilter { + from_block: BlockNumber::Earliest, + to_block: BlockNumber::Latest, + addresses: vec![H160::repeat_byte(0x1)], + topics: Default::default(), + updates: Default::default(), + }; + + let matched = filter.matches( + &LogBuilder::new() + .set_address(H160::repeat_byte(0x1)) + .build(), + U64::from(100), + ); + assert!(matched); + } + + #[test] + fn test_filter_address_accepts_if_address_set_empty() { + let filter = LogFilter { + from_block: BlockNumber::Earliest, + to_block: BlockNumber::Latest, + addresses: vec![], + topics: Default::default(), + updates: Default::default(), + }; + + let matched = filter.matches( + &LogBuilder::new() + .set_address(H160::repeat_byte(0x1)) + .build(), + U64::from(100), + ); + assert!(matched); + } + + #[test] + fn test_filter_topic_none_accepts_any_topic() { + let filter = LogFilter { + from_block: BlockNumber::Earliest, + to_block: BlockNumber::Latest, + addresses: Default::default(), + topics: [None, None, None, None], + updates: Default::default(), + }; + + for topic_idx in 0..4 { + let mut input_topics = [H256::zero(); 4].to_vec(); + input_topics[topic_idx] = H256::repeat_byte(0x1); + + let matched = filter.matches( + &LogBuilder::new().set_topics(input_topics).build(), + U64::from(100), + ); + assert!(matched, "failed matching log for topic index {}", topic_idx); + } + } + + #[test] + fn test_filter_topic_exactly_one_accepts_exact_topic() { + for topic_idx in 0..4 { + let match_topic = H256::repeat_byte(0x1); + let mut topics = [None, None, None, None]; + topics[topic_idx] = Some(hashset! { match_topic }); + let filter = LogFilter { + from_block: BlockNumber::Earliest, + to_block: BlockNumber::Latest, + addresses: Default::default(), + topics, + updates: Default::default(), + }; + let mut input_topics = [H256::zero(); 4].to_vec(); + input_topics[topic_idx] = match_topic; + + let matched = filter.matches( + &LogBuilder::new().set_topics(input_topics).build(), + U64::from(100), + ); + assert!(matched, "failed matching log for topic index {}", topic_idx); + } + } + + #[test] + fn test_filter_topic_multiple_accepts_either_topic() { + for topic_idx in 0..4 { + let match_topic_1 = H256::repeat_byte(0x1); + let match_topic_2 = H256::repeat_byte(0x2); + let mut topics = [None, None, None, None]; + topics[topic_idx] = Some(hashset! { match_topic_1, match_topic_2, }); + let filter = LogFilter { + from_block: BlockNumber::Earliest, + to_block: BlockNumber::Latest, + addresses: Default::default(), + topics, + updates: Default::default(), + }; + + let mut input_topics = [H256::zero(); 4].to_vec(); + input_topics[topic_idx] = match_topic_1; + let matched = filter.matches( + &LogBuilder::new().set_topics(input_topics).build(), + U64::from(100), + ); + assert!( + matched, + "failed matching log for topic index {} for {}", + topic_idx, match_topic_1 + ); + + let mut input_topics = [H256::zero(); 4].to_vec(); + input_topics[topic_idx] = match_topic_2; + let matched = filter.matches( + &LogBuilder::new().set_topics(input_topics).build(), + U64::from(100), + ); + assert!( + matched, + "failed matching log for topic index {} for {}", + topic_idx, match_topic_2 + ); + } + } + + #[test] + fn test_filter_topic_exactly_one_rejects_different_topic() { + for topic_idx in 0..4 { + let match_topic = H256::repeat_byte(0x1); + let mut topics = [None, None, None, None]; + topics[topic_idx] = Some(hashset! { match_topic }); + let filter = LogFilter { + from_block: BlockNumber::Earliest, + to_block: BlockNumber::Latest, + addresses: Default::default(), + topics, + updates: Default::default(), + }; + let mut input_topics = [H256::zero(); 4].to_vec(); + input_topics[topic_idx] = H256::repeat_byte(0xa); + + let matched = filter.matches( + &LogBuilder::new().set_topics(input_topics).build(), + U64::from(100), + ); + assert!( + !matched, + "erroneously matched log for topic index {}", + topic_idx + ); + } + } + + #[test] + fn test_filter_topic_multiple_rejects_different_topic() { + for topic_idx in 0..4 { + let match_topic_1 = H256::repeat_byte(0x1); + let match_topic_2 = H256::repeat_byte(0x2); + let mut topics = [None, None, None, None]; + topics[topic_idx] = Some(hashset! { match_topic_1, match_topic_2, }); + let filter = LogFilter { + from_block: BlockNumber::Earliest, + to_block: BlockNumber::Latest, + addresses: Default::default(), + topics, + updates: Default::default(), + }; + + let mut input_topics = [H256::zero(); 4].to_vec(); + input_topics[topic_idx] = H256::repeat_byte(0xa); + let matched = filter.matches( + &LogBuilder::new().set_topics(input_topics).build(), + U64::from(100), + ); + assert!( + !matched, + "erroneously matched log for topic index {}", + topic_idx + ); + } + } + + #[test] + fn test_filter_topic_combination_accepts_if_all_topics_valid() { + let filter = LogFilter { + from_block: BlockNumber::Earliest, + to_block: BlockNumber::Latest, + addresses: Default::default(), + topics: [ + None, // matches anything + Some(hashset! { H256::repeat_byte(0x1) }), + Some(hashset! { H256::repeat_byte(0x2), H256::repeat_byte(0x3) }), + Some(hashset! {}), // matches anything + ], + updates: Default::default(), + }; + + let matched = filter.matches( + &LogBuilder::new() + .set_topics(vec![ + H256::zero(), + H256::repeat_byte(0x1), + H256::repeat_byte(0x3), + H256::repeat_byte(0xa), + ]) + .build(), + U64::from(100), + ); + assert!(matched); + } + + #[test] + fn test_filter_topic_combination_rejects_if_any_topic_is_invalid() { + let filter = LogFilter { + from_block: BlockNumber::Earliest, + to_block: BlockNumber::Latest, + addresses: Default::default(), + topics: [ + None, // matches anything + Some(hashset! { H256::repeat_byte(0x1) }), + Some(hashset! { H256::repeat_byte(0x2), H256::repeat_byte(0x3) }), + Some(hashset! {}), // matches anything + ], + updates: Default::default(), + }; + + let matched = filter.matches( + &LogBuilder::new() + .set_topics(vec![ + H256::zero(), + H256::repeat_byte(0xf), // invalid + H256::repeat_byte(0x3), + H256::repeat_byte(0xa), + ]) + .build(), + U64::from(100), + ); + assert!(!matched, "erroneously matched on invalid topic1"); + + let matched = filter.matches( + &LogBuilder::new() + .set_topics(vec![ + H256::zero(), + H256::repeat_byte(0x1), + H256::repeat_byte(0xf), // invalid + H256::repeat_byte(0xa), + ]) + .build(), + U64::from(100), + ); + assert!(!matched, "erroneously matched on invalid topic2"); + } + + #[test] + fn test_filter_combination_accepts_if_all_criteria_valid() { + let filter = LogFilter { + from_block: BlockNumber::Number(U64::from(2)), + to_block: BlockNumber::Number(U64::from(5)), + addresses: vec![H160::repeat_byte(0xab)], + topics: [ + None, + Some(hashset! { H256::repeat_byte(0x1) }), + Some(hashset! { H256::repeat_byte(0x2), H256::repeat_byte(0x3) }), + Some(hashset! {}), + ], + updates: Default::default(), + }; + + let matched = filter.matches( + &LogBuilder::new() + .set_block(U64::from(4)) + .set_address(H160::repeat_byte(0xab)) + .set_topics(vec![ + H256::zero(), + H256::repeat_byte(0x1), + H256::repeat_byte(0x3), + H256::repeat_byte(0xa), + ]) + .build(), + U64::from(100), + ); + assert!(matched); + } + + #[test] + fn test_filter_combination_rejects_if_any_criterion_invalid() { + let filter = LogFilter { + from_block: BlockNumber::Number(U64::from(2)), + to_block: BlockNumber::Number(U64::from(5)), + addresses: vec![H160::repeat_byte(0xab)], + topics: [Some(hashset! { H256::repeat_byte(0x1) }), None, None, None], + updates: Default::default(), + }; + + let matched = filter.matches( + &LogBuilder::new() + .set_block(U64::from(1)) // invalid + .set_address(H160::repeat_byte(0xab)) + .set_topics(vec![ + H256::zero(), + H256::repeat_byte(0x1), + H256::repeat_byte(0x3), + H256::repeat_byte(0xa), + ]) + .build(), + U64::from(100), + ); + assert!(!matched, "erroneously matched on invalid block number"); + + let matched = filter.matches( + &LogBuilder::new() + .set_block(U64::from(1)) + .set_address(H160::repeat_byte(0xde)) // invalid + .set_topics(vec![ + H256::zero(), + H256::repeat_byte(0x1), + H256::repeat_byte(0x3), + H256::repeat_byte(0xa), + ]) + .build(), + U64::from(100), + ); + assert!(!matched, "erroneously matched on invalid address"); + + let matched = filter.matches( + &LogBuilder::new() + .set_block(U64::from(1)) + .set_address(H160::repeat_byte(0xde)) + .set_topics(vec![H256::zero(), H256::zero(), H256::zero(), H256::zero()]) // invalid + .build(), + U64::from(100), + ); + assert!(!matched, "erroneously matched on invalid topic"); + } +} diff --git a/src/node.rs b/src/node.rs index c0585a52..4b1d454e 100644 --- a/src/node.rs +++ b/src/node.rs @@ -2717,4 +2717,158 @@ mod tests { ); } } + + #[tokio::test] + async fn test_new_block_filter_returns_filter_id() { + let node = InMemoryNode::::default(); + + let actual_filter_id = node + .new_block_filter() + .await + .expect("failed creating filter"); + + assert_eq!(U256::from(1), actual_filter_id); + } + + #[tokio::test] + async fn test_new_filter_returns_filter_id() { + let node = InMemoryNode::::default(); + + let actual_filter_id = node + .new_filter(Filter::default()) + .await + .expect("failed creating filter"); + + assert_eq!(U256::from(1), actual_filter_id); + } + + #[tokio::test] + async fn test_new_pending_transaction_filter_returns_filter_id() { + let node = InMemoryNode::::default(); + + let actual_filter_id = node + .new_pending_transaction_filter() + .await + .expect("failed creating filter"); + + assert_eq!(U256::from(1), actual_filter_id); + } + + #[tokio::test] + async fn test_uninstall_filter_returns_true_if_filter_exists() { + let node = InMemoryNode::::default(); + let filter_id = node + .new_block_filter() + .await + .expect("failed creating filter"); + + let actual_result = node + .uninstall_filter(filter_id) + .await + .expect("failed creating filter"); + + assert!(actual_result); + } + + #[tokio::test] + async fn test_uninstall_filter_returns_false_if_filter_does_not_exist() { + let node = InMemoryNode::::default(); + + let actual_result = node + .uninstall_filter(U256::from(100)) + .await + .expect("failed creating filter"); + + assert!(!actual_result); + } + + #[tokio::test] + async fn test_get_filter_changes_returns_block_hash_updates_only_once() { + let node = InMemoryNode::::default(); + let filter_id = node + .new_block_filter() + .await + .expect("failed creating filter"); + let block_hash = testing::apply_tx(&node, H256::repeat_byte(0x1)); + + match node + .get_filter_changes(filter_id) + .await + .expect("failed getting filter changes") + { + FilterChanges::Hashes(result) => assert_eq!(vec![block_hash], result), + changes => panic!("unexpected filter changes: {:?}", changes), + } + + match node + .get_filter_changes(filter_id) + .await + .expect("failed getting filter changes") + { + FilterChanges::Empty(_) => (), + changes => panic!("expected no changes in the second call, got {:?}", changes), + } + } + + #[tokio::test] + async fn test_get_filter_changes_returns_log_updates_only_once() { + let node = InMemoryNode::::default(); + let filter_id = node + .new_filter(Filter { + from_block: None, + to_block: None, + address: None, + topics: None, + block_hash: None, + }) + .await + .expect("failed creating filter"); + testing::apply_tx(&node, H256::repeat_byte(0x1)); + + match node + .get_filter_changes(filter_id) + .await + .expect("failed getting filter changes") + { + FilterChanges::Logs(result) => assert_eq!(3, result.len()), + changes => panic!("unexpected filter changes: {:?}", changes), + } + + match node + .get_filter_changes(filter_id) + .await + .expect("failed getting filter changes") + { + FilterChanges::Empty(_) => (), + changes => panic!("expected no changes in the second call, got {:?}", changes), + } + } + + #[tokio::test] + async fn test_get_filter_changes_returns_pending_transaction_updates_only_once() { + let node = InMemoryNode::::default(); + let filter_id = node + .new_pending_transaction_filter() + .await + .expect("failed creating filter"); + testing::apply_tx(&node, H256::repeat_byte(0x1)); + + match node + .get_filter_changes(filter_id) + .await + .expect("failed getting filter changes") + { + FilterChanges::Hashes(result) => assert_eq!(vec![H256::repeat_byte(0x1)], result), + changes => panic!("unexpected filter changes: {:?}", changes), + } + + match node + .get_filter_changes(filter_id) + .await + .expect("failed getting filter changes") + { + FilterChanges::Empty(_) => (), + changes => panic!("expected no changes in the second call, got {:?}", changes), + } + } } diff --git a/src/testing.rs b/src/testing.rs index 24f6dc8c..621e18ad 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -15,6 +15,8 @@ use httptest::{ }; use itertools::Itertools; use std::str::FromStr; +use zksync_basic_types::{H160, U64}; +use zksync_types::api::Log; use zksync_types::{fee::Fee, l2::L2Tx, Address, L2ChainId, Nonce, PackedEthSignature, H256, U256}; /// Configuration for the [MockServer]'s initial block. @@ -386,6 +388,57 @@ pub fn apply_tx(node: &InMemoryNode, tx_hash produced_block_hash } +/// Builds transaction logs +#[derive(Debug, Default, Clone)] +pub struct LogBuilder { + block_number: U64, + address: Option, + topics: Option>, +} + +impl LogBuilder { + /// Create a new instance of [LogBuilder] + pub fn new() -> Self { + Self::default() + } + + /// Sets the log's block number + pub fn set_block(&mut self, number: U64) -> &mut Self { + self.block_number = number; + self + } + + /// Sets the log address + pub fn set_address(&mut self, address: H160) -> &mut Self { + self.address = Some(address); + self + } + + /// Sets the log topics + pub fn set_topics(&mut self, topics: Vec) -> &mut Self { + self.topics = Some(topics); + self + } + + /// Builds the [Log] object + pub fn build(&mut self) -> Log { + Log { + address: self.address.clone().unwrap_or_default(), + topics: self.topics.clone().unwrap_or_default(), + data: Default::default(), + block_hash: Some(H256::zero()), + block_number: Some(self.block_number), + l1_batch_number: Default::default(), + transaction_hash: Default::default(), + transaction_index: Default::default(), + log_index: Default::default(), + transaction_log_index: Default::default(), + log_type: Default::default(), + removed: Default::default(), + } + } +} + mod test { use super::*; use crate::http_fork_source::HttpForkSource; @@ -493,4 +546,42 @@ mod test { "block was not produced" ); } + + #[test] + fn test_log_builder_set_block() { + let log = LogBuilder::new().set_block(U64::from(2)).build(); + + assert_eq!(Some(U64::from(2)), log.block_number); + } + + #[test] + fn test_log_builder_set_address() { + let log = LogBuilder::new() + .set_address(H160::repeat_byte(0x1)) + .build(); + + assert_eq!(H160::repeat_byte(0x1), log.address); + } + + #[test] + fn test_log_builder_set_topics() { + let log = LogBuilder::new() + .set_topics(vec![ + H256::repeat_byte(0x1), + H256::repeat_byte(0x2), + H256::repeat_byte(0x3), + H256::repeat_byte(0x4), + ]) + .build(); + + assert_eq!( + vec![ + H256::repeat_byte(0x1), + H256::repeat_byte(0x2), + H256::repeat_byte(0x3), + H256::repeat_byte(0x4), + ], + log.topics + ); + } }