Skip to main content

iota_tool/
commands.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{collections::BTreeMap, env, num::NonZeroUsize, path::PathBuf, sync::Arc};
6
7use anyhow::Result;
8use clap::*;
9use futures::{StreamExt, future::join_all};
10use iota_config::{
11    genesis::Genesis,
12    object_storage_config::{ObjectStoreConfig, ObjectStoreType},
13};
14use iota_core::{
15    authority_aggregator::AuthorityAggregatorBuilder,
16    authority_client::{validator::ValidatorAPI, validator_peer::ValidatorPeerAPI},
17};
18use iota_replay::{ReplayToolCommand, execute_replay_command};
19use iota_sdk::{IotaClient, IotaClientBuilder, rpc_types::IotaTransactionBlockResponseOptions};
20use iota_sdk_types::{Address, ObjectId, SenderSignedTransaction, TransactionDigest};
21use iota_snapshot::progress::LOG_TARGET_PROGRESS;
22use iota_types::{
23    base_types::*,
24    crypto::AuthorityPublicKeyBytes,
25    messages_checkpoint::{CheckpointRequest, CheckpointResponse, CheckpointSequenceNumber},
26    messages_grpc::TransactionInfoRequest,
27    transaction::TransactionEnvelope,
28};
29use telemetry_subscribers::TracingHandle;
30
31use crate::{
32    ConciseObjectOutput, GroupedObjectOutput, SnapshotVerifyMode, VerboseObjectOutput,
33    backfill_checkpoint_summaries, check_completed_snapshot,
34    db_tool::{DbToolCommand, execute_db_tool_command, print_db_all_tables},
35    download_formal_snapshot, get_latest_available_epoch, get_object, get_transaction_block,
36    make_clients,
37};
38
39/// Log filter for the restore commands' non-verbose default: silence
40/// everything except the progress status lines, which are the only progress
41/// output left once the progress bars can't be drawn.
42fn progress_only_log_directives() -> String {
43    format!("off,{LOG_TARGET_PROGRESS}=info")
44}
45
46#[derive(Parser, Clone, ValueEnum)]
47pub enum Verbosity {
48    Grouped,
49    Concise,
50    Verbose,
51}
52
53/// Networks that publish snapshots downloadable with this tool.
54#[derive(Debug, Copy, Clone, ValueEnum)]
55pub enum Network {
56    Mainnet,
57    Testnet,
58    Devnet,
59}
60
61#[derive(Parser)]
62pub enum ToolCommand {
63    /// Inspect if a specific object is or all gas objects owned by an address
64    /// are locked by validators
65    LockedObject {
66        /// Either id or address must be provided
67        /// The object to check
68        #[arg(long, help = "The object ID to fetch")]
69        id: Option<ObjectId>,
70        /// Either id or address must be provided
71        /// If provided, check all gas objects owned by this account
72        #[arg(long)]
73        address: Option<Address>,
74        /// RPC address to provide the up-to-date committee info
75        #[arg(long)]
76        fullnode_rpc_url: String,
77        /// Should attempt to rescue the object if it's locked but not fully
78        /// locked
79        #[arg(long)]
80        rescue: bool,
81    },
82
83    /// Fetch the same object from all validators
84    FetchObject {
85        #[arg(long, help = "The object ID to fetch")]
86        id: ObjectId,
87
88        #[arg(long, help = "Fetch object at a specific sequence")]
89        version: Option<u64>,
90
91        #[arg(
92            long,
93            help = "Validator to fetch from - if not specified, all validators are queried"
94        )]
95        validator: Option<AuthorityName>,
96
97        // RPC address to provide the up-to-date committee info
98        #[arg(long)]
99        fullnode_rpc_url: String,
100
101        /// Concise mode groups responses by results.
102        /// prints tabular output suitable for processing with unix tools. For
103        /// instance, to quickly check that all validators agree on the history
104        /// of an object: ```text
105        /// $ iota-tool fetch-object --id
106        /// 0x260efde76ebccf57f4c5e951157f5c361cde822c \      --genesis
107        /// $HOME/.iota/iota_config/genesis.blob \      --verbosity
108        /// concise --concise-no-header ```
109        #[arg(value_enum, long, default_value = "grouped", ignore_case = true)]
110        verbosity: Verbosity,
111
112        #[arg(long, help = "don't show header in concise output")]
113        concise_no_header: bool,
114    },
115
116    /// Fetch the effects association with transaction `digest`
117    FetchTransaction {
118        // RPC address to provide the up-to-date committee info
119        #[arg(long)]
120        fullnode_rpc_url: String,
121
122        #[arg(long, help = "The transaction ID to fetch")]
123        digest: TransactionDigest,
124
125        /// If true, show the input transaction as well as the effects
126        #[arg(long = "show-tx")]
127        show_input_tx: bool,
128    },
129
130    /// Tool to read validator & node db.
131    DbTool {
132        /// Path of the DB to read
133        #[arg(long)]
134        db_path: String,
135        #[command(subcommand)]
136        cmd: Option<DbToolCommand>,
137    },
138
139    /// Download all packages to the local filesystem from a GraphQL service.
140    /// Each package gets its own sub-directory, named for its ID on chain
141    /// and version containing two metadata files (linkage.json and
142    /// origins.json), a file containing the overall object and a file for every
143    /// module it contains. Each module file is named for its module name, with
144    /// a .mv suffix, and contains Move bytecode (suitable for passing into
145    /// a disassembler).
146    DumpPackages {
147        /// Connection information for a GraphQL service.
148        #[arg(long, short)]
149        rpc_url: String,
150
151        /// Path to a non-existent directory that can be created and filled with
152        /// package information.
153        #[arg(long, short)]
154        output_dir: PathBuf,
155
156        /// Only fetch packages that were created before this checkpoint (given
157        /// by its sequence number).
158        #[arg(long)]
159        before_checkpoint: Option<u64>,
160
161        /// If false (default), log level will be overridden to "off", and
162        /// output will be reduced to necessary status information.
163        #[arg(short, long)]
164        verbose: bool,
165    },
166
167    DumpValidators {
168        #[arg(long)]
169        genesis: PathBuf,
170
171        #[arg(
172            long,
173            help = "show concise output - name, authority key and network address"
174        )]
175        concise: bool,
176    },
177
178    DumpGenesis {
179        #[arg(long)]
180        genesis: PathBuf,
181    },
182
183    /// Fetch authenticated checkpoint information at a specific sequence
184    /// number. If sequence number is not specified, get the latest
185    /// authenticated checkpoint.
186    FetchCheckpoint {
187        // RPC address to provide the up-to-date committee info
188        #[arg(long)]
189        fullnode_rpc_url: String,
190
191        #[arg(long, help = "Fetch checkpoint at a specific sequence number")]
192        sequence_number: Option<CheckpointSequenceNumber>,
193    },
194
195    Anemo {
196        #[command(next_help_heading = "foo", flatten)]
197        args: anemo_cli::Args,
198    },
199
200    // Restore from formal (slim, DB agnostic) snapshot.
201    #[command(
202        about = "Downloads formal database snapshot via cloud object store, outputs to local disk"
203    )]
204    DownloadFormalSnapshot {
205        /// Epoch to restore to the end of. Mutually exclusive with `--latest`.
206        #[arg(long, conflicts_with = "latest")]
207        epoch: Option<u64>,
208        /// Path to the network's `genesis.blob`.
209        #[arg(long)]
210        genesis: PathBuf,
211        /// Directory to restore into. The restored database is written to a
212        /// `live` subdirectory of this path.
213        #[arg(long)]
214        path: PathBuf,
215        /// Number of parallel downloads to perform. Defaults to logical cores -
216        /// 1, capped at 8.
217        #[arg(long)]
218        num_parallel_downloads: Option<NonZeroUsize>,
219        /// Verification mode to employ.
220        #[arg(long, default_value = "normal")]
221        verify: Option<SnapshotVerifyMode>,
222        /// Network to download snapshot for. Defaults to "mainnet".
223        /// If `--snapshot-bucket` is not specified, the value of this flag is
224        /// used to construct the default bucket name.
225        #[arg(long, default_value = "mainnet")]
226        network: Network,
227        /// Snapshot bucket name. If not specified, defaults are
228        /// based on value of `--network` flag.
229        #[arg(long, conflicts_with = "no_sign_request")]
230        snapshot_bucket: Option<String>,
231        /// Snapshot bucket type
232        #[arg(
233            long,
234            conflicts_with = "no_sign_request",
235            help = "Required if --no-sign-request is not set"
236        )]
237        snapshot_bucket_type: Option<ObjectStoreType>,
238        /// Path to snapshot directory on local filesystem.
239        /// Only applicable if `--snapshot-bucket-type` is "file".
240        #[arg(long)]
241        snapshot_path: Option<PathBuf>,
242        /// If true, no authentication is needed for snapshot restores
243        #[arg(
244            long,
245            conflicts_with_all = &["snapshot_bucket", "snapshot_bucket_type"],
246            help = "if set, no authentication is needed for snapshot restore"
247        )]
248        no_sign_request: bool,
249        /// Download snapshot of the latest available epoch.
250        /// If `--epoch` is specified, then this flag gets ignored.
251        #[arg(
252            long,
253            conflicts_with = "epoch",
254            help = "defaults to latest available snapshot in chosen bucket"
255        )]
256        latest: bool,
257        /// If false (default), log level will be overridden to "off",
258        /// and output will be reduced to necessary status information.
259        #[arg(long)]
260        verbose: bool,
261
262        /// Report progress as a status line logged once per second.
263        #[arg(long)]
264        disable_progress_bar: bool,
265
266        /// Skip building the gRPC index store during the restore. By default
267        /// it is built from the same object stream that restores the state,
268        /// so a fullnode started with gRPC enabled opens it in place instead
269        /// of re-indexing the whole restored state on first start.
270        #[arg(long)]
271        skip_grpc_indexes: bool,
272    },
273
274    /// Backfill the full checkpoint summary history from the checkpoint
275    /// archive into a stopped node's checkpoint store.
276    ///
277    /// A node restored from a formal snapshot holds only the end-of-epoch
278    /// summaries. This downloads every intermediate summary up to the node's
279    /// highest synced checkpoint, so the node holds the complete header chain
280    /// from genesis (to serve historical checkpoint queries, or to be a full
281    /// summary source for syncing peers). Only historical summaries are added;
282    /// no watermark is moved.
283    ///
284    /// Summaries are downloaded from the checkpoint archive at
285    /// `--ingestion-url` and inserted without chain verification, so the
286    /// checkpoint archive is trusted to serve this node's own chain.
287    BackfillCheckpointSummaries {
288        /// Path to the node's live database directory (the one containing
289        /// `checkpoints/`, `store/`, and `epochs/`). The node must be stopped.
290        #[arg(long)]
291        path: PathBuf,
292        /// URL of the checkpoint archive to download summaries from (the same
293        /// store a node's state sync reads from, e.g. an S3/GCS bucket or HTTP
294        /// endpoint).
295        #[arg(long)]
296        ingestion_url: String,
297        /// Number of parallel downloads to perform. Defaults to logical cores -
298        /// 1, capped at 8.
299        #[arg(long)]
300        num_parallel_downloads: Option<NonZeroUsize>,
301        /// If false (default), log level will be overridden to "off", and
302        /// output will be reduced to necessary status information.
303        #[arg(long)]
304        verbose: bool,
305        /// Report progress as a status line logged once per second.
306        #[arg(long)]
307        disable_progress_bar: bool,
308    },
309
310    Replay {
311        #[arg(long = "rpc")]
312        rpc_url: Option<String>,
313        #[arg(long)]
314        safety_checks: bool,
315        #[arg(long = "authority")]
316        use_authority: bool,
317        #[arg(
318            long,
319            short,
320            help = "Path to the network config file. This should be specified when rpc_url is not present. \
321            If not specified we will use the default network config file at ~/.iota-replay/network-config.yaml"
322        )]
323        cfg_path: Option<PathBuf>,
324        #[arg(
325            long,
326            help = "The name of the chain to replay from, could be one of: mainnet, testnet, devnet.\
327            When rpc_url is not specified, this is used to load the corresponding config from the network config file.\
328            If not specified, mainnet will be used by default"
329        )]
330        chain: Option<String>,
331        #[command(subcommand)]
332        cmd: ReplayToolCommand,
333    },
334
335    /// Ask all validators to sign a transaction through AuthorityAggregator.
336    SignTransaction {
337        #[arg(long)]
338        genesis: PathBuf,
339
340        #[arg(
341            long,
342            help = "The Base64-encoding of the bcs bytes of SenderSignedTransaction"
343        )]
344        sender_signed_data: String,
345    },
346
347    /// Create an IOTA Genesis Ceremony with multiple remote validators.
348    GenesisCeremony(crate::genesis_ceremony::Ceremony),
349    /// Tool for Fire Drill
350    FireDrill {
351        #[command(subcommand)]
352        fire_drill: crate::fire_drill::FireDrill,
353    },
354
355    /// Check the health of a running gRPC server.
356    /// Exits with code 0 if healthy, non-zero otherwise.
357    #[command(name = "grpc-health-check")]
358    GrpcHealthCheck {
359        /// The gRPC server address (e.g., "http://localhost:50051")
360        #[arg(long, default_value = "http://localhost:50051")]
361        address: String,
362    },
363}
364
365async fn check_locked_object(
366    iota_client: &Arc<IotaClient>,
367    committee: Arc<BTreeMap<AuthorityPublicKeyBytes, u64>>,
368    id: ObjectId,
369    rescue: bool,
370) -> anyhow::Result<()> {
371    let clients = Arc::new(make_clients(iota_client).await?);
372    let output = get_object(id, None, None, clients.clone()).await?;
373    let output = GroupedObjectOutput::new(output, committee);
374    if output.fully_locked {
375        println!("Object {id} is fully locked.");
376        return Ok(());
377    }
378    let top_record = output.voting_power.first().unwrap();
379    let top_record_stake = top_record.1;
380    let top_record = top_record.0.unwrap();
381    if top_record.4.is_none() {
382        println!(
383            "Object {id} does not seem to be locked by majority of validators (unlocked stake: {top_record_stake})"
384        );
385        return Ok(());
386    }
387
388    let tx_digest = top_record.2;
389    if !rescue {
390        println!("Object {id} is rescueable, top tx: {tx_digest}");
391        return Ok(());
392    }
393    println!("Object {id} is rescueable, trying tx {tx_digest}");
394    let validator = output
395        .grouped_results
396        .get(&Some(top_record))
397        .unwrap()
398        .first()
399        .unwrap();
400    let client = &clients.get(validator).unwrap().1;
401    let tx = client
402        .handle_transaction_info_request(TransactionInfoRequest {
403            transaction_digest: tx_digest,
404        })
405        .await?
406        .transaction;
407    let res = iota_client
408        .quorum_driver_api()
409        .execute_transaction_block(
410            TransactionEnvelope::new(tx),
411            IotaTransactionBlockResponseOptions::full_content(),
412            None,
413        )
414        .await;
415    match res {
416        Ok(_) => {
417            println!("Transaction executed successfully ({tx_digest})");
418        }
419        Err(e) => {
420            println!("Failed to execute transaction ({tx_digest}): {e:?}");
421        }
422    }
423    Ok(())
424}
425
426impl ToolCommand {
427    pub async fn execute(self, tracing_handle: TracingHandle) -> Result<(), anyhow::Error> {
428        match self {
429            ToolCommand::LockedObject {
430                id,
431                fullnode_rpc_url,
432                rescue,
433                address,
434            } => {
435                let iota_client =
436                    Arc::new(IotaClientBuilder::default().build(fullnode_rpc_url).await?);
437                let committee = Arc::new(
438                    iota_client
439                        .governance_api()
440                        .get_committee_info(None)
441                        .await?
442                        .validators
443                        .into_iter()
444                        .collect::<BTreeMap<_, _>>(),
445                );
446                let object_ids = match id {
447                    Some(id) => vec![id],
448                    None => {
449                        let address = address.expect("Either id or address must be provided");
450                        iota_client
451                            .coin_read_api()
452                            .get_coins_stream(address, None)
453                            .map(|c| c.coin_object_id)
454                            .collect()
455                            .await
456                    }
457                };
458                for ids in object_ids.chunks(30) {
459                    let mut tasks = vec![];
460                    for id in ids {
461                        tasks.push(check_locked_object(
462                            &iota_client,
463                            committee.clone(),
464                            *id,
465                            rescue,
466                        ))
467                    }
468                    join_all(tasks)
469                        .await
470                        .into_iter()
471                        .collect::<Result<Vec<_>, _>>()?;
472                }
473            }
474            ToolCommand::FetchObject {
475                id,
476                validator,
477                version,
478                fullnode_rpc_url,
479                verbosity,
480                concise_no_header,
481            } => {
482                let iota_client =
483                    Arc::new(IotaClientBuilder::default().build(fullnode_rpc_url).await?);
484                let clients = Arc::new(make_clients(&iota_client).await?);
485                let output = get_object(id, version, validator, clients).await?;
486
487                match verbosity {
488                    Verbosity::Grouped => {
489                        let committee = Arc::new(
490                            iota_client
491                                .governance_api()
492                                .get_committee_info(None)
493                                .await?
494                                .validators
495                                .into_iter()
496                                .collect::<BTreeMap<_, _>>(),
497                        );
498                        println!("{}", GroupedObjectOutput::new(output, committee));
499                    }
500                    Verbosity::Verbose => {
501                        println!("{}", VerboseObjectOutput(output));
502                    }
503                    Verbosity::Concise => {
504                        if !concise_no_header {
505                            println!("{}", ConciseObjectOutput::header());
506                        }
507                        println!("{}", ConciseObjectOutput(output));
508                    }
509                }
510            }
511            ToolCommand::FetchTransaction {
512                digest,
513                show_input_tx,
514                fullnode_rpc_url,
515            } => {
516                print!(
517                    "{}",
518                    get_transaction_block(digest, show_input_tx, fullnode_rpc_url).await?
519                );
520            }
521            ToolCommand::DbTool { db_path, cmd } => {
522                let path = PathBuf::from(db_path);
523                match cmd {
524                    Some(c) => execute_db_tool_command(path, c).await?,
525                    None => print_db_all_tables(path)?,
526                }
527            }
528            ToolCommand::DumpPackages {
529                rpc_url,
530                output_dir,
531                before_checkpoint,
532                verbose,
533            } => {
534                if !verbose {
535                    tracing_handle
536                        .update_log("off")
537                        .expect("Failed to update log level");
538                }
539
540                iota_package_dump::dump(rpc_url, output_dir, before_checkpoint).await?;
541            }
542            ToolCommand::DumpValidators { genesis, concise } => {
543                let genesis = Genesis::load(genesis).unwrap();
544                if !concise {
545                    println!("{:#?}", genesis.validator_set_for_tooling());
546                } else {
547                    for (i, val_info) in genesis.validator_set_for_tooling().iter().enumerate() {
548                        let metadata = val_info.verified_metadata();
549                        println!(
550                            "#{:<2} {:<20} {:?} {:?} {}",
551                            i,
552                            metadata.name,
553                            metadata.iota_pubkey_bytes().concise(),
554                            metadata.net_address,
555                            anemo::PeerId(metadata.network_pubkey.0.to_bytes()),
556                        )
557                    }
558                }
559            }
560            ToolCommand::DumpGenesis { genesis } => {
561                let genesis = Genesis::load(genesis)?;
562                println!("{genesis:#?}");
563            }
564            ToolCommand::FetchCheckpoint {
565                sequence_number,
566                fullnode_rpc_url,
567            } => {
568                let iota_client =
569                    Arc::new(IotaClientBuilder::default().build(fullnode_rpc_url).await?);
570                let clients = make_clients(&iota_client).await?;
571
572                for (name, (_, client)) in clients {
573                    let resp = client
574                        .get_checkpoint_v2(CheckpointRequest {
575                            sequence_number,
576                            request_content: true,
577                            certified: true,
578                        })
579                        .await
580                        .unwrap();
581                    let CheckpointResponse {
582                        checkpoint,
583                        contents,
584                    } = resp;
585                    println!("Validator: {:?}\n", name.concise());
586                    println!("Checkpoint: {checkpoint:?}\n");
587                    println!("Content: {contents:?}\n");
588                }
589            }
590            ToolCommand::Anemo { args } => {
591                let config = crate::make_anemo_config();
592                anemo_cli::run(config, args).await
593            }
594            ToolCommand::DownloadFormalSnapshot {
595                epoch,
596                genesis,
597                path,
598                num_parallel_downloads,
599                verify,
600                network,
601                snapshot_bucket,
602                snapshot_bucket_type,
603                snapshot_path,
604                no_sign_request,
605                latest,
606                verbose,
607                disable_progress_bar,
608                skip_grpc_indexes,
609            } => {
610                if !verbose {
611                    tracing_handle
612                        .update_log(progress_only_log_directives())
613                        .expect("Failed to update log level");
614                }
615                let num_parallel_downloads = num_parallel_downloads
616                    .unwrap_or_else(iota_snapshot::default_download_concurrency);
617                let snapshot_bucket =
618                    snapshot_bucket.or_else(|| match (network, no_sign_request) {
619                        (Network::Mainnet, false) => Some(
620                            env::var("MAINNET_FORMAL_SIGNED_BUCKET")
621                                .unwrap_or("iota-mainnet-formal".to_string()),
622                        ),
623                        (Network::Mainnet, true) => env::var("MAINNET_FORMAL_UNSIGNED_BUCKET").ok(),
624                        (Network::Testnet, false) => Some(
625                            env::var("TESTNET_FORMAL_SIGNED_BUCKET")
626                                .unwrap_or("iota-testnet-formal".to_string()),
627                        ),
628                        (Network::Testnet, true) => env::var("TESTNET_FORMAL_UNSIGNED_BUCKET").ok(),
629                        (Network::Devnet, false) => Some(
630                            env::var("DEVNET_FORMAL_SIGNED_BUCKET")
631                                .unwrap_or("iota-devnet-formal".to_string()),
632                        ),
633                        (Network::Devnet, true) => env::var("DEVNET_FORMAL_UNSIGNED_BUCKET").ok(),
634                    });
635
636                let aws_endpoint = env::var("AWS_SNAPSHOT_ENDPOINT").ok().or_else(|| {
637                    no_sign_request.then(|| {
638                        match network {
639                            Network::Mainnet => "https://formal-snapshot.mainnet.iota.cafe",
640                            Network::Testnet => "https://formal-snapshot.testnet.iota.cafe",
641                            Network::Devnet => "https://formal-snapshot.devnet.iota.cafe",
642                        }
643                        .to_string()
644                    })
645                });
646
647                let snapshot_bucket_type = if no_sign_request {
648                    ObjectStoreType::S3
649                } else {
650                    snapshot_bucket_type
651                        .expect("You must set either --snapshot-bucket-type or --no-sign-request")
652                };
653                let snapshot_store_config = match snapshot_bucket_type {
654                    ObjectStoreType::S3 => ObjectStoreConfig {
655                        object_store: Some(ObjectStoreType::S3),
656                        bucket: snapshot_bucket.filter(|s| !s.is_empty()),
657                        aws_access_key_id: env::var("AWS_SNAPSHOT_ACCESS_KEY_ID").ok(),
658                        aws_secret_access_key: env::var("AWS_SNAPSHOT_SECRET_ACCESS_KEY").ok(),
659                        aws_region: env::var("AWS_SNAPSHOT_REGION").ok(),
660                        aws_endpoint: aws_endpoint.filter(|s| !s.is_empty()),
661                        aws_virtual_hosted_style_request: env::var(
662                            "AWS_SNAPSHOT_VIRTUAL_HOSTED_REQUESTS",
663                        )
664                        .ok()
665                        .and_then(|b| b.parse().ok())
666                        .unwrap_or(no_sign_request),
667                        object_store_connection_limit: 200,
668                        no_sign_request,
669                        ..Default::default()
670                    },
671                    ObjectStoreType::GCS => ObjectStoreConfig {
672                        object_store: Some(ObjectStoreType::GCS),
673                        bucket: snapshot_bucket,
674                        google_service_account: env::var("GCS_SNAPSHOT_SERVICE_ACCOUNT_FILE_PATH")
675                            .ok(),
676                        object_store_connection_limit: 200,
677                        no_sign_request,
678                        ..Default::default()
679                    },
680                    ObjectStoreType::Azure => ObjectStoreConfig {
681                        object_store: Some(ObjectStoreType::Azure),
682                        bucket: snapshot_bucket,
683                        azure_storage_account: env::var("AZURE_SNAPSHOT_STORAGE_ACCOUNT").ok(),
684                        azure_storage_access_key: env::var("AZURE_SNAPSHOT_STORAGE_ACCESS_KEY")
685                            .ok(),
686                        object_store_connection_limit: 200,
687                        no_sign_request,
688                        ..Default::default()
689                    },
690                    ObjectStoreType::File => {
691                        if snapshot_path.is_some() {
692                            ObjectStoreConfig {
693                                object_store: Some(ObjectStoreType::File),
694                                directory: snapshot_path,
695                                ..Default::default()
696                            }
697                        } else {
698                            panic!(
699                                "--snapshot-path must be specified for --snapshot-bucket-type=file"
700                            );
701                        }
702                    }
703                };
704
705                let latest_available_epoch =
706                    latest.then_some(get_latest_available_epoch(&snapshot_store_config).await?);
707                let epoch_to_download = epoch.or(latest_available_epoch).expect(
708                    "Either pass epoch with --epoch <epoch_num> or use latest with --latest",
709                );
710
711                if let Err(e) =
712                    check_completed_snapshot(&snapshot_store_config, epoch_to_download).await
713                {
714                    panic!("Aborting snapshot restore: {e}, snapshot may not be uploaded yet");
715                }
716
717                let verify = verify.unwrap_or_default();
718                download_formal_snapshot(
719                    &path,
720                    epoch_to_download,
721                    &genesis,
722                    snapshot_store_config,
723                    num_parallel_downloads,
724                    verify,
725                    skip_grpc_indexes,
726                    disable_progress_bar,
727                )
728                .await?;
729            }
730            ToolCommand::BackfillCheckpointSummaries {
731                path,
732                ingestion_url,
733                num_parallel_downloads,
734                verbose,
735                disable_progress_bar,
736            } => {
737                if !verbose {
738                    tracing_handle
739                        .update_log(progress_only_log_directives())
740                        .expect("Failed to update log level");
741                }
742                let num_parallel_downloads = num_parallel_downloads
743                    .unwrap_or_else(iota_snapshot::default_download_concurrency);
744                backfill_checkpoint_summaries(
745                    &path,
746                    ingestion_url,
747                    num_parallel_downloads,
748                    disable_progress_bar,
749                )
750                .await?;
751            }
752            ToolCommand::Replay {
753                rpc_url,
754                safety_checks,
755                cmd,
756                use_authority,
757                cfg_path,
758                chain,
759            } => {
760                execute_replay_command(rpc_url, safety_checks, use_authority, cfg_path, chain, cmd)
761                    .await?;
762            }
763            ToolCommand::SignTransaction {
764                genesis,
765                sender_signed_data,
766            } => {
767                let genesis = Genesis::load(genesis)?;
768                let sender_signed_tx =
769                    SenderSignedTransaction::from_base64(sender_signed_data.as_str()).unwrap();
770                let transaction = TransactionEnvelope::new(sender_signed_tx);
771                let (agg, _) =
772                    AuthorityAggregatorBuilder::from_genesis(&genesis).build_network_clients();
773                let result = agg.process_transaction(transaction, None).await;
774                println!("{result:?}");
775            }
776            ToolCommand::GenesisCeremony(cmd) => {
777                crate::genesis_ceremony::run(cmd).await?;
778            }
779            ToolCommand::FireDrill { fire_drill } => {
780                crate::fire_drill::run_fire_drill(fire_drill).await?;
781            }
782            ToolCommand::GrpcHealthCheck { address } => {
783                let client = iota_grpc_client::GrpcClient::new(address)?;
784                client.health(None).await?;
785                println!("OK");
786            }
787        };
788        Ok(())
789    }
790}