1use 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
16pub const PAUSED_TX_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30 * 60); pub const PAUSED_POOL_GLOBAL_CAP: usize = 10_000;
26
27#[derive(Debug, Clone)]
29pub struct PausedEntry {
30 pub tx: Arc<ValidPoolTransaction<TempoPooledTransaction>>,
32 pub valid_before: Option<u64>,
34}
35
36#[derive(Debug, Clone)]
38struct PausedTokenMeta {
39 paused_at: Instant,
41 entries: Vec<PausedEntry>,
43}
44
45#[derive(Debug, Default)]
51pub struct PausedFeeTokenPool {
52 by_token: AddressMap<PausedTokenMeta>,
54}
55
56impl PausedFeeTokenPool {
57 pub fn new() -> Self {
59 Self::default()
60 }
61
62 pub fn len(&self) -> usize {
64 self.by_token.values().map(|m| m.entries.len()).sum()
65 }
66
67 pub fn is_empty(&self) -> bool {
69 self.by_token.is_empty()
70 }
71
72 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 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 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 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 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 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 self.by_token.retain(|_, m| !m.entries.is_empty());
174 count
175 }
176
177 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 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 self.by_token.retain(|_, m| !m.entries.is_empty());
290 count
291 }
292
293 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), },
390 PausedEntry {
391 tx: create_valid_tx(Address::random()),
392 valid_before: Some(200), },
394 PausedEntry {
395 tx: create_valid_tx(Address::random()),
396 valid_before: None, },
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 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 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 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 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}