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 parking_lot::RwLock;
6use std::collections::HashSet;
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::Arc;
9
10/// How many blocks the cache had seen committed when the snapshot was taken.
11///
12/// A committed-state reader takes one **before** it reads state and hands it back together
13/// with the copy it read; [`DataContractCache::insert_committed`] drops the copy if a block
14/// was committed in between, because the copy may then predate a rewrite that block made.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub struct CommittedGeneration(u64);
17
18/// DataContract cache that handles both global and block data.
19///
20/// Two kinds of reader share this cache, and neither may be served the other's view of a
21/// contract:
22///
23/// * **Block execution** reads through the block transaction, on the single consensus
24///   thread. Its reads and rewrites land in the block cache, which is cleared when a block
25///   starts ([`Self::clear_block_cache`]) and promoted into the global cache once the block
26///   is committed ([`Self::merge_and_clear_block_cache`]).
27/// * **Committed-state readers** (the query threads) read with no transaction, concurrently
28///   with block execution, and populate the global cache with what they read. `check_tx`
29///   threads read through a fresh transaction of their own and never write to the cache.
30///
31/// CONSENSUS-CRITICAL. Block execution serializes documents and validates transitions
32/// against whatever contract definition this cache hands it. If two honest validators
33/// resolve the same contract differently while executing the same block, they write
34/// different bytes and produce different app hashes. Two mechanisms keep a transactional
35/// read from ever resolving to a definition older than the one its transaction holds:
36///
37/// * [`Self::mark_modified_in_block`]: once the block transaction rewrites a contract, a
38///   transactional read of it never falls back to the global cache. The global copy is
39///   committed state, which a committed-state reader may legitimately have (re)inserted
40///   after the rewrite, and which the transaction has moved past. A block-cache miss for
41///   such a contract goes to state through the transaction instead.
42/// * [`CommittedGeneration`]: a committed-state copy enters the global cache only if no block
43///   was committed between the read that produced it and the insert. Without this, a query
44///   thread that read a contract, was descheduled across the block's commit and promotion,
45///   and inserted afterwards would clobber the promoted definition with the pre-block one.
46pub struct DataContractCache {
47    global_cache: Cache<[u8; 32], Arc<DataContractFetchInfo>>,
48    block_cache: Cache<[u8; 32], Arc<DataContractFetchInfo>>,
49    /// Contracts the block transaction rewrote since the block cache was last cleared.
50    ///
51    /// This is a record of what the transaction changed, not a cache: [`Self::clear`] leaves
52    /// it alone. It is reset with the block cache at block start and drained at promotion.
53    block_modified: RwLock<HashSet<[u8; 32]>>,
54    /// Bumped once per promotion, that is once per committed block.
55    committed_generation: AtomicU64,
56}
57
58impl DataContractCache {
59    /// Create a new DataContract cache instance
60    pub fn new(global_cache_max_capacity: u64, block_cache_max_capacity: u64) -> Self {
61        Self {
62            global_cache: Cache::new(global_cache_max_capacity),
63            block_cache: Cache::new(block_cache_max_capacity),
64            block_modified: RwLock::new(HashSet::new()),
65            committed_generation: AtomicU64::new(0),
66        }
67    }
68
69    /// The snapshot a committed-state reader must take **before** reading state, to pass to
70    /// [`Self::insert_committed`] with what it read.
71    pub fn committed_generation(&self) -> CommittedGeneration {
72        CommittedGeneration(self.committed_generation.load(Ordering::SeqCst))
73    }
74
75    /// Inserts a contract read or rewritten through the block transaction into the block
76    /// cache.
77    ///
78    /// Only the consensus thread writes the block cache, so its inserts are sequential; the
79    /// insert is nevertheless skipped if the block cache already holds the same contract at
80    /// a strictly higher version, as defense in depth. Contract versions increase strictly
81    /// monotonically (the update transition enforces `new == old + 1`, token configuration
82    /// updates and system contract migrations bump the version), so a lower version is never
83    /// fresh information. Same-version inserts overwrite: re-inserting an identical contract
84    /// with a freshly calculated fee is the normal cache-hit fee path.
85    pub fn insert_block(&self, fetch_info: Arc<DataContractFetchInfo>) {
86        let data_contract_id_bytes = fetch_info.contract.id().to_buffer();
87
88        self.block_cache
89            .entry(data_contract_id_bytes)
90            .and_compute_with(|existing| match existing {
91                Some(entry) if entry.value().contract.version() > fetch_info.contract.version() => {
92                    Op::Nop
93                }
94                _ => Op::Put(Arc::clone(&fetch_info)),
95            });
96    }
97
98    /// Inserts a contract read from committed state into the global cache, unless a block
99    /// was committed since `observed` was taken.
100    ///
101    /// The generation is compared inside moka's per-key compute closure, which runs and
102    /// applies its write under the key-level lock, and every write that must be ordered
103    /// against this insert ([`Self::merge_and_clear_block_cache`],
104    /// [`Self::replace_committed`]) goes through that same lock. A promotion therefore either
105    /// runs before this insert reaches the lock, in which case the insert sees the new
106    /// generation and is dropped, or after it, in which case the promotion overwrites what
107    /// was inserted. There is no interleaving in which an insert approved under the old
108    /// generation lands after the promotion. The same-contract version guard of
109    /// [`Self::insert_block`] applies as well.
110    pub fn insert_committed(
111        &self,
112        fetch_info: Arc<DataContractFetchInfo>,
113        observed: CommittedGeneration,
114    ) {
115        let data_contract_id_bytes = fetch_info.contract.id().to_buffer();
116
117        self.global_cache
118            .entry(data_contract_id_bytes)
119            .and_compute_with(|existing| {
120                if self.committed_generation.load(Ordering::SeqCst) != observed.0 {
121                    return Op::Nop;
122                }
123                match existing {
124                    Some(entry)
125                        if entry.value().contract.version() > fetch_info.contract.version() =>
126                    {
127                        Op::Nop
128                    }
129                    _ => Op::Put(Arc::clone(&fetch_info)),
130                }
131            });
132    }
133
134    /// Seeds the copy a rewrite of the contract in state produced.
135    ///
136    /// When the rewrite went through the block transaction (`in_block_transaction`), the
137    /// contract is marked as modified in the block and the copy goes to the block cache, see
138    /// [`Self::mark_modified_in_block`]. Otherwise the rewrite is committed already and the
139    /// copy replaces the global entry through [`Self::replace_committed`].
140    pub fn insert_rewritten(
141        &self,
142        fetch_info: Arc<DataContractFetchInfo>,
143        in_block_transaction: bool,
144    ) {
145        let contract_id = fetch_info.contract.id().to_buffer();
146        if in_block_transaction {
147            self.mark_modified_in_block(contract_id);
148            self.insert_block(fetch_info);
149        } else {
150            self.replace_committed(contract_id, Some(fetch_info));
151        }
152    }
153
154    /// Records a rewrite of the contract in committed state, made outside any block
155    /// transaction, and replaces the global entry with `fetch_info`, or removes it when
156    /// `None`.
157    ///
158    /// Such a rewrite is a commit like any other: a committed-state reader that took its
159    /// snapshot before it may hold the pre-write copy. The generation is advanced first, so
160    /// that reader inserts nothing, and the replacement goes through the per-key compute
161    /// lock that [`Self::insert_committed`] evaluates its check under, so a reader whose
162    /// insert was already approved lands it before, not after, the replacement. Block
163    /// execution never takes this path; it exists for direct callers that write outside a
164    /// block.
165    pub fn replace_committed(
166        &self,
167        contract_id: [u8; 32],
168        fetch_info: Option<Arc<DataContractFetchInfo>>,
169    ) {
170        self.committed_generation.fetch_add(1, Ordering::SeqCst);
171        self.block_cache.remove(&contract_id);
172        self.global_cache
173            .entry(contract_id)
174            .and_compute_with(|_| match fetch_info {
175                Some(fetch_info) => Op::Put(fetch_info),
176                None => Op::Remove,
177            });
178    }
179
180    /// Tries to get a data contract from the block cache if the read is transactional, then
181    /// from the global cache.
182    ///
183    /// A transactional read of a contract the block transaction rewrote
184    /// ([`Self::mark_modified_in_block`]) does not fall back to the global cache: on a
185    /// block-cache miss it returns `None`, and the caller reads state through the
186    /// transaction. A read with no transaction only consults the global cache.
187    pub fn get(
188        &self,
189        contract_id: [u8; 32],
190        is_block_cache: bool,
191    ) -> Option<Arc<DataContractFetchInfo>> {
192        if is_block_cache {
193            if let Some(fetch_info) = self.block_cache.get(&contract_id) {
194                return Some(fetch_info);
195            }
196            if self.block_modified.read().contains(&contract_id) {
197                return None;
198            }
199        }
200
201        self.global_cache.get(&contract_id)
202    }
203
204    /// Remove contract from both block and global cache
205    pub fn remove(&self, contract_id: [u8; 32]) {
206        self.block_cache.remove(&contract_id);
207        self.global_cache.remove(&contract_id);
208    }
209
210    /// Records that the block transaction rewrote `contract_id`.
211    ///
212    /// From now until the block cache is cleared or promoted, a transactional read of this
213    /// contract is served from the block cache or from state through the transaction, never
214    /// from the global cache. Callers seed the post-write copy with [`Self::insert_block`]
215    /// so that the next read is a hit; the mark is what makes an eviction or a rollback safe.
216    pub fn mark_modified_in_block(&self, contract_id: [u8; 32]) {
217        self.block_modified.write().insert(contract_id);
218    }
219
220    /// Whether the block transaction rewrote `contract_id` since the block cache was cleared.
221    pub fn is_modified_in_block(&self, contract_id: [u8; 32]) -> bool {
222        self.block_modified.read().contains(&contract_id)
223    }
224
225    /// Drops from the block cache every contract the block transaction rewrote, keeping the
226    /// contracts marked as modified.
227    ///
228    /// For after a savepoint rollback: the rollback reverted the rewrites in state, but the
229    /// block cache still holds the post-write copies seeded when they were applied. Dropping
230    /// them makes the next transactional read go to state, which now holds whatever the
231    /// rollback restored. Entries the block only read stay: nothing rewrote what they hold.
232    pub fn drop_block_modified_entries(&self) {
233        for contract_id in self.block_modified.read().iter() {
234            self.block_cache.remove(contract_id);
235        }
236    }
237
238    /// Promotes the block cache into the global cache and clears it.
239    ///
240    /// Call this once the block transaction is **committed**, never before: from this call
241    /// on, committed-state readers are served the block's definitions, and a reader that
242    /// took its [`CommittedGeneration`] snapshot before this call can no longer insert. Both
243    /// are only correct once state itself holds what the block wrote.
244    ///
245    /// Promotion is unconditional: everything the block read or rewrote through its
246    /// transaction is committed state now, and nothing else could have written state in the
247    /// meantime. A rewritten contract that is no longer in the block cache (evicted, or
248    /// dropped by a rollback and not read again) is removed from the global cache instead,
249    /// because a committed-state reader may have inserted the pre-block definition there
250    /// while the block was executing.
251    ///
252    /// Every global-cache write here goes through moka's per-key compute lock, the lock
253    /// [`Self::insert_committed`] evaluates its generation check under. A reader whose
254    /// closure already approved its insert holds that lock until the insert is applied, so
255    /// the promotion waits for it and then overwrites (or removes) it; a reader that reaches
256    /// the lock afterwards sees the new generation and inserts nothing. moka's plain
257    /// `insert` and `remove` do not take that lock and would let an approved insert of the
258    /// pre-block definition land after the promotion.
259    pub fn merge_and_clear_block_cache(&self) {
260        // Bumping first closes the door on committed-state readers that read before the
261        // commit: their inserts are dropped from here on, and any that already landed, or
262        // are about to under the key lock, are overwritten or removed below.
263        self.committed_generation.fetch_add(1, Ordering::SeqCst);
264
265        let modified = std::mem::take(&mut *self.block_modified.write());
266        for contract_id in modified {
267            if !self.block_cache.contains_key(&contract_id) {
268                self.global_cache
269                    .entry(contract_id)
270                    .and_compute_with(|_| Op::Remove);
271            }
272        }
273
274        for (contract_id, fetch_info) in self.block_cache.iter() {
275            self.global_cache
276                .entry(*contract_id)
277                .and_compute_with(|_| Op::Put(fetch_info));
278        }
279        self.block_cache.invalidate_all();
280    }
281
282    /// Clears the block cache and the record of what the block transaction rewrote.
283    ///
284    /// For the start of a block: a fresh transaction sees exactly committed state, which is
285    /// what the global cache mirrors, so nothing needs protecting yet.
286    pub fn clear_block_cache(&self) {
287        self.block_cache.invalidate_all();
288        self.block_modified.write().clear();
289    }
290
291    /// Drops every cached entry from both caches.
292    ///
293    /// The record of what the block transaction rewrote is kept: it is not a cache, and a
294    /// clear in the middle of a block (a migration that rewrote contracts in state and wants
295    /// every reader to reload them) must not let transactional reads fall back to the
296    /// global cache again.
297    pub fn clear(&self) {
298        self.block_cache.invalidate_all();
299        self.global_cache.invalidate_all();
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306    use dpp::data_contract::accessors::v0::{DataContractV0Getters, DataContractV0Setters};
307    use dpp::fee::fee_result::FeeResult;
308    use dpp::version::PlatformVersion;
309    use std::sync::Barrier;
310    use std::thread;
311    use std::time::Duration;
312
313    /// Two copies of the SAME contract (same id) at the given versions. The fixture generates
314    /// a fresh contract id per call, so both copies must derive from a single fixture.
315    fn same_contract_at_versions(
316        first: u32,
317        second: u32,
318    ) -> (Arc<DataContractFetchInfo>, Arc<DataContractFetchInfo>) {
319        let fetch_info = DataContractFetchInfo::dpns_contract_fixture(
320            PlatformVersion::latest().protocol_version,
321        );
322        let mut first_info = fetch_info.clone();
323        first_info.contract.set_version(first);
324        let mut second_info = fetch_info;
325        second_info.contract.set_version(second);
326        (Arc::new(first_info), Arc::new(second_info))
327    }
328
329    mod get {
330        use super::*;
331
332        #[test]
333        fn test_get_from_global_cache_when_block_cache_is_not_requested() {
334            let data_contract_cache = DataContractCache::new(10, 10);
335
336            let protocol_version = PlatformVersion::latest().protocol_version;
337
338            // Create global contract
339            let fetch_info_global = Arc::new(DataContractFetchInfo::dpns_contract_fixture(
340                protocol_version,
341            ));
342
343            let contract_id = fetch_info_global.contract.id().to_buffer();
344
345            data_contract_cache
346                .global_cache
347                .insert(contract_id, Arc::clone(&fetch_info_global));
348
349            // Create transactional contract with a new version
350            let mut fetch_info_block =
351                DataContractFetchInfo::dpns_contract_fixture(protocol_version);
352
353            fetch_info_block.contract.increment_version();
354
355            let fetch_info_block_boxed = Arc::new(fetch_info_block);
356
357            data_contract_cache
358                .block_cache
359                .insert(contract_id, Arc::clone(&fetch_info_block_boxed));
360
361            let fetch_info_from_cache = data_contract_cache
362                .get(contract_id, false)
363                .expect("should be present");
364
365            assert_eq!(fetch_info_from_cache, fetch_info_global)
366        }
367
368        #[test]
369        fn test_get_from_global_cache_when_block_cache_does_not_have_contract() {
370            let data_contract_cache = DataContractCache::new(10, 10);
371
372            let protocol_version = PlatformVersion::latest().protocol_version;
373
374            let fetch_info_global = Arc::new(DataContractFetchInfo::dpns_contract_fixture(
375                protocol_version,
376            ));
377
378            let contract_id = fetch_info_global.contract.id().to_buffer();
379
380            data_contract_cache
381                .global_cache
382                .insert(contract_id, Arc::clone(&fetch_info_global));
383
384            let fetch_info_from_cache = data_contract_cache
385                .get(contract_id, true)
386                .expect("should be present");
387
388            assert_eq!(fetch_info_from_cache, fetch_info_global)
389        }
390
391        #[test]
392        fn test_get_from_block_cache() {
393            let data_contract_cache = DataContractCache::new(10, 10);
394
395            let protocol_version = PlatformVersion::latest().protocol_version;
396
397            let fetch_info_block = Arc::new(DataContractFetchInfo::dpns_contract_fixture(
398                protocol_version,
399            ));
400
401            let contract_id = fetch_info_block.contract.id().to_buffer();
402
403            data_contract_cache
404                .block_cache
405                .insert(contract_id, Arc::clone(&fetch_info_block));
406
407            let fetch_info_from_cache = data_contract_cache
408                .get(contract_id, true)
409                .expect("should be present");
410
411            assert_eq!(fetch_info_from_cache, fetch_info_block)
412        }
413
414        /// The report's interleaving at the cache level: the block rewrote the contract and
415        /// the block cache no longer holds it, while a committed-state reader has since put
416        /// the committed (pre-rewrite) copy into the global cache. A transactional read must
417        /// miss, so that the caller reads state through the transaction, rather than be
418        /// handed the committed copy.
419        #[test]
420        fn test_get_in_block_does_not_fall_back_to_global_for_a_modified_contract() {
421            let data_contract_cache = DataContractCache::new(10, 10);
422
423            let (committed, _) = same_contract_at_versions(1, 2);
424            let contract_id = committed.contract.id().to_buffer();
425
426            data_contract_cache.mark_modified_in_block(contract_id);
427            data_contract_cache
428                .insert_committed(committed, data_contract_cache.committed_generation());
429
430            assert!(
431                data_contract_cache.get(contract_id, true).is_none(),
432                "a transactional read of a rewritten contract must not be served the committed copy"
433            );
434        }
435
436        #[test]
437        fn test_get_in_block_reads_block_cache_for_a_modified_contract() {
438            let data_contract_cache = DataContractCache::new(10, 10);
439
440            let (committed, rewritten) = same_contract_at_versions(1, 2);
441            let contract_id = committed.contract.id().to_buffer();
442
443            data_contract_cache.mark_modified_in_block(contract_id);
444            data_contract_cache.insert_block(rewritten);
445            data_contract_cache
446                .insert_committed(committed, data_contract_cache.committed_generation());
447
448            let cached = data_contract_cache
449                .get(contract_id, true)
450                .expect("should be present");
451            assert_eq!(cached.contract.version(), 2);
452        }
453
454        /// Committed-state readers are unaffected by the block's rewrite until it commits.
455        #[test]
456        fn test_get_committed_still_reads_global_for_a_modified_contract() {
457            let data_contract_cache = DataContractCache::new(10, 10);
458
459            let (committed, rewritten) = same_contract_at_versions(1, 2);
460            let contract_id = committed.contract.id().to_buffer();
461
462            data_contract_cache.mark_modified_in_block(contract_id);
463            data_contract_cache.insert_block(rewritten);
464            data_contract_cache
465                .insert_committed(committed, data_contract_cache.committed_generation());
466
467            let cached = data_contract_cache
468                .get(contract_id, false)
469                .expect("should be present");
470            assert_eq!(cached.contract.version(), 1);
471        }
472    }
473
474    mod insert_block {
475        use super::*;
476
477        /// A lower version must not clobber a higher one already in the block cache.
478        #[test]
479        fn test_insert_does_not_overwrite_newer_version_with_older() {
480            let data_contract_cache = DataContractCache::new(10, 10);
481
482            let (stale, newer) = same_contract_at_versions(1, 2);
483            let contract_id = newer.contract.id().to_buffer();
484            data_contract_cache.insert_block(newer);
485
486            data_contract_cache.insert_block(stale);
487
488            let cached = data_contract_cache
489                .get(contract_id, true)
490                .expect("should be present");
491            assert_eq!(cached.contract.version(), 2);
492        }
493
494        /// Same-version inserts must overwrite: re-inserting the same contract with a
495        /// freshly calculated fee is the normal cache-hit fee path.
496        #[test]
497        fn test_insert_overwrites_same_version() {
498            let data_contract_cache = DataContractCache::new(10, 10);
499
500            let (mut original, mut with_fee) = same_contract_at_versions(1, 1);
501            let contract_id = original.contract.id().to_buffer();
502            Arc::make_mut(&mut original).fee = None;
503            data_contract_cache.insert_block(original);
504
505            Arc::make_mut(&mut with_fee).fee = Some(FeeResult::new_from_processing_fee(1));
506            data_contract_cache.insert_block(with_fee);
507
508            let cached = data_contract_cache
509                .get(contract_id, true)
510                .expect("should be present");
511            assert!(cached.fee.is_some(), "same-version insert must overwrite");
512        }
513    }
514
515    mod insert_committed {
516        use super::*;
517
518        #[test]
519        fn test_insert_with_a_current_snapshot_is_stored() {
520            let data_contract_cache = DataContractCache::new(10, 10);
521
522            let (fetch_info, _) = same_contract_at_versions(1, 1);
523            let contract_id = fetch_info.contract.id().to_buffer();
524
525            let observed = data_contract_cache.committed_generation();
526            data_contract_cache.insert_committed(fetch_info, observed);
527
528            assert!(data_contract_cache.get(contract_id, false).is_some());
529        }
530
531        /// A delayed insert carrying an older contract version must not clobber a newer
532        /// entry, even when the reader's snapshot is current (nothing committed in between
533        /// and the newer copy was inserted by another committed-state reader).
534        #[test]
535        fn test_insert_does_not_overwrite_newer_version_with_older() {
536            let data_contract_cache = DataContractCache::new(10, 10);
537
538            let (stale, newer) = same_contract_at_versions(1, 2);
539            let contract_id = newer.contract.id().to_buffer();
540            let observed = data_contract_cache.committed_generation();
541            data_contract_cache.insert_committed(newer, observed);
542
543            data_contract_cache.insert_committed(stale, observed);
544
545            let cached = data_contract_cache
546                .get(contract_id, false)
547                .expect("should be present");
548            assert_eq!(cached.contract.version(), 2);
549        }
550
551        /// The full race, end to end: a query thread snapshots the generation and reads the
552        /// pre-block contract from committed state, block execution rewrites the contract
553        /// and seeds the block cache, the block commits and promotes, and only then does the
554        /// query thread perform its insert. The promoted contract must survive.
555        #[test]
556        fn test_insert_with_a_snapshot_taken_before_a_promotion_is_dropped() {
557            let data_contract_cache = DataContractCache::new(10, 10);
558
559            let (stale, rewritten) = same_contract_at_versions(1, 2);
560            let contract_id = stale.contract.id().to_buffer();
561
562            // The query thread snapshots and reads committed state, then is descheduled.
563            let observed_before_the_block = data_contract_cache.committed_generation();
564
565            // Block execution rewrites the contract; the block commits and promotes.
566            data_contract_cache.mark_modified_in_block(contract_id);
567            data_contract_cache.insert_block(rewritten);
568            data_contract_cache.merge_and_clear_block_cache();
569
570            // The query thread wakes up and performs its stale insert.
571            data_contract_cache.insert_committed(stale, observed_before_the_block);
572
573            let cached = data_contract_cache
574                .get(contract_id, false)
575                .expect("should be present");
576            assert_eq!(
577                cached.contract.version(),
578                2,
579                "the promoted contract must survive a delayed stale insert"
580            );
581        }
582
583        /// The same delayed insert when the rewrite did not bump the contract version (an
584        /// in-place migration such as the schema property strip) and the promoted entry is
585        /// no longer resident. The version guard has nothing to compare against here; only
586        /// the generation check stops the stale copy from sticking.
587        #[test]
588        fn test_same_version_insert_with_a_snapshot_taken_before_a_promotion_is_dropped() {
589            let data_contract_cache = DataContractCache::new(10, 10);
590
591            let (mut stale, mut rewritten) = same_contract_at_versions(1, 1);
592            let contract_id = stale.contract.id().to_buffer();
593            Arc::make_mut(&mut stale).fee = None;
594            Arc::make_mut(&mut rewritten).fee = Some(FeeResult::new_from_processing_fee(1));
595
596            let observed_before_the_block = data_contract_cache.committed_generation();
597
598            // The block rewrote the contract but nothing read it afterwards, so the block
599            // cache has no copy to promote and the global entry is removed instead.
600            data_contract_cache.mark_modified_in_block(contract_id);
601            data_contract_cache.merge_and_clear_block_cache();
602
603            data_contract_cache.insert_committed(stale, observed_before_the_block);
604
605            assert!(
606                data_contract_cache.get(contract_id, false).is_none(),
607                "a committed-state copy read before the commit must not enter the global cache after it"
608            );
609        }
610    }
611
612    mod replace_committed {
613        use super::*;
614
615        /// A rewrite outside any block transaction is a commit: a reader that read the
616        /// pre-write contract before it must not be able to put it back afterwards, whether
617        /// the rewrite published its replacement or only evicted the superseded copy.
618        #[test]
619        fn test_a_reader_that_read_before_the_rewrite_cannot_insert_after_it() {
620            let data_contract_cache = DataContractCache::new(10, 10);
621            let (pre_write, rewritten) = same_contract_at_versions(1, 2);
622            let contract_id = pre_write.contract.id().to_buffer();
623
624            let observed_before_the_rewrite = data_contract_cache.committed_generation();
625            data_contract_cache.replace_committed(contract_id, Some(rewritten));
626            data_contract_cache
627                .insert_committed(Arc::clone(&pre_write), observed_before_the_rewrite);
628            assert_eq!(
629                data_contract_cache
630                    .get(contract_id, false)
631                    .expect("should be present")
632                    .contract
633                    .version(),
634                2
635            );
636
637            let observed_before_the_eviction = data_contract_cache.committed_generation();
638            data_contract_cache.replace_committed(contract_id, None);
639            data_contract_cache.insert_committed(pre_write, observed_before_the_eviction);
640            assert!(
641                data_contract_cache.get(contract_id, false).is_none(),
642                "an evicted contract must not be re-admitted by a reader that read before the rewrite"
643            );
644        }
645
646        #[test]
647        fn test_replaces_the_block_cache_copy_as_well() {
648            let data_contract_cache = DataContractCache::new(10, 10);
649            let (stale, rewritten) = same_contract_at_versions(1, 2);
650            let contract_id = stale.contract.id().to_buffer();
651
652            data_contract_cache.insert_block(stale);
653            data_contract_cache.replace_committed(contract_id, Some(rewritten));
654
655            assert_eq!(
656                data_contract_cache
657                    .get(contract_id, true)
658                    .expect("should be present")
659                    .contract
660                    .version(),
661                2
662            );
663        }
664    }
665
666    mod remove {
667        use super::*;
668
669        #[test]
670        fn test_remove_clears_global_cache_entry() {
671            let data_contract_cache = DataContractCache::new(10, 10);
672
673            let protocol_version = PlatformVersion::latest().protocol_version;
674            let fetch_info = Arc::new(DataContractFetchInfo::dpns_contract_fixture(
675                protocol_version,
676            ));
677            let contract_id = fetch_info.contract.id().to_buffer();
678
679            data_contract_cache
680                .insert_committed(fetch_info, data_contract_cache.committed_generation());
681            data_contract_cache.remove(contract_id);
682
683            assert!(data_contract_cache.get(contract_id, false).is_none());
684        }
685
686        #[test]
687        fn test_remove_clears_entry_from_both_caches() {
688            let data_contract_cache = DataContractCache::new(10, 10);
689
690            let protocol_version = PlatformVersion::latest().protocol_version;
691            let fetch_info_global = Arc::new(DataContractFetchInfo::dpns_contract_fixture(
692                protocol_version,
693            ));
694            let contract_id = fetch_info_global.contract.id().to_buffer();
695            let fetch_info_block = Arc::clone(&fetch_info_global);
696
697            data_contract_cache.insert_committed(
698                fetch_info_global,
699                data_contract_cache.committed_generation(),
700            );
701            data_contract_cache.insert_block(fetch_info_block);
702            data_contract_cache.remove(contract_id);
703
704            assert!(data_contract_cache.block_cache.get(&contract_id).is_none());
705            assert!(data_contract_cache.global_cache.get(&contract_id).is_none());
706        }
707    }
708
709    mod drop_block_modified_entries {
710        use super::*;
711
712        /// After a rollback the post-write copies must go, but the contracts stay marked so
713        /// that the next transactional read still bypasses the global cache.
714        #[test]
715        fn test_drops_modified_entries_from_block_cache_and_keeps_them_marked() {
716            let data_contract_cache = DataContractCache::new(10, 10);
717
718            let (committed, rewritten) = same_contract_at_versions(1, 2);
719            let contract_id = committed.contract.id().to_buffer();
720
721            data_contract_cache
722                .insert_committed(committed, data_contract_cache.committed_generation());
723            data_contract_cache.mark_modified_in_block(contract_id);
724            data_contract_cache.insert_block(rewritten);
725
726            data_contract_cache.drop_block_modified_entries();
727
728            assert!(data_contract_cache.block_cache.get(&contract_id).is_none());
729            assert!(data_contract_cache.is_modified_in_block(contract_id));
730            assert!(
731                data_contract_cache.get(contract_id, true).is_none(),
732                "the transactional read must go to state, not to the committed copy"
733            );
734        }
735
736        #[test]
737        fn test_leaves_unmodified_block_entries_in_place() {
738            let data_contract_cache = DataContractCache::new(10, 10);
739
740            let (only_read, _) = same_contract_at_versions(1, 1);
741            let contract_id = only_read.contract.id().to_buffer();
742            data_contract_cache.insert_block(only_read);
743
744            data_contract_cache.drop_block_modified_entries();
745
746            assert!(data_contract_cache.get(contract_id, true).is_some());
747        }
748    }
749
750    mod merge_and_clear_block_cache {
751        use super::*;
752
753        #[test]
754        fn test_merge_moves_block_items_to_global_cache() {
755            let data_contract_cache = DataContractCache::new(10, 10);
756
757            let protocol_version = PlatformVersion::latest().protocol_version;
758            let fetch_info = Arc::new(DataContractFetchInfo::dpns_contract_fixture(
759                protocol_version,
760            ));
761            let contract_id = fetch_info.contract.id().to_buffer();
762
763            data_contract_cache.insert_block(fetch_info);
764            data_contract_cache.merge_and_clear_block_cache();
765
766            assert!(data_contract_cache.global_cache.get(&contract_id).is_some());
767        }
768
769        #[test]
770        fn test_merge_clears_block_cache() {
771            let data_contract_cache = DataContractCache::new(10, 10);
772
773            let protocol_version = PlatformVersion::latest().protocol_version;
774            let fetch_info = Arc::new(DataContractFetchInfo::dpns_contract_fixture(
775                protocol_version,
776            ));
777            let contract_id = fetch_info.contract.id().to_buffer();
778
779            data_contract_cache.insert_block(fetch_info);
780            data_contract_cache.merge_and_clear_block_cache();
781
782            assert!(data_contract_cache.block_cache.get(&contract_id).is_none());
783        }
784
785        /// A committed-state reader put the pre-block copy into the global cache while the
786        /// block was executing. Promotion must replace it with what the block wrote.
787        #[test]
788        fn test_merge_promotes_a_rewritten_contract_over_the_committed_copy() {
789            let data_contract_cache = DataContractCache::new(10, 10);
790
791            let (committed, rewritten) = same_contract_at_versions(1, 2);
792            let contract_id = committed.contract.id().to_buffer();
793
794            data_contract_cache.mark_modified_in_block(contract_id);
795            data_contract_cache.insert_block(rewritten);
796            data_contract_cache
797                .insert_committed(committed, data_contract_cache.committed_generation());
798
799            data_contract_cache.merge_and_clear_block_cache();
800
801            let cached = data_contract_cache
802                .get(contract_id, false)
803                .expect("should be present");
804            assert_eq!(cached.contract.version(), 2);
805        }
806
807        /// Same, but the block cache no longer holds the rewritten copy (evicted, or dropped
808        /// by a rollback and not read again). There is nothing to promote, so the committed
809        /// copy a reader inserted during the block must be removed: it is stale now.
810        #[test]
811        fn test_merge_removes_the_committed_copy_of_a_rewritten_contract_missing_from_the_block_cache(
812        ) {
813            let data_contract_cache = DataContractCache::new(10, 10);
814
815            let (committed, _) = same_contract_at_versions(1, 2);
816            let contract_id = committed.contract.id().to_buffer();
817
818            data_contract_cache.mark_modified_in_block(contract_id);
819            data_contract_cache
820                .insert_committed(committed, data_contract_cache.committed_generation());
821
822            data_contract_cache.merge_and_clear_block_cache();
823
824            assert!(data_contract_cache.get(contract_id, false).is_none());
825        }
826
827        #[test]
828        fn test_merge_clears_modified_marks() {
829            let data_contract_cache = DataContractCache::new(10, 10);
830
831            let (_, rewritten) = same_contract_at_versions(1, 2);
832            let contract_id = rewritten.contract.id().to_buffer();
833
834            data_contract_cache.mark_modified_in_block(contract_id);
835            data_contract_cache.insert_block(rewritten);
836
837            data_contract_cache.merge_and_clear_block_cache();
838
839            assert!(!data_contract_cache.is_modified_in_block(contract_id));
840        }
841
842        /// A committed-state reader whose insert has passed its generation check and is about
843        /// to be applied when the block is promoted. The reader holds moka's per-key lock from
844        /// the moment its closure runs until its write lands; a promotion through plain
845        /// `insert` would slip in between and be overwritten by the reader's pre-block copy.
846        /// The promotion must wait for the reader and overwrite it instead. The reader is
847        /// modelled with the entry API directly, paused inside its closure, which is exactly
848        /// the state `insert_committed` is in once its check has passed.
849        #[test]
850        fn test_merge_overwrites_a_committed_insert_approved_before_the_promotion() {
851            let data_contract_cache = Arc::new(DataContractCache::new(10, 10));
852            let (committed, rewritten) = same_contract_at_versions(1, 2);
853            let contract_id = committed.contract.id().to_buffer();
854
855            data_contract_cache.mark_modified_in_block(contract_id);
856            data_contract_cache.insert_block(rewritten);
857
858            let final_version =
859                promote_while_a_committed_insert_is_paused(&data_contract_cache, committed);
860
861            assert_eq!(
862                final_version,
863                Some(2),
864                "the promoted contract must survive a committed insert approved before the promotion"
865            );
866        }
867
868        /// Same interleaving when the rewritten contract is no longer in the block cache: the
869        /// promotion removes the global entry, and the reader's pre-block copy must not
870        /// reappear behind it.
871        #[test]
872        fn test_merge_removal_wins_over_a_committed_insert_approved_before_the_promotion() {
873            let data_contract_cache = Arc::new(DataContractCache::new(10, 10));
874            let (committed, _) = same_contract_at_versions(1, 2);
875            let contract_id = committed.contract.id().to_buffer();
876
877            data_contract_cache.mark_modified_in_block(contract_id);
878
879            let final_version =
880                promote_while_a_committed_insert_is_paused(&data_contract_cache, committed);
881
882            assert_eq!(
883                final_version, None,
884                "a rewritten contract absent from the block cache must not be resurrected by a committed insert approved before the promotion"
885            );
886        }
887
888        /// Runs a committed insert of `committed` on another thread, pauses it inside its
889        /// compute closure (after the point where `insert_committed` has approved the put),
890        /// promotes the block cache while it is paused, then lets it finish. Returns the
891        /// version left in the global cache.
892        fn promote_while_a_committed_insert_is_paused(
893            data_contract_cache: &Arc<DataContractCache>,
894            committed: Arc<DataContractFetchInfo>,
895        ) -> Option<u32> {
896            let contract_id = committed.contract.id().to_buffer();
897            let reader_is_inside = Arc::new(Barrier::new(2));
898            let release_reader = Arc::new(Barrier::new(2));
899
900            let reader = {
901                let data_contract_cache = Arc::clone(data_contract_cache);
902                let reader_is_inside = Arc::clone(&reader_is_inside);
903                let release_reader = Arc::clone(&release_reader);
904                thread::spawn(move || {
905                    data_contract_cache
906                        .global_cache
907                        .entry(contract_id)
908                        .and_compute_with(|_| {
909                            reader_is_inside.wait();
910                            release_reader.wait();
911                            Op::Put(committed)
912                        });
913                })
914            };
915            reader_is_inside.wait();
916
917            let promotion = {
918                let data_contract_cache = Arc::clone(data_contract_cache);
919                thread::spawn(move || data_contract_cache.merge_and_clear_block_cache())
920            };
921            // Let the promotion reach the key. With the per-key lock it blocks there until
922            // the reader is released; without it, it would write now and lose.
923            thread::sleep(Duration::from_millis(100));
924            release_reader.wait();
925
926            reader.join().expect("the reader must finish");
927            promotion.join().expect("the promotion must finish");
928
929            data_contract_cache
930                .get(contract_id, false)
931                .map(|fetch_info| fetch_info.contract.version())
932        }
933
934        #[test]
935        fn test_merge_advances_the_committed_generation() {
936            let data_contract_cache = DataContractCache::new(10, 10);
937
938            let before = data_contract_cache.committed_generation();
939            data_contract_cache.merge_and_clear_block_cache();
940
941            assert_ne!(before, data_contract_cache.committed_generation());
942        }
943    }
944
945    mod clear {
946        use super::*;
947
948        #[test]
949        fn test_clear_empties_global_and_block_caches() {
950            let data_contract_cache = DataContractCache::new(10, 10);
951
952            let protocol_version = PlatformVersion::latest().protocol_version;
953            let fetch_info_global = Arc::new(DataContractFetchInfo::dpns_contract_fixture(
954                protocol_version,
955            ));
956            let contract_id = fetch_info_global.contract.id().to_buffer();
957            let fetch_info_block = Arc::clone(&fetch_info_global);
958
959            data_contract_cache.insert_committed(
960                fetch_info_global,
961                data_contract_cache.committed_generation(),
962            );
963            data_contract_cache.insert_block(fetch_info_block);
964            data_contract_cache.clear();
965
966            assert!(data_contract_cache.get(contract_id, false).is_none());
967            assert!(data_contract_cache.block_cache.get(&contract_id).is_none());
968        }
969
970        /// A mid-block clear (the schema property strip migration) must not let
971        /// transactional reads of the rewritten contracts fall back to the global cache.
972        #[test]
973        fn test_clear_keeps_modified_marks() {
974            let data_contract_cache = DataContractCache::new(10, 10);
975
976            let (_, rewritten) = same_contract_at_versions(1, 2);
977            let contract_id = rewritten.contract.id().to_buffer();
978
979            data_contract_cache.mark_modified_in_block(contract_id);
980            data_contract_cache.insert_block(rewritten);
981            data_contract_cache.clear();
982
983            assert!(data_contract_cache.is_modified_in_block(contract_id));
984        }
985
986        #[test]
987        fn test_clear_block_cache_clears_modified_marks() {
988            let data_contract_cache = DataContractCache::new(10, 10);
989
990            let (_, rewritten) = same_contract_at_versions(1, 2);
991            let contract_id = rewritten.contract.id().to_buffer();
992
993            data_contract_cache.mark_modified_in_block(contract_id);
994            data_contract_cache.insert_block(rewritten);
995            data_contract_cache.clear_block_cache();
996
997            assert!(!data_contract_cache.is_modified_in_block(contract_id));
998            assert!(data_contract_cache.block_cache.get(&contract_id).is_none());
999        }
1000    }
1001}