mithril_client_cli/commands/mithril_stake_distribution/
download.rs

1use anyhow::Context;
2use clap::Parser;
3use std::sync::Arc;
4use std::{
5    collections::HashMap,
6    path::{Path, PathBuf},
7};
8
9use crate::utils::{self, IndicatifFeedbackReceiver, ProgressOutputType, ProgressPrinter};
10use crate::{
11    commands::{client_builder, SharedArgs},
12    configuration::{ConfigError, ConfigSource},
13    utils::ExpanderUtils,
14    CommandContext,
15};
16use mithril_client::MessageBuilder;
17use mithril_client::MithrilResult;
18
19/// Download and verify a Mithril stake distribution information. If the
20/// verification fails, the file is not persisted.
21#[derive(Parser, Debug, Clone)]
22pub struct MithrilStakeDistributionDownloadCommand {
23    #[clap(flatten)]
24    shared_args: SharedArgs,
25
26    /// Hash of the Mithril stake distribution artifact, or `latest` for the latest artifact.
27    artifact_hash: String,
28
29    /// Directory where the Mithril stake distribution will be downloaded.
30    ///
31    /// By default, a subdirectory will be created in this directory to extract and verify the
32    /// certificate.
33    #[clap(long)]
34    download_dir: Option<PathBuf>,
35
36    /// Genesis verification key to check the certificate chain.
37    #[clap(long, env = "GENESIS_VERIFICATION_KEY")]
38    genesis_verification_key: Option<String>,
39}
40
41impl MithrilStakeDistributionDownloadCommand {
42    /// Is JSON output enabled
43    pub fn is_json_output_enabled(&self) -> bool {
44        self.shared_args.json
45    }
46
47    /// Main command execution
48    pub async fn execute(&self, context: CommandContext) -> MithrilResult<()> {
49        let params = context.config_parameters()?.add_source(self)?;
50        let download_dir = params.get_or("download_dir", ".");
51        let download_dir = Path::new(&download_dir);
52        let logger = context.logger();
53
54        let progress_output_type = if self.is_json_output_enabled() {
55            ProgressOutputType::JsonReporter
56        } else {
57            ProgressOutputType::Tty
58        };
59        let progress_printer = ProgressPrinter::new(progress_output_type, 4);
60        let client = client_builder(&params)?
61            .add_feedback_receiver(Arc::new(IndicatifFeedbackReceiver::new(
62                progress_output_type,
63                logger.clone(),
64            )))
65            .with_logger(logger.clone())
66            .build()?;
67
68        let get_list_of_artifact_ids = || async {
69            let mithril_stake_distributions = client.mithril_stake_distribution().list().await.with_context(|| {
70                "Can not get the list of artifacts while retrieving the latest stake distribution hash"
71            })?;
72
73            Ok(mithril_stake_distributions
74                .iter()
75                .map(|msd| msd.hash.to_owned())
76                .collect::<Vec<String>>())
77        };
78        progress_printer.report_step(
79            1,
80            &format!(
81                "Fetching Mithril stake distribution '{}' …",
82                self.artifact_hash
83            ),
84        )?;
85        let mithril_stake_distribution = client
86            .mithril_stake_distribution()
87            .get(
88                &ExpanderUtils::expand_eventual_id_alias(
89                    &self.artifact_hash,
90                    get_list_of_artifact_ids(),
91                )
92                .await?,
93            )
94            .await?
95            .with_context(|| {
96                format!(
97                    "Can not download and verify the artifact for hash: '{}'",
98                    self.artifact_hash
99                )
100            })?;
101
102        progress_printer.report_step(
103            2,
104            "Fetching the certificate and verifying the certificate chain…",
105        )?;
106        let certificate = client
107            .certificate()
108            .verify_chain(&mithril_stake_distribution.certificate_hash)
109            .await
110            .with_context(|| {
111                format!(
112                    "Can not verify the certificate chain from certificate_hash: '{}'",
113                    &mithril_stake_distribution.certificate_hash
114                )
115            })?;
116
117        progress_printer.report_step(
118            3,
119            "Verify that the Mithril stake distribution is signed in the associated certificate",
120        )?;
121        let message = MessageBuilder::new()
122            .compute_mithril_stake_distribution_message(&certificate, &mithril_stake_distribution)
123            .with_context(|| {
124                "Can not compute the message for the given Mithril stake distribution"
125            })?;
126
127        if !certificate.match_message(&message) {
128            return Err(anyhow::anyhow!(
129                    "Certificate and message did not match:\ncertificate_message: '{}'\n computed_message: '{}'",
130                    certificate.signed_message,
131                    message.compute_hash()
132                ));
133        }
134
135        progress_printer.report_step(4, "Writing fetched Mithril stake distribution to a file")?;
136        if !download_dir.is_dir() {
137            std::fs::create_dir_all(download_dir)?;
138        }
139        let filepath = PathBuf::new().join(download_dir).join(format!(
140            "mithril_stake_distribution-{}.json",
141            mithril_stake_distribution.hash
142        ));
143        std::fs::write(
144            &filepath,
145            serde_json::to_string(&mithril_stake_distribution).with_context(|| {
146                format!(
147                    "Can not serialize stake distribution artifact '{mithril_stake_distribution:?}'"
148                )
149            })?,
150        )?;
151
152        if self.is_json_output_enabled() {
153            println!(
154                r#"{{"mithril_stake_distribution_hash": "{}", "filepath": "{}"}}"#,
155                mithril_stake_distribution.hash,
156                filepath.display()
157            );
158        } else {
159            println!(
160                "Mithril stake distribution '{}' has been verified and saved as '{}'.",
161                mithril_stake_distribution.hash,
162                filepath.display()
163            );
164        }
165
166        Ok(())
167    }
168}
169
170impl ConfigSource for MithrilStakeDistributionDownloadCommand {
171    fn collect(&self) -> Result<HashMap<String, String>, ConfigError> {
172        let mut map = HashMap::new();
173
174        if let Some(download_dir) = self.download_dir.clone() {
175            let param = "download_dir".to_string();
176            map.insert(
177                param.clone(),
178                utils::path_to_string(&download_dir)
179                    .map_err(|e| ConfigError::Conversion(param, e))?,
180            );
181        }
182
183        if let Some(genesis_verification_key) = self.genesis_verification_key.clone() {
184            map.insert(
185                "genesis_verification_key".to_string(),
186                genesis_verification_key,
187            );
188        }
189
190        Ok(map)
191    }
192}