1use std::ops::Bound;
13
14use move_core_types::language_storage::StructTag;
15use sui_consistent_store::Encode;
16use sui_consistent_store::reader::Reader;
17use sui_types::base_types::ObjectID;
18use sui_types::base_types::SuiAddress;
19use sui_types::messages_checkpoint::CheckpointSequenceNumber;
20use sui_types::storage::BalanceInfo;
21use sui_types::storage::BalanceIterator;
22use sui_types::storage::CoinInfo;
23use sui_types::storage::DynamicFieldIteratorItem;
24use sui_types::storage::DynamicFieldKey;
25use sui_types::storage::EpochInfo;
26use sui_types::storage::LedgerBitmapBucket;
27use sui_types::storage::LedgerBitmapBucketIter;
28use sui_types::storage::LedgerBitmapBucketIterator;
29use sui_types::storage::LedgerTxSeqDigest;
30use sui_types::storage::LedgerTxSeqDigestIterator;
31use sui_types::storage::OwnedObjectInfo;
32use sui_types::storage::RpcIndexes;
33
34type PackageVersionsIterator<'a> =
37 Box<dyn Iterator<Item = Result<(u64, ObjectID), TypedStoreError>> + 'a>;
38use sui_types::storage::error::Result as StorageResult;
39use typed_store_error::TypedStoreError;
40
41use crate::reader::RpcStoreReader;
42use crate::schema::type_filter::TypeFilter;
43
44enum BoundedBitmapIter<'d, K, V> {
45 Fwd(sui_consistent_store::Iter<'d, K, V>),
46 Rev(sui_consistent_store::RevIter<'d, K, V>),
47}
48
49impl<K, V> BoundedBitmapIter<'_, K, V> {
50 fn seek(&mut self, probe: impl AsRef<[u8]>) {
51 match self {
52 Self::Fwd(iter) => iter.seek(probe),
53 Self::Rev(iter) => iter.seek(probe),
54 }
55 }
56}
57
58struct TransactionBitmapBucketIter<'d> {
59 inner: BoundedBitmapIter<
60 'd,
61 crate::schema::transaction_bitmap::Key,
62 crate::schema::transaction_bitmap::Value,
63 >,
64 dimension_key: Vec<u8>,
65}
66
67impl Iterator for TransactionBitmapBucketIter<'_> {
68 type Item = Result<LedgerBitmapBucket, TypedStoreError>;
69
70 fn next(&mut self) -> Option<Self::Item> {
71 let row = match &mut self.inner {
72 BoundedBitmapIter::Fwd(iter) => iter.next(),
73 BoundedBitmapIter::Rev(iter) => iter.next(),
74 };
75 row.map(project_bitmap_row)
76 }
77}
78
79impl LedgerBitmapBucketIter for TransactionBitmapBucketIter<'_> {
80 fn seek_bucket(&mut self, bucket_id: u64) {
81 let probe = crate::schema::transaction_bitmap::Key {
82 dimension_key: self.dimension_key.clone(),
83 bucket: bucket_id,
84 }
85 .encode()
86 .expect("bitmap key encodes infallibly: raw bytes + u64 BE");
87 self.inner.seek(probe);
88 }
89}
90
91struct EventBitmapBucketIter<'d> {
92 inner:
93 BoundedBitmapIter<'d, crate::schema::event_bitmap::Key, crate::schema::event_bitmap::Value>,
94 dimension_key: Vec<u8>,
95}
96
97impl Iterator for EventBitmapBucketIter<'_> {
98 type Item = Result<LedgerBitmapBucket, TypedStoreError>;
99
100 fn next(&mut self) -> Option<Self::Item> {
101 let row = match &mut self.inner {
102 BoundedBitmapIter::Fwd(iter) => iter.next(),
103 BoundedBitmapIter::Rev(iter) => iter.next(),
104 };
105 row.map(project_event_bitmap_row)
106 }
107}
108
109impl LedgerBitmapBucketIter for EventBitmapBucketIter<'_> {
110 fn seek_bucket(&mut self, bucket_id: u64) {
111 let probe = crate::schema::event_bitmap::Key {
112 dimension_key: self.dimension_key.clone(),
113 bucket: bucket_id,
114 }
115 .encode()
116 .expect("bitmap key encodes infallibly: raw bytes + u64 BE");
117 self.inner.seek(probe);
118 }
119}
120
121fn to_typed_store_err(e: sui_consistent_store::error::Error) -> TypedStoreError {
122 TypedStoreError::RocksDBError(format!("{e:#}"))
123}
124
125fn first_object_of_type<R: Reader + Send + Sync>(
132 reader: &RpcStoreReader<R>,
133 struct_tag: move_core_types::language_storage::StructTag,
134) -> StorageResult<Option<ObjectID>> {
135 let filter = TypeFilter::Type(struct_tag);
136 let mut iter = reader
137 .schema()
138 .iter_objects_of_type(&filter)
139 .map_err(sui_types::storage::error::Error::custom)?;
140 match iter.next() {
141 Some(Ok((key, _value))) => Ok(Some(key.object_id)),
142 Some(Err(e)) => Err(sui_types::storage::error::Error::custom(e)),
143 None => Ok(None),
144 }
145}
146
147impl<R: Reader + Send + Sync> RpcIndexes for RpcStoreReader<R> {
148 fn get_epoch_info(
149 &self,
150 epoch: sui_types::committee::EpochId,
151 ) -> StorageResult<Option<EpochInfo>> {
152 self.schema()
153 .get_epoch(epoch)
154 .map_err(sui_types::storage::error::Error::custom)
155 }
156
157 fn owned_objects_iter(
158 &self,
159 owner: SuiAddress,
160 object_type: Option<StructTag>,
161 cursor: Option<OwnedObjectInfo>,
162 ) -> StorageResult<Box<dyn Iterator<Item = Result<OwnedObjectInfo, TypedStoreError>> + '_>>
163 {
164 use crate::schema::object_by_owner::{Key, OwnerKind};
165 let map = &self.schema().object_by_owner;
166 let kind = OwnerKind::AddressOwner(owner);
167 let from = cursor.map(|c| Key {
171 kind,
172 type_: c.object_type,
173 inverted_balance: c.balance.map(|b| !b),
174 object_id: c.object_id,
175 });
176 let iter = match (object_type, &from) {
177 (Some(struct_tag), Some(from)) => {
178 map.iter_prefix_from(&(kind, TypeFilter::Type(struct_tag)), from)
179 }
180 (Some(struct_tag), None) => map.iter_prefix(&(kind, TypeFilter::Type(struct_tag))),
181 (None, Some(from)) => map.iter_prefix_from(&kind, from),
182 (None, None) => map.iter_prefix(&kind),
183 }
184 .map_err(sui_types::storage::error::Error::custom)?;
185
186 let mapped = iter.map(move |row| {
187 let (key, value) = row.map_err(to_typed_store_err)?;
188 Ok(OwnedObjectInfo {
189 owner,
190 object_type: key.type_,
191 balance: key.inverted_balance.map(|b| !b),
192 object_id: key.object_id,
193 version: sui_types::base_types::SequenceNumber::from_u64(value.0),
194 })
195 });
196
197 Ok(Box::new(mapped))
198 }
199
200 fn dynamic_field_iter(
201 &self,
202 parent: ObjectID,
203 cursor: Option<DynamicFieldKey>,
204 ) -> StorageResult<Box<dyn Iterator<Item = DynamicFieldIteratorItem> + '_>> {
205 use crate::schema::object_by_owner::{Key, OwnerKind};
206 let map = &self.schema().object_by_owner;
213 let kind = OwnerKind::ObjectOwner(parent.into());
214 let from = cursor.map(|c| Key {
215 kind,
216 type_: c.object_type,
217 inverted_balance: None,
218 object_id: c.field_id,
219 });
220 let iter = match &from {
221 Some(from) => map.iter_prefix_from(&kind, from),
222 None => map.iter_prefix(&kind),
223 }
224 .map_err(sui_types::storage::error::Error::custom)?;
225
226 let mapped = iter.map(move |row| {
227 let (key, _value) = row.map_err(to_typed_store_err)?;
228 Ok(DynamicFieldKey {
229 parent,
230 field_id: key.object_id,
231 object_type: key.type_,
232 })
233 });
234
235 Ok(Box::new(mapped))
236 }
237
238 fn get_coin_info(&self, coin_type: &StructTag) -> StorageResult<Option<CoinInfo>> {
239 let coin_metadata_object_id = first_object_of_type(
245 self,
246 sui_types::coin::CoinMetadata::type_(coin_type.clone()),
247 )?;
248 let treasury_object_id =
249 first_object_of_type(self, sui_types::coin::TreasuryCap::type_(coin_type.clone()))?;
250 let regulated_coin_metadata_object_id = first_object_of_type(
251 self,
252 sui_types::coin::RegulatedCoinMetadata::type_(coin_type.clone()),
253 )?;
254
255 if coin_metadata_object_id.is_none()
256 && treasury_object_id.is_none()
257 && regulated_coin_metadata_object_id.is_none()
258 {
259 return Ok(None);
260 }
261
262 Ok(Some(CoinInfo {
263 coin_metadata_object_id,
264 treasury_object_id,
265 regulated_coin_metadata_object_id,
266 }))
267 }
268
269 fn get_balance(
270 &self,
271 owner: &SuiAddress,
272 coin_type: &StructTag,
273 ) -> StorageResult<Option<BalanceInfo>> {
274 let balance = self
275 .schema()
276 .get_balance(*owner, coin_type.clone().into())
277 .map_err(sui_types::storage::error::Error::custom)?;
278 Ok(balance.map(|b| BalanceInfo {
283 coin_balance: b.coin.clamp(0, u64::MAX as i128) as u64,
284 address_balance: b.address.clamp(0, u64::MAX as i128) as u64,
285 }))
286 }
287
288 fn balance_iter(
289 &self,
290 owner: &SuiAddress,
291 cursor: Option<(SuiAddress, StructTag)>,
292 ) -> StorageResult<BalanceIterator<'_>> {
293 use crate::schema::balance::{Key, OwnerPrefix};
294 let map = &self.schema().balance;
295 let from = cursor.map(|(_, tag)| Key {
299 owner: *owner,
300 coin_type: move_core_types::language_storage::TypeTag::Struct(Box::new(tag)),
301 });
302 let iter = match &from {
303 Some(from) => map.iter_prefix_from(&OwnerPrefix(*owner), from),
304 None => map.iter_prefix(&OwnerPrefix(*owner)),
305 }
306 .map_err(sui_types::storage::error::Error::custom)?;
307
308 let mapped = iter.filter_map(move |row| {
309 let (key, value) = match row {
310 Ok(pair) => pair,
311 Err(e) => return Some(Err(sui_types::storage::error::Error::custom(e))),
312 };
313 let balance = match crate::schema::balance::Balance::from_delta(&value.into_inner()) {
318 Ok(b) => b,
319 Err(e) => return Some(Err(sui_types::storage::error::Error::custom(e))),
320 };
321 let info = BalanceInfo {
325 coin_balance: balance.coin.clamp(0, u64::MAX as i128) as u64,
326 address_balance: balance.address.clamp(0, u64::MAX as i128) as u64,
327 };
328 let struct_tag = match key.coin_type {
329 move_core_types::language_storage::TypeTag::Struct(b) => *b,
330 _ => return None,
331 };
332 Some(Ok((struct_tag, info)))
333 });
334
335 Ok(Box::new(mapped))
336 }
337
338 fn package_versions_iter(
339 &self,
340 original_id: ObjectID,
341 cursor: Option<u64>,
342 ) -> StorageResult<PackageVersionsIterator<'_>> {
343 use crate::schema::package_versions::{Key, OriginalIdPrefix};
344 let map = &self.schema().package_versions;
350 let iter = match cursor {
351 Some(version) => map.iter_prefix_from(
352 &OriginalIdPrefix(original_id),
353 &Key {
354 original_id,
355 version,
356 },
357 ),
358 None => map.iter_prefix(&OriginalIdPrefix(original_id)),
359 }
360 .map_err(sui_types::storage::error::Error::custom)?;
361 let mapped = iter.map(move |row| {
362 let (key, value) = row.map_err(to_typed_store_err)?;
363 let storage_id_bytes: [u8; 32] = (&value.into_inner().storage_id[..])
365 .try_into()
366 .map_err(|_| {
367 TypedStoreError::SerializationError("package_versions storage_id length".into())
368 })?;
369 Ok((key.version, ObjectID::new(storage_id_bytes)))
370 });
371 Ok(Box::new(mapped))
372 }
373
374 fn get_package_version_storage_id(
375 &self,
376 original_id: ObjectID,
377 version: u64,
378 ) -> StorageResult<Option<ObjectID>> {
379 self.schema()
380 .get_package_storage_id(original_id, version)
381 .map_err(sui_types::storage::error::Error::custom)
382 }
383
384 fn get_package_at_checkpoint(
385 &self,
386 original_id: ObjectID,
387 checkpoint: CheckpointSequenceNumber,
388 ) -> StorageResult<Option<(u64, ObjectID)>> {
389 self.schema()
390 .get_package_at_checkpoint(original_id, checkpoint)
391 .map_err(sui_types::storage::error::Error::custom)
392 }
393
394 fn get_highest_indexed_checkpoint_seq_number(
395 &self,
396 ) -> StorageResult<Option<CheckpointSequenceNumber>> {
397 self.min_committed(self.pipelines.iter().copied())
407 }
408
409 fn get_highest_live_indexed_checkpoint_seq_number(
410 &self,
411 ) -> StorageResult<Option<CheckpointSequenceNumber>> {
412 self.highest_live_committed_checkpoint()
416 }
417
418 fn ledger_tx_seq_digest(&self, tx_seq: u64) -> StorageResult<Option<LedgerTxSeqDigest>> {
419 let meta = self
420 .schema()
421 .get_tx_metadata_by_seq(tx_seq)
422 .map_err(sui_types::storage::error::Error::custom)?;
423 Ok(meta.map(|m| LedgerTxSeqDigest {
424 tx_sequence_number: tx_seq,
425 digest: m.digest,
426 event_count: m.event_count,
427 tx_offset: m.ckpt_position,
428 checkpoint_number: m.checkpoint_seq,
429 }))
430 }
431
432 fn ledger_tx_seq_digest_iter(
433 &self,
434 start: u64,
435 end_exclusive: u64,
436 descending: bool,
437 ) -> StorageResult<LedgerTxSeqDigestIterator<'_>> {
438 use crate::schema::primitives::U64Be;
439 let range = (
440 Bound::Included(U64Be(start)),
441 Bound::Excluded(U64Be(end_exclusive)),
442 );
443 let map = &self.schema().tx_metadata_by_seq;
444 let project = move |row: Result<
445 (U64Be, crate::schema::tx_metadata_by_seq::Value),
446 sui_consistent_store::error::Error,
447 >| {
448 let (U64Be(seq), value) = row.map_err(to_typed_store_err)?;
449 let stored = value.into_inner();
450 let digest_bytes: [u8; 32] = (&stored.digest[..]).try_into().map_err(|_| {
451 TypedStoreError::SerializationError("tx_metadata digest length".into())
452 })?;
453 Ok(LedgerTxSeqDigest {
454 tx_sequence_number: seq,
455 digest: sui_types::digests::TransactionDigest::new(digest_bytes),
456 event_count: stored.event_count,
457 tx_offset: stored.ckpt_position,
458 checkpoint_number: stored.checkpoint_seq,
459 })
460 };
461 if descending {
462 let iter = map
463 .iter_rev(range)
464 .map_err(sui_types::storage::error::Error::custom)?;
465 Ok(Box::new(iter.map(project)))
466 } else {
467 let iter = map
468 .iter(range)
469 .map_err(sui_types::storage::error::Error::custom)?;
470 Ok(Box::new(iter.map(project)))
471 }
472 }
473
474 fn transaction_bitmap_bucket_iter(
475 &self,
476 dimension_key: Vec<u8>,
477 start_bucket: u64,
478 end_bucket_exclusive: u64,
479 descending: bool,
480 ) -> StorageResult<LedgerBitmapBucketIterator<'_>> {
481 let map = &self.schema().transaction_bitmap;
482 let lower = crate::schema::transaction_bitmap::Key {
483 dimension_key: dimension_key.clone(),
484 bucket: start_bucket,
485 };
486 let upper = crate::schema::transaction_bitmap::Key {
487 dimension_key: dimension_key.clone(),
488 bucket: end_bucket_exclusive,
489 };
490 let inner = if descending {
491 BoundedBitmapIter::Rev(
492 map.iter_rev(lower..upper)
493 .map_err(sui_types::storage::error::Error::custom)?,
494 )
495 } else {
496 BoundedBitmapIter::Fwd(
497 map.iter(lower..upper)
498 .map_err(sui_types::storage::error::Error::custom)?,
499 )
500 };
501 Ok(Box::new(TransactionBitmapBucketIter {
502 inner,
503 dimension_key,
504 }))
505 }
506
507 fn event_bitmap_bucket_iter(
508 &self,
509 dimension_key: Vec<u8>,
510 start_bucket: u64,
511 end_bucket_exclusive: u64,
512 descending: bool,
513 ) -> StorageResult<LedgerBitmapBucketIterator<'_>> {
514 let map = &self.schema().event_bitmap;
515 let lower = crate::schema::event_bitmap::Key {
516 dimension_key: dimension_key.clone(),
517 bucket: start_bucket,
518 };
519 let upper = crate::schema::event_bitmap::Key {
520 dimension_key: dimension_key.clone(),
521 bucket: end_bucket_exclusive,
522 };
523 let inner = if descending {
524 BoundedBitmapIter::Rev(
525 map.iter_rev(lower..upper)
526 .map_err(sui_types::storage::error::Error::custom)?,
527 )
528 } else {
529 BoundedBitmapIter::Fwd(
530 map.iter(lower..upper)
531 .map_err(sui_types::storage::error::Error::custom)?,
532 )
533 };
534 Ok(Box::new(EventBitmapBucketIter {
535 inner,
536 dimension_key,
537 }))
538 }
539}
540
541fn project_bitmap_row(
545 row: Result<
546 (
547 crate::schema::transaction_bitmap::Key,
548 crate::schema::transaction_bitmap::Value,
549 ),
550 sui_consistent_store::error::Error,
551 >,
552) -> Result<sui_types::storage::LedgerBitmapBucket, TypedStoreError> {
553 let (key, value) = row.map_err(to_typed_store_err)?;
554 let bitmap = roaring::RoaringBitmap::deserialize_from(value.into_inner().data.as_ref())
555 .map_err(|e| TypedStoreError::SerializationError(format!("RoaringBitmap: {e}")))?;
556 Ok(sui_types::storage::LedgerBitmapBucket {
557 bucket_id: key.bucket,
558 bitmap,
559 })
560}
561
562fn project_event_bitmap_row(
567 row: Result<
568 (
569 crate::schema::event_bitmap::Key,
570 crate::schema::event_bitmap::Value,
571 ),
572 sui_consistent_store::error::Error,
573 >,
574) -> Result<sui_types::storage::LedgerBitmapBucket, TypedStoreError> {
575 let (key, value) = row.map_err(to_typed_store_err)?;
576 let bitmap = roaring::RoaringBitmap::deserialize_from(value.into_inner().data.as_ref())
577 .map_err(|e| TypedStoreError::SerializationError(format!("RoaringBitmap: {e}")))?;
578 Ok(sui_types::storage::LedgerBitmapBucket {
579 bucket_id: key.bucket,
580 bitmap,
581 })
582}
583
584#[cfg(test)]
585mod tests {
586 use std::sync::Arc;
587
588 use sui_consistent_store::Db;
589 use sui_consistent_store::DbOptions;
590 use sui_types::base_types::ObjectID;
591 use sui_types::storage::RpcIndexes;
592
593 use crate::RpcStoreSchema;
594 use crate::reader::RpcStoreReader;
595 use crate::schema::transaction_bitmap;
596
597 fn setup() -> (tempfile::TempDir, Db, RpcStoreReader) {
598 let dir = tempfile::tempdir().unwrap();
599 let (db, schema) = Db::open::<RpcStoreSchema>(dir.path(), DbOptions::default()).unwrap();
600 let reader = RpcStoreReader::new(db.clone(), Arc::new(schema));
601 (dir, db, reader)
602 }
603
604 #[test]
605 fn transaction_bitmap_bucket_iter_walks_range_ascending() {
606 let (_dir, db, reader) = setup();
607 let dim = b"sender:alice".to_vec();
608
609 let mut batch = db.batch();
610 for tx_seq in [
611 1u64,
612 transaction_bitmap::TX_BUCKET_SIZE + 5,
613 3 * transaction_bitmap::TX_BUCKET_SIZE + 9,
614 ] {
615 let (k, v) = transaction_bitmap::store_match(dim.clone(), tx_seq);
616 batch
617 .merge(&reader.schema().transaction_bitmap, &k, &v)
618 .unwrap();
619 }
620 batch.commit().unwrap();
621
622 let buckets: Vec<u64> = reader
623 .transaction_bitmap_bucket_iter(dim.clone(), 0, 5, false)
624 .unwrap()
625 .map(|res| res.unwrap().bucket_id)
626 .collect();
627 assert_eq!(buckets, vec![0, 1, 3]);
628 }
629
630 #[test]
631 fn transaction_bitmap_bucket_iter_respects_bucket_range_bounds() {
632 let (_dir, db, reader) = setup();
633 let dim = b"sender:alice".to_vec();
634
635 let mut batch = db.batch();
636 for tx_seq in [
637 1u64,
638 transaction_bitmap::TX_BUCKET_SIZE + 5,
639 3 * transaction_bitmap::TX_BUCKET_SIZE + 9,
640 ] {
641 let (k, v) = transaction_bitmap::store_match(dim.clone(), tx_seq);
642 batch
643 .merge(&reader.schema().transaction_bitmap, &k, &v)
644 .unwrap();
645 }
646 batch.commit().unwrap();
647
648 let buckets: Vec<u64> = reader
650 .transaction_bitmap_bucket_iter(dim.clone(), 1, 3, false)
651 .unwrap()
652 .map(|res| res.unwrap().bucket_id)
653 .collect();
654 assert_eq!(buckets, vec![1]);
655 }
656
657 #[test]
658 fn transaction_bitmap_bucket_iter_descending_reverses_order() {
659 let (_dir, db, reader) = setup();
660 let dim = b"sender:alice".to_vec();
661
662 let mut batch = db.batch();
663 for tx_seq in [
664 1u64,
665 transaction_bitmap::TX_BUCKET_SIZE + 5,
666 3 * transaction_bitmap::TX_BUCKET_SIZE + 9,
667 ] {
668 let (k, v) = transaction_bitmap::store_match(dim.clone(), tx_seq);
669 batch
670 .merge(&reader.schema().transaction_bitmap, &k, &v)
671 .unwrap();
672 }
673 batch.commit().unwrap();
674
675 let buckets: Vec<u64> = reader
676 .transaction_bitmap_bucket_iter(dim, 0, 5, true)
677 .unwrap()
678 .map(|res| res.unwrap().bucket_id)
679 .collect();
680 assert_eq!(buckets, vec![3, 1, 0]);
681 }
682
683 #[test]
684 fn transaction_bitmap_bucket_iter_isolates_dimension() {
685 let (_dir, db, reader) = setup();
686 let alice = b"sender:alice".to_vec();
687 let bob = b"sender:bob".to_vec();
688
689 let mut batch = db.batch();
690 let (k_a, v_a) = transaction_bitmap::store_match(alice.clone(), 1);
691 let (k_b, v_b) = transaction_bitmap::store_match(bob, 1);
692 batch
693 .merge(&reader.schema().transaction_bitmap, &k_a, &v_a)
694 .unwrap();
695 batch
696 .merge(&reader.schema().transaction_bitmap, &k_b, &v_b)
697 .unwrap();
698 batch.commit().unwrap();
699
700 let buckets: Vec<u64> = reader
701 .transaction_bitmap_bucket_iter(alice, 0, 5, false)
702 .unwrap()
703 .map(|res| res.unwrap().bucket_id)
704 .collect();
705 assert_eq!(buckets, vec![0]);
707 }
708
709 #[test]
715 fn highest_indexed_bounds_to_registered_pipelines() {
716 use sui_consistent_store::FrameworkSchema;
717 use sui_consistent_store::PipelineTaskKey;
718 use sui_consistent_store::Watermark;
719
720 let (_dir, db, reader) = setup();
721
722 let framework = FrameworkSchema::new(db.clone());
723 let stamp = |names: &[&str], hi: u64| {
724 let mut batch = db.batch();
725 for name in names {
726 batch
727 .put(
728 &framework.watermarks,
729 &PipelineTaskKey::new(*name),
730 &Watermark::for_checkpoint(hi),
731 )
732 .unwrap();
733 }
734 batch.commit().unwrap();
735 };
736
737 assert_eq!(
740 reader.get_highest_indexed_checkpoint_seq_number().unwrap(),
741 None,
742 );
743 stamp(&["transactions"], 3);
744 assert_eq!(
745 reader.get_highest_indexed_checkpoint_seq_number().unwrap(),
746 None,
747 );
748
749 stamp(crate::LIVE_COHORT, 40);
752 assert_eq!(
753 reader.get_highest_indexed_checkpoint_seq_number().unwrap(),
754 None,
755 );
756
757 stamp(crate::HISTORY_COHORT, 40);
761 stamp(&[crate::HISTORY_COHORT[0]], 25);
762 assert_eq!(
763 reader.get_highest_indexed_checkpoint_seq_number().unwrap(),
764 Some(25),
765 );
766
767 let custom = reader.clone().with_pipelines(["transactions"]);
770 assert_eq!(
771 custom.get_highest_indexed_checkpoint_seq_number().unwrap(),
772 Some(3),
773 );
774 }
775
776 #[test]
777 fn transaction_bitmap_bucket_iter_seeks_within_range() {
778 let (_dir, db, reader) = setup();
779 let dimension_key = b"sender:alice".to_vec();
780 let mut batch = db.batch();
781 for bucket in [0, 3, 7] {
782 let (key, value) = transaction_bitmap::store_match(
783 dimension_key.clone(),
784 bucket * transaction_bitmap::TX_BUCKET_SIZE + 1,
785 );
786 batch
787 .merge(&reader.schema().transaction_bitmap, &key, &value)
788 .unwrap();
789 }
790 batch.commit().unwrap();
791
792 let mut iter = reader
793 .transaction_bitmap_bucket_iter(dimension_key, 0, 8, false)
794 .unwrap();
795 iter.seek_bucket(2);
796 assert_eq!(iter.next().unwrap().unwrap().bucket_id, 3);
797 iter.seek_bucket(7);
798 assert_eq!(iter.next().unwrap().unwrap().bucket_id, 7);
799 iter.seek_bucket(9);
800 assert!(iter.next().is_none());
801 }
802
803 #[test]
804 fn transaction_bitmap_bucket_iter_seeks_descending() {
805 let (_dir, db, reader) = setup();
806 let dimension_key = b"sender:alice".to_vec();
807 let mut batch = db.batch();
808 for bucket in [0, 3, 7] {
809 let (key, value) = transaction_bitmap::store_match(
810 dimension_key.clone(),
811 bucket * transaction_bitmap::TX_BUCKET_SIZE + 1,
812 );
813 batch
814 .merge(&reader.schema().transaction_bitmap, &key, &value)
815 .unwrap();
816 }
817 batch.commit().unwrap();
818
819 let mut iter = reader
820 .transaction_bitmap_bucket_iter(dimension_key, 0, 8, true)
821 .unwrap();
822 iter.seek_bucket(5);
823 assert_eq!(iter.next().unwrap().unwrap().bucket_id, 3);
824 iter.seek_bucket(0);
825 assert_eq!(iter.next().unwrap().unwrap().bucket_id, 0);
826 }
827
828 #[test]
829 fn bitmap_bucket_iter_seek_respects_dimension_isolation() {
830 let (_dir, db, reader) = setup();
831 let alice = b"sender:alice".to_vec();
832 let bob = b"sender:bob".to_vec();
833 let mut batch = db.batch();
834 for (dimension_key, bucket) in [(&alice, 0), (&alice, 7), (&bob, 3), (&bob, 7)] {
835 let (key, value) = transaction_bitmap::store_match(
836 dimension_key.clone(),
837 bucket * transaction_bitmap::TX_BUCKET_SIZE + 1,
838 );
839 batch
840 .merge(&reader.schema().transaction_bitmap, &key, &value)
841 .unwrap();
842 }
843 batch.commit().unwrap();
844
845 let mut iter = reader
846 .transaction_bitmap_bucket_iter(alice, 0, 8, false)
847 .unwrap();
848 iter.seek_bucket(3);
849 assert_eq!(iter.next().unwrap().unwrap().bucket_id, 7);
850 assert!(iter.next().is_none());
851 }
852
853 #[test]
854 fn get_coin_info_finds_metadata_and_treasury_objects() {
855 use move_core_types::language_storage::StructTag;
856
857 use crate::schema::object_by_type;
858 use crate::schema::primitives::U64Varint;
859
860 let (_dir, db, reader) = setup();
861
862 let coin_type = StructTag {
868 address: move_core_types::account_address::AccountAddress::new([2u8; 32]),
869 module: move_core_types::identifier::Identifier::new("sui").unwrap(),
870 name: move_core_types::identifier::Identifier::new("SUI").unwrap(),
871 type_params: vec![],
872 };
873 let metadata_type = sui_types::coin::CoinMetadata::type_(coin_type.clone());
874 let treasury_type = sui_types::coin::TreasuryCap::type_(coin_type.clone());
875
876 let metadata_object_id = ObjectID::from_single_byte(0xA1);
877 let treasury_object_id = ObjectID::from_single_byte(0xA2);
878
879 let mut batch = db.batch();
880 batch
881 .put(
882 &reader.schema().object_by_type,
883 &object_by_type::Key {
884 type_: metadata_type,
885 object_id: metadata_object_id,
886 },
887 &U64Varint(1),
888 )
889 .unwrap();
890 batch
891 .put(
892 &reader.schema().object_by_type,
893 &object_by_type::Key {
894 type_: treasury_type,
895 object_id: treasury_object_id,
896 },
897 &U64Varint(1),
898 )
899 .unwrap();
900 batch.commit().unwrap();
901
902 let info = reader
903 .get_coin_info(&coin_type)
904 .unwrap()
905 .expect("coin info present");
906 assert_eq!(info.coin_metadata_object_id, Some(metadata_object_id));
907 assert_eq!(info.treasury_object_id, Some(treasury_object_id));
908 assert_eq!(info.regulated_coin_metadata_object_id, None);
909 }
910
911 #[test]
912 fn get_coin_info_returns_none_when_no_wrappers_indexed() {
913 use move_core_types::language_storage::StructTag;
914
915 let (_dir, _db, reader) = setup();
916
917 let coin_type = StructTag {
918 address: move_core_types::account_address::AccountAddress::new([3u8; 32]),
919 module: move_core_types::identifier::Identifier::new("custom").unwrap(),
920 name: move_core_types::identifier::Identifier::new("COIN").unwrap(),
921 type_params: vec![],
922 };
923 assert!(reader.get_coin_info(&coin_type).unwrap().is_none());
924 }
925
926 #[test]
927 fn transaction_bitmap_bucket_iter_returns_decoded_bitmap() {
928 let (_dir, db, reader) = setup();
929 let dim = b"sender:alice".to_vec();
930
931 let mut batch = db.batch();
932 for tx_seq in [1u64, 17, 256] {
933 let (k, v) = transaction_bitmap::store_match(dim.clone(), tx_seq);
934 batch
935 .merge(&reader.schema().transaction_bitmap, &k, &v)
936 .unwrap();
937 }
938 batch.commit().unwrap();
939
940 let first = reader
941 .transaction_bitmap_bucket_iter(dim, 0, 1, false)
942 .unwrap()
943 .next()
944 .unwrap()
945 .unwrap();
946 let bits: Vec<u32> = first.bitmap.iter().collect();
947 assert_eq!(bits, vec![1, 17, 256]);
948 }
949
950 fn a_type() -> move_core_types::language_storage::StructTag {
957 move_core_types::language_storage::StructTag {
958 address: move_core_types::account_address::AccountAddress::TWO,
959 module: move_core_types::identifier::Identifier::new("m").unwrap(),
960 name: move_core_types::identifier::Identifier::new("T").unwrap(),
961 type_params: vec![],
962 }
963 }
964
965 #[test]
966 fn owned_objects_iter_paginates_each_object_once() {
967 use crate::schema::object_by_owner::{Key, OwnerKind};
968 use crate::schema::primitives::U64Varint;
969 use sui_types::base_types::SuiAddress;
970
971 let (_dir, db, reader) = setup();
972 let owner = SuiAddress::ZERO;
973 let ids: Vec<ObjectID> = (1u8..=5).map(ObjectID::from_single_byte).collect();
974
975 let mut batch = db.batch();
976 for id in &ids {
977 batch
978 .put(
979 &reader.schema().object_by_owner,
980 &Key {
981 kind: OwnerKind::AddressOwner(owner),
982 type_: a_type(),
983 inverted_balance: None,
984 object_id: *id,
985 },
986 &U64Varint(1),
987 )
988 .unwrap();
989 }
990 batch.commit().unwrap();
991
992 let mut seen = Vec::new();
993 let mut cursor = None;
994 for _ in 0..ids.len() + 2 {
997 let mut iter = reader
998 .owned_objects_iter(owner, None, cursor.take())
999 .unwrap();
1000 for _ in 0..2 {
1001 match iter.next() {
1002 Some(res) => seen.push(res.unwrap().object_id),
1003 None => break,
1004 }
1005 }
1006 cursor = iter.next().transpose().unwrap();
1007 if cursor.is_none() {
1008 break;
1009 }
1010 }
1011
1012 assert_eq!(seen.len(), ids.len(), "each object yielded exactly once");
1013 let mut unique = seen.clone();
1014 unique.sort();
1015 unique.dedup();
1016 assert_eq!(unique, ids, "every object, no gaps or duplicates");
1017 }
1018
1019 #[test]
1020 fn dynamic_field_iter_paginates_each_field_once() {
1021 use crate::schema::object_by_owner::{Key, OwnerKind};
1022 use crate::schema::primitives::U64Varint;
1023
1024 let (_dir, db, reader) = setup();
1025 let parent = ObjectID::from_single_byte(0xAA);
1026 let fields: Vec<ObjectID> = (1u8..=5).map(ObjectID::from_single_byte).collect();
1027
1028 let mut batch = db.batch();
1029 for id in &fields {
1030 batch
1031 .put(
1032 &reader.schema().object_by_owner,
1033 &Key {
1034 kind: OwnerKind::ObjectOwner(parent.into()),
1035 type_: a_type(),
1036 inverted_balance: None,
1037 object_id: *id,
1038 },
1039 &U64Varint(1),
1040 )
1041 .unwrap();
1042 }
1043 batch.commit().unwrap();
1044
1045 let mut seen = Vec::new();
1046 let mut cursor = None;
1047 for _ in 0..fields.len() + 2 {
1048 let mut iter = reader.dynamic_field_iter(parent, cursor.take()).unwrap();
1049 for _ in 0..2 {
1050 match iter.next() {
1051 Some(res) => seen.push(res.unwrap().field_id),
1052 None => break,
1053 }
1054 }
1055 cursor = iter.next().transpose().unwrap();
1056 if cursor.is_none() {
1057 break;
1058 }
1059 }
1060
1061 assert_eq!(seen.len(), fields.len(), "each field yielded exactly once");
1062 let mut unique = seen.clone();
1063 unique.sort();
1064 unique.dedup();
1065 assert_eq!(unique, fields, "every field, no gaps or duplicates");
1066 }
1067
1068 #[test]
1069 fn balance_iter_paginates_each_coin_type_once() {
1070 use crate::schema::balance::coin_delta;
1071 use move_core_types::language_storage::{StructTag, TypeTag};
1072 use sui_types::base_types::SuiAddress;
1073
1074 let coin_type = |name: &str| -> TypeTag {
1075 TypeTag::Struct(Box::new(StructTag {
1076 address: move_core_types::account_address::AccountAddress::TWO,
1077 module: move_core_types::identifier::Identifier::new("coin").unwrap(),
1078 name: move_core_types::identifier::Identifier::new(name).unwrap(),
1079 type_params: vec![],
1080 }))
1081 };
1082
1083 let (_dir, db, reader) = setup();
1084 let owner = SuiAddress::ZERO;
1085 let names = ["c0", "c1", "c2", "c3", "c4"];
1086
1087 let mut batch = db.batch();
1088 for name in names {
1089 let (k, v) = coin_delta(owner, coin_type(name), 100);
1090 batch.merge(&reader.schema().balance, &k, &v).unwrap();
1091 }
1092 batch.commit().unwrap();
1093
1094 let mut seen: Vec<StructTag> = Vec::new();
1095 let mut cursor: Option<(SuiAddress, StructTag)> = None;
1096 for _ in 0..names.len() + 2 {
1097 let mut iter = reader.balance_iter(&owner, cursor.take()).unwrap();
1098 for _ in 0..2 {
1099 match iter.next() {
1100 Some(res) => seen.push(res.unwrap().0),
1101 None => break,
1102 }
1103 }
1104 cursor = iter
1105 .next()
1106 .transpose()
1107 .unwrap()
1108 .map(|(tag, _)| (owner, tag));
1109 if cursor.is_none() {
1110 break;
1111 }
1112 }
1113
1114 assert_eq!(
1115 seen.len(),
1116 names.len(),
1117 "each coin type yielded exactly once"
1118 );
1119 let mut unique = seen.clone();
1120 unique.sort();
1121 unique.dedup();
1122 assert_eq!(unique.len(), names.len(), "no duplicate coin types");
1123 }
1124
1125 #[test]
1126 fn package_versions_iter_paginates_each_version_once() {
1127 let (_dir, db, reader) = setup();
1128 let original = ObjectID::from_single_byte(0xBB);
1129 let versions: Vec<u64> = (1..=5).collect();
1130
1131 let mut batch = db.batch();
1132 for &version in &versions {
1133 let (k, v) = crate::schema::package_versions::store(
1134 original,
1135 version,
1136 ObjectID::from_single_byte(version as u8),
1137 version,
1138 );
1139 batch
1140 .put(&reader.schema().package_versions, &k, &v)
1141 .unwrap();
1142 }
1143 batch.commit().unwrap();
1144
1145 let mut seen: Vec<u64> = Vec::new();
1146 let mut cursor: Option<u64> = None;
1147 for _ in 0..versions.len() + 2 {
1148 let mut iter = reader
1149 .package_versions_iter(original, cursor.take())
1150 .unwrap();
1151 for _ in 0..2 {
1152 match iter.next() {
1153 Some(res) => seen.push(res.unwrap().0),
1154 None => break,
1155 }
1156 }
1157 cursor = iter.next().transpose().unwrap().map(|(v, _)| v);
1158 if cursor.is_none() {
1159 break;
1160 }
1161 }
1162
1163 assert_eq!(
1164 seen.len(),
1165 versions.len(),
1166 "each version yielded exactly once"
1167 );
1168 let mut unique = seen.clone();
1169 unique.sort();
1170 unique.dedup();
1171 assert_eq!(unique, versions, "every version, no gaps or duplicates");
1172 }
1173
1174 #[test]
1175 fn package_lookups_resolve_exact_version_and_checkpoint_bound() {
1176 let (_dir, db, reader) = setup();
1177 let original = ObjectID::from_single_byte(0xCC);
1178 let s1 = ObjectID::from_single_byte(0x01);
1179 let s2 = ObjectID::from_single_byte(0x02);
1180
1181 let mut batch = db.batch();
1182 let (k, v) = crate::schema::package_versions::store(original, 1, s1, 1);
1183 batch
1184 .put(&reader.schema().package_versions, &k, &v)
1185 .unwrap();
1186 let (k, v) = crate::schema::package_versions::store(original, 2, s2, 10);
1187 batch
1188 .put(&reader.schema().package_versions, &k, &v)
1189 .unwrap();
1190 batch.commit().unwrap();
1191
1192 assert_eq!(
1193 reader.get_package_version_storage_id(original, 1).unwrap(),
1194 Some(s1),
1195 );
1196 assert_eq!(
1197 reader.get_package_version_storage_id(original, 2).unwrap(),
1198 Some(s2),
1199 );
1200 assert_eq!(
1201 reader.get_package_version_storage_id(original, 3).unwrap(),
1202 None,
1203 );
1204
1205 assert_eq!(reader.get_package_at_checkpoint(original, 0).unwrap(), None);
1206 assert_eq!(
1207 reader.get_package_at_checkpoint(original, 1).unwrap(),
1208 Some((1, s1)),
1209 );
1210 assert_eq!(
1211 reader.get_package_at_checkpoint(original, 9).unwrap(),
1212 Some((1, s1)),
1213 );
1214 assert_eq!(
1215 reader.get_package_at_checkpoint(original, 10).unwrap(),
1216 Some((2, s2)),
1217 );
1218 assert_eq!(
1219 reader
1220 .get_package_at_checkpoint(original, u64::MAX)
1221 .unwrap(),
1222 Some((2, s2)),
1223 );
1224
1225 let unknown = ObjectID::from_single_byte(0xDD);
1226 assert_eq!(
1227 reader.get_package_version_storage_id(unknown, 1).unwrap(),
1228 None,
1229 );
1230 assert_eq!(reader.get_package_at_checkpoint(unknown, 5).unwrap(), None);
1231
1232 let floor_original = ObjectID::from_single_byte(0xEE);
1236 let floor_storage = ObjectID::from_single_byte(0x03);
1237 let mut batch = db.batch();
1238 let (k, v) =
1239 crate::schema::package_versions::store_restored(floor_original, 1, floor_storage);
1240 batch
1241 .put(&reader.schema().package_versions, &k, &v)
1242 .unwrap();
1243 let (k, v) = crate::schema::package_versions::store(floor_original, 2, s2, 10);
1244 batch
1245 .put(&reader.schema().package_versions, &k, &v)
1246 .unwrap();
1247 batch.commit().unwrap();
1248
1249 assert_eq!(
1250 reader.get_package_at_checkpoint(floor_original, 0).unwrap(),
1251 Some((1, floor_storage)),
1252 );
1253 assert_eq!(
1254 reader
1255 .get_package_at_checkpoint(floor_original, 10)
1256 .unwrap(),
1257 Some((2, s2)),
1258 );
1259 }
1260}