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