mithril_client_cli/commands/cardano_db/download/
mod.rs

1mod v1;
2mod v2;
3
4use v1::PreparedCardanoDbV1Download;
5use v2::PreparedCardanoDbV2Download;
6
7use clap::Parser;
8use std::{collections::HashMap, path::PathBuf};
9
10use crate::{
11    CommandContext,
12    commands::cardano_db::CardanoDbCommandsBackend,
13    configuration::{ConfigError, ConfigSource},
14    utils::{self, JSON_CAUTION_KEY},
15};
16use mithril_client::{MithrilResult, common::ImmutableFileNumber};
17
18const DB_DIRECTORY_NAME: &str = "db";
19
20/// Clap command to download a Cardano db and verify its associated certificate.
21#[derive(Parser, Debug, Clone)]
22pub struct CardanoDbDownloadCommand {
23    ///Backend to use, either: `v1` (default, full database restoration only) or `v2` (full or partial database restoration)
24    #[arg(short, long, value_enum, default_value_t)]
25    backend: CardanoDbCommandsBackend,
26
27    /// Digest of the Cardano db snapshot to download  or `latest` for the latest artifact
28    ///
29    /// Use the `list` command to get that information.
30    digest: String,
31
32    /// Directory where the immutable and ancillary files will be downloaded.
33    ///
34    /// By default, a subdirectory will be created in this directory to extract and verify the
35    /// certificate.
36    #[clap(long)]
37    download_dir: Option<PathBuf>,
38
39    /// Genesis verification key to check the certificate chain.
40    #[clap(long, env = "GENESIS_VERIFICATION_KEY")]
41    genesis_verification_key: Option<String>,
42
43    /// Include ancillary files in the download, if set the `ancillary_verification_key` is required
44    /// in order to verify the ancillary files.
45    ///
46    /// By default, only finalized immutable files are downloaded.
47    /// The last ledger state snapshot and the last immutable file (the ancillary files) can be
48    /// downloaded with this option.
49    #[clap(long)]
50    include_ancillary: bool,
51
52    /// Ancillary verification key to verify the ancillary files.
53    #[clap(long, env = "ANCILLARY_VERIFICATION_KEY")]
54    ancillary_verification_key: Option<String>,
55
56    /// [backend `v2` only] The first immutable file number to download.
57    ///
58    /// If not set, the download process will start from the first immutable file.
59    #[clap(long)]
60    start: Option<ImmutableFileNumber>,
61
62    /// [backend `v2` only] The last immutable file number to download.
63    ///
64    /// If not set, the download will continue until the last certified immutable file.
65    #[clap(long)]
66    end: Option<ImmutableFileNumber>,
67
68    /// [backend `v2` only] Allow existing files in the download directory to be overridden.
69    #[clap(long)]
70    allow_override: bool,
71}
72
73impl CardanoDbDownloadCommand {
74    /// Command execution
75    pub async fn execute(&self, mut context: CommandContext) -> MithrilResult<()> {
76        context.config_parameters_mut().add_source(self)?;
77
78        match self.backend {
79            CardanoDbCommandsBackend::V1 => {
80                let prepared_command = self.prepare_v1(&context)?;
81                prepared_command.execute(&context).await
82            }
83            CardanoDbCommandsBackend::V2 => {
84                let prepared_command = self.prepare_v2(&context)?;
85                prepared_command.execute(&context).await
86            }
87        }
88    }
89
90    fn prepare_v1(&self, context: &CommandContext) -> MithrilResult<PreparedCardanoDbV1Download> {
91        if self.allow_override || self.start.is_some() || self.end.is_some() {
92            self.warn_unused_parameter_with_v1_backend(context);
93        }
94
95        let ancillary_verification_key = if self.include_ancillary {
96            self.warn_ancillary_not_signed_by_mithril(context);
97            Some(context.config_parameters().require("ancillary_verification_key")?)
98        } else {
99            self.warn_fast_bootstrap_not_available(context);
100            None
101        };
102
103        Ok(PreparedCardanoDbV1Download {
104            digest: self.digest.clone(),
105            download_dir: context.config_parameters().require("download_dir")?,
106            include_ancillary: self.include_ancillary,
107            ancillary_verification_key,
108        })
109    }
110
111    fn prepare_v2(&self, context: &CommandContext) -> MithrilResult<PreparedCardanoDbV2Download> {
112        let ancillary_verification_key = if self.include_ancillary {
113            self.warn_ancillary_not_signed_by_mithril(context);
114            Some(context.config_parameters().require("ancillary_verification_key")?)
115        } else {
116            self.warn_fast_bootstrap_not_available(context);
117            None
118        };
119
120        Ok(PreparedCardanoDbV2Download {
121            hash: self.digest.clone(),
122            download_dir: context.config_parameters().require("download_dir")?,
123            start: self.start,
124            end: self.end,
125            include_ancillary: self.include_ancillary,
126            ancillary_verification_key,
127            allow_override: self.allow_override,
128        })
129    }
130
131    /// Provides guidance on how to enable fast bootstrap by including ancillary files
132    fn warn_fast_bootstrap_not_available(&self, context: &CommandContext) {
133        if context.is_json_output_enabled() {
134            let json = serde_json::json!({
135                JSON_CAUTION_KEY: "The fast bootstrap of the Cardano node is not available with the current parameters used in this command",
136                "impact": "The ledger state will be recomputed from genesis at startup of the Cardano node",
137                "solution": {
138                    "description": "To activate the fast bootstrap of the Cardano node, add the following parameters to the command:",
139                    "parameters": [
140                        "--include-ancillary",
141                        "--ancillary-verification-key (or environment variable ANCILLARY_VERIFICATION_KEY)"
142                    ]
143                },
144            });
145            eprintln!("{json}");
146        } else {
147            eprintln!("The fast bootstrap of the Cardano node is not available with the current parameters used in this command.
148This means that the ledger state will be recomputed from genesis at startup of the Cardano node.
149
150In order to activate the fast bootstrap of the Cardano node, add the following parameters to the command:
151--include-ancillary and --ancillary-verification-key (or environment variable ANCILLARY_VERIFICATION_KEY).
152
153Caution: The ancillary files, including the ledger state, are not currently signed by Mithril.
154As a mitigation, IOG owned keys are used to sign these files.
155For more information, please refer to the network configuration page of the documentation (https://mithril.network/doc/manual/getting-started/network-configurations).");
156        }
157    }
158
159    fn warn_ancillary_not_signed_by_mithril(&self, context: &CommandContext) {
160        let message = "Ancillary verification does not use the Mithril certification: as a mitigation, IOG owned keys are used to sign these files.";
161        if context.is_json_output_enabled() {
162            eprintln!(r#"{{"{JSON_CAUTION_KEY}":"{message}"}}"#);
163        } else {
164            eprintln!("{message}");
165        }
166    }
167
168    fn warn_unused_parameter_with_v1_backend(&self, context: &CommandContext) {
169        let message = "`--start`, `--end`, and `--allow-override` are only available with the `v2` backend. They will be ignored.";
170        if context.is_json_output_enabled() {
171            eprintln!(r#"{{"{JSON_CAUTION_KEY}":"{message}"}}"#);
172        } else {
173            eprintln!("{message}");
174            // Add a blank line to separate this message from the one related to the fast bootstrap that comes next.
175            eprintln!();
176        }
177    }
178}
179
180impl ConfigSource for CardanoDbDownloadCommand {
181    fn collect(&self) -> Result<HashMap<String, String>, ConfigError> {
182        let mut map = HashMap::new();
183
184        if let Some(download_dir) = self.download_dir.clone() {
185            let param = "download_dir".to_string();
186            map.insert(
187                param.clone(),
188                utils::path_to_string(&download_dir)
189                    .map_err(|e| ConfigError::Conversion(param, e))?,
190            );
191        }
192
193        if let Some(genesis_verification_key) = self.genesis_verification_key.clone() {
194            map.insert(
195                "genesis_verification_key".to_string(),
196                genesis_verification_key,
197            );
198        }
199
200        if let Some(ancillary_verification_key) = self.ancillary_verification_key.clone() {
201            map.insert(
202                "ancillary_verification_key".to_string(),
203                ancillary_verification_key,
204            );
205        }
206
207        Ok(map)
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use slog::Logger;
214
215    use crate::ConfigParameters;
216
217    use super::*;
218
219    fn dummy_command() -> CardanoDbDownloadCommand {
220        CardanoDbDownloadCommand {
221            backend: Default::default(),
222            digest: "whatever_digest".to_string(),
223            download_dir: Some(std::path::PathBuf::from("whatever_dir")),
224            genesis_verification_key: "whatever".to_string().into(),
225            include_ancillary: true,
226            ancillary_verification_key: "whatever".to_string().into(),
227            start: None,
228            end: None,
229            allow_override: false,
230        }
231    }
232
233    #[tokio::test]
234    async fn ancillary_verification_key_is_mandatory_when_include_ancillary_is_true() {
235        let command = CardanoDbDownloadCommand {
236            include_ancillary: true,
237            ancillary_verification_key: None,
238            ..dummy_command()
239        };
240        let command_context = CommandContext::new(
241            ConfigParameters::default(),
242            false,
243            true,
244            Logger::root(slog::Discard, slog::o!()),
245        );
246
247        let result = command.execute(command_context).await;
248
249        assert!(result.is_err());
250        assert_eq!(
251            result.unwrap_err().to_string(),
252            "Parameter 'ancillary_verification_key' is mandatory."
253        );
254    }
255
256    mod prepare_v1 {
257        use super::*;
258
259        #[test]
260        fn ancillary_verification_key_can_be_read_through_configuration_file() {
261            let command = CardanoDbDownloadCommand {
262                ancillary_verification_key: None,
263                ..dummy_command()
264            };
265            let config = ConfigParameters::new(HashMap::from([(
266                "ancillary_verification_key".to_string(),
267                "value from config".to_string(),
268            )]));
269            let mut command_context =
270                CommandContext::new(config, false, true, Logger::root(slog::Discard, slog::o!()));
271            command_context.config_parameters_mut().add_source(&command).unwrap();
272
273            let result = command.prepare_v1(&command_context);
274
275            assert!(result.is_ok());
276        }
277
278        #[test]
279        fn db_download_dir_is_mandatory_to_execute_command() {
280            let command = CardanoDbDownloadCommand {
281                download_dir: None,
282                ..dummy_command()
283            };
284            let mut command_context = CommandContext::new(
285                ConfigParameters::default(),
286                false,
287                true,
288                Logger::root(slog::Discard, slog::o!()),
289            );
290
291            command_context.config_parameters_mut().add_source(&command).unwrap();
292
293            let result = command.prepare_v1(&command_context);
294
295            assert!(result.is_err());
296            assert_eq!(
297                result.unwrap_err().to_string(),
298                "Parameter 'download_dir' is mandatory."
299            );
300        }
301    }
302
303    mod prepare_v2 {
304        use super::*;
305
306        #[test]
307        fn ancillary_verification_key_can_be_read_through_configuration_file() {
308            let command = CardanoDbDownloadCommand {
309                ancillary_verification_key: None,
310                ..dummy_command()
311            };
312            let config = ConfigParameters::new(HashMap::from([(
313                "ancillary_verification_key".to_string(),
314                "value from config".to_string(),
315            )]));
316            let mut command_context =
317                CommandContext::new(config, false, true, Logger::root(slog::Discard, slog::o!()));
318
319            command_context.config_parameters_mut().add_source(&command).unwrap();
320
321            let result = command.prepare_v2(&command_context);
322
323            assert!(result.is_ok());
324        }
325
326        #[test]
327        fn db_download_dir_is_mandatory_to_execute_command() {
328            let command = CardanoDbDownloadCommand {
329                download_dir: None,
330                ..dummy_command()
331            };
332            let mut command_context = CommandContext::new(
333                ConfigParameters::default(),
334                false,
335                true,
336                Logger::root(slog::Discard, slog::o!()),
337            );
338
339            command_context.config_parameters_mut().add_source(&command).unwrap();
340
341            let result = command.prepare_v2(&command_context);
342
343            assert!(result.is_err());
344            assert_eq!(
345                result.unwrap_err().to_string(),
346                "Parameter 'download_dir' is mandatory."
347            );
348        }
349    }
350}