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::debug_fatal;
59use mysten_common::random_util::randomize_cache_capacity_in_tests;
60use mysten_common::sync::notify_read::NotifyRead;
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 starting_root_version = pre_root_version;
1471        let mut loop_iter = 0;
1472        loop {
1473            loop_iter += 1;
1474            // Safe because of (1) and (2) above: the stability check below bounds the
1475            // lifetime of `pre_root_version` to a window in which no pruning happens.
1476            let value = self.get_account_amount_at_version(account_id, pre_root_version);
1477            let post_root_version =
1478                ObjectCacheRead::get_object(self, &SUI_ACCUMULATOR_ROOT_OBJECT_ID)
1479                    .unwrap()
1480                    .version();
1481            if pre_root_version == post_root_version {
1482                if loop_iter > 10 {
1483                    debug!(
1484                        iterations = loop_iter,
1485                        starting_root_version = %starting_root_version,
1486                        ending_root_version = %post_root_version,
1487                        "Root version stabilized after multiple iterations during MVCC read"
1488                    );
1489                }
1490                return (value, pre_root_version);
1491            }
1492            debug!(
1493                "Root version changed from {} to {} during MVCC read, retrying",
1494                pre_root_version, post_root_version
1495            );
1496            pre_root_version = post_root_version;
1497        }
1498    }
1499
1500    fn get_account_amount_at_version(
1501        &self,
1502        account_id: &AccumulatorObjId,
1503        version: SequenceNumber,
1504    ) -> u128 {
1505        let account_obj = self.find_object_lt_or_eq_version(*account_id.inner(), version);
1506        account_obj
1507            .map(|account_obj| account_amount_from_object(&account_obj))
1508            .unwrap_or(0)
1509    }
1510}
1511
1512impl ExecutionCacheAPI for WritebackCache {}
1513
1514impl ExecutionCacheCommit for WritebackCache {
1515    fn build_db_batch(&self, epoch: EpochId, digests: &[TransactionDigest]) -> Batch {
1516        self.build_db_batch(epoch, digests)
1517    }
1518
1519    fn set_highest_committed_checkpoint_in_batch(
1520        &self,
1521        batch: &mut Batch,
1522        checkpoint: CheckpointSequenceNumber,
1523    ) {
1524        self.store
1525            .perpetual_tables
1526            .set_highest_committed_checkpoint(&mut batch.1, checkpoint)
1527            .expect("db error");
1528    }
1529
1530    fn commit_transaction_outputs(
1531        &self,
1532        epoch: EpochId,
1533        batch: Batch,
1534        digests: &[TransactionDigest],
1535    ) {
1536        WritebackCache::commit_transaction_outputs(self, epoch, batch, digests)
1537    }
1538
1539    fn persist_transaction(&self, tx: &VerifiedExecutableTransaction) {
1540        self.store.persist_transaction(tx).expect("db error");
1541    }
1542
1543    fn approximate_pending_transaction_count(&self) -> u64 {
1544        WritebackCache::approximate_pending_transaction_count(self)
1545    }
1546}
1547
1548impl ObjectCacheRead for WritebackCache {
1549    fn get_package_object(&self, package_id: &ObjectID) -> SuiResult<Option<PackageObject>> {
1550        self.metrics
1551            .record_cache_request("package", "package_cache");
1552        if let Some(p) = self.packages.get(package_id) {
1553            if cfg!(debug_assertions) {
1554                let canonical_package = self
1555                    .dirty
1556                    .objects
1557                    .get(package_id)
1558                    .and_then(|v| match v.get_highest().map(|v| v.1.clone()) {
1559                        Some(ObjectEntry::Object(object)) => Some(object),
1560                        _ => None,
1561                    })
1562                    .or_else(|| self.store.get_object(package_id));
1563
1564                if let Some(canonical_package) = canonical_package {
1565                    assert_eq!(
1566                        canonical_package.digest(),
1567                        p.object().digest(),
1568                        "Package object cache is inconsistent for package {:?}",
1569                        package_id
1570                    );
1571                }
1572            }
1573            self.metrics.record_cache_hit("package", "package_cache");
1574            return Ok(Some(p));
1575        } else {
1576            self.metrics.record_cache_miss("package", "package_cache");
1577        }
1578
1579        // We try the dirty objects cache as well before going to the database. This is necessary
1580        // because the package could be evicted from the package cache before it is committed
1581        // to the database.
1582        if let Some(p) = self.get_object_impl("package", package_id) {
1583            if p.is_package() {
1584                let p = PackageObject::new(p);
1585                tracing::trace!(
1586                    "caching package: {:?}",
1587                    p.object().compute_object_reference()
1588                );
1589                self.metrics.record_cache_write("package");
1590                self.packages.insert(*package_id, p.clone());
1591                Ok(Some(p))
1592            } else {
1593                Err(SuiErrorKind::UserInputError {
1594                    error: UserInputError::MoveObjectAsPackage {
1595                        object_id: *package_id,
1596                    },
1597                }
1598                .into())
1599            }
1600        } else {
1601            Ok(None)
1602        }
1603    }
1604
1605    fn force_reload_system_packages(&self, _system_package_ids: &[ObjectID]) {
1606        // This is a no-op because all writes go through the cache, therefore it can never
1607        // be incoherent
1608    }
1609
1610    // get_object and variants.
1611
1612    fn get_object(&self, id: &ObjectID) -> Option<Object> {
1613        self.get_object_impl("object_latest", id)
1614    }
1615
1616    fn get_object_by_key(&self, object_id: &ObjectID, version: SequenceNumber) -> Option<Object> {
1617        match self.get_object_by_key_cache_only(object_id, version) {
1618            CacheResult::Hit(object) => Some(object),
1619            CacheResult::NegativeHit => None,
1620            CacheResult::Miss => self
1621                .record_db_get("object_by_version")
1622                .get_object_by_key(object_id, version),
1623        }
1624    }
1625
1626    fn multi_get_objects_by_key(&self, object_keys: &[ObjectKey]) -> Vec<Option<Object>> {
1627        do_fallback_lookup(
1628            object_keys,
1629            |key| match self.get_object_by_key_cache_only(&key.0, key.1) {
1630                CacheResult::Hit(maybe_object) => CacheResult::Hit(Some(maybe_object)),
1631                CacheResult::NegativeHit => CacheResult::NegativeHit,
1632                CacheResult::Miss => CacheResult::Miss,
1633            },
1634            |remaining| {
1635                self.record_db_multi_get("object_by_version", remaining.len())
1636                    .multi_get_objects_by_key(remaining)
1637                    .expect("db error")
1638            },
1639        )
1640    }
1641
1642    fn object_exists_by_key(&self, object_id: &ObjectID, version: SequenceNumber) -> bool {
1643        match self.get_object_by_key_cache_only(object_id, version) {
1644            CacheResult::Hit(_) => true,
1645            CacheResult::NegativeHit => false,
1646            CacheResult::Miss => self
1647                .record_db_get("object_by_version")
1648                .object_exists_by_key(object_id, version)
1649                .expect("db error"),
1650        }
1651    }
1652
1653    fn multi_object_exists_by_key(&self, object_keys: &[ObjectKey]) -> Vec<bool> {
1654        do_fallback_lookup(
1655            object_keys,
1656            |key| match self.get_object_by_key_cache_only(&key.0, key.1) {
1657                CacheResult::Hit(_) => CacheResult::Hit(true),
1658                CacheResult::NegativeHit => CacheResult::Hit(false),
1659                CacheResult::Miss => CacheResult::Miss,
1660            },
1661            |remaining| {
1662                self.record_db_multi_get("object_by_version", remaining.len())
1663                    .multi_object_exists_by_key(remaining)
1664                    .expect("db error")
1665            },
1666        )
1667    }
1668
1669    fn get_latest_object_ref_or_tombstone(&self, object_id: ObjectID) -> Option<ObjectRef> {
1670        match self.get_object_entry_by_id_cache_only("latest_objref_or_tombstone", &object_id) {
1671            CacheResult::Hit((version, entry)) => Some(match entry {
1672                ObjectEntry::Object(object) => object.compute_object_reference(),
1673                ObjectEntry::Deleted => (object_id, version, ObjectDigest::OBJECT_DIGEST_DELETED),
1674                ObjectEntry::Wrapped => (object_id, version, ObjectDigest::OBJECT_DIGEST_WRAPPED),
1675            }),
1676            CacheResult::NegativeHit => None,
1677            CacheResult::Miss => self
1678                .record_db_get("latest_objref_or_tombstone")
1679                .get_latest_object_ref_or_tombstone(object_id)
1680                .expect("db error"),
1681        }
1682    }
1683
1684    fn get_latest_object_or_tombstone(
1685        &self,
1686        object_id: ObjectID,
1687    ) -> Option<(ObjectKey, ObjectOrTombstone)> {
1688        match self.get_object_entry_by_id_cache_only("latest_object_or_tombstone", &object_id) {
1689            CacheResult::Hit((version, entry)) => {
1690                let key = ObjectKey(object_id, version);
1691                Some(match entry {
1692                    ObjectEntry::Object(object) => (key, object.into()),
1693                    ObjectEntry::Deleted => (
1694                        key,
1695                        ObjectOrTombstone::Tombstone((
1696                            object_id,
1697                            version,
1698                            ObjectDigest::OBJECT_DIGEST_DELETED,
1699                        )),
1700                    ),
1701                    ObjectEntry::Wrapped => (
1702                        key,
1703                        ObjectOrTombstone::Tombstone((
1704                            object_id,
1705                            version,
1706                            ObjectDigest::OBJECT_DIGEST_WRAPPED,
1707                        )),
1708                    ),
1709                })
1710            }
1711            CacheResult::NegativeHit => None,
1712            CacheResult::Miss => self
1713                .record_db_get("latest_object_or_tombstone")
1714                .get_latest_object_or_tombstone(object_id)
1715                .expect("db error"),
1716        }
1717    }
1718
1719    fn multi_input_objects_available_cache_only(&self, keys: &[InputKey]) -> Vec<bool> {
1720        keys.iter()
1721            .map(|key| {
1722                if key.is_cancelled() {
1723                    true
1724                } else {
1725                    match key {
1726                        InputKey::VersionedObject { id, version } => {
1727                            matches!(
1728                                self.get_object_by_key_cache_only(&id.id(), *version),
1729                                CacheResult::Hit(_)
1730                            )
1731                        }
1732                        InputKey::Package { id } => self.packages.contains_key(id),
1733                    }
1734                }
1735            })
1736            .collect()
1737    }
1738
1739    #[instrument(level = "trace", skip_all, fields(object_id, version_bound))]
1740    fn find_object_lt_or_eq_version(
1741        &self,
1742        object_id: ObjectID,
1743        version_bound: SequenceNumber,
1744    ) -> Option<Object> {
1745        macro_rules! check_cache_entry {
1746            ($level: expr, $objects: expr) => {
1747                self.metrics
1748                    .record_cache_request("object_lt_or_eq_version", $level);
1749                if let Some(objects) = $objects {
1750                    if let Some((_, object)) = objects
1751                        .all_versions_lt_or_eq_descending(&version_bound)
1752                        .next()
1753                    {
1754                        if let ObjectEntry::Object(object) = object {
1755                            self.metrics
1756                                .record_cache_hit("object_lt_or_eq_version", $level);
1757                            return Some(object.clone());
1758                        } else {
1759                            // if we find a tombstone, the object does not exist
1760                            self.metrics
1761                                .record_cache_negative_hit("object_lt_or_eq_version", $level);
1762                            return None;
1763                        }
1764                    } else {
1765                        self.metrics
1766                            .record_cache_miss("object_lt_or_eq_version", $level);
1767                    }
1768                }
1769            };
1770        }
1771
1772        // if we have the latest version cached, and it is within the bound, we are done
1773        self.metrics
1774            .record_cache_request("object_lt_or_eq_version", "object_by_id");
1775        let latest_cache_entry = self.object_by_id_cache.get(&object_id);
1776        if let Some(latest) = &latest_cache_entry {
1777            let latest = latest.lock();
1778            match &*latest {
1779                LatestObjectCacheEntry::Object(latest_version, object) => {
1780                    if *latest_version <= version_bound {
1781                        if let ObjectEntry::Object(object) = object {
1782                            self.metrics
1783                                .record_cache_hit("object_lt_or_eq_version", "object_by_id");
1784                            return Some(object.clone());
1785                        } else {
1786                            // object is a tombstone, but is still within the version bound
1787                            self.metrics.record_cache_negative_hit(
1788                                "object_lt_or_eq_version",
1789                                "object_by_id",
1790                            );
1791                            return None;
1792                        }
1793                    }
1794                    // latest object is not within the version bound. fall through.
1795                }
1796                // No object by this ID exists at all
1797                LatestObjectCacheEntry::NonExistent => {
1798                    self.metrics
1799                        .record_cache_negative_hit("object_lt_or_eq_version", "object_by_id");
1800                    return None;
1801                }
1802            }
1803        }
1804        self.metrics
1805            .record_cache_miss("object_lt_or_eq_version", "object_by_id");
1806
1807        Self::with_locked_cache_entries(
1808            &self.dirty.objects,
1809            &self.cached.object_cache,
1810            &object_id,
1811            |dirty_entry, cached_entry| {
1812                check_cache_entry!("committed", dirty_entry);
1813                check_cache_entry!("uncommitted", cached_entry);
1814
1815                // Much of the time, the query will be for the very latest object version, so
1816                // try that first. But we have to be careful:
1817                // 1. We must load the tombstone if it is present, because its version may exceed
1818                //    the version_bound, in which case we must do a scan.
1819                // 2. You might think we could just call `self.store.get_latest_object_or_tombstone` here.
1820                //    But we cannot, because there may be a more recent version in the dirty set, which
1821                //    we skipped over in check_cache_entry! because of the version bound. However, if we
1822                //    skipped it above, we will skip it here as well, again due to the version bound.
1823                // 3. Despite that, we really want to warm the cache here. Why? Because if the object is
1824                //    cold (not being written to), then we will very soon be able to start serving reads
1825                //    of it from the object_by_id cache, IF we can warm the cache. If we don't warm the
1826                //    the cache here, and no writes to the object occur, then we will always have to go
1827                //    to the db for the object.
1828                //
1829                // Lastly, it is important to understand the rationale for all this: If the object is
1830                // write-hot, we will serve almost all reads to it from the dirty set (or possibly the
1831                // cached set if it is only written to once every few checkpoints). If the object is
1832                // write-cold (or non-existent) and read-hot, then we will serve almost all reads to it
1833                // from the object_by_id cache check above.  Most of the apparently wasteful code here
1834                // exists only to ensure correctness in all the edge cases.
1835                let latest: Option<(SequenceNumber, ObjectEntry)> =
1836                    if let Some(dirty_set) = dirty_entry {
1837                        dirty_set
1838                            .get_highest()
1839                            .cloned()
1840                            .tap_none(|| panic!("dirty set cannot be empty"))
1841                    } else {
1842                        // TODO: we should try not to read from the db while holding the locks.
1843                        self.record_db_get("object_lt_or_eq_version_latest")
1844                            .get_latest_object_or_tombstone(object_id)
1845                            .expect("db error")
1846                            .map(|(ObjectKey(_, version), obj_or_tombstone)| {
1847                                (version, ObjectEntry::from(obj_or_tombstone))
1848                            })
1849                    };
1850
1851                if let Some((obj_version, obj_entry)) = latest {
1852                    // we can always cache the latest object (or tombstone), even if it is not within the
1853                    // version_bound. This is done in order to warm the cache in the case where a sequence
1854                    // of transactions all read the same child object without writing to it.
1855
1856                    // Note: no need to call with_object_by_id_cache_update here, because we are holding
1857                    // the lock on the dirty cache entry, and `latest` cannot become out-of-date
1858                    // while we hold that lock.
1859                    self.cache_latest_object_by_id(
1860                        &object_id,
1861                        LatestObjectCacheEntry::Object(obj_version, obj_entry.clone()),
1862                        // We can get a ticket at the last second, because we are holding the lock
1863                        // on dirty, so there cannot be any concurrent writes.
1864                        self.object_by_id_cache.get_ticket_for_read(&object_id),
1865                    );
1866
1867                    if obj_version <= version_bound {
1868                        match obj_entry {
1869                            ObjectEntry::Object(object) => Some(object),
1870                            ObjectEntry::Deleted | ObjectEntry::Wrapped => None,
1871                        }
1872                    } else {
1873                        // The latest object exceeded the bound, so now we have to do a scan
1874                        // But we already know there is no dirty entry within the bound,
1875                        // so we go to the db.
1876                        self.record_db_get("object_lt_or_eq_version_scan")
1877                            .find_object_lt_or_eq_version(object_id, version_bound)
1878                            .expect("db error")
1879                    }
1880
1881                // no object found in dirty set or db, object does not exist
1882                // When this is called from a read api (i.e. not the execution path) it is
1883                // possible that the object has been deleted and pruned. In this case,
1884                // there would be no entry at all on disk, but we may have a tombstone in the
1885                // cache
1886                } else if let Some(latest_cache_entry) = latest_cache_entry {
1887                    // If there is a latest cache entry, it had better not be a live object!
1888                    assert!(!latest_cache_entry.lock().is_alive());
1889                    None
1890                } else {
1891                    // If there is no latest cache entry, we can insert one.
1892                    let highest = cached_entry.and_then(|c| c.get_highest());
1893                    assert!(highest.is_none() || highest.unwrap().1.is_tombstone());
1894                    self.cache_object_not_found(
1895                        &object_id,
1896                        // okay to get ticket at last second - see above
1897                        self.object_by_id_cache.get_ticket_for_read(&object_id),
1898                    );
1899                    None
1900                }
1901            },
1902        )
1903    }
1904
1905    fn get_sui_system_state_object_unsafe(&self) -> SuiResult<SuiSystemState> {
1906        get_sui_system_state(self)
1907    }
1908
1909    fn get_bridge_object_unsafe(&self) -> SuiResult<Bridge> {
1910        get_bridge(self)
1911    }
1912
1913    fn get_marker_value(
1914        &self,
1915        object_key: FullObjectKey,
1916        epoch_id: EpochId,
1917    ) -> Option<MarkerValue> {
1918        match self.get_marker_value_cache_only(object_key, epoch_id) {
1919            CacheResult::Hit(marker) => Some(marker),
1920            CacheResult::NegativeHit => None,
1921            CacheResult::Miss => self
1922                .record_db_get("marker_by_version")
1923                .get_marker_value(object_key, epoch_id)
1924                .expect("db error"),
1925        }
1926    }
1927
1928    fn get_latest_marker(
1929        &self,
1930        object_id: FullObjectID,
1931        epoch_id: EpochId,
1932    ) -> Option<(SequenceNumber, MarkerValue)> {
1933        match self.get_latest_marker_value_cache_only(object_id, epoch_id) {
1934            CacheResult::Hit((v, marker)) => Some((v, marker)),
1935            CacheResult::NegativeHit => {
1936                panic!("cannot have negative hit when getting latest marker")
1937            }
1938            CacheResult::Miss => self
1939                .record_db_get("marker_latest")
1940                .get_latest_marker(object_id, epoch_id)
1941                .expect("db error"),
1942        }
1943    }
1944
1945    #[cfg(test)]
1946    fn get_lock(&self, obj_ref: ObjectRef, epoch_store: &AuthorityPerEpochStore) -> SuiLockResult {
1947        let cur_epoch = epoch_store.epoch();
1948        let Some(obj) = self.get_object_impl("lock", &obj_ref.0) else {
1949            return Err(SuiError::from(UserInputError::ObjectNotFound {
1950                object_id: obj_ref.0,
1951                // even though we know the requested version, we leave it as None to indicate
1952                // that the object does not exist at any version
1953                version: None,
1954            }));
1955        };
1956        let actual_objref = obj.compute_object_reference();
1957        if obj_ref != actual_objref {
1958            Ok(ObjectLockStatus::LockedAtDifferentVersion {
1959                locked_ref: actual_objref,
1960            })
1961        } else {
1962            // requested object ref is live, check if there is a lock
1963            Ok(
1964                match self
1965                    .object_locks
1966                    .get_transaction_lock(&obj_ref, epoch_store)?
1967                {
1968                    Some(tx_digest) => ObjectLockStatus::LockedToTx {
1969                        locked_by_tx: LockDetailsDeprecated {
1970                            epoch: cur_epoch,
1971                            tx_digest,
1972                        },
1973                    },
1974                    None => ObjectLockStatus::Initialized,
1975                },
1976            )
1977        }
1978    }
1979
1980    fn _get_live_objref(&self, object_id: ObjectID) -> SuiResult<ObjectRef> {
1981        let obj = self.get_object_impl("live_objref", &object_id).ok_or(
1982            UserInputError::ObjectNotFound {
1983                object_id,
1984                version: None,
1985            },
1986        )?;
1987        Ok(obj.compute_object_reference())
1988    }
1989
1990    fn get_highest_pruned_checkpoint(&self) -> Option<CheckpointSequenceNumber> {
1991        self.store
1992            .perpetual_tables
1993            .get_highest_pruned_checkpoint()
1994            .expect("db error")
1995    }
1996
1997    fn notify_read_input_objects<'a>(
1998        &'a self,
1999        input_and_receiving_keys: &'a [InputKey],
2000        receiving_keys: &'a HashSet<InputKey>,
2001        epoch: EpochId,
2002    ) -> BoxFuture<'a, ()> {
2003        self.object_notify_read
2004            .read(
2005                "notify_read_input_objects",
2006                input_and_receiving_keys,
2007                move |keys| {
2008                    self.multi_input_objects_available(keys, receiving_keys, epoch)
2009                        .into_iter()
2010                        .map(|available| if available { Some(()) } else { None })
2011                        .collect::<Vec<_>>()
2012                },
2013            )
2014            .map(|_| ())
2015            .boxed()
2016    }
2017}
2018
2019impl TransactionCacheRead for WritebackCache {
2020    fn multi_get_transaction_blocks(
2021        &self,
2022        digests: &[TransactionDigest],
2023    ) -> Vec<Option<Arc<VerifiedTransaction>>> {
2024        let digests_and_tickets: Vec<_> = digests
2025            .iter()
2026            .map(|d| (*d, self.cached.transactions.get_ticket_for_read(d)))
2027            .collect();
2028        do_fallback_lookup(
2029            &digests_and_tickets,
2030            |(digest, _)| {
2031                self.metrics
2032                    .record_cache_request("transaction_block", "uncommitted");
2033                if let Some(tx) = self.dirty.pending_transaction_writes.get(digest) {
2034                    self.metrics
2035                        .record_cache_hit("transaction_block", "uncommitted");
2036                    return CacheResult::Hit(Some(tx.transaction.clone()));
2037                }
2038                self.metrics
2039                    .record_cache_miss("transaction_block", "uncommitted");
2040
2041                self.metrics
2042                    .record_cache_request("transaction_block", "committed");
2043
2044                match self
2045                    .cached
2046                    .transactions
2047                    .get(digest)
2048                    .map(|l| l.lock().clone())
2049                {
2050                    Some(PointCacheItem::Some(tx)) => {
2051                        self.metrics
2052                            .record_cache_hit("transaction_block", "committed");
2053                        CacheResult::Hit(Some(tx))
2054                    }
2055                    Some(PointCacheItem::None) => CacheResult::NegativeHit,
2056                    None => {
2057                        self.metrics
2058                            .record_cache_miss("transaction_block", "committed");
2059
2060                        CacheResult::Miss
2061                    }
2062                }
2063            },
2064            |remaining| {
2065                let remaining_digests: Vec<_> = remaining.iter().map(|(d, _)| *d).collect();
2066                let results: Vec<_> = self
2067                    .record_db_multi_get("transaction_block", remaining.len())
2068                    .multi_get_transaction_blocks(&remaining_digests)
2069                    .expect("db error")
2070                    .into_iter()
2071                    .map(|o| o.map(Arc::new))
2072                    .collect();
2073                for ((digest, ticket), result) in remaining.iter().zip_debug_eq(results.iter()) {
2074                    if result.is_none() {
2075                        self.cached.transactions.insert(digest, None, *ticket).ok();
2076                    }
2077                }
2078                results
2079            },
2080        )
2081    }
2082
2083    fn multi_get_executed_effects_digests(
2084        &self,
2085        digests: &[TransactionDigest],
2086    ) -> Vec<Option<TransactionEffectsDigest>> {
2087        let digests_and_tickets: Vec<_> = digests
2088            .iter()
2089            .map(|d| {
2090                (
2091                    *d,
2092                    self.cached.executed_effects_digests.get_ticket_for_read(d),
2093                )
2094            })
2095            .collect();
2096        do_fallback_lookup(
2097            &digests_and_tickets,
2098            |(digest, _)| {
2099                self.metrics
2100                    .record_cache_request("executed_effects_digests", "uncommitted");
2101                if let Some(digest) = self.dirty.executed_effects_digests.get(digest) {
2102                    self.metrics
2103                        .record_cache_hit("executed_effects_digests", "uncommitted");
2104                    return CacheResult::Hit(Some(*digest));
2105                }
2106                self.metrics
2107                    .record_cache_miss("executed_effects_digests", "uncommitted");
2108
2109                self.metrics
2110                    .record_cache_request("executed_effects_digests", "committed");
2111                match self
2112                    .cached
2113                    .executed_effects_digests
2114                    .get(digest)
2115                    .map(|l| *l.lock())
2116                {
2117                    Some(PointCacheItem::Some(digest)) => {
2118                        self.metrics
2119                            .record_cache_hit("executed_effects_digests", "committed");
2120                        CacheResult::Hit(Some(digest))
2121                    }
2122                    Some(PointCacheItem::None) => CacheResult::NegativeHit,
2123                    None => {
2124                        self.metrics
2125                            .record_cache_miss("executed_effects_digests", "committed");
2126                        CacheResult::Miss
2127                    }
2128                }
2129            },
2130            |remaining| {
2131                let remaining_digests: Vec<_> = remaining.iter().map(|(d, _)| *d).collect();
2132                let results = self
2133                    .record_db_multi_get("executed_effects_digests", remaining.len())
2134                    .multi_get_executed_effects_digests(&remaining_digests)
2135                    .expect("db error");
2136                for ((digest, ticket), result) in remaining.iter().zip_debug_eq(results.iter()) {
2137                    if result.is_none() {
2138                        self.cached
2139                            .executed_effects_digests
2140                            .insert(digest, None, *ticket)
2141                            .ok();
2142                    }
2143                }
2144                results
2145            },
2146        )
2147    }
2148
2149    fn multi_get_effects(
2150        &self,
2151        digests: &[TransactionEffectsDigest],
2152    ) -> Vec<Option<TransactionEffects>> {
2153        let digests_and_tickets: Vec<_> = digests
2154            .iter()
2155            .map(|d| (*d, self.cached.transaction_effects.get_ticket_for_read(d)))
2156            .collect();
2157        do_fallback_lookup(
2158            &digests_and_tickets,
2159            |(digest, _)| {
2160                self.metrics
2161                    .record_cache_request("transaction_effects", "uncommitted");
2162                if let Some(effects) = self.dirty.transaction_effects.get(digest) {
2163                    self.metrics
2164                        .record_cache_hit("transaction_effects", "uncommitted");
2165                    return CacheResult::Hit(Some(effects.clone()));
2166                }
2167                self.metrics
2168                    .record_cache_miss("transaction_effects", "uncommitted");
2169
2170                self.metrics
2171                    .record_cache_request("transaction_effects", "committed");
2172                match self
2173                    .cached
2174                    .transaction_effects
2175                    .get(digest)
2176                    .map(|l| l.lock().clone())
2177                {
2178                    Some(PointCacheItem::Some(effects)) => {
2179                        self.metrics
2180                            .record_cache_hit("transaction_effects", "committed");
2181                        CacheResult::Hit(Some((*effects).clone()))
2182                    }
2183                    Some(PointCacheItem::None) => CacheResult::NegativeHit,
2184                    None => {
2185                        self.metrics
2186                            .record_cache_miss("transaction_effects", "committed");
2187                        CacheResult::Miss
2188                    }
2189                }
2190            },
2191            |remaining| {
2192                let remaining_digests: Vec<_> = remaining.iter().map(|(d, _)| *d).collect();
2193                let results = self
2194                    .record_db_multi_get("transaction_effects", remaining.len())
2195                    .multi_get_effects(remaining_digests.iter())
2196                    .expect("db error");
2197                for ((digest, ticket), result) in remaining.iter().zip_debug_eq(results.iter()) {
2198                    if result.is_none() {
2199                        self.cached
2200                            .transaction_effects
2201                            .insert(digest, None, *ticket)
2202                            .ok();
2203                    }
2204                }
2205                results
2206            },
2207        )
2208    }
2209
2210    fn transaction_executed_in_last_epoch(
2211        &self,
2212        digest: &TransactionDigest,
2213        current_epoch: EpochId,
2214    ) -> bool {
2215        if current_epoch == 0 {
2216            return false;
2217        }
2218        let last_epoch = current_epoch - 1;
2219        let cache_key = (last_epoch, *digest);
2220
2221        let ticket = self
2222            .cached
2223            .transaction_executed_in_last_epoch
2224            .get_ticket_for_read(&cache_key);
2225
2226        if let Some(cached) = self
2227            .cached
2228            .transaction_executed_in_last_epoch
2229            .get(&cache_key)
2230        {
2231            return cached.lock().is_some();
2232        }
2233
2234        let was_executed = self
2235            .store
2236            .perpetual_tables
2237            .was_transaction_executed_in_last_epoch(digest, current_epoch);
2238
2239        let value = if was_executed { Some(()) } else { None };
2240        self.cached
2241            .transaction_executed_in_last_epoch
2242            .insert(&cache_key, value, ticket)
2243            .ok();
2244
2245        was_executed
2246    }
2247
2248    fn notify_read_executed_effects_digests<'a>(
2249        &'a self,
2250        task_name: &'static str,
2251        digests: &'a [TransactionDigest],
2252    ) -> BoxFuture<'a, Vec<TransactionEffectsDigest>> {
2253        self.executed_effects_digests_notify_read
2254            .read(task_name, digests, |digests| {
2255                self.multi_get_executed_effects_digests(digests)
2256            })
2257            .boxed()
2258    }
2259
2260    fn multi_get_events(
2261        &self,
2262        event_digests: &[TransactionDigest],
2263    ) -> Vec<Option<TransactionEvents>> {
2264        fn map_events(events: TransactionEvents) -> Option<TransactionEvents> {
2265            if events.data.is_empty() {
2266                None
2267            } else {
2268                Some(events)
2269            }
2270        }
2271
2272        let digests_and_tickets: Vec<_> = event_digests
2273            .iter()
2274            .map(|d| (*d, self.cached.transaction_events.get_ticket_for_read(d)))
2275            .collect();
2276        do_fallback_lookup(
2277            &digests_and_tickets,
2278            |(digest, _)| {
2279                self.metrics
2280                    .record_cache_request("transaction_events", "uncommitted");
2281                if let Some(events) = self.dirty.transaction_events.get(digest).map(|e| e.clone()) {
2282                    self.metrics
2283                        .record_cache_hit("transaction_events", "uncommitted");
2284
2285                    return CacheResult::Hit(map_events(events));
2286                }
2287                self.metrics
2288                    .record_cache_miss("transaction_events", "uncommitted");
2289
2290                self.metrics
2291                    .record_cache_request("transaction_events", "committed");
2292                match self
2293                    .cached
2294                    .transaction_events
2295                    .get(digest)
2296                    .map(|l| l.lock().clone())
2297                {
2298                    Some(PointCacheItem::Some(events)) => {
2299                        self.metrics
2300                            .record_cache_hit("transaction_events", "committed");
2301                        CacheResult::Hit(map_events((*events).clone()))
2302                    }
2303                    Some(PointCacheItem::None) => CacheResult::NegativeHit,
2304                    None => {
2305                        self.metrics
2306                            .record_cache_miss("transaction_events", "committed");
2307
2308                        CacheResult::Miss
2309                    }
2310                }
2311            },
2312            |remaining| {
2313                let remaining_digests: Vec<_> = remaining.iter().map(|(d, _)| *d).collect();
2314                let results = self
2315                    .store
2316                    .multi_get_events(&remaining_digests)
2317                    .expect("db error");
2318                for ((digest, ticket), result) in remaining.iter().zip_debug_eq(results.iter()) {
2319                    if result.is_none() {
2320                        self.cached
2321                            .transaction_events
2322                            .insert(digest, None, *ticket)
2323                            .ok();
2324                    }
2325                }
2326                results
2327            },
2328        )
2329    }
2330
2331    fn get_unchanged_loaded_runtime_objects(
2332        &self,
2333        digest: &TransactionDigest,
2334    ) -> Option<Vec<ObjectKey>> {
2335        self.dirty
2336            .unchanged_loaded_runtime_objects
2337            .get(digest)
2338            .map(|b| b.clone())
2339            .or_else(|| {
2340                self.store
2341                    .get_unchanged_loaded_runtime_objects(digest)
2342                    .expect("db error")
2343            })
2344    }
2345
2346    fn multi_get_unchanged_loaded_runtime_objects(
2347        &self,
2348        digests: &[TransactionDigest],
2349    ) -> Vec<Option<Vec<ObjectKey>>> {
2350        do_fallback_lookup(
2351            digests,
2352            |digest| match self.dirty.unchanged_loaded_runtime_objects.get(digest) {
2353                Some(objects) => CacheResult::Hit(Some(objects.clone())),
2354                None => CacheResult::Miss,
2355            },
2356            |digests| {
2357                self.store
2358                    .multi_get_unchanged_loaded_runtime_objects(digests)
2359                    .expect("db error")
2360            },
2361        )
2362    }
2363
2364    fn take_accumulator_events(&self, digest: &TransactionDigest) -> Option<Vec<AccumulatorEvent>> {
2365        self.dirty
2366            .pending_transaction_writes
2367            .get(digest)
2368            .map(|transaction_output| transaction_output.take_accumulator_events())
2369    }
2370}
2371
2372impl ExecutionCacheWrite for WritebackCache {
2373    fn validate_owned_object_versions(&self, owned_input_objects: &[ObjectRef]) -> SuiResult {
2374        ObjectLocks::validate_owned_object_versions(self, owned_input_objects)
2375    }
2376
2377    fn write_transaction_outputs(&self, epoch_id: EpochId, tx_outputs: Arc<TransactionOutputs>) {
2378        WritebackCache::write_transaction_outputs(self, epoch_id, tx_outputs);
2379    }
2380
2381    #[cfg(test)]
2382    fn write_object_entry_for_test(&self, object: Object) {
2383        self.write_object_entry(&object.id(), object.version(), object.into());
2384    }
2385}
2386
2387implement_passthrough_traits!(WritebackCache);
2388
2389impl GlobalStateHashStore for WritebackCache {
2390    fn get_object_ref_prior_to_key_deprecated(
2391        &self,
2392        object_id: &ObjectID,
2393        version: SequenceNumber,
2394    ) -> SuiResult<Option<ObjectRef>> {
2395        // There is probably a more efficient way to implement this, but since this is only used by
2396        // old protocol versions, it is better to do the simple thing that is obviously correct.
2397        // In this case we previous version from all sources and choose the highest
2398        let mut candidates = Vec::new();
2399
2400        let check_versions =
2401            |versions: &CachedVersionMap<ObjectEntry>| match versions.get_prior_to(&version) {
2402                Some((version, object_entry)) => match object_entry {
2403                    ObjectEntry::Object(object) => {
2404                        assert_eq!(object.version(), version);
2405                        Some(object.compute_object_reference())
2406                    }
2407                    ObjectEntry::Deleted => {
2408                        Some((*object_id, version, ObjectDigest::OBJECT_DIGEST_DELETED))
2409                    }
2410                    ObjectEntry::Wrapped => {
2411                        Some((*object_id, version, ObjectDigest::OBJECT_DIGEST_WRAPPED))
2412                    }
2413                },
2414                None => None,
2415            };
2416
2417        // first check dirty data
2418        if let Some(objects) = self.dirty.objects.get(object_id)
2419            && let Some(prior) = check_versions(&objects)
2420        {
2421            candidates.push(prior);
2422        }
2423
2424        if let Some(objects) = self.cached.object_cache.get(object_id)
2425            && let Some(prior) = check_versions(&objects.lock())
2426        {
2427            candidates.push(prior);
2428        }
2429
2430        if let Some(prior) = self
2431            .store
2432            .get_object_ref_prior_to_key_deprecated(object_id, version)?
2433        {
2434            candidates.push(prior);
2435        }
2436
2437        // sort candidates by version, and return the highest
2438        candidates.sort_by_key(|(_, version, _)| *version);
2439        Ok(candidates.pop())
2440    }
2441
2442    fn get_root_state_hash_for_epoch(
2443        &self,
2444        epoch: EpochId,
2445    ) -> SuiResult<Option<(CheckpointSequenceNumber, GlobalStateHash)>> {
2446        self.store.get_root_state_hash_for_epoch(epoch)
2447    }
2448
2449    fn get_root_state_hash_for_highest_epoch(
2450        &self,
2451    ) -> SuiResult<Option<(EpochId, (CheckpointSequenceNumber, GlobalStateHash))>> {
2452        self.store.get_root_state_hash_for_highest_epoch()
2453    }
2454
2455    fn insert_state_hash_for_epoch(
2456        &self,
2457        epoch: EpochId,
2458        checkpoint_seq_num: &CheckpointSequenceNumber,
2459        acc: &GlobalStateHash,
2460    ) -> SuiResult {
2461        self.store
2462            .insert_state_hash_for_epoch(epoch, checkpoint_seq_num, acc)
2463    }
2464
2465    fn iter_live_object_set(
2466        &self,
2467        include_wrapped_tombstone: bool,
2468    ) -> Box<dyn Iterator<Item = LiveObject> + '_> {
2469        // The only time it is safe to iterate the live object set is at an epoch boundary,
2470        // at which point the db is consistent and the dirty cache is empty. So this does
2471        // read the cache
2472        assert!(
2473            self.dirty.is_empty(),
2474            "cannot iterate live object set with dirty data"
2475        );
2476        self.store.iter_live_object_set(include_wrapped_tombstone)
2477    }
2478
2479    // A version of iter_live_object_set that reads the cache. Only use for testing. If used
2480    // on a live validator, can cause the server to block for as long as it takes to iterate
2481    // the entire live object set.
2482    fn iter_cached_live_object_set_for_testing(
2483        &self,
2484        include_wrapped_tombstone: bool,
2485    ) -> Box<dyn Iterator<Item = LiveObject> + '_> {
2486        // hold iter until we are finished to prevent any concurrent inserts/deletes
2487        let iter = self.dirty.objects.iter();
2488        let mut dirty_objects = BTreeMap::new();
2489
2490        // add everything from the store
2491        for obj in self.store.iter_live_object_set(include_wrapped_tombstone) {
2492            dirty_objects.insert(obj.object_id(), obj);
2493        }
2494
2495        // add everything from the cache, but also remove deletions
2496        for entry in iter {
2497            let id = *entry.key();
2498            let value = entry.value();
2499            match value.get_highest().unwrap() {
2500                (_, ObjectEntry::Object(object)) => {
2501                    dirty_objects.insert(id, LiveObject::Normal(object.clone()));
2502                }
2503                (version, ObjectEntry::Wrapped) => {
2504                    if include_wrapped_tombstone {
2505                        dirty_objects.insert(id, LiveObject::Wrapped(ObjectKey(id, *version)));
2506                    } else {
2507                        dirty_objects.remove(&id);
2508                    }
2509                }
2510                (_, ObjectEntry::Deleted) => {
2511                    dirty_objects.remove(&id);
2512                }
2513            }
2514        }
2515
2516        Box::new(dirty_objects.into_values())
2517    }
2518}
2519
2520// TODO: For correctness, we must at least invalidate the cache when items are written through this
2521// trait (since they could be negatively cached as absent). But it may or may not be optimal to
2522// actually insert them into the cache. For instance if state sync is running ahead of execution,
2523// they might evict other items that are about to be read. This could be an area for tuning in the
2524// future.
2525impl StateSyncAPI for WritebackCache {
2526    fn insert_transaction_and_effects(
2527        &self,
2528        transaction: &VerifiedTransaction,
2529        transaction_effects: &TransactionEffects,
2530    ) {
2531        self.store
2532            .insert_transaction_and_effects(transaction, transaction_effects)
2533            .expect("db error");
2534        self.cached
2535            .transactions
2536            .insert(
2537                transaction.digest(),
2538                PointCacheItem::Some(Arc::new(transaction.clone())),
2539                Ticket::Write,
2540            )
2541            .ok();
2542        self.cached
2543            .transaction_effects
2544            .insert(
2545                &transaction_effects.digest(),
2546                PointCacheItem::Some(Arc::new(transaction_effects.clone())),
2547                Ticket::Write,
2548            )
2549            .ok();
2550    }
2551
2552    fn multi_insert_transaction_and_effects(
2553        &self,
2554        transactions_and_effects: &[VerifiedExecutionData],
2555    ) {
2556        self.store
2557            .multi_insert_transaction_and_effects(transactions_and_effects.iter())
2558            .expect("db error");
2559        for VerifiedExecutionData {
2560            transaction,
2561            effects,
2562        } in transactions_and_effects
2563        {
2564            self.cached
2565                .transactions
2566                .insert(
2567                    transaction.digest(),
2568                    PointCacheItem::Some(Arc::new(transaction.clone())),
2569                    Ticket::Write,
2570                )
2571                .ok();
2572            self.cached
2573                .transaction_effects
2574                .insert(
2575                    &effects.digest(),
2576                    PointCacheItem::Some(Arc::new(effects.clone())),
2577                    Ticket::Write,
2578                )
2579                .ok();
2580        }
2581    }
2582}