Skip to main content

poi_rs/cache/
in_memory.rs

1// Copyright 2020-2026 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::BTreeMap;
5use std::sync::Arc;
6
7use iota_types::committee::Committee;
8#[cfg(test)]
9use iota_types::committee::EpochId;
10use tokio::sync::RwLock;
11
12use super::{CommitteeCache, CommitteeCacheError, CommitteeCacheKey};
13
14/// In-memory committee cache for application use and tests.
15#[derive(Clone, Debug, Default)]
16pub struct MemoryCommitteeCache {
17    committees: Arc<RwLock<BTreeMap<CommitteeCacheKey, Committee>>>,
18}
19
20impl MemoryCommitteeCache {
21    /// Creates an empty in-memory committee cache.
22    pub fn new() -> Self {
23        Self::default()
24    }
25
26    /// Returns the number of cached committees.
27    pub async fn len(&self) -> usize {
28        self.committees.read().await.len()
29    }
30
31    /// Returns whether the cache contains no committees.
32    pub async fn is_empty(&self) -> bool {
33        self.committees.read().await.is_empty()
34    }
35}
36
37#[async_trait::async_trait]
38impl CommitteeCache for MemoryCommitteeCache {
39    async fn committee(&self, key: CommitteeCacheKey) -> Result<Option<Committee>, CommitteeCacheError> {
40        Ok(self.committees.read().await.get(&key).cloned())
41    }
42
43    async fn store(&self, key: CommitteeCacheKey, committee: &Committee) -> Result<(), CommitteeCacheError> {
44        let epoch = committee.epoch;
45        if key.epoch() != epoch {
46            return Err(CommitteeCacheError::Conflict { epoch });
47        }
48
49        let mut committees = self.committees.write().await;
50
51        if committees.get(&key).is_some_and(|cached| cached != committee) {
52            return Err(CommitteeCacheError::Conflict { epoch });
53        }
54
55        committees.entry(key).or_insert_with(|| committee.clone());
56
57        Ok(())
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use iota_types::digests::ChainIdentifier;
64
65    use super::*;
66
67    fn committee_at(epoch: EpochId) -> Committee {
68        let (committee, _) = Committee::new_simple_test_committee();
69
70        Committee::new(epoch, committee.voting_rights.iter().cloned().collect())
71    }
72
73    fn chain_identifier(byte: u8) -> ChainIdentifier {
74        ChainIdentifier::from(iota_sdk_types::CheckpointDigest::new([byte; 32]))
75    }
76
77    fn key(chain_identifier: ChainIdentifier, epoch: EpochId) -> CommitteeCacheKey {
78        CommitteeCacheKey::new(chain_identifier, epoch)
79    }
80
81    #[tokio::test]
82    async fn new_cache_is_empty() {
83        let cache = MemoryCommitteeCache::new();
84        let chain_identifier = chain_identifier(1);
85
86        assert!(cache.is_empty().await);
87        assert_eq!(cache.len().await, 0);
88        assert!(cache.committee(key(chain_identifier, 7)).await.unwrap().is_none());
89    }
90
91    #[tokio::test]
92    async fn store_makes_a_committee_available_by_epoch() {
93        let cache = MemoryCommitteeCache::new();
94        let chain_identifier = chain_identifier(1);
95        let committee = committee_at(7);
96
97        cache.store(key(chain_identifier, 7), &committee).await.unwrap();
98
99        assert_eq!(
100            cache.committee(key(chain_identifier, 7)).await.unwrap(),
101            Some(committee)
102        );
103        assert_eq!(cache.len().await, 1);
104        assert!(!cache.is_empty().await);
105    }
106
107    #[tokio::test]
108    async fn storing_the_same_committee_is_idempotent() {
109        let cache = MemoryCommitteeCache::new();
110        let chain_identifier = chain_identifier(1);
111        let committee = committee_at(7);
112
113        cache.store(key(chain_identifier, 7), &committee).await.unwrap();
114        cache.store(key(chain_identifier, 7), &committee).await.unwrap();
115
116        assert_eq!(
117            cache.committee(key(chain_identifier, 7)).await.unwrap(),
118            Some(committee)
119        );
120        assert_eq!(cache.len().await, 1);
121    }
122
123    #[tokio::test]
124    async fn conflicting_committee_is_rejected_without_replacing_the_original() {
125        let cache = MemoryCommitteeCache::new();
126        let chain_identifier = chain_identifier(1);
127        let original = committee_at(7);
128        let (conflicting, _) = Committee::new_simple_test_committee_of_size(5);
129        let conflicting = Committee::new(7, conflicting.voting_rights.iter().cloned().collect());
130        cache.store(key(chain_identifier, 7), &original).await.unwrap();
131
132        let error = cache.store(key(chain_identifier, 7), &conflicting).await.unwrap_err();
133
134        assert!(matches!(error, CommitteeCacheError::Conflict { epoch: 7 }));
135        assert_eq!(cache.committee(key(chain_identifier, 7)).await.unwrap(), Some(original));
136        assert_eq!(cache.len().await, 1);
137    }
138
139    #[tokio::test]
140    async fn clones_share_cached_committees() {
141        let cache = MemoryCommitteeCache::new();
142        let clone = cache.clone();
143        let chain_identifier = chain_identifier(1);
144        let committee = committee_at(7);
145
146        cache.store(key(chain_identifier, 7), &committee).await.unwrap();
147
148        assert_eq!(
149            clone.committee(key(chain_identifier, 7)).await.unwrap(),
150            Some(committee)
151        );
152    }
153
154    #[tokio::test]
155    async fn the_same_epoch_is_isolated_between_networks() {
156        let cache = MemoryCommitteeCache::new();
157        let first_chain = chain_identifier(1);
158        let second_chain = chain_identifier(2);
159        let committee = committee_at(7);
160
161        cache
162            .store(key(first_chain, committee.epoch), &committee)
163            .await
164            .unwrap();
165
166        assert_eq!(
167            cache.committee(key(first_chain, committee.epoch)).await.unwrap(),
168            Some(committee.clone())
169        );
170        assert!(
171            cache
172                .committee(key(second_chain, committee.epoch))
173                .await
174                .unwrap()
175                .is_none()
176        );
177    }
178}