Skip to main content

sui_core/authority/
authority_store_tables.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use super::*;
5#[cfg(tidehunter)]
6use crate::authority::epoch_marker_key::EPOCH_MARKER_KEY_SIZE;
7use crate::authority::epoch_marker_key::EpochMarkerKey;
8use serde::{Deserialize, Serialize};
9use std::path::Path;
10use std::sync::atomic::AtomicU64;
11use sui_types::base_types::SequenceNumber;
12use sui_types::effects::{TransactionEffects, TransactionEvents};
13use sui_types::global_state_hash::GlobalStateHash;
14use sui_types::messages_consensus::SharedTransactionDenyConfig;
15use sui_types::storage::MarkerValue;
16use typed_store::metrics::SamplingInterval;
17use typed_store::rocks::{DBBatch, DBMap, MetricConf};
18#[cfg(not(tidehunter))]
19use typed_store::rocks::{DBMapTableConfigMap, DBOptions, default_db_options, read_size_from_env};
20use typed_store::traits::Map;
21
22use crate::authority::authority_store_types::{
23    StoreObject, StoreObjectValue, StoreObjectWrapper, get_store_object, try_construct_object,
24};
25use crate::authority::epoch_start_configuration::EpochStartConfiguration;
26use typed_store::{DBMapUtils, DbIterator};
27
28#[cfg(not(tidehunter))]
29const ENV_VAR_OBJECTS_BLOCK_CACHE_SIZE: &str = "OBJECTS_BLOCK_CACHE_MB";
30#[cfg(not(tidehunter))]
31pub(crate) const ENV_VAR_LOCKS_BLOCK_CACHE_SIZE: &str = "LOCKS_BLOCK_CACHE_MB";
32#[cfg(not(tidehunter))]
33const ENV_VAR_TRANSACTIONS_BLOCK_CACHE_SIZE: &str = "TRANSACTIONS_BLOCK_CACHE_MB";
34#[cfg(not(tidehunter))]
35const ENV_VAR_EFFECTS_BLOCK_CACHE_SIZE: &str = "EFFECTS_BLOCK_CACHE_MB";
36
37/// Options to apply to every column family of the `perpetual` DB.
38#[derive(Default)]
39pub struct AuthorityPerpetualTablesOptions {
40    /// Whether to enable write stalling on all column families.
41    pub enable_write_stall: bool,
42    /// On tidehunter, attach the per-keyspace objects compactor that retains
43    /// only the latest version per ObjectID. Mutually exclusive with the
44    /// object pruner — see `AuthorityStorePruner::new`.
45    pub enable_objects_compactor: bool,
46}
47
48impl AuthorityPerpetualTablesOptions {
49    #[cfg(not(tidehunter))]
50    fn apply_to(&self, mut db_options: DBOptions) -> DBOptions {
51        if !self.enable_write_stall {
52            db_options = db_options.disable_write_throttling();
53        }
54        db_options
55    }
56}
57
58/// AuthorityPerpetualTables contains data that must be preserved from one epoch to the next.
59#[derive(DBMapUtils)]
60#[cfg_attr(tidehunter, tidehunter)]
61pub struct AuthorityPerpetualTables {
62    /// This is a map between the object (ID, version) and the latest state of the object, namely the
63    /// state that is needed to process new transactions.
64    /// State is represented by `StoreObject` enum, which is either a move module or a move object.
65    ///
66    /// Note that while this map can store all versions of an object, we will eventually
67    /// prune old object versions from the db.
68    ///
69    /// IMPORTANT: object versions must *only* be pruned if they appear as inputs in some
70    /// TransactionEffects. Simply pruning all objects but the most recent is an error!
71    /// This is because there can be partially executed transactions whose effects have not yet
72    /// been written out, and which must be retried. But, they cannot be retried unless their input
73    /// objects are still accessible!
74    pub(crate) objects: DBMap<ObjectKey, StoreObjectWrapper>,
75
76    /// This is a map between the transaction digest and the corresponding transaction that's known to be
77    /// executable. This means that it may have been executed locally, or it may have been synced through
78    /// state-sync but hasn't been executed yet.
79    pub(crate) transactions: DBMap<TransactionDigest, TrustedTransaction>,
80
81    /// A map between the transaction digest of a certificate to the effects of its execution.
82    /// We store effects into this table in two different cases:
83    /// 1. When a transaction is synced through state_sync, we store the effects here. These effects
84    ///    are known to be final in the network, but may not have been executed locally yet.
85    /// 2. When the transaction is executed locally on this node, we store the effects here. This means that
86    ///    it's possible to store the same effects twice (once for the synced transaction, and once for the executed).
87    ///
88    /// It's also possible for the effects to be reverted if the transaction didn't make it into the epoch.
89    pub(crate) effects: DBMap<TransactionEffectsDigest, TransactionEffects>,
90
91    /// Transactions that have been executed locally on this node. We need this table since the `effects` table
92    /// doesn't say anything about the execution status of the transaction on this node. When we wait for transactions
93    /// to be executed, we wait for them to appear in this table. When we revert transactions, we remove them from both
94    /// tables.
95    pub(crate) executed_effects: DBMap<TransactionDigest, TransactionEffectsDigest>,
96
97    // Events keyed by the digest of the transaction that produced them.
98    pub(crate) events_2: DBMap<TransactionDigest, TransactionEvents>,
99
100    // Loaded (and unchanged) runtime object references.
101    pub(crate) unchanged_loaded_runtime_objects: DBMap<TransactionDigest, Vec<ObjectKey>>,
102
103    /// DEPRECATED in favor of the table of the same name in authority_per_epoch_store.
104    /// Please do not add new accessors/callsites.
105    /// When transaction is executed via checkpoint executor, we store association here
106    pub(crate) executed_transactions_to_checkpoint:
107        DBMap<TransactionDigest, (EpochId, CheckpointSequenceNumber)>,
108
109    // Finalized root state hash for epoch, to be included in CheckpointSummary
110    // of last checkpoint of epoch. These values should only ever be written once
111    // and never changed
112    pub(crate) root_state_hash_by_epoch:
113        DBMap<EpochId, (CheckpointSequenceNumber, GlobalStateHash)>,
114
115    /// Parameters of the system fixed at the epoch start
116    pub(crate) epoch_start_configuration: DBMap<(), EpochStartConfiguration>,
117
118    /// A singleton table that stores latest pruned checkpoint. Used to keep objects pruner progress
119    pub(crate) pruned_checkpoint: DBMap<(), CheckpointSequenceNumber>,
120
121    /// Expected total amount of SUI in the network. This is expected to remain constant
122    /// throughout the lifetime of the network. We check it at the end of each epoch if
123    /// expensive checks are enabled. We cannot use 10B today because in tests we often
124    /// inject extra gas objects into genesis.
125    pub(crate) expected_network_sui_amount: DBMap<(), u64>,
126
127    /// Expected imbalance between storage fund balance and the sum of storage rebate of all live objects.
128    /// This could be non-zero due to bugs in earlier protocol versions.
129    /// This number is the result of storage_fund_balance - sum(storage_rebate).
130    pub(crate) expected_storage_fund_imbalance: DBMap<(), i64>,
131
132    /// Table that stores the set of received objects and deleted objects and the version at
133    /// which they were received. This is used to prevent possible race conditions around receiving
134    /// objects (since they are not locked by the transaction manager) and for tracking shared
135    /// objects that have been deleted. This table is meant to be pruned per-epoch, and all
136    /// previous epochs other than the current epoch may be pruned safely.
137    pub(crate) object_per_epoch_marker_table: DBMap<(EpochId, ObjectKey), MarkerValue>,
138    pub(crate) object_per_epoch_marker_table_v2: DBMap<EpochMarkerKey, MarkerValue>,
139
140    /// Tracks executed transaction digests across epochs.
141    /// Used to support address balance gas payments feature.
142    /// This table uses epoch-prefixed keys to support efficient pruning via range delete.
143    pub(crate) executed_transaction_digests: DBMap<(EpochId, TransactionDigest), ()>,
144
145    /// A singleton table recording the highest checkpoint whose transaction
146    /// outputs (objects, effects, etc.) are durably committed to this store.
147    /// It is written in the *same* atomic batch as those outputs (see
148    /// `AuthorityStore::build_db_batch` and the checkpoint executor), so it is
149    /// always consistent with the live object set even after an unclean stop.
150    ///
151    /// This differs from the checkpoint store's `highest_executed` watermark,
152    /// which is bumped in a separate write after the outputs are committed: an
153    /// abrupt stop can leave the object writes durable while `highest_executed`
154    /// still lags. Consumers that read the live object set directly (e.g. the
155    /// embedded rpc-store's bulk restore) use this watermark instead, so they
156    /// never observe objects beyond the checkpoint they think they are at.
157    ///
158    /// IMPORTANT: TideHunter keyspaces are order-sensitive once written to disk.
159    /// Keep new keyspaces append-only to preserve compatibility with existing DBs.
160    pub(crate) highest_committed_checkpoint: DBMap<(), CheckpointSequenceNumber>,
161
162    /// Most recent `UpdateTransactionDenyConfig` consensus message accepted from each
163    /// allowlisted peer authority. Persisted across epochs so we can rebuild the
164    /// merged effective deny config after a node restart. Pruned at epoch transitions
165    /// when a peer is no longer in the active committee.
166    pub(crate) shared_transaction_deny_configs: DBMap<AuthorityName, SharedTransactionDenyConfig>,
167
168    /// Singleton: the highest generation this node has ever broadcast in an
169    /// `UpdateTransactionDenyConfig` message.
170    pub(crate) last_broadcast_deny_generation: DBMap<(), u64>,
171}
172
173impl AuthorityPerpetualTables {
174    pub fn path(parent_path: &Path) -> PathBuf {
175        parent_path.join("perpetual")
176    }
177
178    #[cfg(not(tidehunter))]
179    pub fn open(
180        parent_path: &Path,
181        db_options_override: Option<AuthorityPerpetualTablesOptions>,
182        _pruner_watermark: Option<Arc<AtomicU64>>,
183    ) -> Self {
184        let db_options_override = db_options_override.unwrap_or_default();
185        let db_options = db_options_override
186            .apply_to(default_db_options().optimize_db_for_write_throughput(4, false));
187        let table_options = DBMapTableConfigMap::new(BTreeMap::from([
188            (
189                "objects".to_string(),
190                objects_table_config(db_options.clone()),
191            ),
192            (
193                "transactions".to_string(),
194                transactions_table_config(db_options.clone()),
195            ),
196            (
197                "effects".to_string(),
198                effects_table_config(db_options.clone()),
199            ),
200        ]));
201
202        Self::open_tables_read_write(
203            Self::path(parent_path),
204            MetricConf::new("perpetual")
205                .with_sampling(SamplingInterval::new(Duration::from_secs(60), 0)),
206            Some(db_options.options),
207            Some(table_options),
208        )
209    }
210
211    #[cfg(tidehunter)]
212    pub fn open(
213        parent_path: &Path,
214        db_options_override: Option<AuthorityPerpetualTablesOptions>,
215        pruner_watermark: Option<Arc<AtomicU64>>,
216    ) -> Self {
217        use crate::authority::authority_store_pruner::apply_relocation_filter;
218        tracing::warn!("AuthorityPerpetualTables using tidehunter");
219        use typed_store::tidehunter_util::{
220            Bytes, Decision, KeyIndexing, KeySpaceConfig, KeyType, ThConfig,
221            default_cells_per_mutex, default_max_dirty_keys, default_mutex_count,
222            default_value_cache_size,
223        };
224        let mutexes = default_mutex_count() * 2;
225        let transaction_mutexes = mutexes * 4;
226        let value_cache_size = default_value_cache_size();
227        // effectively disables pruning if not set
228        let pruner_watermark = pruner_watermark.unwrap_or(Arc::new(AtomicU64::new(0)));
229
230        let bloom_config = KeySpaceConfig::new().with_bloom_filter(0.001, 32_000);
231        let objects_compactor = |iter: &mut dyn DoubleEndedIterator<Item = &Bytes>| {
232            let mut retain = HashSet::new();
233            let mut previous: Option<&[u8]> = None;
234            const OID_SIZE: usize = 32;
235            for key in iter.rev() {
236                if let Some(prev) = previous
237                    && prev == &key[..OID_SIZE]
238                {
239                    continue;
240                }
241                previous = Some(&key[..OID_SIZE]);
242                retain.insert(key.clone());
243            }
244            retain
245        };
246        let mut digest_prefix = vec![0; 8];
247        digest_prefix[7] = 32;
248        let uniform_key = KeyType::uniform(default_cells_per_mutex());
249        let epoch_prefix_key = KeyType::from_prefix_bits(9 * 8 + 4);
250        // TransactionDigest is serialized with an 8-byte prefix, so we include it in the key calculation
251        let epoch_tx_digest_prefix_key =
252            KeyType::from_prefix_bits((8/*EpochId*/ + 8/*TransactionDigest prefix*/) * 8 + 12);
253        let object_indexing = KeyIndexing::fixed(32 + 8); //  KeyIndexing::key_reduction(32 + 8, 16..(32 + 8));
254        let mut objects_config = KeySpaceConfig::new()
255            .with_max_dirty_keys(16 * default_max_dirty_keys())
256            .with_value_cache_size(value_cache_size);
257        if matches!(db_options_override, Some(options) if options.enable_objects_compactor) {
258            objects_config = objects_config.with_compactor(Box::new(objects_compactor));
259        }
260
261        let configs = vec![
262            (
263                "objects".to_string(),
264                ThConfig::new_with_config_indexing(
265                    object_indexing,
266                    mutexes * 4,
267                    KeyType::uniform(1),
268                    objects_config,
269                ),
270            ),
271            (
272                "transactions".to_string(),
273                ThConfig::new_with_rm_prefix_indexing(
274                    KeyIndexing::key_reduction(32, 0..16),
275                    transaction_mutexes,
276                    uniform_key,
277                    KeySpaceConfig::new()
278                        .with_value_cache_size(value_cache_size)
279                        .with_relocation_filter(|_, _| Decision::Remove),
280                    digest_prefix.clone(),
281                ),
282            ),
283            (
284                "effects".to_string(),
285                ThConfig::new_with_rm_prefix_indexing(
286                    KeyIndexing::key_reduction(32, 0..16),
287                    transaction_mutexes,
288                    uniform_key,
289                    apply_relocation_filter(
290                        bloom_config.clone().with_value_cache_size(value_cache_size),
291                        pruner_watermark.clone(),
292                        |effects: TransactionEffects| effects.executed_epoch(),
293                        false,
294                    ),
295                    digest_prefix.clone(),
296                ),
297            ),
298            (
299                "executed_effects".to_string(),
300                ThConfig::new_with_rm_prefix_indexing(
301                    KeyIndexing::key_reduction(32, 0..16),
302                    transaction_mutexes,
303                    uniform_key,
304                    bloom_config
305                        .clone()
306                        .with_value_cache_size(value_cache_size)
307                        .with_relocation_filter(|_, _| Decision::Remove),
308                    digest_prefix.clone(),
309                ),
310            ),
311            (
312                "events".to_string(),
313                ThConfig::new_with_rm_prefix(
314                    32 + 8,
315                    mutexes,
316                    uniform_key,
317                    KeySpaceConfig::default().with_relocation_filter(|_, _| Decision::Remove),
318                    digest_prefix.clone(),
319                ),
320            ),
321            (
322                "events_2".to_string(),
323                ThConfig::new_with_rm_prefix(
324                    32,
325                    mutexes,
326                    uniform_key,
327                    KeySpaceConfig::default().with_relocation_filter(|_, _| Decision::Remove),
328                    digest_prefix.clone(),
329                ),
330            ),
331            (
332                "unchanged_loaded_runtime_objects".to_string(),
333                ThConfig::new_with_rm_prefix(
334                    32,
335                    mutexes,
336                    uniform_key,
337                    KeySpaceConfig::default().with_relocation_filter(|_, _| Decision::Remove),
338                    digest_prefix.clone(),
339                ),
340            ),
341            (
342                "executed_transactions_to_checkpoint".to_string(),
343                ThConfig::new_with_rm_prefix(
344                    32,
345                    mutexes,
346                    uniform_key,
347                    apply_relocation_filter(
348                        KeySpaceConfig::default(),
349                        pruner_watermark.clone(),
350                        |(epoch_id, _): (EpochId, CheckpointSequenceNumber)| epoch_id,
351                        false,
352                    ),
353                    digest_prefix.clone(),
354                ),
355            ),
356            (
357                "root_state_hash_by_epoch".to_string(),
358                ThConfig::new(8, 1, KeyType::uniform(1)),
359            ),
360            (
361                "epoch_start_configuration".to_string(),
362                ThConfig::new(0, 1, KeyType::uniform(1)),
363            ),
364            (
365                "pruned_checkpoint".to_string(),
366                ThConfig::new(0, 1, KeyType::uniform(1)),
367            ),
368            (
369                "expected_network_sui_amount".to_string(),
370                ThConfig::new(0, 1, KeyType::uniform(1)),
371            ),
372            (
373                "expected_storage_fund_imbalance".to_string(),
374                ThConfig::new(0, 1, KeyType::uniform(1)),
375            ),
376            (
377                "object_per_epoch_marker_table".to_string(),
378                ThConfig::new_with_config_indexing(
379                    KeyIndexing::VariableLength,
380                    mutexes,
381                    epoch_prefix_key,
382                    apply_relocation_filter(
383                        KeySpaceConfig::default(),
384                        pruner_watermark.clone(),
385                        |(epoch_id, _): (EpochId, ObjectKey)| epoch_id,
386                        true,
387                    ),
388                ),
389            ),
390            (
391                "object_per_epoch_marker_table_v2".to_string(),
392                ThConfig::new_with_config_indexing(
393                    KeyIndexing::fixed(EPOCH_MARKER_KEY_SIZE),
394                    mutexes,
395                    epoch_prefix_key,
396                    apply_relocation_filter(
397                        bloom_config.clone(),
398                        pruner_watermark.clone(),
399                        |k: EpochMarkerKey| k.0,
400                        true,
401                    ),
402                ),
403            ),
404            (
405                "executed_transaction_digests".to_string(),
406                ThConfig::new_with_config_indexing(
407                    // EpochId + (TransactionDigest)
408                    KeyIndexing::fixed(8 + (32 + 8)),
409                    transaction_mutexes,
410                    epoch_tx_digest_prefix_key,
411                    apply_relocation_filter(
412                        bloom_config.clone(),
413                        pruner_watermark.clone(),
414                        |(epoch_id, _): (EpochId, TransactionDigest)| epoch_id,
415                        true,
416                    ),
417                ),
418            ),
419            (
420                "highest_committed_checkpoint".to_string(),
421                ThConfig::new(0, 1, KeyType::uniform(1)),
422            ),
423            (
424                "shared_transaction_deny_configs".to_string(),
425                // AuthorityName encodes to 104 bytes via BCS (8-byte tuple-struct
426                // prefix + 96-byte BLS12381 pubkey), matching authority_capabilities_v2.
427                // Tiny table (one row per allowlisted committee member).
428                ThConfig::new(104, 1, KeyType::uniform(1)),
429            ),
430            (
431                "last_broadcast_deny_generation".to_string(),
432                ThConfig::new(0, 1, KeyType::uniform(1)),
433            ),
434        ];
435        Self::open_tables_read_write(
436            Self::path(parent_path),
437            MetricConf::new("perpetual")
438                .with_sampling(SamplingInterval::new(Duration::from_secs(60), 0)),
439            configs.into_iter().collect(),
440        )
441    }
442
443    #[cfg(not(tidehunter))]
444    pub fn open_readonly(parent_path: &Path) -> AuthorityPerpetualTablesReadOnly {
445        Self::get_read_only_handle(
446            Self::path(parent_path),
447            None,
448            None,
449            MetricConf::new("perpetual_readonly"),
450        )
451    }
452
453    #[cfg(tidehunter)]
454    pub fn open_readonly(parent_path: &Path) -> Self {
455        Self::open(parent_path, None, None)
456    }
457
458    #[cfg(tidehunter)]
459    pub fn force_rebuild_control_region(&self) -> anyhow::Result<()> {
460        self.objects.db.force_rebuild_control_region()
461    }
462
463    /// Wait for tidehunter background threads to finish before allowing the caller
464    /// to e.g. rename the database directory. Consumes `Arc<Self>` so all internal
465    /// `Arc<Database>` clones held by the column family `DBMap`s are released as
466    /// part of the drop, leaving only the `Arc<Database>` extracted here.
467    #[cfg(tidehunter)]
468    pub fn wait_for_tidehunter_background_threads(self: Arc<Self>) {
469        let strong = Arc::strong_count(&self);
470        if strong != 1 {
471            println!(
472                "WARNING: wait_for_tidehunter_background_threads called with Arc<AuthorityPerpetualTables> strong_count={} (expected 1); other clones will keep DBMap.db Arc<Database> alive past drop(self) and the inner Database wait will warn/timeout",
473                strong,
474            );
475        }
476        let db = self.objects.db.clone();
477        drop(self);
478        db.wait_for_tidehunter_background_threads();
479    }
480
481    // This is used by indexer to find the correct version of dynamic field child object.
482    // We do not store the version of the child object, but because of lamport timestamp,
483    // we know the child must have version number less then or eq to the parent.
484    pub fn find_object_lt_or_eq_version(
485        &self,
486        object_id: ObjectID,
487        version: SequenceNumber,
488    ) -> SuiResult<Option<Object>> {
489        let mut iter = self.objects.reversed_safe_iter_with_bounds(
490            Some(ObjectKey::min_for_id(&object_id)),
491            Some(ObjectKey(object_id, version)),
492        )?;
493        match iter.next() {
494            Some(Ok((key, o))) => self.object(&key, o),
495            Some(Err(e)) => Err(e.into()),
496            None => Ok(None),
497        }
498    }
499
500    fn construct_object(
501        &self,
502        object_key: &ObjectKey,
503        store_object: StoreObjectValue,
504    ) -> Result<Object, SuiError> {
505        try_construct_object(object_key, store_object)
506    }
507
508    // Constructs `sui_types::object::Object` from `StoreObjectWrapper`.
509    // Returns `None` if object was deleted/wrapped
510    pub fn object(
511        &self,
512        object_key: &ObjectKey,
513        store_object: StoreObjectWrapper,
514    ) -> Result<Option<Object>, SuiError> {
515        let StoreObject::Value(store_object) = store_object.migrate().into_inner() else {
516            return Ok(None);
517        };
518        Ok(Some(self.construct_object(object_key, *store_object)?))
519    }
520
521    pub fn object_reference(
522        &self,
523        object_key: &ObjectKey,
524        store_object: StoreObjectWrapper,
525    ) -> Result<ObjectRef, SuiError> {
526        let obj_ref = match store_object.migrate().into_inner() {
527            StoreObject::Value(object) => self
528                .construct_object(object_key, *object)?
529                .compute_object_reference(),
530            StoreObject::Deleted => (
531                object_key.0,
532                object_key.1,
533                ObjectDigest::OBJECT_DIGEST_DELETED,
534            ),
535            StoreObject::Wrapped => (
536                object_key.0,
537                object_key.1,
538                ObjectDigest::OBJECT_DIGEST_WRAPPED,
539            ),
540        };
541        Ok(obj_ref)
542    }
543
544    pub fn tombstone_reference(
545        &self,
546        object_key: &ObjectKey,
547        store_object: &StoreObjectWrapper,
548    ) -> Result<Option<ObjectRef>, SuiError> {
549        let obj_ref = match store_object.inner() {
550            StoreObject::Deleted => Some((
551                object_key.0,
552                object_key.1,
553                ObjectDigest::OBJECT_DIGEST_DELETED,
554            )),
555            StoreObject::Wrapped => Some((
556                object_key.0,
557                object_key.1,
558                ObjectDigest::OBJECT_DIGEST_WRAPPED,
559            )),
560            _ => None,
561        };
562        Ok(obj_ref)
563    }
564
565    pub fn get_latest_object_ref_or_tombstone(
566        &self,
567        object_id: ObjectID,
568    ) -> Result<Option<ObjectRef>, SuiError> {
569        let mut iterator = self.objects.reversed_safe_iter_with_bounds(
570            Some(ObjectKey::min_for_id(&object_id)),
571            Some(ObjectKey::max_for_id(&object_id)),
572        )?;
573
574        if let Some(Ok((object_key, value))) = iterator.next()
575            && object_key.0 == object_id
576        {
577            return Ok(Some(self.object_reference(&object_key, value)?));
578        }
579        Ok(None)
580    }
581
582    pub fn get_latest_object_or_tombstone(
583        &self,
584        object_id: ObjectID,
585    ) -> Result<Option<(ObjectKey, StoreObjectWrapper)>, SuiError> {
586        let mut iterator = self.objects.reversed_safe_iter_with_bounds(
587            Some(ObjectKey::min_for_id(&object_id)),
588            Some(ObjectKey::max_for_id(&object_id)),
589        )?;
590
591        if let Some(Ok((object_key, value))) = iterator.next()
592            && object_key.0 == object_id
593        {
594            return Ok(Some((object_key, value)));
595        }
596        Ok(None)
597    }
598
599    pub fn get_recovery_epoch_at_restart(&self) -> SuiResult<EpochId> {
600        Ok(self
601            .epoch_start_configuration
602            .get(&())?
603            .expect("Must have current epoch.")
604            .epoch_start_state()
605            .epoch())
606    }
607
608    pub fn set_epoch_start_configuration(
609        &self,
610        epoch_start_configuration: &EpochStartConfiguration,
611    ) -> SuiResult {
612        let mut wb = self.epoch_start_configuration.batch();
613        wb.insert_batch(
614            &self.epoch_start_configuration,
615            std::iter::once(((), epoch_start_configuration)),
616        )?;
617        wb.write()?;
618        Ok(())
619    }
620
621    pub fn get_highest_pruned_checkpoint(
622        &self,
623    ) -> Result<Option<CheckpointSequenceNumber>, TypedStoreError> {
624        self.pruned_checkpoint.get(&())
625    }
626
627    pub fn set_highest_pruned_checkpoint(
628        &self,
629        wb: &mut DBBatch,
630        checkpoint_number: CheckpointSequenceNumber,
631    ) -> SuiResult {
632        wb.insert_batch(&self.pruned_checkpoint, [((), checkpoint_number)])?;
633        Ok(())
634    }
635
636    pub fn get_highest_committed_checkpoint(
637        &self,
638    ) -> Result<Option<CheckpointSequenceNumber>, TypedStoreError> {
639        self.highest_committed_checkpoint.get(&())
640    }
641
642    /// Stage the highest-committed-checkpoint watermark into `wb`. Must be
643    /// called on the same batch that commits the checkpoint's transaction
644    /// outputs so the watermark and the outputs land atomically.
645    pub fn set_highest_committed_checkpoint(
646        &self,
647        wb: &mut DBBatch,
648        checkpoint_number: CheckpointSequenceNumber,
649    ) -> SuiResult {
650        wb.insert_batch(
651            &self.highest_committed_checkpoint,
652            [((), checkpoint_number)],
653        )?;
654        Ok(())
655    }
656
657    pub fn get_transaction(
658        &self,
659        digest: &TransactionDigest,
660    ) -> SuiResult<Option<TrustedTransaction>> {
661        let Some(transaction) = self.transactions.get(digest)? else {
662            return Ok(None);
663        };
664        Ok(Some(transaction))
665    }
666
667    pub fn list_transactions_from(
668        &self,
669        start: Option<TransactionDigest>,
670        limit: usize,
671    ) -> Result<Vec<TransactionDigest>, typed_store::TypedStoreError> {
672        let iter = self.transactions.safe_iter_with_bounds(start, None);
673        let mut result = Vec::with_capacity(limit);
674        for item in iter.take(limit) {
675            let (digest, _) = item?;
676            result.push(digest);
677        }
678        Ok(result)
679    }
680
681    pub fn get_executed_effects_digest(
682        &self,
683        tx_digest: &TransactionDigest,
684    ) -> Result<Option<TransactionEffectsDigest>, typed_store::TypedStoreError> {
685        self.executed_effects.get(tx_digest)
686    }
687
688    pub fn get_effects_by_digest(
689        &self,
690        effects_digest: &TransactionEffectsDigest,
691    ) -> Result<Option<TransactionEffects>, typed_store::TypedStoreError> {
692        self.effects.get(effects_digest)
693    }
694
695    /// Batch insert executed transaction digests for a given epoch.
696    /// Used by formal snapshot restore to backfill transaction digests from the previous epoch.
697    pub fn insert_executed_transaction_digests_batch(
698        &self,
699        epoch: EpochId,
700        digests: impl Iterator<Item = TransactionDigest>,
701    ) -> SuiResult {
702        let mut batch = self.executed_transaction_digests.batch();
703        batch.insert_batch(
704            &self.executed_transaction_digests,
705            digests.map(|digest| ((epoch, digest), ())),
706        )?;
707        batch.write()?;
708        Ok(())
709    }
710
711    pub fn get_effects(&self, digest: &TransactionDigest) -> SuiResult<Option<TransactionEffects>> {
712        let Some(effect_digest) = self.executed_effects.get(digest)? else {
713            return Ok(None);
714        };
715        Ok(self.effects.get(&effect_digest)?)
716    }
717
718    pub(crate) fn was_transaction_executed_in_last_epoch(
719        &self,
720        digest: &TransactionDigest,
721        current_epoch: EpochId,
722    ) -> bool {
723        if current_epoch == 0 {
724            return false;
725        }
726        self.executed_transaction_digests
727            .contains_key(&(current_epoch - 1, *digest))
728            .expect("db error")
729    }
730
731    // DEPRECATED as the backing table has been moved to authority_per_epoch_store.
732    // Please do not add new accessors/callsites.
733    pub fn get_checkpoint_sequence_number(
734        &self,
735        digest: &TransactionDigest,
736    ) -> SuiResult<Option<(EpochId, CheckpointSequenceNumber)>> {
737        Ok(self.executed_transactions_to_checkpoint.get(digest)?)
738    }
739
740    pub fn set_highest_pruned_checkpoint_without_wb(
741        &self,
742        checkpoint_number: CheckpointSequenceNumber,
743    ) -> SuiResult {
744        let mut wb = self.pruned_checkpoint.batch();
745        self.set_highest_pruned_checkpoint(&mut wb, checkpoint_number)?;
746        wb.write()?;
747        Ok(())
748    }
749
750    pub fn database_is_empty(&self) -> SuiResult<bool> {
751        Ok(self.objects.safe_iter().next().is_none())
752    }
753
754    pub fn iter_live_object_set(&self, include_wrapped_object: bool) -> LiveSetIter<'_> {
755        LiveSetIter {
756            iter: Box::new(self.objects.safe_iter()),
757            tables: self,
758            prev: None,
759            include_wrapped_object,
760        }
761    }
762
763    pub fn range_iter_live_object_set(
764        &self,
765        lower_bound: Option<ObjectID>,
766        upper_bound: Option<ObjectID>,
767        include_wrapped_object: bool,
768    ) -> LiveSetIter<'_> {
769        let lower_bound = lower_bound.as_ref().map(ObjectKey::min_for_id);
770        let upper_bound = upper_bound.as_ref().map(ObjectKey::max_for_id);
771
772        LiveSetIter {
773            iter: Box::new(self.objects.safe_iter_with_bounds(lower_bound, upper_bound)),
774            tables: self,
775            prev: None,
776            include_wrapped_object,
777        }
778    }
779
780    pub fn checkpoint_db(&self, path: &Path) -> SuiResult {
781        // This checkpoints the entire db and not just objects table
782        self.objects.checkpoint_db(path).map_err(Into::into)
783    }
784
785    pub fn insert_root_state_hash(
786        &self,
787        epoch: EpochId,
788        last_checkpoint_of_epoch: CheckpointSequenceNumber,
789        hash: GlobalStateHash,
790    ) -> SuiResult {
791        self.root_state_hash_by_epoch
792            .insert(&epoch, &(last_checkpoint_of_epoch, hash))?;
793        Ok(())
794    }
795
796    pub fn insert_object_test_only(&self, object: Object) -> SuiResult {
797        let object_reference = object.compute_object_reference();
798        let wrapper = get_store_object(object);
799        let mut wb = self.objects.batch();
800        wb.insert_batch(
801            &self.objects,
802            std::iter::once((ObjectKey::from(object_reference), wrapper)),
803        )?;
804        wb.write()?;
805        Ok(())
806    }
807
808    // fallible get object methods for sui-tool, which may need to attempt to read a corrupted database
809    pub fn get_object_fallible(&self, object_id: &ObjectID) -> SuiResult<Option<Object>> {
810        let obj_entry = self
811            .objects
812            .reversed_safe_iter_with_bounds(None, Some(ObjectKey::max_for_id(object_id)))?
813            .next();
814
815        match obj_entry.transpose()? {
816            Some((ObjectKey(obj_id, version), obj)) if obj_id == *object_id => {
817                Ok(self.object(&ObjectKey(obj_id, version), obj)?)
818            }
819            _ => Ok(None),
820        }
821    }
822
823    pub fn get_object_by_key_fallible(
824        &self,
825        object_id: &ObjectID,
826        version: VersionNumber,
827    ) -> SuiResult<Option<Object>> {
828        Ok(self
829            .objects
830            .get(&ObjectKey(*object_id, version))?
831            .and_then(|object| {
832                self.object(&ObjectKey(*object_id, version), object)
833                    .expect("object construction error")
834            }))
835    }
836}
837
838impl ObjectStore for AuthorityPerpetualTables {
839    /// Read an object and return it, or Ok(None) if the object was not found.
840    fn get_object(&self, object_id: &ObjectID) -> Option<Object> {
841        self.get_object_fallible(object_id).expect("db error")
842    }
843
844    fn get_object_by_key(&self, object_id: &ObjectID, version: VersionNumber) -> Option<Object> {
845        self.get_object_by_key_fallible(object_id, version)
846            .expect("db error")
847    }
848}
849
850pub struct LiveSetIter<'a> {
851    iter: DbIterator<'a, (ObjectKey, StoreObjectWrapper)>,
852    tables: &'a AuthorityPerpetualTables,
853    prev: Option<(ObjectKey, StoreObjectWrapper)>,
854    /// Whether a wrapped object is considered as a live object.
855    include_wrapped_object: bool,
856}
857
858#[derive(Eq, PartialEq, Debug, Clone, Deserialize, Serialize, Hash)]
859pub enum LiveObject {
860    Normal(Object),
861    Wrapped(ObjectKey),
862}
863
864impl LiveObject {
865    pub fn object_id(&self) -> ObjectID {
866        match self {
867            LiveObject::Normal(obj) => obj.id(),
868            LiveObject::Wrapped(key) => key.0,
869        }
870    }
871
872    pub fn version(&self) -> SequenceNumber {
873        match self {
874            LiveObject::Normal(obj) => obj.version(),
875            LiveObject::Wrapped(key) => key.1,
876        }
877    }
878
879    pub fn object_reference(&self) -> ObjectRef {
880        match self {
881            LiveObject::Normal(obj) => obj.compute_object_reference(),
882            LiveObject::Wrapped(key) => (key.0, key.1, ObjectDigest::OBJECT_DIGEST_WRAPPED),
883        }
884    }
885}
886
887impl LiveSetIter<'_> {
888    fn store_object_wrapper_to_live_object(
889        &self,
890        object_key: ObjectKey,
891        store_object: StoreObjectWrapper,
892    ) -> Option<LiveObject> {
893        match store_object.migrate().into_inner() {
894            StoreObject::Value(object) => {
895                let object = self
896                    .tables
897                    .construct_object(&object_key, *object)
898                    .expect("Constructing object from store cannot fail");
899                Some(LiveObject::Normal(object))
900            }
901            StoreObject::Wrapped => {
902                if self.include_wrapped_object {
903                    Some(LiveObject::Wrapped(object_key))
904                } else {
905                    None
906                }
907            }
908            StoreObject::Deleted => None,
909        }
910    }
911}
912
913impl Iterator for LiveSetIter<'_> {
914    type Item = LiveObject;
915
916    fn next(&mut self) -> Option<Self::Item> {
917        loop {
918            if let Some(Ok((next_key, next_value))) = self.iter.next() {
919                let prev = self.prev.take();
920                self.prev = Some((next_key, next_value));
921
922                if let Some((prev_key, prev_value)) = prev
923                    && prev_key.0 != next_key.0
924                {
925                    let live_object =
926                        self.store_object_wrapper_to_live_object(prev_key, prev_value);
927                    if live_object.is_some() {
928                        return live_object;
929                    }
930                }
931                continue;
932            }
933            if let Some((key, value)) = self.prev.take() {
934                let live_object = self.store_object_wrapper_to_live_object(key, value);
935                if live_object.is_some() {
936                    return live_object;
937                }
938            }
939            return None;
940        }
941    }
942}
943
944// These functions are used to initialize the DB tables
945#[cfg(not(tidehunter))]
946fn objects_table_config(db_options: DBOptions) -> DBOptions {
947    db_options
948        .optimize_for_write_throughput()
949        .optimize_for_read(read_size_from_env(ENV_VAR_OBJECTS_BLOCK_CACHE_SIZE).unwrap_or(5 * 1024))
950}
951
952#[cfg(not(tidehunter))]
953fn transactions_table_config(db_options: DBOptions) -> DBOptions {
954    db_options
955        .optimize_for_write_throughput()
956        .optimize_for_point_lookup(
957            read_size_from_env(ENV_VAR_TRANSACTIONS_BLOCK_CACHE_SIZE).unwrap_or(512),
958        )
959}
960
961#[cfg(not(tidehunter))]
962fn effects_table_config(db_options: DBOptions) -> DBOptions {
963    db_options
964        .optimize_for_write_throughput()
965        .optimize_for_point_lookup(
966            read_size_from_env(ENV_VAR_EFFECTS_BLOCK_CACHE_SIZE).unwrap_or(1024),
967        )
968}