sui_tool/
commands.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use 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    /// Inspect if a specific object is or all gas objects owned by an address are locked by validators
59    #[command(name = "locked-object")]
60    LockedObject {
61        /// Either id or address must be provided
62        /// The object to check
63        #[arg(long, help = "The object ID to fetch")]
64        id: Option<ObjectID>,
65        /// Either id or address must be provided
66        /// If provided, check all gas objects owned by this account
67        #[arg(long = "address")]
68        address: Option<SuiAddress>,
69        /// RPC address to provide the up-to-date committee info
70        #[arg(long = "fullnode-rpc-url")]
71        fullnode_rpc_url: String,
72        /// Should attempt to rescue the object if it's locked but not fully locked
73        #[arg(long = "rescue")]
74        rescue: bool,
75    },
76
77    /// Fetch the same object from all validators
78    #[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        // RPC address to provide the up-to-date committee info
93        #[arg(long = "fullnode-rpc-url")]
94        fullnode_rpc_url: String,
95
96        /// Concise mode groups responses by results.
97        /// prints tabular output suitable for processing with unix tools. For
98        /// instance, to quickly check that all validators agree on the history of an object:
99        /// ```text
100        /// $ sui-tool fetch-object --id 0x260efde76ebccf57f4c5e951157f5c361cde822c \
101        ///      --genesis $HOME/.sui/sui_config/genesis.blob \
102        ///      --verbosity concise --concise-no-header
103        /// ```
104        #[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    /// Fetch the effects association with transaction `digest`
120    #[command(name = "fetch-transaction")]
121    FetchTransaction {
122        // RPC address to provide the up-to-date committee info
123        #[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        /// If true, show the input transaction as well as the effects
130        #[arg(long = "show-tx")]
131        show_input_tx: bool,
132    },
133
134    /// Tool to read validator & node db.
135    #[command(name = "db-tool")]
136    DbTool {
137        /// Path of the DB to read
138        #[arg(long = "db-path")]
139        db_path: String,
140        #[command(subcommand)]
141        cmd: Option<DbToolCommand>,
142    },
143    /// Download all packages to the local filesystem from a GraphQL service. Each package gets its
144    /// own sub-directory, named for its ID on chain and version containing two metadata files
145    /// (linkage.json and origins.json), a file containing the overall object and a file for every
146    /// module it contains. Each module file is named for its module name, with a .mv suffix, and
147    /// contains Move bytecode (suitable for passing into a disassembler).
148    #[command(name = "dump-packages")]
149    DumpPackages {
150        /// Connection information for a GraphQL service.
151        #[clap(long, short)]
152        rpc_url: String,
153
154        /// Path to a non-existent directory that can be created and filled with package information.
155        #[clap(long, short)]
156        output_dir: PathBuf,
157
158        /// Only fetch packages that were created before this checkpoint (given by its sequence
159        /// number).
160        #[clap(long)]
161        before_checkpoint: Option<u64>,
162
163        /// If false (default), log level will be overridden to "off", and output will be reduced to
164        /// necessary status information.
165        #[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    /// Fetch authenticated checkpoint information at a specific sequence number.
188    /// If sequence number is not specified, get the latest authenticated checkpoint.
189    #[command(name = "fetch-checkpoint")]
190    FetchCheckpoint {
191        // RPC address to provide the up-to-date committee info
192        #[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        /// skip downloading indexes dir
226        #[clap(long = "skip-indexes")]
227        skip_indexes: bool,
228        /// Number of parallel downloads to perform. Defaults to 50, max 200.
229        #[clap(long = "num-parallel-downloads")]
230        num_parallel_downloads: Option<usize>,
231        /// Network to download snapshot for. Defaults to "mainnet".
232        /// If `--snapshot-bucket` or `--archive-bucket` is not specified,
233        /// the value of this flag is used to construct default bucket names.
234        #[clap(long = "network", default_value = "mainnet")]
235        network: Chain,
236        /// Snapshot bucket name. If not specified, defaults are
237        /// based on value of `--network` flag.
238        #[clap(long = "snapshot-bucket", conflicts_with = "no_sign_request")]
239        snapshot_bucket: Option<String>,
240        /// Snapshot bucket type
241        #[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        /// Path to snapshot directory on local filesystem.
248        /// Only applicable if `--snapshot-bucket-type` is "file".
249        #[clap(
250            long = "snapshot-path",
251            help = "only used for testing, when --snapshot-bucket-type=FILE"
252        )]
253        snapshot_path: Option<PathBuf>,
254        /// If true, no authentication is needed for snapshot restores
255        #[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        /// Download snapshot of the latest available epoch.
262        /// If `--epoch` is specified, then this flag gets ignored.
263        #[clap(
264            long = "latest",
265            conflicts_with = "epoch",
266            help = "defaults to latest available snapshot in chosen bucket"
267        )]
268        latest: bool,
269        /// If false (default), log level will be overridden to "off",
270        /// and output will be reduced to necessary status information.
271        #[clap(long = "verbose")]
272        verbose: bool,
273        /// Number of retries for failed HTTP requests when downloading snapshot files.
274        /// Defaults to 3 retries. Set to 0 to disable retries.
275        #[clap(long = "max-retries", default_value = "3")]
276        max_retries: usize,
277    },
278
279    // Restore from formal (slim, DB agnostic) snapshot. Note that this is only supported
280    /// for protocol versions supporting `commit_root_state_digest`. For mainnet, this is
281    /// epoch 20+, and for testnet this is epoch 12+
282    #[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        /// Number of parallel downloads to perform. Defaults to 50, max 200.
294        #[clap(long = "num-parallel-downloads")]
295        num_parallel_downloads: Option<usize>,
296        /// Verification mode to employ.
297        #[clap(long = "verify", default_value = "normal")]
298        verify: Option<SnapshotVerifyMode>,
299        /// Network to download snapshot for. Defaults to "mainnet".
300        /// If `--snapshot-bucket` or `--archive-bucket` is not specified,
301        /// the value of this flag is used to construct default bucket names.
302        #[clap(long = "network", default_value = "mainnet")]
303        network: Chain,
304        /// Snapshot bucket name. If not specified, defaults are
305        /// based on value of `--network` flag.
306        #[clap(long = "snapshot-bucket", conflicts_with = "no_sign_request")]
307        snapshot_bucket: Option<String>,
308        /// Snapshot bucket type
309        #[clap(
310            long = "snapshot-bucket-type",
311            conflicts_with = "no_sign_request",
312            help = "Required if --no-sign-request is not set"
313        )]
314        snapshot_bucket_type: Option<ObjectStoreType>,
315        /// Path to snapshot directory on local filesystem.
316        /// Only applicable if `--snapshot-bucket-type` is "file".
317        #[clap(long = "snapshot-path")]
318        snapshot_path: Option<PathBuf>,
319        /// If true, no authentication is needed for snapshot restores
320        #[clap(
321            long = "no-sign-request",
322            conflicts_with_all = &["snapshot_bucket", "snapshot_bucket_type"],
323            help = "if set, no authentication is needed for snapshot restore"
324        )]
325        no_sign_request: bool,
326        /// Download snapshot of the latest available epoch.
327        /// If `--epoch` is specified, then this flag gets ignored.
328        #[clap(
329            long = "latest",
330            conflicts_with = "epoch",
331            help = "defaults to latest available snapshot in chosen bucket"
332        )]
333        latest: bool,
334        /// If false (default), log level will be overridden to "off",
335        /// and output will be reduced to necessary status information.
336        #[clap(long = "verbose")]
337        verbose: bool,
338
339        /// Number of retries for failed HTTP requests when downloading snapshot files.
340        /// Defaults to 3 retries. Set to 0 to disable retries.
341        #[clap(long = "max-retries", default_value = "3")]
342        max_retries: usize,
343    },
344
345    #[clap(name = "replay")]
346    Replay {
347        #[arg(long = "rpc")]
348        rpc_url: Option<String>,
349        #[arg(long = "safety-checks")]
350        safety_checks: bool,
351        #[arg(long = "authority")]
352        use_authority: bool,
353        #[arg(
354            long = "cfg-path",
355            short,
356            help = "Path to the network config file. This should be specified when rpc_url is not present. \
357            If not specified we will use the default network config file at ~/.sui-replay/network-config.yaml"
358        )]
359        cfg_path: Option<PathBuf>,
360        #[arg(
361            long,
362            help = "The name of the chain to replay from, could be one of: mainnet, testnet, devnet.\
363            When rpc_url is not specified, this is used to load the corresponding config from the network config file.\
364            If not specified, mainnet will be used by default"
365        )]
366        chain: Option<String>,
367        #[command(subcommand)]
368        cmd: ReplayToolCommand,
369    },
370}
371
372async fn check_locked_object(
373    sui_client: &Client,
374    committee: Arc<BTreeMap<AuthorityPublicKeyBytes, u64>>,
375    id: ObjectID,
376    rescue: bool,
377) -> anyhow::Result<()> {
378    let clients = Arc::new(make_clients(sui_client).await?);
379    let output = get_object(id, None, None, clients.clone()).await?;
380    let output = GroupedObjectOutput::new(output, committee);
381    if output.fully_locked {
382        println!("Object {} is fully locked.", id);
383        return Ok(());
384    }
385    let top_record = output.voting_power.first().unwrap();
386    let top_record_stake = top_record.1;
387    let top_record = top_record.0.clone().unwrap();
388    if top_record.4.is_none() {
389        println!(
390            "Object {} does not seem to be locked by majority of validators (unlocked stake: {})",
391            id, top_record_stake
392        );
393        return Ok(());
394    }
395
396    let tx_digest = top_record.2;
397    if !rescue {
398        println!("Object {} is rescueable, top tx: {:?}", id, tx_digest);
399        return Ok(());
400    }
401    println!("Object {} is rescueable, trying tx {}", id, tx_digest);
402    let validator = output
403        .grouped_results
404        .get(&Some(top_record))
405        .unwrap()
406        .first()
407        .unwrap();
408    let client = &clients.get(validator).unwrap().1;
409    let tx = client
410        .handle_transaction_info_request(TransactionInfoRequest {
411            transaction_digest: tx_digest,
412        })
413        .await?
414        .transaction;
415    let tx = Transaction::new(tx);
416    let res = sui_client.clone().execute_transaction(&tx).await;
417    match res {
418        Ok(_) => {
419            println!("Transaction executed successfully ({:?})", tx_digest);
420        }
421        Err(e) => {
422            println!("Failed to execute transaction ({:?}): {:?}", tx_digest, e);
423        }
424    }
425    Ok(())
426}
427
428impl ToolCommand {
429    #[allow(clippy::format_in_format_args)]
430    pub async fn execute(self, tracing_handle: TracingHandle) -> Result<(), anyhow::Error> {
431        match self {
432            ToolCommand::ScanConsensusCommits {
433                db_path,
434                start_commit,
435                end_commit,
436            } => {
437                let rocks_db_store = RocksDBStore::new(&db_path);
438
439                let start_commit = start_commit.unwrap_or(0);
440                let end_commit = end_commit.unwrap_or(u32::MAX);
441
442                let commits = rocks_db_store
443                    .scan_commits(CommitRange::new(start_commit..=end_commit))
444                    .unwrap();
445                println!("found {} consensus commits", commits.len());
446
447                for commit in commits {
448                    let inner = &*commit;
449                    let block_refs = inner.blocks();
450                    let blocks = rocks_db_store.read_blocks(block_refs).unwrap();
451
452                    for block in blocks.iter().flatten() {
453                        let data = block.transactions_data();
454                        println!(
455                            "\"index\": \"{}\", \"leader\": \"{}\", \"blocks\": \"{:#?}\", {} txs",
456                            inner.index(),
457                            inner.leader(),
458                            inner.blocks(),
459                            data.len()
460                        );
461                        for txns in &data {
462                            let tx: ConsensusTransaction = bcs::from_bytes(txns).unwrap();
463                            println!("\t{:?}", tx.key());
464                        }
465                    }
466                }
467            }
468            ToolCommand::LockedObject {
469                id,
470                fullnode_rpc_url,
471                rescue,
472                address,
473            } => {
474                let sui_client = Client::new(fullnode_rpc_url)?;
475                let committee = Arc::new(
476                    sui_client
477                        .get_committee(None)
478                        .await?
479                        .voting_rights
480                        .into_iter()
481                        .collect::<BTreeMap<_, _>>(),
482                );
483                let object_ids = match id {
484                    Some(id) => vec![id],
485                    None => {
486                        let address = address.expect("Either id or address must be provided");
487                        sui_client
488                            .list_owned_objects(address, Some(GasCoin::type_()))
489                            .map_ok(|o| o.id())
490                            .try_collect()
491                            .await?
492                    }
493                };
494                for ids in object_ids.chunks(30) {
495                    let mut tasks = vec![];
496                    for id in ids {
497                        tasks.push(check_locked_object(
498                            &sui_client,
499                            committee.clone(),
500                            *id,
501                            rescue,
502                        ))
503                    }
504                    join_all(tasks)
505                        .await
506                        .into_iter()
507                        .collect::<Result<Vec<_>, _>>()?;
508                }
509            }
510            ToolCommand::FetchObject {
511                id,
512                validator,
513                version,
514                fullnode_rpc_url,
515                verbosity,
516                concise_no_header,
517            } => {
518                let sui_client = Client::new(fullnode_rpc_url)?;
519                let clients = Arc::new(make_clients(&sui_client).await?);
520                let output = get_object(id, version, validator, clients).await?;
521
522                match verbosity {
523                    Verbosity::Grouped => {
524                        let committee = Arc::new(
525                            sui_client
526                                .get_committee(None)
527                                .await?
528                                .voting_rights
529                                .into_iter()
530                                .collect::<BTreeMap<_, _>>(),
531                        );
532                        println!("{}", GroupedObjectOutput::new(output, committee));
533                    }
534                    Verbosity::Verbose => {
535                        println!("{}", VerboseObjectOutput(output));
536                    }
537                    Verbosity::Concise => {
538                        if !concise_no_header {
539                            println!("{}", ConciseObjectOutput::header());
540                        }
541                        println!("{}", ConciseObjectOutput(output));
542                    }
543                }
544            }
545            ToolCommand::FetchTransaction {
546                digest,
547                show_input_tx,
548                fullnode_rpc_url,
549            } => {
550                print!(
551                    "{}",
552                    get_transaction_block(digest, show_input_tx, fullnode_rpc_url).await?
553                );
554            }
555            ToolCommand::DbTool { db_path, cmd } => {
556                let path = PathBuf::from(db_path);
557                match cmd {
558                    Some(c) => execute_db_tool_command(path, c).await?,
559                    None => print_db_all_tables(path)?,
560                }
561            }
562            ToolCommand::DumpPackages {
563                rpc_url,
564                output_dir,
565                before_checkpoint,
566                verbose,
567            } => {
568                if !verbose {
569                    tracing_handle
570                        .update_log("off")
571                        .expect("Failed to update log level");
572                }
573
574                sui_package_dump::dump(rpc_url, output_dir, before_checkpoint).await?;
575            }
576            ToolCommand::DumpValidators { genesis, concise } => {
577                let genesis = Genesis::load(genesis).unwrap();
578                if !concise {
579                    println!("{:#?}", genesis.validator_set_for_tooling());
580                } else {
581                    for (i, val_info) in genesis.validator_set_for_tooling().iter().enumerate() {
582                        let metadata = val_info.verified_metadata();
583                        println!(
584                            "#{:<2} {:<20} {:?} {:?} {}",
585                            i,
586                            metadata.name,
587                            metadata.sui_pubkey_bytes().concise(),
588                            metadata.net_address,
589                            anemo::PeerId(metadata.network_pubkey.0.to_bytes()),
590                        )
591                    }
592                }
593            }
594            ToolCommand::DumpGenesis { genesis } => {
595                let genesis = Genesis::load(genesis)?;
596                println!("{:#?}", genesis);
597            }
598            ToolCommand::FetchCheckpoint {
599                sequence_number,
600                fullnode_rpc_url,
601            } => {
602                let sui_client = Client::new(fullnode_rpc_url)?;
603                let clients = make_clients(&sui_client).await?;
604
605                for (name, (_, client)) in clients {
606                    let resp = client
607                        .handle_checkpoint(CheckpointRequest {
608                            sequence_number,
609                            request_content: true,
610                        })
611                        .await
612                        .unwrap();
613                    let CheckpointResponse {
614                        checkpoint,
615                        contents,
616                    } = resp;
617
618                    let summary = checkpoint.clone().unwrap().data().clone();
619                    // write summary to file
620                    let mut file = std::fs::File::create("/tmp/ckpt_summary")
621                        .expect("Failed to create /tmp/summary");
622                    let bytes =
623                        bcs::to_bytes(&summary).expect("Failed to serialize summary to BCS");
624                    use std::io::Write;
625                    file.write_all(&bytes)
626                        .expect("Failed to write summary to /tmp/ckpt_summary");
627
628                    println!("Validator: {:?}\n", name.concise());
629                    println!("Checkpoint: {:?}\n", checkpoint);
630                    println!("Content: {:?}\n", contents);
631                }
632            }
633            ToolCommand::Anemo { args } => {
634                let config = crate::make_anemo_config();
635                anemo_cli::run(config, args).await
636            }
637            ToolCommand::RestoreFromDBCheckpoint {
638                config_path,
639                db_checkpoint_path,
640            } => {
641                let config = sui_config::NodeConfig::load(config_path)?;
642                restore_from_db_checkpoint(&config, &db_checkpoint_path).await?;
643            }
644            ToolCommand::DownloadFormalSnapshot {
645                epoch,
646                genesis,
647                path,
648                num_parallel_downloads,
649                verify,
650                network,
651                snapshot_bucket,
652                snapshot_bucket_type,
653                snapshot_path,
654                no_sign_request,
655                latest,
656                verbose,
657                max_retries,
658            } => {
659                if !verbose {
660                    tracing_handle
661                        .update_log("off")
662                        .expect("Failed to update log level");
663                }
664                let num_parallel_downloads = num_parallel_downloads.unwrap_or(50).min(200);
665                let snapshot_bucket =
666                    snapshot_bucket.or_else(|| match (network, no_sign_request) {
667                        (Chain::Mainnet, false) => Some(
668                            env::var("MAINNET_FORMAL_SIGNED_BUCKET")
669                                .unwrap_or("mysten-mainnet-formal".to_string()),
670                        ),
671                        (Chain::Mainnet, true) => env::var("MAINNET_FORMAL_UNSIGNED_BUCKET").ok(),
672                        (Chain::Testnet, true) => env::var("TESTNET_FORMAL_UNSIGNED_BUCKET").ok(),
673                        (Chain::Testnet, _) => Some(
674                            env::var("TESTNET_FORMAL_SIGNED_BUCKET")
675                                .unwrap_or("mysten-testnet-formal".to_string()),
676                        ),
677                        (Chain::Unknown, _) => {
678                            panic!("Cannot generate default snapshot bucket for unknown network");
679                        }
680                    });
681
682                let aws_endpoint = env::var("AWS_SNAPSHOT_ENDPOINT").ok().or_else(|| {
683                    if no_sign_request {
684                        if network == Chain::Mainnet {
685                            Some("https://formal-snapshot.mainnet.sui.io".to_string())
686                        } else if network == Chain::Testnet {
687                            Some("https://formal-snapshot.testnet.sui.io".to_string())
688                        } else {
689                            None
690                        }
691                    } else {
692                        None
693                    }
694                });
695
696                let snapshot_bucket_type = if no_sign_request {
697                    ObjectStoreType::S3
698                } else {
699                    snapshot_bucket_type
700                        .expect("You must set either --snapshot-bucket-type or --no-sign-request")
701                };
702                let snapshot_store_config = match snapshot_bucket_type {
703                    ObjectStoreType::S3 => ObjectStoreConfig {
704                        object_store: Some(ObjectStoreType::S3),
705                        bucket: snapshot_bucket.filter(|s| !s.is_empty()),
706                        aws_access_key_id: env::var("AWS_SNAPSHOT_ACCESS_KEY_ID").ok(),
707                        aws_secret_access_key: env::var("AWS_SNAPSHOT_SECRET_ACCESS_KEY").ok(),
708                        aws_region: env::var("AWS_SNAPSHOT_REGION").ok(),
709                        aws_endpoint: aws_endpoint.filter(|s| !s.is_empty()),
710                        aws_virtual_hosted_style_request: env::var(
711                            "AWS_SNAPSHOT_VIRTUAL_HOSTED_REQUESTS",
712                        )
713                        .ok()
714                        .and_then(|b| b.parse().ok())
715                        .unwrap_or(no_sign_request),
716                        object_store_connection_limit: 200,
717                        no_sign_request,
718                        ..Default::default()
719                    },
720                    ObjectStoreType::GCS => ObjectStoreConfig {
721                        object_store: Some(ObjectStoreType::GCS),
722                        bucket: snapshot_bucket,
723                        google_service_account: env::var("GCS_SNAPSHOT_SERVICE_ACCOUNT_FILE_PATH")
724                            .ok(),
725                        object_store_connection_limit: 200,
726                        no_sign_request,
727                        ..Default::default()
728                    },
729                    ObjectStoreType::Azure => ObjectStoreConfig {
730                        object_store: Some(ObjectStoreType::Azure),
731                        bucket: snapshot_bucket,
732                        azure_storage_account: env::var("AZURE_SNAPSHOT_STORAGE_ACCOUNT").ok(),
733                        azure_storage_access_key: env::var("AZURE_SNAPSHOT_STORAGE_ACCESS_KEY")
734                            .ok(),
735                        object_store_connection_limit: 200,
736                        no_sign_request,
737                        ..Default::default()
738                    },
739                    ObjectStoreType::File => {
740                        if snapshot_path.is_some() {
741                            ObjectStoreConfig {
742                                object_store: Some(ObjectStoreType::File),
743                                directory: snapshot_path,
744                                ..Default::default()
745                            }
746                        } else {
747                            panic!(
748                                "--snapshot-path must be specified for --snapshot-bucket-type=file"
749                            );
750                        }
751                    }
752                };
753
754                let ingestion_url = match network {
755                    Chain::Mainnet => "https://checkpoints.mainnet.sui.io",
756                    Chain::Testnet => "https://checkpoints.testnet.sui.io",
757                    _ => panic!("Cannot generate default ingestion url for unknown network"),
758                };
759
760                let latest_available_epoch =
761                    latest.then_some(get_latest_available_epoch(&snapshot_store_config).await?);
762                let epoch_to_download = epoch.or(latest_available_epoch).expect(
763                    "Either pass epoch with --epoch <epoch_num> or use latest with --latest",
764                );
765
766                if let Err(e) =
767                    check_completed_snapshot(&snapshot_store_config, epoch_to_download).await
768                {
769                    panic!(
770                        "Aborting snapshot restore: {}, snapshot may not be uploaded yet",
771                        e
772                    );
773                }
774
775                let verify = verify.unwrap_or_default();
776                download_formal_snapshot(
777                    &path,
778                    epoch_to_download,
779                    &genesis,
780                    snapshot_store_config,
781                    ingestion_url,
782                    num_parallel_downloads,
783                    network,
784                    verify,
785                    max_retries,
786                )
787                .await?;
788            }
789            ToolCommand::DownloadDBSnapshot {
790                epoch,
791                path,
792                skip_indexes,
793                num_parallel_downloads,
794                network,
795                snapshot_bucket,
796                snapshot_bucket_type,
797                snapshot_path,
798                no_sign_request,
799                latest,
800                verbose,
801                max_retries,
802            } => {
803                if no_sign_request {
804                    anyhow::bail!(
805                        "The --no-sign-request flag is no longer supported. \
806                        Please use S3 or GCS buckets with --snapshot-bucket-type and --snapshot-bucket instead. \
807                        For more information, see: https://docs.sui.io/guides/operator/snapshots#mysten-labs-managed-snapshots"
808                    );
809                }
810                if !verbose {
811                    tracing_handle
812                        .update_log("off")
813                        .expect("Failed to update log level");
814                }
815                let num_parallel_downloads = num_parallel_downloads.unwrap_or(50).min(200);
816                let snapshot_bucket =
817                    snapshot_bucket.or_else(|| match (network, no_sign_request) {
818                        (Chain::Mainnet, false) => Some(
819                            env::var("MAINNET_DB_SIGNED_BUCKET")
820                                .unwrap_or("mysten-mainnet-snapshots".to_string()),
821                        ),
822                        (Chain::Mainnet, true) => env::var("MAINNET_DB_UNSIGNED_BUCKET").ok(),
823                        (Chain::Testnet, true) => env::var("TESTNET_DB_UNSIGNED_BUCKET").ok(),
824                        (Chain::Testnet, _) => Some(
825                            env::var("TESTNET_DB_SIGNED_BUCKET")
826                                .unwrap_or("mysten-testnet-snapshots".to_string()),
827                        ),
828                        (Chain::Unknown, _) => {
829                            panic!("Cannot generate default snapshot bucket for unknown network");
830                        }
831                    });
832
833                let aws_endpoint = env::var("AWS_SNAPSHOT_ENDPOINT").ok();
834                let snapshot_bucket_type = if no_sign_request {
835                    ObjectStoreType::S3
836                } else {
837                    snapshot_bucket_type
838                        .expect("You must set either --snapshot-bucket-type or --no-sign-request")
839                };
840                let snapshot_store_config = if no_sign_request {
841                    let aws_endpoint = env::var("AWS_SNAPSHOT_ENDPOINT").ok().or_else(|| {
842                        if network == Chain::Mainnet {
843                            Some("https://db-snapshot.mainnet.sui.io".to_string())
844                        } else if network == Chain::Testnet {
845                            Some("https://db-snapshot.testnet.sui.io".to_string())
846                        } else {
847                            None
848                        }
849                    });
850                    ObjectStoreConfig {
851                        object_store: Some(ObjectStoreType::S3),
852                        aws_endpoint: aws_endpoint.filter(|s| !s.is_empty()),
853                        aws_virtual_hosted_style_request: env::var(
854                            "AWS_SNAPSHOT_VIRTUAL_HOSTED_REQUESTS",
855                        )
856                        .ok()
857                        .and_then(|b| b.parse().ok())
858                        .unwrap_or(no_sign_request),
859                        object_store_connection_limit: 200,
860                        no_sign_request,
861                        ..Default::default()
862                    }
863                } else {
864                    match snapshot_bucket_type {
865                        ObjectStoreType::S3 => ObjectStoreConfig {
866                            object_store: Some(ObjectStoreType::S3),
867                            bucket: snapshot_bucket.filter(|s| !s.is_empty()),
868                            aws_access_key_id: env::var("AWS_SNAPSHOT_ACCESS_KEY_ID").ok(),
869                            aws_secret_access_key: env::var("AWS_SNAPSHOT_SECRET_ACCESS_KEY").ok(),
870                            aws_region: env::var("AWS_SNAPSHOT_REGION").ok(),
871                            aws_endpoint: aws_endpoint.filter(|s| !s.is_empty()),
872                            aws_virtual_hosted_style_request: env::var(
873                                "AWS_SNAPSHOT_VIRTUAL_HOSTED_REQUESTS",
874                            )
875                            .ok()
876                            .and_then(|b| b.parse().ok())
877                            .unwrap_or(no_sign_request),
878                            object_store_connection_limit: 200,
879                            no_sign_request,
880                            ..Default::default()
881                        },
882                        ObjectStoreType::GCS => ObjectStoreConfig {
883                            object_store: Some(ObjectStoreType::GCS),
884                            bucket: snapshot_bucket,
885                            google_service_account: env::var(
886                                "GCS_SNAPSHOT_SERVICE_ACCOUNT_FILE_PATH",
887                            )
888                            .ok(),
889                            google_project_id: env::var("GCS_SNAPSHOT_SERVICE_ACCOUNT_PROJECT_ID")
890                                .ok(),
891                            object_store_connection_limit: 200,
892                            no_sign_request,
893                            ..Default::default()
894                        },
895                        ObjectStoreType::Azure => ObjectStoreConfig {
896                            object_store: Some(ObjectStoreType::Azure),
897                            bucket: snapshot_bucket,
898                            azure_storage_account: env::var("AZURE_SNAPSHOT_STORAGE_ACCOUNT").ok(),
899                            azure_storage_access_key: env::var("AZURE_SNAPSHOT_STORAGE_ACCESS_KEY")
900                                .ok(),
901                            object_store_connection_limit: 200,
902                            no_sign_request,
903                            ..Default::default()
904                        },
905                        ObjectStoreType::File => {
906                            if snapshot_path.is_some() {
907                                ObjectStoreConfig {
908                                    object_store: Some(ObjectStoreType::File),
909                                    directory: snapshot_path,
910                                    ..Default::default()
911                                }
912                            } else {
913                                panic!(
914                                    "--snapshot-path must be specified for --snapshot-bucket-type=file"
915                                );
916                            }
917                        }
918                    }
919                };
920
921                let latest_available_epoch =
922                    latest.then_some(get_latest_available_epoch(&snapshot_store_config).await?);
923                let epoch_to_download = epoch.or(latest_available_epoch).expect(
924                    "Either pass epoch with --epoch <epoch_num> or use latest with --latest",
925                );
926
927                if let Err(e) =
928                    check_completed_snapshot(&snapshot_store_config, epoch_to_download).await
929                {
930                    panic!(
931                        "Aborting snapshot restore: {}, snapshot may not be uploaded yet",
932                        e
933                    );
934                }
935                download_db_snapshot(
936                    &path,
937                    epoch_to_download,
938                    snapshot_store_config,
939                    skip_indexes,
940                    num_parallel_downloads,
941                    max_retries,
942                )
943                .await?;
944            }
945            ToolCommand::Replay {
946                rpc_url,
947                safety_checks,
948                cmd,
949                use_authority,
950                cfg_path,
951                chain,
952            } => {
953                execute_replay_command(rpc_url, safety_checks, use_authority, cfg_path, chain, cmd)
954                    .await?;
955            }
956        };
957        Ok(())
958    }
959}