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