1use std::collections::HashMap;
8use std::time::{Duration, Instant};
9
10use irontide_core::Id20;
11
12use crate::bep44::{ImmutableItem, MutableItem, compute_mutable_target};
13
14pub trait DhtStorage: Send + Sync + 'static {
16 fn get_immutable(&mut self, target: &Id20) -> Option<ImmutableItem>;
20
21 fn put_immutable(&mut self, item: ImmutableItem) -> bool;
23
24 fn get_mutable(&mut self, public_key: &[u8; 32], salt: &[u8]) -> Option<MutableItem>;
28
29 fn get_mutable_by_target(&mut self, target: &Id20) -> Option<MutableItem>;
35
36 fn put_mutable(&mut self, item: MutableItem) -> bool;
41
42 fn count(&self) -> (usize, usize);
44
45 fn expire(&mut self, lifetime: Duration);
47}
48
49struct StoredEntry<T> {
51 item: T,
52 last_touched: Instant,
53}
54
55impl<T> StoredEntry<T> {
56 fn new(item: T) -> Self {
57 Self {
58 item,
59 last_touched: Instant::now(),
60 }
61 }
62
63 fn touch(&mut self) {
64 self.last_touched = Instant::now();
65 }
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Hash)]
70struct MutableKey {
71 public_key: [u8; 32],
72 salt: Vec<u8>,
73}
74
75pub struct InMemoryDhtStorage {
80 immutable: HashMap<Id20, StoredEntry<ImmutableItem>>,
81 mutable: HashMap<MutableKey, StoredEntry<MutableItem>>,
82 max_items: usize,
83}
84
85impl InMemoryDhtStorage {
86 #[must_use]
88 pub fn new(max_items: usize) -> Self {
89 Self {
90 immutable: HashMap::new(),
91 mutable: HashMap::new(),
92 max_items,
93 }
94 }
95
96 fn total_count(&self) -> usize {
98 self.immutable.len() + self.mutable.len()
99 }
100
101 fn evict_lru(&mut self) {
103 let oldest_immutable = self
104 .immutable
105 .iter()
106 .min_by_key(|(_, e)| e.last_touched)
107 .map(|(k, e)| (*k, e.last_touched));
108
109 let oldest_mutable = self
110 .mutable
111 .iter()
112 .min_by_key(|(_, e)| e.last_touched)
113 .map(|(k, e)| (k.clone(), e.last_touched));
114
115 match (oldest_immutable, oldest_mutable) {
116 (Some((ik, it)), Some((mk, mt))) => {
117 if it <= mt {
118 self.immutable.remove(&ik);
119 } else {
120 self.mutable.remove(&mk);
121 }
122 }
123 (Some((ik, _)), None) => {
124 self.immutable.remove(&ik);
125 }
126 (None, Some((mk, _))) => {
127 self.mutable.remove(&mk);
128 }
129 (None, None) => {}
130 }
131 }
132}
133
134impl Default for InMemoryDhtStorage {
135 fn default() -> Self {
136 Self::new(700)
137 }
138}
139
140impl DhtStorage for InMemoryDhtStorage {
141 fn get_immutable(&mut self, target: &Id20) -> Option<ImmutableItem> {
142 self.immutable.get_mut(target).map(|e| {
143 e.touch();
144 e.item.clone()
145 })
146 }
147
148 fn put_immutable(&mut self, item: ImmutableItem) -> bool {
149 let target = item.target;
150
151 if self.immutable.contains_key(&target) {
152 if let Some(entry) = self.immutable.get_mut(&target) {
154 entry.touch();
155 }
156 return false;
157 }
158
159 while self.total_count() >= self.max_items {
161 self.evict_lru();
162 }
163
164 self.immutable.insert(target, StoredEntry::new(item));
165 true
166 }
167
168 fn get_mutable(&mut self, public_key: &[u8; 32], salt: &[u8]) -> Option<MutableItem> {
169 let key = MutableKey {
170 public_key: *public_key,
171 salt: salt.to_vec(),
172 };
173 self.mutable.get_mut(&key).map(|e| {
174 e.touch();
175 e.item.clone()
176 })
177 }
178
179 fn get_mutable_by_target(&mut self, target: &Id20) -> Option<MutableItem> {
180 for entry in self.mutable.values_mut() {
182 if compute_mutable_target(&entry.item.public_key, &entry.item.salt) == *target {
183 entry.touch();
184 return Some(entry.item.clone());
185 }
186 }
187 None
188 }
189
190 fn put_mutable(&mut self, item: MutableItem) -> bool {
191 let key = MutableKey {
192 public_key: item.public_key,
193 salt: item.salt.clone(),
194 };
195
196 if let Some(existing) = self.mutable.get(&key)
198 && item.seq <= existing.item.seq
199 {
200 return false;
201 }
202
203 let is_new = !self.mutable.contains_key(&key);
204
205 if is_new {
206 while self.total_count() >= self.max_items {
208 self.evict_lru();
209 }
210 }
211
212 self.mutable.insert(key, StoredEntry::new(item));
213 true
214 }
215
216 fn count(&self) -> (usize, usize) {
217 (self.immutable.len(), self.mutable.len())
218 }
219
220 fn expire(&mut self, lifetime: Duration) {
224 let now = Instant::now();
225 self.immutable
226 .retain(|_, e| now.duration_since(e.last_touched) <= lifetime);
227 self.mutable
228 .retain(|_, e| now.duration_since(e.last_touched) <= lifetime);
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use crate::bep44::ImmutableItem;
236 use ed25519_dalek::SigningKey;
237
238 fn make_immutable(data: &[u8]) -> ImmutableItem {
239 ImmutableItem::new(data.to_vec()).unwrap()
240 }
241
242 fn make_mutable(seed: u8, seq: i64, salt: &[u8]) -> MutableItem {
243 let keypair = SigningKey::from_bytes(&[seed; 32]);
244 MutableItem::create(&keypair, b"4:test".to_vec(), seq, salt.to_vec()).unwrap()
245 }
246
247 #[test]
248 fn immutable_put_get_round_trip() {
249 let mut store = InMemoryDhtStorage::new(100);
250 let item = make_immutable(b"5:hello");
251 let target = item.target;
252 assert!(store.put_immutable(item.clone()));
253 assert_eq!(store.get_immutable(&target), Some(item));
254 }
255
256 #[test]
257 fn immutable_duplicate_returns_false() {
258 let mut store = InMemoryDhtStorage::new(100);
259 let item = make_immutable(b"5:hello");
260 assert!(store.put_immutable(item.clone()));
261 assert!(!store.put_immutable(item));
262 }
263
264 #[test]
265 fn immutable_get_missing_returns_none() {
266 let mut store = InMemoryDhtStorage::new(100);
267 assert_eq!(store.get_immutable(&Id20::ZERO), None);
268 }
269
270 #[test]
271 fn mutable_put_get_round_trip() {
272 let mut store = InMemoryDhtStorage::new(100);
273 let item = make_mutable(1, 1, b"");
274 let pk = item.public_key;
275 assert!(store.put_mutable(item.clone()));
276 assert_eq!(store.get_mutable(&pk, b""), Some(item));
277 }
278
279 #[test]
280 fn mutable_sequence_monotonicity() {
281 let mut store = InMemoryDhtStorage::new(100);
282 let item1 = make_mutable(1, 5, b"");
283 let item2_old = make_mutable(1, 3, b"");
284 let item3_same = make_mutable(1, 5, b"");
285 let item4_new = make_mutable(1, 6, b"");
286
287 assert!(store.put_mutable(item1));
288 assert!(!store.put_mutable(item2_old)); assert!(!store.put_mutable(item3_same)); assert!(store.put_mutable(item4_new.clone())); let pk = item4_new.public_key;
293 let stored = store.get_mutable(&pk, b"").unwrap();
294 assert_eq!(stored.seq, 6);
295 }
296
297 #[test]
298 fn mutable_salt_separates_items() {
299 let mut store = InMemoryDhtStorage::new(100);
300 let item_a = make_mutable(1, 1, b"salt-a");
301 let item_b = make_mutable(1, 1, b"salt-b");
302 let pk = item_a.public_key;
303
304 assert!(store.put_mutable(item_a.clone()));
305 assert!(store.put_mutable(item_b.clone()));
306 assert_eq!(store.count(), (0, 2));
307 assert_eq!(store.get_mutable(&pk, b"salt-a"), Some(item_a));
308 assert_eq!(store.get_mutable(&pk, b"salt-b"), Some(item_b));
309 }
310
311 #[test]
312 fn lru_eviction_at_capacity() {
313 let mut store = InMemoryDhtStorage::new(3);
314
315 let item1 = make_immutable(b"1:a");
316 let item2 = make_immutable(b"1:b");
317 let item3 = make_immutable(b"1:c");
318 let target1 = item1.target;
319
320 store.put_immutable(item1);
321 store.put_immutable(item2);
322 store.put_immutable(item3);
323 assert_eq!(store.total_count(), 3);
324
325 let item4 = make_immutable(b"1:d");
327 store.put_immutable(item4);
328 assert_eq!(store.total_count(), 3);
329 assert!(store.get_immutable(&target1).is_none());
330 }
331
332 #[test]
333 fn expire_removes_old_items() {
334 let mut store = InMemoryDhtStorage::new(100);
335 store.put_immutable(make_immutable(b"1:a"));
336 store.put_mutable(make_mutable(1, 1, b""));
337 assert_eq!(store.count(), (1, 1));
338
339 store.expire(Duration::from_secs(0));
341 assert_eq!(store.count(), (0, 0));
342 }
343
344 #[test]
345 fn count_includes_both_types() {
346 let mut store = InMemoryDhtStorage::new(100);
347 store.put_immutable(make_immutable(b"1:a"));
348 store.put_mutable(make_mutable(1, 1, b""));
349 assert_eq!(store.count(), (1, 1));
350 }
351
352 #[test]
353 fn default_max_items_is_700() {
354 let store = InMemoryDhtStorage::default();
355 assert_eq!(store.max_items, 700);
356 }
357
358 #[test]
359 fn get_mutable_by_target_finds_item() {
360 let mut store = InMemoryDhtStorage::new(100);
361 let item = make_mutable(1, 1, b"my-salt");
362 let target = compute_mutable_target(&item.public_key, &item.salt);
363 store.put_mutable(item.clone());
364
365 let found = store.get_mutable_by_target(&target);
366 assert_eq!(found, Some(item));
367 }
368
369 #[test]
370 fn get_mutable_by_target_returns_none_for_missing() {
371 let mut store = InMemoryDhtStorage::new(100);
372 assert_eq!(store.get_mutable_by_target(&Id20::ZERO), None);
373 }
374
375 #[test]
376 fn get_immutable_touches_entry() {
377 let mut store = InMemoryDhtStorage::new(3);
378
379 let item1 = make_immutable(b"1:a");
380 let item2 = make_immutable(b"1:b");
381 let item3 = make_immutable(b"1:c");
382 let target1 = item1.target;
383 let target2 = item2.target;
384
385 store.put_immutable(item1);
386 store.put_immutable(item2);
387 store.put_immutable(item3);
388
389 store.get_immutable(&target1);
391
392 let item4 = make_immutable(b"1:d");
394 store.put_immutable(item4);
395 assert_eq!(store.total_count(), 3);
396 assert!(store.get_immutable(&target1).is_some());
397 assert!(store.get_immutable(&target2).is_none());
398 }
399}