sui_tool/
commands.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4#[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    /// Inspect if a specific object is or all gas objects owned by an address are locked by validators
60    #[command(name = "locked-object")]
61    LockedObject {
62        /// Either id or address must be provided
63        /// The object to check
64        #[arg(long, help = "The object ID to fetch")]
65        id: Option<ObjectID>,
66        /// Either id or address must be provided
67        /// If provided, check all gas objects owned by this account
68        #[arg(long = "address")]
69        address: Option<SuiAddress>,
70        /// RPC address to provide the up-to-date committee info
71        #[arg(long = "fullnode-rpc-url")]
72        fullnode_rpc_url: String,
73        /// Should attempt to rescue the object if it's locked but not fully locked
74        #[arg(long = "rescue")]
75        rescue: bool,
76    },
77
78    /// Fetch the same object from all validators
79    #[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        // RPC address to provide the up-to-date committee info
94        #[arg(long = "fullnode-rpc-url")]
95        fullnode_rpc_url: String,
96
97        /// Concise mode groups responses by results.
98        /// prints tabular output suitable for processing with unix tools. For
99        /// instance, to quickly check that all validators agree on the history of an object:
100        /// ```text
101        /// $ sui-tool fetch-object --id 0x260efde76ebccf57f4c5e951157f5c361cde822c \
102        ///      --genesis $HOME/.sui/sui_config/genesis.blob \
103        ///      --verbosity concise --concise-no-header
104        /// ```
105        #[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    /// Fetch the effects association with transaction `digest`
121    #[command(name = "fetch-transaction")]
122    FetchTransaction {
123        // RPC address to provide the up-to-date committee info
124        #[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        /// If true, show the input transaction as well as the effects
131        #[arg(long = "show-tx")]
132        show_input_tx: bool,
133    },
134
135    /// Tool to read validator & node db.
136    #[cfg(not(tidehunter))]
137    #[command(name = "db-tool")]
138    DbTool {
139        /// Path of the DB to read
140        #[arg(long = "db-path")]
141        db_path: String,
142        #[command(subcommand)]
143        cmd: Option<DbToolCommand>,
144    },
145    /// Download all packages to the local filesystem from a GraphQL service. Each package gets its
146    /// own sub-directory, named for its ID on chain and version containing two metadata files
147    /// (linkage.json and origins.json), a file containing the overall object and a file for every
148    /// module it contains. Each module file is named for its module name, with a .mv suffix, and
149    /// contains Move bytecode (suitable for passing into a disassembler).
150    #[command(name = "dump-packages")]
151    DumpPackages {
152        /// Connection information for a GraphQL service.
153        #[clap(long, short)]
154        rpc_url: String,
155
156        /// Path to a non-existent directory that can be created and filled with package information.
157        #[clap(long, short)]
158        output_dir: PathBuf,
159
160        /// Only fetch packages that were created before this checkpoint (given by its sequence
161        /// number).
162        #[clap(long)]
163        before_checkpoint: Option<u64>,
164
165        /// If false (default), log level will be overridden to "off", and output will be reduced to
166        /// necessary status information.
167        #[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    /// Fetch authenticated checkpoint information at a specific sequence number.
190    /// If sequence number is not specified, get the latest authenticated checkpoint.
191    #[command(name = "fetch-checkpoint")]
192    FetchCheckpoint {
193        // RPC address to provide the up-to-date committee info
194        #[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        /// skip downloading indexes dir
228        #[clap(long = "skip-indexes")]
229        skip_indexes: bool,
230        /// Number of parallel downloads to perform. Defaults to 50, max 200.
231        #[clap(long = "num-parallel-downloads")]
232        num_parallel_downloads: Option<usize>,
233        /// Network to download snapshot for. Defaults to "mainnet".
234        /// If `--snapshot-bucket` or `--archive-bucket` is not specified,
235        /// the value of this flag is used to construct default bucket names.
236        #[clap(long = "network", default_value = "mainnet")]
237        network: Chain,
238        /// Snapshot bucket name. If not specified, defaults are
239        /// based on value of `--network` flag.
240        #[clap(long = "snapshot-bucket", conflicts_with = "no_sign_request")]
241        snapshot_bucket: Option<String>,
242        /// Snapshot bucket type
243        #[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        /// Path to snapshot directory on local filesystem.
250        /// Only applicable if `--snapshot-bucket-type` is "file".
251        #[clap(
252            long = "snapshot-path",
253            help = "only used for testing, when --snapshot-bucket-type=FILE"
254        )]
255        snapshot_path: Option<PathBuf>,
256        /// If true, no authentication is needed for snapshot restores
257        #[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        /// Download snapshot of the latest available epoch.
264        /// If `--epoch` is specified, then this flag gets ignored.
265        #[clap(
266            long = "latest",
267            conflicts_with = "epoch",
268            help = "defaults to latest available snapshot in chosen bucket"
269        )]
270        latest: bool,
271        /// If false (default), log level will be overridden to "off",
272        /// and output will be reduced to necessary status information.
273        #[clap(long = "verbose")]
274        verbose: bool,
275        /// Number of retries for failed HTTP requests when downloading snapshot files.
276        /// Defaults to 3 retries. Set to 0 to disable retries.
277        #[clap(long = "max-retries", default_value = "3")]
278        max_retries: usize,
279    },
280
281    // Restore from formal (slim, DB agnostic) snapshot. Note that this is only supported
282    /// for protocol versions supporting `commit_root_state_digest`. For mainnet, this is
283    /// epoch 20+, and for testnet this is epoch 12+
284    #[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        /// Number of parallel downloads to perform. Defaults to 50, max 200.
296        #[clap(long = "num-parallel-downloads")]
297        num_parallel_downloads: Option<usize>,
298        /// Verification mode to employ.
299        #[clap(long = "verify", default_value = "normal")]
300        verify: Option<SnapshotVerifyMode>,
301        /// Network to download snapshot for. Defaults to "mainnet".
302        /// If `--snapshot-bucket` or `--archive-bucket` is not specified,
303        /// the value of this flag is used to construct default bucket names.
304        #[clap(long = "network", default_value = "mainnet")]
305        network: Chain,
306        /// Snapshot bucket name. If not specified, defaults are
307        /// based on value of `--network` flag.
308        #[clap(long = "snapshot-bucket", conflicts_with = "no_sign_request")]
309        snapshot_bucket: Option<String>,
310        /// Snapshot bucket type
311        #[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        /// Path to snapshot directory on local filesystem.
318        /// Only applicable if `--snapshot-bucket-type` is "file".
319        #[clap(long = "snapshot-path")]
320        snapshot_path: Option<PathBuf>,
321        /// If true, no authentication is needed for snapshot restores
322        #[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        /// Download snapshot of the latest available epoch.
329        /// If `--epoch` is specified, then this flag gets ignored.
330        #[clap(
331            long = "latest",
332            conflicts_with = "epoch",
333            help = "defaults to latest available snapshot in chosen bucket"
334        )]
335        latest: bool,
336        /// If false (default), log level will be overridden to "off",
337        /// and output will be reduced to necessary status information.
338        #[clap(long = "verbose")]
339        verbose: bool,
340
341        /// Number of retries for failed HTTP requests when downloading snapshot files.
342        /// Defaults to 3 retries. Set to 0 to disable retries.
343        #[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    /// Ask all validators to sign a transaction through AuthorityAggregator.
374    #[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                    // write summary to file
648                    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}