mithril_client_cli/commands/cardano_db/
verify.rs1use 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#[derive(Parser, Debug, Clone)]
32pub struct CardanoDbVerifyCommand {
33 #[arg(short, long, value_enum, default_value_t = CardanoDbCommandsBackend::V2)]
35 backend: CardanoDbCommandsBackend,
36
37 digest: String,
41
42 #[clap(long)]
44 db_dir: Option<PathBuf>,
45
46 #[clap(long, env = "GENESIS_VERIFICATION_KEY")]
48 genesis_verification_key: Option<String>,
49
50 #[clap(long)]
54 start: Option<ImmutableFileNumber>,
55
56 #[clap(long)]
60 end: Option<ImmutableFileNumber>,
61
62 #[clap(long)]
64 allow_missing: bool,
65}
66
67impl CardanoDbVerifyCommand {
68 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}