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