Skip to main content

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_formal_snapshot, get_latest_available_epoch, get_object,
8    get_transaction_block, make_clients, restore_from_db_checkpoint,
9};
10use anyhow::Result;
11use consensus_core::storage::{Store, rocksdb_store::RocksDBStore};
12use consensus_core::{BlockAPI, CommitAPI, CommitRange};
13use futures::TryStreamExt;
14use futures::future::join_all;
15use std::path::PathBuf;
16use std::{collections::BTreeMap, env, sync::Arc};
17use sui_config::genesis::Genesis;
18use sui_core::authority_client::AuthorityAPI;
19use sui_protocol_config::Chain;
20use sui_rpc_api::Client;
21use sui_types::gas_coin::GasCoin;
22use sui_types::messages_consensus::ConsensusTransaction;
23use sui_types::transaction::Transaction;
24use telemetry_subscribers::TracingHandle;
25
26use sui_types::{
27    base_types::*, crypto::AuthorityPublicKeyBytes, messages_grpc::TransactionInfoRequest,
28};
29
30use clap::*;
31use sui_config::Config;
32use sui_config::object_storage_config::{ObjectStoreConfig, ObjectStoreType};
33use sui_types::messages_checkpoint::{
34    CheckpointRequest, CheckpointResponse, CheckpointSequenceNumber,
35};
36
37#[derive(Parser, Clone, ValueEnum)]
38pub enum Verbosity {
39    Grouped,
40    Concise,
41    Verbose,
42}
43
44#[derive(Parser)]
45pub enum ToolCommand {
46    #[command(name = "scan-consensus-commits")]
47    ScanConsensusCommits {
48        #[arg(long = "db-path")]
49        db_path: String,
50        #[arg(long = "start-commit")]
51        start_commit: Option<u32>,
52        #[arg(long = "end-commit")]
53        end_commit: Option<u32>,
54    },
55
56    /// Inspect if a specific object is or all gas objects owned by an address are locked by validators
57    #[command(name = "locked-object")]
58    LockedObject {
59        /// Either id or address must be provided
60        /// The object to check
61        #[arg(long, help = "The object ID to fetch")]
62        id: Option<ObjectID>,
63        /// Either id or address must be provided
64        /// If provided, check all gas objects owned by this account
65        #[arg(long = "address")]
66        address: Option<SuiAddress>,
67        /// RPC address to provide the up-to-date committee info
68        #[arg(long = "fullnode-rpc-url")]
69        fullnode_rpc_url: String,
70        /// Should attempt to rescue the object if it's locked but not fully locked
71        #[arg(long = "rescue")]
72        rescue: bool,
73    },
74
75    /// Fetch the same object from all validators
76    #[command(name = "fetch-object")]
77    FetchObject {
78        #[arg(long, help = "The object ID to fetch")]
79        id: ObjectID,
80
81        #[arg(long, help = "Fetch object at a specific sequence")]
82        version: Option<u64>,
83
84        #[arg(
85            long,
86            help = "Validator to fetch from - if not specified, all validators are queried"
87        )]
88        validator: Option<AuthorityName>,
89
90        // RPC address to provide the up-to-date committee info
91        #[arg(long = "fullnode-rpc-url")]
92        fullnode_rpc_url: String,
93
94        /// Concise mode groups responses by results.
95        /// prints tabular output suitable for processing with unix tools. For
96        /// instance, to quickly check that all validators agree on the history of an object:
97        /// ```text
98        /// $ sui-tool fetch-object --id 0x260efde76ebccf57f4c5e951157f5c361cde822c \
99        ///      --genesis $HOME/.sui/sui_config/genesis.blob \
100        ///      --verbosity concise --concise-no-header
101        /// ```
102        #[arg(
103            value_enum,
104            long = "verbosity",
105            default_value = "grouped",
106            ignore_case = true
107        )]
108        verbosity: Verbosity,
109
110        #[arg(
111            long = "concise-no-header",
112            help = "don't show header in concise output"
113        )]
114        concise_no_header: bool,
115    },
116
117    /// Fetch the effects association with transaction `digest`
118    #[command(name = "fetch-transaction")]
119    FetchTransaction {
120        // RPC address to provide the up-to-date committee info
121        #[arg(long = "fullnode-rpc-url")]
122        fullnode_rpc_url: String,
123
124        #[arg(long, help = "The transaction ID to fetch")]
125        digest: TransactionDigest,
126
127        /// If true, show the input transaction as well as the effects
128        #[arg(long = "show-tx")]
129        show_input_tx: bool,
130    },
131
132    /// Tool to read validator & node db.
133    #[command(name = "db-tool")]
134    DbTool {
135        /// Path of the DB to read
136        #[arg(long = "db-path")]
137        db_path: String,
138        #[command(subcommand)]
139        cmd: Option<DbToolCommand>,
140    },
141    /// Download all packages to the local filesystem from a GraphQL service. Each package gets its
142    /// own sub-directory, named for its ID on chain and version containing two metadata files
143    /// (linkage.json and origins.json), a file containing the overall object and a file for every
144    /// module it contains. Each module file is named for its module name, with a .mv suffix, and
145    /// contains Move bytecode (suitable for passing into a disassembler).
146    #[command(name = "dump-packages")]
147    DumpPackages {
148        /// Connection information for a GraphQL service.
149        #[clap(long, short)]
150        rpc_url: String,
151
152        /// Path to a non-existent directory that can be created and filled with package information.
153        #[clap(long, short)]
154        output_dir: PathBuf,
155
156        /// Only fetch packages that were created before this checkpoint (given by its sequence
157        /// number).
158        #[clap(long)]
159        before_checkpoint: Option<u64>,
160
161        /// If false (default), log level will be overridden to "off", and output will be reduced to
162        /// necessary status information.
163        #[clap(short, long = "verbose")]
164        verbose: bool,
165    },
166
167    #[command(name = "dump-validators")]
168    DumpValidators {
169        #[arg(long = "genesis")]
170        genesis: PathBuf,
171
172        #[arg(
173            long = "concise",
174            help = "show concise output - name, protocol key and network address"
175        )]
176        concise: bool,
177    },
178
179    #[command(name = "dump-genesis")]
180    DumpGenesis {
181        #[arg(long = "genesis")]
182        genesis: PathBuf,
183    },
184
185    /// Fetch authenticated checkpoint information at a specific sequence number.
186    /// If sequence number is not specified, get the latest authenticated checkpoint.
187    #[command(name = "fetch-checkpoint")]
188    FetchCheckpoint {
189        // RPC address to provide the up-to-date committee info
190        #[arg(long = "fullnode-rpc-url")]
191        fullnode_rpc_url: String,
192
193        #[arg(long, help = "Fetch checkpoint at a specific sequence number")]
194        sequence_number: Option<CheckpointSequenceNumber>,
195    },
196
197    #[command(name = "anemo")]
198    Anemo {
199        #[command(next_help_heading = "foo", flatten)]
200        args: anemo_cli::Args,
201    },
202
203    #[command(name = "restore-db")]
204    RestoreFromDBCheckpoint {
205        #[arg(long = "config-path")]
206        config_path: PathBuf,
207        #[arg(long = "db-checkpoint-path")]
208        db_checkpoint_path: PathBuf,
209    },
210
211    // Restore from formal (slim, DB agnostic) snapshot. Note that this is only supported
212    /// for protocol versions supporting `commit_root_state_digest`. For mainnet, this is
213    /// epoch 20+, and for testnet this is epoch 12+
214    #[clap(
215        name = "download-formal-snapshot",
216        about = "Downloads formal database snapshot via cloud object store, outputs to local disk"
217    )]
218    DownloadFormalSnapshot {
219        #[clap(long = "epoch", conflicts_with = "latest")]
220        epoch: Option<u64>,
221        #[clap(long = "genesis")]
222        genesis: PathBuf,
223        #[clap(long = "path")]
224        path: PathBuf,
225        /// Number of parallel downloads to perform. Defaults to 50, max 200.
226        #[clap(long = "num-parallel-downloads")]
227        num_parallel_downloads: Option<usize>,
228        /// Number of parallel chunks for object insertion. Defaults to 8.
229        #[clap(long = "num-parallel-chunks", default_value = "8")]
230        num_parallel_chunks: usize,
231        /// Verification mode to employ.
232        #[clap(long = "verify", default_value = "normal")]
233        verify: Option<SnapshotVerifyMode>,
234        /// Network to download snapshot for. Defaults to "mainnet".
235        /// If `--snapshot-bucket` or `--archive-bucket` is not specified,
236        /// the value of this flag is used to construct default bucket names.
237        #[clap(long = "network", default_value = "mainnet")]
238        network: Chain,
239        /// Snapshot bucket name. If not specified, defaults are
240        /// based on value of `--network` flag.
241        #[clap(long = "snapshot-bucket", conflicts_with = "no_sign_request")]
242        snapshot_bucket: Option<String>,
243        /// Snapshot bucket type
244        #[clap(
245            long = "snapshot-bucket-type",
246            conflicts_with = "no_sign_request",
247            help = "Required if --no-sign-request is not set"
248        )]
249        snapshot_bucket_type: Option<ObjectStoreType>,
250        /// Path to snapshot directory on local filesystem.
251        /// Only applicable if `--snapshot-bucket-type` is "file".
252        #[clap(long = "snapshot-path")]
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
274        /// Number of retries for failed HTTP requests when downloading snapshot files.
275        /// Set to 0 to disable retries.
276        #[clap(long = "max-retries", default_value = "10")]
277        max_retries: usize,
278        /// Port for the Prometheus metrics server. Defaults to 9185.
279        #[clap(long = "metrics-port", default_value = "9185")]
280        metrics_port: u16,
281    },
282
283    /// Interactive shell for navigating the validator database.
284    #[command(name = "db-shell")]
285    DbShell(crate::db_shell::DbShellArgs),
286
287    /// Interactive Rhai shell for inspecting a TideHunter database.
288    #[cfg(all(feature = "tideconsole", not(windows)))]
289    #[command(name = "tideconsole")]
290    TideConsole {
291        /// Path to a TideHunter database directory to open on startup (bound to variable 'db').
292        #[arg(short, long)]
293        db: Option<PathBuf>,
294        /// Rhai snippet to evaluate non-interactively, then exit.
295        #[arg(short, long)]
296        exec: Option<String>,
297        /// Path to a Rhai script file to evaluate non-interactively, then exit.
298        #[arg(short, long)]
299        script: Option<PathBuf>,
300    },
301}
302
303async fn check_locked_object(
304    sui_client: &Client,
305    committee: Arc<BTreeMap<AuthorityPublicKeyBytes, u64>>,
306    id: ObjectID,
307    rescue: bool,
308) -> anyhow::Result<()> {
309    let clients = Arc::new(make_clients(sui_client).await?);
310    let output = get_object(id, None, None, clients.clone()).await?;
311    let output = GroupedObjectOutput::new(output, committee);
312    if output.fully_locked {
313        println!("Object {} is fully locked.", id);
314        return Ok(());
315    }
316    let top_record = output.voting_power.first().unwrap();
317    let top_record_stake = top_record.1;
318    let top_record = top_record.0.clone().unwrap();
319    if top_record.4.is_none() {
320        println!(
321            "Object {} does not seem to be locked by majority of validators (unlocked stake: {})",
322            id, top_record_stake
323        );
324        return Ok(());
325    }
326
327    let tx_digest = top_record.2;
328    if !rescue {
329        println!("Object {} is rescueable, top tx: {:?}", id, tx_digest);
330        return Ok(());
331    }
332    println!("Object {} is rescueable, trying tx {}", id, tx_digest);
333    let validator = output
334        .grouped_results
335        .get(&Some(top_record))
336        .unwrap()
337        .first()
338        .unwrap();
339    let client = &clients.get(validator).unwrap().1;
340    let tx = client
341        .handle_transaction_info_request(TransactionInfoRequest {
342            transaction_digest: tx_digest,
343        })
344        .await?
345        .transaction;
346    let tx = Transaction::new(tx);
347    let res = sui_client.clone().execute_transaction(&tx).await;
348    match res {
349        Ok(_) => {
350            println!("Transaction executed successfully ({:?})", tx_digest);
351        }
352        Err(e) => {
353            println!("Failed to execute transaction ({:?}): {:?}", tx_digest, e);
354        }
355    }
356    Ok(())
357}
358
359impl ToolCommand {
360    #[allow(clippy::format_in_format_args)]
361    pub async fn execute(self, tracing_handle: TracingHandle) -> Result<(), anyhow::Error> {
362        match self {
363            ToolCommand::ScanConsensusCommits {
364                db_path,
365                start_commit,
366                end_commit,
367            } => {
368                let rocks_db_store = RocksDBStore::new(&db_path);
369
370                let start_commit = start_commit.unwrap_or(0);
371                let end_commit = end_commit.unwrap_or(u32::MAX);
372
373                let commits = rocks_db_store
374                    .scan_commits(CommitRange::new(start_commit..=end_commit))
375                    .unwrap();
376                println!("found {} consensus commits", commits.len());
377
378                for commit in commits {
379                    let inner = &*commit;
380                    let block_refs = inner.blocks();
381                    let blocks = rocks_db_store.read_blocks(block_refs).unwrap();
382
383                    for block in blocks.iter().flatten() {
384                        let data = block.transactions_data();
385                        println!(
386                            "\"index\": \"{}\", \"leader\": \"{}\", \"blocks\": \"{:#?}\", {} txs",
387                            inner.index(),
388                            inner.leader(),
389                            inner.blocks(),
390                            data.len()
391                        );
392                        for txns in &data {
393                            let tx: ConsensusTransaction = bcs::from_bytes(txns).unwrap();
394                            println!("\t{:?}", tx.key());
395                        }
396                    }
397                }
398            }
399            ToolCommand::LockedObject {
400                id,
401                fullnode_rpc_url,
402                rescue,
403                address,
404            } => {
405                let sui_client = Client::new(fullnode_rpc_url)?;
406                let committee = Arc::new(
407                    sui_client
408                        .get_committee(None)
409                        .await?
410                        .voting_rights
411                        .into_iter()
412                        .collect::<BTreeMap<_, _>>(),
413                );
414                let object_ids = match id {
415                    Some(id) => vec![id],
416                    None => {
417                        let address = address.expect("Either id or address must be provided");
418                        sui_client
419                            .list_owned_objects(address, Some(GasCoin::type_()))
420                            .map_ok(|o| o.id())
421                            .try_collect()
422                            .await?
423                    }
424                };
425                for ids in object_ids.chunks(30) {
426                    let mut tasks = vec![];
427                    for id in ids {
428                        tasks.push(check_locked_object(
429                            &sui_client,
430                            committee.clone(),
431                            *id,
432                            rescue,
433                        ))
434                    }
435                    join_all(tasks)
436                        .await
437                        .into_iter()
438                        .collect::<Result<Vec<_>, _>>()?;
439                }
440            }
441            ToolCommand::FetchObject {
442                id,
443                validator,
444                version,
445                fullnode_rpc_url,
446                verbosity,
447                concise_no_header,
448            } => {
449                let sui_client = Client::new(fullnode_rpc_url)?;
450                let clients = Arc::new(make_clients(&sui_client).await?);
451                let output = get_object(id, version, validator, clients).await?;
452
453                match verbosity {
454                    Verbosity::Grouped => {
455                        let committee = Arc::new(
456                            sui_client
457                                .get_committee(None)
458                                .await?
459                                .voting_rights
460                                .into_iter()
461                                .collect::<BTreeMap<_, _>>(),
462                        );
463                        println!("{}", GroupedObjectOutput::new(output, committee));
464                    }
465                    Verbosity::Verbose => {
466                        println!("{}", VerboseObjectOutput(output));
467                    }
468                    Verbosity::Concise => {
469                        if !concise_no_header {
470                            println!("{}", ConciseObjectOutput::header());
471                        }
472                        println!("{}", ConciseObjectOutput(output));
473                    }
474                }
475            }
476            ToolCommand::FetchTransaction {
477                digest,
478                show_input_tx,
479                fullnode_rpc_url,
480            } => {
481                print!(
482                    "{}",
483                    get_transaction_block(digest, show_input_tx, fullnode_rpc_url).await?
484                );
485            }
486            ToolCommand::DbTool { db_path, cmd } => {
487                let path = PathBuf::from(db_path);
488                match cmd {
489                    Some(c) => execute_db_tool_command(path, c).await?,
490                    None => print_db_all_tables(path)?,
491                }
492            }
493            ToolCommand::DumpPackages {
494                rpc_url,
495                output_dir,
496                before_checkpoint,
497                verbose,
498            } => {
499                if !verbose {
500                    tracing_handle
501                        .update_log("off")
502                        .expect("Failed to update log level");
503                }
504
505                sui_package_dump::dump(rpc_url, output_dir, before_checkpoint).await?;
506            }
507            ToolCommand::DumpValidators { genesis, concise } => {
508                let genesis = Genesis::load(genesis).unwrap();
509                if !concise {
510                    println!("{:#?}", genesis.validator_set_for_tooling());
511                } else {
512                    for (i, val_info) in genesis.validator_set_for_tooling().iter().enumerate() {
513                        let metadata = val_info.verified_metadata();
514                        println!(
515                            "#{:<2} {:<20} {:?} {:?} {}",
516                            i,
517                            metadata.name,
518                            metadata.sui_pubkey_bytes().concise(),
519                            metadata.net_address,
520                            anemo::PeerId(metadata.network_pubkey.0.to_bytes()),
521                        )
522                    }
523                }
524            }
525            ToolCommand::DumpGenesis { genesis } => {
526                let genesis = Genesis::load(genesis)?;
527                println!("{:#?}", genesis);
528            }
529            ToolCommand::FetchCheckpoint {
530                sequence_number,
531                fullnode_rpc_url,
532            } => {
533                let sui_client = Client::new(fullnode_rpc_url)?;
534                let clients = make_clients(&sui_client).await?;
535
536                for (name, (_, client)) in clients {
537                    let resp = client
538                        .handle_checkpoint(CheckpointRequest {
539                            sequence_number,
540                            request_content: true,
541                        })
542                        .await
543                        .unwrap();
544                    let CheckpointResponse {
545                        checkpoint,
546                        contents,
547                    } = resp;
548
549                    let summary = checkpoint.clone().unwrap().data().clone();
550                    // write summary to file
551                    let mut file = std::fs::File::create("/tmp/ckpt_summary")
552                        .expect("Failed to create /tmp/summary");
553                    let bytes =
554                        bcs::to_bytes(&summary).expect("Failed to serialize summary to BCS");
555                    use std::io::Write;
556                    file.write_all(&bytes)
557                        .expect("Failed to write summary to /tmp/ckpt_summary");
558
559                    println!("Validator: {:?}\n", name.concise());
560                    println!("Checkpoint: {:?}\n", checkpoint);
561                    println!("Content: {:?}\n", contents);
562                }
563            }
564            ToolCommand::Anemo { args } => {
565                let config = crate::make_anemo_config();
566                anemo_cli::run(config, args).await
567            }
568            ToolCommand::RestoreFromDBCheckpoint {
569                config_path,
570                db_checkpoint_path,
571            } => {
572                let config = sui_config::NodeConfig::load(config_path)?;
573                restore_from_db_checkpoint(&config, &db_checkpoint_path).await?;
574            }
575            ToolCommand::DownloadFormalSnapshot {
576                epoch,
577                genesis,
578                path,
579                num_parallel_downloads,
580                num_parallel_chunks,
581                verify,
582                network,
583                snapshot_bucket,
584                snapshot_bucket_type,
585                snapshot_path,
586                no_sign_request,
587                latest,
588                verbose,
589                max_retries,
590                metrics_port,
591            } => {
592                if !verbose {
593                    tracing_handle
594                        .update_log("off")
595                        .expect("Failed to update log level");
596                }
597                let num_parallel_downloads = num_parallel_downloads.unwrap_or(50).min(200);
598                let snapshot_bucket =
599                    snapshot_bucket.or_else(|| match (network, no_sign_request) {
600                        (Chain::Mainnet, false) => Some(
601                            env::var("MAINNET_FORMAL_SIGNED_BUCKET")
602                                .unwrap_or("mysten-mainnet-formal".to_string()),
603                        ),
604                        (Chain::Mainnet, true) => env::var("MAINNET_FORMAL_UNSIGNED_BUCKET").ok(),
605                        (Chain::Testnet, true) => env::var("TESTNET_FORMAL_UNSIGNED_BUCKET").ok(),
606                        (Chain::Testnet, _) => Some(
607                            env::var("TESTNET_FORMAL_SIGNED_BUCKET")
608                                .unwrap_or("mysten-testnet-formal".to_string()),
609                        ),
610                        (Chain::Unknown, _) => {
611                            panic!("Cannot generate default snapshot bucket for unknown network");
612                        }
613                    });
614
615                let aws_endpoint = env::var("AWS_SNAPSHOT_ENDPOINT").ok().or_else(|| {
616                    if no_sign_request {
617                        if network == Chain::Mainnet {
618                            Some("https://formal-snapshot.mainnet.sui.io".to_string())
619                        } else if network == Chain::Testnet {
620                            Some("https://formal-snapshot.testnet.sui.io".to_string())
621                        } else {
622                            None
623                        }
624                    } else {
625                        None
626                    }
627                });
628
629                let snapshot_bucket_type = if no_sign_request {
630                    ObjectStoreType::S3
631                } else {
632                    snapshot_bucket_type
633                        .expect("You must set either --snapshot-bucket-type or --no-sign-request")
634                };
635                let snapshot_store_config = match snapshot_bucket_type {
636                    ObjectStoreType::S3 => ObjectStoreConfig {
637                        object_store: Some(ObjectStoreType::S3),
638                        bucket: snapshot_bucket.filter(|s| !s.is_empty()),
639                        aws_access_key_id: env::var("AWS_SNAPSHOT_ACCESS_KEY_ID").ok(),
640                        aws_secret_access_key: env::var("AWS_SNAPSHOT_SECRET_ACCESS_KEY").ok(),
641                        aws_region: env::var("AWS_SNAPSHOT_REGION").ok(),
642                        aws_endpoint: aws_endpoint.filter(|s| !s.is_empty()),
643                        aws_virtual_hosted_style_request: env::var(
644                            "AWS_SNAPSHOT_VIRTUAL_HOSTED_REQUESTS",
645                        )
646                        .ok()
647                        .and_then(|b| b.parse().ok())
648                        .unwrap_or(no_sign_request),
649                        object_store_connection_limit: 200,
650                        no_sign_request,
651                        ..Default::default()
652                    },
653                    ObjectStoreType::GCS => ObjectStoreConfig {
654                        object_store: Some(ObjectStoreType::GCS),
655                        bucket: snapshot_bucket,
656                        google_service_account: env::var("GCS_SNAPSHOT_SERVICE_ACCOUNT_FILE_PATH")
657                            .ok(),
658                        object_store_connection_limit: 200,
659                        no_sign_request,
660                        ..Default::default()
661                    },
662                    ObjectStoreType::Azure => ObjectStoreConfig {
663                        object_store: Some(ObjectStoreType::Azure),
664                        bucket: snapshot_bucket,
665                        azure_storage_account: env::var("AZURE_SNAPSHOT_STORAGE_ACCOUNT").ok(),
666                        azure_storage_access_key: env::var("AZURE_SNAPSHOT_STORAGE_ACCESS_KEY")
667                            .ok(),
668                        object_store_connection_limit: 200,
669                        no_sign_request,
670                        ..Default::default()
671                    },
672                    ObjectStoreType::File => {
673                        if snapshot_path.is_some() {
674                            ObjectStoreConfig {
675                                object_store: Some(ObjectStoreType::File),
676                                directory: snapshot_path,
677                                ..Default::default()
678                            }
679                        } else {
680                            panic!(
681                                "--snapshot-path must be specified for --snapshot-bucket-type=file"
682                            );
683                        }
684                    }
685                };
686
687                let ingestion_url = match network {
688                    Chain::Mainnet => "https://checkpoints.mainnet.sui.io",
689                    Chain::Testnet => "https://checkpoints.testnet.sui.io",
690                    _ => panic!("Cannot generate default ingestion url for unknown network"),
691                };
692
693                let latest_available_epoch =
694                    latest.then_some(get_latest_available_epoch(&snapshot_store_config).await?);
695                let epoch_to_download = epoch.or(latest_available_epoch).expect(
696                    "Either pass epoch with --epoch <epoch_num> or use latest with --latest",
697                );
698
699                if let Err(e) =
700                    check_completed_snapshot(&snapshot_store_config, epoch_to_download).await
701                {
702                    panic!(
703                        "Aborting snapshot restore: {}, snapshot may not be uploaded yet",
704                        e
705                    );
706                }
707
708                let verify = verify.unwrap_or_default();
709                download_formal_snapshot(
710                    &path,
711                    epoch_to_download,
712                    &genesis,
713                    snapshot_store_config,
714                    ingestion_url,
715                    num_parallel_downloads,
716                    num_parallel_chunks,
717                    network,
718                    verify,
719                    max_retries,
720                    metrics_port,
721                )
722                .await?;
723            }
724            ToolCommand::DbShell(args) => {
725                tokio::task::spawn_blocking(move || crate::db_shell::run(args)).await??;
726            }
727            #[cfg(all(feature = "tideconsole", not(windows)))]
728            ToolCommand::TideConsole { db, exec, script } => {
729                tokio::task::spawn_blocking(move || crate::tideconsole_cmd::run(db, exec, script))
730                    .await??;
731            }
732        };
733        Ok(())
734    }
735}