Skip to main content

agent_pay/
replay.rs

1//! Cache to reject replayed preimages within their invoice's TTL.
2
3use std::collections::HashMap;
4use std::collections::VecDeque;
5use std::sync::Mutex;
6
7/// Now provider returning milliseconds since the Unix epoch.
8pub type NowFn = Box<dyn Fn() -> u64 + Send + Sync>;
9
10pub struct ReplayCache {
11    map: Mutex<Inner>,
12    max_entries: usize,
13    now: NowFn,
14}
15
16struct Inner {
17    expirations: HashMap<String, u64>,
18    order: VecDeque<String>,
19}
20
21fn default_now_ms() -> u64 {
22    std::time::SystemTime::now()
23        .duration_since(std::time::UNIX_EPOCH)
24        .map(|d| d.as_millis() as u64)
25        .unwrap_or(0)
26}
27
28impl Default for ReplayCache {
29    fn default() -> Self {
30        Self::new(100_000, Box::new(default_now_ms))
31    }
32}
33
34impl ReplayCache {
35    pub fn new(max_entries: usize, now: NowFn) -> Self {
36        Self {
37            map: Mutex::new(Inner {
38                expirations: HashMap::new(),
39                order: VecDeque::new(),
40            }),
41            max_entries,
42            now,
43        }
44    }
45
46    pub fn mark_used(&self, key: &str, expires_at_ms: u64) {
47        let mut inner = self.map.lock().unwrap();
48        if inner.expirations.len() >= self.max_entries {
49            if let Some(oldest) = inner.order.pop_front() {
50                inner.expirations.remove(&oldest);
51            }
52        }
53        if inner
54            .expirations
55            .insert(key.to_string(), expires_at_ms)
56            .is_none()
57        {
58            inner.order.push_back(key.to_string());
59        }
60    }
61
62    pub fn is_used(&self, key: &str) -> bool {
63        let mut inner = self.map.lock().unwrap();
64        let Some(exp) = inner.expirations.get(key).copied() else {
65            return false;
66        };
67        if exp <= (self.now)() {
68            inner.expirations.remove(key);
69            // Lazy: leave order entry; it'll be skipped if popped.
70            return false;
71        }
72        true
73    }
74}