mithril_client_cli/commands/cardano_stake_distribution/
download.rs

1use anyhow::{Context, anyhow};
2use clap::Parser;
3use std::sync::Arc;
4use std::{
5    collections::HashMap,
6    path::{Path, PathBuf},
7};
8
9use crate::utils::{
10    self, ExpanderUtils, IndicatifFeedbackReceiver, ProgressOutputType, ProgressPrinter,
11};
12use crate::{
13    CommandContext,
14    commands::client_builder,
15    configuration::{ConfigError, ConfigSource},
16};
17use mithril_client::Client;
18use mithril_client::common::Epoch;
19use mithril_client::{CardanoStakeDistribution, MessageBuilder, MithrilResult};
20
21/// Download and verify a Cardano stake distribution information.
22#[derive(Parser, Debug, Clone)]
23pub struct CardanoStakeDistributionDownloadCommand {
24    /// Hash or Epoch of the Cardano stake distribution artifact, or `latest` for the latest artifact.
25    ///
26    /// The epoch represents the epoch at the end of which the Cardano stake distribution is computed by the Cardano node.
27    unique_identifier: String,
28
29    /// Directory where the Cardano stake distribution will be downloaded.
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 CardanoStakeDistributionDownloadCommand {
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        progress_printer.report_step(
61            1,
62            &format!(
63                "Fetching Cardano stake distribution for identifier: '{}' …",
64                self.unique_identifier
65            ),
66        )?;
67        let cardano_stake_distribution =
68            Self::fetch_cardano_stake_distribution_from_unique_identifier(
69                &client,
70                &self.unique_identifier,
71            )
72            .await
73            .with_context(|| {
74                format!(
75                    "Can not fetch Cardano stake distribution from unique identifier: '{}'",
76                    &self.unique_identifier
77                )
78            })?;
79
80        progress_printer.report_step(
81            2,
82            "Fetching the certificate and verifying the certificate chain…",
83        )?;
84        let certificate = client
85            .certificate()
86            .verify_chain(&cardano_stake_distribution.certificate_hash)
87            .await
88            .with_context(|| {
89                format!(
90                    "Can not verify the certificate chain from certificate_hash: '{}'",
91                    &cardano_stake_distribution.certificate_hash
92                )
93            })?;
94
95        progress_printer.report_step(
96            3,
97            "Verify that the Cardano stake distribution is signed in the associated certificate",
98        )?;
99        let message = MessageBuilder::new()
100            .compute_cardano_stake_distribution_message(&certificate, &cardano_stake_distribution)
101            .with_context(
102                || "Can not compute the message for the given Cardano stake distribution",
103            )?;
104
105        if !certificate.match_message(&message) {
106            return Err(anyhow!(
107                "Certificate and message did not match:\ncertificate_message: '{}'\n computed_message: '{}'",
108                certificate.signed_message,
109                message.compute_hash()
110            ));
111        }
112
113        progress_printer.report_step(4, "Writing fetched Cardano stake distribution to a file")?;
114        if !download_dir.is_dir() {
115            std::fs::create_dir_all(download_dir)?;
116        }
117        let filepath = PathBuf::new().join(download_dir).join(format!(
118            "cardano_stake_distribution-{}.json",
119            cardano_stake_distribution.epoch
120        ));
121        std::fs::write(
122            &filepath,
123            serde_json::to_string(&cardano_stake_distribution).with_context(|| {
124                format!(
125                    "Can not serialize Cardano stake distribution artifact '{cardano_stake_distribution:?}'"
126                )
127            })?,
128        )?;
129
130        if context.is_json_output_enabled() {
131            println!(
132                r#"{{"cardano_stake_distribution_epoch": "{}", "filepath": "{}"}}"#,
133                cardano_stake_distribution.epoch,
134                filepath.display()
135            );
136        } else {
137            println!(
138                "Cardano stake distribution for epoch '{}' has been verified and saved as '{}'.",
139                cardano_stake_distribution.epoch,
140                filepath.display()
141            );
142        }
143
144        Ok(())
145    }
146
147    fn is_sha256_hash(identifier: &str) -> bool {
148        identifier.len() == 64 && identifier.chars().all(|c| c.is_ascii_hexdigit())
149    }
150
151    // The unique identifier can be either a SHA256 hash, an epoch,  or 'latest'.
152    async fn fetch_cardano_stake_distribution_from_unique_identifier(
153        client: &Client,
154        unique_identifier: &str,
155    ) -> MithrilResult<CardanoStakeDistribution> {
156        if Self::is_sha256_hash(unique_identifier) {
157            client
158                .cardano_stake_distribution()
159                .get(unique_identifier)
160                .await
161                .with_context(|| {
162                    format!(
163                        "Can not download and verify the artifact for hash: '{unique_identifier}'"
164                    )
165                })?
166                .ok_or(anyhow!(
167                    "No Cardano stake distribution could be found for hash: '{}'",
168                    unique_identifier
169                ))
170        } else {
171            let epoch = {
172                let get_list_of_artifact_epochs = || async {
173                    let cardano_stake_distributions = client.cardano_stake_distribution().list().await.with_context(|| {
174                        "Can not get the list of artifacts while retrieving the latest Cardano stake distribution epoch"
175                    })?;
176
177                    Ok(cardano_stake_distributions
178                        .iter()
179                        .map(|csd| csd.epoch.to_string())
180                        .collect::<Vec<String>>())
181                };
182
183                let epoch = ExpanderUtils::expand_eventual_id_alias(
184                    unique_identifier,
185                    get_list_of_artifact_epochs(),
186                )
187                .await?;
188
189                Epoch(
190                    epoch.parse().with_context(|| {
191                        format!("Can not convert: '{epoch}' into a valid Epoch")
192                    })?,
193                )
194            };
195
196            client
197                .cardano_stake_distribution()
198                .get_by_epoch(epoch)
199                .await
200                .with_context(|| {
201                    format!("Can not download and verify the artifact for epoch: '{epoch}'")
202                })?
203                .ok_or(anyhow!(
204                    "No Cardano stake distribution could be found for epoch: '{}'",
205                    epoch
206                ))
207        }
208    }
209}
210
211impl ConfigSource for CardanoStakeDistributionDownloadCommand {
212    fn collect(&self) -> Result<HashMap<String, String>, ConfigError> {
213        let mut map = HashMap::new();
214
215        if let Some(download_dir) = self.download_dir.clone() {
216            let param = "download_dir".to_string();
217            map.insert(
218                param.clone(),
219                utils::path_to_string(&download_dir)
220                    .map_err(|e| ConfigError::Conversion(param, e))?,
221            );
222        }
223
224        if let Some(genesis_verification_key) = self.genesis_verification_key.clone() {
225            map.insert(
226                "genesis_verification_key".to_string(),
227                genesis_verification_key,
228            );
229        }
230
231        Ok(map)
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn is_sha_256_returns_false_with_len_different_than_64_and_hex_digit() {
241        let len_65_hex_digit = "65aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
242        assert!(!CardanoStakeDistributionDownloadCommand::is_sha256_hash(
243            len_65_hex_digit
244        ));
245
246        let len_63_hex_digit = "63aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
247        assert!(!CardanoStakeDistributionDownloadCommand::is_sha256_hash(
248            len_63_hex_digit
249        ));
250    }
251
252    #[test]
253    fn is_sha_256_returns_false_with_len_equal_to_64_and_not_hex_digit() {
254        let len_64_not_hex_digit =
255            "64zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz";
256        assert!(!CardanoStakeDistributionDownloadCommand::is_sha256_hash(
257            len_64_not_hex_digit
258        ));
259    }
260
261    #[test]
262    fn is_sha_256_returns_true_with_len_equal_to_64_and_hex_digit() {
263        let len_64_hex_digit = "64aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
264        assert!(CardanoStakeDistributionDownloadCommand::is_sha256_hash(
265            len_64_hex_digit
266        ));
267    }
268}