ados_protocol/
nonce_cache.rs1use std::collections::{BTreeMap, VecDeque};
16use std::sync::Mutex;
17
18pub struct NonceCache {
20 capacity: usize,
21 retention_seconds: i64,
22 inner: Mutex<Inner>,
23}
24
25struct Inner {
26 seen: BTreeMap<String, i64>,
29 order: VecDeque<String>,
32}
33
34impl NonceCache {
35 pub const fn new(capacity: usize, retention_seconds: i64) -> Self {
38 Self {
39 capacity,
40 retention_seconds,
41 inner: Mutex::new(Inner {
42 seen: BTreeMap::new(),
43 order: VecDeque::new(),
44 }),
45 }
46 }
47
48 pub fn admit(&self, nonce: &str, now: i64, valid_until: i64) -> bool {
55 let mut inner = self.inner.lock().unwrap_or_else(|p| p.into_inner());
56 if let Some(forget_at) = inner.seen.remove(nonce) {
57 if forget_at > now {
58 inner.seen.insert(nonce.to_owned(), forget_at);
59 return false;
60 }
61 inner.order.retain(|n| n != nonce);
63 }
64 let forget_at = now
65 .saturating_add(self.retention_seconds)
66 .max(valid_until.saturating_add(1));
67 if inner.seen.len() >= self.capacity {
68 inner.seen.retain(|_, f| *f > now);
69 let Inner { seen, order } = &mut *inner;
70 order.retain(|n| seen.contains_key(n));
71 }
72 while inner.seen.len() >= self.capacity {
73 let Some(oldest) = inner.order.pop_front() else {
74 break;
75 };
76 inner.seen.remove(&oldest);
77 }
78 inner.seen.insert(nonce.to_owned(), forget_at);
79 inner.order.push_back(nonce.to_owned());
80 true
81 }
82
83 pub fn len(&self) -> usize {
85 self.inner
86 .lock()
87 .unwrap_or_else(|p| p.into_inner())
88 .seen
89 .len()
90 }
91
92 pub fn is_empty(&self) -> bool {
94 self.len() == 0
95 }
96}
97
98pub fn random_nonce_hex() -> Option<String> {
102 let mut bytes = [0u8; 16];
103 getrandom::fill(&mut bytes).ok()?;
104 Some(hex::encode(bytes))
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn a_nonce_is_admitted_once() {
113 let cache = NonceCache::new(8, 600);
114 assert!(cache.admit("aa", 1_000, 1_030));
115 assert!(!cache.admit("aa", 1_001, 1_030), "a second use is a replay");
116 assert!(cache.admit("bb", 1_001, 1_030));
117 }
118
119 #[test]
120 fn an_entry_outlives_the_ticket_that_carried_it() {
121 let cache = NonceCache::new(8, 600);
124 assert!(cache.admit("aa", 1_000, 1_900));
125 assert!(!cache.admit("aa", 1_650, 1_900));
126 assert!(!cache.admit("aa", 1_900, 1_900));
127 assert!(cache.admit("aa", 1_901, 2_000), "forgotten once unusable");
128 }
129
130 #[test]
131 fn the_oldest_entry_is_evicted_at_capacity() {
132 let cache = NonceCache::new(2, 600);
133 assert!(cache.admit("a", 1_000, 1_030));
134 assert!(cache.admit("b", 1_000, 1_030));
135 assert!(cache.admit("c", 1_000, 1_030));
136 assert_eq!(cache.len(), 2);
137 assert!(cache.admit("a", 1_001, 1_030), "the oldest was evicted");
138 assert!(!cache.admit("c", 1_001, 1_030));
139 }
140
141 #[test]
142 fn expired_entries_are_purged_before_anything_live_is_evicted() {
143 let cache = NonceCache::new(2, 10);
144 assert!(cache.admit("old", 1_000, 1_000));
145 assert!(cache.admit("live", 1_050, 1_100));
146 assert!(cache.admit("new", 1_060, 1_100));
148 assert!(!cache.admit("live", 1_061, 1_100));
149 }
150
151 #[test]
152 fn random_nonces_are_sixteen_bytes_of_hex_and_differ() {
153 let a = random_nonce_hex().unwrap();
154 let b = random_nonce_hex().unwrap();
155 assert_eq!(a.len(), 32);
156 assert!(a.bytes().all(|c| c.is_ascii_hexdigit()));
157 assert_ne!(a, b);
158 }
159}