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::path::Path;
6
7use iota_types::{
8    base_types::SequenceNumber,
9    digests::TransactionEventsDigest,
10    effects::{TransactionEffects, TransactionEvents},
11    global_state_hash::GlobalStateHash,
12    storage::MarkerValue,
13};
14use serde::{Deserialize, Serialize};
15use tracing::error;
16use typed_store::{
17    DBMapUtils, DbIterator,
18    metrics::SamplingInterval,
19    rocks::{
20        DBBatch, DBMap, DBMapTableConfigMap, DBOptions, MetricConf, default_db_options,
21        read_size_from_env,
22    },
23    rocksdb::compaction_filter::Decision,
24    traits::Map,
25};
26
27use super::*;
28use crate::authority::{
29    authority_store_pruner::ObjectsCompactionFilter,
30    authority_store_types::{
31        StoreObject, StoreObjectValueV2, StoreObjectWrapper, get_store_object, try_construct_object,
32    },
33    epoch_start_configuration::EpochStartConfiguration,
34};
35
36const ENV_VAR_OBJECTS_BLOCK_CACHE_SIZE: &str = "OBJECTS_BLOCK_CACHE_MB";
37pub(crate) const ENV_VAR_LOCKS_BLOCK_CACHE_SIZE: &str = "LOCKS_BLOCK_CACHE_MB";
38const ENV_VAR_TRANSACTIONS_BLOCK_CACHE_SIZE: &str = "TRANSACTIONS_BLOCK_CACHE_MB";
39const ENV_VAR_EFFECTS_BLOCK_CACHE_SIZE: &str = "EFFECTS_BLOCK_CACHE_MB";
40const ENV_VAR_EVENTS_BLOCK_CACHE_SIZE: &str = "EVENTS_BLOCK_CACHE_MB";
41
42/// Options to apply to every column family of the `perpetual` DB.
43#[derive(Default)]
44pub struct AuthorityPerpetualTablesOptions {
45    /// Whether to enable write stalling on all column families.
46    pub enable_write_stall: bool,
47    pub compaction_filter: Option<ObjectsCompactionFilter>,
48}
49
50impl AuthorityPerpetualTablesOptions {
51    fn apply_to(&self, mut db_options: DBOptions) -> DBOptions {
52        if !self.enable_write_stall {
53            db_options = db_options.disable_write_throttling();
54        }
55        db_options
56    }
57}
58
59/// AuthorityPerpetualTables contains data that must be preserved from one epoch
60/// to the next.
61#[derive(DBMapUtils)]
62pub struct AuthorityPerpetualTables {
63    /// This is a map between the object (ID, version) and the latest state of
64    /// the object, namely the state that is needed to process new
65    /// transactions. State is represented by `StoreObject` enum, which is
66    /// either a move module or a move object.
67    ///
68    /// Note that while this map can store all versions of an object, we will
69    /// eventually prune old object versions from the db.
70    ///
71    /// IMPORTANT: object versions must *only* be pruned if they appear as
72    /// inputs in some TransactionEffects. Simply pruning all objects but
73    /// the most recent is an error! This is because there can be partially
74    /// executed transactions whose effects have not yet been written out,
75    /// and which must be retried. But, they cannot be retried unless their
76    /// input objects are still accessible!
77    pub(crate) objects: DBMap<ObjectKey, StoreObjectWrapper>,
78
79    /// Object references of currently active objects that can be mutated.
80    pub(crate) live_owned_object_markers: DBMap<ObjectRef, ()>,
81
82    /// This is a map between the transaction digest and the corresponding
83    /// transaction that's known to be executable. This means that it may
84    /// have been executed locally, or it may have been synced through
85    /// state-sync but hasn't been executed yet.
86    pub(crate) transactions: DBMap<TransactionDigest, TrustedTransaction>,
87
88    /// A map between the transaction digest of a certificate to the effects of
89    /// its execution. We store effects into this table in two different
90    /// cases:
91    /// 1. When a transaction is synced through state_sync, we store the effects
92    ///    here. These effects are known to be final in the network, but may not
93    ///    have been executed locally yet.
94    /// 2. When the transaction is executed locally on this node, we store the
95    ///    effects here. This means that it's possible to store the same effects
96    ///    twice (once for the synced transaction, and once for the executed).
97    ///
98    /// It's also possible for the effects to be reverted if the transaction
99    /// didn't make it into the epoch.
100    pub(crate) effects: DBMap<TransactionEffectsDigest, TransactionEffects>,
101
102    /// Transactions that have been executed locally on this node. We need this
103    /// table since the `effects` table doesn't say anything about the
104    /// execution status of the transaction on this node. When we wait for
105    /// transactions to be executed, we wait for them to appear in this
106    /// table. When we revert transactions, we remove them from both tables.
107    pub(crate) executed_effects: DBMap<TransactionDigest, TransactionEffectsDigest>,
108
109    // Currently this is needed in the validator for returning events during process certificates.
110    // We could potentially remove this if we decided not to provide events in the execution path.
111    // TODO: Figure out what to do with this table in the long run.
112    // Also we need a pruning policy for this table. We can prune this table along with tx/effects.
113    pub(crate) events: DBMap<(TransactionEventsDigest, usize), Event>,
114
115    // Events keyed by the digest of the transaction that produced them.
116    pub(crate) events_2: DBMap<TransactionDigest, TransactionEvents>,
117
118    /// Epoch and checkpoint of transactions finalized by checkpoint
119    /// executor. Currently, mainly used to implement JSON RPC `ReadApi`.
120    /// Note, there is a table with the same name in
121    /// `AuthorityEpochTables`/`AuthorityPerEpochStore`.
122    pub(crate) executed_transactions_to_checkpoint:
123        DBMap<TransactionDigest, (EpochId, CheckpointSequenceNumber)>,
124
125    // Finalized root state hash for epoch, to be included in CheckpointSummary
126    // of last checkpoint of epoch. These values should only ever be written once
127    // and never changed
128    pub(crate) root_state_hash_by_epoch:
129        DBMap<EpochId, (CheckpointSequenceNumber, GlobalStateHash)>,
130
131    /// Parameters of the system fixed at the epoch start
132    pub(crate) epoch_start_configuration: DBMap<(), EpochStartConfiguration>,
133
134    /// A singleton table that stores latest pruned checkpoint. Used to keep
135    /// objects pruner progress
136    pub(crate) pruned_checkpoint: DBMap<(), CheckpointSequenceNumber>,
137
138    /// The total IOTA supply and the epoch at which it was stored.
139    /// We check and update it at the end of each epoch if expensive checks are
140    /// enabled.
141    pub(crate) total_iota_supply: DBMap<(), TotalIotaSupplyCheck>,
142
143    /// Expected imbalance between storage fund balance and the sum of storage
144    /// rebate of all live objects. This could be non-zero due to bugs in
145    /// earlier protocol versions. This number is the result of
146    /// storage_fund_balance - sum(storage_rebate).
147    pub(crate) expected_storage_fund_imbalance: DBMap<(), i64>,
148
149    /// Table that stores the set of received objects and deleted objects and
150    /// the version at which they were received. This is used to prevent
151    /// possible race conditions around receiving objects (since they are
152    /// not locked by the transaction manager) and for tracking shared
153    /// objects that have been deleted. This table is meant to be pruned
154    /// per-epoch, and all previous epochs other than the current epoch may
155    /// be pruned safely.
156    pub(crate) object_per_epoch_marker_table: DBMap<(EpochId, ObjectKey), MarkerValue>,
157}
158
159#[derive(DBMapUtils)]
160pub struct AuthorityPrunerTables {
161    pub(crate) object_tombstones: DBMap<ObjectId, SequenceNumber>,
162}
163
164impl AuthorityPrunerTables {
165    pub fn path(parent_path: &Path) -> PathBuf {
166        parent_path.join("pruner")
167    }
168
169    pub fn open(parent_path: &Path) -> Self {
170        Self::open_tables_read_write(
171            Self::path(parent_path),
172            MetricConf::new("pruner")
173                .with_sampling(SamplingInterval::new(Duration::from_secs(60), 0)),
174            None,
175            None,
176        )
177    }
178}
179
180/// The total IOTA supply used during conservation checks.
181#[derive(Debug, Serialize, Deserialize)]
182pub(crate) struct TotalIotaSupplyCheck {
183    /// The IOTA supply at the time of `last_check_epoch`.
184    pub(crate) total_supply: u64,
185    /// The epoch at which the total supply was last checked or updated.
186    pub(crate) last_check_epoch: EpochId,
187}
188
189impl AuthorityPerpetualTables {
190    pub fn path(parent_path: &Path) -> PathBuf {
191        parent_path.join("perpetual")
192    }
193
194    pub fn open(
195        parent_path: &Path,
196        db_options_override: Option<AuthorityPerpetualTablesOptions>,
197    ) -> Self {
198        let db_options_override = db_options_override.unwrap_or_default();
199        let db_options =
200            db_options_override.apply_to(default_db_options().optimize_db_for_write_throughput(4));
201        let table_options = DBMapTableConfigMap::new(BTreeMap::from([
202            (
203                "objects".to_string(),
204                objects_table_config(db_options.clone(), db_options_override.compaction_filter),
205            ),
206            (
207                "live_owned_object_markers".to_string(),
208                live_owned_object_markers_table_config(db_options.clone()),
209            ),
210            (
211                "transactions".to_string(),
212                transactions_table_config(db_options.clone()),
213            ),
214            (
215                "effects".to_string(),
216                effects_table_config(db_options.clone()),
217            ),
218            (
219                "events".to_string(),
220                events_table_config(db_options.clone()),
221            ),
222        ]));
223        Self::open_tables_read_write(
224            Self::path(parent_path),
225            MetricConf::new("perpetual")
226                .with_sampling(SamplingInterval::new(Duration::from_secs(60), 0)),
227            Some(db_options.options),
228            Some(table_options),
229        )
230    }
231
232    pub fn open_readonly(parent_path: &Path) -> AuthorityPerpetualTablesReadOnly {
233        Self::get_read_only_handle(
234            Self::path(parent_path),
235            None,
236            None,
237            MetricConf::new("perpetual_readonly"),
238        )
239    }
240
241    // This is used by indexer to find the correct version of dynamic field child
242    // object. We do not store the version of the child object, but because of
243    // lamport timestamp, we know the child must have version number less then
244    // or eq to the parent.
245    pub fn find_object_lt_or_eq_version(
246        &self,
247        object_id: ObjectId,
248        version: SequenceNumber,
249    ) -> IotaResult<Option<Object>> {
250        let mut iter = self.objects.safe_range_iter_reversed(
251            ObjectKey::min_for_id(&object_id)..=ObjectKey(object_id, version),
252        );
253        match iter.next() {
254            Some(Ok((key, o))) => self.object(&key, o),
255            Some(Err(e)) => Err(e.into()),
256            None => Ok(None),
257        }
258    }
259
260    fn construct_object(
261        &self,
262        object_key: &ObjectKey,
263        store_object: StoreObjectValueV2,
264    ) -> Result<Object, IotaError> {
265        try_construct_object(object_key, store_object)
266    }
267
268    // Constructs `iota_types::object::Object` from `StoreObjectWrapper`.
269    // Returns `None` if object was deleted/wrapped
270    pub fn object(
271        &self,
272        object_key: &ObjectKey,
273        store_object: StoreObjectWrapper,
274    ) -> Result<Option<Object>, IotaError> {
275        let StoreObject::Value(store_object) = store_object.migrate().into_inner() else {
276            return Ok(None);
277        };
278        Ok(Some(self.construct_object(object_key, *store_object)?))
279    }
280
281    pub fn object_reference(
282        &self,
283        object_key: &ObjectKey,
284        store_object: StoreObjectWrapper,
285    ) -> Result<ObjectRef, IotaError> {
286        let obj_ref = match store_object.migrate().into_inner() {
287            StoreObject::Value(object) => self.construct_object(object_key, *object)?.object_ref(),
288            StoreObject::Deleted => {
289                ObjectRef::new(object_key.0, object_key.1, ObjectDigest::OBJECT_DELETED)
290            }
291            StoreObject::Wrapped => {
292                ObjectRef::new(object_key.0, object_key.1, ObjectDigest::OBJECT_WRAPPED)
293            }
294        };
295        Ok(obj_ref)
296    }
297
298    pub fn tombstone_reference(
299        &self,
300        object_key: &ObjectKey,
301        store_object: &StoreObjectWrapper,
302    ) -> Result<Option<ObjectRef>, IotaError> {
303        let obj_ref = match store_object.inner() {
304            StoreObject::Deleted => Some(ObjectRef::new(
305                object_key.0,
306                object_key.1,
307                ObjectDigest::OBJECT_DELETED,
308            )),
309            StoreObject::Wrapped => Some(ObjectRef::new(
310                object_key.0,
311                object_key.1,
312                ObjectDigest::OBJECT_WRAPPED,
313            )),
314            _ => None,
315        };
316        Ok(obj_ref)
317    }
318
319    pub fn get_latest_object_ref_or_tombstone(
320        &self,
321        object_id: ObjectId,
322    ) -> Result<Option<ObjectRef>, IotaError> {
323        let mut iterator = self.objects.safe_iter_with_prefix_reversed(&object_id);
324
325        if let Some(Ok((object_key, value))) = iterator.next() {
326            if object_key.0 == object_id {
327                return Ok(Some(self.object_reference(&object_key, value)?));
328            }
329        }
330        Ok(None)
331    }
332
333    pub fn get_latest_object_or_tombstone(
334        &self,
335        object_id: ObjectId,
336    ) -> Result<Option<(ObjectKey, StoreObjectWrapper)>, IotaError> {
337        let mut iterator = self.objects.safe_iter_with_prefix_reversed(&object_id);
338
339        if let Some(Ok((object_key, value))) = iterator.next() {
340            if object_key.0 == object_id {
341                // Migrate legacy V1 rows before returning; callers inspect the
342                // wrapper via `inner()`, which panics on an un-migrated V1.
343                return Ok(Some((object_key, value.migrate())));
344            }
345        }
346        Ok(None)
347    }
348
349    pub fn get_recovery_epoch_at_restart(&self) -> IotaResult<EpochId> {
350        Ok(self
351            .epoch_start_configuration
352            .get(&())?
353            .expect("Must have current epoch.")
354            .epoch_start_state()
355            .epoch())
356    }
357
358    pub fn set_epoch_start_configuration(
359        &self,
360        epoch_start_configuration: &EpochStartConfiguration,
361    ) -> IotaResult {
362        let mut wb = self.epoch_start_configuration.batch();
363        wb.insert_batch(
364            &self.epoch_start_configuration,
365            std::iter::once(((), epoch_start_configuration)),
366        )?;
367        wb.write()?;
368        Ok(())
369    }
370
371    pub fn get_highest_pruned_checkpoint(
372        &self,
373    ) -> Result<Option<CheckpointSequenceNumber>, TypedStoreError> {
374        self.pruned_checkpoint.get(&())
375    }
376
377    pub fn set_highest_pruned_checkpoint(
378        &self,
379        wb: &mut DBBatch,
380        checkpoint_number: CheckpointSequenceNumber,
381    ) -> IotaResult {
382        wb.insert_batch(&self.pruned_checkpoint, [((), checkpoint_number)])?;
383        Ok(())
384    }
385
386    pub fn get_transaction(
387        &self,
388        digest: &TransactionDigest,
389    ) -> IotaResult<Option<TrustedTransaction>> {
390        let Some(transaction) = self.transactions.get(digest)? else {
391            return Ok(None);
392        };
393        Ok(Some(transaction))
394    }
395
396    pub fn get_effects(
397        &self,
398        digest: &TransactionDigest,
399    ) -> IotaResult<Option<TransactionEffects>> {
400        let Some(effect_digest) = self.executed_effects.get(digest)? else {
401            return Ok(None);
402        };
403        Ok(self.effects.get(&effect_digest)?)
404    }
405
406    pub fn get_checkpoint_sequence_number(
407        &self,
408        digest: &TransactionDigest,
409    ) -> IotaResult<Option<(EpochId, CheckpointSequenceNumber)>> {
410        Ok(self.executed_transactions_to_checkpoint.get(digest)?)
411    }
412
413    pub fn get_newer_object_keys(
414        &self,
415        object: &(ObjectId, SequenceNumber),
416    ) -> IotaResult<Vec<ObjectKey>> {
417        let mut objects = vec![];
418        for result in self
419            .objects
420            .safe_iter_with_prefix_from(&object.0, &object.1.next().unwrap())
421        {
422            let (key, _) = result?;
423            objects.push(key);
424        }
425        Ok(objects)
426    }
427
428    pub fn set_highest_pruned_checkpoint_without_wb(
429        &self,
430        checkpoint_number: CheckpointSequenceNumber,
431    ) -> IotaResult {
432        let mut wb = self.pruned_checkpoint.batch();
433        self.set_highest_pruned_checkpoint(&mut wb, checkpoint_number)?;
434        wb.write()?;
435        Ok(())
436    }
437
438    pub fn database_is_empty(&self) -> IotaResult<bool> {
439        Ok(self.objects.safe_iter().next().is_none())
440    }
441
442    pub fn iter_live_object_set(&self) -> LiveSetIter<'_> {
443        LiveSetIter {
444            iter: Box::new(self.objects.safe_iter()),
445            tables: self,
446            prev: None,
447        }
448    }
449
450    pub fn range_iter_live_object_set(
451        &self,
452        lower_bound: Option<ObjectId>,
453        upper_bound: Option<ObjectId>,
454    ) -> LiveSetIter<'_> {
455        let lower_bound = lower_bound.as_ref().map(ObjectKey::min_for_id);
456        let upper_bound = upper_bound.as_ref().map(ObjectKey::max_for_id);
457
458        LiveSetIter {
459            iter: Box::new(self.objects.safe_iter_with_bounds(lower_bound, upper_bound)),
460            tables: self,
461            prev: None,
462        }
463    }
464
465    pub fn checkpoint_db(&self, path: &Path) -> IotaResult {
466        // This checkpoints the entire db and not just objects table
467        self.objects.checkpoint_db(path).map_err(Into::into)
468    }
469
470    pub fn get_root_state_hash(
471        &self,
472        epoch: EpochId,
473    ) -> IotaResult<Option<(CheckpointSequenceNumber, GlobalStateHash)>> {
474        Ok(self.root_state_hash_by_epoch.get(&epoch)?)
475    }
476
477    pub fn insert_root_state_hash(
478        &self,
479        epoch: EpochId,
480        last_checkpoint_of_epoch: CheckpointSequenceNumber,
481        hash: GlobalStateHash,
482    ) -> IotaResult {
483        self.root_state_hash_by_epoch
484            .insert(&epoch, &(last_checkpoint_of_epoch, hash))?;
485        Ok(())
486    }
487
488    pub fn insert_store_object_v1_test_only(&self, object: Object) -> IotaResult {
489        use crate::authority::authority_store_types::{StoreObjectV1, StoreObjectValue};
490
491        let object_reference = object.object_ref();
492        let v2_value = match get_store_object(object, None).into_inner() {
493            StoreObject::Value(v) => *v,
494            other => unreachable!("get_store_object must produce a Value variant, got {other:?}"),
495        };
496        let v1_value = StoreObjectValue {
497            data: v2_value.data,
498            owner: v2_value.owner,
499            previous_transaction: v2_value.previous_transaction,
500            storage_rebate: v2_value.storage_rebate,
501        };
502        let wrapper = StoreObjectWrapper::V1(StoreObjectV1::Value(Box::new(v1_value)));
503
504        let mut wb = self.objects.batch();
505        wb.insert_batch(
506            &self.objects,
507            std::iter::once((ObjectKey::from(object_reference), wrapper)),
508        )?;
509        wb.write()?;
510        Ok(())
511    }
512
513    pub fn insert_store_object_v2_test_only(
514        &self,
515        object: Object,
516        previous_transaction_checkpoint: Option<CheckpointSequenceNumber>,
517    ) -> IotaResult {
518        let object_reference = object.object_ref();
519        let wrapper = get_store_object(object, previous_transaction_checkpoint);
520
521        let mut wb = self.objects.batch();
522        wb.insert_batch(
523            &self.objects,
524            std::iter::once((ObjectKey::from(object_reference), wrapper)),
525        )?;
526        wb.write()?;
527        Ok(())
528    }
529}
530
531impl ObjectStore for AuthorityPerpetualTables {
532    /// Read an object and return it, or Ok(None) if the object was not found.
533    fn try_get_object(
534        &self,
535        object_id: &ObjectId,
536    ) -> Result<Option<Object>, iota_types::storage::error::Error> {
537        let obj_entry = self
538            .objects
539            .safe_iter_with_prefix_reversed(object_id)
540            .next();
541
542        match obj_entry.transpose()? {
543            Some((ObjectKey(obj_id, version), obj)) if obj_id == *object_id => Ok(self
544                .object(&ObjectKey(obj_id, version), obj)
545                .map_err(iota_types::storage::error::Error::custom)?),
546            _ => Ok(None),
547        }
548    }
549
550    fn try_get_object_by_key(
551        &self,
552        object_id: &ObjectId,
553        version: VersionNumber,
554    ) -> Result<Option<Object>, iota_types::storage::error::Error> {
555        Ok(self
556            .objects
557            .get(&ObjectKey(*object_id, version))
558            .map_err(iota_types::storage::error::Error::custom)?
559            .map(|object| self.object(&ObjectKey(*object_id, version), object))
560            .transpose()
561            .map_err(iota_types::storage::error::Error::custom)?
562            .flatten())
563    }
564}
565
566/// In-process iterator item for a live object together with the checkpoint
567/// sequence number that contained the transaction whose effects produced this
568/// object version. Yielded by [`LiveSetIter`].
569///
570/// `previous_transaction_checkpoint` is `Option`: production write paths
571/// always produce `Some(seq)`, but `LiveSetIter` will yield `None` for rows
572/// lifted from a pre-V2 on-disk format (the checkpoint was never recorded and
573/// is unrecoverable).
574#[derive(Eq, PartialEq, Debug, Clone, Hash)]
575pub struct LiveObject {
576    pub object: Object,
577    pub previous_transaction_checkpoint: Option<CheckpointSequenceNumber>,
578}
579
580impl LiveObject {
581    pub fn object_id(&self) -> ObjectId {
582        self.object.id()
583    }
584
585    pub fn version(&self) -> SequenceNumber {
586        self.object.version()
587    }
588
589    pub fn object_reference(&self) -> ObjectRef {
590        self.object.object_ref()
591    }
592}
593
594/// On-disk record format for a live object as emitted into snapshot V2 `.obj`
595/// files (`iota-snapshot::writer::write_object`) and decoded by
596/// `iota-snapshot::reader::LiveObjectIter`.
597#[derive(Deserialize, Serialize)]
598pub struct SnapshotLiveObject {
599    pub object: Object,
600    pub previous_transaction_checkpoint: CheckpointSequenceNumber,
601}
602
603impl From<SnapshotLiveObject> for LiveObject {
604    fn from(snap: SnapshotLiveObject) -> Self {
605        let SnapshotLiveObject {
606            object,
607            previous_transaction_checkpoint,
608        } = snap;
609        LiveObject {
610            object,
611            previous_transaction_checkpoint: Some(previous_transaction_checkpoint),
612        }
613    }
614}
615
616pub struct LiveSetIter<'a> {
617    iter: DbIterator<'a, (ObjectKey, StoreObjectWrapper)>,
618    tables: &'a AuthorityPerpetualTables,
619    prev: Option<(ObjectKey, StoreObjectWrapper)>,
620}
621
622impl LiveSetIter<'_> {
623    fn store_object_wrapper_to_live_object(
624        &self,
625        object_key: ObjectKey,
626        store_object: StoreObjectWrapper,
627    ) -> Option<LiveObject> {
628        match store_object.migrate().into_inner() {
629            StoreObject::Value(value) => {
630                let previous_transaction_checkpoint = value.previous_transaction_checkpoint;
631                let object = self
632                    .tables
633                    .construct_object(&object_key, *value)
634                    .expect("Constructing object from store cannot fail");
635                Some(LiveObject {
636                    object,
637                    previous_transaction_checkpoint,
638                })
639            }
640            StoreObject::Wrapped | StoreObject::Deleted => None,
641        }
642    }
643}
644
645impl Iterator for LiveSetIter<'_> {
646    type Item = LiveObject;
647
648    fn next(&mut self) -> Option<Self::Item> {
649        loop {
650            if let Some(Ok((next_key, next_value))) = self.iter.next() {
651                let prev = self.prev.take();
652                self.prev = Some((next_key, next_value));
653
654                if let Some((prev_key, prev_value)) = prev {
655                    if prev_key.0 != next_key.0 {
656                        let live_object =
657                            self.store_object_wrapper_to_live_object(prev_key, prev_value);
658                        if live_object.is_some() {
659                            return live_object;
660                        }
661                    }
662                }
663                continue;
664            }
665            if let Some((key, value)) = self.prev.take() {
666                let live_object = self.store_object_wrapper_to_live_object(key, value);
667                if live_object.is_some() {
668                    return live_object;
669                }
670            }
671            return None;
672        }
673    }
674}
675
676// These functions are used to initialize the DB tables
677fn live_owned_object_markers_table_config(db_options: DBOptions) -> DBOptions {
678    DBOptions {
679        options: db_options
680            .clone()
681            .optimize_for_write_throughput()
682            .optimize_for_read(read_size_from_env(ENV_VAR_LOCKS_BLOCK_CACHE_SIZE).unwrap_or(1024))
683            .options,
684        rw_options: db_options.rw_options,
685    }
686}
687
688fn objects_table_config(
689    mut db_options: DBOptions,
690    compaction_filter: Option<ObjectsCompactionFilter>,
691) -> DBOptions {
692    if let Some(mut compaction_filter) = compaction_filter {
693        db_options
694            .options
695            .set_compaction_filter("objects", move |_, key, value| {
696                match compaction_filter.filter(key, value) {
697                    Ok(decision) => decision,
698                    Err(err) => {
699                        error!("Compaction error: {:?}", err);
700                        Decision::Keep
701                    }
702                }
703            });
704    }
705    db_options
706        .optimize_for_write_throughput()
707        .optimize_for_read(read_size_from_env(ENV_VAR_OBJECTS_BLOCK_CACHE_SIZE).unwrap_or(5 * 1024))
708}
709
710fn transactions_table_config(db_options: DBOptions) -> DBOptions {
711    db_options
712        .optimize_for_write_throughput()
713        .optimize_for_point_lookup(
714            read_size_from_env(ENV_VAR_TRANSACTIONS_BLOCK_CACHE_SIZE).unwrap_or(512),
715        )
716}
717
718fn effects_table_config(db_options: DBOptions) -> DBOptions {
719    db_options
720        .optimize_for_write_throughput()
721        .optimize_for_point_lookup(
722            read_size_from_env(ENV_VAR_EFFECTS_BLOCK_CACHE_SIZE).unwrap_or(1024),
723        )
724}
725
726fn events_table_config(db_options: DBOptions) -> DBOptions {
727    db_options
728        .optimize_for_write_throughput()
729        .optimize_for_read(read_size_from_env(ENV_VAR_EVENTS_BLOCK_CACHE_SIZE).unwrap_or(1024))
730}
731
732#[cfg(test)]
733mod tests {
734    use super::*;
735    use crate::authority::authority_store_types::StoreObjectV2;
736
737    /// `LiveSetIter` must filter `StoreObject::Wrapped` and
738    /// `StoreObject::Deleted` rows at the source so downstream consumers
739    /// (snapshot writer, state-hash accumulator, restore path) only ever
740    /// observe live objects.
741    #[tokio::test]
742    async fn live_set_iter_filters_wrapped_and_deleted_store_rows() {
743        let tmp_dir = iota_common::tempdir();
744        let perpetual_db = AuthorityPerpetualTables::open(tmp_dir.path(), None);
745
746        // A live `Normal` row alongside `Wrapped` and `Deleted` tombstones for
747        // distinct object IDs.
748        let live_id = ObjectId::random();
749        let wrapped_id = ObjectId::random();
750        let deleted_id = ObjectId::random();
751
752        let live_object = Object::immutable_with_id_for_testing(live_id);
753        perpetual_db
754            .insert_store_object_v2_test_only(live_object, None)
755            .unwrap();
756
757        let mut wb = perpetual_db.objects.batch();
758        let wrapped_key = ObjectKey(wrapped_id, SequenceNumber::from_u64(1));
759        wb.insert_batch(
760            &perpetual_db.objects,
761            std::iter::once::<(ObjectKey, StoreObjectWrapper)>((
762                wrapped_key,
763                StoreObjectV2::Wrapped.into(),
764            )),
765        )
766        .unwrap();
767        let deleted_key = ObjectKey(deleted_id, SequenceNumber::from_u64(1));
768        wb.insert_batch(
769            &perpetual_db.objects,
770            std::iter::once::<(ObjectKey, StoreObjectWrapper)>((
771                deleted_key,
772                StoreObjectV2::Deleted.into(),
773            )),
774        )
775        .unwrap();
776        wb.write().unwrap();
777
778        let yielded: Vec<_> = perpetual_db.iter_live_object_set().collect();
779        assert_eq!(yielded.len(), 1, "wrapped/deleted rows must be filtered");
780        assert_eq!(yielded[0].object.id(), live_id);
781    }
782
783    /// `LiveSetIter` must surface the exact `previous_transaction_checkpoint`
784    /// stored on `StoreObjectValueV2` - it is the load-bearing input to each
785    /// `LiveObject` record the snapshot V2 writer emits into `.obj` files
786    /// (and, on restore, to the `previous_transaction_checkpoint` field
787    /// stamped onto `StoreObjectV2` via `bulk_insert_live_objects`). A bug
788    /// that, e.g., always stamped `0` here would silently corrupt every
789    /// snapshot's per-object record; this is the focused canary for that
790    /// contract.
791    #[tokio::test]
792    async fn live_set_iter_propagates_previous_transaction_checkpoint() {
793        let tmp_dir = iota_common::tempdir();
794        let perpetual_db = AuthorityPerpetualTables::open(tmp_dir.path(), None);
795
796        // Insert a live object with a distinct, recognizable checkpoint.
797        let object = Object::immutable_with_id_for_testing(ObjectId::random());
798        let object_ref = object.object_ref();
799        let object_key = ObjectKey::from(object_ref);
800        let distinct_checkpoint: u64 = 0xCAFE_F00D_BEEF_1234;
801
802        let store_object_value =
803            match get_store_object(object, Some(distinct_checkpoint)).into_inner() {
804                StoreObject::Value(value) => value,
805                other => panic!("expected StoreObject::Value, got {other:?}"),
806            };
807        let wrapper: StoreObjectWrapper = StoreObjectV2::Value(store_object_value).into();
808        let mut wb = perpetual_db.objects.batch();
809        wb.insert_batch(
810            &perpetual_db.objects,
811            std::iter::once((object_key, wrapper)),
812        )
813        .unwrap();
814        wb.write().unwrap();
815
816        let yielded: Vec<_> = perpetual_db.iter_live_object_set().collect();
817        assert_eq!(yielded.len(), 1);
818        assert_eq!(
819            yielded[0].previous_transaction_checkpoint,
820            Some(distinct_checkpoint),
821            "LiveSetIter must surface the on-row checkpoint, not a default"
822        );
823    }
824
825    /// A legacy V1 row (written by a pre-V2 binary, e.g. restored from a V1
826    /// formal snapshot) must be migrated to the latest version at the read
827    /// boundary. `get_latest_object_or_tombstone` feeds its result to
828    /// `tombstone_reference`, which reaches `StoreObjectWrapper::inner()` and
829    /// panics on an un-migrated V1 wrapper.
830    #[tokio::test]
831    async fn get_latest_object_or_tombstone_migrates_legacy_v1_row() {
832        let tmp_dir = iota_common::tempdir();
833        let perpetual_db = AuthorityPerpetualTables::open(tmp_dir.path(), None);
834
835        let object_id = ObjectId::random();
836        let object = Object::immutable_with_id_for_testing(object_id);
837        let object_ref = object.object_ref();
838        perpetual_db
839            .insert_store_object_v1_test_only(object)
840            .unwrap();
841
842        let (object_key, wrapper) = perpetual_db
843            .get_latest_object_or_tombstone(object_id)
844            .unwrap()
845            .expect("row must be found");
846        assert!(
847            matches!(wrapper, StoreObjectWrapper::V2(_)),
848            "read boundary must migrate the V1 row to V2"
849        );
850
851        // Both consumers of the returned wrapper must run without panicking.
852        assert!(
853            perpetual_db
854                .tombstone_reference(&object_key, &wrapper)
855                .unwrap()
856                .is_none(),
857            "a live value is not a tombstone"
858        );
859        let reconstructed = perpetual_db
860            .object(&object_key, wrapper)
861            .unwrap()
862            .expect("value must reconstruct");
863        assert_eq!(reconstructed.object_ref(), object_ref);
864    }
865}