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