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 = "num-parallel-chunks", default_value = "8")]
298 num_parallel_chunks: usize,
299 #[clap(long = "verify", default_value = "normal")]
301 verify: Option<SnapshotVerifyMode>,
302 #[clap(long = "network", default_value = "mainnet")]
306 network: Chain,
307 #[clap(long = "snapshot-bucket", conflicts_with = "no_sign_request")]
310 snapshot_bucket: Option<String>,
311 #[clap(
313 long = "snapshot-bucket-type",
314 conflicts_with = "no_sign_request",
315 help = "Required if --no-sign-request is not set"
316 )]
317 snapshot_bucket_type: Option<ObjectStoreType>,
318 #[clap(long = "snapshot-path")]
321 snapshot_path: Option<PathBuf>,
322 #[clap(
324 long = "no-sign-request",
325 conflicts_with_all = &["snapshot_bucket", "snapshot_bucket_type"],
326 help = "if set, no authentication is needed for snapshot restore"
327 )]
328 no_sign_request: bool,
329 #[clap(
332 long = "latest",
333 conflicts_with = "epoch",
334 help = "defaults to latest available snapshot in chosen bucket"
335 )]
336 latest: bool,
337 #[clap(long = "verbose")]
340 verbose: bool,
341
342 #[clap(long = "max-retries", default_value = "3")]
345 max_retries: usize,
346 #[clap(long = "metrics-port", default_value = "9184")]
348 metrics_port: u16,
349 },
350
351 #[clap(name = "replay")]
352 Replay {
353 #[arg(long = "rpc")]
354 rpc_url: Option<String>,
355 #[arg(long = "safety-checks")]
356 safety_checks: bool,
357 #[arg(long = "authority")]
358 use_authority: bool,
359 #[arg(
360 long = "cfg-path",
361 short,
362 help = "Path to the network config file. This should be specified when rpc_url is not present. \
363 If not specified we will use the default network config file at ~/.sui-replay/network-config.yaml"
364 )]
365 cfg_path: Option<PathBuf>,
366 #[arg(
367 long,
368 help = "The name of the chain to replay from, could be one of: mainnet, testnet, devnet.\
369 When rpc_url is not specified, this is used to load the corresponding config from the network config file.\
370 If not specified, mainnet will be used by default"
371 )]
372 chain: Option<String>,
373 #[command(subcommand)]
374 cmd: ReplayToolCommand,
375 },
376
377 #[cfg(all(feature = "tideconsole", not(windows)))]
379 #[command(name = "tideconsole")]
380 TideConsole {
381 #[arg(short, long)]
383 db: Option<PathBuf>,
384 #[arg(short, long)]
386 exec: Option<String>,
387 #[arg(short, long)]
389 script: Option<PathBuf>,
390 },
391}
392
393async fn check_locked_object(
394 sui_client: &Client,
395 committee: Arc<BTreeMap<AuthorityPublicKeyBytes, u64>>,
396 id: ObjectID,
397 rescue: bool,
398) -> anyhow::Result<()> {
399 let clients = Arc::new(make_clients(sui_client).await?);
400 let output = get_object(id, None, None, clients.clone()).await?;
401 let output = GroupedObjectOutput::new(output, committee);
402 if output.fully_locked {
403 println!("Object {} is fully locked.", id);
404 return Ok(());
405 }
406 let top_record = output.voting_power.first().unwrap();
407 let top_record_stake = top_record.1;
408 let top_record = top_record.0.clone().unwrap();
409 if top_record.4.is_none() {
410 println!(
411 "Object {} does not seem to be locked by majority of validators (unlocked stake: {})",
412 id, top_record_stake
413 );
414 return Ok(());
415 }
416
417 let tx_digest = top_record.2;
418 if !rescue {
419 println!("Object {} is rescueable, top tx: {:?}", id, tx_digest);
420 return Ok(());
421 }
422 println!("Object {} is rescueable, trying tx {}", id, tx_digest);
423 let validator = output
424 .grouped_results
425 .get(&Some(top_record))
426 .unwrap()
427 .first()
428 .unwrap();
429 let client = &clients.get(validator).unwrap().1;
430 let tx = client
431 .handle_transaction_info_request(TransactionInfoRequest {
432 transaction_digest: tx_digest,
433 })
434 .await?
435 .transaction;
436 let tx = Transaction::new(tx);
437 let res = sui_client.clone().execute_transaction(&tx).await;
438 match res {
439 Ok(_) => {
440 println!("Transaction executed successfully ({:?})", tx_digest);
441 }
442 Err(e) => {
443 println!("Failed to execute transaction ({:?}): {:?}", tx_digest, e);
444 }
445 }
446 Ok(())
447}
448
449impl ToolCommand {
450 #[allow(clippy::format_in_format_args)]
451 pub async fn execute(self, tracing_handle: TracingHandle) -> Result<(), anyhow::Error> {
452 match self {
453 ToolCommand::ScanConsensusCommits {
454 db_path,
455 start_commit,
456 end_commit,
457 } => {
458 let rocks_db_store = RocksDBStore::new(&db_path, true);
459
460 let start_commit = start_commit.unwrap_or(0);
461 let end_commit = end_commit.unwrap_or(u32::MAX);
462
463 let commits = rocks_db_store
464 .scan_commits(CommitRange::new(start_commit..=end_commit))
465 .unwrap();
466 println!("found {} consensus commits", commits.len());
467
468 for commit in commits {
469 let inner = &*commit;
470 let block_refs = inner.blocks();
471 let blocks = rocks_db_store.read_blocks(block_refs).unwrap();
472
473 for block in blocks.iter().flatten() {
474 let data = block.transactions_data();
475 println!(
476 "\"index\": \"{}\", \"leader\": \"{}\", \"blocks\": \"{:#?}\", {} txs",
477 inner.index(),
478 inner.leader(),
479 inner.blocks(),
480 data.len()
481 );
482 for txns in &data {
483 let tx: ConsensusTransaction = bcs::from_bytes(txns).unwrap();
484 println!("\t{:?}", tx.key());
485 }
486 }
487 }
488 }
489 ToolCommand::LockedObject {
490 id,
491 fullnode_rpc_url,
492 rescue,
493 address,
494 } => {
495 let sui_client = Client::new(fullnode_rpc_url)?;
496 let committee = Arc::new(
497 sui_client
498 .get_committee(None)
499 .await?
500 .voting_rights
501 .into_iter()
502 .collect::<BTreeMap<_, _>>(),
503 );
504 let object_ids = match id {
505 Some(id) => vec![id],
506 None => {
507 let address = address.expect("Either id or address must be provided");
508 sui_client
509 .list_owned_objects(address, Some(GasCoin::type_()))
510 .map_ok(|o| o.id())
511 .try_collect()
512 .await?
513 }
514 };
515 for ids in object_ids.chunks(30) {
516 let mut tasks = vec![];
517 for id in ids {
518 tasks.push(check_locked_object(
519 &sui_client,
520 committee.clone(),
521 *id,
522 rescue,
523 ))
524 }
525 join_all(tasks)
526 .await
527 .into_iter()
528 .collect::<Result<Vec<_>, _>>()?;
529 }
530 }
531 ToolCommand::FetchObject {
532 id,
533 validator,
534 version,
535 fullnode_rpc_url,
536 verbosity,
537 concise_no_header,
538 } => {
539 let sui_client = Client::new(fullnode_rpc_url)?;
540 let clients = Arc::new(make_clients(&sui_client).await?);
541 let output = get_object(id, version, validator, clients).await?;
542
543 match verbosity {
544 Verbosity::Grouped => {
545 let committee = Arc::new(
546 sui_client
547 .get_committee(None)
548 .await?
549 .voting_rights
550 .into_iter()
551 .collect::<BTreeMap<_, _>>(),
552 );
553 println!("{}", GroupedObjectOutput::new(output, committee));
554 }
555 Verbosity::Verbose => {
556 println!("{}", VerboseObjectOutput(output));
557 }
558 Verbosity::Concise => {
559 if !concise_no_header {
560 println!("{}", ConciseObjectOutput::header());
561 }
562 println!("{}", ConciseObjectOutput(output));
563 }
564 }
565 }
566 ToolCommand::FetchTransaction {
567 digest,
568 show_input_tx,
569 fullnode_rpc_url,
570 } => {
571 print!(
572 "{}",
573 get_transaction_block(digest, show_input_tx, fullnode_rpc_url).await?
574 );
575 }
576 ToolCommand::DbTool { db_path, cmd } => {
577 let path = PathBuf::from(db_path);
578 match cmd {
579 Some(c) => execute_db_tool_command(path, c).await?,
580 None => print_db_all_tables(path)?,
581 }
582 }
583 ToolCommand::DumpPackages {
584 rpc_url,
585 output_dir,
586 before_checkpoint,
587 verbose,
588 } => {
589 if !verbose {
590 tracing_handle
591 .update_log("off")
592 .expect("Failed to update log level");
593 }
594
595 sui_package_dump::dump(rpc_url, output_dir, before_checkpoint).await?;
596 }
597 ToolCommand::DumpValidators { genesis, concise } => {
598 let genesis = Genesis::load(genesis).unwrap();
599 if !concise {
600 println!("{:#?}", genesis.validator_set_for_tooling());
601 } else {
602 for (i, val_info) in genesis.validator_set_for_tooling().iter().enumerate() {
603 let metadata = val_info.verified_metadata();
604 println!(
605 "#{:<2} {:<20} {:?} {:?} {}",
606 i,
607 metadata.name,
608 metadata.sui_pubkey_bytes().concise(),
609 metadata.net_address,
610 anemo::PeerId(metadata.network_pubkey.0.to_bytes()),
611 )
612 }
613 }
614 }
615 ToolCommand::DumpGenesis { genesis } => {
616 let genesis = Genesis::load(genesis)?;
617 println!("{:#?}", genesis);
618 }
619 ToolCommand::FetchCheckpoint {
620 sequence_number,
621 fullnode_rpc_url,
622 } => {
623 let sui_client = Client::new(fullnode_rpc_url)?;
624 let clients = make_clients(&sui_client).await?;
625
626 for (name, (_, client)) in clients {
627 let resp = client
628 .handle_checkpoint(CheckpointRequest {
629 sequence_number,
630 request_content: true,
631 })
632 .await
633 .unwrap();
634 let CheckpointResponse {
635 checkpoint,
636 contents,
637 } = resp;
638
639 let summary = checkpoint.clone().unwrap().data().clone();
640 let mut file = std::fs::File::create("/tmp/ckpt_summary")
642 .expect("Failed to create /tmp/summary");
643 let bytes =
644 bcs::to_bytes(&summary).expect("Failed to serialize summary to BCS");
645 use std::io::Write;
646 file.write_all(&bytes)
647 .expect("Failed to write summary to /tmp/ckpt_summary");
648
649 println!("Validator: {:?}\n", name.concise());
650 println!("Checkpoint: {:?}\n", checkpoint);
651 println!("Content: {:?}\n", contents);
652 }
653 }
654 ToolCommand::Anemo { args } => {
655 let config = crate::make_anemo_config();
656 anemo_cli::run(config, args).await
657 }
658 ToolCommand::RestoreFromDBCheckpoint {
659 config_path,
660 db_checkpoint_path,
661 } => {
662 let config = sui_config::NodeConfig::load(config_path)?;
663 restore_from_db_checkpoint(&config, &db_checkpoint_path).await?;
664 }
665 ToolCommand::DownloadFormalSnapshot {
666 epoch,
667 genesis,
668 path,
669 num_parallel_downloads,
670 num_parallel_chunks,
671 verify,
672 network,
673 snapshot_bucket,
674 snapshot_bucket_type,
675 snapshot_path,
676 no_sign_request,
677 latest,
678 verbose,
679 max_retries,
680 metrics_port,
681 } => {
682 if !verbose {
683 tracing_handle
684 .update_log("off")
685 .expect("Failed to update log level");
686 }
687 let num_parallel_downloads = num_parallel_downloads.unwrap_or(50).min(200);
688 let snapshot_bucket =
689 snapshot_bucket.or_else(|| match (network, no_sign_request) {
690 (Chain::Mainnet, false) => Some(
691 env::var("MAINNET_FORMAL_SIGNED_BUCKET")
692 .unwrap_or("mysten-mainnet-formal".to_string()),
693 ),
694 (Chain::Mainnet, true) => env::var("MAINNET_FORMAL_UNSIGNED_BUCKET").ok(),
695 (Chain::Testnet, true) => env::var("TESTNET_FORMAL_UNSIGNED_BUCKET").ok(),
696 (Chain::Testnet, _) => Some(
697 env::var("TESTNET_FORMAL_SIGNED_BUCKET")
698 .unwrap_or("mysten-testnet-formal".to_string()),
699 ),
700 (Chain::Unknown, _) => {
701 panic!("Cannot generate default snapshot bucket for unknown network");
702 }
703 });
704
705 let aws_endpoint = env::var("AWS_SNAPSHOT_ENDPOINT").ok().or_else(|| {
706 if no_sign_request {
707 if network == Chain::Mainnet {
708 Some("https://formal-snapshot.mainnet.sui.io".to_string())
709 } else if network == Chain::Testnet {
710 Some("https://formal-snapshot.testnet.sui.io".to_string())
711 } else {
712 None
713 }
714 } else {
715 None
716 }
717 });
718
719 let snapshot_bucket_type = if no_sign_request {
720 ObjectStoreType::S3
721 } else {
722 snapshot_bucket_type
723 .expect("You must set either --snapshot-bucket-type or --no-sign-request")
724 };
725 let snapshot_store_config = match snapshot_bucket_type {
726 ObjectStoreType::S3 => ObjectStoreConfig {
727 object_store: Some(ObjectStoreType::S3),
728 bucket: snapshot_bucket.filter(|s| !s.is_empty()),
729 aws_access_key_id: env::var("AWS_SNAPSHOT_ACCESS_KEY_ID").ok(),
730 aws_secret_access_key: env::var("AWS_SNAPSHOT_SECRET_ACCESS_KEY").ok(),
731 aws_region: env::var("AWS_SNAPSHOT_REGION").ok(),
732 aws_endpoint: aws_endpoint.filter(|s| !s.is_empty()),
733 aws_virtual_hosted_style_request: env::var(
734 "AWS_SNAPSHOT_VIRTUAL_HOSTED_REQUESTS",
735 )
736 .ok()
737 .and_then(|b| b.parse().ok())
738 .unwrap_or(no_sign_request),
739 object_store_connection_limit: 200,
740 no_sign_request,
741 ..Default::default()
742 },
743 ObjectStoreType::GCS => ObjectStoreConfig {
744 object_store: Some(ObjectStoreType::GCS),
745 bucket: snapshot_bucket,
746 google_service_account: env::var("GCS_SNAPSHOT_SERVICE_ACCOUNT_FILE_PATH")
747 .ok(),
748 object_store_connection_limit: 200,
749 no_sign_request,
750 ..Default::default()
751 },
752 ObjectStoreType::Azure => ObjectStoreConfig {
753 object_store: Some(ObjectStoreType::Azure),
754 bucket: snapshot_bucket,
755 azure_storage_account: env::var("AZURE_SNAPSHOT_STORAGE_ACCOUNT").ok(),
756 azure_storage_access_key: env::var("AZURE_SNAPSHOT_STORAGE_ACCESS_KEY")
757 .ok(),
758 object_store_connection_limit: 200,
759 no_sign_request,
760 ..Default::default()
761 },
762 ObjectStoreType::File => {
763 if snapshot_path.is_some() {
764 ObjectStoreConfig {
765 object_store: Some(ObjectStoreType::File),
766 directory: snapshot_path,
767 ..Default::default()
768 }
769 } else {
770 panic!(
771 "--snapshot-path must be specified for --snapshot-bucket-type=file"
772 );
773 }
774 }
775 };
776
777 let ingestion_url = match network {
778 Chain::Mainnet => "https://checkpoints.mainnet.sui.io",
779 Chain::Testnet => "https://checkpoints.testnet.sui.io",
780 _ => panic!("Cannot generate default ingestion url for unknown network"),
781 };
782
783 let latest_available_epoch =
784 latest.then_some(get_latest_available_epoch(&snapshot_store_config).await?);
785 let epoch_to_download = epoch.or(latest_available_epoch).expect(
786 "Either pass epoch with --epoch <epoch_num> or use latest with --latest",
787 );
788
789 if let Err(e) =
790 check_completed_snapshot(&snapshot_store_config, epoch_to_download).await
791 {
792 panic!(
793 "Aborting snapshot restore: {}, snapshot may not be uploaded yet",
794 e
795 );
796 }
797
798 let verify = verify.unwrap_or_default();
799 download_formal_snapshot(
800 &path,
801 epoch_to_download,
802 &genesis,
803 snapshot_store_config,
804 ingestion_url,
805 num_parallel_downloads,
806 num_parallel_chunks,
807 network,
808 verify,
809 max_retries,
810 metrics_port,
811 )
812 .await?;
813 }
814 ToolCommand::DownloadDBSnapshot {
815 epoch,
816 path,
817 skip_indexes,
818 num_parallel_downloads,
819 network,
820 snapshot_bucket,
821 snapshot_bucket_type,
822 snapshot_path,
823 no_sign_request,
824 latest,
825 verbose,
826 max_retries,
827 } => {
828 if no_sign_request {
829 anyhow::bail!(
830 "The --no-sign-request flag is no longer supported. \
831 Please use S3 or GCS buckets with --snapshot-bucket-type and --snapshot-bucket instead. \
832 For more information, see: https://docs.sui.io/guides/operator/snapshots#mysten-labs-managed-snapshots"
833 );
834 }
835 if !verbose {
836 tracing_handle
837 .update_log("off")
838 .expect("Failed to update log level");
839 }
840 let num_parallel_downloads = num_parallel_downloads.unwrap_or(50).min(200);
841 let snapshot_bucket =
842 snapshot_bucket.or_else(|| match (network, no_sign_request) {
843 (Chain::Mainnet, false) => Some(
844 env::var("MAINNET_DB_SIGNED_BUCKET")
845 .unwrap_or("mysten-mainnet-snapshots".to_string()),
846 ),
847 (Chain::Mainnet, true) => env::var("MAINNET_DB_UNSIGNED_BUCKET").ok(),
848 (Chain::Testnet, true) => env::var("TESTNET_DB_UNSIGNED_BUCKET").ok(),
849 (Chain::Testnet, _) => Some(
850 env::var("TESTNET_DB_SIGNED_BUCKET")
851 .unwrap_or("mysten-testnet-snapshots".to_string()),
852 ),
853 (Chain::Unknown, _) => {
854 panic!("Cannot generate default snapshot bucket for unknown network");
855 }
856 });
857
858 let aws_endpoint = env::var("AWS_SNAPSHOT_ENDPOINT").ok();
859 let snapshot_bucket_type = if no_sign_request {
860 ObjectStoreType::S3
861 } else {
862 snapshot_bucket_type
863 .expect("You must set either --snapshot-bucket-type or --no-sign-request")
864 };
865 let snapshot_store_config = if no_sign_request {
866 let aws_endpoint = env::var("AWS_SNAPSHOT_ENDPOINT").ok().or_else(|| {
867 if network == Chain::Mainnet {
868 Some("https://db-snapshot.mainnet.sui.io".to_string())
869 } else if network == Chain::Testnet {
870 Some("https://db-snapshot.testnet.sui.io".to_string())
871 } else {
872 None
873 }
874 });
875 ObjectStoreConfig {
876 object_store: Some(ObjectStoreType::S3),
877 aws_endpoint: aws_endpoint.filter(|s| !s.is_empty()),
878 aws_virtual_hosted_style_request: env::var(
879 "AWS_SNAPSHOT_VIRTUAL_HOSTED_REQUESTS",
880 )
881 .ok()
882 .and_then(|b| b.parse().ok())
883 .unwrap_or(no_sign_request),
884 object_store_connection_limit: 200,
885 no_sign_request,
886 ..Default::default()
887 }
888 } else {
889 match snapshot_bucket_type {
890 ObjectStoreType::S3 => ObjectStoreConfig {
891 object_store: Some(ObjectStoreType::S3),
892 bucket: snapshot_bucket.filter(|s| !s.is_empty()),
893 aws_access_key_id: env::var("AWS_SNAPSHOT_ACCESS_KEY_ID").ok(),
894 aws_secret_access_key: env::var("AWS_SNAPSHOT_SECRET_ACCESS_KEY").ok(),
895 aws_region: env::var("AWS_SNAPSHOT_REGION").ok(),
896 aws_endpoint: aws_endpoint.filter(|s| !s.is_empty()),
897 aws_virtual_hosted_style_request: env::var(
898 "AWS_SNAPSHOT_VIRTUAL_HOSTED_REQUESTS",
899 )
900 .ok()
901 .and_then(|b| b.parse().ok())
902 .unwrap_or(no_sign_request),
903 object_store_connection_limit: 200,
904 no_sign_request,
905 ..Default::default()
906 },
907 ObjectStoreType::GCS => ObjectStoreConfig {
908 object_store: Some(ObjectStoreType::GCS),
909 bucket: snapshot_bucket,
910 google_service_account: env::var(
911 "GCS_SNAPSHOT_SERVICE_ACCOUNT_FILE_PATH",
912 )
913 .ok(),
914 google_project_id: env::var("GCS_SNAPSHOT_SERVICE_ACCOUNT_PROJECT_ID")
915 .ok(),
916 object_store_connection_limit: 200,
917 no_sign_request,
918 ..Default::default()
919 },
920 ObjectStoreType::Azure => ObjectStoreConfig {
921 object_store: Some(ObjectStoreType::Azure),
922 bucket: snapshot_bucket,
923 azure_storage_account: env::var("AZURE_SNAPSHOT_STORAGE_ACCOUNT").ok(),
924 azure_storage_access_key: env::var("AZURE_SNAPSHOT_STORAGE_ACCESS_KEY")
925 .ok(),
926 object_store_connection_limit: 200,
927 no_sign_request,
928 ..Default::default()
929 },
930 ObjectStoreType::File => {
931 if snapshot_path.is_some() {
932 ObjectStoreConfig {
933 object_store: Some(ObjectStoreType::File),
934 directory: snapshot_path,
935 ..Default::default()
936 }
937 } else {
938 panic!(
939 "--snapshot-path must be specified for --snapshot-bucket-type=file"
940 );
941 }
942 }
943 }
944 };
945
946 let latest_available_epoch =
947 latest.then_some(get_latest_available_epoch(&snapshot_store_config).await?);
948 let epoch_to_download = epoch.or(latest_available_epoch).expect(
949 "Either pass epoch with --epoch <epoch_num> or use latest with --latest",
950 );
951
952 if let Err(e) =
953 check_completed_snapshot(&snapshot_store_config, epoch_to_download).await
954 {
955 panic!(
956 "Aborting snapshot restore: {}, snapshot may not be uploaded yet",
957 e
958 );
959 }
960 download_db_snapshot(
961 &path,
962 epoch_to_download,
963 snapshot_store_config,
964 skip_indexes,
965 num_parallel_downloads,
966 max_retries,
967 )
968 .await?;
969 }
970 ToolCommand::Replay {
971 rpc_url,
972 safety_checks,
973 cmd,
974 use_authority,
975 cfg_path,
976 chain,
977 } => {
978 execute_replay_command(rpc_url, safety_checks, use_authority, cfg_path, chain, cmd)
979 .await?;
980 }
981 #[cfg(all(feature = "tideconsole", not(windows)))]
982 ToolCommand::TideConsole { db, exec, script } => {
983 tokio::task::spawn_blocking(move || crate::tideconsole_cmd::run(db, exec, script))
984 .await??;
985 }
986 };
987 Ok(())
988 }
989}