Skip to main content

sui_rpc_store/reader/
indexes.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! [`RpcIndexes`] adapter — owned-object, dynamic-field, balance,
5//! package-versions, epoch-info, ledger-history, and coin-info
6//! lookups.
7//!
8//! Trait methods returning `Result` over [`typed_store_error::TypedStoreError`]
9//! wrap our [`sui_consistent_store::error::Error`] via
10//! [`TypedStoreError::RocksDBError`].
11
12use 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
34/// Local alias matching the inaccessible
35/// `sui_types::storage::read_store::PackageVersionsIterator`.
36type 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
125/// Find the object id of the first row whose Move type matches the
126/// pinned `struct_tag`. The `object_by_type` index sorts by
127/// `(type, id)`, so the first prefix-scan row IS the lowest-id
128/// match — there should only be at most one in practice for the
129/// coin-wrapper types this is used with (CoinMetadata, TreasuryCap,
130/// RegulatedCoinMetadata are all unique per coin type).
131fn 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        // When resuming, the page token carries the cursor object's full sort
168        // position -- type, balance, and id -- so the scan seeks straight to it
169        // (inclusive) and stops at the end of the prefix. No post-filtering.
170        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        // Dynamic fields are `Field<Name, Value>` objects owned (in the
207        // object-owner sense) by `parent`, so they share the `object_by_owner`
208        // index with address-owned objects. The cursor carries the field's
209        // type and id -- its full sort position -- so the scan seeks straight
210        // to it. Field objects are never coins, so the balance component is
211        // always absent.
212        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        // Coin metadata / treasury cap / regulated coin metadata
240        // are typed objects whose Move type wraps the requested
241        // `coin_type`. Discover each via an `object_by_type`
242        // prefix scan keyed on the corresponding wrapper struct
243        // tag and take the first match.
244        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        // Report the coin and address halves independently (each clamped to
279        // non-negative); the caller sums them for the total. Reporting the
280        // total as `coin_balance` would double-count the address half once
281        // the caller adds them back together.
282        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        // When resuming, the page token carries the cursor's coin type -- the
296        // index's sort key after the owner -- so the scan seeks straight to it
297        // (inclusive) and stops at the end of the owner's balances.
298        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            // Project the merged proto value back into the typed
314            // `Balance` view through the same decoder `get_balance`
315            // uses, so a malformed payload surfaces as an error here
316            // too rather than being silently read as zero.
317            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            // Report the coin and address halves independently; the caller
322            // sums them for the total (reporting the total here would
323            // double-count the address half).
324            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        // The cursor passed in by `list_package_versions` is the version of the
345        // first row of the next page (the previous page popped its
346        // `page_size + 1`th row to derive it), so seek straight to it and
347        // resume inclusively. Versions sort ascending within a package, so the
348        // seek lands on the first row with `version >= cursor`.
349        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            // Decode storage_id (32 bytes).
364            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        // Min across the registered pipelines' watermarks: every CF
398        // this reader serves has caught up to at least this
399        // checkpoint, so reads against any of them are coherent
400        // through here. Bounded to the registered set -- rather than
401        // every row in the framework CF -- so a stale watermark left
402        // behind by a pipeline that is no longer registered cannot
403        // pin the reported tip forever. `None` while any registered
404        // pipeline has no watermark yet: nothing is fully indexed,
405        // so nothing is served (fail closed).
406        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        // Only the live cohort (owned objects, types, balances); the
413        // ledger-history cohort backfills independently and is excluded so a
414        // node is healthy as soon as its live-object reads are caught up.
415        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
541/// Project a `(transaction_bitmap::Key, Protobuf<BitmapBlob>)`
542/// row into a [`LedgerBitmapBucket`], deserializing the raw
543/// RoaringBitmap bytes off the protobuf payload.
544fn 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
562/// Project an `(event_bitmap::Key, Protobuf<BitmapBlob>)` row into
563/// a [`LedgerBitmapBucket`]. Same shape as
564/// [`project_bitmap_row`] but typed against the distinct event-CF
565/// key.
566fn 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        // Buckets `[1, 3)` — only the middle bucket survives.
649        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        // Bob's bucket is not visible under Alice's dimension.
706        assert_eq!(buckets, vec![0]);
707    }
708
709    /// The indexed tip is bounded by the registered pipeline set: a
710    /// stale watermark from a pipeline that is no longer registered
711    /// (e.g. `transactions`, disabled on the embedded deployment) must
712    /// not pin the reported tip, and a registered pipeline with no
713    /// watermark yet reads as "nothing fully indexed".
714    #[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        // Nothing committed yet: nothing is fully indexed. A stale row
738        // from an unregistered pipeline does not change that.
739        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        // Only part of the registered set committed: still nothing
750        // fully indexed (fail closed).
751        stamp(crate::LIVE_COHORT, 40);
752        assert_eq!(
753            reader.get_highest_indexed_checkpoint_seq_number().unwrap(),
754            None,
755        );
756
757        // The whole registered set committed: the slowest registered
758        // pipeline bounds the tip, and the stale `transactions` row at
759        // 3 stays excluded.
760        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        // A reader for a different deployment bounds by exactly its
768        // own registered set.
769        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        // Construct a synthetic coin type and seed
863        // `object_by_type` rows for its CoinMetadata and
864        // TreasuryCap wrappers. The real on-chain pipeline writes
865        // these rows for every Move object it sees; the test
866        // bypasses pipelines and writes the rows directly.
867        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    // Pagination regression coverage for the cursor-skip iterators. The
951    // `list_*` handlers page by taking `page_size` rows and then peeking the
952    // next row as the opaque page token; a broken skip-past-cursor predicate
953    // re-yields earlier rows on every page (duplicates, and a cursor that never
954    // advances). Each test walks every page and asserts each row appears once.
955
956    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        // Bounded so a non-advancing cursor surfaces as a failed assertion
995        // rather than an infinite loop.
996        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        // A restore-floor row (no recorded publish checkpoint) counts
1233        // as always existed: before any real publish it is the only
1234        // version, and it is superseded by the first real publish.
1235        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}