Skip to main content

tempo_transaction_pool/
paused.rs

1//! Pool for transactions whose fee token is temporarily paused.
2//!
3//! When a TIP20 fee token emits `PauseStateUpdate(isPaused=true)`, transactions
4//! using that fee token are moved here instead of being evicted entirely.
5//! When the token is unpaused, transactions are moved back to the main pool
6//! and re-validated.
7
8use crate::{RevokedKeys, SpendingLimitUpdates, transaction::TempoPooledTransaction};
9use alloy_primitives::{
10    Address, TxHash,
11    map::{AddressMap, B256Set},
12};
13use reth_transaction_pool::ValidPoolTransaction;
14use std::{sync::Arc, time::Instant};
15
16/// Duration after which paused transactions are expired and removed.
17/// If a token isn't unpaused within this time, we clear all pending transactions.
18pub const PAUSED_TX_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30 * 60); // 30 minutes
19
20/// Global cap on the total number of paused transactions across all tokens.
21///
22/// Without this cap, an attacker could repeatedly fill the main pool, trigger a pause, and shift
23/// transactions into the paused pool indefinitely. This bounds memory usage regardless of how many
24/// tokens are paused or how frequently pause events occur.
25pub const PAUSED_POOL_GLOBAL_CAP: usize = 10_000;
26
27/// Entry in the paused pool.
28#[derive(Debug, Clone)]
29pub struct PausedEntry {
30    /// The valid pool transaction that was paused (Arc to avoid expensive clones).
31    pub tx: Arc<ValidPoolTransaction<TempoPooledTransaction>>,
32    /// The `valid_before` timestamp, if any (for expiry tracking).
33    pub valid_before: Option<u64>,
34}
35
36/// Metadata for a paused fee token.
37#[derive(Debug, Clone)]
38struct PausedTokenMeta {
39    /// When this token was paused.
40    paused_at: Instant,
41    /// Transactions waiting for this token to be unpaused.
42    entries: Vec<PausedEntry>,
43}
44
45/// Pool for transactions whose fee token is temporarily paused.
46///
47/// Transactions are indexed by fee token address for efficient batch operations.
48/// Since all transactions for a token are paused/unpaused together, we track
49/// the pause timestamp at the token level rather than per-transaction.
50#[derive(Debug, Default)]
51pub struct PausedFeeTokenPool {
52    /// Fee token -> metadata including pause time and entries
53    by_token: AddressMap<PausedTokenMeta>,
54}
55
56impl PausedFeeTokenPool {
57    /// Creates a new empty paused pool.
58    pub fn new() -> Self {
59        Self::default()
60    }
61
62    /// Returns the total number of paused transactions across all tokens.
63    pub fn len(&self) -> usize {
64        self.by_token.values().map(|m| m.entries.len()).sum()
65    }
66
67    /// Returns true if there are no paused transactions.
68    pub fn is_empty(&self) -> bool {
69        self.by_token.is_empty()
70    }
71
72    /// Inserts transactions for a fee token into the paused pool.
73    ///
74    /// Takes the full batch at once since all transactions for a token
75    /// are paused together. The pause timestamp is recorded at insertion time.
76    ///
77    /// Enforces [`PAUSED_POOL_GLOBAL_CAP`]: if adding the batch would exceed the cap,
78    /// the oldest-paused tokens are evicted first to make room. If the batch itself
79    /// exceeds the cap, it is truncated.
80    ///
81    /// Returns the number of existing entries that were evicted to make room.
82    pub fn insert_batch(&mut self, fee_token: Address, entries: Vec<PausedEntry>) -> usize {
83        if entries.is_empty() {
84            return 0;
85        }
86
87        let current = self.len();
88        let incoming = entries.len();
89        let available = PAUSED_POOL_GLOBAL_CAP.saturating_sub(current);
90        let mut evicted = 0;
91
92        if incoming > available {
93            let need = incoming - available;
94            evicted = self.evict_oldest(need);
95        }
96
97        let remaining_capacity = PAUSED_POOL_GLOBAL_CAP.saturating_sub(self.len());
98        let to_insert = if incoming > remaining_capacity {
99            entries.into_iter().take(remaining_capacity).collect()
100        } else {
101            entries
102        };
103
104        self.by_token
105            .entry(fee_token)
106            .or_insert_with(|| PausedTokenMeta {
107                paused_at: Instant::now(),
108                entries: Vec::new(),
109            })
110            .entries
111            .extend(to_insert);
112
113        evicted
114    }
115
116    /// Evicts at least `need` entries from the oldest-paused tokens.
117    ///
118    /// Returns the total number of entries evicted.
119    fn evict_oldest(&mut self, need: usize) -> usize {
120        let mut tokens_by_age: Vec<_> = self
121            .by_token
122            .iter()
123            .map(|(addr, meta)| (*addr, meta.paused_at))
124            .collect();
125        tokens_by_age.sort_unstable_by_key(|(_, paused_at)| *paused_at);
126
127        let mut evicted = 0;
128        for (token, _) in tokens_by_age {
129            if evicted >= need {
130                break;
131            }
132            if let Some(meta) = self.by_token.remove(&token) {
133                evicted += meta.entries.len();
134            }
135        }
136        evicted
137    }
138
139    /// Drains all transactions for a given fee token.
140    ///
141    /// Returns the list of paused entries for that token.
142    pub fn drain_token(&mut self, fee_token: &Address) -> Vec<PausedEntry> {
143        self.by_token
144            .remove(fee_token)
145            .map(|m| m.entries)
146            .unwrap_or_default()
147    }
148
149    /// Returns the number of transactions paused for a given fee token.
150    pub fn count_for_token(&self, fee_token: &Address) -> usize {
151        self.by_token.get(fee_token).map_or(0, |m| m.entries.len())
152    }
153
154    /// Returns true if a transaction with the given hash is in the paused pool.
155    pub fn contains(&self, tx_hash: &TxHash) -> bool {
156        self.by_token
157            .values()
158            .any(|m| m.entries.iter().any(|e| e.tx.hash() == tx_hash))
159    }
160
161    /// Evicts expired transactions based on `valid_before` timestamp.
162    ///
163    /// Returns the number of transactions removed.
164    pub fn evict_expired(&mut self, tip_timestamp: u64) -> usize {
165        let mut count = 0;
166        for meta in self.by_token.values_mut() {
167            let before = meta.entries.len();
168            meta.entries
169                .retain(|e| e.valid_before.is_none_or(|vb| vb > tip_timestamp));
170            count += before - meta.entries.len();
171        }
172        // Clean up empty token entries
173        self.by_token.retain(|_, m| !m.entries.is_empty());
174        count
175    }
176
177    /// Evicts all transactions for tokens that have been paused for too long (timeout).
178    ///
179    /// Since all transactions for a token are paused together, we evict the entire
180    /// token's transactions when the token-level timeout expires.
181    ///
182    /// Returns the number of transactions removed.
183    pub fn evict_timed_out(&mut self) -> usize {
184        let now = Instant::now();
185        let mut count = 0;
186        self.by_token.retain(|_, meta| {
187            if now.duration_since(meta.paused_at) >= PAUSED_TX_TIMEOUT {
188                count += meta.entries.len();
189                false
190            } else {
191                true
192            }
193        });
194        count
195    }
196
197    /// Removes transactions matching invalidation criteria from the paused pool.
198    ///
199    /// This handles hard keychain invalidations in a single pass. Keychain
200    /// `IAccountKeychain::AccessKeySpend` events are intentionally omitted: they only prove
201    /// partial spending-limit consumption, and paused transactions are fully revalidated when
202    /// their fee token unpauses.
203    /// Uses account-keyed indexes for O(1) account lookup per transaction.
204    /// Returns the number of transactions removed.
205    pub fn evict_invalidated(
206        &mut self,
207        revoked_keys: &RevokedKeys,
208        key_authorization_target_changes: &RevokedKeys,
209        spending_limit_updates: &SpendingLimitUpdates,
210        key_authorization_witness_burns: &AddressMap<B256Set>,
211    ) -> usize {
212        if revoked_keys.is_empty()
213            && key_authorization_target_changes.is_empty()
214            && spending_limit_updates.is_empty()
215            && key_authorization_witness_burns.is_empty()
216        {
217            return 0;
218        }
219
220        let mut count = 0;
221        let has_keychain_subject_updates =
222            !revoked_keys.is_empty() || !spending_limit_updates.is_empty();
223        let has_key_authorization_target_updates = !key_authorization_target_changes.is_empty();
224        for meta in self.by_token.values_mut() {
225            let before = meta.entries.len();
226            meta.entries.retain(|entry| {
227                let key_authorization_subject = (!revoked_keys.is_empty())
228                    .then(|| entry.tx.transaction.key_authorization_signer_subject())
229                    .flatten();
230                let key_authorization_target = has_key_authorization_target_updates
231                    .then(|| entry.tx.transaction.key_authorization_target_subject())
232                    .flatten();
233
234                let keychain_subject = has_keychain_subject_updates
235                    .then(|| entry.tx.transaction.keychain_subject())
236                    .flatten();
237                let Some(subject) = keychain_subject else {
238                    let Some(witness_subject) =
239                        entry.tx.transaction.key_authorization_witness_subject()
240                    else {
241                        return !key_authorization_subject
242                            .as_ref()
243                            .is_some_and(|subject| subject.matches_revoked(revoked_keys))
244                            && !key_authorization_target.as_ref().is_some_and(|subject| {
245                                subject.matches_key_update(key_authorization_target_changes)
246                            });
247                    };
248
249                    return !key_authorization_subject
250                        .as_ref()
251                        .is_some_and(|subject| subject.matches_revoked(revoked_keys))
252                        && !key_authorization_target.as_ref().is_some_and(|subject| {
253                            subject.matches_key_update(key_authorization_target_changes)
254                        })
255                        && !key_authorization_witness_burns
256                            .get(&witness_subject.account)
257                            .is_some_and(|witnesses| witnesses.contains(&witness_subject.witness));
258                };
259
260                let matches_limit_update =
261                    subject.matches_spending_limit_update(spending_limit_updates);
262                let sender_paid = matches_limit_update && entry.tx.transaction.is_sender_paid_fee();
263
264                if subject.matches_revoked(revoked_keys)
265                    || key_authorization_subject
266                        .as_ref()
267                        .is_some_and(|subject| subject.matches_revoked(revoked_keys))
268                    || key_authorization_target.as_ref().is_some_and(|subject| {
269                        subject.matches_key_update(key_authorization_target_changes)
270                    })
271                    || (sender_paid && matches_limit_update)
272                {
273                    return false;
274                }
275
276                let Some(witness_subject) =
277                    entry.tx.transaction.key_authorization_witness_subject()
278                else {
279                    return true;
280                };
281
282                !key_authorization_witness_burns
283                    .get(&witness_subject.account)
284                    .is_some_and(|witnesses| witnesses.contains(&witness_subject.witness))
285            });
286            count += before - meta.entries.len();
287        }
288        // Clean up empty token entries
289        self.by_token.retain(|_, m| !m.entries.is_empty());
290        count
291    }
292
293    /// Returns an iterator over all paused entries across all tokens.
294    pub fn all_entries(&self) -> impl Iterator<Item = &PausedEntry> {
295        self.by_token.values().flat_map(|m| &m.entries)
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302    use crate::test_utils::{TxBuilder, wrap_valid_tx};
303    use alloy_primitives::B256;
304    use alloy_signer::SignerSync;
305    use alloy_signer_local::PrivateKeySigner;
306    use reth_primitives_traits::Recovered;
307    use reth_transaction_pool::TransactionOrigin;
308    use tempo_primitives::{
309        SignatureType, TempoTxEnvelope,
310        transaction::{KeyAuthorization, PrimitiveSignature, tt_signed::AASigned},
311    };
312
313    fn create_valid_tx(sender: Address) -> Arc<ValidPoolTransaction<TempoPooledTransaction>> {
314        let pooled = TxBuilder::aa(sender).build();
315        Arc::new(wrap_valid_tx(pooled, TransactionOrigin::External))
316    }
317
318    fn create_valid_keychain_tx(
319        sender: Address,
320        fee_token: Address,
321        sponsored: bool,
322    ) -> Arc<ValidPoolTransaction<TempoPooledTransaction>> {
323        let access_key_signer = PrivateKeySigner::random();
324        let pooled = TxBuilder::aa(sender)
325            .fee_token(fee_token)
326            .build_keychain(sender, &access_key_signer);
327
328        let pooled = if sponsored {
329            let sponsor = PrivateKeySigner::random();
330            let aa = pooled
331                .inner()
332                .as_aa()
333                .expect("builder should produce AA tx");
334            let mut tx = aa.tx().clone();
335            tx.fee_payer_signature = Some(alloy_primitives::Signature::new(
336                alloy_primitives::U256::ZERO,
337                alloy_primitives::U256::ZERO,
338                false,
339            ));
340            let fee_payer_hash = tx.fee_payer_signature_hash(sender);
341            tx.fee_payer_signature = Some(
342                sponsor
343                    .sign_hash_sync(&fee_payer_hash)
344                    .expect("sponsor signing should succeed"),
345            );
346
347            let aa_signed = AASigned::new_unhashed(tx, aa.signature().clone());
348            let envelope: TempoTxEnvelope = aa_signed.into();
349            TempoPooledTransaction::new(Recovered::new_unchecked(envelope, sender))
350        } else {
351            pooled
352        };
353
354        Arc::new(wrap_valid_tx(pooled, TransactionOrigin::External))
355    }
356
357    #[test]
358    fn test_insert_and_drain() {
359        let mut pool = PausedFeeTokenPool::new();
360        let fee_token = Address::random();
361
362        let entries: Vec<_> = (0..3)
363            .map(|_| PausedEntry {
364                tx: create_valid_tx(Address::random()),
365                valid_before: None,
366            })
367            .collect();
368
369        assert!(pool.is_empty());
370        pool.insert_batch(fee_token, entries);
371
372        assert_eq!(pool.len(), 3);
373        assert_eq!(pool.count_for_token(&fee_token), 3);
374
375        let drained = pool.drain_token(&fee_token);
376        assert_eq!(drained.len(), 3);
377        assert!(pool.is_empty());
378    }
379
380    #[test]
381    fn test_evict_expired() {
382        let mut pool = PausedFeeTokenPool::new();
383        let fee_token = Address::random();
384
385        let entries = vec![
386            PausedEntry {
387                tx: create_valid_tx(Address::random()),
388                valid_before: Some(100), // Will expire
389            },
390            PausedEntry {
391                tx: create_valid_tx(Address::random()),
392                valid_before: Some(200), // Won't expire
393            },
394            PausedEntry {
395                tx: create_valid_tx(Address::random()),
396                valid_before: None, // No expiry
397            },
398        ];
399
400        pool.insert_batch(fee_token, entries);
401        assert_eq!(pool.len(), 3);
402
403        let evicted = pool.evict_expired(150);
404        assert_eq!(evicted, 1);
405        assert_eq!(pool.len(), 2);
406    }
407
408    #[test]
409    fn test_global_cap_evicts_oldest() {
410        let mut pool = PausedFeeTokenPool::new();
411
412        let token_a = Address::random();
413        let token_b = Address::random();
414
415        let make_entries = |n: usize| -> Vec<PausedEntry> {
416            (0..n)
417                .map(|_| PausedEntry {
418                    tx: create_valid_tx(Address::random()),
419                    valid_before: None,
420                })
421                .collect()
422        };
423
424        // Fill to cap
425        let evicted = pool.insert_batch(token_a, make_entries(PAUSED_POOL_GLOBAL_CAP));
426        assert_eq!(evicted, 0);
427        assert_eq!(pool.len(), PAUSED_POOL_GLOBAL_CAP);
428
429        // Inserting more should evict token_a (oldest) to make room
430        let evicted = pool.insert_batch(token_b, make_entries(100));
431        assert!(evicted > 0);
432        assert!(pool.len() <= PAUSED_POOL_GLOBAL_CAP);
433        assert_eq!(pool.count_for_token(&token_b), 100);
434    }
435
436    #[test]
437    fn test_global_cap_truncates_oversized_batch() {
438        let mut pool = PausedFeeTokenPool::new();
439        let token = Address::random();
440
441        let entries: Vec<_> = (0..PAUSED_POOL_GLOBAL_CAP + 500)
442            .map(|_| PausedEntry {
443                tx: create_valid_tx(Address::random()),
444                valid_before: None,
445            })
446            .collect();
447
448        let evicted = pool.insert_batch(token, entries);
449        assert_eq!(evicted, 0);
450        assert_eq!(pool.len(), PAUSED_POOL_GLOBAL_CAP);
451    }
452
453    #[test]
454    fn test_evict_invalidated_with_spending_limit_updates() {
455        let mut pool = PausedFeeTokenPool::new();
456        let user_address = Address::random();
457        let fee_token = Address::random();
458
459        // Create a keychain-signed tx using the builder
460        let access_key_signer = alloy_signer_local::PrivateKeySigner::random();
461        let key_id = alloy_signer::Signer::address(&access_key_signer);
462        let tx = TxBuilder::aa(user_address)
463            .fee_token(fee_token)
464            .build_keychain(user_address, &access_key_signer);
465        let tx = Arc::new(wrap_valid_tx(
466            tx,
467            reth_transaction_pool::TransactionOrigin::External,
468        ));
469
470        // Also add a non-keychain tx that should NOT be evicted
471        let other_tx = create_valid_tx(Address::random());
472
473        pool.insert_batch(
474            fee_token,
475            vec![
476                PausedEntry {
477                    tx,
478                    valid_before: None,
479                },
480                PausedEntry {
481                    tx: other_tx,
482                    valid_before: None,
483                },
484            ],
485        );
486        assert_eq!(pool.len(), 2);
487
488        let mut updates = SpendingLimitUpdates::new();
489        updates.insert(user_address, key_id, Some(fee_token));
490
491        let evicted = pool.evict_invalidated(
492            &RevokedKeys::new(),
493            &RevokedKeys::new(),
494            &updates,
495            &AddressMap::default(),
496        );
497
498        assert_eq!(
499            evicted, 1,
500            "Should evict the keychain tx matching the spending limit update"
501        );
502        assert_eq!(pool.len(), 1, "Non-keychain tx should remain");
503    }
504
505    #[test]
506    fn test_evict_invalidated_keeps_sponsored_keychain_for_spending_limit_updates() {
507        let mut pool = PausedFeeTokenPool::new();
508        let user_address = Address::random();
509        let fee_token = Address::random();
510
511        let sponsored_keychain_tx = create_valid_keychain_tx(user_address, fee_token, true);
512        pool.insert_batch(
513            fee_token,
514            vec![PausedEntry {
515                tx: sponsored_keychain_tx,
516                valid_before: None,
517            }],
518        );
519
520        let key_id = pool
521            .all_entries()
522            .next()
523            .and_then(|entry| entry.tx.transaction.keychain_subject())
524            .map(|subject| subject.key_id)
525            .expect("sponsored keychain tx should have keychain subject");
526
527        let mut updates = SpendingLimitUpdates::new();
528        updates.insert(user_address, key_id, Some(fee_token));
529
530        let evicted = pool.evict_invalidated(
531            &RevokedKeys::new(),
532            &RevokedKeys::new(),
533            &updates,
534            &AddressMap::default(),
535        );
536
537        assert_eq!(evicted, 0, "Sponsored keychain tx should not be evicted");
538        assert_eq!(pool.len(), 1);
539    }
540
541    #[test]
542    fn test_evict_invalidated_with_key_authorization_witness_burn() {
543        let mut pool = PausedFeeTokenPool::new();
544        let user_address = Address::random();
545        let fee_token = Address::random();
546        let burned_witness = B256::random();
547        let other_witness = B256::random();
548
549        let key_authorization = |witness| {
550            KeyAuthorization::unrestricted(42431, SignatureType::Secp256k1, Address::random())
551                .with_witness(witness)
552                .into_signed(PrimitiveSignature::Secp256k1(
553                    alloy_primitives::Signature::test_signature(),
554                ))
555        };
556
557        let matching = Arc::new(wrap_valid_tx(
558            TxBuilder::aa(user_address)
559                .fee_token(fee_token)
560                .key_authorization(key_authorization(burned_witness))
561                .build(),
562            TransactionOrigin::External,
563        ));
564        let untouched = Arc::new(wrap_valid_tx(
565            TxBuilder::aa(user_address)
566                .nonce(1)
567                .fee_token(fee_token)
568                .key_authorization(key_authorization(other_witness))
569                .build(),
570            TransactionOrigin::External,
571        ));
572
573        pool.insert_batch(
574            fee_token,
575            vec![
576                PausedEntry {
577                    tx: matching,
578                    valid_before: None,
579                },
580                PausedEntry {
581                    tx: untouched,
582                    valid_before: None,
583                },
584            ],
585        );
586
587        let mut burned = AddressMap::default();
588        burned
589            .entry(user_address)
590            .or_insert_with(B256Set::default)
591            .insert(burned_witness);
592
593        let evicted = pool.evict_invalidated(
594            &RevokedKeys::new(),
595            &RevokedKeys::new(),
596            &SpendingLimitUpdates::new(),
597            &burned,
598        );
599
600        assert_eq!(evicted, 1);
601        assert_eq!(pool.len(), 1);
602        assert_eq!(
603            pool.all_entries()
604                .next()
605                .and_then(|entry| entry.tx.transaction.key_authorization_witness_subject())
606                .map(|subject| subject.witness),
607            Some(other_witness)
608        );
609    }
610
611    #[test]
612    fn test_evict_invalidated_with_revoked_key_authorization_signer() {
613        let mut pool = PausedFeeTokenPool::new();
614        let user_address = Address::random();
615        let fee_token = Address::random();
616        let admin_signer = PrivateKeySigner::random();
617        let admin_key = alloy_signer::Signer::address(&admin_signer);
618        let other_signer = PrivateKeySigner::random();
619
620        let key_authorization = |signer: &PrivateKeySigner| {
621            let authorization =
622                KeyAuthorization::unrestricted(42431, SignatureType::Secp256k1, Address::random())
623                    .with_account(user_address);
624            let signature = signer
625                .sign_hash_sync(&authorization.signature_hash())
626                .expect("key authorization signing should succeed");
627            authorization.into_signed(PrimitiveSignature::Secp256k1(signature))
628        };
629
630        let matching = Arc::new(wrap_valid_tx(
631            TxBuilder::aa(user_address)
632                .fee_token(fee_token)
633                .key_authorization(key_authorization(&admin_signer))
634                .build(),
635            TransactionOrigin::External,
636        ));
637        let untouched = Arc::new(wrap_valid_tx(
638            TxBuilder::aa(user_address)
639                .nonce(1)
640                .fee_token(fee_token)
641                .key_authorization(key_authorization(&other_signer))
642                .build(),
643            TransactionOrigin::External,
644        ));
645
646        pool.insert_batch(
647            fee_token,
648            vec![
649                PausedEntry {
650                    tx: matching,
651                    valid_before: None,
652                },
653                PausedEntry {
654                    tx: untouched,
655                    valid_before: None,
656                },
657            ],
658        );
659
660        let mut revoked_keys = RevokedKeys::new();
661        revoked_keys.insert(user_address, admin_key);
662
663        let evicted = pool.evict_invalidated(
664            &revoked_keys,
665            &RevokedKeys::new(),
666            &SpendingLimitUpdates::new(),
667            &AddressMap::default(),
668        );
669
670        assert_eq!(evicted, 1);
671        assert_eq!(pool.len(), 1);
672    }
673
674    #[test]
675    fn test_evict_invalidated_with_key_authorization_target_change() {
676        let mut pool = PausedFeeTokenPool::new();
677        let user_address = Address::random();
678        let fee_token = Address::random();
679        let signer = PrivateKeySigner::random();
680        let target_key = Address::random();
681        let other_key = Address::random();
682
683        let key_authorization = |key_id| {
684            let authorization =
685                KeyAuthorization::unrestricted(42431, SignatureType::Secp256k1, key_id)
686                    .with_account(user_address);
687            let signature = signer
688                .sign_hash_sync(&authorization.signature_hash())
689                .expect("key authorization signing should succeed");
690            authorization.into_signed(PrimitiveSignature::Secp256k1(signature))
691        };
692
693        let matching = Arc::new(wrap_valid_tx(
694            TxBuilder::aa(user_address)
695                .fee_token(fee_token)
696                .key_authorization(key_authorization(target_key))
697                .build(),
698            TransactionOrigin::External,
699        ));
700        let untouched = Arc::new(wrap_valid_tx(
701            TxBuilder::aa(user_address)
702                .nonce(1)
703                .fee_token(fee_token)
704                .key_authorization(key_authorization(other_key))
705                .build(),
706            TransactionOrigin::External,
707        ));
708
709        pool.insert_batch(
710            fee_token,
711            vec![
712                PausedEntry {
713                    tx: matching,
714                    valid_before: None,
715                },
716                PausedEntry {
717                    tx: untouched,
718                    valid_before: None,
719                },
720            ],
721        );
722
723        let mut target_changes = RevokedKeys::new();
724        target_changes.insert(user_address, target_key);
725
726        let evicted = pool.evict_invalidated(
727            &RevokedKeys::new(),
728            &target_changes,
729            &SpendingLimitUpdates::new(),
730            &AddressMap::default(),
731        );
732
733        assert_eq!(evicted, 1);
734        assert_eq!(pool.len(), 1);
735    }
736
737    #[test]
738    fn test_contains() {
739        let mut pool = PausedFeeTokenPool::new();
740        let fee_token = Address::random();
741
742        let tx = create_valid_tx(Address::random());
743        let tx_hash = *tx.hash();
744
745        let entry = PausedEntry {
746            tx,
747            valid_before: None,
748        };
749
750        assert!(!pool.contains(&tx_hash));
751        pool.insert_batch(fee_token, vec![entry]);
752        assert!(pool.contains(&tx_hash));
753    }
754}