1use std::{
2 collections::HashMap,
3 fs::File,
4 path::{Path, PathBuf},
5 sync::Arc,
6};
7
8use anyhow::{anyhow, Context};
9use chrono::Utc;
10use clap::Parser;
11use slog::{debug, warn, Logger};
12
13use mithril_client::{
14 cardano_database_client::{CardanoDatabaseClient, DownloadUnpackOptions, ImmutableFileRange},
15 common::{ImmutableFileNumber, MKProof, ProtocolMessage},
16 CardanoDatabaseSnapshot, Client, MessageBuilder, MithrilCertificate, MithrilResult,
17};
18
19use crate::{
20 commands::{client_builder, SharedArgs},
21 configuration::{ConfigError, ConfigSource},
22 utils::{
23 self, CardanoDbDownloadChecker, CardanoDbUtils, ExpanderUtils, IndicatifFeedbackReceiver,
24 ProgressOutputType, ProgressPrinter,
25 },
26 CommandContext,
27};
28
29const DISK_SPACE_SAFETY_MARGIN_RATIO: f64 = 0.1;
30
31struct RestorationOptions {
32 db_dir: PathBuf,
33 immutable_file_range: ImmutableFileRange,
34 download_unpack_options: DownloadUnpackOptions,
35 disk_space_safety_margin_ratio: f64,
36}
37
38#[derive(Parser, Debug, Clone)]
40pub struct CardanoDbV2DownloadCommand {
41 #[clap(flatten)]
42 shared_args: SharedArgs,
43
44 hash: String,
48
49 #[clap(long)]
54 download_dir: Option<PathBuf>,
55
56 #[clap(long, env = "GENESIS_VERIFICATION_KEY")]
58 genesis_verification_key: Option<String>,
59
60 #[clap(long)]
64 start: Option<ImmutableFileNumber>,
65
66 #[clap(long)]
70 end: Option<ImmutableFileNumber>,
71
72 #[clap(long, requires = "ancillary_verification_key")]
79 include_ancillary: bool,
80
81 #[clap(long, env = "ANCILLARY_VERIFICATION_KEY")]
83 ancillary_verification_key: Option<String>,
84
85 #[clap(long)]
87 allow_override: bool,
88}
89
90impl CardanoDbV2DownloadCommand {
91 pub fn is_json_output_enabled(&self) -> bool {
93 self.shared_args.json
94 }
95
96 fn immutable_file_range(
97 start: Option<ImmutableFileNumber>,
98 end: Option<ImmutableFileNumber>,
99 ) -> ImmutableFileRange {
100 match (start, end) {
101 (None, None) => ImmutableFileRange::Full,
102 (Some(start), None) => ImmutableFileRange::From(start),
103 (Some(start), Some(end)) => ImmutableFileRange::Range(start, end),
104 (None, Some(end)) => ImmutableFileRange::UpTo(end),
105 }
106 }
107
108 pub async fn execute(&self, context: CommandContext) -> MithrilResult<()> {
110 let params = context.config_parameters()?.add_source(self)?;
111 let download_dir: &String = ¶ms.require("download_dir")?;
112 let restoration_options = RestorationOptions {
113 db_dir: Path::new(download_dir).join("db_v2"),
114 immutable_file_range: Self::immutable_file_range(self.start, self.end),
115 download_unpack_options: DownloadUnpackOptions {
116 allow_override: self.allow_override,
117 include_ancillary: self.include_ancillary,
118 ..DownloadUnpackOptions::default()
119 },
120 disk_space_safety_margin_ratio: DISK_SPACE_SAFETY_MARGIN_RATIO,
121 };
122 let logger = context.logger();
123
124 let progress_output_type = if self.is_json_output_enabled() {
125 ProgressOutputType::JsonReporter
126 } else {
127 ProgressOutputType::Tty
128 };
129 let progress_printer = ProgressPrinter::new(progress_output_type, 6);
130 let client = client_builder(¶ms)?
131 .add_feedback_receiver(Arc::new(IndicatifFeedbackReceiver::new(
132 progress_output_type,
133 logger.clone(),
134 )))
135 .set_ancillary_verification_key(self.ancillary_verification_key.clone())
136 .with_logger(logger.clone())
137 .build()?;
138
139 let get_list_of_artifact_ids = || async {
140 let cardano_db_snapshots =
141 client.cardano_database_v2().list().await.with_context(|| {
142 "Can not get the list of artifacts while retrieving the latest cardano db hash"
143 })?;
144
145 Ok(cardano_db_snapshots
146 .iter()
147 .map(|cardano_db| cardano_db.hash.to_owned())
148 .collect::<Vec<String>>())
149 };
150
151 let cardano_db_message = client
152 .cardano_database_v2()
153 .get(
154 &ExpanderUtils::expand_eventual_id_alias(&self.hash, get_list_of_artifact_ids())
155 .await?,
156 )
157 .await?
158 .with_context(|| format!("Can not get the cardano db for hash: '{}'", self.hash))?;
159
160 Self::check_local_disk_info(
161 1,
162 &progress_printer,
163 &restoration_options,
164 &cardano_db_message,
165 self.allow_override,
166 )?;
167
168 let certificate = Self::fetch_certificate_and_verifying_chain(
169 2,
170 &progress_printer,
171 &client,
172 &cardano_db_message.certificate_hash,
173 )
174 .await?;
175
176 Self::download_and_unpack_cardano_database_snapshot(
177 logger,
178 3,
179 &progress_printer,
180 client.cardano_database_v2(),
181 &cardano_db_message,
182 &restoration_options,
183 )
184 .await
185 .with_context(|| {
186 format!(
187 "Can not download and unpack cardano db snapshot for hash: '{}'",
188 self.hash
189 )
190 })?;
191
192 let merkle_proof = Self::compute_verify_merkle_proof(
193 4,
194 &progress_printer,
195 &client,
196 &certificate,
197 &cardano_db_message,
198 &restoration_options.immutable_file_range,
199 &restoration_options.db_dir,
200 )
201 .await?;
202
203 let message = Self::compute_cardano_db_snapshot_message(
204 5,
205 &progress_printer,
206 &certificate,
207 &merkle_proof,
208 )
209 .await?;
210
211 Self::verify_cardano_db_snapshot_signature(
212 logger,
213 6,
214 &progress_printer,
215 &certificate,
216 &message,
217 &cardano_db_message,
218 &restoration_options.db_dir,
219 )
220 .await?;
221
222 Self::log_download_information(
223 &restoration_options.db_dir,
224 &cardano_db_message,
225 self.is_json_output_enabled(),
226 )?;
227
228 Ok(())
229 }
230
231 fn compute_total_immutables_restored_size(
232 cardano_db: &CardanoDatabaseSnapshot,
233 restoration_options: &RestorationOptions,
234 ) -> u64 {
235 let total_immutables_restored = restoration_options
236 .immutable_file_range
237 .length(cardano_db.beacon.immutable_file_number);
238
239 total_immutables_restored * cardano_db.immutables.average_size_uncompressed
240 }
241
242 fn add_safety_margin(size: u64, margin_ratio: f64) -> u64 {
243 (size as f64 * (1.0 + margin_ratio)) as u64
244 }
245
246 fn compute_required_disk_space_for_snapshot(
247 cardano_db: &CardanoDatabaseSnapshot,
248 restoration_options: &RestorationOptions,
249 ) -> u64 {
250 if restoration_options.immutable_file_range == ImmutableFileRange::Full {
251 cardano_db.total_db_size_uncompressed
252 } else {
253 let total_immutables_restored_size =
254 Self::compute_total_immutables_restored_size(cardano_db, restoration_options);
255
256 let mut total_size =
257 total_immutables_restored_size + cardano_db.digests.size_uncompressed;
258 if restoration_options
259 .download_unpack_options
260 .include_ancillary
261 {
262 total_size += cardano_db.ancillary.size_uncompressed;
263 }
264
265 Self::add_safety_margin(
266 total_size,
267 restoration_options.disk_space_safety_margin_ratio,
268 )
269 }
270 }
271
272 fn check_local_disk_info(
273 step_number: u16,
274 progress_printer: &ProgressPrinter,
275 restoration_options: &RestorationOptions,
276 cardano_db: &CardanoDatabaseSnapshot,
277 allow_override: bool,
278 ) -> MithrilResult<()> {
279 progress_printer.report_step(step_number, "Checking local disk info…")?;
280
281 CardanoDbDownloadChecker::ensure_dir_exist(&restoration_options.db_dir)?;
282 if let Err(e) = CardanoDbDownloadChecker::check_prerequisites_for_uncompressed_data(
283 &restoration_options.db_dir,
284 Self::compute_required_disk_space_for_snapshot(cardano_db, restoration_options),
285 allow_override,
286 ) {
287 progress_printer
288 .report_step(step_number, &CardanoDbUtils::check_disk_space_error(e)?)?;
289 }
290
291 Ok(())
292 }
293
294 async fn fetch_certificate_and_verifying_chain(
295 step_number: u16,
296 progress_printer: &ProgressPrinter,
297 client: &Client,
298 certificate_hash: &str,
299 ) -> MithrilResult<MithrilCertificate> {
300 progress_printer.report_step(
301 step_number,
302 "Fetching the certificate and verifying the certificate chain…",
303 )?;
304 let certificate = client
305 .certificate()
306 .verify_chain(certificate_hash)
307 .await
308 .with_context(|| {
309 format!(
310 "Can not verify the certificate chain from certificate_hash: '{}'",
311 certificate_hash
312 )
313 })?;
314
315 Ok(certificate)
316 }
317
318 async fn download_and_unpack_cardano_database_snapshot(
319 logger: &Logger,
320 step_number: u16,
321 progress_printer: &ProgressPrinter,
322 client: Arc<CardanoDatabaseClient>,
323 cardano_database_snapshot: &CardanoDatabaseSnapshot,
324 restoration_options: &RestorationOptions,
325 ) -> MithrilResult<()> {
326 progress_printer.report_step(
327 step_number,
328 "Downloading and unpacking the cardano db snapshot",
329 )?;
330 client
331 .download_unpack(
332 cardano_database_snapshot,
333 &restoration_options.immutable_file_range,
334 &restoration_options.db_dir,
335 restoration_options.download_unpack_options,
336 )
337 .await?;
338
339 let full_restoration = restoration_options.immutable_file_range == ImmutableFileRange::Full;
342 let include_ancillary = restoration_options
343 .download_unpack_options
344 .include_ancillary;
345 let number_of_immutable_files_restored = restoration_options
346 .immutable_file_range
347 .length(cardano_database_snapshot.beacon.immutable_file_number);
348 if let Err(e) = client
349 .add_statistics(
350 full_restoration,
351 include_ancillary,
352 number_of_immutable_files_restored,
353 )
354 .await
355 {
356 warn!(
357 logger, "Could not increment cardano db snapshot download statistics";
358 "error" => ?e
359 );
360 }
361
362 if let Err(error) = File::create(restoration_options.db_dir.join("clean")) {
364 warn!(
365 logger, "Could not create clean shutdown marker file in directory '{}'", restoration_options.db_dir.display();
366 "error" => error.to_string()
367 );
368 };
369
370 Ok(())
371 }
372
373 async fn compute_verify_merkle_proof(
374 step_number: u16,
375 progress_printer: &ProgressPrinter,
376 client: &Client,
377 certificate: &MithrilCertificate,
378 cardano_database_snapshot: &CardanoDatabaseSnapshot,
379 immutable_file_range: &ImmutableFileRange,
380 unpacked_dir: &Path,
381 ) -> MithrilResult<MKProof> {
382 progress_printer.report_step(step_number, "Computing and verifying the Merkle proof…")?;
383 let merkle_proof = client
384 .cardano_database_v2()
385 .compute_merkle_proof(
386 certificate,
387 cardano_database_snapshot,
388 immutable_file_range,
389 unpacked_dir,
390 )
391 .await?;
392
393 merkle_proof
394 .verify()
395 .with_context(|| "Merkle proof verification failed")?;
396
397 Ok(merkle_proof)
398 }
399
400 async fn compute_cardano_db_snapshot_message(
401 step_number: u16,
402 progress_printer: &ProgressPrinter,
403 certificate: &MithrilCertificate,
404 merkle_proof: &MKProof,
405 ) -> MithrilResult<ProtocolMessage> {
406 progress_printer.report_step(step_number, "Computing the cardano db snapshot message")?;
407 let message = CardanoDbUtils::wait_spinner(
408 progress_printer,
409 MessageBuilder::new().compute_cardano_database_message(certificate, merkle_proof),
410 )
411 .await
412 .with_context(|| "Can not compute the cardano db snapshot message")?;
413
414 Ok(message)
415 }
416
417 async fn verify_cardano_db_snapshot_signature(
418 logger: &Logger,
419 step_number: u16,
420 progress_printer: &ProgressPrinter,
421 certificate: &MithrilCertificate,
422 message: &ProtocolMessage,
423 cardano_db_snapshot: &CardanoDatabaseSnapshot,
424 db_dir: &Path,
425 ) -> MithrilResult<()> {
426 progress_printer.report_step(step_number, "Verifying the cardano db signature…")?;
427 if !certificate.match_message(message) {
428 debug!(
429 logger,
430 "Merkle root verification failed, removing unpacked files & directory."
431 );
432
433 if let Err(error) = std::fs::remove_dir_all(db_dir) {
434 warn!(
435 logger, "Error while removing unpacked files & directory";
436 "error" => error.to_string()
437 );
438 }
439
440 return Err(anyhow!(
441 "Certificate verification failed (cardano db snapshot hash = '{}').",
442 cardano_db_snapshot.hash.clone()
443 ));
444 }
445
446 Ok(())
447 }
448
449 fn log_download_information(
450 db_dir: &Path,
451 cardano_db_snapshot: &CardanoDatabaseSnapshot,
452 json_output: bool,
453 ) -> MithrilResult<()> {
454 let canonicalized_filepath = &db_dir.canonicalize().with_context(|| {
455 format!(
456 "Could not get canonicalized filepath of '{}'",
457 db_dir.display()
458 )
459 })?;
460
461 if json_output {
462 println!(
463 r#"{{"timestamp": "{}", "db_directory": "{}"}}"#,
464 Utc::now().to_rfc3339(),
465 canonicalized_filepath.display()
466 );
467 } else {
468 let cardano_node_version = &cardano_db_snapshot.cardano_node_version;
469 println!(
470 r###"Cardano database snapshot '{}' archives have been successfully unpacked. Immutable files have been successfully checked against Mithril multi-signature contained in the certificate.
471
472 Files in the directory '{}' can be used to run a Cardano node with version >= {cardano_node_version}.
473
474 If you are using Cardano Docker image, you can restore a Cardano Node with:
475
476 docker run -v cardano-node-ipc:/ipc -v cardano-node-data:/data --mount type=bind,source="{}",target=/data/db/ -e NETWORK={} ghcr.io/intersectmbo/cardano-node:{cardano_node_version}
477
478 "###,
479 cardano_db_snapshot.hash,
480 db_dir.display(),
481 canonicalized_filepath.display(),
482 cardano_db_snapshot.network,
483 );
484 }
485
486 Ok(())
487 }
488}
489
490impl ConfigSource for CardanoDbV2DownloadCommand {
491 fn collect(&self) -> Result<HashMap<String, String>, ConfigError> {
492 let mut map = HashMap::new();
493
494 if let Some(download_dir) = self.download_dir.clone() {
495 let param = "download_dir".to_string();
496 map.insert(
497 param.clone(),
498 utils::path_to_string(&download_dir)
499 .map_err(|e| ConfigError::Conversion(param, e))?,
500 );
501 }
502
503 if let Some(genesis_verification_key) = self.genesis_verification_key.clone() {
504 map.insert(
505 "genesis_verification_key".to_string(),
506 genesis_verification_key,
507 );
508 }
509
510 Ok(map)
511 }
512}
513
514#[cfg(test)]
515mod tests {
516 use mithril_client::{
517 common::{
518 AncillaryMessagePart, CardanoDbBeacon, DigestsMessagePart, ImmutablesMessagePart,
519 ProtocolMessagePartKey, SignedEntityType,
520 },
521 MithrilCertificateMetadata,
522 };
523 use mithril_common::test_utils::TempDir;
524
525 use super::*;
526
527 fn dummy_certificate() -> MithrilCertificate {
528 let mut protocol_message = ProtocolMessage::new();
529 protocol_message.set_message_part(
530 ProtocolMessagePartKey::CardanoDatabaseMerkleRoot,
531 CardanoDatabaseSnapshot::dummy().hash.to_string(),
532 );
533 protocol_message.set_message_part(
534 ProtocolMessagePartKey::NextAggregateVerificationKey,
535 "whatever".to_string(),
536 );
537 let beacon = CardanoDbBeacon::new(10, 100);
538
539 MithrilCertificate {
540 hash: "hash".to_string(),
541 previous_hash: "previous_hash".to_string(),
542 epoch: beacon.epoch,
543 signed_entity_type: SignedEntityType::CardanoDatabase(beacon),
544 metadata: MithrilCertificateMetadata::dummy(),
545 protocol_message: protocol_message.clone(),
546 signed_message: "signed_message".to_string(),
547 aggregate_verification_key: String::new(),
548 multi_signature: String::new(),
549 genesis_signature: String::new(),
550 }
551 }
552
553 #[test]
554 fn ancillary_verification_key_is_mandatory_when_include_ancillary_is_true() {
555 CardanoDbV2DownloadCommand::try_parse_from([
556 "cdbv2-command",
557 "--include-ancillary",
558 "whatever_hash",
559 ])
560 .expect_err("The command should fail because ancillary_verification_key is not set");
561 }
562
563 #[tokio::test]
564 async fn verify_cardano_db_snapshot_signature_should_remove_db_dir_if_messages_mismatch() {
565 let progress_printer = ProgressPrinter::new(ProgressOutputType::Tty, 1);
566 let certificate = dummy_certificate();
567 let mut message = ProtocolMessage::new();
568 message.set_message_part(
569 ProtocolMessagePartKey::CardanoDatabaseMerkleRoot,
570 "merkle-root-123456".to_string(),
571 );
572 message.set_message_part(
573 ProtocolMessagePartKey::NextAggregateVerificationKey,
574 "avk-123456".to_string(),
575 );
576 let cardano_db = CardanoDatabaseSnapshot::dummy();
577 let db_dir = TempDir::create(
578 "client-cli",
579 "verify_cardano_db_snapshot_signature_should_remove_db_dir_if_messages_mismatch",
580 );
581
582 let result = CardanoDbV2DownloadCommand::verify_cardano_db_snapshot_signature(
583 &Logger::root(slog::Discard, slog::o!()),
584 1,
585 &progress_printer,
586 &certificate,
587 &message,
588 &cardano_db,
589 &db_dir,
590 )
591 .await;
592
593 assert!(result.is_err());
594 assert!(
595 !db_dir.exists(),
596 "The db directory should have been removed but it still exists"
597 );
598 }
599
600 #[test]
601 fn immutable_file_range_without_start_without_end_returns_variant_full() {
602 let range = CardanoDbV2DownloadCommand::immutable_file_range(None, None);
603
604 assert_eq!(range, ImmutableFileRange::Full);
605 }
606
607 #[test]
608 fn immutable_file_range_with_start_without_end_returns_variant_from() {
609 let start = Some(12);
610
611 let range = CardanoDbV2DownloadCommand::immutable_file_range(start, None);
612
613 assert_eq!(range, ImmutableFileRange::From(12));
614 }
615
616 #[test]
617 fn immutable_file_range_with_start_with_end_returns_variant_range() {
618 let start = Some(12);
619 let end = Some(345);
620
621 let range = CardanoDbV2DownloadCommand::immutable_file_range(start, end);
622
623 assert_eq!(range, ImmutableFileRange::Range(12, 345));
624 }
625
626 #[test]
627 fn immutable_file_range_without_start_with_end_returns_variant_up_to() {
628 let end = Some(345);
629
630 let range = CardanoDbV2DownloadCommand::immutable_file_range(None, end);
631
632 assert_eq!(range, ImmutableFileRange::UpTo(345));
633 }
634
635 #[test]
636 fn compute_required_disk_space_for_snapshot_when_full_restoration() {
637 let cardano_db_snapshot = CardanoDatabaseSnapshot {
638 total_db_size_uncompressed: 123,
639 ..CardanoDatabaseSnapshot::dummy()
640 };
641 let restoration_options = RestorationOptions {
642 immutable_file_range: ImmutableFileRange::Full,
643 db_dir: PathBuf::from("db_dir"),
644 download_unpack_options: DownloadUnpackOptions::default(),
645 disk_space_safety_margin_ratio: 0.0,
646 };
647
648 let required_size = CardanoDbV2DownloadCommand::compute_required_disk_space_for_snapshot(
649 &cardano_db_snapshot,
650 &restoration_options,
651 );
652
653 assert_eq!(required_size, 123);
654 }
655
656 #[test]
657 fn compute_required_disk_space_for_snapshot_when_partial_restoration_and_no_ancillary_files() {
658 let cardano_db_snapshot = CardanoDatabaseSnapshot {
659 digests: DigestsMessagePart {
660 size_uncompressed: 50,
661 locations: vec![],
662 },
663 immutables: ImmutablesMessagePart {
664 average_size_uncompressed: 100,
665 locations: vec![],
666 },
667 ancillary: AncillaryMessagePart {
668 size_uncompressed: 300,
669 locations: vec![],
670 },
671 ..CardanoDatabaseSnapshot::dummy()
672 };
673 let restoration_options = RestorationOptions {
674 immutable_file_range: ImmutableFileRange::Range(10, 19),
675 db_dir: PathBuf::from("db_dir"),
676 download_unpack_options: DownloadUnpackOptions {
677 include_ancillary: false,
678 ..DownloadUnpackOptions::default()
679 },
680 disk_space_safety_margin_ratio: 0.0,
681 };
682
683 let required_size = CardanoDbV2DownloadCommand::compute_required_disk_space_for_snapshot(
684 &cardano_db_snapshot,
685 &restoration_options,
686 );
687
688 let digest_size = cardano_db_snapshot.digests.size_uncompressed;
689 let average_size_uncompressed_immutable =
690 cardano_db_snapshot.immutables.average_size_uncompressed;
691
692 let expected_size = digest_size + 10 * average_size_uncompressed_immutable;
693 assert_eq!(required_size, expected_size);
694 }
695
696 #[test]
697 fn compute_required_disk_space_for_snapshot_when_partial_restoration_and_ancillary_files() {
698 let cardano_db_snapshot = CardanoDatabaseSnapshot {
699 digests: DigestsMessagePart {
700 size_uncompressed: 50,
701 locations: vec![],
702 },
703 immutables: ImmutablesMessagePart {
704 average_size_uncompressed: 100,
705 locations: vec![],
706 },
707 ancillary: AncillaryMessagePart {
708 size_uncompressed: 300,
709 locations: vec![],
710 },
711 ..CardanoDatabaseSnapshot::dummy()
712 };
713 let restoration_options = RestorationOptions {
714 immutable_file_range: ImmutableFileRange::Range(10, 19),
715 db_dir: PathBuf::from("db_dir"),
716 download_unpack_options: DownloadUnpackOptions {
717 include_ancillary: true,
718 ..DownloadUnpackOptions::default()
719 },
720 disk_space_safety_margin_ratio: 0.0,
721 };
722
723 let required_size = CardanoDbV2DownloadCommand::compute_required_disk_space_for_snapshot(
724 &cardano_db_snapshot,
725 &restoration_options,
726 );
727
728 let digest_size = cardano_db_snapshot.digests.size_uncompressed;
729 let average_size_uncompressed_immutable =
730 cardano_db_snapshot.immutables.average_size_uncompressed;
731 let ancillary_size = cardano_db_snapshot.ancillary.size_uncompressed;
732
733 let expected_size = digest_size + 10 * average_size_uncompressed_immutable + ancillary_size;
734 assert_eq!(required_size, expected_size);
735 }
736
737 #[test]
738 fn add_safety_margin_apply_margin_with_ratio() {
739 assert_eq!(CardanoDbV2DownloadCommand::add_safety_margin(100, 0.1), 110);
740 assert_eq!(CardanoDbV2DownloadCommand::add_safety_margin(100, 0.5), 150);
741 assert_eq!(CardanoDbV2DownloadCommand::add_safety_margin(100, 1.5), 250);
742
743 assert_eq!(CardanoDbV2DownloadCommand::add_safety_margin(0, 0.1), 0);
744
745 assert_eq!(CardanoDbV2DownloadCommand::add_safety_margin(100, 0.0), 100);
746 }
747}