Skip to main content

iota_core/authority/
authority_store_tables.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{
6    collections::{BTreeSet, HashMap},
7    path::Path,
8    sync::Mutex,
9    time::{Duration, SystemTime, UNIX_EPOCH},
10};
11
12use iota_metrics::spawn_monitored_task;
13use iota_sdk_types::{TransactionEffects, TransactionEvents, Version};
14use iota_types::{global_state_hash::GlobalStateHash, storage::MarkerValue};
15use serde::{Deserialize, Serialize};
16pub use typed_store::DbSnapshot;
17use typed_store::{
18    DBMapUtils, DbIterator,
19    metrics::SamplingInterval,
20    rocks::{
21        DBMap, DBMapTableConfigMap, DBOptions, MetricConf, ReadWriteOptions, default_db_options,
22        read_size_from_env, synced_write_options,
23    },
24    rocksdb::LiveFile,
25    traits::Map,
26};
27
28use super::*;
29use crate::{
30    authority::{
31        authority_store_types::{
32            StoreObject, StoreObjectValueV2, StoreObjectWrapper, get_store_object,
33            try_construct_object,
34        },
35        epoch_markers::EpochMarkers,
36        epoch_start_configuration::EpochStartConfiguration,
37        historic_ledger::HistoricLedger,
38        historic_objects::HistoricObjects,
39        ledger_backlog_migration::LedgerBacklogMigrationProgress,
40        object_backlog_sweep::ObjectBacklogSweepProgress,
41    },
42    epoch_buckets::historic_root,
43};
44
45const ENV_VAR_OBJECTS_BLOCK_CACHE_SIZE: &str = "OBJECTS_BLOCK_CACHE_MB";
46pub(crate) const ENV_VAR_LOCKS_BLOCK_CACHE_SIZE: &str = "LOCKS_BLOCK_CACHE_MB";
47const ENV_VAR_TRANSACTIONS_BLOCK_CACHE_SIZE: &str = "TRANSACTIONS_BLOCK_CACHE_MB";
48const ENV_VAR_EFFECTS_BLOCK_CACHE_SIZE: &str = "EFFECTS_BLOCK_CACHE_MB";
49
50/// The column family an older release keeps its objects pruner's progress
51/// watermark in. Read in place until the first start retires it, see
52/// [`AuthorityPerpetualTables::retire_objects_pruner_watermark`].
53// TODO(https://github.com/iotaledger/iota/issues/12712): remove with the sweep.
54const OBJECTS_PRUNER_WATERMARK_CF: &str = "pruned_checkpoint";
55
56/// Options to apply to every column family of the `perpetual` DB.
57#[derive(Default)]
58pub struct AuthorityPerpetualTablesOptions {
59    /// Whether to enable write stalling on all column families.
60    pub enable_write_stall: bool,
61    /// The directory the historic object and ledger buckets keep their files
62    /// in. Anything opening a node's database must pass the same path the
63    /// node does; unset, it is
64    /// [`HISTORIC_DB_DIR`](crate::epoch_buckets::HISTORIC_DB_DIR) in the
65    /// perpetual database directory.
66    pub historic_db_path: Option<PathBuf>,
67}
68
69impl AuthorityPerpetualTablesOptions {
70    fn apply_to(&self, mut db_options: DBOptions) -> DBOptions {
71        if !self.enable_write_stall {
72            db_options = db_options.disable_write_throttling();
73        }
74        db_options
75    }
76}
77
78/// AuthorityPerpetualTables contains data that must be preserved from one epoch
79/// to the next.
80#[derive(DBMapUtils)]
81pub struct AuthorityPerpetualTables {
82    /// This is a map between the object (ID, version) and the latest state of
83    /// the object, namely the state that is needed to process new
84    /// transactions. State is represented by `StoreObject` enum, which is
85    /// either a move module or a move object.
86    ///
87    /// Note that while this map can store all versions of an object, we will
88    /// eventually prune old object versions from the db.
89    ///
90    /// IMPORTANT: object versions must *only* be pruned if they appear as
91    /// inputs in some TransactionEffects. Simply pruning all objects but
92    /// the most recent is an error! This is because there can be partially
93    /// executed transactions whose effects have not yet been written out,
94    /// and which must be retried. But, they cannot be retried unless their
95    /// input objects are still accessible!
96    ///
97    /// Superseded versions are moved into the [`HistoricObjects`] buckets
98    /// and expire with them; see [`HistoricObjects::prune`].
99    pub(crate) objects: DBMap<ObjectKey, StoreObjectWrapper>,
100
101    /// Object references of currently active objects that can be mutated.
102    pub(crate) live_owned_object_markers: DBMap<ObjectReference, ()>,
103
104    /// This is a map between the transaction digest and the corresponding
105    /// transaction that's known to be executable. This means that it may
106    /// have been executed locally, or it may have been synced through
107    /// state-sync but hasn't been executed yet.
108    ///
109    /// Superseded by [`HistoricLedger`]; the rows still here are read only by
110    /// the one-time migration into the buckets.
111    pub(crate) transactions: DBMap<TransactionDigest, TrustedTransaction>,
112
113    /// A map between the transaction digest of a certificate to the effects of
114    /// its execution. We store effects into this table in two different
115    /// cases:
116    /// 1. When a transaction is synced through state_sync, we store the effects here. These
117    ///    effects are known to be final in the network, but may not have been executed locally
118    ///    yet.
119    /// 2. When the transaction is executed locally on this node, we store the effects here. This
120    ///    means that it's possible to store the same effects twice (once for the synced
121    ///    transaction, and once for the executed).
122    ///
123    /// It's also possible for the effects to be reverted if the transaction
124    /// didn't make it into the epoch.
125    ///
126    /// Superseded by [`HistoricLedger`], like `transactions`.
127    pub(crate) effects: DBMap<TransactionEffectsDigest, TransactionEffects>,
128
129    /// Transactions that have been executed locally on this node. We need this
130    /// table since the `effects` table doesn't say anything about the
131    /// execution status of the transaction on this node. When we wait for
132    /// transactions to be executed, we wait for them to appear in this
133    /// table. When we revert transactions, we remove them from both tables.
134    ///
135    /// Superseded by [`HistoricLedger`], like `transactions`.
136    pub(crate) executed_effects: DBMap<TransactionDigest, TransactionEffectsDigest>,
137
138    /// Events produced by each transaction, keyed by the transaction's
139    /// digest.
140    ///
141    /// Superseded by [`HistoricLedger`], like `transactions`.
142    pub(crate) events_2: DBMap<TransactionDigest, TransactionEvents>,
143
144    /// Epoch and checkpoint of transactions finalized by checkpoint
145    /// executor.
146    ///
147    /// Superseded by [`HistoricLedger`]; the one-time migration into the
148    /// buckets reads the rows still here to learn which epoch each
149    /// transaction belongs to. The value type must stay as it is, or `bcs`
150    /// fails to decode those rows.
151    ///
152    /// Note, there is a table with the same name in
153    /// `AuthorityEpochTables`/`AuthorityPerEpochStore`.
154    pub(crate) executed_transactions_to_checkpoint:
155        DBMap<TransactionDigest, (EpochId, CheckpointSequenceNumber)>,
156
157    // Finalized root state hash for epoch, to be included in CheckpointSummary
158    // of last checkpoint of epoch. These values should only ever be written once
159    // and never changed
160    pub(crate) root_state_hash_by_epoch:
161        DBMap<EpochId, (CheckpointSequenceNumber, GlobalStateHash)>,
162
163    /// Parameters of the system fixed at the epoch start
164    pub(crate) epoch_start_configuration: DBMap<(), EpochStartConfiguration>,
165
166    /// The total IOTA supply and the epoch at which it was stored.
167    /// We check and update it at the end of each epoch if expensive checks are
168    /// enabled.
169    pub(crate) total_iota_supply: DBMap<(), TotalIotaSupplyCheck>,
170
171    /// Expected imbalance between storage fund balance and the sum of storage
172    /// rebate of all live objects. This could be non-zero due to bugs in
173    /// earlier protocol versions. This number is the result of
174    /// storage_fund_balance - sum(storage_rebate).
175    pub(crate) expected_storage_fund_imbalance: DBMap<(), i64>,
176
177    /// Superseded by [`EpochMarkers`]; the rows still here are read only by
178    /// the one-time migration into the buckets.
179    // TODO(https://github.com/iotaledger/iota/issues/12712): remove this table.
180    pub(crate) object_per_epoch_marker_table: DBMap<(EpochId, ObjectKey), MarkerValue>,
181
182    /// How far the one-time sweep of superseded object versions has got
183    /// through `objects`, and whether it is done. Empty until the sweep first
184    /// writes a slice.
185    // TODO(https://github.com/iotaledger/iota/issues/12712): remove this table.
186    pub(crate) object_backlog_sweep_progress: DBMap<(), ObjectBacklogSweepProgress>,
187
188    /// The last checkpoint the objects pruner pruned, copied out of an older
189    /// release's `pruned_checkpoint`; absent if the pruner never ran, and
190    /// until the first start retires that column family. Every version
191    /// superseded at or below it is already deleted, so the object history
192    /// is complete only above it.
193    // TODO(https://github.com/iotaledger/iota/issues/12712): remove this table.
194    pub(crate) object_backlog_sweep_bound: DBMap<(), CheckpointSequenceNumber>,
195
196    /// The epoch whose bucket the one-time sweep moved the superseded
197    /// versions into, written when the sweep finishes. Absent where no sweep
198    /// ran, such as on a database created from genesis or a formal snapshot.
199    // TODO(https://github.com/iotaledger/iota/issues/12712): remove this table.
200    pub(crate) object_backlog_sweep_epoch: DBMap<(), EpochId>,
201
202    /// Which of the flat ledger tables the one-time migration into the
203    /// per-epoch buckets is draining, and how far through it. Empty until the
204    /// migration first writes a slice.
205    // TODO(https://github.com/iotaledger/iota/issues/12763): remove this table.
206    pub(crate) ledger_backlog_migration_progress: DBMap<(), LedgerBacklogMigrationProgress>,
207
208    /// The oldest epoch the one-time ledger migration keeps, as computed by
209    /// the run that started it. Empty until that run writes anything.
210    // TODO(https://github.com/iotaledger/iota/issues/12763): remove this table.
211    pub(crate) ledger_backlog_migration_floor: DBMap<(), EpochId>,
212}
213
214/// The total IOTA supply used during conservation checks.
215#[derive(Debug, Serialize, Deserialize)]
216pub(crate) struct TotalIotaSupplyCheck {
217    /// The IOTA supply at the time of `last_check_epoch`.
218    pub(crate) total_supply: u64,
219    /// The epoch at which the total supply was last checked or updated.
220    pub(crate) last_check_epoch: EpochId,
221}
222
223impl AuthorityPerpetualTablesReadOnly {
224    /// Whether the first start's ledger migration has finished, after which
225    /// the history is in the buckets rather than in the flat tables.
226    // TODO(https://github.com/iotaledger/iota/issues/12763): remove with the migration.
227    pub fn ledger_backlog_migrated(&self) -> Result<bool, TypedStoreError> {
228        self.ledger_backlog_migration_progress
229            .try_catch_up_with_primary()?;
230        Ok(matches!(
231            self.ledger_backlog_migration_progress.get(&())?,
232            Some(LedgerBacklogMigrationProgress::Done | LedgerBacklogMigrationProgress::Cleared)
233        ))
234    }
235}
236
237impl AuthorityPerpetualTables {
238    pub fn path(parent_path: &Path) -> PathBuf {
239        parent_path.join("perpetual")
240    }
241
242    pub fn open(
243        parent_path: &Path,
244        db_options_override: Option<AuthorityPerpetualTablesOptions>,
245    ) -> Self {
246        Self::open_with_db_options(parent_path, db_options_override).0
247    }
248
249    /// Opens the perpetual tables together with the historic object, ledger
250    /// and epoch marker buckets, which are column families of the same
251    /// database.
252    pub fn open_with_historic_objects(
253        parent_path: &Path,
254        db_options_override: Option<AuthorityPerpetualTablesOptions>,
255    ) -> Result<(Self, HistoricObjects, HistoricLedger, EpochMarkers), TypedStoreError> {
256        let (tables, db_options, ledger_db_options, historic_root) =
257            Self::open_with_db_options(parent_path, db_options_override);
258        let mut historic_objects = HistoricObjects::open(
259            tables.objects.db.clone(),
260            &db_options,
261            tables.objects.clone(),
262            &historic_root,
263        )?;
264        historic_objects.objects_pruned_through = tables.objects_pruner_watermark()?;
265        if let Some(epoch) = tables.object_backlog_sweep_epoch.get(&())? {
266            historic_objects.record_backlog_sweep_epoch(epoch);
267        }
268        if matches!(
269            tables.object_backlog_sweep_progress.get(&())?,
270            Some(ObjectBacklogSweepProgress::Done)
271        ) {
272            historic_objects.mark_backlog_swept();
273        }
274        let historic_ledger = HistoricLedger::open(
275            tables.objects.db.clone(),
276            &ledger_db_options,
277            &historic_root,
278        )?;
279        let epoch_markers = EpochMarkers::open(tables.objects.db.clone(), &db_options)?;
280        Ok((tables, historic_objects, historic_ledger, epoch_markers))
281    }
282
283    /// The perpetual tables, the base options their column families were
284    /// opened with, the base options of the historic ledger buckets, and the
285    /// root of the historic buckets.
286    fn open_with_db_options(
287        parent_path: &Path,
288        db_options_override: Option<AuthorityPerpetualTablesOptions>,
289    ) -> (Self, DBOptions, DBOptions, PathBuf) {
290        let db_options_override = db_options_override.unwrap_or_default();
291        let db_options =
292            db_options_override.apply_to(default_db_options().optimize_db_for_write_throughput(4));
293        let path = Self::path(parent_path);
294        let historic_root = historic_root(&path, db_options_override.historic_db_path.as_deref());
295        let ledger_db_options = historic_ledger_db_options(&db_options);
296        let table_options =
297            Self::table_options(&path, &db_options, &ledger_db_options, &historic_root);
298        let tables = Self::open_tables_read_write(
299            path,
300            MetricConf::new("perpetual")
301                .with_sampling(SamplingInterval::new(Duration::from_secs(60), 0)),
302            Some(db_options.options.clone()),
303            Some(table_options),
304        );
305        (tables, db_options, ledger_db_options, historic_root)
306    }
307
308    /// The options of the column families of the perpetual database at
309    /// `path` that do not take `db_options` unchanged, including the historic
310    /// buckets. The historic ledger buckets take `ledger_db_options`, from
311    /// [`historic_ledger_db_options`]. Every open of the database must pass
312    /// these.
313    fn table_options(
314        path: &Path,
315        db_options: &DBOptions,
316        ledger_db_options: &DBOptions,
317        historic_root: &Path,
318    ) -> DBMapTableConfigMap {
319        let mut table_options = BTreeMap::from([
320            (
321                "objects".to_string(),
322                objects_table_config(db_options.clone()),
323            ),
324            (
325                "live_owned_object_markers".to_string(),
326                live_owned_object_markers_table_config(db_options.clone()),
327            ),
328            (
329                "transactions".to_string(),
330                transactions_table_config(db_options.clone()),
331            ),
332            (
333                "effects".to_string(),
334                effects_table_config(db_options.clone()),
335            ),
336        ]);
337        table_options.extend(HistoricObjects::extra_column_family_options(
338            path,
339            db_options,
340            historic_root,
341        ));
342        table_options.extend(HistoricLedger::extra_column_family_options(
343            path,
344            ledger_db_options,
345            historic_root,
346        ));
347        table_options.extend(EpochMarkers::extra_column_family_options(path, db_options));
348        DBMapTableConfigMap::new(table_options)
349    }
350
351    /// Opens the tables read-only. `historic_db_path` is the root of the
352    /// historic buckets, as for [`AuthorityPerpetualTablesOptions`].
353    pub fn open_readonly(
354        parent_path: &Path,
355        historic_db_path: Option<&Path>,
356    ) -> AuthorityPerpetualTablesReadOnly {
357        let path = Self::path(parent_path);
358        let db_options = default_db_options().optimize_db_for_write_throughput(4);
359        let table_options = Self::table_options(
360            &path,
361            &db_options,
362            &historic_ledger_db_options(&db_options),
363            &historic_root(&path, historic_db_path),
364        );
365        Self::get_read_only_handle_with_table_options(
366            path,
367            None,
368            None,
369            MetricConf::new("perpetual_readonly"),
370            table_options,
371        )
372    }
373
374    /// The newest row for `object_id` at or below `version`, still wrapped.
375    ///
376    /// A `Deleted` or `Wrapped` row means the object was deleted or wrapped
377    /// at or below the bound, which callers must not treat like `None` (no
378    /// row in range). Use [`Self::object`] to resolve a row.
379    pub fn find_object_lt_or_eq_version(
380        &self,
381        object_id: ObjectId,
382        version: Version,
383    ) -> Result<Option<(ObjectKey, StoreObjectWrapper)>, IotaError> {
384        let mut iter = self.objects.safe_range_iter_reversed(
385            ObjectKey::min_for_id(&object_id)..=ObjectKey(object_id, version),
386        );
387        match iter.next() {
388            // Migrate legacy V1 rows before returning; callers inspect the
389            // wrapper via `inner()`, which panics on an un-migrated V1.
390            Some(Ok((key, o))) => Ok(Some((key, o.migrate()))),
391            Some(Err(e)) => Err(e.into()),
392            None => Ok(None),
393        }
394    }
395
396    fn construct_object(
397        &self,
398        object_key: &ObjectKey,
399        store_object: StoreObjectValueV2,
400    ) -> Result<Object, IotaError> {
401        try_construct_object(object_key, store_object)
402    }
403
404    // Constructs `iota_types::object::Object` from `StoreObjectWrapper`.
405    // Returns `None` if object was deleted/wrapped
406    pub fn object(
407        &self,
408        object_key: &ObjectKey,
409        store_object: StoreObjectWrapper,
410    ) -> Result<Option<Object>, IotaError> {
411        let StoreObject::Value(store_object) = store_object.migrate().into_inner() else {
412            return Ok(None);
413        };
414        Ok(Some(self.construct_object(object_key, *store_object)?))
415    }
416
417    pub fn object_reference(
418        &self,
419        object_key: &ObjectKey,
420        store_object: StoreObjectWrapper,
421    ) -> Result<ObjectReference, IotaError> {
422        let obj_ref = match store_object.migrate().into_inner() {
423            StoreObject::Value(object) => self.construct_object(object_key, *object)?.object_ref(),
424            StoreObject::Deleted => {
425                ObjectReference::new(object_key.0, object_key.1, ObjectDigest::OBJECT_DELETED)
426            }
427            StoreObject::Wrapped => {
428                ObjectReference::new(object_key.0, object_key.1, ObjectDigest::OBJECT_WRAPPED)
429            }
430        };
431        Ok(obj_ref)
432    }
433
434    pub fn tombstone_reference(
435        &self,
436        object_key: &ObjectKey,
437        store_object: &StoreObjectWrapper,
438    ) -> Result<Option<ObjectReference>, IotaError> {
439        let obj_ref = match store_object.inner() {
440            StoreObject::Deleted => Some(ObjectReference::new(
441                object_key.0,
442                object_key.1,
443                ObjectDigest::OBJECT_DELETED,
444            )),
445            StoreObject::Wrapped => Some(ObjectReference::new(
446                object_key.0,
447                object_key.1,
448                ObjectDigest::OBJECT_WRAPPED,
449            )),
450            _ => None,
451        };
452        Ok(obj_ref)
453    }
454
455    pub fn get_latest_object_ref_or_tombstone(
456        &self,
457        object_id: ObjectId,
458    ) -> Result<Option<ObjectReference>, IotaError> {
459        let mut iterator = self.objects.safe_iter_with_prefix_reversed(&object_id);
460
461        if let Some(Ok((object_key, value))) = iterator.next() {
462            if object_key.0 == object_id {
463                return Ok(Some(self.object_reference(&object_key, value)?));
464            }
465        }
466        Ok(None)
467    }
468
469    pub fn get_latest_object_or_tombstone(
470        &self,
471        object_id: ObjectId,
472    ) -> Result<Option<(ObjectKey, StoreObjectWrapper)>, IotaError> {
473        let mut iterator = self.objects.safe_iter_with_prefix_reversed(&object_id);
474
475        if let Some(Ok((object_key, value))) = iterator.next() {
476            if object_key.0 == object_id {
477                // Migrate legacy V1 rows before returning; callers inspect the
478                // wrapper via `inner()`, which panics on an un-migrated V1.
479                return Ok(Some((object_key, value.migrate())));
480            }
481        }
482        Ok(None)
483    }
484
485    pub fn get_recovery_epoch_at_restart(&self) -> IotaResult<EpochId> {
486        Ok(self
487            .epoch_start_configuration
488            .get(&())?
489            .expect("Must have current epoch.")
490            .epoch_start_state()
491            .epoch())
492    }
493
494    pub fn set_epoch_start_configuration(
495        &self,
496        epoch_start_configuration: &EpochStartConfiguration,
497    ) -> IotaResult {
498        let mut wb = self.epoch_start_configuration.batch();
499        wb.insert_batch(
500            &self.epoch_start_configuration,
501            std::iter::once(((), epoch_start_configuration)),
502        )?;
503        wb.write()?;
504        Ok(())
505    }
506
507    pub fn get_newer_object_keys(
508        &self,
509        object: &(ObjectId, Version),
510    ) -> IotaResult<Vec<ObjectKey>> {
511        let mut objects = vec![];
512        for result in self.objects.safe_iter_with_prefix_from(
513            &object.0,
514            std::ops::Bound::Included(&object.1.next().unwrap()),
515        ) {
516            let (key, _) = result?;
517            objects.push(key);
518        }
519        Ok(objects)
520    }
521
522    pub fn database_is_empty(&self) -> IotaResult<bool> {
523        Ok(self.objects.safe_iter().next().is_none())
524    }
525
526    /// A point-in-time snapshot of the perpetual store, for a scan that must
527    /// see one state while the node keeps executing. See [`DbSnapshot`] for
528    /// what holding one costs.
529    pub fn db_snapshot(&self) -> DbSnapshot<'_> {
530        self.objects.db.snapshot()
531    }
532
533    /// [`Self::iter_live_object_set`] reading as of `db_snapshot`.
534    pub fn iter_live_object_set_at<'a>(
535        &'a self,
536        db_snapshot: &'a DbSnapshot<'a>,
537    ) -> LiveSetIter<'a> {
538        LiveSetIter {
539            iter: Box::new(self.objects.safe_iter_at_snapshot(db_snapshot)),
540            tables: self,
541            prev: None,
542        }
543    }
544
545    pub fn iter_live_object_set(&self) -> LiveSetIter<'_> {
546        LiveSetIter {
547            iter: Box::new(self.objects.safe_iter()),
548            tables: self,
549            prev: None,
550        }
551    }
552
553    pub fn range_iter_live_object_set(
554        &self,
555        lower_bound: Option<ObjectId>,
556        upper_bound: Option<ObjectId>,
557    ) -> LiveSetIter<'_> {
558        let lower_bound = lower_bound.as_ref().map(ObjectKey::min_for_id);
559        let upper_bound = upper_bound.as_ref().map(ObjectKey::max_for_id);
560
561        LiveSetIter {
562            iter: Box::new(self.objects.safe_iter_with_bounds(lower_bound, upper_bound)),
563            tables: self,
564            prev: None,
565        }
566    }
567
568    /// Compacts the whole key range of the live `objects` table, blocking
569    /// until RocksDB has rewritten it.
570    pub fn compact(&self) -> Result<(), TypedStoreError> {
571        self.objects.compact_range(
572            &ObjectKey(ObjectId::ZERO, Version::MIN_VALID_INCL),
573            &ObjectKey(ObjectId::MAX, Version::MAX_VALID_EXCL),
574        )
575    }
576
577    /// The column families whose aged SST files
578    /// [`Self::spawn_periodic_compaction`] rewrites: the ones rows are
579    /// deleted from.
580    fn periodically_compacted_tables(&self) -> BTreeSet<&str> {
581        [
582            self.objects.cf_name(),
583            self.transactions.cf_name(),
584            self.effects.cf_name(),
585            self.executed_effects.cf_name(),
586            self.events_2.cf_name(),
587            self.executed_transactions_to_checkpoint.cf_name(),
588        ]
589        .into_iter()
590        .collect()
591    }
592
593    /// Compacts and returns the largest SST file of
594    /// [`Self::periodically_compacted_tables`] untouched for `delay_days`, or
595    /// `None` when no file qualifies. `last_processed` must be carried from one
596    /// call to the next so that a file is not picked again within the delay.
597    ///
598    /// Blocks until the compaction finishes, so an async caller must use
599    /// `spawn_blocking`.
600    fn compact_next_sst_file(
601        &self,
602        delay_days: usize,
603        last_processed: &Mutex<HashMap<String, SystemTime>>,
604    ) -> Result<Option<LiveFile>, anyhow::Error> {
605        let compacted_tables = self.periodically_compacted_tables();
606        let db_path = self.objects.db.path_for_pruning();
607        let mut state = last_processed
608            .lock()
609            .expect("failed to obtain a lock for last processed SST files");
610        let mut sst_file_for_compaction: Option<LiveFile> = None;
611        let time_threshold =
612            SystemTime::now() - Duration::from_secs(delay_days as u64 * 24 * 60 * 60);
613        for sst_file in self.objects.db.live_files()? {
614            // Checked first: the historic buckets keep their files outside
615            // `db_path`.
616            if !compacted_tables.contains(sst_file.column_family_name.as_str()) {
617                continue;
618            }
619            let file_path = db_path.join(sst_file.name.clone().trim_matches('/'));
620            let last_modified = std::fs::metadata(file_path)?.modified()?;
621            if sst_file.level < 1
622                || sst_file.start_key.is_none()
623                || sst_file.end_key.is_none()
624                || last_modified > time_threshold
625                || state.get(&sst_file.name).unwrap_or(&UNIX_EPOCH) > &time_threshold
626            {
627                continue;
628            }
629            if let Some(candidate) = &sst_file_for_compaction {
630                if candidate.size > sst_file.size {
631                    continue;
632                }
633            }
634            sst_file_for_compaction = Some(sst_file);
635        }
636        let Some(sst_file) = sst_file_for_compaction else {
637            return Ok(None);
638        };
639        info!(
640            "Manual compaction of sst file {:?}. Size: {:?}, level: {:?}",
641            sst_file.name, sst_file.size, sst_file.level
642        );
643        self.objects.compact_range_raw(
644            &sst_file.column_family_name,
645            sst_file.start_key.clone().unwrap(),
646            sst_file.end_key.clone().unwrap(),
647        )?;
648        state.insert(sst_file.name.clone(), SystemTime::now());
649        Ok(Some(sst_file))
650    }
651
652    /// Spawns a task that keeps compacting SST files older than `delay_days`,
653    /// one at a time, until these tables are dropped. RocksDB does not compact
654    /// files that are no longer written to, so their deleted rows are not
655    /// reclaimed otherwise.
656    pub fn spawn_periodic_compaction(self: &Arc<Self>, delay_days: usize) {
657        // Held weakly so the task cannot keep a dropped node's database open.
658        let perpetual_tables = Arc::downgrade(self);
659        spawn_monitored_task!(async move {
660            let last_processed = Arc::new(Mutex::new(HashMap::new()));
661            loop {
662                let Some(tables) = perpetual_tables.upgrade() else {
663                    break;
664                };
665                let state = last_processed.clone();
666                let result = tokio::task::spawn_blocking(move || {
667                    tables.compact_next_sst_file(delay_days, &state)
668                })
669                .await;
670                let mut sleep_interval_secs = 1;
671                match result {
672                    Err(err) => error!("Failed to compact sst file: {:?}", err),
673                    Ok(Err(err)) => error!("Failed to compact sst file: {:?}", err),
674                    Ok(Ok(None)) => {
675                        sleep_interval_secs = 3600;
676                    }
677                    _ => {}
678                }
679                tokio::time::sleep(Duration::from_secs(sleep_interval_secs)).await;
680            }
681        });
682    }
683
684    pub fn get_root_state_hash(
685        &self,
686        epoch: EpochId,
687    ) -> IotaResult<Option<(CheckpointSequenceNumber, GlobalStateHash)>> {
688        Ok(self.root_state_hash_by_epoch.get(&epoch)?)
689    }
690
691    pub fn insert_root_state_hash(
692        &self,
693        epoch: EpochId,
694        last_checkpoint_of_epoch: CheckpointSequenceNumber,
695        hash: GlobalStateHash,
696    ) -> IotaResult {
697        self.root_state_hash_by_epoch
698            .insert(&epoch, &(last_checkpoint_of_epoch, hash))?;
699        Ok(())
700    }
701
702    /// The objects pruner's progress watermark: the copy in
703    /// `object_backlog_sweep_bound`, or, before the first start has retired
704    /// it, the older release's own.
705    fn objects_pruner_watermark(
706        &self,
707    ) -> Result<Option<CheckpointSequenceNumber>, TypedStoreError> {
708        if let Some(watermark) = self.object_backlog_sweep_bound.get(&())? {
709            return Ok(Some(watermark));
710        }
711        let db = &self.objects.db;
712        if db.cf_handle(OBJECTS_PRUNER_WATERMARK_CF).is_none() {
713            return Ok(None);
714        }
715        DBMap::<(), CheckpointSequenceNumber>::reopen(
716            db,
717            Some(OBJECTS_PRUNER_WATERMARK_CF),
718            &ReadWriteOptions::default(),
719            true,
720        )?
721        .get(&())
722    }
723
724    /// Copies the objects pruner's progress watermark out of the older
725    /// release's column family into `object_backlog_sweep_bound`, then drops
726    /// that column family. Does nothing on a database without it.
727    ///
728    /// One-way: the older release reads that column family, so call this only
729    /// once nothing can refuse the first start any more.
730    // TODO(https://github.com/iotaledger/iota/issues/12712): remove with the sweep.
731    pub fn retire_objects_pruner_watermark(&self) -> Result<(), TypedStoreError> {
732        let db = &self.objects.db;
733        if db.cf_handle(OBJECTS_PRUNER_WATERMARK_CF).is_none() {
734            return Ok(());
735        }
736        let pruned: DBMap<(), CheckpointSequenceNumber> = DBMap::reopen(
737            db,
738            Some(OBJECTS_PRUNER_WATERMARK_CF),
739            &ReadWriteOptions::default(),
740            true,
741        )?;
742        if let Some(watermark) = pruned.get(&())? {
743            // Synced: the drop below is durable at once, so a lost copy would
744            // lose the watermark for good.
745            let mut batch = self.object_backlog_sweep_bound.batch();
746            batch.insert_batch(&self.object_backlog_sweep_bound, [((), watermark)])?;
747            batch.write_opt(&synced_write_options())?;
748            info!(watermark, "carried the objects pruner's watermark over");
749        }
750        drop(pruned);
751        db.drop_cf(OBJECTS_PRUNER_WATERMARK_CF)
752    }
753
754    /// Marks the one-time object-backlog sweep as done, so that a later node
755    /// start skips it.
756    ///
757    /// Call this only on a database that holds no superseded object
758    /// versions, such as one just restored from a formal snapshot.
759    pub fn mark_object_backlog_swept(&self) -> IotaResult {
760        self.object_backlog_sweep_progress
761            .insert(&(), &ObjectBacklogSweepProgress::Done)?;
762        Ok(())
763    }
764
765    /// Marks the one-time object-backlog sweep as done after it moved the
766    /// superseded versions into `epoch`'s bucket, so that a later node start
767    /// skips it.
768    pub fn mark_object_backlog_swept_into(&self, epoch: EpochId) -> IotaResult {
769        let mut batch = self.object_backlog_sweep_progress.batch();
770        batch.insert_batch(&self.object_backlog_sweep_epoch, [((), epoch)])?;
771        batch.insert_batch(
772            &self.object_backlog_sweep_progress,
773            [((), ObjectBacklogSweepProgress::Done)],
774        )?;
775        batch.write()?;
776        Ok(())
777    }
778
779    /// Marks the one-time object-backlog sweep as skipped, so that a later
780    /// node start does not run it, on a database that may still hold
781    /// superseded versions in the live `objects` table. Reads keep checking
782    /// the historic buckets behind such a version.
783    pub fn mark_object_backlog_sweep_skipped(&self) -> IotaResult {
784        self.object_backlog_sweep_progress
785            .insert(&(), &ObjectBacklogSweepProgress::Skipped)?;
786        Ok(())
787    }
788
789    /// Marks the one-time migration of the flat ledger tables into the
790    /// per-epoch buckets as done, so that a later node start skips it.
791    ///
792    /// Call this only on a database with no rows in the flat ledger tables,
793    /// such as one just restored from a formal snapshot.
794    // TODO(https://github.com/iotaledger/iota/issues/12763): remove with the migration.
795    pub fn mark_ledger_backlog_migrated(&self) -> IotaResult {
796        self.ledger_backlog_migration_progress
797            .insert(&(), &LedgerBacklogMigrationProgress::Cleared)?;
798        Ok(())
799    }
800
801    pub fn insert_store_object_v1_test_only(&self, object: Object) -> IotaResult {
802        use crate::authority::authority_store_types::{StoreObjectV1, StoreObjectValue};
803
804        let object_reference = object.object_ref();
805        let v2_value = match get_store_object(object, None).into_inner() {
806            StoreObject::Value(v) => *v,
807            other => unreachable!("get_store_object must produce a Value variant, got {other:?}"),
808        };
809        let v1_value = StoreObjectValue {
810            data: v2_value.data,
811            owner: v2_value.owner,
812            previous_transaction: v2_value.previous_transaction,
813            storage_rebate: v2_value.storage_rebate,
814        };
815        let wrapper = StoreObjectWrapper::V1(StoreObjectV1::Value(Box::new(v1_value)));
816
817        let mut wb = self.objects.batch();
818        wb.insert_batch(
819            &self.objects,
820            std::iter::once((ObjectKey::from(object_reference), wrapper)),
821        )?;
822        wb.write()?;
823        Ok(())
824    }
825
826    pub fn insert_store_object_v2_test_only(
827        &self,
828        object: Object,
829        previous_transaction_checkpoint: Option<CheckpointSequenceNumber>,
830    ) -> IotaResult {
831        let object_reference = object.object_ref();
832        let wrapper = get_store_object(object, previous_transaction_checkpoint);
833
834        let mut wb = self.objects.batch();
835        wb.insert_batch(
836            &self.objects,
837            std::iter::once((ObjectKey::from(object_reference), wrapper)),
838        )?;
839        wb.write()?;
840        Ok(())
841    }
842}
843
844impl ObjectStore for AuthorityPerpetualTables {
845    /// Read an object and return it, or Ok(None) if the object was not found.
846    fn try_get_object(
847        &self,
848        object_id: &ObjectId,
849    ) -> Result<Option<Object>, iota_types::storage::error::Error> {
850        let obj_entry = self
851            .objects
852            .safe_iter_with_prefix_reversed(object_id)
853            .next();
854
855        match obj_entry.transpose()? {
856            Some((ObjectKey(obj_id, version), obj)) if obj_id == *object_id => Ok(self
857                .object(&ObjectKey(obj_id, version), obj)
858                .map_err(iota_types::storage::error::Error::custom)?),
859            _ => Ok(None),
860        }
861    }
862
863    fn try_get_object_by_key(
864        &self,
865        object_id: &ObjectId,
866        version: VersionNumber,
867    ) -> Result<Option<Object>, iota_types::storage::error::Error> {
868        Ok(self
869            .objects
870            .get(&ObjectKey(*object_id, version))
871            .map_err(iota_types::storage::error::Error::custom)?
872            .map(|object| self.object(&ObjectKey(*object_id, version), object))
873            .transpose()
874            .map_err(iota_types::storage::error::Error::custom)?
875            .flatten())
876    }
877}
878
879/// In-process iterator item for a live object together with the checkpoint
880/// sequence number that contained the transaction whose effects produced this
881/// object version. Yielded by [`LiveSetIter`].
882///
883/// `previous_transaction_checkpoint` is `Option`: production write paths
884/// always produce `Some(seq)`, but `LiveSetIter` will yield `None` for rows
885/// lifted from a pre-V2 on-disk format (the checkpoint was never recorded and
886/// is unrecoverable).
887#[derive(Eq, PartialEq, Debug, Clone, Hash)]
888pub struct LiveObject {
889    pub object: Object,
890    pub previous_transaction_checkpoint: Option<CheckpointSequenceNumber>,
891}
892
893impl LiveObject {
894    pub fn object_id(&self) -> ObjectId {
895        self.object.id()
896    }
897
898    pub fn version(&self) -> Version {
899        self.object.version()
900    }
901
902    pub fn object_reference(&self) -> ObjectReference {
903        self.object.object_ref()
904    }
905}
906
907/// On-disk record format for a live object as emitted into snapshot V2 `.obj`
908/// files (`iota-snapshot::writer::write_object`) and decoded by
909/// `iota-snapshot::reader::LiveObjectIter`.
910#[derive(Deserialize, Serialize)]
911pub struct SnapshotLiveObject {
912    pub object: Object,
913    pub previous_transaction_checkpoint: CheckpointSequenceNumber,
914}
915
916impl From<SnapshotLiveObject> for LiveObject {
917    fn from(snap: SnapshotLiveObject) -> Self {
918        let SnapshotLiveObject {
919            object,
920            previous_transaction_checkpoint,
921        } = snap;
922        LiveObject {
923            object,
924            previous_transaction_checkpoint: Some(previous_transaction_checkpoint),
925        }
926    }
927}
928
929/// Yields the latest live version of every object in range, surfacing a read
930/// error instead of ending the scan.
931pub struct LiveSetIter<'a> {
932    iter: DbIterator<'a, (ObjectKey, StoreObjectWrapper)>,
933    tables: &'a AuthorityPerpetualTables,
934    prev: Option<(ObjectKey, StoreObjectWrapper)>,
935}
936
937impl LiveSetIter<'_> {
938    fn store_object_wrapper_to_live_object(
939        &self,
940        object_key: ObjectKey,
941        store_object: StoreObjectWrapper,
942    ) -> Option<LiveObject> {
943        match store_object.migrate().into_inner() {
944            StoreObject::Value(value) => {
945                let previous_transaction_checkpoint = value.previous_transaction_checkpoint;
946                let object = self
947                    .tables
948                    .construct_object(&object_key, *value)
949                    .expect("Constructing object from store cannot fail");
950                Some(LiveObject {
951                    object,
952                    previous_transaction_checkpoint,
953                })
954            }
955            StoreObject::Wrapped | StoreObject::Deleted => None,
956        }
957    }
958}
959
960impl Iterator for LiveSetIter<'_> {
961    type Item = Result<LiveObject, TypedStoreError>;
962
963    fn next(&mut self) -> Option<Self::Item> {
964        loop {
965            match self.iter.next() {
966                Some(Ok((next_key, next_value))) => {
967                    let prev = self.prev.take();
968                    self.prev = Some((next_key, next_value));
969
970                    if let Some((prev_key, prev_value)) = prev {
971                        if prev_key.0 != next_key.0 {
972                            if let Some(live_object) =
973                                self.store_object_wrapper_to_live_object(prev_key, prev_value)
974                            {
975                                return Some(Ok(live_object));
976                            }
977                        }
978                    }
979                }
980                Some(Err(err)) => {
981                    // The buffered row may not be the object's latest version, so drop
982                    // it rather than emit it as the tail of a scan that failed.
983                    self.prev = None;
984                    return Some(Err(err));
985                }
986                None => {
987                    if let Some((key, value)) = self.prev.take() {
988                        if let Some(live_object) =
989                            self.store_object_wrapper_to_live_object(key, value)
990                        {
991                            return Some(Ok(live_object));
992                        }
993                    }
994                    return None;
995                }
996            }
997        }
998    }
999}
1000
1001// These functions are used to initialize the DB tables
1002fn live_owned_object_markers_table_config(db_options: DBOptions) -> DBOptions {
1003    DBOptions {
1004        options: db_options
1005            .clone()
1006            .optimize_for_write_throughput()
1007            .optimize_for_read(read_size_from_env(ENV_VAR_LOCKS_BLOCK_CACHE_SIZE).unwrap_or(1024))
1008            .options,
1009        rw_options: db_options.rw_options,
1010    }
1011}
1012
1013fn objects_table_config(db_options: DBOptions) -> DBOptions {
1014    db_options
1015        .optimize_for_write_throughput()
1016        .optimize_for_read(read_size_from_env(ENV_VAR_OBJECTS_BLOCK_CACHE_SIZE).unwrap_or(5 * 1024))
1017}
1018
1019fn transactions_table_config(db_options: DBOptions) -> DBOptions {
1020    db_options
1021        .optimize_for_write_throughput()
1022        .optimize_for_point_lookup(
1023            read_size_from_env(ENV_VAR_TRANSACTIONS_BLOCK_CACHE_SIZE).unwrap_or(512),
1024        )
1025}
1026
1027fn effects_table_config(db_options: DBOptions) -> DBOptions {
1028    db_options
1029        .optimize_for_write_throughput()
1030        .optimize_for_point_lookup(
1031            read_size_from_env(ENV_VAR_EFFECTS_BLOCK_CACHE_SIZE).unwrap_or(1024),
1032        )
1033}
1034
1035/// `db_options` with one block cache that every historic ledger bucket opened
1036/// from them shares, as large as the transactions and effects caches together,
1037/// since the buckets hold what those tables held. Build them once per open of
1038/// the database: each call makes a new cache.
1039fn historic_ledger_db_options(db_options: &DBOptions) -> DBOptions {
1040    db_options.clone().set_block_options(
1041        read_size_from_env(ENV_VAR_TRANSACTIONS_BLOCK_CACHE_SIZE).unwrap_or(512)
1042            + read_size_from_env(ENV_VAR_EFFECTS_BLOCK_CACHE_SIZE).unwrap_or(1024),
1043        16 << 10,
1044    )
1045}
1046
1047#[cfg(test)]
1048mod tests {
1049    use super::*;
1050    use crate::authority::authority_store_types::StoreObjectV2;
1051
1052    /// A scan through a database snapshot sees the object set as it stood
1053    /// when the snapshot was taken, not writes made afterwards.
1054    #[tokio::test]
1055    async fn a_db_snapshot_scan_is_unchanged_by_later_writes() {
1056        let tmp_dir = iota_common::tempdir();
1057        let perpetual_db = AuthorityPerpetualTables::open(tmp_dir.path(), None);
1058
1059        let kept_id = ObjectId::random();
1060        let deleted_id = ObjectId::random();
1061        for id in [kept_id, deleted_id] {
1062            perpetual_db
1063                .insert_store_object_v2_test_only(
1064                    Object::immutable_with_id_for_testing(id),
1065                    Some(0),
1066                )
1067                .unwrap();
1068        }
1069        let live_set = |tables: &AuthorityPerpetualTables| -> Vec<(ObjectId, Version)> {
1070            tables
1071                .iter_live_object_set()
1072                .map(|live| live.map(|live| (live.object_id(), live.version())))
1073                .collect::<Result<_, _>>()
1074                .unwrap()
1075        };
1076        let before = live_set(&perpetual_db);
1077        assert_eq!(before.len(), 2);
1078
1079        let db_snapshot = perpetual_db.db_snapshot();
1080
1081        // What a running node does to the table while a scan is in flight: a
1082        // new object appears, and another is tombstoned.
1083        perpetual_db
1084            .insert_store_object_v2_test_only(
1085                Object::immutable_with_id_for_testing(ObjectId::random()),
1086                Some(1),
1087            )
1088            .unwrap();
1089        let mut wb = perpetual_db.objects.batch();
1090        wb.insert_batch(
1091            &perpetual_db.objects,
1092            std::iter::once::<(ObjectKey, StoreObjectWrapper)>((
1093                ObjectKey(deleted_id, Version::from_u64(2)),
1094                StoreObjectV2::Deleted.into(),
1095            )),
1096        )
1097        .unwrap();
1098        wb.write().unwrap();
1099
1100        let through_view: Vec<_> = perpetual_db
1101            .iter_live_object_set_at(&db_snapshot)
1102            .map(|live| live.map(|live| (live.object_id(), live.version())))
1103            .collect::<Result<_, _>>()
1104            .unwrap();
1105        assert_eq!(
1106            through_view, before,
1107            "the snapshot must not show writes made after it was taken"
1108        );
1109
1110        // Without this the test would also pass against a snapshot that saw
1111        // everything, or one that saw nothing.
1112        assert_ne!(
1113            live_set(&perpetual_db),
1114            before,
1115            "a scan outside the snapshot must show the later writes"
1116        );
1117    }
1118
1119    /// `LiveSetIter` must filter `StoreObject::Wrapped` and
1120    /// `StoreObject::Deleted` rows at the source so downstream consumers
1121    /// (snapshot writer, state-hash accumulator, restore path) only ever
1122    /// observe live objects.
1123    #[tokio::test]
1124    async fn live_set_iter_filters_wrapped_and_deleted_store_rows() {
1125        let tmp_dir = iota_common::tempdir();
1126        let perpetual_db = AuthorityPerpetualTables::open(tmp_dir.path(), None);
1127
1128        // A live `Normal` row alongside `Wrapped` and `Deleted` tombstones for
1129        // distinct object IDs.
1130        let live_id = ObjectId::random();
1131        let wrapped_id = ObjectId::random();
1132        let deleted_id = ObjectId::random();
1133
1134        let live_object = Object::immutable_with_id_for_testing(live_id);
1135        perpetual_db
1136            .insert_store_object_v2_test_only(live_object, None)
1137            .unwrap();
1138
1139        let mut wb = perpetual_db.objects.batch();
1140        let wrapped_key = ObjectKey(wrapped_id, Version::from_u64(1));
1141        wb.insert_batch(
1142            &perpetual_db.objects,
1143            std::iter::once::<(ObjectKey, StoreObjectWrapper)>((
1144                wrapped_key,
1145                StoreObjectV2::Wrapped.into(),
1146            )),
1147        )
1148        .unwrap();
1149        let deleted_key = ObjectKey(deleted_id, Version::from_u64(1));
1150        wb.insert_batch(
1151            &perpetual_db.objects,
1152            std::iter::once::<(ObjectKey, StoreObjectWrapper)>((
1153                deleted_key,
1154                StoreObjectV2::Deleted.into(),
1155            )),
1156        )
1157        .unwrap();
1158        wb.write().unwrap();
1159
1160        let yielded: Vec<_> = perpetual_db
1161            .iter_live_object_set()
1162            .collect::<Result<_, _>>()
1163            .unwrap();
1164        assert_eq!(yielded.len(), 1, "wrapped/deleted rows must be filtered");
1165        assert_eq!(yielded[0].object.id(), live_id);
1166    }
1167
1168    /// `LiveSetIter` must surface the exact `previous_transaction_checkpoint`
1169    /// stored on `StoreObjectValueV2` - it is the load-bearing input to each
1170    /// `LiveObject` record the snapshot V2 writer emits into `.obj` files
1171    /// (and, on restore, to the `previous_transaction_checkpoint` field
1172    /// stamped onto `StoreObjectV2` via `bulk_insert_live_objects`). A bug
1173    /// that, e.g., always stamped `0` here would silently corrupt every
1174    /// snapshot's per-object record; this is the focused canary for that
1175    /// contract.
1176    #[tokio::test]
1177    async fn live_set_iter_propagates_previous_transaction_checkpoint() {
1178        let tmp_dir = iota_common::tempdir();
1179        let perpetual_db = AuthorityPerpetualTables::open(tmp_dir.path(), None);
1180
1181        // Insert a live object with a distinct, recognizable checkpoint.
1182        let object = Object::immutable_with_id_for_testing(ObjectId::random());
1183        let object_ref = object.object_ref();
1184        let object_key = ObjectKey::from(object_ref);
1185        let distinct_checkpoint: u64 = 0xCAFE_F00D_BEEF_1234;
1186
1187        let store_object_value =
1188            match get_store_object(object, Some(distinct_checkpoint)).into_inner() {
1189                StoreObject::Value(value) => value,
1190                other => panic!("expected StoreObject::Value, got {other:?}"),
1191            };
1192        let wrapper: StoreObjectWrapper = StoreObjectV2::Value(store_object_value).into();
1193        let mut wb = perpetual_db.objects.batch();
1194        wb.insert_batch(
1195            &perpetual_db.objects,
1196            std::iter::once((object_key, wrapper)),
1197        )
1198        .unwrap();
1199        wb.write().unwrap();
1200
1201        let yielded: Vec<_> = perpetual_db
1202            .iter_live_object_set()
1203            .collect::<Result<_, _>>()
1204            .unwrap();
1205        assert_eq!(yielded.len(), 1);
1206        assert_eq!(
1207            yielded[0].previous_transaction_checkpoint,
1208            Some(distinct_checkpoint),
1209            "LiveSetIter must surface the on-row checkpoint, not a default"
1210        );
1211    }
1212
1213    /// A read error part way through the scan must be yielded, not treated as
1214    /// the end of the live object set.
1215    #[tokio::test]
1216    async fn live_set_iter_surfaces_read_errors() {
1217        let tmp_dir = iota_common::tempdir();
1218        let perpetual_db = AuthorityPerpetualTables::open(tmp_dir.path(), None);
1219
1220        let object = Object::immutable_with_id_for_testing(ObjectId::random());
1221        let object_key = ObjectKey::from(object.object_ref());
1222        let rows = vec![
1223            Ok((object_key, get_store_object(object, Some(1)))),
1224            Err(TypedStoreError::RocksDB(
1225                "injected read failure".to_string(),
1226            )),
1227        ];
1228
1229        let yielded: Vec<_> = LiveSetIter {
1230            iter: Box::new(rows.into_iter()),
1231            tables: &perpetual_db,
1232            prev: None,
1233        }
1234        .collect();
1235
1236        assert_eq!(yielded.len(), 1);
1237        assert!(
1238            yielded[0].is_err(),
1239            "a failed read must not be reported as a complete scan"
1240        );
1241    }
1242
1243    /// A legacy V1 row (written by a pre-V2 binary, e.g. restored from a V1
1244    /// formal snapshot) must be migrated to the latest version at the read
1245    /// boundary. `get_latest_object_or_tombstone` feeds its result to
1246    /// `tombstone_reference`, which reaches `StoreObjectWrapper::inner()` and
1247    /// panics on an un-migrated V1 wrapper.
1248    #[tokio::test]
1249    async fn get_latest_object_or_tombstone_migrates_legacy_v1_row() {
1250        let tmp_dir = iota_common::tempdir();
1251        let perpetual_db = AuthorityPerpetualTables::open(tmp_dir.path(), None);
1252
1253        let object_id = ObjectId::random();
1254        let object = Object::immutable_with_id_for_testing(object_id);
1255        let object_ref = object.object_ref();
1256        perpetual_db
1257            .insert_store_object_v1_test_only(object)
1258            .unwrap();
1259
1260        let (object_key, wrapper) = perpetual_db
1261            .get_latest_object_or_tombstone(object_id)
1262            .unwrap()
1263            .expect("row must be found");
1264        assert!(
1265            matches!(wrapper, StoreObjectWrapper::V2(_)),
1266            "read boundary must migrate the V1 row to V2"
1267        );
1268
1269        // Both consumers of the returned wrapper must run without panicking.
1270        assert!(
1271            perpetual_db
1272                .tombstone_reference(&object_key, &wrapper)
1273                .unwrap()
1274                .is_none(),
1275            "a live value is not a tombstone"
1276        );
1277        let reconstructed = perpetual_db
1278            .object(&object_key, wrapper)
1279            .unwrap()
1280            .expect("value must reconstruct");
1281        assert_eq!(reconstructed.object_ref(), object_ref);
1282    }
1283
1284    /// An older release's database with the objects pruner's watermark.
1285    fn open_with_pruner_watermark(
1286        dir: &Path,
1287        watermark: Option<CheckpointSequenceNumber>,
1288    ) -> AuthorityPerpetualTables {
1289        let db = AuthorityPerpetualTables::open(dir, None);
1290        db.objects
1291            .db
1292            .create_cf(
1293                OBJECTS_PRUNER_WATERMARK_CF,
1294                &typed_store::rocksdb::Options::default(),
1295            )
1296            .unwrap();
1297        if let Some(watermark) = watermark {
1298            DBMap::<(), CheckpointSequenceNumber>::reopen(
1299                &db.objects.db,
1300                Some(OBJECTS_PRUNER_WATERMARK_CF),
1301                &ReadWriteOptions::default(),
1302                true,
1303            )
1304            .unwrap()
1305            .insert(&(), &watermark)
1306            .unwrap();
1307        }
1308        db
1309    }
1310
1311    /// Opening the database reads the older release's watermark in place and
1312    /// leaves its column family alone, so that a start refused afterwards
1313    /// leaves a database that release still reads; retiring it copies the
1314    /// watermark over and drops the column family.
1315    #[tokio::test]
1316    async fn the_objects_pruner_watermark_is_read_in_place_until_retired() {
1317        let tmp_dir = iota_common::tempdir();
1318        let db = open_with_pruner_watermark(tmp_dir.path(), Some(4_242));
1319        drop(db);
1320        let db = AuthorityPerpetualTables::open(tmp_dir.path(), None);
1321
1322        assert!(
1323            db.objects
1324                .db
1325                .cf_handle(OBJECTS_PRUNER_WATERMARK_CF)
1326                .is_some()
1327        );
1328        assert_eq!(db.objects_pruner_watermark().unwrap(), Some(4_242));
1329        assert_eq!(db.object_backlog_sweep_bound.get(&()).unwrap(), None);
1330
1331        db.retire_objects_pruner_watermark().unwrap();
1332
1333        assert!(
1334            db.objects
1335                .db
1336                .cf_handle(OBJECTS_PRUNER_WATERMARK_CF)
1337                .is_none()
1338        );
1339        assert_eq!(db.object_backlog_sweep_bound.get(&()).unwrap(), Some(4_242));
1340        assert_eq!(db.objects_pruner_watermark().unwrap(), Some(4_242));
1341        db.retire_objects_pruner_watermark()
1342            .expect("retiring again does nothing");
1343    }
1344
1345    /// Without a watermark, retiring writes no bound and still drops the
1346    /// older release's column family.
1347    #[tokio::test]
1348    async fn no_watermark_is_carried_over_when_the_pruner_never_ran() {
1349        let tmp_dir = iota_common::tempdir();
1350        let db = open_with_pruner_watermark(tmp_dir.path(), None);
1351
1352        db.retire_objects_pruner_watermark().unwrap();
1353
1354        assert_eq!(db.object_backlog_sweep_bound.get(&()).unwrap(), None);
1355        assert!(
1356            db.objects
1357                .db
1358                .cf_handle(OBJECTS_PRUNER_WATERMARK_CF)
1359                .is_none()
1360        );
1361    }
1362
1363    #[tokio::test]
1364    async fn mark_object_backlog_swept_records_done() {
1365        let tmp_dir = iota_common::tempdir();
1366        let perpetual_db = AuthorityPerpetualTables::open(tmp_dir.path(), None);
1367
1368        assert_eq!(
1369            perpetual_db.object_backlog_sweep_progress.get(&()).unwrap(),
1370            None
1371        );
1372
1373        perpetual_db.mark_object_backlog_swept().unwrap();
1374
1375        assert_eq!(
1376            perpetual_db.object_backlog_sweep_progress.get(&()).unwrap(),
1377            Some(ObjectBacklogSweepProgress::Done)
1378        );
1379    }
1380
1381    /// Only a sweep that ran, not one recorded as skipped, tells the reopened
1382    /// historic store that the live table holds no superseded version.
1383    #[tokio::test]
1384    async fn only_a_finished_sweep_marks_the_backlog_swept_at_open() {
1385        for (skip, swept) in [(true, false), (false, true)] {
1386            let tmp_dir = iota_common::tempdir();
1387            {
1388                let perpetual_db = AuthorityPerpetualTables::open(tmp_dir.path(), None);
1389                if skip {
1390                    perpetual_db.mark_object_backlog_sweep_skipped().unwrap();
1391                } else {
1392                    perpetual_db.mark_object_backlog_swept().unwrap();
1393                }
1394                let weak_db = perpetual_db.objects.db.close_handle();
1395                drop(perpetual_db);
1396                assert!(typed_store::database::wait_for_database_close(weak_db).await);
1397            }
1398            let (_perpetual_db, historic_objects, ..) =
1399                AuthorityPerpetualTables::open_with_historic_objects(tmp_dir.path(), None).unwrap();
1400            assert_eq!(historic_objects.backlog_swept(), swept, "skipped: {skip}");
1401        }
1402    }
1403
1404    /// [`AuthorityPerpetualTables::compact`] reclaims the space of deleted
1405    /// object versions.
1406    #[cfg(not(target_env = "msvc"))]
1407    #[tokio::test]
1408    async fn compact_reclaims_the_space_of_deleted_object_versions() {
1409        fn sst_size(path: &Path) -> u64 {
1410            let mut size = 0;
1411            for entry in std::fs::read_dir(path).unwrap() {
1412                let path = entry.unwrap().path();
1413                if path.extension().is_some_and(|ext| ext == "sst") {
1414                    size += std::fs::metadata(path).unwrap().len();
1415                }
1416            }
1417            size
1418        }
1419
1420        let tmp_dir = iota_common::tempdir();
1421        let perpetual_db = AuthorityPerpetualTables::open(tmp_dir.path(), None);
1422        let total_unique_object_ids = 10_000;
1423        let num_versions_per_object = 10;
1424        let mut id = ObjectId::ZERO;
1425        let mut to_delete = vec![];
1426        for _ in 0..total_unique_object_ids {
1427            for i in (0..num_versions_per_object).rev() {
1428                if i < num_versions_per_object - 2 {
1429                    to_delete.push(ObjectKey(id, Version::from(i)));
1430                }
1431                let object = get_store_object(Object::immutable_with_id_for_testing(id), None);
1432                perpetual_db
1433                    .objects
1434                    .insert(&ObjectKey(id, Version::from(i)), &object)
1435                    .unwrap();
1436            }
1437            id = id.next_lexicographical();
1438        }
1439
1440        let db_path = tmp_dir.path().join("perpetual");
1441        perpetual_db.compact().unwrap();
1442        let before_compaction_size = sst_size(&db_path);
1443
1444        let mut batch = perpetual_db.objects.batch();
1445        batch
1446            .delete_batch(&perpetual_db.objects, to_delete.into_iter())
1447            .unwrap();
1448        batch.write().unwrap();
1449
1450        perpetual_db.compact().unwrap();
1451        let after_compaction_size = sst_size(&db_path);
1452
1453        more_asserts::assert_lt!(after_compaction_size, before_compaction_size);
1454    }
1455
1456    /// The periodic compaction skips the historic bucket files, which live
1457    /// outside the perpetual database directory, instead of failing on them.
1458    #[tokio::test]
1459    async fn periodic_compaction_passes_over_the_historic_bucket_files() {
1460        let tmp_dir = iota_common::tempdir();
1461        let (perpetual_db, historic_objects, _historic_ledger, _epoch_markers) =
1462            AuthorityPerpetualTables::open_with_historic_objects(tmp_dir.path(), None).unwrap();
1463        let object = Object::immutable_with_id_for_testing(ObjectId::random());
1464        let bucket = historic_objects.ensure(1).unwrap();
1465        let mut batch = perpetual_db.objects.batch();
1466        batch
1467            .insert_batch_tagged(
1468                &bucket.objects,
1469                [(ObjectKey(object.id(), object.version()), object)],
1470            )
1471            .unwrap();
1472        batch.write().unwrap();
1473        perpetual_db.objects.flush_all().unwrap();
1474
1475        perpetual_db
1476            .compact_next_sst_file(0, &Mutex::new(HashMap::new()))
1477            .unwrap();
1478    }
1479}