1pub mod balance;
21pub mod checkpoint_broadcast;
22pub mod checkpoint_contents;
23pub mod checkpoint_seq_by_digest;
24pub mod checkpoint_summary;
25pub mod effects;
26pub mod epochs;
27pub mod event_bitmap;
28pub mod events;
29pub mod object_by_owner;
30pub mod object_by_type;
31pub mod object_version_by_checkpoint;
32pub mod objects;
33pub mod package_versions;
34pub mod pruner;
35pub mod restore;
36pub mod transaction_bitmap;
37pub mod transactions;
38pub mod tx_metadata_by_seq;
39pub mod tx_seq_by_digest;
40
41use std::collections::BTreeMap;
42use std::collections::HashSet;
43use std::collections::btree_map::Entry;
44use std::path::Path;
45use std::sync::Arc;
46
47use anyhow::Context as _;
48use prometheus::Registry;
49use sui_consistent_store::Db;
50use sui_consistent_store::DbOptions;
51use sui_consistent_store::PipelineTaskKey;
52use sui_consistent_store::Synchronizer;
53use sui_consistent_store::restore_state;
54use sui_indexer_alt_framework as framework;
55use sui_indexer_alt_framework::IndexerArgs;
56use sui_indexer_alt_framework::ingestion::ArcStreamingClient;
57use sui_indexer_alt_framework::ingestion::IngestionConfig;
58use sui_indexer_alt_framework::ingestion::ingestion_client::IngestionClient;
59use sui_indexer_alt_framework::pipeline::CommitterConfig;
60use sui_indexer_alt_framework::pipeline::sequential::SequentialConfig;
61use sui_indexer_alt_framework::pipeline::sequential::{self};
62use sui_indexer_alt_framework::service::Service;
63use sui_types::base_types::ObjectID;
64use sui_types::digests::ObjectDigest;
65use sui_types::effects::TransactionEffectsAPI;
66use sui_types::full_checkpoint_content::Checkpoint;
67use sui_types::object::Object;
68
69use crate::RpcStoreSchema;
70use crate::config::PipelineLayer;
71use crate::config::PrunerConfig;
72use crate::indexer::pruner::PrunerMetrics;
73
74pub const METRICS_PREFIX: &str = "rpc_store_indexer";
80
81pub type Schema = RpcStoreSchema;
84
85pub type Store = sui_consistent_store::Store<Schema>;
87
88pub fn first_tx_seq(checkpoint: &Checkpoint) -> u64 {
95 checkpoint.summary.network_total_transactions - checkpoint.transactions.len() as u64
96}
97
98pub fn tx_seq_at(checkpoint: &Checkpoint, i: usize) -> u64 {
101 first_tx_seq(checkpoint) + i as u64
102}
103
104pub fn checkpoint_input_objects(
115 checkpoint: &Checkpoint,
116) -> anyhow::Result<BTreeMap<ObjectID, (&Object, ObjectDigest)>> {
117 let mut from_this_checkpoint = HashSet::new();
118 let mut input_objects = BTreeMap::new();
119 for tx in &checkpoint.transactions {
120 let input_objects_map: BTreeMap<_, _> = tx
121 .input_objects(&checkpoint.object_set)
122 .map(|obj| ((obj.id(), obj.version()), obj))
123 .collect();
124
125 for change in tx.effects.object_changes() {
126 let id = change.id;
127
128 let Some(version) = change.input_version else {
129 continue;
130 };
131
132 if from_this_checkpoint.contains(&id) {
133 continue;
134 }
135
136 let Entry::Vacant(entry) = input_objects.entry(id) else {
137 continue;
138 };
139
140 let input_object = *input_objects_map
141 .get(&(id, version))
142 .with_context(|| format!("{id} at {version} in effects, not in input_objects"))?;
143
144 let digest = change.input_digest.unwrap_or_else(|| input_object.digest());
147 entry.insert((input_object, digest));
148 }
149
150 for change in tx.effects.object_changes() {
151 if change.output_version.is_some() {
152 from_this_checkpoint.insert(change.id);
153 }
154 }
155 }
156 Ok(input_objects)
157}
158
159pub fn checkpoint_output_objects(
168 checkpoint: &Checkpoint,
169) -> anyhow::Result<BTreeMap<ObjectID, (&Object, ObjectDigest)>> {
170 let mut output_objects = BTreeMap::new();
171 for tx in &checkpoint.transactions {
172 let output_objects_map: BTreeMap<_, _> = tx
173 .output_objects(&checkpoint.object_set)
174 .map(|obj| ((obj.id(), obj.version()), obj))
175 .collect();
176
177 for change in tx.effects.object_changes() {
178 let id = change.id;
179
180 output_objects.remove(&id);
182
183 let (Some(version), Some(digest)) = (change.output_version, change.output_digest)
184 else {
185 continue;
186 };
187
188 let output_object = *output_objects_map
189 .get(&(id, version))
190 .with_context(|| format!("{id} at {version} in effects, not in output_objects"))?;
191
192 output_objects.insert(id, (output_object, digest));
193 }
194 }
195 Ok(output_objects)
196}
197
198pub struct Indexer {
222 indexer: framework::Indexer<Store>,
223
224 sync: Synchronizer,
228
229 pruner: Option<(PrunerConfig, Arc<PrunerMetrics>)>,
234}
235
236impl Indexer {
237 #[allow(clippy::too_many_arguments)]
254 pub async fn new(
255 path: impl AsRef<Path>,
256 indexer_args: IndexerArgs,
257 ingestion_client: IngestionClient,
258 streaming_client: Option<ArcStreamingClient>,
259 consistency_config: crate::config::ConsistencyConfig,
260 pruner_config: Option<PrunerConfig>,
261 ingestion_config: IngestionConfig,
262 db_options: DbOptions,
263 registry: &Registry,
264 ) -> anyhow::Result<Self> {
265 let (db, schema) = Db::open::<RpcStoreSchema>(path, db_options)
266 .context("Failed to open sui-rpc-store database")?;
267 let store = sui_consistent_store::Store::new(db, Arc::new(schema));
268 Self::from_store(
269 store,
270 indexer_args,
271 ingestion_client,
272 streaming_client,
273 consistency_config,
274 pruner_config,
275 ingestion_config,
276 registry,
277 )
278 .await
279 }
280
281 #[allow(clippy::too_many_arguments)]
288 pub async fn from_store(
289 store: Store,
290 indexer_args: IndexerArgs,
291 ingestion_client: IngestionClient,
292 streaming_client: Option<ArcStreamingClient>,
293 consistency_config: crate::config::ConsistencyConfig,
294 pruner_config: Option<PrunerConfig>,
295 ingestion_config: IngestionConfig,
296 registry: &Registry,
297 ) -> anyhow::Result<Self> {
298 let metrics_prefix = Some(METRICS_PREFIX);
299
300 let sync = Synchronizer::new(
301 store.db().clone(),
302 consistency_config.buffer_size,
303 indexer_args.first_checkpoint,
304 );
305
306 let indexer = framework::Indexer::with_ingestion_clients(
307 store,
308 indexer_args,
309 ingestion_client,
310 streaming_client,
311 ingestion_config,
312 metrics_prefix,
313 registry,
314 )
315 .await
316 .context("Failed to construct framework indexer")?;
317
318 let pruner = pruner_config.map(|config| (config, PrunerMetrics::new(None, registry)));
321
322 Ok(Self {
323 indexer,
324 sync,
325 pruner,
326 })
327 }
328
329 pub fn store(&self) -> &Store {
333 self.indexer.store()
334 }
335
336 pub fn pipelines(&self) -> impl Iterator<Item = &'static str> + '_ {
342 self.indexer.pipelines()
343 }
344
345 pub async fn add_pipelines(
355 &mut self,
356 layer: PipelineLayer,
357 committer: CommitterConfig,
358 ) -> anyhow::Result<()> {
359 let PipelineLayer {
360 epochs,
361 checkpoint_summary,
362 checkpoint_contents,
363 checkpoint_seq_by_digest,
364 transactions,
365 tx_seq_by_digest,
366 tx_metadata_by_seq,
367 effects,
368 events,
369 objects,
370 object_version_by_checkpoint,
371 object_by_owner,
372 object_by_type,
373 balance,
374 package_versions,
375 transaction_bitmap,
376 event_bitmap,
377 } = layer;
378
379 macro_rules! add {
380 ($handler:expr, $cfg:expr) => {
381 if let Some(layer) = $cfg {
382 self.sequential_pipeline(
383 $handler,
384 SequentialConfig {
385 committer: layer.finish(committer.clone()),
386 max_batch_checkpoints: Some(1),
391 ..Default::default()
392 },
393 )
394 .await?
395 }
396 };
397 }
398
399 add!(self::epochs::Epochs, epochs);
401 add!(
402 self::checkpoint_summary::CheckpointSummary,
403 checkpoint_summary
404 );
405 add!(
406 self::checkpoint_contents::CheckpointContents,
407 checkpoint_contents
408 );
409 add!(
410 self::checkpoint_seq_by_digest::CheckpointSeqByDigest,
411 checkpoint_seq_by_digest
412 );
413 add!(self::transactions::Transactions, transactions);
414 add!(self::tx_seq_by_digest::TxSeqByDigest, tx_seq_by_digest);
415 add!(
416 self::tx_metadata_by_seq::TxMetadataBySeq,
417 tx_metadata_by_seq
418 );
419 add!(self::effects::Effects, effects);
420 add!(self::events::Events, events);
421 add!(self::objects::Objects, objects);
422 let ovbc_anchor = self::object_version_by_checkpoint::restored_anchor(self.store().db())?;
427 add!(
428 self::object_version_by_checkpoint::ObjectVersionByCheckpoint::with_anchor(ovbc_anchor),
429 object_version_by_checkpoint
430 );
431
432 add!(self::object_by_owner::ObjectByOwner, object_by_owner);
434 add!(self::object_by_type::ObjectByType, object_by_type);
435 add!(self::balance::Balance, balance);
436 add!(self::package_versions::PackageVersions, package_versions);
437 add!(
438 self::transaction_bitmap::TransactionBitmap,
439 transaction_bitmap
440 );
441 add!(self::event_bitmap::EventBitmap, event_bitmap);
442
443 Ok(())
444 }
445
446 pub async fn add_checkpoint_broadcast(
461 &mut self,
462 sender: tokio::sync::broadcast::Sender<Arc<Checkpoint>>,
463 committer: CommitterConfig,
464 ) -> anyhow::Result<()> {
465 self.sequential_pipeline(
466 self::checkpoint_broadcast::CheckpointBroadcast::new(sender),
467 SequentialConfig {
468 committer,
469 max_batch_checkpoints: Some(1),
470 ..Default::default()
471 },
472 )
473 .await
474 }
475
476 async fn sequential_pipeline<H>(
491 &mut self,
492 handler: H,
493 config: SequentialConfig,
494 ) -> anyhow::Result<()>
495 where
496 H: sequential::Handler<Store = Store> + Send + Sync + 'static,
497 {
498 let restore_state = self
499 .store()
500 .db()
501 .framework()
502 .restore
503 .get(&PipelineTaskKey::new(H::NAME))
504 .with_context(|| format!("Reading restore state for pipeline {:?}", H::NAME))?;
505
506 if let Some(state) = restore_state.as_ref().and_then(|s| s.state.as_ref()) {
507 match state {
508 restore_state::State::InProgress(_) => {
509 anyhow::bail!("Restoration in progress for pipeline {:?}", H::NAME);
510 }
511 restore_state::State::Complete(_) => {
512 }
514 }
515 }
516
517 self.sync
518 .register_pipeline(H::NAME)
519 .with_context(|| format!("Failed to add pipeline {:?} to synchronizer", H::NAME))?;
520
521 self.indexer
522 .sequential_pipeline(handler, config)
523 .await
524 .with_context(|| format!("Failed to add pipeline {:?} to indexer", H::NAME))?;
525
526 Ok(())
527 }
528
529 pub async fn run(self) -> anyhow::Result<Service> {
533 let Self {
534 indexer,
535 sync,
536 pruner: pruner_setup,
537 } = self;
538
539 let store = indexer.store().clone();
542
543 let mut sync_join_set = indexer
544 .store()
545 .install_sync(sync)
546 .context("Failed to install synchronizer onto store")?;
547
548 let s_sync = Service::new().spawn(async move {
554 while let Some(res) = sync_join_set.join_next().await {
555 res.context("Synchronizer task panicked")??;
556 }
557 Ok(())
558 });
559
560 let s_indexer = indexer.run().await?;
561 let mut service = s_indexer.attach(s_sync);
562
563 if let Some((config, metrics)) = pruner_setup {
567 let s_pruner = pruner::start_pruner(store, config, metrics)
568 .context("Failed to start the rpc-store pruner")?;
569 service = service.attach(s_pruner);
570 }
571
572 Ok(service)
573 }
574}
575
576#[cfg(test)]
577mod tests {
578 use async_trait::async_trait;
579 use sui_indexer_alt_framework::ingestion::ingestion_client::CheckpointError;
580 use sui_indexer_alt_framework::ingestion::ingestion_client::CheckpointResult;
581 use sui_indexer_alt_framework::ingestion::ingestion_client::IngestionClientTrait;
582 use sui_indexer_alt_framework::metrics::IngestionMetrics;
583 use sui_types::digests::ChainIdentifier;
584
585 use super::*;
586
587 struct StubIngestionClient;
594
595 #[async_trait]
596 impl IngestionClientTrait for StubIngestionClient {
597 async fn chain_id(&self) -> anyhow::Result<ChainIdentifier> {
598 Ok(ChainIdentifier::from(
599 sui_types::digests::CheckpointDigest::new([0u8; 32]),
600 ))
601 }
602
603 async fn checkpoint(&self, _checkpoint: u64) -> CheckpointResult {
604 Err(CheckpointError::NotFound)
605 }
606
607 async fn latest_checkpoint_number(&self) -> anyhow::Result<u64> {
608 Ok(0)
609 }
610 }
611
612 async fn build_indexer(layer: PipelineLayer) -> Indexer {
613 let dir = tempfile::tempdir().unwrap();
614 let registry = Registry::new();
615 let ingestion_metrics = IngestionMetrics::new(Some(METRICS_PREFIX), ®istry);
616 let ingestion_client =
617 IngestionClient::from_trait(Arc::new(StubIngestionClient), ingestion_metrics);
618 let mut indexer = Indexer::new(
619 dir.path().join("db"),
620 IndexerArgs::default(),
621 ingestion_client,
622 None,
623 crate::config::ConsistencyConfig::default(),
624 None,
625 IngestionConfig::default(),
626 DbOptions::default(),
627 ®istry,
628 )
629 .await
630 .expect("Indexer::new");
631
632 indexer
633 .add_pipelines(layer, CommitterConfig::default())
634 .await
635 .expect("add_pipelines");
636
637 std::mem::forget(dir);
641 indexer
642 }
643
644 #[tokio::test]
655 async fn embedded_registers_only_cohort_pipelines() {
656 let indexer = build_indexer(PipelineLayer::embedded()).await;
657 let names: std::collections::BTreeSet<_> = indexer.pipelines().collect();
658 let expected: std::collections::BTreeSet<_> = crate::indexer::restore::LIVE_COHORT
659 .iter()
660 .chain(crate::indexer::restore::HISTORY_COHORT)
661 .copied()
662 .collect();
663 assert_eq!(names, expected);
664 }
665
666 #[tokio::test]
668 async fn all_registers_every_pipeline() {
669 let indexer = build_indexer(PipelineLayer::all()).await;
670 let names: std::collections::BTreeSet<_> = indexer.pipelines().collect();
671 assert_eq!(
672 names,
673 std::collections::BTreeSet::from([
674 "epochs",
676 "checkpoint_summary",
677 "checkpoint_contents",
678 "checkpoint_seq_by_digest",
679 "transactions",
680 "tx_seq_by_digest",
681 "tx_metadata_by_seq",
682 "effects",
683 "events",
684 "objects",
685 "object_version_by_checkpoint",
686 "object_by_owner",
688 "object_by_type",
689 "balance",
690 "package_versions",
691 "transaction_bitmap",
692 "event_bitmap",
693 ])
694 );
695 }
696
697 fn open_with_seeded_restore(
704 path: &std::path::Path,
705 pipeline: &str,
706 state: sui_consistent_store::RestoreState,
707 ) -> Store {
708 let (db, schema) = Db::open::<RpcStoreSchema>(path, DbOptions::default()).unwrap();
709 let framework = sui_consistent_store::FrameworkSchema::new(db.clone());
710 let mut batch = db.batch();
711 batch
712 .put(&framework.restore, &PipelineTaskKey::new(pipeline), &state)
713 .unwrap();
714 batch.commit().unwrap();
715 sui_consistent_store::Store::new(db, Arc::new(schema))
716 }
717
718 async fn build_indexer_with_store(store: Store) -> anyhow::Result<Indexer> {
719 let registry = Registry::new();
720 let ingestion_metrics = IngestionMetrics::new(Some(METRICS_PREFIX), ®istry);
721 let ingestion_client =
722 IngestionClient::from_trait(Arc::new(StubIngestionClient), ingestion_metrics);
723 Indexer::from_store(
724 store,
725 IndexerArgs::default(),
726 ingestion_client,
727 None,
728 crate::config::ConsistencyConfig::default(),
729 None,
730 IngestionConfig::default(),
731 ®istry,
732 )
733 .await
734 }
735
736 #[tokio::test]
737 async fn add_pipelines_refuses_pipeline_with_in_progress_restore() {
738 let dir = tempfile::tempdir().unwrap();
739 let in_progress = sui_consistent_store::RestoreState {
740 state: Some(restore_state::State::InProgress(
741 restore_state::InProgress::default(),
742 )),
743 };
744 let store = open_with_seeded_restore(&dir.path().join("db"), "balance", in_progress);
745
746 let mut indexer = build_indexer_with_store(store).await.unwrap();
747 let err = indexer
748 .add_pipelines(
749 PipelineLayer {
750 balance: Some(crate::config::CommitterLayer::default()),
751 ..PipelineLayer::default()
752 },
753 CommitterConfig::default(),
754 )
755 .await
756 .unwrap_err();
757 assert!(
758 format!("{err:#}").contains("Restoration in progress for pipeline"),
759 "expected restore-in-progress error, got: {err:#}",
760 );
761 }
762
763 #[tokio::test]
764 async fn add_pipelines_allows_pipeline_with_completed_restore() {
765 let dir = tempfile::tempdir().unwrap();
766 let complete = sui_consistent_store::RestoreState {
767 state: Some(restore_state::State::Complete(restore_state::Complete {
768 restored_at: 42,
769 })),
770 };
771 let store = open_with_seeded_restore(&dir.path().join("db"), "balance", complete);
772
773 let mut indexer = build_indexer_with_store(store).await.unwrap();
774 indexer
775 .add_pipelines(
776 PipelineLayer {
777 balance: Some(crate::config::CommitterLayer::default()),
778 ..PipelineLayer::default()
779 },
780 CommitterConfig::default(),
781 )
782 .await
783 .unwrap();
784 let names: std::collections::BTreeSet<_> = indexer.pipelines().collect();
785 assert_eq!(names, std::collections::BTreeSet::from(["balance"]));
786 }
787}