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