Skip to main content

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}