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