openmina_archive_breadcrumb_compare/
main.rs1use mina_p2p_messages::v2::ArchiveTransitionFrontierDiff;
2use std::{collections::HashSet, path::PathBuf};
3
4use anyhow::Result;
5use clap::Parser;
6use serde::{Deserialize, Serialize};
7use tokio::time::{interval, timeout, Duration};
8
9#[derive(Parser, Debug)]
10#[command(author, version, about, long_about = None)]
11struct Args {
12 #[arg(env = "OCAML_NODE_GRAPHQL")]
14 ocaml_node_graphql: Option<String>,
15
16 #[arg(env = "OCAML_NODE_DIR", required = true)]
18 ocaml_node_dir: PathBuf,
19
20 #[arg(env = "OPENMINA_NODE_GRAPHQL")]
22 openmina_node_graphql: Option<String>,
23
24 #[arg(env = "OPENMINA_NODE_DIR", required = true)]
26 openmina_node_dir: PathBuf,
27
28 #[arg(long)]
30 check_missing: bool,
31}
32
33#[derive(Serialize)]
34struct GraphQLQuery {
35 query: String,
36}
37
38#[derive(Deserialize, Debug)]
39struct SyncStatusResponse {
40 data: SyncStatusData,
41}
42
43#[derive(Deserialize, Debug)]
44#[serde(rename_all = "camelCase")]
45struct SyncStatusData {
46 sync_status: String,
47}
48
49#[derive(Deserialize, Debug)]
50#[serde(rename_all = "camelCase")]
51struct BlockInfo {
52 state_hash: String,
53}
54
55#[derive(Deserialize, Debug)]
56struct BestChainResponse {
57 data: BestChainData,
58}
59
60#[derive(Deserialize, Debug)]
61#[serde(rename_all = "camelCase")]
62struct BestChainData {
63 best_chain: Vec<BlockInfo>,
64}
65
66async fn check_sync_status(endpoint: &str) -> Result<String> {
67 let client = reqwest::Client::new();
68
69 let query = GraphQLQuery {
70 query: "query MyQuery { syncStatus }".to_string(),
71 };
72
73 let response = client
74 .post(endpoint)
75 .json(&query)
76 .send()
77 .await?
78 .json::<SyncStatusResponse>()
79 .await?;
80
81 Ok(response.data.sync_status)
82}
83
84async fn get_best_chain(endpoint: &str) -> Result<Vec<String>> {
85 let client = reqwest::Client::new();
86
87 let query = GraphQLQuery {
88 query: "query MyQuery { bestChain(maxLength: 290) { stateHash } }".to_string(),
89 };
90
91 let response = client
92 .post(endpoint)
93 .json(&query)
94 .send()
95 .await?
96 .json::<BestChainResponse>()
97 .await?;
98
99 Ok(response
100 .data
101 .best_chain
102 .into_iter()
103 .map(|block| block.state_hash)
104 .collect())
105}
106
107async fn wait_for_sync(endpoint: &str, node_name: &str) -> Result<()> {
108 const TIMEOUT_DURATION: Duration = Duration::from_secs(300); const CHECK_INTERVAL: Duration = Duration::from_secs(5);
110
111 let sync_check = async {
112 let mut interval = interval(CHECK_INTERVAL);
113
114 loop {
115 interval.tick().await;
116
117 let status = check_sync_status(endpoint).await?;
118 println!("{} sync status: {}", node_name, status);
119
120 if status == "SYNCED" {
121 return Ok(());
122 }
123
124 println!("Waiting for {} to sync...", node_name);
125 }
126 };
127
128 timeout(TIMEOUT_DURATION, sync_check).await.map_err(|_| {
129 anyhow::anyhow!(
130 "Timeout waiting for {} to sync after {:?}",
131 node_name,
132 TIMEOUT_DURATION
133 )
134 })?
135}
136
137async fn compare_chains(ocaml_endpoint: &str, openmina_endpoint: &str) -> Result<Vec<String>> {
138 const MAX_RETRIES: u32 = 3;
139 const RETRY_INTERVAL: Duration = Duration::from_secs(5);
140 let mut interval = interval(RETRY_INTERVAL);
141
142 for attempt in 1..=MAX_RETRIES {
143 println!(
144 "\nAttempting chain comparison (attempt {}/{})",
145 attempt, MAX_RETRIES
146 );
147
148 let ocaml_chain = get_best_chain(ocaml_endpoint).await?;
149 let openmina_chain = get_best_chain(openmina_endpoint).await?;
150
151 println!("Chain comparison:");
152 println!("OCaml chain length: {}", ocaml_chain.len());
153 println!("Openmina chain length: {}", openmina_chain.len());
154
155 if let Err(e) = compare_chain_data(&ocaml_chain, &openmina_chain) {
157 if attempt == MAX_RETRIES {
158 return Err(e);
159 }
160 println!("Comparison failed: {}. Retrying in 5s...", e);
161 interval.tick().await;
162 continue;
163 }
164
165 println!("✅ Chains match perfectly!");
166 return Ok(ocaml_chain);
167 }
168
169 unreachable!()
170}
171
172fn compare_chain_data(ocaml_chain: &[String], openmina_chain: &[String]) -> Result<()> {
173 if ocaml_chain.len() != openmina_chain.len() {
174 anyhow::bail!(
175 "Chain lengths don't match! OCaml: {}, Openmina: {}",
176 ocaml_chain.len(),
177 openmina_chain.len()
178 );
179 }
180
181 for (i, (ocaml_hash, openmina_hash)) in
182 ocaml_chain.iter().zip(openmina_chain.iter()).enumerate()
183 {
184 if ocaml_hash != openmina_hash {
185 anyhow::bail!(
186 "Chain mismatch at position {}: \nOCaml: {}\nOpenmina: {}",
187 i,
188 ocaml_hash,
189 openmina_hash
190 );
191 }
192 }
193
194 Ok(())
195}
196
197#[derive(Debug)]
198struct DiffMismatch {
199 state_hash: String,
200 reason: String,
201}
202
203async fn compare_binary_diffs(
204 ocaml_dir: PathBuf,
205 openmina_dir: PathBuf,
206 state_hashes: &[String],
207) -> Result<Vec<DiffMismatch>> {
208 let mut mismatches = Vec::new();
209
210 if state_hashes.is_empty() {
211 println!("No state hashes provided, comparing all diffs");
212 let files = openmina_dir.read_dir()?;
213 files.for_each(|file| {
214 let file = file.unwrap();
215 let file_name = file.file_name();
216 let file_name_str = file_name.to_str().unwrap();
217 let ocaml_path = ocaml_dir.join(file_name_str);
218 let openmina_path = openmina_dir.join(file_name_str);
219
220 let ocaml_diff = match load_and_deserialize(&ocaml_path) {
222 Ok(diff) => diff,
223 Err(e) => {
224 mismatches.push(DiffMismatch {
225 state_hash: file_name_str.to_string(),
226 reason: format!("Failed to load OCaml diff: {}", e),
227 });
228 return;
229 }
230 };
231
232 let openmina_diff = match load_and_deserialize(&openmina_path) {
233 Ok(diff) => diff,
234 Err(e) => {
235 mismatches.push(DiffMismatch {
236 state_hash: file_name_str.to_string(),
237 reason: format!("Failed to load Openmina diff: {}", e),
238 });
239 return;
240 }
241 };
242
243 if let Some(reason) = compare_diffs(&ocaml_diff, &openmina_diff) {
245 mismatches.push(DiffMismatch {
246 state_hash: file_name_str.to_string(),
247 reason,
248 });
249 }
250 });
251 Ok(mismatches)
252 } else {
253 for state_hash in state_hashes {
254 let ocaml_path = ocaml_dir.join(format!("{}.bin", state_hash));
255 let openmina_path = openmina_dir.join(format!("{}.bin", state_hash));
256
257 let ocaml_diff = match load_and_deserialize(&ocaml_path) {
259 Ok(diff) => diff,
260 Err(e) => {
261 mismatches.push(DiffMismatch {
262 state_hash: state_hash.clone(),
263 reason: format!("Failed to load OCaml diff: {}", e),
264 });
265 continue;
266 }
267 };
268
269 let openmina_diff = match load_and_deserialize(&openmina_path) {
270 Ok(diff) => diff,
271 Err(e) => {
272 mismatches.push(DiffMismatch {
273 state_hash: state_hash.clone(),
274 reason: format!("Failed to load Openmina diff: {}", e),
275 });
276 continue;
277 }
278 };
279
280 if let Some(reason) = compare_diffs(&ocaml_diff, &openmina_diff) {
282 mismatches.push(DiffMismatch {
283 state_hash: state_hash.clone(),
284 reason,
285 });
286 }
287 }
288 Ok(mismatches)
289 }
290}
291
292fn load_and_deserialize(path: &PathBuf) -> Result<ArchiveTransitionFrontierDiff> {
293 let data = std::fs::read(path)?;
294 let diff = binprot::BinProtRead::binprot_read(&mut data.as_slice())?;
295 Ok(diff)
296}
297
298fn compare_diffs(
299 ocaml: &ArchiveTransitionFrontierDiff,
300 openmina: &ArchiveTransitionFrontierDiff,
301) -> Option<String> {
302 match (ocaml, openmina) {
303 (
304 ArchiveTransitionFrontierDiff::BreadcrumbAdded {
305 block: (b1, (body_hash1, state_hash1)),
306 accounts_accessed: a1,
307 accounts_created: c1,
308 tokens_used: t1,
309 sender_receipt_chains_from_parent_ledger: s1,
310 },
311 ArchiveTransitionFrontierDiff::BreadcrumbAdded {
312 block: (b2, (body_hash2, state_hash2)),
313 accounts_accessed: a2,
314 accounts_created: c2,
315 tokens_used: t2,
316 sender_receipt_chains_from_parent_ledger: s2,
317 },
318 ) => {
319 let mut mismatches = Vec::new();
320
321 if body_hash1 != body_hash2 {
322 if body_hash1.is_some() {
323 mismatches.push(format!(
324 "Body hash mismatch:\nOCaml: {:?}\nOpenmina: {:?}",
325 body_hash1, body_hash2
326 ));
327 }
328 } else if state_hash1 != state_hash2 {
329 mismatches.push(format!(
330 "State hash mismatch:\nOCaml: {}\nOpenmina: {}",
331 state_hash1, state_hash2
332 ));
333 } else if b1.header.protocol_state_proof != b2.header.protocol_state_proof {
334 let mut b1_with_b2_proof = b1.clone();
338 b1_with_b2_proof.header.protocol_state_proof =
339 b2.header.protocol_state_proof.clone();
340
341 if &b1_with_b2_proof != b2 {
342 let ocaml_json =
343 serde_json::to_string_pretty(&serde_json::to_value(b1).unwrap()).unwrap();
344 let openmina_json =
345 serde_json::to_string_pretty(&serde_json::to_value(b2).unwrap()).unwrap();
346 mismatches.push(format!(
347 "Block data mismatch:\nOCaml:\n{}\nOpenmina:\n{}",
348 ocaml_json, openmina_json
349 ));
350 }
351 } else if b1 != b2 {
352 let ocaml_json =
353 serde_json::to_string_pretty(&serde_json::to_value(b1).unwrap()).unwrap();
354 let openmina_json =
355 serde_json::to_string_pretty(&serde_json::to_value(b2).unwrap()).unwrap();
356 mismatches.push(format!(
357 "Block data mismatch:\nOCaml:\n{}\nOpenmina:\n{}",
358 ocaml_json, openmina_json
359 ));
360 }
361
362 if a1 != a2 {
363 let ids_ocaml = a1.iter().map(|(id, _)| id.as_u64()).collect::<HashSet<_>>();
364 let ids_openmina = a2.iter().map(|(id, _)| id.as_u64()).collect::<HashSet<_>>();
365
366 let missing_in_openmina: Vec<_> = ids_ocaml.difference(&ids_openmina).collect();
368 let extra_in_openmina: Vec<_> = ids_openmina.difference(&ids_ocaml).collect();
370
371 if !missing_in_openmina.is_empty() {
372 println!("Missing in Openmina: {:?}", missing_in_openmina);
373 }
374 if !extra_in_openmina.is_empty() {
375 println!("Extra in Openmina: {:?}", extra_in_openmina);
376 }
377
378 let ocaml_json =
379 serde_json::to_string_pretty(&serde_json::to_value(a1).unwrap()).unwrap();
380 let openmina_json =
381 serde_json::to_string_pretty(&serde_json::to_value(a2).unwrap()).unwrap();
382 mismatches.push(format!(
383 "Accounts accessed mismatch:\nOCaml:\n{}\nOpenmina:\n{}",
384 ocaml_json, openmina_json
385 ));
386 }
387 if c1 != c2 {
388 let ocaml_json =
389 serde_json::to_string_pretty(&serde_json::to_value(c1).unwrap()).unwrap();
390 let openmina_json =
391 serde_json::to_string_pretty(&serde_json::to_value(c2).unwrap()).unwrap();
392 mismatches.push(format!(
393 "Accounts created mismatch:\nOCaml:\n{}\nOpenmina:\n{}",
394 ocaml_json, openmina_json
395 ));
396 }
397 if t1 != t2 {
398 let ocaml_json =
399 serde_json::to_string_pretty(&serde_json::to_value(t1).unwrap()).unwrap();
400 let openmina_json =
401 serde_json::to_string_pretty(&serde_json::to_value(t2).unwrap()).unwrap();
402 mismatches.push(format!(
403 "Tokens used mismatch:\nOCaml:\n{}\nOpenmina:\n{}",
404 ocaml_json, openmina_json
405 ));
406 }
407 if s1 != s2 {
408 let ocaml_json =
409 serde_json::to_string_pretty(&serde_json::to_value(s1).unwrap()).unwrap();
410 let openmina_json =
411 serde_json::to_string_pretty(&serde_json::to_value(s2).unwrap()).unwrap();
412 mismatches.push(format!(
413 "Sender receipt chains mismatch:\nOCaml:\n{}\nOpenmina:\n{}",
414 ocaml_json, openmina_json
415 ));
416 }
417
418 if mismatches.is_empty() {
419 None
420 } else {
421 Some(mismatches.join("\n\n"))
422 }
423 }
424 _ => {
425 let ocaml_json =
426 serde_json::to_string_pretty(&serde_json::to_value(ocaml).unwrap()).unwrap();
427 let openmina_json =
428 serde_json::to_string_pretty(&serde_json::to_value(openmina).unwrap()).unwrap();
429 Some(format!(
430 "Different diff types:\nOCaml:\n{}\nOpenmina:\n{}",
431 ocaml_json, openmina_json
432 ))
433 }
434 }
435}
436
437async fn check_missing_breadcrumbs(
438 openmina_node_dir: PathBuf,
439 openmina_endpoint: &str,
440) -> Result<()> {
441 let files = openmina_node_dir.read_dir()?;
442 let best_chain = get_best_chain(openmina_endpoint).await?;
443 let mut missing_breadcrumbs = Vec::new();
444
445 let file_names = files
446 .map(|file| {
447 file.unwrap()
448 .file_name()
449 .to_str()
450 .unwrap()
451 .to_string()
452 .strip_suffix(".bin")
453 .unwrap()
454 .to_owned()
455 })
456 .collect::<HashSet<String>>();
457
458 for best_chain_hash in best_chain {
459 if !file_names.contains(&best_chain_hash.to_string()) {
460 missing_breadcrumbs.push(best_chain_hash.to_string());
461 }
462 }
463
464 if !missing_breadcrumbs.is_empty() {
465 println!(
466 "❌ Found {} missing breadcrumbs:",
467 missing_breadcrumbs.len()
468 );
469 for missing_breadcrumb in missing_breadcrumbs {
470 println!("{}", missing_breadcrumb);
471 }
472 } else {
473 println!("✅ All breadcrumbs present!");
474 }
475
476 Ok(())
477}
478
479#[tokio::main]
480async fn main() -> Result<()> {
481 let args = Args::parse();
482
483 let mut best_chain = Vec::new();
484
485 println!("Checking for missing breadcrumbs...");
486
487 if args.check_missing {
488 check_missing_breadcrumbs(
489 args.openmina_node_dir,
490 args.openmina_node_graphql.as_deref().unwrap(),
491 )
492 .await?;
493 return Ok(());
494 }
495
496 if let (Some(ocaml_graphql), Some(openmina_graphql)) =
497 (args.ocaml_node_graphql, args.openmina_node_graphql)
498 {
499 println!("Waiting for nodes to sync...");
501 wait_for_sync(&ocaml_graphql, "OCaml Node").await?;
502 wait_for_sync(&openmina_graphql, "Openmina Node").await?;
503 println!("Both nodes are synced! ✅\n");
504 let bc = compare_chains(&ocaml_graphql, &openmina_graphql).await?;
506 println!("Comparing binary diffs for {} blocks...", bc.len());
507 best_chain.extend_from_slice(&bc);
508 } else {
509 println!("No graphql endpoints provided, skipping chain comparison");
510 }
511
512 let mismatches =
513 compare_binary_diffs(args.ocaml_node_dir, args.openmina_node_dir, &best_chain).await?;
514
515 if mismatches.is_empty() {
516 println!("✅ All binary diffs match perfectly!");
517 } else {
518 println!("\n❌ Found {} mismatches:", mismatches.len());
519
520 for (i, mismatch) in mismatches.iter().enumerate() {
527 println!(
528 "\nMismatch #{}: \nState Hash: {}\nReason: {}",
529 i + 1,
530 mismatch.state_hash,
531 mismatch.reason
532 );
533 }
534 anyhow::bail!("Binary diff comparison failed");
535 }
536
537 Ok(())
538}