mithril_client_cli/commands/cardano_db/
verify.rs

1use std::{
2    collections::HashMap,
3    path::{Path, PathBuf},
4    sync::Arc,
5};
6
7use anyhow::Context;
8use chrono::{DateTime, Utc};
9use clap::Parser;
10use mithril_client::{
11    CardanoDatabaseSnapshot, MithrilResult,
12    cardano_database_client::ImmutableFileRange,
13    cardano_database_client::{CardanoDatabaseVerificationError, ImmutableVerificationResult},
14    common::ImmutableFileNumber,
15};
16
17use crate::{
18    CommandContext,
19    commands::{
20        cardano_db::{
21            CardanoDbCommandsBackend,
22            shared_steps::{self, ComputeCardanoDatabaseMessageOptions},
23        },
24        client_builder,
25    },
26    configuration::{ConfigError, ConfigSource},
27    utils::{self, ExpanderUtils, IndicatifFeedbackReceiver, ProgressOutputType, ProgressPrinter},
28};
29
30/// Clap command to verify a Cardano db and its associated certificate.
31#[derive(Parser, Debug, Clone)]
32pub struct CardanoDbVerifyCommand {
33    ///Backend to use, either: `v1` (default, full database restoration only) or `v2` (full or partial database restoration)
34    #[arg(short, long, value_enum, default_value_t = CardanoDbCommandsBackend::V2)]
35    backend: CardanoDbCommandsBackend,
36
37    /// Digest of the Cardano db snapshot to verify  or `latest` for the latest artifact
38    ///
39    /// Use the `list` command to get that information.
40    digest: String,
41
42    /// Directory from where the immutable will be verified.
43    #[clap(long)]
44    db_dir: Option<PathBuf>,
45
46    /// Genesis verification key to check the certificate chain.
47    #[clap(long, env = "GENESIS_VERIFICATION_KEY")]
48    genesis_verification_key: Option<String>,
49
50    /// The first immutable file number to verify.
51    ///
52    /// If not set, the verify process will start from the first immutable file.
53    #[clap(long)]
54    start: Option<ImmutableFileNumber>,
55
56    /// The last immutable file number to verify.
57    ///
58    /// If not set, the verify will continue until the last certified immutable file.
59    #[clap(long)]
60    end: Option<ImmutableFileNumber>,
61
62    /// If set, the verification will not fail if some immutable files are missing.
63    #[clap(long)]
64    allow_missing: bool,
65}
66
67impl CardanoDbVerifyCommand {
68    /// Main command execution
69    pub async fn execute(&self, mut context: CommandContext) -> MithrilResult<()> {
70        match self.backend {
71            CardanoDbCommandsBackend::V1 => Err(anyhow::anyhow!(
72                r#"The "verify" subcommand is not available for the v1, use --backend v2 instead"#,
73            )),
74            CardanoDbCommandsBackend::V2 => {
75                context.config_parameters_mut().add_source(self)?;
76                self.verify(&context).await
77            }
78        }
79    }
80
81    async fn verify(&self, context: &CommandContext) -> MithrilResult<()> {
82        let db_dir = context.config_parameters().require("db_dir")?;
83        let db_dir = Path::new(&db_dir);
84
85        let progress_output_type = if context.is_json_output_enabled() {
86            ProgressOutputType::JsonReporter
87        } else {
88            ProgressOutputType::Tty
89        };
90        let progress_printer = ProgressPrinter::new(progress_output_type, 5);
91        let client = client_builder(context.config_parameters())?
92            .add_feedback_receiver(Arc::new(IndicatifFeedbackReceiver::new(
93                progress_output_type,
94                context.logger().clone(),
95            )))
96            .with_logger(context.logger().clone())
97            .build()?;
98
99        client.cardano_database_v2().check_has_immutables(db_dir)?;
100
101        let get_list_of_artifact_ids = || async {
102            let cardano_db_snapshots = client.cardano_database_v2().list().await.with_context(
103                || "Can not get the list of artifacts while retrieving the latest cardano db hash",
104            )?;
105
106            Ok(cardano_db_snapshots
107                .iter()
108                .map(|cardano_db| cardano_db.hash.to_owned())
109                .collect::<Vec<String>>())
110        };
111
112        let cardano_db_message = client
113            .cardano_database_v2()
114            .get(
115                &ExpanderUtils::expand_eventual_id_alias(&self.digest, get_list_of_artifact_ids())
116                    .await?,
117            )
118            .await?
119            .with_context(|| format!("Can not get the cardano db for hash: '{}'", self.digest))?;
120
121        let immutable_file_range = shared_steps::immutable_file_range(self.start, self.end);
122
123        print_immutables_range_to_verify(
124            &cardano_db_message,
125            &immutable_file_range,
126            context.is_json_output_enabled(),
127        )?;
128
129        let certificate = shared_steps::fetch_certificate_and_verifying_chain(
130            1,
131            &progress_printer,
132            &client,
133            &cardano_db_message.certificate_hash,
134        )
135        .await?;
136
137        let verified_digests = shared_steps::download_and_verify_digests(
138            2,
139            &progress_printer,
140            &client,
141            &certificate,
142            &cardano_db_message,
143        )
144        .await?;
145
146        let options = ComputeCardanoDatabaseMessageOptions {
147            db_dir: db_dir.to_path_buf(),
148            immutable_file_range,
149            allow_missing: self.allow_missing,
150        };
151
152        let merkle_proof = shared_steps::verify_cardano_database(
153            3,
154            &progress_printer,
155            &client,
156            &certificate,
157            &cardano_db_message,
158            &options,
159            &verified_digests,
160        )
161        .await;
162
163        match merkle_proof {
164            Err(e) => match e.downcast_ref::<CardanoDatabaseVerificationError>() {
165                Some(CardanoDatabaseVerificationError::ImmutableFilesVerification(lists)) => {
166                    Self::print_immutables_verification_error(
167                        lists,
168                        context.is_json_output_enabled(),
169                    );
170                    Ok(())
171                }
172                _ => Err(e),
173            },
174            Ok(merkle_proof) => {
175                let message = shared_steps::compute_cardano_db_snapshot_message(
176                    4,
177                    &progress_printer,
178                    &certificate,
179                    &merkle_proof,
180                )
181                .await?;
182
183                shared_steps::verify_message_matches_certificate(
184                    &context.logger().clone(),
185                    5,
186                    &progress_printer,
187                    &certificate,
188                    &message,
189                    &cardano_db_message,
190                    db_dir,
191                )
192                .await?;
193
194                Self::log_verified_information(
195                    db_dir,
196                    &cardano_db_message.hash,
197                    context.is_json_output_enabled(),
198                )?;
199
200                Ok(())
201            }
202        }
203    }
204
205    fn log_verified_information(
206        db_dir: &Path,
207        snapshot_hash: &str,
208        json_output: bool,
209    ) -> MithrilResult<()> {
210        if json_output {
211            let canonical_filepath = &db_dir.canonicalize().with_context(|| {
212                format!("Could not get canonical filepath of '{}'", db_dir.display())
213            })?;
214            let json = serde_json::json!({
215                "timestamp": Utc::now().to_rfc3339(),
216                "verified_db_directory": canonical_filepath
217            });
218            println!("{json}");
219        } else {
220            println!(
221                "Cardano database snapshot '{snapshot_hash}' archives have been successfully verified. Immutable files have been successfully verified with Mithril."
222            );
223        }
224        Ok(())
225    }
226
227    fn print_immutables_verification_error(lists: &ImmutableVerificationResult, json_output: bool) {
228        let utc_now = Utc::now();
229        let json_file_path = write_json_file_error(utc_now, lists);
230        let error_message = "Verifying immutables files has failed";
231        if json_output {
232            let json = serde_json::json!({
233                "timestamp": utc_now.to_rfc3339(),
234                "verify_error" : {
235                    "message": error_message,
236                    "immutables_verification_error_file": json_file_path,
237                    "immutables_dir": lists.immutables_dir,
238                    "missing_files_count": lists.missing.len(),
239                    "tampered_files_count": lists.tampered.len(),
240                    "non_verifiable_files_count": lists.non_verifiable.len(),
241                }
242            });
243
244            println!("{json}");
245        } else {
246            println!("{error_message}");
247            println!(
248                "See the lists of all missing, tampered and non verifiable files in {}",
249                json_file_path.display()
250            );
251            if !lists.missing.is_empty() {
252                println!("Number of missing immutable files: {}", lists.missing.len());
253            }
254            if !lists.tampered.is_empty() {
255                println!(
256                    "Number of tampered immutable files: {:?}",
257                    lists.tampered.len()
258                );
259            }
260            if !lists.non_verifiable.is_empty() {
261                println!(
262                    "Number of non verifiable immutable files: {:?}",
263                    lists.non_verifiable.len()
264                );
265            }
266        }
267    }
268}
269
270fn write_json_file_error(date: DateTime<Utc>, lists: &ImmutableVerificationResult) -> PathBuf {
271    let file_path = PathBuf::from(format!(
272        "immutables_verification_error-{}.json",
273        date.timestamp()
274    ));
275    std::fs::write(
276        &file_path,
277        serde_json::to_string_pretty(&serde_json::json!({
278        "timestamp": date.to_rfc3339(),
279        "immutables_dir": lists.immutables_dir,
280        "missing-files": lists.missing,
281        "tampered-files": lists.tampered,
282        "non-verifiable-files": lists.non_verifiable,
283        }))
284        .unwrap(),
285    )
286    .expect("Could not write immutables verification error to file");
287    file_path
288}
289
290fn print_immutables_range_to_verify(
291    cardano_db_message: &CardanoDatabaseSnapshot,
292    immutable_file_range: &ImmutableFileRange,
293    json_output: bool,
294) -> Result<(), anyhow::Error> {
295    let range_to_verify =
296        immutable_file_range.to_range_inclusive(cardano_db_message.beacon.immutable_file_number)?;
297    if json_output {
298        let json = serde_json::json!({
299            "timestamp": Utc::now().to_rfc3339(),
300            "local_immutable_range_to_verify": {
301                "start": range_to_verify.start(),
302                "end": range_to_verify.end(),
303            },
304        });
305        println!("{json}");
306    } else {
307        eprintln!(
308            "Verifying local immutable files from number {} to {}",
309            range_to_verify.start(),
310            range_to_verify.end()
311        );
312    }
313    Ok(())
314}
315
316impl ConfigSource for CardanoDbVerifyCommand {
317    fn collect(&self) -> Result<HashMap<String, String>, ConfigError> {
318        let mut map = HashMap::new();
319
320        if let Some(download_dir) = self.db_dir.clone() {
321            let param = "db_dir".to_string();
322            map.insert(
323                param.clone(),
324                utils::path_to_string(&download_dir)
325                    .map_err(|e| ConfigError::Conversion(param, e))?,
326            );
327        }
328
329        if let Some(genesis_verification_key) = self.genesis_verification_key.clone() {
330            map.insert(
331                "genesis_verification_key".to_string(),
332                genesis_verification_key,
333            );
334        }
335
336        Ok(map)
337    }
338}