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