yellowstone_grpc_proto/cuckoo/set.rs
1//! Safe, tracked wrapper around [`CuckooFilter`] for client-side filter construction.
2//!
3//! [`CompressedAccountFilterSet`] is the primary user-facing type for building cuckoo filters to
4//! send in subscribe requests. It maintains an exact-membership [`HashSet`]
5//! alongside the probabilistic filter, which:
6//!
7//! - **Makes `remove` safe**: the filter is only mutated for items actually
8//! present, avoiding the footgun where removing an un-inserted item clears
9//! a different item sharing its fingerprint.
10//! - **Makes `contains` exact**: client-side membership checks return the
11//! true answer, not a probabilistic one.
12//!
13//! The probabilistic filter is only used on the wire. Server-side matching
14//! accepts false positives (clients filter locally on receipt).
15//!
16//! [`HashSet`]: std::collections::HashSet
17
18use {
19 super::{
20 error::{CuckooBuildError, TableFullError},
21 filter::CuckooFilter,
22 },
23 crate::geyser::{
24 CuckooFilter as ProtoCuckooFilter, SubscribeRequest, SubscribeRequestFilterAccounts,
25 SubscribeRequestFilterBlocks, SubscribeRequestFilterTransactions,
26 },
27 solana_pubkey::Pubkey,
28 std::collections::HashSet,
29};
30
31/// A HashMap-like wrapper around [`CuckooFilter`] for safe filter construction.
32///
33/// Maintains two parallel collections:
34/// - A [`HashSet`] as the source of truth for exact membership
35/// - A [`CuckooFilter`] as the compact wire representation
36///
37/// Writes go to both; reads (`contains`, `len`) answer from the [`HashSet`];
38/// serialization reads from the [`CuckooFilter`]. This gives users exact local
39/// semantics while producing a compact probabilistic filter for the server.
40///
41/// # When to Use
42///
43/// Use [`CompressedAccountFilterSet`] whenever you need to build a cuckoo filter to send in a
44/// subscribe request. It is strictly safer than constructing a [`CuckooFilter`]
45/// directly — the filter's `remove` method has a footgun (it can silently remove
46/// the wrong item when fingerprints collide), and [`CompressedAccountFilterSet`] guards against it.
47///
48/// # Example
49///
50/// ```
51/// use {
52/// solana_pubkey::Pubkey,
53/// yellowstone_grpc_proto::cuckoo::CompressedAccountFilterSet,
54/// };
55///
56/// let mut map = CompressedAccountFilterSet::with_capacity(1000).unwrap();
57/// map.insert(Pubkey::new_from_array([42u8; 32])).unwrap();
58/// map.insert(Pubkey::new_from_array([100u8; 32])).unwrap();
59/// assert!(map.contains(Pubkey::new_from_array([42u8; 32])));
60/// assert_eq!(map.len(), 2);
61///
62/// map.remove(Pubkey::new_from_array([42u8; 32]));
63/// assert!(!map.contains(Pubkey::new_from_array([42u8; 32])));
64/// ```
65/// [`HashSet`]: std::collections::HashSet
66pub struct CompressedAccountFilterSet {
67 items: HashSet<[u8; 32]>,
68 filter: CuckooFilter<[u8; 32]>,
69 dirty: bool,
70}
71
72impl CompressedAccountFilterSet {
73 /// Creates an empty map pre-sized for up to `max_capacity` items.
74 ///
75 /// Both the internal [`HashSet`] and [`CuckooFilter`] are allocated up front
76 /// to avoid rehashing or reallocation during inserts.
77 ///
78 /// # Errors
79 ///
80 /// Returns [`CuckooBuildError::CapacityOverflow`] if `max_capacity` exceeds
81 /// what the system can allocate, or if the underlying cuckoo filter cannot
82 /// be built at the requested size.
83 ///
84 /// # Example
85 ///
86 /// ```
87 /// use yellowstone_grpc_proto::cuckoo::CompressedAccountFilterSet;
88 ///
89 /// let map = CompressedAccountFilterSet::with_capacity(10_000).unwrap();
90 /// assert!(map.is_empty());
91 /// ```
92 ///
93 /// [`HashSet`]: std::collections::HashSet
94 pub fn with_capacity(max_capacity: usize) -> Result<Self, CuckooBuildError> {
95 let filter = CuckooFilter::with_capacity(max_capacity)?;
96
97 let mut items: HashSet<[u8; 32]> = HashSet::new();
98 items
99 .try_reserve(max_capacity)
100 .map_err(|_| CuckooBuildError::CapacityOverflow)?;
101
102 Ok(Self {
103 items,
104 filter,
105 dirty: false,
106 })
107 }
108
109 /// Inserts an item into the map.
110 ///
111 /// Returns `Ok(true)` if the item was newly inserted, `Ok(false)` if it was
112 /// already present (idempotent).
113 ///
114 /// # Errors
115 ///
116 /// Returns [`TableFullError`] if the underlying cuckoo filter is saturated and
117 /// cannot accommodate the item. This typically means the map was under-sized
118 /// for the workload; rebuild with a larger `max_capacity`.
119 ///
120 /// On error, the map's state is unchanged — neither the [`HashSet`] nor the
121 /// [`CuckooFilter`] is mutated.
122 ///
123 /// [`HashSet`]: std::collections::HashSet
124 pub fn insert(&mut self, key: Pubkey) -> Result<bool, TableFullError> {
125 let bytes = key.to_bytes();
126
127 if self.items.contains(&bytes) {
128 return Ok(false);
129 }
130 self.filter.insert(&bytes)?;
131 self.items.insert(bytes);
132 self.dirty = true;
133 Ok(true)
134 }
135
136 /// Removes an item from the map.
137 ///
138 /// Returns `true` if the item was present and removed, `false` if it was not
139 /// in the map.
140 ///
141 /// # Safety over [`CuckooFilter::remove`]
142 ///
143 /// The underlying [`CuckooFilter`] has a known footgun: removing an item that
144 /// was never inserted can silently remove a different item that shares the
145 /// same fingerprint. [`CompressedAccountFilterSet`] prevents this by checking its internal
146 /// [`HashSet`] first and only touching the filter when the item is genuinely
147 /// present.
148 ///
149 /// [`HashSet`]: std::collections::HashSet
150 pub fn remove(&mut self, key: Pubkey) -> bool {
151 let bytes = key.to_bytes();
152
153 if self.items.remove(&bytes) {
154 self.filter.remove(&bytes);
155 self.dirty = true;
156 true
157 } else {
158 false
159 }
160 }
161
162 /// Checks if an item is in the map.
163 ///
164 /// Unlike [`CuckooFilter::contains`], this returns an exact answer — the
165 /// [`HashSet`] guarantees no false positives.
166 ///
167 /// [`HashSet`]: std::collections::HashSet
168 pub fn contains(&self, key: Pubkey) -> bool {
169 self.items.contains(&key.to_bytes())
170 }
171
172 /// Returns the number of items in the map.
173 pub fn len(&self) -> usize {
174 self.items.len()
175 }
176
177 /// Returns the number of items the map can hold without reallocating.
178 ///
179 /// Typically larger than the `max_capacity` passed to [`with_capacity`],
180 /// the underlying [`HashSet`] rounds allocation up to its own sizing
181 /// policy. Use this to check remaining headroom before a batch of inserts
182 /// or to report occupancy alongside [`len`].
183 ///
184 /// ```
185 /// use yellowstone_grpc_proto::cuckoo::CompressedAccountFilterSet;
186 ///
187 /// let map = CompressedAccountFilterSet::with_capacity(1000).unwrap();
188 /// assert!(map.capacity() >= 1000);
189 /// ```
190 ///
191 /// [`with_capacity`]: CompressedAccountFilterSet::with_capacity
192 /// [`len`]: CompressedAccountFilterSet::len
193 /// [`HashSet`]: std::collections::HashSet
194 pub fn capacity(&self) -> usize {
195 self.items.capacity()
196 }
197
198 /// Returns an iterator over the items in the map in arbitrary order.
199 ///
200 /// Reads from the internal [`HashSet`], so iteration order matches
201 /// `HashSet` semantics; no ordering guarantee, and two calls on the
202 /// same map may yield items in different orders.
203 ///
204 /// ```
205 /// use {
206 /// solana_pubkey::Pubkey,
207 /// yellowstone_grpc_proto::cuckoo::CompressedAccountFilterSet,
208 /// };
209 ///
210 /// let mut map = CompressedAccountFilterSet::with_capacity(100).unwrap();
211 /// map.insert(Pubkey::new_from_array([1u8; 32])).unwrap();
212 /// map.insert(Pubkey::new_from_array([2u8; 32])).unwrap();
213 /// map.insert(Pubkey::new_from_array([3u8; 32])).unwrap();
214 ///
215 /// let count = map.iter().count();
216 /// assert_eq!(count, 3);
217 /// ```
218 ///
219 /// [`HashSet`]: std::collections::HashSet
220 pub fn iter(&self) -> impl Iterator<Item = &[u8; 32]> {
221 self.items.iter()
222 }
223
224 /// Returns `true` if the map contains no items.
225 pub fn is_empty(&self) -> bool {
226 self.items.is_empty()
227 }
228
229 /// Returns `true` if the map has been mutated since the last call to
230 /// [`take_dirty`] (or since construction).
231 ///
232 /// Use this to check whether the filter needs to be re-sent without
233 /// clearing the flag.
234 pub const fn is_dirty(&self) -> bool {
235 self.dirty
236 }
237
238 /// Returns the dirty flag and clears it.
239 ///
240 /// Call this when transmitting the filter: if it returns `true`, rebuild
241 /// and send; if `false`, skip the send. Clearing means subsequent
242 /// mutations will flip the flag back to `true` for the next cycle.
243 ///
244 /// # Example
245 ///
246 /// ```
247 /// use {
248 /// solana_pubkey::Pubkey,
249 /// yellowstone_grpc_proto::cuckoo::CompressedAccountFilterSet,
250 /// };
251 ///
252 /// let mut map = CompressedAccountFilterSet::with_capacity(100).unwrap();
253 /// assert!(!map.take_dirty()); // fresh map is clean
254 ///
255 /// map.insert(Pubkey::new_from_array([42u8; 32])).unwrap();
256 /// assert!(map.take_dirty()); // mutation flipped it
257 /// assert!(!map.take_dirty()); // and clearing it takes effect
258 /// ```
259 pub const fn take_dirty(&mut self) -> bool {
260 std::mem::replace(&mut self.dirty, false)
261 }
262
263 /// Serializes the underlying cuckoo filter to its proto wire format.
264 ///
265 /// The returned proto carries all parameters needed for deserialization on
266 /// the server side or in cross-language clients: bucket count, entries per
267 /// bucket, fingerprint bits, and the hash seed.
268 pub fn to_proto(&self) -> ProtoCuckooFilter {
269 ProtoCuckooFilter::from(&self.filter)
270 }
271
272 /// Returns a `SubscribeRequestFilterAccounts` that carries only this cuckoo
273 /// filter in essence no explicit account list, no owner, no predicates.
274 ///
275 /// Use this when you want to add the cuckoo filter to a subscribe request
276 /// yourself, under a name of your choosing, alongside other account filters
277 /// or other subscription types:
278 ///
279 /// ```no_run
280 /// use {
281 /// solana_pubkey::Pubkey,
282 /// yellowstone_grpc_proto::{cuckoo::CompressedAccountFilterSet, geyser::SubscribeRequest},
283 /// };
284 ///
285 /// let mut map = CompressedAccountFilterSet::with_capacity(1000).unwrap();
286 /// map.insert(Pubkey::new_from_array([1u8; 32])).unwrap();
287 ///
288 /// let mut req = SubscribeRequest::default();
289 /// req.accounts.insert("my_cuckoo".to_string(), map.to_account_filter());
290 /// ```
291 pub fn to_account_filter(&self) -> SubscribeRequestFilterAccounts {
292 SubscribeRequestFilterAccounts {
293 account: vec![],
294 owner: vec![],
295 filters: vec![],
296 nonempty_txn_signature: None,
297 cuckoo_accounts_filter: Some(self.to_proto()),
298 }
299 }
300
301 /// Returns a [`SubscribeRequestFilterBlocks`] that carries only this cuckoo
302 /// filter, no explicit account include list, no transaction or entry options set.
303 ///
304 /// Use this when you want to subscribe to full blocks but filter the
305 /// transactions and accounts inside them by the cuckoo set:
306 ///
307 /// ```no_run
308 /// use {
309 /// solana_pubkey::Pubkey,
310 /// yellowstone_grpc_proto::{cuckoo::CompressedAccountFilterSet, geyser::SubscribeRequest},
311 /// };
312 ///
313 /// let mut map = CompressedAccountFilterSet::with_capacity(1000).unwrap();
314 /// map.insert(Pubkey::new_from_array([1u8; 32])).unwrap();
315 ///
316 /// let mut req = SubscribeRequest::default();
317 /// req.blocks.insert("blocks".to_string(), map.to_block_filter());
318 /// ```
319 pub fn to_block_filter(&self) -> SubscribeRequestFilterBlocks {
320 SubscribeRequestFilterBlocks {
321 account_include: vec![],
322 include_transactions: None,
323 include_accounts: None,
324 include_entries: None,
325 cuckoo_account_include: Some(self.to_proto()),
326 }
327 }
328
329 /// Returns a [`SubscribeRequestFilterTransactions`] that carries only this
330 /// cuckoo filter, no explicit account include list, no vote/failed/signature
331 /// predicates, no token account expansion.
332 ///
333 /// Use this when you want full transaction details for a large address set
334 /// without sending every pubkey on the wire. The same filter works for
335 /// `transactions` and `transactions_status`:
336 ///
337 /// ```no_run
338 /// use {
339 /// solana_pubkey::Pubkey,
340 /// yellowstone_grpc_proto::{cuckoo::CompressedAccountFilterSet, geyser::SubscribeRequest},
341 /// };
342 ///
343 /// let mut map = CompressedAccountFilterSet::with_capacity(1000).unwrap();
344 /// map.insert(Pubkey::new_from_array([1u8; 32])).unwrap();
345 ///
346 /// let mut req = SubscribeRequest::default();
347 /// req.transactions.insert("txs".to_string(), map.to_transaction_filter());
348 /// ```
349 pub fn to_transaction_filter(&self) -> SubscribeRequestFilterTransactions {
350 SubscribeRequestFilterTransactions {
351 vote: None,
352 failed: None,
353 signature: None,
354 account_include: vec![],
355 account_exclude: vec![],
356 account_required: vec![],
357 cuckoo_account_include: Some(self.to_proto()),
358 token_accounts: None,
359 }
360 }
361
362 /// Inserts this cuckoo filter into `req.accounts` under the given name.
363 ///
364 /// Existing entries in `req.accounts` are preserved. If an entry already
365 /// exists under `name`, it is replaced. Other fields of `req` (transactions,
366 /// blocks, slots, etc.) are untouched.
367 ///
368 /// Marks the map as clean — subsequent mutations will flip the dirty flag
369 /// back to `true` for the next transmission cycle.
370 ///
371 /// # Example
372 ///
373 /// ```no_run
374 /// use {
375 /// solana_pubkey::Pubkey,
376 /// yellowstone_grpc_proto::{cuckoo::CompressedAccountFilterSet, geyser::SubscribeRequest},
377 /// };
378 ///
379 /// let mut map = CompressedAccountFilterSet::with_capacity(1000).unwrap();
380 /// map.insert(Pubkey::new_from_array([1u8; 32])).unwrap();
381 ///
382 /// let mut req = SubscribeRequest::default();
383 /// map.insert_into_subscribe_request(&mut req, "tracked_accounts");
384 /// // req.accounts["tracked_accounts"] now carries the cuckoo filter
385 /// ```
386 pub fn insert_into_subscribe_request(&mut self, req: &mut SubscribeRequest, name: &str) {
387 req.accounts
388 .insert(name.to_string(), self.to_account_filter());
389 self.dirty = false;
390 }
391
392 /// Inserts this cuckoo filter into `req.blocks` under the given name.
393 ///
394 /// Existing entries in `req.blocks` are preserved. If an entry already
395 /// exists under `name`, it is replaced. Other fields of `req` are untouched.
396 ///
397 /// Marks the map as clean, subsequent mutations will flip the dirty flag
398 /// back to `true` for the next transmission cycle.
399 ///
400 /// # Example
401 ///
402 /// ```no_run
403 /// use {
404 /// solana_pubkey::Pubkey,
405 /// yellowstone_grpc_proto::{cuckoo::CompressedAccountFilterSet, geyser::SubscribeRequest},
406 /// };
407 ///
408 /// let mut map = CompressedAccountFilterSet::with_capacity(1000).unwrap();
409 /// map.insert(Pubkey::new_from_array([1u8; 32])).unwrap();
410 ///
411 /// let mut req = SubscribeRequest::default();
412 /// map.insert_into_block_subscribe_request(&mut req, "tracked_blocks");
413 /// ```
414 pub fn insert_into_block_subscribe_request(&mut self, req: &mut SubscribeRequest, name: &str) {
415 req.blocks.insert(name.to_string(), self.to_block_filter());
416 self.dirty = false;
417 }
418
419 /// Inserts this cuckoo filter into `req.transactions` under the given name.
420 ///
421 /// Existing entries in `req.transactions` are preserved. If an entry already
422 /// exists under `name`, it is replaced. Other fields of `req` are untouched.
423 ///
424 /// Marks the map as clean, subsequent mutations will flip the dirty flag
425 /// back to `true` for the next transmission cycle.
426 ///
427 /// # Example
428 ///
429 /// ```no_run
430 /// use {
431 /// solana_pubkey::Pubkey,
432 /// yellowstone_grpc_proto::{cuckoo::CompressedAccountFilterSet, geyser::SubscribeRequest},
433 /// };
434 ///
435 /// let mut map = CompressedAccountFilterSet::with_capacity(1000).unwrap();
436 /// map.insert(Pubkey::new_from_array([1u8; 32])).unwrap();
437 ///
438 /// let mut req = SubscribeRequest::default();
439 /// map.insert_into_transaction_subscribe_request(&mut req, "tracked_txs");
440 /// ```
441 pub fn insert_into_transaction_subscribe_request(
442 &mut self,
443 req: &mut SubscribeRequest,
444 name: &str,
445 ) {
446 req.transactions
447 .insert(name.to_string(), self.to_transaction_filter());
448 self.dirty = false;
449 }
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455
456 // helper: a 32-byte key from a single seed byte
457 fn key(b: u8) -> Pubkey {
458 Pubkey::new_from_array([b; 32])
459 }
460
461 #[test]
462 fn basic_insert_contains() {
463 let mut filter = CompressedAccountFilterSet::with_capacity(100).unwrap();
464 assert!(filter.insert(key(1)).unwrap());
465 assert!(filter.contains(key(1)));
466 assert!(!filter.contains(key(2)));
467 }
468
469 #[test]
470 fn insert_duplicate_returns_false() {
471 let mut filter = CompressedAccountFilterSet::with_capacity(100).unwrap();
472 assert!(filter.insert(key(1)).unwrap());
473 assert!(!filter.insert(key(1)).unwrap());
474 }
475
476 #[test]
477 fn remove_existing() {
478 let mut filter = CompressedAccountFilterSet::with_capacity(100).unwrap();
479 filter.insert(key(1)).unwrap();
480 assert!(filter.remove(key(1)));
481 assert!(!filter.contains(key(1)));
482 }
483
484 #[test]
485 fn remove_nonexistent_is_safe() {
486 let mut filter = CompressedAccountFilterSet::with_capacity(100).unwrap();
487 filter.insert(key(1)).unwrap();
488
489 assert!(!filter.remove(key(2)));
490 assert!(filter.contains(key(1)));
491 }
492
493 #[test]
494 fn len_and_is_empty() {
495 let mut filter = CompressedAccountFilterSet::with_capacity(100).unwrap();
496 assert!(filter.is_empty());
497 assert_eq!(filter.len(), 0);
498
499 filter.insert(key(1)).unwrap();
500 filter.insert(key(2)).unwrap();
501 assert!(!filter.is_empty());
502 assert_eq!(filter.len(), 2);
503
504 filter.remove(key(1));
505 assert_eq!(filter.len(), 1);
506 }
507
508 #[test]
509 fn to_proto_round_trip() {
510 let mut filter = CompressedAccountFilterSet::with_capacity(100).unwrap();
511 filter.insert(key(1)).unwrap();
512 filter.insert(key(2)).unwrap();
513
514 let proto = filter.to_proto();
515 assert!(!proto.data.is_empty());
516 assert!(proto.bucket_count > 0);
517 }
518
519 #[test]
520 fn to_account_filter_carries_cuckoo_and_no_other_matchers() {
521 let mut filter = CompressedAccountFilterSet::with_capacity(100).unwrap();
522 filter.insert(key(1)).unwrap();
523
524 let f = filter.to_account_filter();
525
526 assert!(f.cuckoo_accounts_filter.is_some());
527 assert!(f.account.is_empty());
528 assert!(f.owner.is_empty());
529 assert!(f.filters.is_empty());
530 assert_eq!(f.nonempty_txn_signature, None);
531 }
532
533 #[test]
534 fn insert_into_subscribe_request_uses_given_name_and_preserves_other_filters() {
535 let mut filter = CompressedAccountFilterSet::with_capacity(100).unwrap();
536 filter.insert(key(1)).unwrap();
537
538 let mut req = SubscribeRequest::default();
539 req.accounts.insert(
540 "pre_existing".to_string(),
541 SubscribeRequestFilterAccounts::default(),
542 );
543
544 filter.insert_into_subscribe_request(&mut req, "tracked_accounts");
545
546 assert!(req.accounts.contains_key("tracked_accounts"));
547 assert!(req.accounts.contains_key("pre_existing"));
548 assert_eq!(req.accounts.len(), 2);
549 assert!(req
550 .accounts
551 .get("tracked_accounts")
552 .unwrap()
553 .cuckoo_accounts_filter
554 .is_some());
555 }
556
557 #[test]
558 fn to_block_filter_carries_cuckoo_and_no_other_matchers() {
559 let mut filter = CompressedAccountFilterSet::with_capacity(100).unwrap();
560 filter.insert(key(1)).unwrap();
561
562 let f = filter.to_block_filter();
563
564 assert!(f.cuckoo_account_include.is_some());
565 assert!(f.account_include.is_empty());
566 assert_eq!(f.include_transactions, None);
567 assert_eq!(f.include_accounts, None);
568 assert_eq!(f.include_entries, None);
569 }
570
571 #[test]
572 fn insert_into_block_subscribe_request_uses_given_name_and_preserves_other_filters() {
573 let mut filter = CompressedAccountFilterSet::with_capacity(100).unwrap();
574 filter.insert(key(1)).unwrap();
575
576 let mut req = SubscribeRequest::default();
577 req.blocks.insert(
578 "pre_existing".to_string(),
579 SubscribeRequestFilterBlocks::default(),
580 );
581
582 filter.insert_into_block_subscribe_request(&mut req, "tracked_blocks");
583
584 assert!(req.blocks.contains_key("tracked_blocks"));
585 assert!(req.blocks.contains_key("pre_existing"));
586 assert_eq!(req.blocks.len(), 2);
587 assert!(req
588 .blocks
589 .get("tracked_blocks")
590 .unwrap()
591 .cuckoo_account_include
592 .is_some());
593 }
594
595 #[test]
596 fn insert_into_block_subscribe_request_clears_dirty_flag() {
597 let mut filter = CompressedAccountFilterSet::with_capacity(100).unwrap();
598 filter.insert(key(1)).unwrap();
599 assert!(filter.is_dirty());
600
601 let mut req = SubscribeRequest::default();
602 filter.insert_into_block_subscribe_request(&mut req, "blocks");
603
604 assert!(!filter.is_dirty());
605 }
606
607 #[test]
608 fn to_transaction_filter_carries_cuckoo_and_no_other_matchers() {
609 let mut filter = CompressedAccountFilterSet::with_capacity(100).unwrap();
610 filter.insert(key(1)).unwrap();
611
612 let f = filter.to_transaction_filter();
613
614 assert!(f.cuckoo_account_include.is_some());
615 assert!(f.account_include.is_empty());
616 assert!(f.account_exclude.is_empty());
617 assert!(f.account_required.is_empty());
618 assert_eq!(f.vote, None);
619 assert_eq!(f.failed, None);
620 assert_eq!(f.signature, None);
621 assert_eq!(f.token_accounts, None);
622 }
623
624 #[test]
625 fn insert_into_transaction_subscribe_request_uses_given_name_and_preserves_other_filters() {
626 let mut filter = CompressedAccountFilterSet::with_capacity(100).unwrap();
627 filter.insert(key(1)).unwrap();
628
629 let mut req = SubscribeRequest::default();
630 req.transactions.insert(
631 "pre_existing".to_string(),
632 SubscribeRequestFilterTransactions::default(),
633 );
634
635 filter.insert_into_transaction_subscribe_request(&mut req, "tracked_txs");
636
637 assert!(req.transactions.contains_key("tracked_txs"));
638 assert!(req.transactions.contains_key("pre_existing"));
639 assert_eq!(req.transactions.len(), 2);
640 assert!(req
641 .transactions
642 .get("tracked_txs")
643 .unwrap()
644 .cuckoo_account_include
645 .is_some());
646 }
647
648 #[test]
649 fn insert_into_transaction_subscribe_request_clears_dirty_flag() {
650 let mut filter = CompressedAccountFilterSet::with_capacity(100).unwrap();
651 filter.insert(key(1)).unwrap();
652 assert!(filter.is_dirty());
653
654 let mut req = SubscribeRequest::default();
655 filter.insert_into_transaction_subscribe_request(&mut req, "txs");
656
657 assert!(!filter.is_dirty());
658 }
659
660 #[test]
661 fn pubkey_like_usage() {
662 let mut filter = CompressedAccountFilterSet::with_capacity(1000).unwrap();
663
664 for i in 0..100u8 {
665 filter.insert(key(i)).unwrap();
666 }
667
668 assert_eq!(filter.len(), 100);
669
670 for i in 0..100u8 {
671 assert!(filter.contains(key(i)));
672 }
673
674 assert!(!filter.contains(key(255)));
675 }
676
677 #[test]
678 fn capacity_overflow() {
679 let result = CompressedAccountFilterSet::with_capacity(usize::MAX);
680 assert!(matches!(result, Err(CuckooBuildError::CapacityOverflow)));
681 }
682}