mithril_client_cli/commands/cardano_stake_distribution/
download.rs

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