Skip to main content

dig_keystore/backend/
memory.rs

1//! In-memory backend.
2//!
3//! Originally feature-gated behind `testing`. As of v0.1.2 it is compiled
4//! unconditionally because production adapters in other crates (notably
5//! `dig-l1-wallet`'s `encryption.rs`) use it as a scratch backend to reuse
6//! the full keystore file format without touching the filesystem. The
7//! `testing` module still re-exports it for discoverability in dependent
8//! crates' dev-dependencies.
9//!
10//! Stores blobs in a `parking_lot::Mutex<HashMap>`. Legitimate production
11//! uses: encrypt-to-bytes / decrypt-from-bytes helpers; unit tests; doc
12//! examples.
13
14use std::collections::HashMap;
15
16use parking_lot::Mutex;
17
18use crate::backend::{BackendKey, KeychainBackend};
19use crate::error::{KeystoreError, Result};
20
21/// A keychain backend that lives entirely in process memory.
22///
23/// Legitimate uses:
24/// - **Scratch backend** for bytes-in / bytes-out adapters (e.g.
25///   `dig-l1-wallet::keystore::encryption::encrypt_secret_key`).
26/// - **Tests and doc examples** where touching the filesystem is overhead.
27///
28/// Do **not** use this as the storage medium for a long-lived keystore —
29/// process exit drops all state.
30#[derive(Default)]
31pub struct MemoryBackend {
32    inner: Mutex<HashMap<BackendKey, Vec<u8>>>,
33}
34
35impl MemoryBackend {
36    /// Construct an empty backend.
37    pub fn new() -> Self {
38        Self::default()
39    }
40}
41
42impl KeychainBackend for MemoryBackend {
43    fn read(&self, key: &BackendKey) -> Result<Vec<u8>> {
44        self.inner.lock().get(key).cloned().ok_or_else(|| {
45            KeystoreError::from(std::io::Error::new(
46                std::io::ErrorKind::NotFound,
47                format!("key not found: {key}"),
48            ))
49        })
50    }
51
52    fn write(&self, key: &BackendKey, data: &[u8]) -> Result<()> {
53        self.inner.lock().insert(key.clone(), data.to_vec());
54        Ok(())
55    }
56
57    fn delete(&self, key: &BackendKey) -> Result<()> {
58        self.inner.lock().remove(key);
59        Ok(())
60    }
61
62    fn list(&self, prefix: &str) -> Result<Vec<BackendKey>> {
63        Ok(self
64            .inner
65            .lock()
66            .keys()
67            .filter(|k| k.as_str().starts_with(prefix))
68            .cloned()
69            .collect())
70    }
71
72    fn exists(&self, key: &BackendKey) -> Result<bool> {
73        Ok(self.inner.lock().contains_key(key))
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    /// **Proves:** `MemoryBackend` satisfies the [`KeychainBackend`]
82    /// contract end-to-end — write then read recovers the blob; `exists`
83    /// returns `true` for written keys and `false` for deleted ones.
84    ///
85    /// **Why it matters:** Dependent crates (`apps/validator`,
86    /// `dig-l1-wallet`) build their tests on `MemoryBackend`. If the
87    /// in-memory backend drifted from the `FileBackend` semantics (e.g.,
88    /// `exists` stayed `true` after delete, or `read` returned stale bytes
89    /// after overwrite), those tests would pass in CI and fail in
90    /// production.
91    ///
92    /// **Catches:** a regression in `delete` that leaks the key in the
93    /// internal `HashMap`, or an `exists` override that short-circuits
94    /// without consulting the map.
95    #[test]
96    fn roundtrip() {
97        let be = MemoryBackend::new();
98        let k = BackendKey::new("x");
99        be.write(&k, b"data").unwrap();
100        assert_eq!(be.read(&k).unwrap(), b"data");
101        assert!(be.exists(&k).unwrap());
102        be.delete(&k).unwrap();
103        assert!(!be.exists(&k).unwrap());
104    }
105}