unc_sdk/store/lookup_set/
mod.rs

1mod impls;
2
3use crate::store::key::{Identity, ToKey};
4use crate::{env, IntoStorageKey};
5use borsh::BorshSerialize;
6use std::borrow::Borrow;
7use std::fmt;
8use std::marker::PhantomData;
9
10use unc_sdk_macros::unc;
11
12/// A non-iterable implementation of a set that stores its content directly on the storage trie.
13///
14/// This set stores the values under a hash of the set's `prefix` and [`BorshSerialize`] of the
15/// value and transformed using the set's [`ToKey`] implementation.
16///
17/// The default hash function for [`LookupSet`] is [`Identity`] which just prefixes the serialized
18/// key object and uses these bytes as the key. This is to be backwards-compatible with
19/// [`collections::LookupSet`](crate::collections::LookupSet) and be fast for small keys.
20/// To use a custom function, use [`with_hasher`]. Alternative builtin hash functions can be found
21/// at [`unc_sdk::store::key`](crate::store::key).
22///
23/// # Examples
24/// ```
25/// use unc_sdk::store::LookupSet;
26///
27/// // Initializes a set, the generic types can be inferred to `LookupSet<String, Identity>`
28/// // The `b"a"` parameter is a prefix for the storage keys of this data structure.
29/// let mut books = LookupSet::new(b"a");
30///
31///
32/// // Add some books.
33/// books.insert("A Dance With Dragons".to_string());
34/// books.insert("To Kill a Mockingbird".to_string());
35/// books.insert("The Odyssey".to_string());
36/// books.insert("The Great Gatsby".to_string());
37///
38/// // Check for a specific one.
39/// assert!(!books.contains("The Winds of Winter"));
40/// assert!(books.contains("The Odyssey"));
41///
42/// // Remove a book.
43/// books.remove("The Odyssey");
44///
45/// assert!(!books.contains("The Odyssey"));
46/// ```
47///
48/// [`with_hasher`]: Self::with_hasher
49#[unc(inside_uncsdk)]
50pub struct LookupSet<T, H = Identity>
51where
52    T: BorshSerialize,
53    H: ToKey,
54{
55    prefix: Box<[u8]>,
56
57    #[borsh(skip)]
58    hasher: PhantomData<fn() -> (T, H)>,
59}
60
61impl<T, H> fmt::Debug for LookupSet<T, H>
62where
63    T: BorshSerialize,
64    H: ToKey,
65{
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        f.debug_struct("LookupSet").field("prefix", &self.prefix).finish()
68    }
69}
70
71impl<T> LookupSet<T, Identity>
72where
73    T: BorshSerialize,
74{
75    /// Initialize new [`LookupSet`] with the prefix provided.
76    ///
77    /// This prefix can be anything that implements [`IntoStorageKey`]. The prefix is used when
78    /// storing and looking up values in storage to ensure no collisions with other collections.
79    #[inline]
80    pub fn new<S>(prefix: S) -> Self
81    where
82        S: IntoStorageKey,
83    {
84        Self::with_hasher(prefix)
85    }
86}
87
88impl<T, H> LookupSet<T, H>
89where
90    T: BorshSerialize,
91    H: ToKey,
92{
93    /// Initialize a [`LookupSet`] with a custom hash function.
94    ///
95    /// # Example
96    /// ```
97    /// use unc_sdk::store::{LookupSet, key::Keccak256};
98    ///
99    /// let map = LookupSet::<String, Keccak256>::with_hasher(b"m");
100    /// ```
101    pub fn with_hasher<S>(prefix: S) -> Self
102    where
103        S: IntoStorageKey,
104    {
105        Self { prefix: prefix.into_storage_key().into_boxed_slice(), hasher: Default::default() }
106    }
107
108    /// Returns `true` if the set contains the specified value.
109    ///
110    /// The value may be any borrowed form of the set's value type, but
111    /// [`BorshSerialize`] on the borrowed form *must* match those for the value type.
112    pub fn contains<Q: ?Sized>(&self, value: &Q) -> bool
113    where
114        T: Borrow<Q>,
115        Q: BorshSerialize,
116    {
117        let lookup_key = H::to_key(&self.prefix, value, &mut Vec::new());
118        env::storage_has_key(lookup_key.as_ref())
119    }
120
121    /// Adds a value to the set.
122    ///
123    /// If the set did not have this value present, true is returned.
124    ///
125    /// If the set did have this value present, false is returned.
126    pub fn insert(&mut self, value: T) -> bool {
127        let lookup_key = H::to_key(&self.prefix, &value, &mut Vec::new());
128        !env::storage_write(lookup_key.as_ref(), &[])
129    }
130
131    /// Removes a value from the set. Returns whether the value was present in the set.
132    ///
133    /// The value may be any borrowed form of the set's value type, but
134    /// [`BorshSerialize`] on the borrowed form *must* match those for the value type.
135    pub fn remove<Q: ?Sized>(&mut self, value: &Q) -> bool
136    where
137        T: Borrow<Q>,
138        Q: BorshSerialize,
139    {
140        let lookup_key = H::to_key(&self.prefix, value, &mut Vec::new());
141        env::storage_remove(lookup_key.as_ref())
142    }
143}
144
145#[cfg(not(target_arch = "wasm32"))]
146#[cfg(test)]
147mod tests {
148    use super::LookupSet;
149    use crate::store::key::{Identity, Keccak256, ToKey};
150    use crate::test_utils::test_env::setup_free;
151    use arbitrary::{Arbitrary, Unstructured};
152    use rand::seq::SliceRandom;
153    use rand::RngCore;
154    use rand::{Rng, SeedableRng};
155    use std::collections::HashSet;
156
157    #[test]
158    fn test_insert_contains() {
159        let mut set = LookupSet::new(b"m");
160        let mut rng = rand_xorshift::XorShiftRng::seed_from_u64(0);
161        let mut baseline = HashSet::new();
162        for _ in 0..100 {
163            let value = rng.gen::<u64>();
164            set.insert(value);
165            baseline.insert(value);
166        }
167        // Non existing
168        for _ in 0..100 {
169            let value = rng.gen::<u64>();
170            assert_eq!(set.contains(&value), baseline.contains(&value));
171        }
172        // Existing
173        for value in baseline.iter() {
174            assert!(set.contains(value));
175        }
176    }
177
178    #[test]
179    fn test_insert_remove() {
180        let mut set = LookupSet::new(b"m");
181        let mut rng = rand_xorshift::XorShiftRng::seed_from_u64(1);
182        let mut values = vec![];
183        for _ in 0..100 {
184            let value = rng.gen::<u64>();
185            values.push(value);
186            set.insert(value);
187        }
188        values.shuffle(&mut rng);
189        for value in values {
190            assert!(set.remove(&value));
191        }
192    }
193
194    #[test]
195    fn test_remove_last_reinsert() {
196        let mut set = LookupSet::new(b"m");
197        let value1 = 2u64;
198        set.insert(value1);
199        let value2 = 4u64;
200        set.insert(value2);
201
202        assert!(set.remove(&value2));
203        assert!(set.insert(value2));
204    }
205
206    #[test]
207    fn identity_compat_v1() {
208        use crate::collections::LookupSet as LS1;
209
210        let mut ls1 = LS1::new(b"m");
211        ls1.insert(&8u8);
212        ls1.insert(&0);
213        assert!(ls1.contains(&8));
214
215        let mut ls2 = LookupSet::<u8, _>::new(b"m");
216        assert!(ls2.contains(&8u8));
217        assert!(ls2.remove(&0));
218
219        assert!(!ls1.contains(&0));
220    }
221
222    #[test]
223    fn test_extend() {
224        let mut set = LookupSet::new(b"m");
225        let mut rng = rand_xorshift::XorShiftRng::seed_from_u64(4);
226        let mut values = vec![];
227        for _ in 0..100 {
228            let value = rng.gen::<u64>();
229            values.push(value);
230            set.insert(value);
231        }
232        for _ in 0..10 {
233            let mut tmp = vec![];
234            for _ in 0..=(rng.gen::<u64>() % 20 + 1) {
235                let value = rng.gen::<u64>();
236                tmp.push(value);
237            }
238            values.extend(tmp.iter().cloned());
239            set.extend(tmp.iter().cloned());
240        }
241
242        for value in values {
243            assert!(set.contains(&value));
244        }
245    }
246
247    #[test]
248    fn test_debug() {
249        let set = LookupSet::<u8>::new(b"m");
250
251        assert_eq!(format!("{:?}", set), "LookupSet { prefix: [109] }")
252    }
253
254    #[test]
255    fn test_flush_on_drop() {
256        let mut set = LookupSet::<_, Keccak256>::with_hasher(b"m");
257
258        // Set a value, which does not write to storage yet
259        set.insert(5u8);
260        assert!(set.contains(&5u8));
261
262        // Drop the set which should flush all data
263        drop(set);
264
265        // Create a duplicate set which references same data
266        let dup_set = LookupSet::<u8, Keccak256>::with_hasher(b"m");
267
268        // New map can now load the value
269        assert!(dup_set.contains(&5u8));
270    }
271
272    #[test]
273    fn test_contains_all_states() {
274        let mut set = LookupSet::new(b"m");
275        // Uninitialized value that is absent from the trie
276        assert!(!set.contains(&8));
277        // Initialized value which state is `Absent`
278        assert!(!set.contains(&8));
279        set.insert(8u8);
280        // Initialized value which state is `Inserted`
281        assert!(set.contains(&8));
282        set.remove(&8);
283        // Initialized value which state is `Deleted`
284        assert!(!set.contains(&8));
285        set.insert(8);
286
287        // Drop the set which should flush all data
288        drop(set);
289
290        let dup_set = LookupSet::<u8, _>::new(b"m");
291        // Uninitialized value that is present on the trie
292        assert!(dup_set.contains(&8));
293        // Initialized value which state is `Present`
294        assert!(dup_set.contains(&8));
295    }
296
297    #[test]
298    fn test_insert_all_states() {
299        let mut set = LookupSet::new(b"m");
300        // Uninitialized value that is absent from the trie
301        assert!(set.insert(8u8));
302        // Initialized value which state is `Inserted`
303        assert!(!set.insert(8));
304        set.remove(&8);
305        // Initialized value which state is `Deleted`
306        assert!(set.insert(8));
307
308        {
309            let mut dup_set = LookupSet::new(b"m");
310            // Uninitialized value that is present on the trie
311            dup_set.insert(8u8);
312            assert!(dup_set.contains(&8));
313        }
314
315        {
316            let mut dup_set = LookupSet::new(b"m");
317            assert!(dup_set.contains(&8));
318            // Initialized value which state is `Present`
319            dup_set.insert(8u8);
320            assert!(dup_set.contains(&8));
321        }
322    }
323
324    #[test]
325    fn test_remove_all_states() {
326        let mut set = LookupSet::new(b"m");
327        // Uninitialized value that is absent from the trie
328        assert!(!set.remove(&8));
329        // Initialized value which state is `Absent`
330        assert!(!set.remove(&8));
331        set.insert(8);
332        // Initialized value which state is `Inserted`
333        assert!(set.remove(&8));
334        // Initialized value which state is `Deleted`
335        assert!(!set.remove(&8));
336
337        // Drop the set which should flush all data
338        set.insert(8u8);
339        drop(set);
340
341        {
342            let mut dup_set = LookupSet::new(b"m");
343            // Uninitialized value that is present on the trie
344            assert!(dup_set.remove(&8));
345            dup_set.insert(8u8);
346        }
347
348        {
349            let mut dup_set = LookupSet::<u8, _>::new(b"m");
350            assert!(dup_set.contains(&8));
351            // Initialized value which state is `Present`
352            assert!(dup_set.remove(&8));
353        }
354    }
355
356    #[test]
357    fn test_remove_present_after_insert() {
358        let lookup_key = Identity::to_key(b"m", &8u8, &mut Vec::new());
359        {
360            // Scoped to make sure set is dropped and persist changes
361            let mut set = LookupSet::new(b"m");
362            set.insert(8u8);
363        }
364        assert!(crate::env::storage_has_key(&lookup_key));
365        {
366            let mut set = LookupSet::new(b"m");
367            set.insert(8u8);
368            set.remove(&8);
369        }
370        assert!(!crate::env::storage_has_key(&lookup_key));
371        {
372            let set = LookupSet::<u8, _>::new(b"m");
373            assert!(!set.contains(&8u8));
374        }
375    }
376
377    #[derive(Arbitrary, Debug)]
378    enum Op {
379        Insert(u8),
380        Remove(u8),
381        Restore,
382        Contains(u8),
383    }
384
385    #[test]
386    fn test_arbitrary() {
387        setup_free();
388
389        let mut rng = rand_xorshift::XorShiftRng::seed_from_u64(0);
390        let mut buf = vec![0; 4096];
391        for _ in 0..512 {
392            // Clear storage in-between runs
393            crate::mock::with_mocked_blockchain(|b| b.take_storage());
394            rng.fill_bytes(&mut buf);
395
396            let mut ls = LookupSet::new(b"l");
397            let mut hs = HashSet::new();
398            let u = Unstructured::new(&buf);
399            if let Ok(ops) = Vec::<Op>::arbitrary_take_rest(u) {
400                for op in ops {
401                    match op {
402                        Op::Insert(v) => {
403                            let r1 = ls.insert(v);
404                            let r2 = hs.insert(v);
405                            assert_eq!(r1, r2)
406                        }
407                        Op::Remove(v) => {
408                            let r1 = ls.remove(&v);
409                            let r2 = hs.remove(&v);
410                            assert_eq!(r1, r2)
411                        }
412                        Op::Restore => {
413                            ls = LookupSet::new(b"l");
414                        }
415                        Op::Contains(v) => {
416                            let r1 = ls.contains(&v);
417                            let r2 = hs.contains(&v);
418                            assert_eq!(r1, r2)
419                        }
420                    }
421                }
422            }
423        }
424    }
425}