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, warn_unused_parameter_with_v1_backend},
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            warn_unused_parameter_with_v1_backend(
93                context,
94                ["--start", "--end", "--allow_override"],
95            );
96        }
97
98        let ancillary_verification_key = if self.include_ancillary {
99            self.warn_ancillary_not_signed_by_mithril(context);
100            Some(context.config_parameters().require("ancillary_verification_key")?)
101        } else {
102            self.warn_fast_bootstrap_not_available(context);
103            None
104        };
105
106        Ok(PreparedCardanoDbV1Download {
107            digest: self.digest.clone(),
108            download_dir: context.config_parameters().require("download_dir")?,
109            include_ancillary: self.include_ancillary,
110            ancillary_verification_key,
111        })
112    }
113
114    fn prepare_v2(&self, context: &CommandContext) -> MithrilResult<PreparedCardanoDbV2Download> {
115        let ancillary_verification_key = if self.include_ancillary {
116            self.warn_ancillary_not_signed_by_mithril(context);
117            Some(context.config_parameters().require("ancillary_verification_key")?)
118        } else {
119            self.warn_fast_bootstrap_not_available(context);
120            None
121        };
122
123        Ok(PreparedCardanoDbV2Download {
124            hash: self.digest.clone(),
125            download_dir: context.config_parameters().require("download_dir")?,
126            start: self.start,
127            end: self.end,
128            include_ancillary: self.include_ancillary,
129            ancillary_verification_key,
130            allow_override: self.allow_override,
131        })
132    }
133
134    /// Provides guidance on how to enable fast bootstrap by including ancillary files
135    fn warn_fast_bootstrap_not_available(&self, context: &CommandContext) {
136        if context.is_json_output_enabled() {
137            let json = serde_json::json!({
138                JSON_CAUTION_KEY: "The fast bootstrap of the Cardano node is not available with the current parameters used in this command",
139                "impact": "The ledger state will be recomputed from genesis at startup of the Cardano node",
140                "solution": {
141                    "description": "To activate the fast bootstrap of the Cardano node, add the following parameters to the command:",
142                    "parameters": [
143                        "--include-ancillary",
144                        "--ancillary-verification-key (or environment variable ANCILLARY_VERIFICATION_KEY)"
145                    ]
146                },
147            });
148            eprintln!("{json}");
149        } else {
150            eprintln!("The fast bootstrap of the Cardano node is not available with the current parameters used in this command.
151This means that the ledger state will be recomputed from genesis at startup of the Cardano node.
152
153In order to activate the fast bootstrap of the Cardano node, add the following parameters to the command:
154--include-ancillary and --ancillary-verification-key (or environment variable ANCILLARY_VERIFICATION_KEY).
155
156Caution: The ancillary files, including the ledger state, are not currently signed by Mithril.
157As a mitigation, IOG owned keys are used to sign these files.
158For more information, please refer to the network configuration page of the documentation (https://mithril.network/doc/manual/getting-started/network-configurations).");
159        }
160    }
161
162    fn warn_ancillary_not_signed_by_mithril(&self, context: &CommandContext) {
163        let message = "Ancillary verification does not use the Mithril certification: as a mitigation, IOG owned keys are used to sign these files.";
164        if context.is_json_output_enabled() {
165            eprintln!(r#"{{"{JSON_CAUTION_KEY}":"{message}"}}"#);
166        } else {
167            eprintln!("{message}");
168        }
169    }
170}
171
172impl ConfigSource for CardanoDbDownloadCommand {
173    fn collect(&self) -> Result<HashMap<String, String>, ConfigError> {
174        let mut map = HashMap::new();
175
176        if let Some(download_dir) = self.download_dir.clone() {
177            let param = "download_dir".to_string();
178            map.insert(
179                param.clone(),
180                utils::path_to_string(&download_dir)
181                    .map_err(|e| ConfigError::Conversion(param, e))?,
182            );
183        }
184
185        if let Some(genesis_verification_key) = self.genesis_verification_key.clone() {
186            map.insert(
187                "genesis_verification_key".to_string(),
188                genesis_verification_key,
189            );
190        }
191
192        if let Some(ancillary_verification_key) = self.ancillary_verification_key.clone() {
193            map.insert(
194                "ancillary_verification_key".to_string(),
195                ancillary_verification_key,
196            );
197        }
198
199        Ok(map)
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use slog::Logger;
206
207    use crate::ConfigParameters;
208
209    use super::*;
210
211    fn dummy_command() -> CardanoDbDownloadCommand {
212        CardanoDbDownloadCommand {
213            backend: Default::default(),
214            digest: "whatever_digest".to_string(),
215            download_dir: Some(std::path::PathBuf::from("whatever_dir")),
216            genesis_verification_key: "whatever".to_string().into(),
217            include_ancillary: true,
218            ancillary_verification_key: "whatever".to_string().into(),
219            start: None,
220            end: None,
221            allow_override: false,
222        }
223    }
224
225    #[tokio::test]
226    async fn ancillary_verification_key_is_mandatory_when_include_ancillary_is_true() {
227        let command = CardanoDbDownloadCommand {
228            include_ancillary: true,
229            ancillary_verification_key: None,
230            ..dummy_command()
231        };
232        let command_context = CommandContext::new(
233            ConfigParameters::default(),
234            false,
235            true,
236            Logger::root(slog::Discard, slog::o!()),
237        );
238
239        let result = command.execute(command_context).await;
240
241        assert!(result.is_err());
242        assert_eq!(
243            result.unwrap_err().to_string(),
244            "Parameter 'ancillary_verification_key' is mandatory."
245        );
246    }
247
248    mod prepare_v1 {
249        use super::*;
250
251        #[test]
252        fn ancillary_verification_key_can_be_read_through_configuration_file() {
253            let command = CardanoDbDownloadCommand {
254                ancillary_verification_key: None,
255                ..dummy_command()
256            };
257            let config = ConfigParameters::new(HashMap::from([(
258                "ancillary_verification_key".to_string(),
259                "value from config".to_string(),
260            )]));
261            let mut command_context =
262                CommandContext::new(config, false, true, Logger::root(slog::Discard, slog::o!()));
263            command_context.config_parameters_mut().add_source(&command).unwrap();
264
265            let result = command.prepare_v1(&command_context);
266
267            assert!(result.is_ok());
268        }
269
270        #[test]
271        fn db_download_dir_is_mandatory_to_execute_command() {
272            let command = CardanoDbDownloadCommand {
273                download_dir: None,
274                ..dummy_command()
275            };
276            let mut command_context = CommandContext::new(
277                ConfigParameters::default(),
278                false,
279                true,
280                Logger::root(slog::Discard, slog::o!()),
281            );
282
283            command_context.config_parameters_mut().add_source(&command).unwrap();
284
285            let result = command.prepare_v1(&command_context);
286
287            assert!(result.is_err());
288            assert_eq!(
289                result.unwrap_err().to_string(),
290                "Parameter 'download_dir' is mandatory."
291            );
292        }
293    }
294
295    mod prepare_v2 {
296        use super::*;
297
298        #[test]
299        fn ancillary_verification_key_can_be_read_through_configuration_file() {
300            let command = CardanoDbDownloadCommand {
301                ancillary_verification_key: None,
302                ..dummy_command()
303            };
304            let config = ConfigParameters::new(HashMap::from([(
305                "ancillary_verification_key".to_string(),
306                "value from config".to_string(),
307            )]));
308            let mut command_context =
309                CommandContext::new(config, false, true, Logger::root(slog::Discard, slog::o!()));
310
311            command_context.config_parameters_mut().add_source(&command).unwrap();
312
313            let result = command.prepare_v2(&command_context);
314
315            assert!(result.is_ok());
316        }
317
318        #[test]
319        fn db_download_dir_is_mandatory_to_execute_command() {
320            let command = CardanoDbDownloadCommand {
321                download_dir: None,
322                ..dummy_command()
323            };
324            let mut command_context = CommandContext::new(
325                ConfigParameters::default(),
326                false,
327                true,
328                Logger::root(slog::Discard, slog::o!()),
329            );
330
331            command_context.config_parameters_mut().add_source(&command).unwrap();
332
333            let result = command.prepare_v2(&command_context);
334
335            assert!(result.is_err());
336            assert_eq!(
337                result.unwrap_err().to_string(),
338                "Parameter 'download_dir' is mandatory."
339            );
340        }
341    }
342}