Skip to main content

drive/cache/
data_contract.rs

1use crate::drive::contract::DataContractFetchInfo;
2use dpp::data_contract::accessors::v0::DataContractV0Getters;
3use moka::ops::compute::Op;
4use moka::sync::Cache;
5use std::sync::Arc;
6
7/// DataContract cache that handles both global and block data
8pub struct DataContractCache {
9    global_cache: Cache<[u8; 32], Arc<DataContractFetchInfo>>,
10    block_cache: Cache<[u8; 32], Arc<DataContractFetchInfo>>,
11}
12
13impl DataContractCache {
14    /// Create a new DataContract cache instance
15    pub fn new(global_cache_max_capacity: u64, block_cache_max_capacity: u64) -> Self {
16        Self {
17            global_cache: Cache::new(global_cache_max_capacity),
18            block_cache: Cache::new(block_cache_max_capacity),
19        }
20    }
21
22    /// Inserts DataContract to block cache
23    /// otherwise to goes to global cache
24    ///
25    /// The insert is skipped if the cache already holds the same contract at a
26    /// **higher** version. Contract versions increase strictly monotonically —
27    /// the data contract update transition enforces `new == old + 1`, and system
28    /// contract migrations bump the version — so an insert carrying a lower
29    /// version is always a delayed writer racing a newer copy in, never fresh
30    /// information. CONSENSUS-CRITICAL: a read-only query thread fetches from
31    /// committed state without a transaction and populates the global cache from
32    /// what it read. If such a thread reads a contract, gets descheduled while
33    /// block execution rewrites that contract, and performs its insert after the
34    /// block cache is promoted, an unconditional insert would clobber the newer
35    /// contract with the stale one — and block execution would then serialize
36    /// documents against a different contract than a node whose cache was cold,
37    /// producing a different app hash from the same block. The check-and-insert
38    /// is atomic per key via moka's compute API, so there is no window between
39    /// the version comparison and the write. Same-version inserts still
40    /// overwrite: re-inserting an identical contract with a freshly calculated
41    /// fee is the normal cache-hit fee path.
42    pub fn insert(&self, fetch_info: Arc<DataContractFetchInfo>, is_block_cache: bool) {
43        let data_contract_id_bytes = fetch_info.contract.id().to_buffer();
44
45        let cache = if is_block_cache {
46            &self.block_cache
47        } else {
48            &self.global_cache
49        };
50
51        cache
52            .entry(data_contract_id_bytes)
53            .and_compute_with(|existing| match existing {
54                Some(entry) if entry.value().contract.version() > fetch_info.contract.version() => {
55                    Op::Nop
56                }
57                _ => Op::Put(Arc::clone(&fetch_info)),
58            });
59    }
60
61    /// Tries to get a data contract from block cache if present
62    /// if block cache doesn't have the contract
63    /// then it tries get the contract from global cache
64    pub fn get(
65        &self,
66        contract_id: [u8; 32],
67        is_block_cache: bool,
68    ) -> Option<Arc<DataContractFetchInfo>> {
69        let maybe_fetch_info = if is_block_cache {
70            self.block_cache.get(&contract_id)
71        } else {
72            None
73        };
74
75        maybe_fetch_info.or_else(|| self.global_cache.get(&contract_id))
76    }
77
78    /// Remove contract from both block and global cache
79    pub fn remove(&self, contract_id: [u8; 32]) {
80        self.block_cache.remove(&contract_id);
81        self.global_cache.remove(&contract_id);
82    }
83
84    /// Move block cache to global cache
85    pub fn merge_and_clear_block_cache(&self) {
86        for (contract_id, fetch_info) in self.block_cache.into_iter() {
87            self.global_cache
88                .insert(Arc::unwrap_or_clone(contract_id), fetch_info);
89        }
90        self.clear_block_cache();
91    }
92
93    /// Clear block cache
94    pub fn clear_block_cache(&self) {
95        self.block_cache.invalidate_all();
96    }
97
98    /// Clear cache
99    pub fn clear(&self) {
100        self.block_cache.invalidate_all();
101        self.global_cache.invalidate_all();
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use dpp::version::PlatformVersion;
109
110    mod get {
111        use super::*;
112        use dpp::data_contract::accessors::v0::{DataContractV0Getters, DataContractV0Setters};
113
114        #[test]
115        fn test_get_from_global_cache_when_block_cache_is_not_requested() {
116            let data_contract_cache = DataContractCache::new(10, 10);
117
118            let protocol_version = PlatformVersion::latest().protocol_version;
119
120            // Create global contract
121            let fetch_info_global = Arc::new(DataContractFetchInfo::dpns_contract_fixture(
122                protocol_version,
123            ));
124
125            let contract_id = fetch_info_global.contract.id().to_buffer();
126
127            data_contract_cache
128                .global_cache
129                .insert(contract_id, Arc::clone(&fetch_info_global));
130
131            // Create transactional contract with a new version
132            let mut fetch_info_block =
133                DataContractFetchInfo::dpns_contract_fixture(protocol_version);
134
135            fetch_info_block.contract.increment_version();
136
137            let fetch_info_block_boxed = Arc::new(fetch_info_block);
138
139            data_contract_cache
140                .block_cache
141                .insert(contract_id, Arc::clone(&fetch_info_block_boxed));
142
143            let fetch_info_from_cache = data_contract_cache
144                .get(contract_id, false)
145                .expect("should be present");
146
147            assert_eq!(fetch_info_from_cache, fetch_info_global)
148        }
149
150        #[test]
151        fn test_get_from_global_cache_when_block_cache_does_not_have_contract() {
152            let data_contract_cache = DataContractCache::new(10, 10);
153
154            let protocol_version = PlatformVersion::latest().protocol_version;
155
156            let fetch_info_global = Arc::new(DataContractFetchInfo::dpns_contract_fixture(
157                protocol_version,
158            ));
159
160            let contract_id = fetch_info_global.contract.id().to_buffer();
161
162            data_contract_cache
163                .global_cache
164                .insert(contract_id, Arc::clone(&fetch_info_global));
165
166            let fetch_info_from_cache = data_contract_cache
167                .get(contract_id, true)
168                .expect("should be present");
169
170            assert_eq!(fetch_info_from_cache, fetch_info_global)
171        }
172
173        #[test]
174        fn test_get_from_block_cache() {
175            let data_contract_cache = DataContractCache::new(10, 10);
176
177            let protocol_version = PlatformVersion::latest().protocol_version;
178
179            let fetch_info_block = Arc::new(DataContractFetchInfo::dpns_contract_fixture(
180                protocol_version,
181            ));
182
183            let contract_id = fetch_info_block.contract.id().to_buffer();
184
185            data_contract_cache
186                .block_cache
187                .insert(contract_id, Arc::clone(&fetch_info_block));
188
189            let fetch_info_from_cache = data_contract_cache
190                .get(contract_id, true)
191                .expect("should be present");
192
193            assert_eq!(fetch_info_from_cache, fetch_info_block)
194        }
195    }
196
197    mod insert {
198        use super::*;
199        use dpp::data_contract::accessors::v0::{DataContractV0Getters, DataContractV0Setters};
200
201        /// Two copies of the SAME contract (same id) at the given versions. The
202        /// fixture generates a fresh contract id per call, so both copies must
203        /// derive from a single fixture.
204        fn same_contract_at_versions(
205            first: u32,
206            second: u32,
207        ) -> (Arc<DataContractFetchInfo>, Arc<DataContractFetchInfo>) {
208            let fetch_info = DataContractFetchInfo::dpns_contract_fixture(
209                PlatformVersion::latest().protocol_version,
210            );
211            let mut first_info = fetch_info.clone();
212            first_info.contract.set_version(first);
213            let mut second_info = fetch_info;
214            second_info.contract.set_version(second);
215            (Arc::new(first_info), Arc::new(second_info))
216        }
217
218        /// A delayed insert carrying an older contract version must not clobber a newer
219        /// entry. This is the query-thread race: a read-only query reads a contract from
220        /// committed state, is descheduled while block execution rewrites the contract,
221        /// and performs its cache insert only after the migrated contract was promoted
222        /// to the global cache.
223        #[test]
224        fn test_insert_does_not_overwrite_newer_version_with_older() {
225            let data_contract_cache = DataContractCache::new(10, 10);
226
227            let (stale, newer) = same_contract_at_versions(1, 2);
228            let contract_id = newer.contract.id().to_buffer();
229            data_contract_cache.insert(newer, false);
230
231            // The delayed stale insert
232            data_contract_cache.insert(stale, false);
233
234            let cached = data_contract_cache
235                .get(contract_id, false)
236                .expect("should be present");
237            assert_eq!(cached.contract.version(), 2);
238        }
239
240        /// Same-version inserts must overwrite: re-inserting the same contract with a
241        /// freshly calculated fee is the normal cache-hit fee path.
242        #[test]
243        fn test_insert_overwrites_same_version() {
244            let data_contract_cache = DataContractCache::new(10, 10);
245
246            let (mut original, mut with_fee) = same_contract_at_versions(1, 1);
247            let contract_id = original.contract.id().to_buffer();
248            Arc::make_mut(&mut original).fee = None;
249            data_contract_cache.insert(original, false);
250
251            Arc::make_mut(&mut with_fee).fee =
252                Some(dpp::fee::fee_result::FeeResult::new_from_processing_fee(1));
253            data_contract_cache.insert(with_fee, false);
254
255            let cached = data_contract_cache
256                .get(contract_id, false)
257                .expect("should be present");
258            assert!(cached.fee.is_some(), "same-version insert must overwrite");
259        }
260
261        /// The full race, end to end: block execution seeds the migrated (newer)
262        /// contract into the block cache, the block finalizes and promotes it to the
263        /// global cache, and only then does the delayed query thread insert the
264        /// pre-migration contract it read before the rewrite. The promoted contract
265        /// must survive.
266        #[test]
267        fn test_delayed_stale_insert_after_promotion_does_not_stick() {
268            let data_contract_cache = DataContractCache::new(10, 10);
269
270            // The pre-migration contract, as read from committed state by a query
271            // thread that will be descheduled before its insert.
272            let (stale, migrated) = same_contract_at_versions(1, 2);
273            let contract_id = stale.contract.id().to_buffer();
274
275            // Block execution writes the migrated contract and seeds the block cache.
276            data_contract_cache.insert(migrated, true);
277
278            // The block finalizes: block cache promotes to global.
279            data_contract_cache.merge_and_clear_block_cache();
280
281            // The query thread wakes up and performs its stale insert.
282            data_contract_cache.insert(stale, false);
283
284            let cached = data_contract_cache
285                .get(contract_id, false)
286                .expect("should be present");
287            assert_eq!(
288                cached.contract.version(),
289                2,
290                "the promoted migrated contract must survive a delayed stale insert"
291            );
292        }
293    }
294
295    mod remove {
296        use super::*;
297
298        #[test]
299        fn test_remove_clears_global_cache_entry() {
300            let data_contract_cache = DataContractCache::new(10, 10);
301
302            let protocol_version = PlatformVersion::latest().protocol_version;
303            let fetch_info = Arc::new(DataContractFetchInfo::dpns_contract_fixture(
304                protocol_version,
305            ));
306            let contract_id = fetch_info.contract.id().to_buffer();
307
308            data_contract_cache.insert(fetch_info, false);
309            data_contract_cache.remove(contract_id);
310
311            assert!(data_contract_cache.get(contract_id, false).is_none());
312        }
313
314        #[test]
315        fn test_remove_clears_entry_from_both_caches() {
316            let data_contract_cache = DataContractCache::new(10, 10);
317
318            let protocol_version = PlatformVersion::latest().protocol_version;
319            let fetch_info_global = Arc::new(DataContractFetchInfo::dpns_contract_fixture(
320                protocol_version,
321            ));
322            let contract_id = fetch_info_global.contract.id().to_buffer();
323            let fetch_info_block = Arc::clone(&fetch_info_global);
324
325            data_contract_cache.insert(fetch_info_global, false);
326            data_contract_cache.insert(fetch_info_block, true);
327            data_contract_cache.remove(contract_id);
328
329            assert!(data_contract_cache.block_cache.get(&contract_id).is_none());
330            assert!(data_contract_cache.global_cache.get(&contract_id).is_none());
331        }
332    }
333
334    mod merge_and_clear_block_cache {
335        use super::*;
336
337        #[test]
338        fn test_merge_moves_block_items_to_global_cache() {
339            let data_contract_cache = DataContractCache::new(10, 10);
340
341            let protocol_version = PlatformVersion::latest().protocol_version;
342            let fetch_info = Arc::new(DataContractFetchInfo::dpns_contract_fixture(
343                protocol_version,
344            ));
345            let contract_id = fetch_info.contract.id().to_buffer();
346
347            data_contract_cache.insert(fetch_info, true);
348            data_contract_cache.merge_and_clear_block_cache();
349
350            assert!(data_contract_cache.global_cache.get(&contract_id).is_some());
351        }
352
353        #[test]
354        fn test_merge_clears_block_cache() {
355            let data_contract_cache = DataContractCache::new(10, 10);
356
357            let protocol_version = PlatformVersion::latest().protocol_version;
358            let fetch_info = Arc::new(DataContractFetchInfo::dpns_contract_fixture(
359                protocol_version,
360            ));
361            let contract_id = fetch_info.contract.id().to_buffer();
362
363            data_contract_cache.insert(fetch_info, true);
364            data_contract_cache.merge_and_clear_block_cache();
365
366            assert!(data_contract_cache.block_cache.get(&contract_id).is_none());
367        }
368    }
369
370    mod clear {
371        use super::*;
372
373        #[test]
374        fn test_clear_empties_global_and_block_caches() {
375            let data_contract_cache = DataContractCache::new(10, 10);
376
377            let protocol_version = PlatformVersion::latest().protocol_version;
378            let fetch_info_global = Arc::new(DataContractFetchInfo::dpns_contract_fixture(
379                protocol_version,
380            ));
381            let contract_id = fetch_info_global.contract.id().to_buffer();
382            let fetch_info_block = Arc::clone(&fetch_info_global);
383
384            data_contract_cache.insert(fetch_info_global, false);
385            data_contract_cache.insert(fetch_info_block, true);
386            data_contract_cache.clear();
387
388            assert!(data_contract_cache.get(contract_id, false).is_none());
389            assert!(data_contract_cache.block_cache.get(&contract_id).is_none());
390        }
391    }
392}