Skip to main content

cdk_common/database/
kvstore.rs

1//! Key-Value Store Database traits and utilities
2//!
3//! This module provides shared KVStore functionality that can be used by both
4//! mint and wallet database implementations.
5
6use async_trait::async_trait;
7
8use super::{DbTransactionFinalizer, Error};
9
10/// Valid ASCII characters for namespace and key strings in KV store
11pub const KVSTORE_NAMESPACE_KEY_ALPHABET: &str =
12    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-";
13
14/// Maximum length for namespace and key strings in KV store
15pub const KVSTORE_NAMESPACE_KEY_MAX_LEN: usize = 120;
16
17/// Validates that a string contains only valid KV store characters and is within length limits
18pub fn validate_kvstore_string(s: &str) -> Result<(), Error> {
19    if s.len() > KVSTORE_NAMESPACE_KEY_MAX_LEN {
20        return Err(Error::KVStoreInvalidKey(format!(
21            "{KVSTORE_NAMESPACE_KEY_MAX_LEN} exceeds maximum length of key characters"
22        )));
23    }
24
25    if !s
26        .chars()
27        .all(|c| KVSTORE_NAMESPACE_KEY_ALPHABET.contains(c))
28    {
29        return Err(Error::KVStoreInvalidKey("key contains invalid characters. Only ASCII letters, numbers, underscore, and hyphen are allowed".to_string()));
30    }
31
32    Ok(())
33}
34
35/// Validates namespace and key parameters for KV store operations
36pub fn validate_kvstore_params(
37    primary_namespace: &str,
38    secondary_namespace: &str,
39    key: Option<&str>,
40) -> Result<(), Error> {
41    // Validate primary namespace
42    validate_kvstore_string(primary_namespace)?;
43
44    // Validate secondary namespace
45    validate_kvstore_string(secondary_namespace)?;
46
47    // Check empty namespace rules
48    if primary_namespace.is_empty() && !secondary_namespace.is_empty() {
49        return Err(Error::KVStoreInvalidKey(
50            "If primary_namespace is empty, secondary_namespace must also be empty".to_string(),
51        ));
52    }
53
54    if let Some(key) = key {
55        // Validate key
56        validate_kvstore_string(key)?;
57
58        // Check for potential collisions between keys and namespaces in the same namespace
59        let namespace_key = format!("{primary_namespace}/{secondary_namespace}");
60        if key == primary_namespace || key == secondary_namespace || key == namespace_key {
61            return Err(Error::KVStoreInvalidKey(format!(
62                "Key '{key}' conflicts with namespace names"
63            )));
64        }
65    }
66
67    Ok(())
68}
69
70/// Key-Value Store Transaction trait
71#[async_trait]
72pub trait KVStoreTransaction<Error>: DbTransactionFinalizer<Err = Error> {
73    /// Read value from key-value store
74    async fn kv_read(
75        &mut self,
76        primary_namespace: &str,
77        secondary_namespace: &str,
78        key: &str,
79    ) -> Result<Option<Vec<u8>>, Error>;
80
81    /// Write value to key-value store
82    async fn kv_write(
83        &mut self,
84        primary_namespace: &str,
85        secondary_namespace: &str,
86        key: &str,
87        value: &[u8],
88    ) -> Result<(), Error>;
89
90    /// Write value to key-value store only when the key does not exist.
91    ///
92    /// Returns `true` when the value was written and `false` when the key
93    /// already exists. Implementations must perform the check-and-insert
94    /// atomically (for example `INSERT ... ON CONFLICT DO NOTHING` with a
95    /// rows-affected check).
96    async fn kv_write_if_absent(
97        &mut self,
98        primary_namespace: &str,
99        secondary_namespace: &str,
100        key: &str,
101        value: &[u8],
102    ) -> Result<bool, Error>;
103
104    /// Remove value from key-value store
105    async fn kv_remove(
106        &mut self,
107        primary_namespace: &str,
108        secondary_namespace: &str,
109        key: &str,
110    ) -> Result<(), Error>;
111
112    /// Replace a value only when it currently equals `expected`.
113    ///
114    /// Returns `true` when the value was replaced and `false` when the key
115    /// does not exist or holds a different value. Implementations must perform
116    /// the check-and-write atomically (for example
117    /// `UPDATE ... WHERE value = ?` with a rows-affected check).
118    async fn kv_write_if_equals(
119        &mut self,
120        primary_namespace: &str,
121        secondary_namespace: &str,
122        key: &str,
123        expected: &[u8],
124        replacement: &[u8],
125    ) -> Result<bool, Error>;
126
127    /// List keys in a namespace
128    async fn kv_list(
129        &mut self,
130        primary_namespace: &str,
131        secondary_namespace: &str,
132    ) -> Result<Vec<String>, Error>;
133}
134
135/// Key-Value Store Database trait
136#[async_trait]
137pub trait KVStoreDatabase {
138    /// KV Store Database Error
139    type Err: Into<Error> + From<Error>;
140
141    /// Read value from key-value store
142    async fn kv_read(
143        &self,
144        primary_namespace: &str,
145        secondary_namespace: &str,
146        key: &str,
147    ) -> Result<Option<Vec<u8>>, Self::Err>;
148
149    /// List keys in a namespace
150    async fn kv_list(
151        &self,
152        primary_namespace: &str,
153        secondary_namespace: &str,
154    ) -> Result<Vec<String>, Self::Err>;
155}
156
157/// Key-value store capability for atomic compare-and-swap operations.
158///
159/// This is a separate capability because not every [`KVStoreDatabase`]
160/// backend can guarantee an atomic conditional write.
161#[async_trait]
162pub trait KVStoreCompareAndSwap: KVStoreDatabase {
163    /// Replaces a value only when its current value matches `expected`.
164    ///
165    /// An `expected` value of `None` inserts only when the key does not exist.
166    /// Returns `true` when the value was changed and `false` when the
167    /// expectation did not match.
168    async fn kv_compare_and_swap(
169        &self,
170        primary_namespace: &str,
171        secondary_namespace: &str,
172        key: &str,
173        expected: Option<&[u8]>,
174        replacement: &[u8],
175    ) -> Result<bool, Self::Err>;
176}
177
178/// Key-Value Store trait combining read operations with transaction support
179#[async_trait]
180pub trait KVStore: KVStoreDatabase {
181    /// Begins a KV transaction
182    async fn begin_transaction(
183        &self,
184    ) -> Result<Box<dyn KVStoreTransaction<Self::Err> + Send + Sync>, Error>;
185}
186
187#[cfg(test)]
188mod tests {
189    use super::{
190        validate_kvstore_params, validate_kvstore_string, KVSTORE_NAMESPACE_KEY_ALPHABET,
191        KVSTORE_NAMESPACE_KEY_MAX_LEN,
192    };
193
194    #[test]
195    fn test_validate_kvstore_string_valid_inputs() {
196        // Test valid strings
197        assert!(validate_kvstore_string("").is_ok());
198        assert!(validate_kvstore_string("abc").is_ok());
199        assert!(validate_kvstore_string("ABC").is_ok());
200        assert!(validate_kvstore_string("123").is_ok());
201        assert!(validate_kvstore_string("test_key").is_ok());
202        assert!(validate_kvstore_string("test-key").is_ok());
203        assert!(validate_kvstore_string("test_KEY-123").is_ok());
204
205        // Test max length string
206        let max_length_str = "a".repeat(KVSTORE_NAMESPACE_KEY_MAX_LEN);
207        assert!(validate_kvstore_string(&max_length_str).is_ok());
208    }
209
210    #[test]
211    fn test_validate_kvstore_string_invalid_length() {
212        // Test string too long
213        let too_long_str = "a".repeat(KVSTORE_NAMESPACE_KEY_MAX_LEN + 1);
214        let result = validate_kvstore_string(&too_long_str);
215        assert!(result.is_err());
216        assert!(result
217            .unwrap_err()
218            .to_string()
219            .contains("exceeds maximum length"));
220    }
221
222    #[test]
223    fn test_validate_kvstore_string_invalid_characters() {
224        // Test invalid characters
225        let invalid_chars = vec![
226            "test@key",  // @
227            "test key",  // space
228            "test.key",  // .
229            "test/key",  // /
230            "test\\key", // \
231            "test+key",  // +
232            "test=key",  // =
233            "test!key",  // !
234            "test#key",  // #
235            "test$key",  // $
236            "test%key",  // %
237            "test&key",  // &
238            "test*key",  // *
239            "test(key",  // (
240            "test)key",  // )
241            "test[key",  // [
242            "test]key",  // ]
243            "test{key",  // {
244            "test}key",  // }
245            "test|key",  // |
246            "test;key",  // ;
247            "test:key",  // :
248            "test'key",  // '
249            "test\"key", // "
250            "test<key",  // <
251            "test>key",  // >
252            "test,key",  // ,
253            "test?key",  // ?
254            "test~key",  // ~
255            "test`key",  // `
256        ];
257
258        for invalid_str in invalid_chars {
259            let result = validate_kvstore_string(invalid_str);
260            assert!(result.is_err(), "Expected '{}' to be invalid", invalid_str);
261            assert!(result
262                .unwrap_err()
263                .to_string()
264                .contains("invalid characters"));
265        }
266    }
267
268    #[test]
269    fn test_validate_kvstore_params_valid() {
270        // Test valid parameter combinations
271        assert!(validate_kvstore_params("primary", "secondary", Some("key")).is_ok());
272        assert!(validate_kvstore_params("primary", "", Some("key")).is_ok());
273        assert!(validate_kvstore_params("", "", Some("key")).is_ok());
274        assert!(validate_kvstore_params("p1", "s1", Some("different_key")).is_ok());
275    }
276
277    #[test]
278    fn test_validate_kvstore_params_empty_namespace_rules() {
279        // Test empty namespace rules: if primary is empty, secondary must be empty too
280        let result = validate_kvstore_params("", "secondary", Some("key"));
281        assert!(result.is_err());
282        assert!(result
283            .unwrap_err()
284            .to_string()
285            .contains("If primary_namespace is empty"));
286    }
287
288    #[test]
289    fn test_validate_kvstore_params_collision_prevention() {
290        // Test collision prevention between keys and namespaces
291        let test_cases = vec![
292            ("primary", "secondary", "primary"), // key matches primary namespace
293            ("primary", "secondary", "secondary"), // key matches secondary namespace
294        ];
295
296        for (primary, secondary, key) in test_cases {
297            let result = validate_kvstore_params(primary, secondary, Some(key));
298            assert!(
299                result.is_err(),
300                "Expected collision for key '{}' with namespaces '{}'/'{}'",
301                key,
302                primary,
303                secondary
304            );
305            let error_msg = result.unwrap_err().to_string();
306            assert!(error_msg.contains("conflicts with namespace"));
307        }
308
309        // Test that a combined namespace string would be invalid due to the slash character
310        let result = validate_kvstore_params("primary", "secondary", Some("primary_secondary"));
311        assert!(result.is_ok(), "This should be valid - no actual collision");
312    }
313
314    #[test]
315    fn test_validate_kvstore_params_invalid_strings() {
316        // Test invalid characters in any parameter
317        let result = validate_kvstore_params("primary@", "secondary", Some("key"));
318        assert!(result.is_err());
319
320        let result = validate_kvstore_params("primary", "secondary!", Some("key"));
321        assert!(result.is_err());
322
323        let result = validate_kvstore_params("primary", "secondary", Some("key with space"));
324        assert!(result.is_err());
325    }
326
327    #[test]
328    fn test_alphabet_constants() {
329        // Verify the alphabet constant is as expected
330        assert_eq!(
331            KVSTORE_NAMESPACE_KEY_ALPHABET,
332            "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"
333        );
334        assert_eq!(KVSTORE_NAMESPACE_KEY_MAX_LEN, 120);
335    }
336
337    #[test]
338    fn test_alphabet_coverage() {
339        // Test that all valid characters are actually accepted
340        for ch in KVSTORE_NAMESPACE_KEY_ALPHABET.chars() {
341            let test_str = ch.to_string();
342            assert!(
343                validate_kvstore_string(&test_str).is_ok(),
344                "Character '{}' should be valid",
345                ch
346            );
347        }
348    }
349
350    #[test]
351    fn test_namespace_segmentation_examples() {
352        // Test realistic namespace segmentation scenarios
353
354        // Valid segmentation examples
355        let valid_examples = vec![
356            ("wallets", "user123", "balance"),
357            ("quotes", "mint", "quote_12345"),
358            ("keysets", "", "active_keyset"),
359            ("", "", "global_config"),
360            ("auth", "session_456", "token"),
361            ("mint_info", "", "version"),
362        ];
363
364        for (primary, secondary, key) in valid_examples {
365            assert!(
366                validate_kvstore_params(primary, secondary, Some(key)).is_ok(),
367                "Valid example should pass: '{}'/'{}'/'{}'",
368                primary,
369                secondary,
370                key
371            );
372        }
373    }
374
375    #[test]
376    fn test_per_namespace_uniqueness() {
377        // This test documents the requirement that implementations should ensure
378        // per-namespace key uniqueness. The validation function doesn't enforce
379        // database-level uniqueness (that's handled by the database schema),
380        // but ensures naming conflicts don't occur between keys and namespaces.
381
382        // These should be valid (different namespaces)
383        assert!(validate_kvstore_params("ns1", "sub1", Some("key1")).is_ok());
384        assert!(validate_kvstore_params("ns2", "sub1", Some("key1")).is_ok()); // same key, different primary namespace
385        assert!(validate_kvstore_params("ns1", "sub2", Some("key1")).is_ok()); // same key, different secondary namespace
386
387        // These should fail (collision within namespace)
388        assert!(validate_kvstore_params("ns1", "sub1", Some("ns1")).is_err()); // key conflicts with primary namespace
389        assert!(validate_kvstore_params("ns1", "sub1", Some("sub1")).is_err()); // key conflicts with secondary namespace
390    }
391}