1use 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#[derive(Default)]
44pub struct AuthorityPerpetualTablesOptions {
45 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#[derive(DBMapUtils)]
62pub struct AuthorityPerpetualTables {
63 pub(crate) objects: DBMap<ObjectKey, StoreObjectWrapper>,
78
79 pub(crate) live_owned_object_markers: DBMap<ObjectRef, ()>,
81
82 pub(crate) transactions: DBMap<TransactionDigest, TrustedTransaction>,
87
88 pub(crate) effects: DBMap<TransactionEffectsDigest, TransactionEffects>,
101
102 pub(crate) executed_effects: DBMap<TransactionDigest, TransactionEffectsDigest>,
108
109 pub(crate) events: DBMap<(TransactionEventsDigest, usize), Event>,
114
115 pub(crate) events_2: DBMap<TransactionDigest, TransactionEvents>,
117
118 pub(crate) executed_transactions_to_checkpoint:
123 DBMap<TransactionDigest, (EpochId, CheckpointSequenceNumber)>,
124
125 pub(crate) root_state_hash_by_epoch:
129 DBMap<EpochId, (CheckpointSequenceNumber, GlobalStateHash)>,
130
131 pub(crate) epoch_start_configuration: DBMap<(), EpochStartConfiguration>,
133
134 pub(crate) pruned_checkpoint: DBMap<(), CheckpointSequenceNumber>,
137
138 pub(crate) total_iota_supply: DBMap<(), TotalIotaSupplyCheck>,
142
143 pub(crate) expected_storage_fund_imbalance: DBMap<(), i64>,
148
149 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#[derive(Debug, Serialize, Deserialize)]
182pub(crate) struct TotalIotaSupplyCheck {
183 pub(crate) total_supply: u64,
185 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 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 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 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 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 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#[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#[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
676fn 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 #[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 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 #[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 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 #[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 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}