Skip to main content

sui_core/execution_cache/
writeback_cache.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! MemoryCache is a cache for the transaction execution which delays writes to the database until
5//! transaction results are certified (i.e. they appear in a certified checkpoint, or an effects cert
6//! is observed by a fullnode). The cache also stores committed data in memory in order to serve
7//! future reads without hitting the database.
8//!
9//! For storing uncommitted transaction outputs, we cannot evict the data at all until it is written
10//! to disk. Committed data not only can be evicted, but it is also unbounded (imagine a stream of
11//! transactions that keep splitting a coin into smaller coins).
12//!
13//! We also want to be able to support negative cache hits (i.e. the case where we can determine an
14//! object does not exist without hitting the database).
15//!
16//! To achieve both of these goals, we split the cache data into two pieces, a dirty set and a cached
17//! set. The dirty set has no automatic evictions, data is only removed after being committed. The
18//! cached set is in a bounded-sized cache with automatic evictions. In order to support negative
19//! cache hits, we treat the two halves of the cache as FIFO queue. Newly written (dirty) versions are
20//! inserted to one end of the dirty queue. As versions are committed to disk, they are
21//! removed from the other end of the dirty queue and inserted into the cache queue. The cache queue
22//! is truncated if it exceeds its maximum size, by removing all but the N newest versions.
23//!
24//! This gives us the property that the sequence of versions in the dirty and cached queues are the
25//! most recent versions of the object, i.e. there can be no "gaps". This allows for the following:
26//!
27//!   - Negative cache hits: If the queried version is not in memory, but is higher than the smallest
28//!     version in the cached queue, it does not exist in the db either.
29//!   - Bounded reads: When reading the most recent version that is <= some version bound, we can
30//!     correctly satisfy this query from the cache, or determine that we must go to the db.
31//!
32//! Note that at any time, either or both the dirty or the cached queue may be non-existent. There may be no
33//! dirty versions of the objects, in which case there will be no dirty queue. And, the cached queue
34//! may be evicted from the cache, in which case there will be no cached queue. Because only the cached
35//! queue can be evicted (the dirty queue can only become empty by moving versions from it to the cached
36//! queue), the "highest versions" property still holds in all cases.
37//!
38//! The above design is used for both objects and markers.
39
40use crate::accumulators::funds_read::AccountFundsRead;
41use crate::authority::AuthorityStore;
42use crate::authority::authority_per_epoch_store::AuthorityPerEpochStore;
43use crate::authority::authority_store::ExecutionLockWriteGuard;
44#[cfg(test)]
45use crate::authority::authority_store::{LockDetailsDeprecated, ObjectLockStatus, SuiLockResult};
46use crate::authority::authority_store_tables::LiveObject;
47use crate::authority::backpressure::BackpressureManager;
48use crate::authority::epoch_start_configuration::{EpochFlag, EpochStartConfiguration};
49use crate::fallback_fetch::do_fallback_lookup;
50use crate::global_state_hasher::GlobalStateHashStore;
51use crate::transaction_outputs::TransactionOutputs;
52
53use dashmap::DashMap;
54use dashmap::mapref::entry::Entry as DashMapEntry;
55use futures::{FutureExt, future::BoxFuture};
56use moka::sync::SegmentedCache as MokaCache;
57use mysten_common::ZipDebugEqIteratorExt;
58use mysten_common::random_util::randomize_cache_capacity_in_tests;
59use mysten_common::sync::notify_read::NotifyRead;
60use mysten_common::{debug_fatal, debug_fatal_no_invariant};
61use parking_lot::Mutex;
62use rayon::prelude::*;
63use std::collections::{BTreeMap, HashSet};
64use std::hash::Hash;
65use std::sync::Arc;
66use std::sync::atomic::AtomicU64;
67use std::time::Instant;
68use sui_config::ExecutionCacheConfig;
69use sui_macros::fail_point;
70use sui_protocol_config::ProtocolVersion;
71use sui_types::SUI_ACCUMULATOR_ROOT_OBJECT_ID;
72use sui_types::accumulator_event::AccumulatorEvent;
73use sui_types::accumulator_root::{AccumulatorObjId, AccumulatorValue};
74use sui_types::base_types::{
75    ConsensusObjectVersion, EpochId, FullObjectID, ObjectID, ObjectRef, SequenceNumber,
76    VerifiedExecutionData,
77};
78use sui_types::bridge::{Bridge, get_bridge};
79use sui_types::digests::{ObjectDigest, TransactionDigest, TransactionEffectsDigest};
80use sui_types::effects::{TransactionEffects, TransactionEvents};
81#[cfg(test)]
82use sui_types::error::SuiError;
83use sui_types::error::{SuiErrorKind, SuiResult, UserInputError};
84use sui_types::executable_transaction::VerifiedExecutableTransaction;
85use sui_types::global_state_hash::GlobalStateHash;
86use sui_types::message_envelope::Message;
87use sui_types::messages_checkpoint::CheckpointSequenceNumber;
88use sui_types::object::Object;
89use sui_types::storage::{
90    FullObjectKey, InputKey, MarkerValue, ObjectKey, ObjectOrTombstone, ObjectStore, PackageObject,
91};
92use sui_types::sui_system_state::{SuiSystemState, get_sui_system_state};
93use sui_types::transaction::{TransactionDataAPI, VerifiedTransaction};
94use tap::TapOptional;
95use tracing::{debug, info, instrument, trace, warn};
96
97use super::ExecutionCacheAPI;
98use super::cache_types::Ticket;
99use super::{
100    Batch, CheckpointCache, ExecutionCacheCommit, ExecutionCacheMetrics, ExecutionCacheReconfigAPI,
101    ExecutionCacheWrite, ObjectCacheRead, StateSyncAPI, TestingAPI, TransactionCacheRead,
102    cache_types::{CacheResult, CachedVersionMap, IsNewer, MonotonicCache},
103    implement_passthrough_traits,
104    object_locks::ObjectLocks,
105};
106
107#[cfg(test)]
108#[path = "unit_tests/writeback_cache_tests.rs"]
109pub mod writeback_cache_tests;
110
111#[cfg(test)]
112#[path = "unit_tests/notify_read_input_objects_tests.rs"]
113mod notify_read_input_objects_tests;
114
115#[derive(Clone, PartialEq, Eq)]
116enum ObjectEntry {
117    Object(Object),
118    Deleted,
119    Wrapped,
120}
121
122impl ObjectEntry {
123    #[cfg(test)]
124    fn unwrap_object(&self) -> &Object {
125        match self {
126            ObjectEntry::Object(o) => o,
127            _ => panic!("unwrap_object called on non-Object"),
128        }
129    }
130
131    fn is_tombstone(&self) -> bool {
132        match self {
133            ObjectEntry::Deleted | ObjectEntry::Wrapped => true,
134            ObjectEntry::Object(_) => false,
135        }
136    }
137}
138
139impl std::fmt::Debug for ObjectEntry {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        match self {
142            ObjectEntry::Object(o) => {
143                write!(f, "ObjectEntry::Object({:?})", o.compute_object_reference())
144            }
145            ObjectEntry::Deleted => write!(f, "ObjectEntry::Deleted"),
146            ObjectEntry::Wrapped => write!(f, "ObjectEntry::Wrapped"),
147        }
148    }
149}
150
151impl From<Object> for ObjectEntry {
152    fn from(object: Object) -> Self {
153        ObjectEntry::Object(object)
154    }
155}
156
157impl From<ObjectOrTombstone> for ObjectEntry {
158    fn from(object: ObjectOrTombstone) -> Self {
159        match object {
160            ObjectOrTombstone::Object(o) => o.into(),
161            ObjectOrTombstone::Tombstone(obj_ref) => {
162                if obj_ref.2.is_deleted() {
163                    ObjectEntry::Deleted
164                } else if obj_ref.2.is_wrapped() {
165                    ObjectEntry::Wrapped
166                } else {
167                    panic!("tombstone digest must either be deleted or wrapped");
168                }
169            }
170        }
171    }
172}
173
174#[derive(Debug, Clone, PartialEq, Eq)]
175enum LatestObjectCacheEntry {
176    Object(SequenceNumber, ObjectEntry),
177    NonExistent,
178}
179
180impl LatestObjectCacheEntry {
181    #[cfg(test)]
182    fn version(&self) -> Option<SequenceNumber> {
183        match self {
184            LatestObjectCacheEntry::Object(version, _) => Some(*version),
185            LatestObjectCacheEntry::NonExistent => None,
186        }
187    }
188
189    fn is_alive(&self) -> bool {
190        match self {
191            LatestObjectCacheEntry::Object(_, entry) => !entry.is_tombstone(),
192            LatestObjectCacheEntry::NonExistent => false,
193        }
194    }
195}
196
197impl IsNewer for LatestObjectCacheEntry {
198    fn is_newer_than(&self, other: &LatestObjectCacheEntry) -> bool {
199        match (self, other) {
200            (LatestObjectCacheEntry::Object(v1, _), LatestObjectCacheEntry::Object(v2, _)) => {
201                v1 > v2
202            }
203            (LatestObjectCacheEntry::Object(_, _), LatestObjectCacheEntry::NonExistent) => true,
204            _ => false,
205        }
206    }
207}
208
209type MarkerKey = (EpochId, FullObjectID);
210
211/// UncommittedData stores execution outputs that are not yet written to the db. Entries in this
212/// struct can only be purged after they are committed.
213struct UncommittedData {
214    /// The object dirty set. All writes go into this table first. After we flush the data to the
215    /// db, the data is removed from this table and inserted into the object_cache.
216    ///
217    /// This table may contain both live and dead objects, since we flush both live and dead
218    /// objects to the db in order to support past object queries on fullnodes.
219    ///
220    /// Further, we only remove objects in FIFO order, which ensures that the cached
221    /// sequence of objects has no gaps. In other words, if we have versions 4, 8, 13 of
222    /// an object, we can deduce that version 9 does not exist. This also makes child object
223    /// reads efficient. `object_cache` cannot contain a more recent version of an object than
224    /// `objects`, and neither can have any gaps. Therefore if there is any object <= the version
225    /// bound for a child read in objects, it is the correct object to return.
226    objects: DashMap<ObjectID, CachedVersionMap<ObjectEntry>>,
227
228    // Markers for received objects and deleted shared objects. This contains all of the dirty
229    // marker state, which is committed to the db at the same time as other transaction data.
230    // After markers are committed to the db we remove them from this table and insert them into
231    // marker_cache.
232    markers: DashMap<MarkerKey, CachedVersionMap<MarkerValue>>,
233
234    transaction_effects: DashMap<TransactionEffectsDigest, TransactionEffects>,
235
236    transaction_events: DashMap<TransactionDigest, TransactionEvents>,
237
238    unchanged_loaded_runtime_objects: DashMap<TransactionDigest, Vec<ObjectKey>>,
239
240    executed_effects_digests: DashMap<TransactionDigest, TransactionEffectsDigest>,
241
242    // Transaction outputs that have not yet been written to the DB. Items are removed from this
243    // table as they are flushed to the db.
244    pending_transaction_writes: DashMap<TransactionDigest, Arc<TransactionOutputs>>,
245
246    total_transaction_inserts: AtomicU64,
247    total_transaction_commits: AtomicU64,
248}
249
250impl UncommittedData {
251    fn new() -> Self {
252        Self {
253            objects: DashMap::with_shard_amount(2048),
254            markers: DashMap::with_shard_amount(2048),
255            transaction_effects: DashMap::with_shard_amount(2048),
256            executed_effects_digests: DashMap::with_shard_amount(2048),
257            pending_transaction_writes: DashMap::with_shard_amount(2048),
258            transaction_events: DashMap::with_shard_amount(2048),
259            unchanged_loaded_runtime_objects: DashMap::with_shard_amount(2048),
260            total_transaction_inserts: AtomicU64::new(0),
261            total_transaction_commits: AtomicU64::new(0),
262        }
263    }
264
265    fn clear(&self) {
266        self.objects.clear();
267        self.markers.clear();
268        self.transaction_effects.clear();
269        self.executed_effects_digests.clear();
270        self.pending_transaction_writes.clear();
271        self.transaction_events.clear();
272        self.unchanged_loaded_runtime_objects.clear();
273        self.total_transaction_inserts
274            .store(0, std::sync::atomic::Ordering::Relaxed);
275        self.total_transaction_commits
276            .store(0, std::sync::atomic::Ordering::Relaxed);
277    }
278
279    fn is_empty(&self) -> bool {
280        let empty = self.pending_transaction_writes.is_empty();
281        if empty && cfg!(debug_assertions) {
282            assert!(
283                self.objects.is_empty()
284                    && self.markers.is_empty()
285                    && self.transaction_effects.is_empty()
286                    && self.executed_effects_digests.is_empty()
287                    && self.transaction_events.is_empty()
288                    && self.unchanged_loaded_runtime_objects.is_empty()
289                    && self
290                        .total_transaction_inserts
291                        .load(std::sync::atomic::Ordering::Relaxed)
292                        == self
293                            .total_transaction_commits
294                            .load(std::sync::atomic::Ordering::Relaxed),
295            );
296        }
297        empty
298    }
299}
300
301// Point items (anything without a version number) can be negatively cached as None
302type PointCacheItem<T> = Option<T>;
303
304// PointCacheItem can only be used for insert-only collections, so a Some entry
305// is always newer than a None entry.
306impl<T: Eq + std::fmt::Debug> IsNewer for PointCacheItem<T> {
307    fn is_newer_than(&self, other: &PointCacheItem<T>) -> bool {
308        match (self, other) {
309            (Some(_), None) => true,
310
311            (Some(a), Some(b)) => {
312                // conflicting inserts should never happen
313                debug_assert_eq!(a, b);
314                false
315            }
316
317            _ => false,
318        }
319    }
320}
321
322/// CachedData stores data that has been committed to the db, but is likely to be read soon.
323struct CachedCommittedData {
324    // See module level comment for an explanation of caching strategy.
325    object_cache: MokaCache<ObjectID, Arc<Mutex<CachedVersionMap<ObjectEntry>>>>,
326
327    // See module level comment for an explanation of caching strategy.
328    marker_cache: MokaCache<MarkerKey, Arc<Mutex<CachedVersionMap<MarkerValue>>>>,
329
330    transactions: MonotonicCache<TransactionDigest, PointCacheItem<Arc<VerifiedTransaction>>>,
331
332    transaction_effects:
333        MonotonicCache<TransactionEffectsDigest, PointCacheItem<Arc<TransactionEffects>>>,
334
335    transaction_events: MonotonicCache<TransactionDigest, PointCacheItem<Arc<TransactionEvents>>>,
336
337    executed_effects_digests:
338        MonotonicCache<TransactionDigest, PointCacheItem<TransactionEffectsDigest>>,
339
340    transaction_executed_in_last_epoch:
341        MonotonicCache<(EpochId, TransactionDigest), PointCacheItem<()>>,
342
343    // Objects that were read at transaction signing time - allows us to access them again at
344    // execution time with a single lock / hash lookup
345    _transaction_objects: MokaCache<TransactionDigest, Vec<Object>>,
346}
347
348impl CachedCommittedData {
349    fn new(config: &ExecutionCacheConfig) -> Self {
350        let object_cache = MokaCache::builder(8)
351            .max_capacity(randomize_cache_capacity_in_tests(
352                config.object_cache_size(),
353            ))
354            .build();
355        let marker_cache = MokaCache::builder(8)
356            .max_capacity(randomize_cache_capacity_in_tests(
357                config.marker_cache_size(),
358            ))
359            .build();
360
361        let transactions = MonotonicCache::new(randomize_cache_capacity_in_tests(
362            config.transaction_cache_size(),
363        ));
364        let transaction_effects = MonotonicCache::new(randomize_cache_capacity_in_tests(
365            config.effect_cache_size(),
366        ));
367        let transaction_events = MonotonicCache::new(randomize_cache_capacity_in_tests(
368            config.events_cache_size(),
369        ));
370        let executed_effects_digests = MonotonicCache::new(randomize_cache_capacity_in_tests(
371            config.executed_effect_cache_size(),
372        ));
373
374        let transaction_objects = MokaCache::builder(8)
375            .max_capacity(randomize_cache_capacity_in_tests(
376                config.transaction_objects_cache_size(),
377            ))
378            .build();
379
380        let transaction_executed_in_last_epoch = MonotonicCache::new(
381            randomize_cache_capacity_in_tests(config.executed_effect_cache_size()),
382        );
383
384        Self {
385            object_cache,
386            marker_cache,
387            transactions,
388            transaction_effects,
389            transaction_events,
390            executed_effects_digests,
391            transaction_executed_in_last_epoch,
392            _transaction_objects: transaction_objects,
393        }
394    }
395
396    fn clear_and_assert_empty(&self) {
397        self.object_cache.invalidate_all();
398        self.marker_cache.invalidate_all();
399        self.transactions.invalidate_all();
400        self.transaction_effects.invalidate_all();
401        self.transaction_events.invalidate_all();
402        self.executed_effects_digests.invalidate_all();
403        self.transaction_executed_in_last_epoch.invalidate_all();
404        self._transaction_objects.invalidate_all();
405
406        assert_empty(&self.object_cache);
407        assert_empty(&self.marker_cache);
408        assert!(self.transactions.is_empty());
409        assert!(self.transaction_effects.is_empty());
410        assert!(self.transaction_events.is_empty());
411        assert!(self.executed_effects_digests.is_empty());
412        assert!(self.transaction_executed_in_last_epoch.is_empty());
413        assert_empty(&self._transaction_objects);
414    }
415}
416
417fn assert_empty<K, V>(cache: &MokaCache<K, V>)
418where
419    K: std::hash::Hash + std::cmp::Eq + std::cmp::PartialEq + Send + Sync + 'static,
420    V: std::clone::Clone + std::marker::Send + std::marker::Sync + 'static,
421{
422    if cache.iter().next().is_some() {
423        panic!("cache should be empty");
424    }
425}
426
427pub struct WritebackCache {
428    dirty: UncommittedData,
429    cached: CachedCommittedData,
430
431    // We separately cache the latest version of each object. Although this seems
432    // redundant, it is the only way to support populating the cache after a read.
433    // We cannot simply insert objects that we read off the disk into `object_cache`,
434    // since that may violate the no-missing-versions property.
435    // `object_by_id_cache` is also written to on writes so that it is always coherent.
436    // Hence it contains both committed and dirty object data.
437    object_by_id_cache: MonotonicCache<ObjectID, LatestObjectCacheEntry>,
438
439    // The packages cache is treated separately from objects, because they are immutable and can be
440    // used by any number of transactions. Additionally, many operations require loading large
441    // numbers of packages (due to dependencies), so we want to try to keep all packages in memory.
442    //
443    // Also, this cache can contain packages that are dirty or committed, so it does not live in
444    // UncachedData or CachedCommittedData. The cache is populated in two ways:
445    // - when packages are written (in which case they will also be present in the dirty set)
446    // - after a cache miss. Because package IDs are unique (only one version exists for each ID)
447    //   we do not need to worry about the contiguous version property.
448    // - note that we removed any unfinalized packages from the cache during revert_state_update().
449    packages: MokaCache<ObjectID, PackageObject>,
450
451    object_locks: ObjectLocks,
452
453    executed_effects_digests_notify_read: NotifyRead<TransactionDigest, TransactionEffectsDigest>,
454    object_notify_read: NotifyRead<InputKey, ()>,
455
456    store: Arc<AuthorityStore>,
457    backpressure_threshold: u64,
458    backpressure_manager: Arc<BackpressureManager>,
459    metrics: Arc<ExecutionCacheMetrics>,
460}
461
462macro_rules! check_cache_entry_by_version {
463    ($self: ident, $table: expr, $level: expr, $cache: expr, $version: expr) => {
464        $self.metrics.record_cache_request($table, $level);
465        if let Some(cache) = $cache {
466            if let Some(entry) = cache.get(&$version) {
467                $self.metrics.record_cache_hit($table, $level);
468                return CacheResult::Hit(entry.clone());
469            }
470
471            if let Some(least_version) = cache.get_least() {
472                if least_version.0 < $version {
473                    // If the version is greater than the least version in the cache, then we know
474                    // that the object does not exist anywhere
475                    $self.metrics.record_cache_negative_hit($table, $level);
476                    return CacheResult::NegativeHit;
477                }
478            }
479        }
480        $self.metrics.record_cache_miss($table, $level);
481    };
482}
483
484macro_rules! check_cache_entry_by_latest {
485    ($self: ident, $table: expr, $level: expr, $cache: expr) => {
486        $self.metrics.record_cache_request($table, $level);
487        if let Some(cache) = $cache {
488            if let Some((version, entry)) = cache.get_highest() {
489                $self.metrics.record_cache_hit($table, $level);
490                return CacheResult::Hit((*version, entry.clone()));
491            } else {
492                panic!("empty CachedVersionMap should have been removed");
493            }
494        }
495        $self.metrics.record_cache_miss($table, $level);
496    };
497}
498
499impl WritebackCache {
500    /// Load an implicitly read system object at the requested version.
501    /// In normal execution, this function can block wait until the object is available at the requested version,
502    /// and it is guaranteed to return an object with the requested version.
503    /// In dry-runs, this function will never block wait, but may return None if the requested version was pruned by this point.
504    pub(crate) fn load_implicitly_read_system_object(
505        &self,
506        object_id: &ObjectID,
507        version: ConsensusObjectVersion,
508    ) -> Option<Object> {
509        assert!(
510            sui_types::IMPLICITLY_READ_SYSTEM_OBJECTS.contains(object_id),
511            "{object_id} is not an implicitly read system object"
512        );
513        let ConsensusObjectVersion {
514            initial_shared_version,
515            version,
516        } = version;
517        if let Some(object) = ObjectCacheRead::get_object_by_key(self, object_id, version) {
518            return Some(object);
519        }
520        self.metrics
521            .implicit_system_object_read_waits
522            .with_label_values(&[object_id.to_string().as_str()])
523            .inc();
524        let wait_start = Instant::now();
525        let key = InputKey::VersionedObject {
526            id: FullObjectID::Consensus((*object_id, initial_shared_version)),
527            version,
528        };
529        // Block wait until the object is available at the requested version.
530        // Note that before blocking, we check if the latest version already passed the requested version,
531        // if so it must imply that we have already produced the requested version.
532        // We are doing this check instead of exact version comparison to handle the rare case during
533        // dry-runs where the requested version was pruned by this point.
534        // Also note that in the case of dry-run, this will never block wait.
535        self.object_notify_read.read_one_blocking(
536            "load_implicitly_read_system_object",
537            &key,
538            |_key| {
539                ObjectCacheRead::get_object(self, object_id)
540                    .is_some_and(|latest| latest.version() >= version)
541                    .then_some(())
542            },
543        );
544        self.metrics
545            .implicit_system_object_read_wait_latency
546            .with_label_values(&[object_id.to_string().as_str()])
547            .observe(wait_start.elapsed().as_secs_f64());
548        ObjectCacheRead::get_object_by_key(self, object_id, version)
549    }
550
551    pub fn new(
552        config: &ExecutionCacheConfig,
553        store: Arc<AuthorityStore>,
554        metrics: Arc<ExecutionCacheMetrics>,
555        backpressure_manager: Arc<BackpressureManager>,
556    ) -> Self {
557        let packages = MokaCache::builder(8)
558            .max_capacity(randomize_cache_capacity_in_tests(
559                config.package_cache_size(),
560            ))
561            .build();
562        Self {
563            dirty: UncommittedData::new(),
564            cached: CachedCommittedData::new(config),
565            object_by_id_cache: MonotonicCache::new(randomize_cache_capacity_in_tests(
566                config.object_by_id_cache_size(),
567            )),
568            packages,
569            object_locks: ObjectLocks::new(),
570            executed_effects_digests_notify_read: NotifyRead::new(),
571            object_notify_read: NotifyRead::new(),
572            store,
573            backpressure_manager,
574            backpressure_threshold: config.backpressure_threshold(),
575            metrics,
576        }
577    }
578
579    pub fn new_for_tests(store: Arc<AuthorityStore>) -> Self {
580        Self::new(
581            &Default::default(),
582            store,
583            ExecutionCacheMetrics::new(&prometheus::Registry::new()).into(),
584            BackpressureManager::new_for_tests(),
585        )
586    }
587
588    #[cfg(test)]
589    pub fn reset_for_test(&mut self) {
590        let mut new = Self::new(
591            &Default::default(),
592            self.store.clone(),
593            self.metrics.clone(),
594            self.backpressure_manager.clone(),
595        );
596        std::mem::swap(self, &mut new);
597    }
598
599    pub fn evict_executed_effects_from_cache_for_testing(&self, tx_digest: &TransactionDigest) {
600        self.cached.executed_effects_digests.invalidate(tx_digest);
601        self.cached.transaction_events.invalidate(tx_digest);
602        self.cached.transactions.invalidate(tx_digest);
603    }
604
605    fn write_object_entry(
606        &self,
607        object_id: &ObjectID,
608        version: SequenceNumber,
609        object: ObjectEntry,
610    ) {
611        trace!(?object_id, ?version, ?object, "inserting object entry");
612        self.metrics.record_cache_write("object");
613
614        // We must hold the lock for the object entry while inserting to the
615        // object_by_id_cache. Otherwise, a surprising bug can occur:
616        //
617        // 1. A thread executing TX1 can write object (O,1) to the dirty set and then pause.
618        // 2. TX2, which reads (O,1) can begin executing, because ExecutionScheduler immediately
619        //    schedules transactions if their inputs are available. It does not matter that TX1
620        //    hasn't finished executing yet.
621        // 3. TX2 can write (O,2) to both the dirty set and the object_by_id_cache.
622        // 4. The thread executing TX1 can resume and write (O,1) to the object_by_id_cache.
623        //
624        // Now, any subsequent attempt to get the latest version of O will return (O,1) instead of
625        // (O,2).
626        //
627        // This seems very unlikely, but it may be possible under the following circumstances:
628        // - While a thread is unlikely to pause for so long, moka cache uses optimistic
629        //   lock-free algorithms that have retry loops. Possibly, under high contention, this
630        //   code might spin for a surprisingly long time.
631        // - Additionally, many concurrent re-executions of the same tx could happen due to
632        //   the tx finalizer, plus checkpoint executor, consensus, and RPCs from fullnodes.
633        let mut entry = self.dirty.objects.entry(*object_id).or_default();
634
635        self.object_by_id_cache
636            .insert(
637                object_id,
638                LatestObjectCacheEntry::Object(version, object.clone()),
639                Ticket::Write,
640            )
641            // While Ticket::Write cannot expire, this insert may still fail.
642            // See the comment in `MonotonicCache::insert`.
643            .ok();
644
645        entry.insert(version, object.clone());
646
647        if let ObjectEntry::Object(object) = &object {
648            if object.is_package() {
649                self.object_notify_read
650                    .notify(&InputKey::Package { id: *object_id }, &());
651            } else if !object.is_child_object() {
652                self.object_notify_read.notify(
653                    &InputKey::VersionedObject {
654                        id: object.full_id(),
655                        version: object.version(),
656                    },
657                    &(),
658                );
659            }
660        }
661    }
662
663    fn write_marker_value(
664        &self,
665        epoch_id: EpochId,
666        object_key: FullObjectKey,
667        marker_value: MarkerValue,
668    ) {
669        tracing::trace!("inserting marker value {object_key:?}: {marker_value:?}",);
670        self.metrics.record_cache_write("marker");
671        self.dirty
672            .markers
673            .entry((epoch_id, object_key.id()))
674            .or_default()
675            .value_mut()
676            .insert(object_key.version(), marker_value);
677        // It is possible for a transaction to use a consensus stream ended
678        // object in the input, hence we must notify that it is now available
679        // at the assigned version, so that any transaction waiting for this
680        // object version can start execution.
681        if matches!(marker_value, MarkerValue::ConsensusStreamEnded(_)) {
682            self.object_notify_read.notify(
683                &InputKey::VersionedObject {
684                    id: object_key.id(),
685                    version: object_key.version(),
686                },
687                &(),
688            );
689        }
690    }
691
692    // lock both the dirty and committed sides of the cache, and then pass the entries to
693    // the callback. Written with the `with` pattern because any other way of doing this
694    // creates lifetime hell.
695    fn with_locked_cache_entries<K, V, R>(
696        dirty_map: &DashMap<K, CachedVersionMap<V>>,
697        cached_map: &MokaCache<K, Arc<Mutex<CachedVersionMap<V>>>>,
698        key: &K,
699        cb: impl FnOnce(Option<&CachedVersionMap<V>>, Option<&CachedVersionMap<V>>) -> R,
700    ) -> R
701    where
702        K: Copy + Eq + Hash + Send + Sync + 'static,
703        V: Send + Sync + 'static,
704    {
705        let dirty_entry = dirty_map.entry(*key);
706        let dirty_entry = match &dirty_entry {
707            DashMapEntry::Occupied(occupied) => Some(occupied.get()),
708            DashMapEntry::Vacant(_) => None,
709        };
710
711        let cached_entry = cached_map.get(key);
712        let cached_lock = cached_entry.as_ref().map(|entry| entry.lock());
713        let cached_entry = cached_lock.as_deref();
714
715        cb(dirty_entry, cached_entry)
716    }
717
718    // Attempt to get an object from the cache. The DB is not consulted.
719    // Can return Hit, Miss, or NegativeHit (if the object is known to not exist).
720    fn get_object_entry_by_key_cache_only(
721        &self,
722        object_id: &ObjectID,
723        version: SequenceNumber,
724    ) -> CacheResult<ObjectEntry> {
725        Self::with_locked_cache_entries(
726            &self.dirty.objects,
727            &self.cached.object_cache,
728            object_id,
729            |dirty_entry, cached_entry| {
730                check_cache_entry_by_version!(
731                    self,
732                    "object_by_version",
733                    "uncommitted",
734                    dirty_entry,
735                    version
736                );
737                check_cache_entry_by_version!(
738                    self,
739                    "object_by_version",
740                    "committed",
741                    cached_entry,
742                    version
743                );
744                CacheResult::Miss
745            },
746        )
747    }
748
749    fn get_object_by_key_cache_only(
750        &self,
751        object_id: &ObjectID,
752        version: SequenceNumber,
753    ) -> CacheResult<Object> {
754        match self.get_object_entry_by_key_cache_only(object_id, version) {
755            CacheResult::Hit(entry) => match entry {
756                ObjectEntry::Object(object) => CacheResult::Hit(object),
757                ObjectEntry::Deleted | ObjectEntry::Wrapped => CacheResult::NegativeHit,
758            },
759            CacheResult::Miss => CacheResult::Miss,
760            CacheResult::NegativeHit => CacheResult::NegativeHit,
761        }
762    }
763
764    fn get_object_entry_by_id_cache_only(
765        &self,
766        request_type: &'static str,
767        object_id: &ObjectID,
768    ) -> CacheResult<(SequenceNumber, ObjectEntry)> {
769        self.metrics
770            .record_cache_request(request_type, "object_by_id");
771        let entry = self.object_by_id_cache.get(object_id);
772
773        if cfg!(debug_assertions)
774            && let Some(entry) = &entry
775        {
776            // check that cache is coherent
777            let highest: Option<ObjectEntry> = self
778                .dirty
779                .objects
780                .get(object_id)
781                .and_then(|entry| entry.get_highest().map(|(_, o)| o.clone()))
782                .or_else(|| {
783                    let obj: Option<ObjectEntry> = self
784                        .store
785                        .get_latest_object_or_tombstone(*object_id)
786                        .unwrap()
787                        .map(|(_, o)| o.into());
788                    obj
789                });
790
791            let cache_entry = match &*entry.lock() {
792                LatestObjectCacheEntry::Object(_, entry) => Some(entry.clone()),
793                LatestObjectCacheEntry::NonExistent => None,
794            };
795
796            // If the cache entry is a tombstone, the db entry may be missing if it was pruned.
797            let tombstone_possibly_pruned = highest.is_none()
798                && cache_entry
799                    .as_ref()
800                    .map(|e| e.is_tombstone())
801                    .unwrap_or(false);
802
803            if highest != cache_entry && !tombstone_possibly_pruned {
804                tracing::error!(
805                    ?highest,
806                    ?cache_entry,
807                    ?tombstone_possibly_pruned,
808                    "object_by_id cache is incoherent for {:?}",
809                    object_id
810                );
811                panic!("object_by_id cache is incoherent for {:?}", object_id);
812            }
813        }
814
815        if let Some(entry) = entry {
816            let entry = entry.lock();
817            match &*entry {
818                LatestObjectCacheEntry::Object(latest_version, latest_object) => {
819                    self.metrics.record_cache_hit(request_type, "object_by_id");
820                    return CacheResult::Hit((*latest_version, latest_object.clone()));
821                }
822                LatestObjectCacheEntry::NonExistent => {
823                    self.metrics
824                        .record_cache_negative_hit(request_type, "object_by_id");
825                    return CacheResult::NegativeHit;
826                }
827            }
828        } else {
829            self.metrics.record_cache_miss(request_type, "object_by_id");
830        }
831
832        Self::with_locked_cache_entries(
833            &self.dirty.objects,
834            &self.cached.object_cache,
835            object_id,
836            |dirty_entry, cached_entry| {
837                check_cache_entry_by_latest!(self, request_type, "uncommitted", dirty_entry);
838                check_cache_entry_by_latest!(self, request_type, "committed", cached_entry);
839                CacheResult::Miss
840            },
841        )
842    }
843
844    fn get_marker_value_cache_only(
845        &self,
846        object_key: FullObjectKey,
847        epoch_id: EpochId,
848    ) -> CacheResult<MarkerValue> {
849        Self::with_locked_cache_entries(
850            &self.dirty.markers,
851            &self.cached.marker_cache,
852            &(epoch_id, object_key.id()),
853            |dirty_entry, cached_entry| {
854                check_cache_entry_by_version!(
855                    self,
856                    "marker_by_version",
857                    "uncommitted",
858                    dirty_entry,
859                    object_key.version()
860                );
861                check_cache_entry_by_version!(
862                    self,
863                    "marker_by_version",
864                    "committed",
865                    cached_entry,
866                    object_key.version()
867                );
868                CacheResult::Miss
869            },
870        )
871    }
872
873    fn get_latest_marker_value_cache_only(
874        &self,
875        object_id: FullObjectID,
876        epoch_id: EpochId,
877    ) -> CacheResult<(SequenceNumber, MarkerValue)> {
878        Self::with_locked_cache_entries(
879            &self.dirty.markers,
880            &self.cached.marker_cache,
881            &(epoch_id, object_id),
882            |dirty_entry, cached_entry| {
883                check_cache_entry_by_latest!(self, "marker_latest", "uncommitted", dirty_entry);
884                check_cache_entry_by_latest!(self, "marker_latest", "committed", cached_entry);
885                CacheResult::Miss
886            },
887        )
888    }
889
890    fn get_object_impl(&self, request_type: &'static str, id: &ObjectID) -> Option<Object> {
891        let ticket = self.object_by_id_cache.get_ticket_for_read(id);
892        match self.get_object_entry_by_id_cache_only(request_type, id) {
893            CacheResult::Hit((_, entry)) => match entry {
894                ObjectEntry::Object(object) => Some(object),
895                ObjectEntry::Deleted | ObjectEntry::Wrapped => None,
896            },
897            CacheResult::NegativeHit => None,
898            CacheResult::Miss => {
899                let obj = self
900                    .store
901                    .get_latest_object_or_tombstone(*id)
902                    .expect("db error");
903                match obj {
904                    Some((key, obj)) => {
905                        self.cache_latest_object_by_id(
906                            id,
907                            LatestObjectCacheEntry::Object(key.1, obj.clone().into()),
908                            ticket,
909                        );
910                        match obj {
911                            ObjectOrTombstone::Object(object) => Some(object),
912                            ObjectOrTombstone::Tombstone(_) => None,
913                        }
914                    }
915                    None => {
916                        self.cache_object_not_found(id, ticket);
917                        None
918                    }
919                }
920            }
921        }
922    }
923
924    fn record_db_get(&self, request_type: &'static str) -> &AuthorityStore {
925        self.metrics.record_cache_request(request_type, "db");
926        &self.store
927    }
928
929    fn record_db_multi_get(&self, request_type: &'static str, count: usize) -> &AuthorityStore {
930        self.metrics
931            .record_cache_multi_request(request_type, "db", count);
932        &self.store
933    }
934
935    #[instrument(level = "debug", skip_all)]
936    fn write_transaction_outputs(&self, epoch_id: EpochId, tx_outputs: Arc<TransactionOutputs>) {
937        let tx_digest = *tx_outputs.transaction.digest();
938        trace!(?tx_digest, "writing transaction outputs to cache");
939
940        assert!(
941            !self.transaction_executed_in_last_epoch(&tx_digest, epoch_id),
942            "Transaction {:?} was already executed in epoch {}",
943            tx_digest,
944            epoch_id.saturating_sub(1)
945        );
946
947        let TransactionOutputs {
948            transaction,
949            effects,
950            markers,
951            written,
952            deleted,
953            wrapped,
954            events,
955            unchanged_loaded_runtime_objects,
956            ..
957        } = &*tx_outputs;
958
959        // Deletions and wraps must be written first. The reason is that one of the deletes
960        // may be a child object, and if we write the parent object first, a reader may or may
961        // not see the previous version of the child object, instead of the deleted/wrapped
962        // tombstone, which would cause an execution fork
963        for ObjectKey(id, version) in deleted.iter() {
964            self.write_object_entry(id, *version, ObjectEntry::Deleted);
965        }
966
967        for ObjectKey(id, version) in wrapped.iter() {
968            self.write_object_entry(id, *version, ObjectEntry::Wrapped);
969        }
970
971        // Update all markers
972        for (object_key, marker_value) in markers.iter() {
973            self.write_marker_value(epoch_id, *object_key, *marker_value);
974        }
975
976        // Write children before parents to ensure that readers do not observe a parent object
977        // before its most recent children are visible.
978        for (object_id, object) in written.iter() {
979            if object.is_child_object() {
980                self.write_object_entry(object_id, object.version(), object.clone().into());
981            }
982        }
983        for (object_id, object) in written.iter() {
984            if !object.is_child_object() {
985                self.write_object_entry(object_id, object.version(), object.clone().into());
986                if object.is_package() {
987                    debug!("caching package: {:?}", object.compute_object_reference());
988                    self.packages
989                        .insert(*object_id, PackageObject::new(object.clone()));
990                }
991            }
992        }
993
994        let tx_digest = *transaction.digest();
995        debug!(
996            ?tx_digest,
997            "Writing transaction output objects to cache: {:?}",
998            written
999                .values()
1000                .map(|o| (o.id(), o.version()))
1001                .collect::<Vec<_>>(),
1002        );
1003        let effects_digest = effects.digest();
1004
1005        self.metrics.record_cache_write("transaction_block");
1006        self.dirty
1007            .pending_transaction_writes
1008            .insert(tx_digest, tx_outputs.clone());
1009
1010        // insert transaction effects before executed_effects_digests so that there
1011        // are never dangling entries in executed_effects_digests
1012        self.metrics.record_cache_write("transaction_effects");
1013        self.dirty
1014            .transaction_effects
1015            .insert(effects_digest, effects.clone());
1016
1017        // note: if events.data.is_empty(), then there are no events for this transaction. We
1018        // store it anyway to avoid special cases in commint_transaction_outputs, and translate
1019        // an empty events structure to None when reading.
1020        self.metrics.record_cache_write("transaction_events");
1021        self.dirty
1022            .transaction_events
1023            .insert(tx_digest, events.clone());
1024
1025        self.metrics
1026            .record_cache_write("unchanged_loaded_runtime_objects");
1027        self.dirty
1028            .unchanged_loaded_runtime_objects
1029            .insert(tx_digest, unchanged_loaded_runtime_objects.clone());
1030
1031        self.metrics.record_cache_write("executed_effects_digests");
1032        self.dirty
1033            .executed_effects_digests
1034            .insert(tx_digest, effects_digest);
1035
1036        self.executed_effects_digests_notify_read
1037            .notify(&tx_digest, &effects_digest);
1038
1039        self.metrics
1040            .pending_notify_read
1041            .set(self.executed_effects_digests_notify_read.num_pending() as i64);
1042
1043        let prev = self
1044            .dirty
1045            .total_transaction_inserts
1046            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1047
1048        let pending_count = (prev + 1).saturating_sub(
1049            self.dirty
1050                .total_transaction_commits
1051                .load(std::sync::atomic::Ordering::Relaxed),
1052        );
1053
1054        self.set_backpressure(pending_count);
1055    }
1056
1057    fn build_db_batch(&self, epoch: EpochId, digests: &[TransactionDigest]) -> Batch {
1058        let _metrics_guard = mysten_metrics::monitored_scope("WritebackCache::build_db_batch");
1059        let mut all_outputs = Vec::with_capacity(digests.len());
1060        for tx in digests {
1061            let Some(outputs) = self
1062                .dirty
1063                .pending_transaction_writes
1064                .get(tx)
1065                .map(|o| o.clone())
1066            else {
1067                // This can happen in the following rare case:
1068                // All transactions in the checkpoint are committed to the db (by commit_transaction_outputs,
1069                // called in CheckpointExecutor::process_executed_transactions), but the process crashes before
1070                // the checkpoint water mark is bumped. We will then re-commit the checkpoint at startup,
1071                // despite that all transactions are already executed.
1072                warn!("Attempt to commit unknown transaction {:?}", tx);
1073                continue;
1074            };
1075            all_outputs.push(outputs);
1076        }
1077
1078        let batch = self
1079            .store
1080            .build_db_batch(epoch, &all_outputs)
1081            .expect("db error");
1082        (all_outputs, batch)
1083    }
1084
1085    // Commits dirty data for the given TransactionDigest to the db.
1086    #[instrument(level = "debug", skip_all)]
1087    fn commit_transaction_outputs(
1088        &self,
1089        epoch: EpochId,
1090        (all_outputs, db_batch): Batch,
1091        digests: &[TransactionDigest],
1092    ) {
1093        let _metrics_guard =
1094            mysten_metrics::monitored_scope("WritebackCache::commit_transaction_outputs");
1095        fail_point!("writeback-cache-commit");
1096        trace!(?digests);
1097
1098        // Flush writes to disk before removing anything from dirty set. otherwise,
1099        // a cache eviction could cause a value to disappear briefly, even if we insert to the
1100        // cache before removing from the dirty set.
1101        db_batch.write().expect("db error");
1102
1103        let _metrics_guard =
1104            mysten_metrics::monitored_scope("WritebackCache::commit_transaction_outputs::flush");
1105        // Parallel phase: tx-level metadata is keyed by unique tx_digest/effects_digest,
1106        // so there are no cross-transaction ordering constraints.
1107        all_outputs.par_iter().with_min_len(16).for_each(|outputs| {
1108            let tx_digest = outputs.transaction.digest();
1109            assert!(
1110                self.dirty
1111                    .pending_transaction_writes
1112                    .remove(tx_digest)
1113                    .is_some()
1114            );
1115            self.flush_tx_metadata_from_dirty_to_cached(*tx_digest, outputs);
1116        });
1117
1118        // Sequential phase: object/marker versions must be popped in causal order
1119        // (oldest first) per object_id. Multiple transactions in the batch can touch
1120        // the same shared object at consecutive versions, so this loop must preserve
1121        // the order of all_outputs.
1122        for outputs in all_outputs.iter() {
1123            self.flush_objects_from_dirty_to_cached(epoch, outputs);
1124        }
1125
1126        let num_outputs = all_outputs.len() as u64;
1127        let num_commits = self
1128            .dirty
1129            .total_transaction_commits
1130            .fetch_add(num_outputs, std::sync::atomic::Ordering::Relaxed)
1131            + num_outputs;
1132
1133        let pending_count = self
1134            .dirty
1135            .total_transaction_inserts
1136            .load(std::sync::atomic::Ordering::Relaxed)
1137            .saturating_sub(num_commits);
1138
1139        self.set_backpressure(pending_count);
1140    }
1141
1142    fn approximate_pending_transaction_count(&self) -> u64 {
1143        let num_commits = self
1144            .dirty
1145            .total_transaction_commits
1146            .load(std::sync::atomic::Ordering::Relaxed);
1147
1148        self.dirty
1149            .total_transaction_inserts
1150            .load(std::sync::atomic::Ordering::Relaxed)
1151            .saturating_sub(num_commits)
1152    }
1153
1154    fn set_backpressure(&self, pending_count: u64) {
1155        let backpressure = pending_count > self.backpressure_threshold;
1156        let backpressure_changed = self.backpressure_manager.set_backpressure(backpressure);
1157        if backpressure_changed {
1158            self.metrics.backpressure_toggles.inc();
1159        }
1160        self.metrics
1161            .backpressure_status
1162            .set(if backpressure { 1 } else { 0 });
1163    }
1164
1165    // Flushes tx-level metadata for a single transaction from dirty to cache.
1166    // All keys are unique per transaction (tx_digest, effects_digest), so this
1167    // is safe to call in parallel across transactions.
1168    fn flush_tx_metadata_from_dirty_to_cached(
1169        &self,
1170        tx_digest: TransactionDigest,
1171        outputs: &TransactionOutputs,
1172    ) {
1173        // TODO: outputs should have a strong count of 1 so we should be able to move out of it
1174        let TransactionOutputs {
1175            transaction,
1176            effects,
1177            events,
1178            ..
1179        } = outputs;
1180
1181        let effects_digest = effects.digest();
1182
1183        // Update cache before removing from self.dirty to avoid
1184        // unnecessary cache misses
1185        self.cached
1186            .transactions
1187            .insert(
1188                &tx_digest,
1189                PointCacheItem::Some(transaction.clone()),
1190                Ticket::Write,
1191            )
1192            .ok();
1193        self.cached
1194            .transaction_effects
1195            .insert(
1196                &effects_digest,
1197                PointCacheItem::Some(effects.clone().into()),
1198                Ticket::Write,
1199            )
1200            .ok();
1201        self.cached
1202            .executed_effects_digests
1203            .insert(
1204                &tx_digest,
1205                PointCacheItem::Some(effects_digest),
1206                Ticket::Write,
1207            )
1208            .ok();
1209        self.cached
1210            .transaction_events
1211            .insert(
1212                &tx_digest,
1213                PointCacheItem::Some(events.clone().into()),
1214                Ticket::Write,
1215            )
1216            .ok();
1217
1218        self.dirty
1219            .transaction_effects
1220            .remove(&effects_digest)
1221            .expect("effects must exist");
1222
1223        self.dirty
1224            .transaction_events
1225            .remove(&tx_digest)
1226            .expect("events must exist");
1227
1228        self.dirty
1229            .unchanged_loaded_runtime_objects
1230            .remove(&tx_digest)
1231            .expect("unchanged_loaded_runtime_objects must exist");
1232
1233        self.dirty
1234            .executed_effects_digests
1235            .remove(&tx_digest)
1236            .expect("executed effects must exist");
1237    }
1238
1239    // Flushes object and marker versions for a single transaction from dirty to cache.
1240    // Multiple transactions in the same batch can modify the same shared object at
1241    // consecutive versions, so callers must invoke this in causal (checkpoint) order.
1242    fn flush_objects_from_dirty_to_cached(&self, epoch: EpochId, outputs: &TransactionOutputs) {
1243        let TransactionOutputs {
1244            markers,
1245            written,
1246            deleted,
1247            wrapped,
1248            ..
1249        } = outputs;
1250
1251        for (object_key, marker_value) in markers.iter() {
1252            Self::move_version_from_dirty_to_cache(
1253                &self.dirty.markers,
1254                &self.cached.marker_cache,
1255                (epoch, object_key.id()),
1256                object_key.version(),
1257                marker_value,
1258            );
1259        }
1260
1261        for (object_id, object) in written.iter() {
1262            Self::move_version_from_dirty_to_cache(
1263                &self.dirty.objects,
1264                &self.cached.object_cache,
1265                *object_id,
1266                object.version(),
1267                &ObjectEntry::Object(object.clone()),
1268            );
1269        }
1270
1271        for ObjectKey(object_id, version) in deleted.iter() {
1272            Self::move_version_from_dirty_to_cache(
1273                &self.dirty.objects,
1274                &self.cached.object_cache,
1275                *object_id,
1276                *version,
1277                &ObjectEntry::Deleted,
1278            );
1279        }
1280
1281        for ObjectKey(object_id, version) in wrapped.iter() {
1282            Self::move_version_from_dirty_to_cache(
1283                &self.dirty.objects,
1284                &self.cached.object_cache,
1285                *object_id,
1286                *version,
1287                &ObjectEntry::Wrapped,
1288            );
1289        }
1290    }
1291
1292    // Move the oldest/least entry from the dirty queue to the cache queue.
1293    // This is called after the entry is committed to the db.
1294    fn move_version_from_dirty_to_cache<K, V>(
1295        dirty: &DashMap<K, CachedVersionMap<V>>,
1296        cache: &MokaCache<K, Arc<Mutex<CachedVersionMap<V>>>>,
1297        key: K,
1298        version: SequenceNumber,
1299        value: &V,
1300    ) where
1301        K: Eq + std::hash::Hash + Clone + Send + Sync + Copy + 'static,
1302        V: Send + Sync + Clone + Eq + std::fmt::Debug + 'static,
1303    {
1304        static MAX_VERSIONS: usize = 3;
1305
1306        // IMPORTANT: lock both the dirty set entry and the cache entry before modifying either.
1307        // this ensures that readers cannot see a value temporarily disappear.
1308        let dirty_entry = dirty.entry(key);
1309        let cache_entry = cache.entry(key).or_default();
1310        let mut cache_map = cache_entry.value().lock();
1311
1312        // insert into cache and drop old versions.
1313        cache_map.insert(version, value.clone());
1314        // TODO: make this automatic by giving CachedVersionMap an optional max capacity
1315        cache_map.truncate_to(MAX_VERSIONS);
1316
1317        let DashMapEntry::Occupied(mut occupied_dirty_entry) = dirty_entry else {
1318            panic!("dirty map must exist");
1319        };
1320
1321        let removed = occupied_dirty_entry.get_mut().pop_oldest(&version);
1322
1323        assert_eq!(removed.as_ref(), Some(value), "dirty version must exist");
1324
1325        // if there are no versions remaining, remove the map entry
1326        if occupied_dirty_entry.get().is_empty() {
1327            occupied_dirty_entry.remove();
1328        }
1329    }
1330
1331    // Updates the latest object id cache with an entry that was read from the db.
1332    fn cache_latest_object_by_id(
1333        &self,
1334        object_id: &ObjectID,
1335        object: LatestObjectCacheEntry,
1336        ticket: Ticket,
1337    ) {
1338        trace!("caching object by id: {:?} {:?}", object_id, object);
1339        if self
1340            .object_by_id_cache
1341            .insert(object_id, object, ticket)
1342            .is_ok()
1343        {
1344            self.metrics.record_cache_write("object_by_id");
1345        } else {
1346            trace!("discarded cache write due to expired ticket");
1347            self.metrics.record_ticket_expiry();
1348        }
1349    }
1350
1351    fn cache_object_not_found(&self, object_id: &ObjectID, ticket: Ticket) {
1352        self.cache_latest_object_by_id(object_id, LatestObjectCacheEntry::NonExistent, ticket);
1353    }
1354
1355    fn clear_state_end_of_epoch_impl(&self, execution_guard: &ExecutionLockWriteGuard<'_>) {
1356        info!("clearing state at end of epoch");
1357
1358        // Note: there cannot be any concurrent writes to self.dirty while we are in this function,
1359        // as all transaction execution is paused.
1360        for r in self.dirty.pending_transaction_writes.iter() {
1361            let outputs = r.value();
1362            if !outputs
1363                .transaction
1364                .transaction_data()
1365                .shared_input_objects()
1366                .is_empty()
1367            {
1368                debug_fatal!("transaction must be single writer");
1369            }
1370            info!(
1371                "clearing state for transaction {:?}",
1372                outputs.transaction.digest()
1373            );
1374            for (object_id, object) in outputs.written.iter() {
1375                if object.is_package() {
1376                    info!("removing non-finalized package from cache: {:?}", object_id);
1377                    self.packages.invalidate(object_id);
1378                }
1379                self.object_by_id_cache.invalidate(object_id);
1380                self.cached.object_cache.invalidate(object_id);
1381            }
1382
1383            for ObjectKey(object_id, _) in outputs.deleted.iter().chain(outputs.wrapped.iter()) {
1384                self.object_by_id_cache.invalidate(object_id);
1385                self.cached.object_cache.invalidate(object_id);
1386            }
1387        }
1388
1389        self.dirty.clear();
1390
1391        info!("clearing old transaction locks");
1392        self.object_locks.clear();
1393        info!("clearing object per epoch marker table");
1394        self.store
1395            .clear_object_per_epoch_marker_table(execution_guard)
1396            .expect("db error");
1397    }
1398
1399    fn bulk_insert_genesis_objects_impl(&self, objects: &[Object]) {
1400        self.store
1401            .bulk_insert_genesis_objects(objects)
1402            .expect("db error");
1403        for obj in objects {
1404            self.cached.object_cache.invalidate(&obj.id());
1405            self.object_by_id_cache.invalidate(&obj.id());
1406        }
1407    }
1408
1409    fn insert_genesis_object_impl(&self, object: Object) {
1410        self.object_by_id_cache.invalidate(&object.id());
1411        self.cached.object_cache.invalidate(&object.id());
1412        self.store.insert_genesis_object(object).expect("db error");
1413    }
1414
1415    pub fn clear_caches_and_assert_empty(&self) {
1416        info!("clearing caches");
1417        self.cached.clear_and_assert_empty();
1418        self.object_by_id_cache.invalidate_all();
1419        assert!(&self.object_by_id_cache.is_empty());
1420        self.packages.invalidate_all();
1421        assert_empty(&self.packages);
1422    }
1423}
1424
1425fn account_amount_from_object(account_obj: &Object) -> u128 {
1426    let (_, AccumulatorValue::U128(value)) =
1427        account_obj.data.try_as_move().unwrap().try_into().unwrap();
1428    value.value
1429}
1430
1431impl AccountFundsRead for WritebackCache {
1432    fn get_latest_account_amount(&self, account_id: &AccumulatorObjId) -> u128 {
1433        ObjectCacheRead::get_object(self, account_id.inner())
1434            .map(|account_obj| account_amount_from_object(&account_obj))
1435            .unwrap_or(0)
1436    }
1437
1438    fn get_consistent_latest_account_amount_and_version(
1439        &self,
1440        account_id: &AccumulatorObjId,
1441    ) -> (u128, SequenceNumber) {
1442        // Settlement is not atomic. A settlement transaction writes the accumulator
1443        // objects at version V+1 first, and then a later barrier transaction bumps the
1444        // root from V to V+1. A reader that observes the state in between sees
1445        // post-settlement account objects alongside the pre-settlement root version,
1446        // and reading the "latest" account can therefore disagree with the root we
1447        // just captured.
1448        //
1449        // We handle this with two pieces:
1450        //
1451        // 1. MVCC read capped at the captured root version (see the call site below).
1452        //    By construction of the settlement/barrier ordering, every account object's
1453        //    version is <= the root version after the corresponding barrier runs, so
1454        //    capping at the captured root strips away any newer-settlement writes that
1455        //    have raced ahead of the barrier. The returned amount is the balance at or
1456        //    before the captured root version, even if the account object has a newer
1457        //    latest version by the time this method returns.
1458        //
1459        // 2. Root-version stability check (pre == post). `get_account_amount_at_version`
1460        //    is only safe to call when the target version has not been pruned. Pruning
1461        //    is tied to root advancement, so by reading the root before and after the
1462        //    MVCC read and retrying on mismatch, we ensure that no root advance (and
1463        //    therefore no pruning of the version we read at) could have happened while
1464        //    we were reading — the data we read is still live in the system (memory or
1465        //    db) throughout the call.
1466        let mut pre_root_version =
1467            ObjectCacheRead::get_object(self, &SUI_ACCUMULATOR_ROOT_OBJECT_ID)
1468                .unwrap()
1469                .version();
1470        let mut loop_iter = 0;
1471        loop {
1472            loop_iter += 1;
1473            // Safe because of (1) and (2) above: the stability check below bounds the
1474            // lifetime of `pre_root_version` to a window in which no pruning happens.
1475            let value = self.get_account_amount_at_version(account_id, pre_root_version);
1476            let post_root_version =
1477                ObjectCacheRead::get_object(self, &SUI_ACCUMULATOR_ROOT_OBJECT_ID)
1478                    .unwrap()
1479                    .version();
1480            if pre_root_version == post_root_version {
1481                if loop_iter > 3 {
1482                    debug_fatal_no_invariant!(
1483                        "Root version stabilized after {} iterations during MVCC read",
1484                        loop_iter
1485                    );
1486                }
1487                return (value, pre_root_version);
1488            }
1489            debug!(
1490                "Root version changed from {} to {} during MVCC read, retrying",
1491                pre_root_version, post_root_version
1492            );
1493            pre_root_version = post_root_version;
1494        }
1495    }
1496
1497    fn get_account_amount_at_version(
1498        &self,
1499        account_id: &AccumulatorObjId,
1500        version: SequenceNumber,
1501    ) -> u128 {
1502        let account_obj = self.find_object_lt_or_eq_version(*account_id.inner(), version);
1503        account_obj
1504            .map(|account_obj| account_amount_from_object(&account_obj))
1505            .unwrap_or(0)
1506    }
1507}
1508
1509impl ExecutionCacheAPI for WritebackCache {}
1510
1511impl ExecutionCacheCommit for WritebackCache {
1512    fn build_db_batch(&self, epoch: EpochId, digests: &[TransactionDigest]) -> Batch {
1513        self.build_db_batch(epoch, digests)
1514    }
1515
1516    fn set_highest_committed_checkpoint_in_batch(
1517        &self,
1518        batch: &mut Batch,
1519        checkpoint: CheckpointSequenceNumber,
1520    ) {
1521        self.store
1522            .perpetual_tables
1523            .set_highest_committed_checkpoint(&mut batch.1, checkpoint)
1524            .expect("db error");
1525    }
1526
1527    fn commit_transaction_outputs(
1528        &self,
1529        epoch: EpochId,
1530        batch: Batch,
1531        digests: &[TransactionDigest],
1532    ) {
1533        WritebackCache::commit_transaction_outputs(self, epoch, batch, digests)
1534    }
1535
1536    fn persist_transaction(&self, tx: &VerifiedExecutableTransaction) {
1537        self.store.persist_transaction(tx).expect("db error");
1538    }
1539
1540    fn approximate_pending_transaction_count(&self) -> u64 {
1541        WritebackCache::approximate_pending_transaction_count(self)
1542    }
1543}
1544
1545impl ObjectCacheRead for WritebackCache {
1546    fn get_package_object(&self, package_id: &ObjectID) -> SuiResult<Option<PackageObject>> {
1547        self.metrics
1548            .record_cache_request("package", "package_cache");
1549        if let Some(p) = self.packages.get(package_id) {
1550            if cfg!(debug_assertions) {
1551                let canonical_package = self
1552                    .dirty
1553                    .objects
1554                    .get(package_id)
1555                    .and_then(|v| match v.get_highest().map(|v| v.1.clone()) {
1556                        Some(ObjectEntry::Object(object)) => Some(object),
1557                        _ => None,
1558                    })
1559                    .or_else(|| self.store.get_object(package_id));
1560
1561                if let Some(canonical_package) = canonical_package {
1562                    assert_eq!(
1563                        canonical_package.digest(),
1564                        p.object().digest(),
1565                        "Package object cache is inconsistent for package {:?}",
1566                        package_id
1567                    );
1568                }
1569            }
1570            self.metrics.record_cache_hit("package", "package_cache");
1571            return Ok(Some(p));
1572        } else {
1573            self.metrics.record_cache_miss("package", "package_cache");
1574        }
1575
1576        // We try the dirty objects cache as well before going to the database. This is necessary
1577        // because the package could be evicted from the package cache before it is committed
1578        // to the database.
1579        if let Some(p) = self.get_object_impl("package", package_id) {
1580            if p.is_package() {
1581                let p = PackageObject::new(p);
1582                tracing::trace!(
1583                    "caching package: {:?}",
1584                    p.object().compute_object_reference()
1585                );
1586                self.metrics.record_cache_write("package");
1587                self.packages.insert(*package_id, p.clone());
1588                Ok(Some(p))
1589            } else {
1590                Err(SuiErrorKind::UserInputError {
1591                    error: UserInputError::MoveObjectAsPackage {
1592                        object_id: *package_id,
1593                    },
1594                }
1595                .into())
1596            }
1597        } else {
1598            Ok(None)
1599        }
1600    }
1601
1602    fn force_reload_system_packages(&self, _system_package_ids: &[ObjectID]) {
1603        // This is a no-op because all writes go through the cache, therefore it can never
1604        // be incoherent
1605    }
1606
1607    // get_object and variants.
1608
1609    fn get_object(&self, id: &ObjectID) -> Option<Object> {
1610        self.get_object_impl("object_latest", id)
1611    }
1612
1613    fn get_object_by_key(&self, object_id: &ObjectID, version: SequenceNumber) -> Option<Object> {
1614        match self.get_object_by_key_cache_only(object_id, version) {
1615            CacheResult::Hit(object) => Some(object),
1616            CacheResult::NegativeHit => None,
1617            CacheResult::Miss => self
1618                .record_db_get("object_by_version")
1619                .get_object_by_key(object_id, version),
1620        }
1621    }
1622
1623    fn multi_get_objects_by_key(&self, object_keys: &[ObjectKey]) -> Vec<Option<Object>> {
1624        do_fallback_lookup(
1625            object_keys,
1626            |key| match self.get_object_by_key_cache_only(&key.0, key.1) {
1627                CacheResult::Hit(maybe_object) => CacheResult::Hit(Some(maybe_object)),
1628                CacheResult::NegativeHit => CacheResult::NegativeHit,
1629                CacheResult::Miss => CacheResult::Miss,
1630            },
1631            |remaining| {
1632                self.record_db_multi_get("object_by_version", remaining.len())
1633                    .multi_get_objects_by_key(remaining)
1634                    .expect("db error")
1635            },
1636        )
1637    }
1638
1639    fn object_exists_by_key(&self, object_id: &ObjectID, version: SequenceNumber) -> bool {
1640        match self.get_object_by_key_cache_only(object_id, version) {
1641            CacheResult::Hit(_) => true,
1642            CacheResult::NegativeHit => false,
1643            CacheResult::Miss => self
1644                .record_db_get("object_by_version")
1645                .object_exists_by_key(object_id, version)
1646                .expect("db error"),
1647        }
1648    }
1649
1650    fn multi_object_exists_by_key(&self, object_keys: &[ObjectKey]) -> Vec<bool> {
1651        do_fallback_lookup(
1652            object_keys,
1653            |key| match self.get_object_by_key_cache_only(&key.0, key.1) {
1654                CacheResult::Hit(_) => CacheResult::Hit(true),
1655                CacheResult::NegativeHit => CacheResult::Hit(false),
1656                CacheResult::Miss => CacheResult::Miss,
1657            },
1658            |remaining| {
1659                self.record_db_multi_get("object_by_version", remaining.len())
1660                    .multi_object_exists_by_key(remaining)
1661                    .expect("db error")
1662            },
1663        )
1664    }
1665
1666    fn get_latest_object_ref_or_tombstone(&self, object_id: ObjectID) -> Option<ObjectRef> {
1667        match self.get_object_entry_by_id_cache_only("latest_objref_or_tombstone", &object_id) {
1668            CacheResult::Hit((version, entry)) => Some(match entry {
1669                ObjectEntry::Object(object) => object.compute_object_reference(),
1670                ObjectEntry::Deleted => (object_id, version, ObjectDigest::OBJECT_DIGEST_DELETED),
1671                ObjectEntry::Wrapped => (object_id, version, ObjectDigest::OBJECT_DIGEST_WRAPPED),
1672            }),
1673            CacheResult::NegativeHit => None,
1674            CacheResult::Miss => self
1675                .record_db_get("latest_objref_or_tombstone")
1676                .get_latest_object_ref_or_tombstone(object_id)
1677                .expect("db error"),
1678        }
1679    }
1680
1681    fn get_latest_object_or_tombstone(
1682        &self,
1683        object_id: ObjectID,
1684    ) -> Option<(ObjectKey, ObjectOrTombstone)> {
1685        match self.get_object_entry_by_id_cache_only("latest_object_or_tombstone", &object_id) {
1686            CacheResult::Hit((version, entry)) => {
1687                let key = ObjectKey(object_id, version);
1688                Some(match entry {
1689                    ObjectEntry::Object(object) => (key, object.into()),
1690                    ObjectEntry::Deleted => (
1691                        key,
1692                        ObjectOrTombstone::Tombstone((
1693                            object_id,
1694                            version,
1695                            ObjectDigest::OBJECT_DIGEST_DELETED,
1696                        )),
1697                    ),
1698                    ObjectEntry::Wrapped => (
1699                        key,
1700                        ObjectOrTombstone::Tombstone((
1701                            object_id,
1702                            version,
1703                            ObjectDigest::OBJECT_DIGEST_WRAPPED,
1704                        )),
1705                    ),
1706                })
1707            }
1708            CacheResult::NegativeHit => None,
1709            CacheResult::Miss => self
1710                .record_db_get("latest_object_or_tombstone")
1711                .get_latest_object_or_tombstone(object_id)
1712                .expect("db error"),
1713        }
1714    }
1715
1716    fn multi_input_objects_available_cache_only(&self, keys: &[InputKey]) -> Vec<bool> {
1717        keys.iter()
1718            .map(|key| {
1719                if key.is_cancelled() {
1720                    true
1721                } else {
1722                    match key {
1723                        InputKey::VersionedObject { id, version } => {
1724                            matches!(
1725                                self.get_object_by_key_cache_only(&id.id(), *version),
1726                                CacheResult::Hit(_)
1727                            )
1728                        }
1729                        InputKey::Package { id } => self.packages.contains_key(id),
1730                    }
1731                }
1732            })
1733            .collect()
1734    }
1735
1736    #[instrument(level = "trace", skip_all, fields(object_id, version_bound))]
1737    fn find_object_lt_or_eq_version(
1738        &self,
1739        object_id: ObjectID,
1740        version_bound: SequenceNumber,
1741    ) -> Option<Object> {
1742        macro_rules! check_cache_entry {
1743            ($level: expr, $objects: expr) => {
1744                self.metrics
1745                    .record_cache_request("object_lt_or_eq_version", $level);
1746                if let Some(objects) = $objects {
1747                    if let Some((_, object)) = objects
1748                        .all_versions_lt_or_eq_descending(&version_bound)
1749                        .next()
1750                    {
1751                        if let ObjectEntry::Object(object) = object {
1752                            self.metrics
1753                                .record_cache_hit("object_lt_or_eq_version", $level);
1754                            return Some(object.clone());
1755                        } else {
1756                            // if we find a tombstone, the object does not exist
1757                            self.metrics
1758                                .record_cache_negative_hit("object_lt_or_eq_version", $level);
1759                            return None;
1760                        }
1761                    } else {
1762                        self.metrics
1763                            .record_cache_miss("object_lt_or_eq_version", $level);
1764                    }
1765                }
1766            };
1767        }
1768
1769        // if we have the latest version cached, and it is within the bound, we are done
1770        self.metrics
1771            .record_cache_request("object_lt_or_eq_version", "object_by_id");
1772        let latest_cache_entry = self.object_by_id_cache.get(&object_id);
1773        if let Some(latest) = &latest_cache_entry {
1774            let latest = latest.lock();
1775            match &*latest {
1776                LatestObjectCacheEntry::Object(latest_version, object) => {
1777                    if *latest_version <= version_bound {
1778                        if let ObjectEntry::Object(object) = object {
1779                            self.metrics
1780                                .record_cache_hit("object_lt_or_eq_version", "object_by_id");
1781                            return Some(object.clone());
1782                        } else {
1783                            // object is a tombstone, but is still within the version bound
1784                            self.metrics.record_cache_negative_hit(
1785                                "object_lt_or_eq_version",
1786                                "object_by_id",
1787                            );
1788                            return None;
1789                        }
1790                    }
1791                    // latest object is not within the version bound. fall through.
1792                }
1793                // No object by this ID exists at all
1794                LatestObjectCacheEntry::NonExistent => {
1795                    self.metrics
1796                        .record_cache_negative_hit("object_lt_or_eq_version", "object_by_id");
1797                    return None;
1798                }
1799            }
1800        }
1801        self.metrics
1802            .record_cache_miss("object_lt_or_eq_version", "object_by_id");
1803
1804        Self::with_locked_cache_entries(
1805            &self.dirty.objects,
1806            &self.cached.object_cache,
1807            &object_id,
1808            |dirty_entry, cached_entry| {
1809                check_cache_entry!("committed", dirty_entry);
1810                check_cache_entry!("uncommitted", cached_entry);
1811
1812                // Much of the time, the query will be for the very latest object version, so
1813                // try that first. But we have to be careful:
1814                // 1. We must load the tombstone if it is present, because its version may exceed
1815                //    the version_bound, in which case we must do a scan.
1816                // 2. You might think we could just call `self.store.get_latest_object_or_tombstone` here.
1817                //    But we cannot, because there may be a more recent version in the dirty set, which
1818                //    we skipped over in check_cache_entry! because of the version bound. However, if we
1819                //    skipped it above, we will skip it here as well, again due to the version bound.
1820                // 3. Despite that, we really want to warm the cache here. Why? Because if the object is
1821                //    cold (not being written to), then we will very soon be able to start serving reads
1822                //    of it from the object_by_id cache, IF we can warm the cache. If we don't warm the
1823                //    the cache here, and no writes to the object occur, then we will always have to go
1824                //    to the db for the object.
1825                //
1826                // Lastly, it is important to understand the rationale for all this: If the object is
1827                // write-hot, we will serve almost all reads to it from the dirty set (or possibly the
1828                // cached set if it is only written to once every few checkpoints). If the object is
1829                // write-cold (or non-existent) and read-hot, then we will serve almost all reads to it
1830                // from the object_by_id cache check above.  Most of the apparently wasteful code here
1831                // exists only to ensure correctness in all the edge cases.
1832                let latest: Option<(SequenceNumber, ObjectEntry)> =
1833                    if let Some(dirty_set) = dirty_entry {
1834                        dirty_set
1835                            .get_highest()
1836                            .cloned()
1837                            .tap_none(|| panic!("dirty set cannot be empty"))
1838                    } else {
1839                        // TODO: we should try not to read from the db while holding the locks.
1840                        self.record_db_get("object_lt_or_eq_version_latest")
1841                            .get_latest_object_or_tombstone(object_id)
1842                            .expect("db error")
1843                            .map(|(ObjectKey(_, version), obj_or_tombstone)| {
1844                                (version, ObjectEntry::from(obj_or_tombstone))
1845                            })
1846                    };
1847
1848                if let Some((obj_version, obj_entry)) = latest {
1849                    // we can always cache the latest object (or tombstone), even if it is not within the
1850                    // version_bound. This is done in order to warm the cache in the case where a sequence
1851                    // of transactions all read the same child object without writing to it.
1852
1853                    // Note: no need to call with_object_by_id_cache_update here, because we are holding
1854                    // the lock on the dirty cache entry, and `latest` cannot become out-of-date
1855                    // while we hold that lock.
1856                    self.cache_latest_object_by_id(
1857                        &object_id,
1858                        LatestObjectCacheEntry::Object(obj_version, obj_entry.clone()),
1859                        // We can get a ticket at the last second, because we are holding the lock
1860                        // on dirty, so there cannot be any concurrent writes.
1861                        self.object_by_id_cache.get_ticket_for_read(&object_id),
1862                    );
1863
1864                    if obj_version <= version_bound {
1865                        match obj_entry {
1866                            ObjectEntry::Object(object) => Some(object),
1867                            ObjectEntry::Deleted | ObjectEntry::Wrapped => None,
1868                        }
1869                    } else {
1870                        // The latest object exceeded the bound, so now we have to do a scan
1871                        // But we already know there is no dirty entry within the bound,
1872                        // so we go to the db.
1873                        self.record_db_get("object_lt_or_eq_version_scan")
1874                            .find_object_lt_or_eq_version(object_id, version_bound)
1875                            .expect("db error")
1876                    }
1877
1878                // no object found in dirty set or db, object does not exist
1879                // When this is called from a read api (i.e. not the execution path) it is
1880                // possible that the object has been deleted and pruned. In this case,
1881                // there would be no entry at all on disk, but we may have a tombstone in the
1882                // cache
1883                } else if let Some(latest_cache_entry) = latest_cache_entry {
1884                    // If there is a latest cache entry, it had better not be a live object!
1885                    assert!(!latest_cache_entry.lock().is_alive());
1886                    None
1887                } else {
1888                    // If there is no latest cache entry, we can insert one.
1889                    let highest = cached_entry.and_then(|c| c.get_highest());
1890                    assert!(highest.is_none() || highest.unwrap().1.is_tombstone());
1891                    self.cache_object_not_found(
1892                        &object_id,
1893                        // okay to get ticket at last second - see above
1894                        self.object_by_id_cache.get_ticket_for_read(&object_id),
1895                    );
1896                    None
1897                }
1898            },
1899        )
1900    }
1901
1902    fn get_sui_system_state_object_unsafe(&self) -> SuiResult<SuiSystemState> {
1903        get_sui_system_state(self)
1904    }
1905
1906    fn get_bridge_object_unsafe(&self) -> SuiResult<Bridge> {
1907        get_bridge(self)
1908    }
1909
1910    fn get_marker_value(
1911        &self,
1912        object_key: FullObjectKey,
1913        epoch_id: EpochId,
1914    ) -> Option<MarkerValue> {
1915        match self.get_marker_value_cache_only(object_key, epoch_id) {
1916            CacheResult::Hit(marker) => Some(marker),
1917            CacheResult::NegativeHit => None,
1918            CacheResult::Miss => self
1919                .record_db_get("marker_by_version")
1920                .get_marker_value(object_key, epoch_id)
1921                .expect("db error"),
1922        }
1923    }
1924
1925    fn get_latest_marker(
1926        &self,
1927        object_id: FullObjectID,
1928        epoch_id: EpochId,
1929    ) -> Option<(SequenceNumber, MarkerValue)> {
1930        match self.get_latest_marker_value_cache_only(object_id, epoch_id) {
1931            CacheResult::Hit((v, marker)) => Some((v, marker)),
1932            CacheResult::NegativeHit => {
1933                panic!("cannot have negative hit when getting latest marker")
1934            }
1935            CacheResult::Miss => self
1936                .record_db_get("marker_latest")
1937                .get_latest_marker(object_id, epoch_id)
1938                .expect("db error"),
1939        }
1940    }
1941
1942    #[cfg(test)]
1943    fn get_lock(&self, obj_ref: ObjectRef, epoch_store: &AuthorityPerEpochStore) -> SuiLockResult {
1944        let cur_epoch = epoch_store.epoch();
1945        let Some(obj) = self.get_object_impl("lock", &obj_ref.0) else {
1946            return Err(SuiError::from(UserInputError::ObjectNotFound {
1947                object_id: obj_ref.0,
1948                // even though we know the requested version, we leave it as None to indicate
1949                // that the object does not exist at any version
1950                version: None,
1951            }));
1952        };
1953        let actual_objref = obj.compute_object_reference();
1954        if obj_ref != actual_objref {
1955            Ok(ObjectLockStatus::LockedAtDifferentVersion {
1956                locked_ref: actual_objref,
1957            })
1958        } else {
1959            // requested object ref is live, check if there is a lock
1960            Ok(
1961                match self
1962                    .object_locks
1963                    .get_transaction_lock(&obj_ref, epoch_store)?
1964                {
1965                    Some(tx_digest) => ObjectLockStatus::LockedToTx {
1966                        locked_by_tx: LockDetailsDeprecated {
1967                            epoch: cur_epoch,
1968                            tx_digest,
1969                        },
1970                    },
1971                    None => ObjectLockStatus::Initialized,
1972                },
1973            )
1974        }
1975    }
1976
1977    fn _get_live_objref(&self, object_id: ObjectID) -> SuiResult<ObjectRef> {
1978        let obj = self.get_object_impl("live_objref", &object_id).ok_or(
1979            UserInputError::ObjectNotFound {
1980                object_id,
1981                version: None,
1982            },
1983        )?;
1984        Ok(obj.compute_object_reference())
1985    }
1986
1987    fn get_highest_pruned_checkpoint(&self) -> Option<CheckpointSequenceNumber> {
1988        self.store
1989            .perpetual_tables
1990            .get_highest_pruned_checkpoint()
1991            .expect("db error")
1992    }
1993
1994    fn notify_read_input_objects<'a>(
1995        &'a self,
1996        input_and_receiving_keys: &'a [InputKey],
1997        receiving_keys: &'a HashSet<InputKey>,
1998        epoch: EpochId,
1999    ) -> BoxFuture<'a, ()> {
2000        self.object_notify_read
2001            .read(
2002                "notify_read_input_objects",
2003                input_and_receiving_keys,
2004                move |keys| {
2005                    self.multi_input_objects_available(keys, receiving_keys, epoch)
2006                        .into_iter()
2007                        .map(|available| if available { Some(()) } else { None })
2008                        .collect::<Vec<_>>()
2009                },
2010            )
2011            .map(|_| ())
2012            .boxed()
2013    }
2014}
2015
2016impl TransactionCacheRead for WritebackCache {
2017    fn multi_get_transaction_blocks(
2018        &self,
2019        digests: &[TransactionDigest],
2020    ) -> Vec<Option<Arc<VerifiedTransaction>>> {
2021        let digests_and_tickets: Vec<_> = digests
2022            .iter()
2023            .map(|d| (*d, self.cached.transactions.get_ticket_for_read(d)))
2024            .collect();
2025        do_fallback_lookup(
2026            &digests_and_tickets,
2027            |(digest, _)| {
2028                self.metrics
2029                    .record_cache_request("transaction_block", "uncommitted");
2030                if let Some(tx) = self.dirty.pending_transaction_writes.get(digest) {
2031                    self.metrics
2032                        .record_cache_hit("transaction_block", "uncommitted");
2033                    return CacheResult::Hit(Some(tx.transaction.clone()));
2034                }
2035                self.metrics
2036                    .record_cache_miss("transaction_block", "uncommitted");
2037
2038                self.metrics
2039                    .record_cache_request("transaction_block", "committed");
2040
2041                match self
2042                    .cached
2043                    .transactions
2044                    .get(digest)
2045                    .map(|l| l.lock().clone())
2046                {
2047                    Some(PointCacheItem::Some(tx)) => {
2048                        self.metrics
2049                            .record_cache_hit("transaction_block", "committed");
2050                        CacheResult::Hit(Some(tx))
2051                    }
2052                    Some(PointCacheItem::None) => CacheResult::NegativeHit,
2053                    None => {
2054                        self.metrics
2055                            .record_cache_miss("transaction_block", "committed");
2056
2057                        CacheResult::Miss
2058                    }
2059                }
2060            },
2061            |remaining| {
2062                let remaining_digests: Vec<_> = remaining.iter().map(|(d, _)| *d).collect();
2063                let results: Vec<_> = self
2064                    .record_db_multi_get("transaction_block", remaining.len())
2065                    .multi_get_transaction_blocks(&remaining_digests)
2066                    .expect("db error")
2067                    .into_iter()
2068                    .map(|o| o.map(Arc::new))
2069                    .collect();
2070                for ((digest, ticket), result) in remaining.iter().zip_debug_eq(results.iter()) {
2071                    if result.is_none() {
2072                        self.cached.transactions.insert(digest, None, *ticket).ok();
2073                    }
2074                }
2075                results
2076            },
2077        )
2078    }
2079
2080    fn multi_get_executed_effects_digests(
2081        &self,
2082        digests: &[TransactionDigest],
2083    ) -> Vec<Option<TransactionEffectsDigest>> {
2084        let digests_and_tickets: Vec<_> = digests
2085            .iter()
2086            .map(|d| {
2087                (
2088                    *d,
2089                    self.cached.executed_effects_digests.get_ticket_for_read(d),
2090                )
2091            })
2092            .collect();
2093        do_fallback_lookup(
2094            &digests_and_tickets,
2095            |(digest, _)| {
2096                self.metrics
2097                    .record_cache_request("executed_effects_digests", "uncommitted");
2098                if let Some(digest) = self.dirty.executed_effects_digests.get(digest) {
2099                    self.metrics
2100                        .record_cache_hit("executed_effects_digests", "uncommitted");
2101                    return CacheResult::Hit(Some(*digest));
2102                }
2103                self.metrics
2104                    .record_cache_miss("executed_effects_digests", "uncommitted");
2105
2106                self.metrics
2107                    .record_cache_request("executed_effects_digests", "committed");
2108                match self
2109                    .cached
2110                    .executed_effects_digests
2111                    .get(digest)
2112                    .map(|l| *l.lock())
2113                {
2114                    Some(PointCacheItem::Some(digest)) => {
2115                        self.metrics
2116                            .record_cache_hit("executed_effects_digests", "committed");
2117                        CacheResult::Hit(Some(digest))
2118                    }
2119                    Some(PointCacheItem::None) => CacheResult::NegativeHit,
2120                    None => {
2121                        self.metrics
2122                            .record_cache_miss("executed_effects_digests", "committed");
2123                        CacheResult::Miss
2124                    }
2125                }
2126            },
2127            |remaining| {
2128                let remaining_digests: Vec<_> = remaining.iter().map(|(d, _)| *d).collect();
2129                let results = self
2130                    .record_db_multi_get("executed_effects_digests", remaining.len())
2131                    .multi_get_executed_effects_digests(&remaining_digests)
2132                    .expect("db error");
2133                for ((digest, ticket), result) in remaining.iter().zip_debug_eq(results.iter()) {
2134                    if result.is_none() {
2135                        self.cached
2136                            .executed_effects_digests
2137                            .insert(digest, None, *ticket)
2138                            .ok();
2139                    }
2140                }
2141                results
2142            },
2143        )
2144    }
2145
2146    fn multi_get_effects(
2147        &self,
2148        digests: &[TransactionEffectsDigest],
2149    ) -> Vec<Option<TransactionEffects>> {
2150        let digests_and_tickets: Vec<_> = digests
2151            .iter()
2152            .map(|d| (*d, self.cached.transaction_effects.get_ticket_for_read(d)))
2153            .collect();
2154        do_fallback_lookup(
2155            &digests_and_tickets,
2156            |(digest, _)| {
2157                self.metrics
2158                    .record_cache_request("transaction_effects", "uncommitted");
2159                if let Some(effects) = self.dirty.transaction_effects.get(digest) {
2160                    self.metrics
2161                        .record_cache_hit("transaction_effects", "uncommitted");
2162                    return CacheResult::Hit(Some(effects.clone()));
2163                }
2164                self.metrics
2165                    .record_cache_miss("transaction_effects", "uncommitted");
2166
2167                self.metrics
2168                    .record_cache_request("transaction_effects", "committed");
2169                match self
2170                    .cached
2171                    .transaction_effects
2172                    .get(digest)
2173                    .map(|l| l.lock().clone())
2174                {
2175                    Some(PointCacheItem::Some(effects)) => {
2176                        self.metrics
2177                            .record_cache_hit("transaction_effects", "committed");
2178                        CacheResult::Hit(Some((*effects).clone()))
2179                    }
2180                    Some(PointCacheItem::None) => CacheResult::NegativeHit,
2181                    None => {
2182                        self.metrics
2183                            .record_cache_miss("transaction_effects", "committed");
2184                        CacheResult::Miss
2185                    }
2186                }
2187            },
2188            |remaining| {
2189                let remaining_digests: Vec<_> = remaining.iter().map(|(d, _)| *d).collect();
2190                let results = self
2191                    .record_db_multi_get("transaction_effects", remaining.len())
2192                    .multi_get_effects(remaining_digests.iter())
2193                    .expect("db error");
2194                for ((digest, ticket), result) in remaining.iter().zip_debug_eq(results.iter()) {
2195                    if result.is_none() {
2196                        self.cached
2197                            .transaction_effects
2198                            .insert(digest, None, *ticket)
2199                            .ok();
2200                    }
2201                }
2202                results
2203            },
2204        )
2205    }
2206
2207    fn transaction_executed_in_last_epoch(
2208        &self,
2209        digest: &TransactionDigest,
2210        current_epoch: EpochId,
2211    ) -> bool {
2212        if current_epoch == 0 {
2213            return false;
2214        }
2215        let last_epoch = current_epoch - 1;
2216        let cache_key = (last_epoch, *digest);
2217
2218        let ticket = self
2219            .cached
2220            .transaction_executed_in_last_epoch
2221            .get_ticket_for_read(&cache_key);
2222
2223        if let Some(cached) = self
2224            .cached
2225            .transaction_executed_in_last_epoch
2226            .get(&cache_key)
2227        {
2228            return cached.lock().is_some();
2229        }
2230
2231        let was_executed = self
2232            .store
2233            .perpetual_tables
2234            .was_transaction_executed_in_last_epoch(digest, current_epoch);
2235
2236        let value = if was_executed { Some(()) } else { None };
2237        self.cached
2238            .transaction_executed_in_last_epoch
2239            .insert(&cache_key, value, ticket)
2240            .ok();
2241
2242        was_executed
2243    }
2244
2245    fn notify_read_executed_effects_digests<'a>(
2246        &'a self,
2247        task_name: &'static str,
2248        digests: &'a [TransactionDigest],
2249    ) -> BoxFuture<'a, Vec<TransactionEffectsDigest>> {
2250        self.executed_effects_digests_notify_read
2251            .read(task_name, digests, |digests| {
2252                self.multi_get_executed_effects_digests(digests)
2253            })
2254            .boxed()
2255    }
2256
2257    fn multi_get_events(
2258        &self,
2259        event_digests: &[TransactionDigest],
2260    ) -> Vec<Option<TransactionEvents>> {
2261        fn map_events(events: TransactionEvents) -> Option<TransactionEvents> {
2262            if events.data.is_empty() {
2263                None
2264            } else {
2265                Some(events)
2266            }
2267        }
2268
2269        let digests_and_tickets: Vec<_> = event_digests
2270            .iter()
2271            .map(|d| (*d, self.cached.transaction_events.get_ticket_for_read(d)))
2272            .collect();
2273        do_fallback_lookup(
2274            &digests_and_tickets,
2275            |(digest, _)| {
2276                self.metrics
2277                    .record_cache_request("transaction_events", "uncommitted");
2278                if let Some(events) = self.dirty.transaction_events.get(digest).map(|e| e.clone()) {
2279                    self.metrics
2280                        .record_cache_hit("transaction_events", "uncommitted");
2281
2282                    return CacheResult::Hit(map_events(events));
2283                }
2284                self.metrics
2285                    .record_cache_miss("transaction_events", "uncommitted");
2286
2287                self.metrics
2288                    .record_cache_request("transaction_events", "committed");
2289                match self
2290                    .cached
2291                    .transaction_events
2292                    .get(digest)
2293                    .map(|l| l.lock().clone())
2294                {
2295                    Some(PointCacheItem::Some(events)) => {
2296                        self.metrics
2297                            .record_cache_hit("transaction_events", "committed");
2298                        CacheResult::Hit(map_events((*events).clone()))
2299                    }
2300                    Some(PointCacheItem::None) => CacheResult::NegativeHit,
2301                    None => {
2302                        self.metrics
2303                            .record_cache_miss("transaction_events", "committed");
2304
2305                        CacheResult::Miss
2306                    }
2307                }
2308            },
2309            |remaining| {
2310                let remaining_digests: Vec<_> = remaining.iter().map(|(d, _)| *d).collect();
2311                let results = self
2312                    .store
2313                    .multi_get_events(&remaining_digests)
2314                    .expect("db error");
2315                for ((digest, ticket), result) in remaining.iter().zip_debug_eq(results.iter()) {
2316                    if result.is_none() {
2317                        self.cached
2318                            .transaction_events
2319                            .insert(digest, None, *ticket)
2320                            .ok();
2321                    }
2322                }
2323                results
2324            },
2325        )
2326    }
2327
2328    fn get_unchanged_loaded_runtime_objects(
2329        &self,
2330        digest: &TransactionDigest,
2331    ) -> Option<Vec<ObjectKey>> {
2332        self.dirty
2333            .unchanged_loaded_runtime_objects
2334            .get(digest)
2335            .map(|b| b.clone())
2336            .or_else(|| {
2337                self.store
2338                    .get_unchanged_loaded_runtime_objects(digest)
2339                    .expect("db error")
2340            })
2341    }
2342
2343    fn multi_get_unchanged_loaded_runtime_objects(
2344        &self,
2345        digests: &[TransactionDigest],
2346    ) -> Vec<Option<Vec<ObjectKey>>> {
2347        do_fallback_lookup(
2348            digests,
2349            |digest| match self.dirty.unchanged_loaded_runtime_objects.get(digest) {
2350                Some(objects) => CacheResult::Hit(Some(objects.clone())),
2351                None => CacheResult::Miss,
2352            },
2353            |digests| {
2354                self.store
2355                    .multi_get_unchanged_loaded_runtime_objects(digests)
2356                    .expect("db error")
2357            },
2358        )
2359    }
2360
2361    fn take_accumulator_events(&self, digest: &TransactionDigest) -> Option<Vec<AccumulatorEvent>> {
2362        self.dirty
2363            .pending_transaction_writes
2364            .get(digest)
2365            .map(|transaction_output| transaction_output.take_accumulator_events())
2366    }
2367}
2368
2369impl ExecutionCacheWrite for WritebackCache {
2370    fn validate_owned_object_versions(&self, owned_input_objects: &[ObjectRef]) -> SuiResult {
2371        ObjectLocks::validate_owned_object_versions(self, owned_input_objects)
2372    }
2373
2374    fn write_transaction_outputs(&self, epoch_id: EpochId, tx_outputs: Arc<TransactionOutputs>) {
2375        WritebackCache::write_transaction_outputs(self, epoch_id, tx_outputs);
2376    }
2377
2378    #[cfg(test)]
2379    fn write_object_entry_for_test(&self, object: Object) {
2380        self.write_object_entry(&object.id(), object.version(), object.into());
2381    }
2382}
2383
2384implement_passthrough_traits!(WritebackCache);
2385
2386impl GlobalStateHashStore for WritebackCache {
2387    fn get_object_ref_prior_to_key_deprecated(
2388        &self,
2389        object_id: &ObjectID,
2390        version: SequenceNumber,
2391    ) -> SuiResult<Option<ObjectRef>> {
2392        // There is probably a more efficient way to implement this, but since this is only used by
2393        // old protocol versions, it is better to do the simple thing that is obviously correct.
2394        // In this case we previous version from all sources and choose the highest
2395        let mut candidates = Vec::new();
2396
2397        let check_versions =
2398            |versions: &CachedVersionMap<ObjectEntry>| match versions.get_prior_to(&version) {
2399                Some((version, object_entry)) => match object_entry {
2400                    ObjectEntry::Object(object) => {
2401                        assert_eq!(object.version(), version);
2402                        Some(object.compute_object_reference())
2403                    }
2404                    ObjectEntry::Deleted => {
2405                        Some((*object_id, version, ObjectDigest::OBJECT_DIGEST_DELETED))
2406                    }
2407                    ObjectEntry::Wrapped => {
2408                        Some((*object_id, version, ObjectDigest::OBJECT_DIGEST_WRAPPED))
2409                    }
2410                },
2411                None => None,
2412            };
2413
2414        // first check dirty data
2415        if let Some(objects) = self.dirty.objects.get(object_id)
2416            && let Some(prior) = check_versions(&objects)
2417        {
2418            candidates.push(prior);
2419        }
2420
2421        if let Some(objects) = self.cached.object_cache.get(object_id)
2422            && let Some(prior) = check_versions(&objects.lock())
2423        {
2424            candidates.push(prior);
2425        }
2426
2427        if let Some(prior) = self
2428            .store
2429            .get_object_ref_prior_to_key_deprecated(object_id, version)?
2430        {
2431            candidates.push(prior);
2432        }
2433
2434        // sort candidates by version, and return the highest
2435        candidates.sort_by_key(|(_, version, _)| *version);
2436        Ok(candidates.pop())
2437    }
2438
2439    fn get_root_state_hash_for_epoch(
2440        &self,
2441        epoch: EpochId,
2442    ) -> SuiResult<Option<(CheckpointSequenceNumber, GlobalStateHash)>> {
2443        self.store.get_root_state_hash_for_epoch(epoch)
2444    }
2445
2446    fn get_root_state_hash_for_highest_epoch(
2447        &self,
2448    ) -> SuiResult<Option<(EpochId, (CheckpointSequenceNumber, GlobalStateHash))>> {
2449        self.store.get_root_state_hash_for_highest_epoch()
2450    }
2451
2452    fn insert_state_hash_for_epoch(
2453        &self,
2454        epoch: EpochId,
2455        checkpoint_seq_num: &CheckpointSequenceNumber,
2456        acc: &GlobalStateHash,
2457    ) -> SuiResult {
2458        self.store
2459            .insert_state_hash_for_epoch(epoch, checkpoint_seq_num, acc)
2460    }
2461
2462    fn iter_live_object_set(
2463        &self,
2464        include_wrapped_tombstone: bool,
2465    ) -> Box<dyn Iterator<Item = LiveObject> + '_> {
2466        // The only time it is safe to iterate the live object set is at an epoch boundary,
2467        // at which point the db is consistent and the dirty cache is empty. So this does
2468        // read the cache
2469        assert!(
2470            self.dirty.is_empty(),
2471            "cannot iterate live object set with dirty data"
2472        );
2473        self.store.iter_live_object_set(include_wrapped_tombstone)
2474    }
2475
2476    // A version of iter_live_object_set that reads the cache. Only use for testing. If used
2477    // on a live validator, can cause the server to block for as long as it takes to iterate
2478    // the entire live object set.
2479    fn iter_cached_live_object_set_for_testing(
2480        &self,
2481        include_wrapped_tombstone: bool,
2482    ) -> Box<dyn Iterator<Item = LiveObject> + '_> {
2483        // hold iter until we are finished to prevent any concurrent inserts/deletes
2484        let iter = self.dirty.objects.iter();
2485        let mut dirty_objects = BTreeMap::new();
2486
2487        // add everything from the store
2488        for obj in self.store.iter_live_object_set(include_wrapped_tombstone) {
2489            dirty_objects.insert(obj.object_id(), obj);
2490        }
2491
2492        // add everything from the cache, but also remove deletions
2493        for entry in iter {
2494            let id = *entry.key();
2495            let value = entry.value();
2496            match value.get_highest().unwrap() {
2497                (_, ObjectEntry::Object(object)) => {
2498                    dirty_objects.insert(id, LiveObject::Normal(object.clone()));
2499                }
2500                (version, ObjectEntry::Wrapped) => {
2501                    if include_wrapped_tombstone {
2502                        dirty_objects.insert(id, LiveObject::Wrapped(ObjectKey(id, *version)));
2503                    } else {
2504                        dirty_objects.remove(&id);
2505                    }
2506                }
2507                (_, ObjectEntry::Deleted) => {
2508                    dirty_objects.remove(&id);
2509                }
2510            }
2511        }
2512
2513        Box::new(dirty_objects.into_values())
2514    }
2515}
2516
2517// TODO: For correctness, we must at least invalidate the cache when items are written through this
2518// trait (since they could be negatively cached as absent). But it may or may not be optimal to
2519// actually insert them into the cache. For instance if state sync is running ahead of execution,
2520// they might evict other items that are about to be read. This could be an area for tuning in the
2521// future.
2522impl StateSyncAPI for WritebackCache {
2523    fn insert_transaction_and_effects(
2524        &self,
2525        transaction: &VerifiedTransaction,
2526        transaction_effects: &TransactionEffects,
2527    ) {
2528        self.store
2529            .insert_transaction_and_effects(transaction, transaction_effects)
2530            .expect("db error");
2531        self.cached
2532            .transactions
2533            .insert(
2534                transaction.digest(),
2535                PointCacheItem::Some(Arc::new(transaction.clone())),
2536                Ticket::Write,
2537            )
2538            .ok();
2539        self.cached
2540            .transaction_effects
2541            .insert(
2542                &transaction_effects.digest(),
2543                PointCacheItem::Some(Arc::new(transaction_effects.clone())),
2544                Ticket::Write,
2545            )
2546            .ok();
2547    }
2548
2549    fn multi_insert_transaction_and_effects(
2550        &self,
2551        transactions_and_effects: &[VerifiedExecutionData],
2552    ) {
2553        self.store
2554            .multi_insert_transaction_and_effects(transactions_and_effects.iter())
2555            .expect("db error");
2556        for VerifiedExecutionData {
2557            transaction,
2558            effects,
2559        } in transactions_and_effects
2560        {
2561            self.cached
2562                .transactions
2563                .insert(
2564                    transaction.digest(),
2565                    PointCacheItem::Some(Arc::new(transaction.clone())),
2566                    Ticket::Write,
2567                )
2568                .ok();
2569            self.cached
2570                .transaction_effects
2571                .insert(
2572                    &effects.digest(),
2573                    PointCacheItem::Some(Arc::new(effects.clone())),
2574                    Ticket::Write,
2575                )
2576                .ok();
2577        }
2578    }
2579}