1use std::collections::HashMap;
5use std::fs;
6use std::net::IpAddr;
7use std::net::Ipv4Addr;
8use std::net::SocketAddr;
9use std::path::Path;
10use std::time::Duration;
11
12use anyhow::Context;
13use anyhow::ensure;
14use diesel::ExpressionMethods;
15use diesel::OptionalExtension;
16use diesel::QueryDsl;
17use diesel_async::RunQueryDsl;
18use prost::Message;
19use reqwest::Client;
20use serde_json::Value;
21use serde_json::json;
22use simulacrum::AdvanceEpochConfig;
23use simulacrum::Simulacrum;
24use sui_futures::service::Service;
25use sui_indexer_alt::BootstrapGenesis;
26use sui_indexer_alt::config::IndexerConfig;
27use sui_indexer_alt::setup_indexer;
28use sui_indexer_alt_consistent_api::proto::rpc::consistent::v1alpha::AvailableRangeRequest;
29use sui_indexer_alt_consistent_api::proto::rpc::consistent::v1alpha::consistent_service_client::ConsistentServiceClient;
30use sui_indexer_alt_consistent_store::args::RpcArgs as ConsistentArgs;
31use sui_indexer_alt_consistent_store::args::TlsArgs as ConsistentTlsArgs;
32use sui_indexer_alt_consistent_store::config::ServiceConfig as ConsistentConfig;
33use sui_indexer_alt_consistent_store::start_service as start_consistent_store;
34use sui_indexer_alt_framework::IndexerArgs;
35use sui_indexer_alt_framework::ingestion::ClientArgs;
36use sui_indexer_alt_framework::ingestion::ingestion_client::IngestionClientArgs;
37use sui_indexer_alt_framework::pipeline::CommitterConfig;
38use sui_indexer_alt_framework::postgres::schema::watermarks;
39use sui_indexer_alt_graphql::RpcArgs as GraphQlArgs;
40use sui_indexer_alt_graphql::args::SubscriptionArgs;
41use sui_indexer_alt_graphql::config::RpcConfig as GraphQlConfig;
42use sui_indexer_alt_graphql::start_rpc as start_graphql;
43use sui_indexer_alt_reader::consistent_reader::ConsistentReaderArgs;
44use sui_indexer_alt_reader::fullnode_client::FullnodeArgs;
45use sui_indexer_alt_reader::kv_loader::KvArgs;
46use sui_indexer_alt_reader::system_package_task::SystemPackageTaskArgs;
47use sui_kv_rpc::KvRpcConfig;
48use sui_kv_rpc::KvRpcServer;
49use sui_kvstore::ALL_PIPELINE_NAMES;
50use sui_kvstore::BigTableClient;
51use sui_kvstore::BigTableIndexer;
52use sui_kvstore::CHECKPOINTS_PIPELINE;
53use sui_kvstore::IndexerConfig as BtIndexerConfig;
54use sui_kvstore::IngestionConfig as BtIngestionConfig;
55use sui_kvstore::KeyValueStoreReader;
56use sui_kvstore::PipelineLayer;
57use sui_kvstore::testing::BigTableEmulator;
58use sui_kvstore::testing::INSTANCE_ID;
59use sui_kvstore::testing::create_tables;
60use sui_pg_db::Db;
61use sui_pg_db::DbArgs;
62use sui_pg_db::temp::TempDb;
63use sui_pg_db::temp::get_available_port;
64use sui_protocol_config::Chain;
65use sui_rpc::field::FieldMask;
66use sui_rpc::field::FieldMaskUtil;
67use sui_rpc::merge::Merge;
68use sui_rpc::proto::sui::rpc;
69use sui_types::base_types::ObjectRef;
70use sui_types::base_types::SuiAddress;
71use sui_types::crypto::AccountKeyPair;
72use sui_types::effects::TransactionEffects;
73use sui_types::error::ExecutionError;
74use sui_types::full_checkpoint_content::Checkpoint;
75use sui_types::messages_checkpoint::VerifiedCheckpoint;
76use sui_types::transaction::Transaction;
77use tempfile::TempDir;
78use tokio::time::error::Elapsed;
79use tokio::time::interval;
80use tokio::try_join;
81use url::Url;
82
83pub mod coin_registry;
84pub mod find;
85pub mod graphql;
86pub mod move_helpers;
87pub mod transaction;
88
89pub struct FullCluster {
92 executor: Simulacrum,
94
95 offchain: OffchainCluster,
98
99 #[allow(unused)]
101 temp_dir: TempDir,
102}
103
104pub struct OffchainCluster {
112 consistent_listen_address: SocketAddr,
114
115 graphql_listen_address: SocketAddr,
117
118 kv_rpc_listen_address: SocketAddr,
120
121 kv_rpc_plaintext_listen_address: Option<SocketAddr>,
124
125 bigtable_client: BigTableClient,
127
128 db: Db,
130
131 pipelines: Vec<&'static str>,
133
134 #[allow(unused)]
137 services: Service,
138
139 #[allow(unused)]
141 bigtable_emulator: BigTableEmulator,
142
143 #[allow(unused)]
145 database: TempDb,
146
147 #[allow(unused)]
150 dir: TempDir,
151}
152
153pub struct OffchainClusterConfig {
154 pub indexer_args: IndexerArgs,
155 pub consistent_indexer_args: IndexerArgs,
156 pub fullnode_args: FullnodeArgs,
157 pub indexer_config: IndexerConfig,
158 pub consistent_config: ConsistentConfig,
159 pub graphql_config: GraphQlConfig,
160 pub bootstrap_genesis: Option<BootstrapGenesis>,
161 pub kv_rpc_config: KvRpcConfig,
162 pub bt_pipeline_layer: PipelineLayer,
164 pub kv_rpc_plaintext_listener: bool,
168}
169
170impl FullCluster {
171 pub async fn new() -> anyhow::Result<Self> {
174 Self::new_with_configs(
175 Simulacrum::new(),
176 OffchainClusterConfig::default(),
177 &prometheus::Registry::new(),
178 )
179 .await
180 }
181
182 pub async fn new_with_configs(
186 mut executor: Simulacrum,
187 offchain_cluster_config: OffchainClusterConfig,
188 registry: &prometheus::Registry,
189 ) -> anyhow::Result<Self> {
190 let (client_args, temp_dir) = local_ingestion_client_args();
191 executor.set_data_ingestion_path(temp_dir.path().to_owned());
192
193 let offchain = OffchainCluster::new(client_args, offchain_cluster_config, registry)
194 .await
195 .context("Failed to create off-chain cluster")?;
196
197 Ok(Self {
198 executor,
199 offchain,
200 temp_dir,
201 })
202 }
203
204 pub fn reference_gas_price(&self) -> u64 {
206 self.executor.reference_gas_price()
207 }
208
209 pub fn funded_account(
212 &mut self,
213 amount: u64,
214 ) -> anyhow::Result<(SuiAddress, AccountKeyPair, ObjectRef)> {
215 self.executor.funded_account(amount)
216 }
217
218 pub fn request_gas(
221 &mut self,
222 address: SuiAddress,
223 amount: u64,
224 ) -> anyhow::Result<TransactionEffects> {
225 self.executor.request_gas(address, amount)
226 }
227
228 pub fn execute_transaction(
230 &mut self,
231 tx: Transaction,
232 ) -> anyhow::Result<(TransactionEffects, Option<ExecutionError>)> {
233 self.executor.execute_transaction(tx)
234 }
235
236 pub fn advance_clock(&mut self, duration: Duration) -> TransactionEffects {
238 self.executor.advance_clock(duration)
239 }
240
241 pub fn advance_epoch(&mut self) {
245 self.executor.advance_epoch(AdvanceEpochConfig::default());
246 }
247
248 pub async fn create_checkpoint(&mut self) -> VerifiedCheckpoint {
252 let checkpoint = self.executor.create_checkpoint();
253 let timeout = Duration::from_secs(100);
254 let indexer = self
255 .offchain
256 .wait_for_indexer(checkpoint.sequence_number, timeout);
257 let consistent_store = self
258 .offchain
259 .wait_for_consistent_store(checkpoint.sequence_number, timeout);
260 let graphql = self
261 .offchain
262 .wait_for_graphql(checkpoint.sequence_number, timeout);
263 let bigtable = self.offchain.wait_for_bigtable(
264 &ALL_PIPELINE_NAMES,
265 checkpoint.sequence_number,
266 timeout,
267 );
268 let kv_rpc = self
269 .offchain
270 .wait_for_kv_rpc(checkpoint.sequence_number, timeout);
271
272 try_join!(indexer, consistent_store, graphql, bigtable, kv_rpc)
273 .expect("Timed out waiting for off-chain services");
274
275 checkpoint
276 }
277
278 pub async fn create_checkpoint_before_list_apis_sync(&mut self) -> VerifiedCheckpoint {
286 let checkpoint = self.executor.create_checkpoint();
287 let timeout = Duration::from_secs(100);
288 self.offchain
289 .wait_for_bigtable(&[CHECKPOINTS_PIPELINE], checkpoint.sequence_number, timeout)
290 .await
291 .expect("Timed out waiting for the base checkpoints pipeline");
292
293 checkpoint
294 }
295
296 pub async fn wait_for_bigtable(
299 &self,
300 pipelines: &[&str],
301 checkpoint: u64,
302 timeout: Duration,
303 ) -> Result<(), Elapsed> {
304 self.offchain
305 .wait_for_bigtable(pipelines, checkpoint, timeout)
306 .await
307 }
308
309 pub fn db_url(&self) -> Url {
311 self.offchain.db_url()
312 }
313
314 pub fn consistent_store_url(&self) -> Url {
316 self.offchain.consistent_store_url()
317 }
318
319 pub fn graphql_url(&self) -> Url {
321 self.offchain.graphql_url()
322 }
323
324 pub fn kv_rpc_url(&self) -> Url {
326 self.offchain.kv_rpc_url()
327 }
328
329 pub fn kv_rpc_plaintext_url(&self) -> Option<Url> {
332 self.offchain.kv_rpc_plaintext_url()
333 }
334
335 pub async fn latest_checkpoint(&self) -> anyhow::Result<Option<u64>> {
338 self.offchain.latest_checkpoint().await
339 }
340
341 pub async fn wait_for_indexer(
344 &self,
345 checkpoint: u64,
346 timeout: Duration,
347 ) -> Result<(), Elapsed> {
348 self.offchain.wait_for_indexer(checkpoint, timeout).await
349 }
350
351 pub async fn wait_for_pruner(
354 &self,
355 pipeline: &str,
356 checkpoint: u64,
357 timeout: Duration,
358 ) -> Result<(), Elapsed> {
359 self.offchain
360 .wait_for_pruner(pipeline, checkpoint, timeout)
361 .await
362 }
363
364 pub async fn wait_for_graphql(
367 &self,
368 checkpoint: u64,
369 timeout: Duration,
370 ) -> Result<(), Elapsed> {
371 self.offchain.wait_for_graphql(checkpoint, timeout).await
372 }
373
374 pub async fn wait_for_kv_rpc(&self, checkpoint: u64, timeout: Duration) -> Result<(), Elapsed> {
376 self.offchain.wait_for_kv_rpc(checkpoint, timeout).await
377 }
378}
379
380impl OffchainCluster {
381 pub async fn new(
388 client_args: ClientArgs,
389 OffchainClusterConfig {
390 indexer_args,
391 consistent_indexer_args,
392 fullnode_args,
393 indexer_config,
394 consistent_config,
395 graphql_config,
396 bootstrap_genesis,
397 kv_rpc_config,
398 bt_pipeline_layer,
399 kv_rpc_plaintext_listener,
400 }: OffchainClusterConfig,
401 registry: &prometheus::Registry,
402 ) -> anyhow::Result<Self> {
403 let consistent_port = get_available_port();
404 let consistent_listen_address =
405 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), consistent_port);
406
407 let graphql_port = get_available_port();
408 let graphql_listen_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), graphql_port);
409
410 let kv_rpc_port = get_available_port();
411 let kv_rpc_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), kv_rpc_port);
412
413 let kv_rpc_plaintext_address = kv_rpc_plaintext_listener.then(|| {
414 let port = get_available_port();
415 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port)
416 });
417
418 let database = TempDb::new().context("Failed to create database")?;
419 let database_url = database.database().url();
420
421 let dir = tempfile::tempdir().context("Failed to create temporary directory")?;
422 let rocksdb_path = dir.path().join("rocksdb");
423
424 let consistent_args = ConsistentArgs {
425 rpc_listen_address: consistent_listen_address,
426 tls: ConsistentTlsArgs::default(),
427 };
428
429 let graphql_args = GraphQlArgs {
430 rpc_listen_address: graphql_listen_address,
431 no_ide: true,
432 };
433
434 let db = Db::for_read(database_url.clone(), DbArgs::default())
435 .await
436 .context("Failed to connect to database")?;
437
438 let indexer = setup_indexer(
439 database_url.clone(),
440 DbArgs::default(),
441 indexer_args,
442 client_args.clone(),
443 indexer_config,
444 bootstrap_genesis,
445 registry,
446 )
447 .await
448 .context("Failed to setup indexer")?;
449
450 let pipelines: Vec<_> = indexer.pipelines().collect();
451 let indexer = indexer.run().await.context("Failed to start indexer")?;
452
453 let consistent_store = start_consistent_store(
454 rocksdb_path,
455 consistent_indexer_args,
456 client_args.clone(),
457 consistent_args,
458 "0.0.0",
459 consistent_config,
460 registry,
461 )
462 .await
463 .context("Failed to start Consistent Store")?;
464
465 let consistent_reader_args = ConsistentReaderArgs {
466 consistent_store_url: Some(
467 Url::parse(&format!("http://{consistent_listen_address}")).unwrap(),
468 ),
469 ..Default::default()
470 };
471
472 let (bigtable_client, bigtable_emulator, archival_service) = start_archival(
473 client_args.clone(),
474 kv_rpc_address,
475 kv_rpc_plaintext_address,
476 kv_rpc_config,
477 bt_pipeline_layer,
478 registry,
479 )
480 .await?;
481
482 let kv_args = KvArgs {
483 ledger_grpc_url: Some(
484 format!("http://{kv_rpc_address}")
485 .parse()
486 .expect("Failed to parse kv-rpc URI"),
487 ),
488 ..Default::default()
489 };
490
491 let graphql = start_graphql(
492 Some(database_url.clone()),
493 fullnode_args,
494 DbArgs::default(),
495 kv_args,
496 consistent_reader_args,
497 graphql_args,
498 SystemPackageTaskArgs::default(),
499 SubscriptionArgs::default(),
500 "0.0.0",
501 graphql_config,
502 pipelines.iter().map(|p| p.to_string()).collect(),
503 registry,
504 )
505 .await
506 .context("Failed to start GraphQL server")?;
507
508 let services = indexer
509 .merge(consistent_store)
510 .merge(graphql)
511 .merge(archival_service);
512
513 Ok(Self {
514 consistent_listen_address,
515 graphql_listen_address,
516 kv_rpc_listen_address: kv_rpc_address,
517 kv_rpc_plaintext_listen_address: kv_rpc_plaintext_address,
518 bigtable_client,
519 db,
520 pipelines,
521 services,
522 bigtable_emulator,
523 database,
524 dir,
525 })
526 }
527
528 pub fn db_url(&self) -> Url {
530 self.database.database().url().clone()
531 }
532
533 pub fn consistent_store_url(&self) -> Url {
535 Url::parse(&format!("http://{}/", self.consistent_listen_address))
536 .expect("Failed to parse RPC URL")
537 }
538
539 pub fn graphql_url(&self) -> Url {
541 Url::parse(&format!("http://{}/graphql", self.graphql_listen_address))
542 .expect("Failed to parse RPC URL")
543 }
544
545 pub fn kv_rpc_url(&self) -> Url {
547 Url::parse(&format!("http://{}/", self.kv_rpc_listen_address))
548 .expect("Failed to parse RPC URL")
549 }
550
551 pub fn kv_rpc_plaintext_url(&self) -> Option<Url> {
554 let address = self.kv_rpc_plaintext_listen_address?;
555 Some(Url::parse(&format!("http://{address}/")).expect("Failed to parse RPC URL"))
556 }
557
558 pub async fn latest_checkpoint(&self) -> anyhow::Result<Option<u64>> {
561 use watermarks::dsl as w;
562
563 let mut conn = self
564 .db
565 .connect()
566 .await
567 .context("Failed to connect to database")?;
568
569 let latest: HashMap<String, i64> = w::watermarks
570 .select((w::pipeline, w::checkpoint_hi_inclusive))
571 .filter(w::pipeline.eq_any(&self.pipelines))
572 .filter(w::reader_lo.le(w::checkpoint_hi_inclusive))
573 .load(&mut conn)
574 .await?
575 .into_iter()
576 .collect();
577
578 if latest.len() != self.pipelines.len() {
579 return Ok(None);
580 }
581
582 Ok(latest.into_values().min().map(|l| l as u64))
583 }
584
585 pub async fn latest_pruner_checkpoint(&self, pipeline: &str) -> anyhow::Result<Option<u64>> {
588 use watermarks::dsl as w;
589
590 let mut conn = self
591 .db
592 .connect()
593 .await
594 .context("Failed to connect to database")?;
595
596 let latest: Option<i64> = w::watermarks
597 .select(w::reader_lo)
598 .filter(w::pipeline.eq(pipeline))
599 .first(&mut conn)
600 .await
601 .optional()?;
602
603 Ok(latest.map(|l| l as u64))
604 }
605
606 pub async fn latest_consistent_store_checkpoint(&self) -> anyhow::Result<u64> {
608 ConsistentServiceClient::connect(self.consistent_store_url().to_string())
609 .await
610 .context("Failed to connect to Consistent Store")?
611 .available_range(AvailableRangeRequest {})
612 .await
613 .context("Failed to fetch available range from Consistent Store")?
614 .into_inner()
615 .max_checkpoint
616 .context("Consistent Store has not started yet")
617 }
618
619 pub async fn latest_graphql_checkpoint(&self) -> anyhow::Result<u64> {
621 let query = json!({
622 "query": "query { checkpoint { sequenceNumber } }"
623 });
624
625 let client = Client::new();
626 let request = client.post(self.graphql_url()).json(&query);
627 let response = request
628 .send()
629 .await
630 .context("Request to GraphQL server failed")?;
631
632 let body: Value = response
633 .json()
634 .await
635 .context("Failed to parse GraphQL response")?;
636
637 let sequence_number = body
638 .pointer("/data/checkpoint/sequenceNumber")
639 .context("Failed to find checkpoint sequence number in response")?;
640
641 let sequence_number: i64 = serde_json::from_value(sequence_number.clone())
642 .context("Failed to parse sequence number as i64")?;
643
644 ensure!(sequence_number != i64::MAX, "Indexer has not started yet");
645
646 Ok(sequence_number as u64)
647 }
648
649 pub async fn latest_graphql_epoch(&self) -> anyhow::Result<u64> {
651 let query = json!({
652 "query": "query { epoch { epochId } }"
653 });
654
655 let client = Client::new();
656 let request = client.post(self.graphql_url()).json(&query);
657 let response = request
658 .send()
659 .await
660 .context("Request to GraphQL server failed")?;
661
662 let body: Value = response
663 .json()
664 .await
665 .context("Failed to parse GraphQL response")?;
666
667 let epoch_id = body
668 .pointer("/data/epoch/epochId")
669 .context("Failed to find epochId in response")?;
670
671 let epoch_id: i64 =
672 serde_json::from_value(epoch_id.clone()).context("Failed to parse epochId as i64")?;
673
674 ensure!(epoch_id != i64::MAX, "Indexer has not started yet");
675
676 Ok(epoch_id as u64)
677 }
678
679 pub async fn wait_for_indexer(
682 &self,
683 checkpoint: u64,
684 timeout: Duration,
685 ) -> Result<(), Elapsed> {
686 tokio::time::timeout(timeout, async move {
687 let mut interval = interval(Duration::from_millis(200));
688 loop {
689 interval.tick().await;
690 if matches!(self.latest_checkpoint().await, Ok(Some(l)) if l >= checkpoint) {
691 break;
692 }
693 }
694 })
695 .await
696 }
697
698 pub async fn wait_for_pruner(
701 &self,
702 pipeline: &str,
703 checkpoint: u64,
704 timeout: Duration,
705 ) -> Result<(), Elapsed> {
706 tokio::time::timeout(timeout, async move {
707 let mut interval = interval(Duration::from_millis(200));
708 loop {
709 interval.tick().await;
710 if matches!(self.latest_pruner_checkpoint(pipeline).await, Ok(Some(l)) if l >= checkpoint) {
711 break;
712 }
713 }
714 }).await
715 }
716
717 pub async fn wait_for_consistent_store(
720 &self,
721 checkpoint: u64,
722 timeout: Duration,
723 ) -> Result<(), Elapsed> {
724 tokio::time::timeout(timeout, async move {
725 let mut interval = interval(Duration::from_millis(200));
726 loop {
727 interval.tick().await;
728 if matches!(self.latest_consistent_store_checkpoint().await, Ok(l) if l >= checkpoint) {
729 break;
730 }
731 }
732 })
733 .await
734 }
735
736 pub async fn wait_for_graphql(
739 &self,
740 checkpoint: u64,
741 timeout: Duration,
742 ) -> Result<(), Elapsed> {
743 tokio::time::timeout(timeout, async move {
744 let mut interval = interval(Duration::from_millis(200));
745 loop {
746 interval.tick().await;
747 if matches!(self.latest_graphql_checkpoint().await, Ok(l) if l >= checkpoint) {
748 break;
749 }
750 }
751 })
752 .await
753 }
754
755 pub async fn wait_for_kv_rpc(&self, checkpoint: u64, timeout: Duration) -> Result<(), Elapsed> {
760 let url = self.kv_rpc_url().to_string();
761 tokio::time::timeout(timeout, async move {
762 let mut interval = interval(Duration::from_millis(200));
763 let mut client = None;
764 loop {
765 interval.tick().await;
766 if client.is_none() {
767 client =
768 rpc::v2::ledger_service_client::LedgerServiceClient::connect(url.clone())
769 .await
770 .ok();
771 }
772 let Some(client) = client.as_mut() else {
773 continue;
774 };
775 if client
776 .get_service_info(rpc::v2::GetServiceInfoRequest::default())
777 .await
778 .is_ok_and(|response| {
779 response
780 .into_inner()
781 .checkpoint_height
782 .is_some_and(|cp| cp >= checkpoint)
783 })
784 {
785 break;
786 }
787 }
788 })
789 .await
790 }
791
792 pub async fn wait_for_bigtable(
795 &self,
796 pipelines: &[&str],
797 checkpoint: u64,
798 timeout: Duration,
799 ) -> Result<(), Elapsed> {
800 let mut client = self.bigtable_client.clone();
801 tokio::time::timeout(timeout, async move {
802 let mut interval = interval(Duration::from_millis(200));
803 loop {
804 interval.tick().await;
805 if client
806 .get_watermark_for_pipelines(pipelines)
807 .await
808 .is_ok_and(|wm| {
809 wm.is_some_and(|wm| {
810 wm.checkpoint_hi_inclusive
811 .is_some_and(|cp| cp >= checkpoint)
812 })
813 })
814 {
815 break;
816 }
817 }
818 })
819 .await
820 }
821}
822
823impl Default for OffchainClusterConfig {
824 fn default() -> Self {
825 Self {
826 indexer_args: Default::default(),
827 consistent_indexer_args: Default::default(),
828 fullnode_args: FullnodeArgs::default(),
829 indexer_config: IndexerConfig::for_test(),
830 consistent_config: ConsistentConfig::for_test(),
831 graphql_config: Default::default(),
832 bootstrap_genesis: None,
833 kv_rpc_config: KvRpcConfig::default(),
834 bt_pipeline_layer: PipelineLayer::default(),
835 kv_rpc_plaintext_listener: false,
836 }
837 }
838}
839
840pub fn local_ingestion_client_args() -> (ClientArgs, TempDir) {
842 let temp_dir = tempfile::tempdir()
843 .context("Failed to create data ingestion path")
844 .unwrap();
845 let client_args = ClientArgs {
846 ingestion: IngestionClientArgs {
847 local_ingestion_path: Some(temp_dir.path().to_owned()),
848 ..Default::default()
849 },
850 ..Default::default()
851 };
852 (client_args, temp_dir)
853}
854
855pub async fn write_checkpoint(path: &Path, checkpoint: Checkpoint) -> anyhow::Result<()> {
857 let sequence_number = checkpoint.summary.sequence_number;
858
859 let mask = FieldMask::from_paths([
860 rpc::v2::Checkpoint::path_builder().sequence_number(),
861 rpc::v2::Checkpoint::path_builder().summary().bcs().value(),
862 rpc::v2::Checkpoint::path_builder().signature().finish(),
863 rpc::v2::Checkpoint::path_builder().contents().bcs().value(),
864 rpc::v2::Checkpoint::path_builder()
865 .transactions()
866 .transaction()
867 .bcs()
868 .value(),
869 rpc::v2::Checkpoint::path_builder()
870 .transactions()
871 .effects()
872 .bcs()
873 .value(),
874 rpc::v2::Checkpoint::path_builder()
875 .transactions()
876 .effects()
877 .unchanged_loaded_runtime_objects()
878 .finish(),
879 rpc::v2::Checkpoint::path_builder()
880 .transactions()
881 .events()
882 .bcs()
883 .value(),
884 rpc::v2::Checkpoint::path_builder()
885 .objects()
886 .objects()
887 .bcs()
888 .value(),
889 ]);
890
891 let proto_checkpoint = rpc::v2::Checkpoint::merge_from(&checkpoint, &mask.into());
892 let proto_bytes = proto_checkpoint.encode_to_vec();
893 let compressed = zstd::encode_all(&proto_bytes[..], 3)?;
894
895 let file_name = format!("{}.binpb.zst", sequence_number);
896 let file_path = path.join(file_name);
897 fs::write(file_path, compressed)?;
898 Ok(())
899}
900
901async fn start_archival(
903 client_args: ClientArgs,
904 kv_rpc_address: SocketAddr,
905 kv_rpc_plaintext_address: Option<SocketAddr>,
906 kv_rpc_config: KvRpcConfig,
907 bt_pipeline_layer: PipelineLayer,
908 registry: &prometheus::Registry,
909) -> anyhow::Result<(BigTableClient, BigTableEmulator, Service)> {
910 let emulator = tokio::task::spawn_blocking(BigTableEmulator::start)
911 .await
912 .context("spawn_blocking panicked")?
913 .context("Failed to start BigTable emulator")?;
914
915 create_tables(emulator.host(), INSTANCE_ID)
916 .await
917 .context("Failed to create BigTable tables")?;
918
919 let bigtable_client =
920 BigTableClient::new_local(emulator.host().to_string(), INSTANCE_ID.to_string())
921 .await
922 .context("Failed to create BigTable client")?;
923
924 let indexer_client =
925 BigTableClient::new_local(emulator.host().to_string(), INSTANCE_ID.to_string())
926 .await
927 .context("Failed to create BigTable client for indexer")?;
928 let bt_indexer = BigTableIndexer::new(
929 indexer_client,
930 IndexerArgs::default(),
931 client_args,
932 BtIngestionConfig::default(),
933 CommitterConfig::default(),
934 BtIndexerConfig::default(),
935 bt_pipeline_layer,
936 &[],
937 Chain::Unknown,
938 registry,
939 )
940 .await
941 .context("Failed to create BigTable indexer")?;
942
943 let bt_indexer_service = bt_indexer
946 .run()
947 .await
948 .context("Failed to start BigTable indexer")?;
949
950 let kv_rpc_server = KvRpcServer::new_local_with_config(
951 emulator.host().to_string(),
952 INSTANCE_ID.to_string(),
953 None,
954 kv_rpc_config.ledger_history(),
955 kv_rpc_config.request_bigtable_concurrency(),
956 kv_rpc_config.monotonic_read_wait_timeout(),
957 kv_rpc_config.stages(),
958 kv_rpc_config.watermark_source(),
959 )
960 .await
961 .context("Failed to create KvRpcServer")?;
962 let kv_rpc_service = kv_rpc_server
963 .start_service(
964 kv_rpc_address,
965 sui_kv_rpc::ServerConfig {
966 plaintext_address: kv_rpc_plaintext_address,
967 ..Default::default()
968 },
969 )
970 .await
971 .context("Failed to start kv-rpc server")?;
972
973 let service = bt_indexer_service.merge(kv_rpc_service);
974 Ok((bigtable_client, emulator, service))
975}