1use crate::db_tool::{DbToolCommand, execute_db_tool_command, print_db_all_tables};
5use crate::{
6 ConciseObjectOutput, GroupedObjectOutput, SnapshotVerifyMode, VerboseObjectOutput,
7 check_completed_snapshot, download_db_snapshot, download_formal_snapshot,
8 get_latest_available_epoch, get_object, get_transaction_block, make_clients,
9 restore_from_db_checkpoint,
10};
11use anyhow::Result;
12use consensus_core::storage::{Store, rocksdb_store::RocksDBStore};
13use consensus_core::{BlockAPI, CommitAPI, CommitRange};
14use futures::TryStreamExt;
15use futures::future::join_all;
16use std::path::PathBuf;
17use std::{collections::BTreeMap, env, sync::Arc};
18use sui_config::genesis::Genesis;
19use sui_core::authority_client::AuthorityAPI;
20use sui_protocol_config::Chain;
21use sui_replay::{ReplayToolCommand, execute_replay_command};
22use sui_rpc_api::Client;
23use sui_types::gas_coin::GasCoin;
24use sui_types::messages_consensus::ConsensusTransaction;
25use sui_types::transaction::Transaction;
26use telemetry_subscribers::TracingHandle;
27
28use sui_types::{
29 base_types::*, crypto::AuthorityPublicKeyBytes, messages_grpc::TransactionInfoRequest,
30};
31
32use clap::*;
33use sui_config::Config;
34use sui_config::object_storage_config::{ObjectStoreConfig, ObjectStoreType};
35use sui_types::messages_checkpoint::{
36 CheckpointRequest, CheckpointResponse, CheckpointSequenceNumber,
37};
38
39#[derive(Parser, Clone, ValueEnum)]
40pub enum Verbosity {
41 Grouped,
42 Concise,
43 Verbose,
44}
45
46#[derive(Parser)]
47pub enum ToolCommand {
48 #[command(name = "scan-consensus-commits")]
49 ScanConsensusCommits {
50 #[arg(long = "db-path")]
51 db_path: String,
52 #[arg(long = "start-commit")]
53 start_commit: Option<u32>,
54 #[arg(long = "end-commit")]
55 end_commit: Option<u32>,
56 },
57
58 #[command(name = "locked-object")]
60 LockedObject {
61 #[arg(long, help = "The object ID to fetch")]
64 id: Option<ObjectID>,
65 #[arg(long = "address")]
68 address: Option<SuiAddress>,
69 #[arg(long = "fullnode-rpc-url")]
71 fullnode_rpc_url: String,
72 #[arg(long = "rescue")]
74 rescue: bool,
75 },
76
77 #[command(name = "fetch-object")]
79 FetchObject {
80 #[arg(long, help = "The object ID to fetch")]
81 id: ObjectID,
82
83 #[arg(long, help = "Fetch object at a specific sequence")]
84 version: Option<u64>,
85
86 #[arg(
87 long,
88 help = "Validator to fetch from - if not specified, all validators are queried"
89 )]
90 validator: Option<AuthorityName>,
91
92 #[arg(long = "fullnode-rpc-url")]
94 fullnode_rpc_url: String,
95
96 #[arg(
105 value_enum,
106 long = "verbosity",
107 default_value = "grouped",
108 ignore_case = true
109 )]
110 verbosity: Verbosity,
111
112 #[arg(
113 long = "concise-no-header",
114 help = "don't show header in concise output"
115 )]
116 concise_no_header: bool,
117 },
118
119 #[command(name = "fetch-transaction")]
121 FetchTransaction {
122 #[arg(long = "fullnode-rpc-url")]
124 fullnode_rpc_url: String,
125
126 #[arg(long, help = "The transaction ID to fetch")]
127 digest: TransactionDigest,
128
129 #[arg(long = "show-tx")]
131 show_input_tx: bool,
132 },
133
134 #[command(name = "db-tool")]
136 DbTool {
137 #[arg(long = "db-path")]
139 db_path: String,
140 #[command(subcommand)]
141 cmd: Option<DbToolCommand>,
142 },
143 #[command(name = "dump-packages")]
149 DumpPackages {
150 #[clap(long, short)]
152 rpc_url: String,
153
154 #[clap(long, short)]
156 output_dir: PathBuf,
157
158 #[clap(long)]
161 before_checkpoint: Option<u64>,
162
163 #[clap(short, long = "verbose")]
166 verbose: bool,
167 },
168
169 #[command(name = "dump-validators")]
170 DumpValidators {
171 #[arg(long = "genesis")]
172 genesis: PathBuf,
173
174 #[arg(
175 long = "concise",
176 help = "show concise output - name, protocol key and network address"
177 )]
178 concise: bool,
179 },
180
181 #[command(name = "dump-genesis")]
182 DumpGenesis {
183 #[arg(long = "genesis")]
184 genesis: PathBuf,
185 },
186
187 #[command(name = "fetch-checkpoint")]
190 FetchCheckpoint {
191 #[arg(long = "fullnode-rpc-url")]
193 fullnode_rpc_url: String,
194
195 #[arg(long, help = "Fetch checkpoint at a specific sequence number")]
196 sequence_number: Option<CheckpointSequenceNumber>,
197 },
198
199 #[command(name = "anemo")]
200 Anemo {
201 #[command(next_help_heading = "foo", flatten)]
202 args: anemo_cli::Args,
203 },
204
205 #[command(name = "restore-db")]
206 RestoreFromDBCheckpoint {
207 #[arg(long = "config-path")]
208 config_path: PathBuf,
209 #[arg(long = "db-checkpoint-path")]
210 db_checkpoint_path: PathBuf,
211 },
212
213 #[clap(
214 name = "download-db-snapshot",
215 about = "Downloads the legacy database snapshot via cloud object store, outputs to local disk"
216 )]
217 DownloadDBSnapshot {
218 #[clap(long = "epoch", conflicts_with = "latest")]
219 epoch: Option<u64>,
220 #[clap(
221 long = "path",
222 help = "the path to write the downloaded snapshot files"
223 )]
224 path: PathBuf,
225 #[clap(long = "skip-indexes")]
227 skip_indexes: bool,
228 #[clap(long = "num-parallel-downloads")]
230 num_parallel_downloads: Option<usize>,
231 #[clap(long = "network", default_value = "mainnet")]
235 network: Chain,
236 #[clap(long = "snapshot-bucket", conflicts_with = "no_sign_request")]
239 snapshot_bucket: Option<String>,
240 #[clap(
242 long = "snapshot-bucket-type",
243 conflicts_with = "no_sign_request",
244 help = "Required if --no-sign-request is not set"
245 )]
246 snapshot_bucket_type: Option<ObjectStoreType>,
247 #[clap(
250 long = "snapshot-path",
251 help = "only used for testing, when --snapshot-bucket-type=FILE"
252 )]
253 snapshot_path: Option<PathBuf>,
254 #[clap(
256 long = "no-sign-request",
257 conflicts_with_all = &["snapshot_bucket", "snapshot_bucket_type"],
258 help = "if set, no authentication is needed for snapshot restore"
259 )]
260 no_sign_request: bool,
261 #[clap(
264 long = "latest",
265 conflicts_with = "epoch",
266 help = "defaults to latest available snapshot in chosen bucket"
267 )]
268 latest: bool,
269 #[clap(long = "verbose")]
272 verbose: bool,
273 #[clap(long = "max-retries", default_value = "3")]
276 max_retries: usize,
277 },
278
279 #[clap(
283 name = "download-formal-snapshot",
284 about = "Downloads formal database snapshot via cloud object store, outputs to local disk"
285 )]
286 DownloadFormalSnapshot {
287 #[clap(long = "epoch", conflicts_with = "latest")]
288 epoch: Option<u64>,
289 #[clap(long = "genesis")]
290 genesis: PathBuf,
291 #[clap(long = "path")]
292 path: PathBuf,
293 #[clap(long = "num-parallel-downloads")]
295 num_parallel_downloads: Option<usize>,
296 #[clap(long = "verify", default_value = "normal")]
298 verify: Option<SnapshotVerifyMode>,
299 #[clap(long = "network", default_value = "mainnet")]
303 network: Chain,
304 #[clap(long = "snapshot-bucket", conflicts_with = "no_sign_request")]
307 snapshot_bucket: Option<String>,
308 #[clap(
310 long = "snapshot-bucket-type",
311 conflicts_with = "no_sign_request",
312 help = "Required if --no-sign-request is not set"
313 )]
314 snapshot_bucket_type: Option<ObjectStoreType>,
315 #[clap(long = "snapshot-path")]
318 snapshot_path: Option<PathBuf>,
319 #[clap(
321 long = "no-sign-request",
322 conflicts_with_all = &["snapshot_bucket", "snapshot_bucket_type"],
323 help = "if set, no authentication is needed for snapshot restore"
324 )]
325 no_sign_request: bool,
326 #[clap(
329 long = "latest",
330 conflicts_with = "epoch",
331 help = "defaults to latest available snapshot in chosen bucket"
332 )]
333 latest: bool,
334 #[clap(long = "verbose")]
337 verbose: bool,
338
339 #[clap(long = "max-retries", default_value = "3")]
342 max_retries: usize,
343 },
344
345 #[clap(name = "replay")]
346 Replay {
347 #[arg(long = "rpc")]
348 rpc_url: Option<String>,
349 #[arg(long = "safety-checks")]
350 safety_checks: bool,
351 #[arg(long = "authority")]
352 use_authority: bool,
353 #[arg(
354 long = "cfg-path",
355 short,
356 help = "Path to the network config file. This should be specified when rpc_url is not present. \
357 If not specified we will use the default network config file at ~/.sui-replay/network-config.yaml"
358 )]
359 cfg_path: Option<PathBuf>,
360 #[arg(
361 long,
362 help = "The name of the chain to replay from, could be one of: mainnet, testnet, devnet.\
363 When rpc_url is not specified, this is used to load the corresponding config from the network config file.\
364 If not specified, mainnet will be used by default"
365 )]
366 chain: Option<String>,
367 #[command(subcommand)]
368 cmd: ReplayToolCommand,
369 },
370}
371
372async fn check_locked_object(
373 sui_client: &Client,
374 committee: Arc<BTreeMap<AuthorityPublicKeyBytes, u64>>,
375 id: ObjectID,
376 rescue: bool,
377) -> anyhow::Result<()> {
378 let clients = Arc::new(make_clients(sui_client).await?);
379 let output = get_object(id, None, None, clients.clone()).await?;
380 let output = GroupedObjectOutput::new(output, committee);
381 if output.fully_locked {
382 println!("Object {} is fully locked.", id);
383 return Ok(());
384 }
385 let top_record = output.voting_power.first().unwrap();
386 let top_record_stake = top_record.1;
387 let top_record = top_record.0.clone().unwrap();
388 if top_record.4.is_none() {
389 println!(
390 "Object {} does not seem to be locked by majority of validators (unlocked stake: {})",
391 id, top_record_stake
392 );
393 return Ok(());
394 }
395
396 let tx_digest = top_record.2;
397 if !rescue {
398 println!("Object {} is rescueable, top tx: {:?}", id, tx_digest);
399 return Ok(());
400 }
401 println!("Object {} is rescueable, trying tx {}", id, tx_digest);
402 let validator = output
403 .grouped_results
404 .get(&Some(top_record))
405 .unwrap()
406 .first()
407 .unwrap();
408 let client = &clients.get(validator).unwrap().1;
409 let tx = client
410 .handle_transaction_info_request(TransactionInfoRequest {
411 transaction_digest: tx_digest,
412 })
413 .await?
414 .transaction;
415 let tx = Transaction::new(tx);
416 let res = sui_client.clone().execute_transaction(&tx).await;
417 match res {
418 Ok(_) => {
419 println!("Transaction executed successfully ({:?})", tx_digest);
420 }
421 Err(e) => {
422 println!("Failed to execute transaction ({:?}): {:?}", tx_digest, e);
423 }
424 }
425 Ok(())
426}
427
428impl ToolCommand {
429 #[allow(clippy::format_in_format_args)]
430 pub async fn execute(self, tracing_handle: TracingHandle) -> Result<(), anyhow::Error> {
431 match self {
432 ToolCommand::ScanConsensusCommits {
433 db_path,
434 start_commit,
435 end_commit,
436 } => {
437 let rocks_db_store = RocksDBStore::new(&db_path);
438
439 let start_commit = start_commit.unwrap_or(0);
440 let end_commit = end_commit.unwrap_or(u32::MAX);
441
442 let commits = rocks_db_store
443 .scan_commits(CommitRange::new(start_commit..=end_commit))
444 .unwrap();
445 println!("found {} consensus commits", commits.len());
446
447 for commit in commits {
448 let inner = &*commit;
449 let block_refs = inner.blocks();
450 let blocks = rocks_db_store.read_blocks(block_refs).unwrap();
451
452 for block in blocks.iter().flatten() {
453 let data = block.transactions_data();
454 println!(
455 "\"index\": \"{}\", \"leader\": \"{}\", \"blocks\": \"{:#?}\", {} txs",
456 inner.index(),
457 inner.leader(),
458 inner.blocks(),
459 data.len()
460 );
461 for txns in &data {
462 let tx: ConsensusTransaction = bcs::from_bytes(txns).unwrap();
463 println!("\t{:?}", tx.key());
464 }
465 }
466 }
467 }
468 ToolCommand::LockedObject {
469 id,
470 fullnode_rpc_url,
471 rescue,
472 address,
473 } => {
474 let sui_client = Client::new(fullnode_rpc_url)?;
475 let committee = Arc::new(
476 sui_client
477 .get_committee(None)
478 .await?
479 .voting_rights
480 .into_iter()
481 .collect::<BTreeMap<_, _>>(),
482 );
483 let object_ids = match id {
484 Some(id) => vec![id],
485 None => {
486 let address = address.expect("Either id or address must be provided");
487 sui_client
488 .list_owned_objects(address, Some(GasCoin::type_()))
489 .map_ok(|o| o.id())
490 .try_collect()
491 .await?
492 }
493 };
494 for ids in object_ids.chunks(30) {
495 let mut tasks = vec![];
496 for id in ids {
497 tasks.push(check_locked_object(
498 &sui_client,
499 committee.clone(),
500 *id,
501 rescue,
502 ))
503 }
504 join_all(tasks)
505 .await
506 .into_iter()
507 .collect::<Result<Vec<_>, _>>()?;
508 }
509 }
510 ToolCommand::FetchObject {
511 id,
512 validator,
513 version,
514 fullnode_rpc_url,
515 verbosity,
516 concise_no_header,
517 } => {
518 let sui_client = Client::new(fullnode_rpc_url)?;
519 let clients = Arc::new(make_clients(&sui_client).await?);
520 let output = get_object(id, version, validator, clients).await?;
521
522 match verbosity {
523 Verbosity::Grouped => {
524 let committee = Arc::new(
525 sui_client
526 .get_committee(None)
527 .await?
528 .voting_rights
529 .into_iter()
530 .collect::<BTreeMap<_, _>>(),
531 );
532 println!("{}", GroupedObjectOutput::new(output, committee));
533 }
534 Verbosity::Verbose => {
535 println!("{}", VerboseObjectOutput(output));
536 }
537 Verbosity::Concise => {
538 if !concise_no_header {
539 println!("{}", ConciseObjectOutput::header());
540 }
541 println!("{}", ConciseObjectOutput(output));
542 }
543 }
544 }
545 ToolCommand::FetchTransaction {
546 digest,
547 show_input_tx,
548 fullnode_rpc_url,
549 } => {
550 print!(
551 "{}",
552 get_transaction_block(digest, show_input_tx, fullnode_rpc_url).await?
553 );
554 }
555 ToolCommand::DbTool { db_path, cmd } => {
556 let path = PathBuf::from(db_path);
557 match cmd {
558 Some(c) => execute_db_tool_command(path, c).await?,
559 None => print_db_all_tables(path)?,
560 }
561 }
562 ToolCommand::DumpPackages {
563 rpc_url,
564 output_dir,
565 before_checkpoint,
566 verbose,
567 } => {
568 if !verbose {
569 tracing_handle
570 .update_log("off")
571 .expect("Failed to update log level");
572 }
573
574 sui_package_dump::dump(rpc_url, output_dir, before_checkpoint).await?;
575 }
576 ToolCommand::DumpValidators { genesis, concise } => {
577 let genesis = Genesis::load(genesis).unwrap();
578 if !concise {
579 println!("{:#?}", genesis.validator_set_for_tooling());
580 } else {
581 for (i, val_info) in genesis.validator_set_for_tooling().iter().enumerate() {
582 let metadata = val_info.verified_metadata();
583 println!(
584 "#{:<2} {:<20} {:?} {:?} {}",
585 i,
586 metadata.name,
587 metadata.sui_pubkey_bytes().concise(),
588 metadata.net_address,
589 anemo::PeerId(metadata.network_pubkey.0.to_bytes()),
590 )
591 }
592 }
593 }
594 ToolCommand::DumpGenesis { genesis } => {
595 let genesis = Genesis::load(genesis)?;
596 println!("{:#?}", genesis);
597 }
598 ToolCommand::FetchCheckpoint {
599 sequence_number,
600 fullnode_rpc_url,
601 } => {
602 let sui_client = Client::new(fullnode_rpc_url)?;
603 let clients = make_clients(&sui_client).await?;
604
605 for (name, (_, client)) in clients {
606 let resp = client
607 .handle_checkpoint(CheckpointRequest {
608 sequence_number,
609 request_content: true,
610 })
611 .await
612 .unwrap();
613 let CheckpointResponse {
614 checkpoint,
615 contents,
616 } = resp;
617
618 let summary = checkpoint.clone().unwrap().data().clone();
619 let mut file = std::fs::File::create("/tmp/ckpt_summary")
621 .expect("Failed to create /tmp/summary");
622 let bytes =
623 bcs::to_bytes(&summary).expect("Failed to serialize summary to BCS");
624 use std::io::Write;
625 file.write_all(&bytes)
626 .expect("Failed to write summary to /tmp/ckpt_summary");
627
628 println!("Validator: {:?}\n", name.concise());
629 println!("Checkpoint: {:?}\n", checkpoint);
630 println!("Content: {:?}\n", contents);
631 }
632 }
633 ToolCommand::Anemo { args } => {
634 let config = crate::make_anemo_config();
635 anemo_cli::run(config, args).await
636 }
637 ToolCommand::RestoreFromDBCheckpoint {
638 config_path,
639 db_checkpoint_path,
640 } => {
641 let config = sui_config::NodeConfig::load(config_path)?;
642 restore_from_db_checkpoint(&config, &db_checkpoint_path).await?;
643 }
644 ToolCommand::DownloadFormalSnapshot {
645 epoch,
646 genesis,
647 path,
648 num_parallel_downloads,
649 verify,
650 network,
651 snapshot_bucket,
652 snapshot_bucket_type,
653 snapshot_path,
654 no_sign_request,
655 latest,
656 verbose,
657 max_retries,
658 } => {
659 if !verbose {
660 tracing_handle
661 .update_log("off")
662 .expect("Failed to update log level");
663 }
664 let num_parallel_downloads = num_parallel_downloads.unwrap_or(50).min(200);
665 let snapshot_bucket =
666 snapshot_bucket.or_else(|| match (network, no_sign_request) {
667 (Chain::Mainnet, false) => Some(
668 env::var("MAINNET_FORMAL_SIGNED_BUCKET")
669 .unwrap_or("mysten-mainnet-formal".to_string()),
670 ),
671 (Chain::Mainnet, true) => env::var("MAINNET_FORMAL_UNSIGNED_BUCKET").ok(),
672 (Chain::Testnet, true) => env::var("TESTNET_FORMAL_UNSIGNED_BUCKET").ok(),
673 (Chain::Testnet, _) => Some(
674 env::var("TESTNET_FORMAL_SIGNED_BUCKET")
675 .unwrap_or("mysten-testnet-formal".to_string()),
676 ),
677 (Chain::Unknown, _) => {
678 panic!("Cannot generate default snapshot bucket for unknown network");
679 }
680 });
681
682 let aws_endpoint = env::var("AWS_SNAPSHOT_ENDPOINT").ok().or_else(|| {
683 if no_sign_request {
684 if network == Chain::Mainnet {
685 Some("https://formal-snapshot.mainnet.sui.io".to_string())
686 } else if network == Chain::Testnet {
687 Some("https://formal-snapshot.testnet.sui.io".to_string())
688 } else {
689 None
690 }
691 } else {
692 None
693 }
694 });
695
696 let snapshot_bucket_type = if no_sign_request {
697 ObjectStoreType::S3
698 } else {
699 snapshot_bucket_type
700 .expect("You must set either --snapshot-bucket-type or --no-sign-request")
701 };
702 let snapshot_store_config = match snapshot_bucket_type {
703 ObjectStoreType::S3 => ObjectStoreConfig {
704 object_store: Some(ObjectStoreType::S3),
705 bucket: snapshot_bucket.filter(|s| !s.is_empty()),
706 aws_access_key_id: env::var("AWS_SNAPSHOT_ACCESS_KEY_ID").ok(),
707 aws_secret_access_key: env::var("AWS_SNAPSHOT_SECRET_ACCESS_KEY").ok(),
708 aws_region: env::var("AWS_SNAPSHOT_REGION").ok(),
709 aws_endpoint: aws_endpoint.filter(|s| !s.is_empty()),
710 aws_virtual_hosted_style_request: env::var(
711 "AWS_SNAPSHOT_VIRTUAL_HOSTED_REQUESTS",
712 )
713 .ok()
714 .and_then(|b| b.parse().ok())
715 .unwrap_or(no_sign_request),
716 object_store_connection_limit: 200,
717 no_sign_request,
718 ..Default::default()
719 },
720 ObjectStoreType::GCS => ObjectStoreConfig {
721 object_store: Some(ObjectStoreType::GCS),
722 bucket: snapshot_bucket,
723 google_service_account: env::var("GCS_SNAPSHOT_SERVICE_ACCOUNT_FILE_PATH")
724 .ok(),
725 object_store_connection_limit: 200,
726 no_sign_request,
727 ..Default::default()
728 },
729 ObjectStoreType::Azure => ObjectStoreConfig {
730 object_store: Some(ObjectStoreType::Azure),
731 bucket: snapshot_bucket,
732 azure_storage_account: env::var("AZURE_SNAPSHOT_STORAGE_ACCOUNT").ok(),
733 azure_storage_access_key: env::var("AZURE_SNAPSHOT_STORAGE_ACCESS_KEY")
734 .ok(),
735 object_store_connection_limit: 200,
736 no_sign_request,
737 ..Default::default()
738 },
739 ObjectStoreType::File => {
740 if snapshot_path.is_some() {
741 ObjectStoreConfig {
742 object_store: Some(ObjectStoreType::File),
743 directory: snapshot_path,
744 ..Default::default()
745 }
746 } else {
747 panic!(
748 "--snapshot-path must be specified for --snapshot-bucket-type=file"
749 );
750 }
751 }
752 };
753
754 let ingestion_url = match network {
755 Chain::Mainnet => "https://checkpoints.mainnet.sui.io",
756 Chain::Testnet => "https://checkpoints.testnet.sui.io",
757 _ => panic!("Cannot generate default ingestion url for unknown network"),
758 };
759
760 let latest_available_epoch =
761 latest.then_some(get_latest_available_epoch(&snapshot_store_config).await?);
762 let epoch_to_download = epoch.or(latest_available_epoch).expect(
763 "Either pass epoch with --epoch <epoch_num> or use latest with --latest",
764 );
765
766 if let Err(e) =
767 check_completed_snapshot(&snapshot_store_config, epoch_to_download).await
768 {
769 panic!(
770 "Aborting snapshot restore: {}, snapshot may not be uploaded yet",
771 e
772 );
773 }
774
775 let verify = verify.unwrap_or_default();
776 download_formal_snapshot(
777 &path,
778 epoch_to_download,
779 &genesis,
780 snapshot_store_config,
781 ingestion_url,
782 num_parallel_downloads,
783 network,
784 verify,
785 max_retries,
786 )
787 .await?;
788 }
789 ToolCommand::DownloadDBSnapshot {
790 epoch,
791 path,
792 skip_indexes,
793 num_parallel_downloads,
794 network,
795 snapshot_bucket,
796 snapshot_bucket_type,
797 snapshot_path,
798 no_sign_request,
799 latest,
800 verbose,
801 max_retries,
802 } => {
803 if no_sign_request {
804 anyhow::bail!(
805 "The --no-sign-request flag is no longer supported. \
806 Please use S3 or GCS buckets with --snapshot-bucket-type and --snapshot-bucket instead. \
807 For more information, see: https://docs.sui.io/guides/operator/snapshots#mysten-labs-managed-snapshots"
808 );
809 }
810 if !verbose {
811 tracing_handle
812 .update_log("off")
813 .expect("Failed to update log level");
814 }
815 let num_parallel_downloads = num_parallel_downloads.unwrap_or(50).min(200);
816 let snapshot_bucket =
817 snapshot_bucket.or_else(|| match (network, no_sign_request) {
818 (Chain::Mainnet, false) => Some(
819 env::var("MAINNET_DB_SIGNED_BUCKET")
820 .unwrap_or("mysten-mainnet-snapshots".to_string()),
821 ),
822 (Chain::Mainnet, true) => env::var("MAINNET_DB_UNSIGNED_BUCKET").ok(),
823 (Chain::Testnet, true) => env::var("TESTNET_DB_UNSIGNED_BUCKET").ok(),
824 (Chain::Testnet, _) => Some(
825 env::var("TESTNET_DB_SIGNED_BUCKET")
826 .unwrap_or("mysten-testnet-snapshots".to_string()),
827 ),
828 (Chain::Unknown, _) => {
829 panic!("Cannot generate default snapshot bucket for unknown network");
830 }
831 });
832
833 let aws_endpoint = env::var("AWS_SNAPSHOT_ENDPOINT").ok();
834 let snapshot_bucket_type = if no_sign_request {
835 ObjectStoreType::S3
836 } else {
837 snapshot_bucket_type
838 .expect("You must set either --snapshot-bucket-type or --no-sign-request")
839 };
840 let snapshot_store_config = if no_sign_request {
841 let aws_endpoint = env::var("AWS_SNAPSHOT_ENDPOINT").ok().or_else(|| {
842 if network == Chain::Mainnet {
843 Some("https://db-snapshot.mainnet.sui.io".to_string())
844 } else if network == Chain::Testnet {
845 Some("https://db-snapshot.testnet.sui.io".to_string())
846 } else {
847 None
848 }
849 });
850 ObjectStoreConfig {
851 object_store: Some(ObjectStoreType::S3),
852 aws_endpoint: aws_endpoint.filter(|s| !s.is_empty()),
853 aws_virtual_hosted_style_request: env::var(
854 "AWS_SNAPSHOT_VIRTUAL_HOSTED_REQUESTS",
855 )
856 .ok()
857 .and_then(|b| b.parse().ok())
858 .unwrap_or(no_sign_request),
859 object_store_connection_limit: 200,
860 no_sign_request,
861 ..Default::default()
862 }
863 } else {
864 match snapshot_bucket_type {
865 ObjectStoreType::S3 => ObjectStoreConfig {
866 object_store: Some(ObjectStoreType::S3),
867 bucket: snapshot_bucket.filter(|s| !s.is_empty()),
868 aws_access_key_id: env::var("AWS_SNAPSHOT_ACCESS_KEY_ID").ok(),
869 aws_secret_access_key: env::var("AWS_SNAPSHOT_SECRET_ACCESS_KEY").ok(),
870 aws_region: env::var("AWS_SNAPSHOT_REGION").ok(),
871 aws_endpoint: aws_endpoint.filter(|s| !s.is_empty()),
872 aws_virtual_hosted_style_request: env::var(
873 "AWS_SNAPSHOT_VIRTUAL_HOSTED_REQUESTS",
874 )
875 .ok()
876 .and_then(|b| b.parse().ok())
877 .unwrap_or(no_sign_request),
878 object_store_connection_limit: 200,
879 no_sign_request,
880 ..Default::default()
881 },
882 ObjectStoreType::GCS => ObjectStoreConfig {
883 object_store: Some(ObjectStoreType::GCS),
884 bucket: snapshot_bucket,
885 google_service_account: env::var(
886 "GCS_SNAPSHOT_SERVICE_ACCOUNT_FILE_PATH",
887 )
888 .ok(),
889 google_project_id: env::var("GCS_SNAPSHOT_SERVICE_ACCOUNT_PROJECT_ID")
890 .ok(),
891 object_store_connection_limit: 200,
892 no_sign_request,
893 ..Default::default()
894 },
895 ObjectStoreType::Azure => ObjectStoreConfig {
896 object_store: Some(ObjectStoreType::Azure),
897 bucket: snapshot_bucket,
898 azure_storage_account: env::var("AZURE_SNAPSHOT_STORAGE_ACCOUNT").ok(),
899 azure_storage_access_key: env::var("AZURE_SNAPSHOT_STORAGE_ACCESS_KEY")
900 .ok(),
901 object_store_connection_limit: 200,
902 no_sign_request,
903 ..Default::default()
904 },
905 ObjectStoreType::File => {
906 if snapshot_path.is_some() {
907 ObjectStoreConfig {
908 object_store: Some(ObjectStoreType::File),
909 directory: snapshot_path,
910 ..Default::default()
911 }
912 } else {
913 panic!(
914 "--snapshot-path must be specified for --snapshot-bucket-type=file"
915 );
916 }
917 }
918 }
919 };
920
921 let latest_available_epoch =
922 latest.then_some(get_latest_available_epoch(&snapshot_store_config).await?);
923 let epoch_to_download = epoch.or(latest_available_epoch).expect(
924 "Either pass epoch with --epoch <epoch_num> or use latest with --latest",
925 );
926
927 if let Err(e) =
928 check_completed_snapshot(&snapshot_store_config, epoch_to_download).await
929 {
930 panic!(
931 "Aborting snapshot restore: {}, snapshot may not be uploaded yet",
932 e
933 );
934 }
935 download_db_snapshot(
936 &path,
937 epoch_to_download,
938 snapshot_store_config,
939 skip_indexes,
940 num_parallel_downloads,
941 max_retries,
942 )
943 .await?;
944 }
945 ToolCommand::Replay {
946 rpc_url,
947 safety_checks,
948 cmd,
949 use_authority,
950 cfg_path,
951 chain,
952 } => {
953 execute_replay_command(rpc_url, safety_checks, use_authority, cfg_path, chain, cmd)
954 .await?;
955 }
956 };
957 Ok(())
958 }
959}