mithril_client_cli/commands/mithril_stake_distribution/
download.rs

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