mithril_cardano_node_internal_database/entities/
ledger_state_snapshot.rs1use std::{
2 cmp::Ordering,
3 ffi::OsString,
4 fs,
5 path::{Path, PathBuf},
6};
7use thiserror::Error;
8use walkdir::WalkDir;
9
10use mithril_common::{StdResult, entities::SlotNumber};
11
12use crate::LEDGER_DIR;
13
14fn find_ledger_dir(path_to_walk: &Path) -> Option<PathBuf> {
16 WalkDir::new(path_to_walk)
17 .into_iter()
18 .filter_entry(|e| e.file_type().is_dir())
19 .filter_map(|e| e.ok())
20 .find(|f| f.file_name() == LEDGER_DIR)
21 .map(|e| e.into_path())
22}
23
24fn has_in_memory_meta_backend(path: &Path) -> StdResult<bool> {
27 let meta_content = fs::read_to_string(path.join(LedgerStateSnapshot::IN_MEMORY_META))?;
28 let meta_json = serde_json::from_str::<serde_json::Value>(&meta_content)?;
29
30 Ok(meta_json.get("backend").and_then(|v| v.as_str()) == Some("utxohd-mem"))
31}
32
33fn is_ledger_state_snapshot(path: &Path) -> bool {
34 if path.is_dir() {
35 has_in_memory_meta_backend(path).unwrap_or(false)
36 && path.join(LedgerStateSnapshot::IN_MEMORY_STATE).is_file()
37 && (path
38 .join(LedgerStateSnapshot::IN_MEMORY_TABLES)
39 .join(LedgerStateSnapshot::IN_MEMORY_TVAR)
40 .is_file()
41 || path.join(LedgerStateSnapshot::IN_MEMORY_TABLES).is_file())
42 } else {
43 path.is_file()
44 }
45}
46
47#[derive(Debug, PartialEq, Eq, Clone)]
49pub enum LedgerStateSnapshot {
50 Legacy {
52 path: PathBuf,
54 slot_number: SlotNumber,
56 filename: OsString,
58 },
59 InMemoryUpTo10_6 {
61 path: PathBuf,
63 slot_number: SlotNumber,
65 folder_name: OsString,
67 },
68 InMemoryFrom10_7 {
70 path: PathBuf,
72 slot_number: SlotNumber,
74 folder_name: OsString,
76 },
77}
78
79#[derive(Error, Debug)]
81pub enum LedgerStateSnapshotListingError {
82 #[error("Couldn't find the 'ledger' folder in '{0:?}'")]
84 MissingLedgerFolder(PathBuf),
85}
86
87impl LedgerStateSnapshot {
88 pub const IN_MEMORY_META: &'static str = "meta";
90 pub const IN_MEMORY_STATE: &'static str = "state";
92 pub const IN_MEMORY_TABLES: &'static str = "tables";
94 pub const IN_MEMORY_TVAR: &'static str = "tvar";
96
97 pub fn legacy(path: PathBuf, slot_number: SlotNumber, filename: OsString) -> Self {
99 Self::Legacy {
100 path,
101 slot_number,
102 filename,
103 }
104 }
105
106 pub fn from_path(path: &Path) -> Option<LedgerStateSnapshot> {
112 path.file_name().and_then(|filename| {
113 filename
114 .to_string_lossy()
115 .parse::<u64>()
116 .map(|number| {
117 if path.is_dir() {
118 let tables_path = path.join(Self::IN_MEMORY_TABLES);
119 if tables_path.is_file() {
120 Self::InMemoryFrom10_7 {
121 path: path.to_path_buf(),
122 slot_number: SlotNumber(number),
123 folder_name: filename.to_os_string(),
124 }
125 } else {
126 Self::InMemoryUpTo10_6 {
127 path: path.to_path_buf(),
128 slot_number: SlotNumber(number),
129 folder_name: filename.to_os_string(),
130 }
131 }
132 } else {
133 Self::legacy(
134 path.to_path_buf(),
135 SlotNumber(number),
136 filename.to_os_string(),
137 )
138 }
139 })
140 .ok()
141 })
142 }
143
144 pub fn list_all_in_dir(
146 dir: &Path,
147 ) -> Result<Vec<LedgerStateSnapshot>, LedgerStateSnapshotListingError> {
148 let ledger_dir = find_ledger_dir(dir).ok_or(
149 LedgerStateSnapshotListingError::MissingLedgerFolder(dir.to_path_buf()),
150 )?;
151 let mut files: Vec<LedgerStateSnapshot> = vec![];
152
153 for path in WalkDir::new(ledger_dir)
154 .min_depth(1)
155 .max_depth(1)
156 .into_iter()
157 .filter_entry(|e| is_ledger_state_snapshot(e.path()))
158 .filter_map(|file| file.ok())
159 {
160 if let Some(ledger_file) = LedgerStateSnapshot::from_path(path.path()) {
161 files.push(ledger_file);
162 }
163 }
164 files.sort();
165
166 Ok(files)
167 }
168
169 pub fn get_files_relative_path(&self) -> Vec<PathBuf> {
173 match self {
174 LedgerStateSnapshot::Legacy { filename, .. } => {
175 vec![PathBuf::from(filename)]
176 }
177 LedgerStateSnapshot::InMemoryUpTo10_6 { folder_name, .. } => {
178 vec![
179 PathBuf::from(folder_name).join(Self::IN_MEMORY_META),
180 PathBuf::from(folder_name).join(Self::IN_MEMORY_STATE),
181 PathBuf::from(folder_name)
182 .join(Self::IN_MEMORY_TABLES)
183 .join(Self::IN_MEMORY_TVAR),
184 ]
185 }
186 LedgerStateSnapshot::InMemoryFrom10_7 { folder_name, .. } => {
187 vec![
188 PathBuf::from(folder_name).join(Self::IN_MEMORY_META),
189 PathBuf::from(folder_name).join(Self::IN_MEMORY_STATE),
190 PathBuf::from(folder_name).join(Self::IN_MEMORY_TABLES),
191 ]
192 }
193 }
194 }
195
196 pub fn slot_number(&self) -> SlotNumber {
198 match self {
199 LedgerStateSnapshot::Legacy { slot_number, .. }
200 | LedgerStateSnapshot::InMemoryUpTo10_6 { slot_number, .. }
201 | LedgerStateSnapshot::InMemoryFrom10_7 { slot_number, .. } => *slot_number,
202 }
203 }
204}
205
206impl PartialOrd for LedgerStateSnapshot {
207 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
208 Some(self.cmp(other))
209 }
210}
211
212impl Ord for LedgerStateSnapshot {
213 fn cmp(&self, other: &Self) -> Ordering {
214 self.slot_number().cmp(&other.slot_number())
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use std::fs::{File, create_dir};
221 use std::io::prelude::*;
222
223 use mithril_common::test::temp_dir_create;
224
225 use super::*;
226
227 fn create_ledger_dir(parent_dir: &Path) -> PathBuf {
228 let ledger_dir = parent_dir.join(LEDGER_DIR);
229 create_dir(&ledger_dir).unwrap();
230 ledger_dir
231 }
232
233 fn create_fake_files(parent_dir: &Path, child_filenames: &[&str]) {
234 for filename in child_filenames {
235 let file = parent_dir.join(Path::new(filename));
236 let mut source_file = File::create(file).unwrap();
237 write!(source_file, "This is a test file named '{filename}'").unwrap();
238 }
239 }
240
241 fn create_in_memory_meta_file(dir: &Path) {
242 let mut f = File::create(dir.join(LedgerStateSnapshot::IN_MEMORY_META)).unwrap();
243 write!(f, r#"{{"backend": "utxohd-mem"}}"#).unwrap();
244 }
245
246 fn extract_filenames(ledger_files: &[LedgerStateSnapshot]) -> Vec<String> {
247 ledger_files
248 .iter()
249 .flat_map(|i| i.get_files_relative_path())
250 .map(|p| p.file_name().unwrap().to_string_lossy().to_string())
251 .collect()
252 }
253
254 #[test]
255 fn list_all_ledger_file_fail_if_not_in_ledger_dir() {
256 let target_dir = temp_dir_create!();
257
258 LedgerStateSnapshot::list_all_in_dir(&target_dir)
259 .expect_err("LedgerStateSnapshot::list_all_in_dir should have Failed");
260 }
261
262 #[test]
263 fn list_all_ledger_file_should_works_in_a_empty_folder() {
264 let target_dir = temp_dir_create!();
265 create_ledger_dir(&target_dir);
266 let result = LedgerStateSnapshot::list_all_in_dir(&target_dir)
267 .expect("LedgerStateSnapshot::list_all_in_dir should work in a empty folder");
268
269 assert_eq!(Vec::<LedgerStateSnapshot>::new(), result);
270 }
271
272 mod legacy_ledger_state {
273 use super::*;
274
275 #[test]
276 fn list_all_ledger_file_order_should_be_deterministic() {
277 let target_dir = temp_dir_create!();
278 let ledger_dir = create_ledger_dir(&target_dir);
279 let entries = vec!["424", "123", "124", "00125", "21", "223", "0423"];
280 create_fake_files(&ledger_dir, &entries);
281 let ledger_files = LedgerStateSnapshot::list_all_in_dir(&target_dir)
282 .expect("LedgerStateSnapshot::list_all_in_dir Failed");
283
284 assert_eq!(
285 vec!["21", "123", "124", "00125", "223", "0423", "424"],
286 extract_filenames(&ledger_files)
287 );
288 }
289
290 #[test]
291 fn list_all_ledger_file_should_work_with_non_ledger_files() {
292 let target_dir = temp_dir_create!();
293 let ledger_dir = create_ledger_dir(&target_dir);
294 let entries = vec!["123", "124", "README.md", "124.back"];
295 create_fake_files(&ledger_dir, &entries);
296 let ledger_files = LedgerStateSnapshot::list_all_in_dir(&target_dir)
297 .expect("LedgerStateSnapshot::list_all_in_dir Failed");
298
299 assert_eq!(vec!["123", "124"], extract_filenames(&ledger_files));
300 }
301 }
302
303 mod utxo_hd_in_memory_ledger_state {
311 use std::fs::create_dir_all;
312
313 use super::*;
314
315 #[test]
316 fn list_all_ledger_state_should_not_include_utxo_hd_folder_that_does_not_contains_meta_state_or_tvar_files()
317 {
318 let target_dir = temp_dir_create!();
319 let ledger_dir = create_ledger_dir(&target_dir);
320
321 let ledger_empty_dir = ledger_dir.join("000");
322 create_dir(&ledger_empty_dir).unwrap();
323
324 let ledger_with_missing_meta_files = ledger_dir.join("100");
325 create_dir_all(
326 ledger_with_missing_meta_files.join(LedgerStateSnapshot::IN_MEMORY_TABLES),
327 )
328 .unwrap();
329 create_fake_files(
330 &ledger_with_missing_meta_files,
331 &[LedgerStateSnapshot::IN_MEMORY_STATE],
332 );
333 create_fake_files(
334 &ledger_with_missing_meta_files.join(LedgerStateSnapshot::IN_MEMORY_TABLES),
335 &[LedgerStateSnapshot::IN_MEMORY_TVAR],
336 );
337
338 let ledger_with_missing_state_files = ledger_dir.join("200");
339 create_dir_all(
340 ledger_with_missing_state_files.join(LedgerStateSnapshot::IN_MEMORY_TABLES),
341 )
342 .unwrap();
343 create_in_memory_meta_file(&ledger_with_missing_state_files);
344 create_fake_files(
345 &ledger_with_missing_state_files.join(LedgerStateSnapshot::IN_MEMORY_TABLES),
346 &[LedgerStateSnapshot::IN_MEMORY_TVAR],
347 );
348
349 let ledger_with_missing_tvar_files = ledger_dir.join("300");
350 create_dir_all(
351 ledger_with_missing_tvar_files.join(LedgerStateSnapshot::IN_MEMORY_TABLES),
352 )
353 .unwrap();
354 create_in_memory_meta_file(&ledger_with_missing_tvar_files);
355 create_fake_files(
356 &ledger_with_missing_tvar_files,
357 &[LedgerStateSnapshot::IN_MEMORY_STATE],
358 );
359
360 let ledger_with_missing_table_folder = ledger_dir.join("400");
361 create_dir(&ledger_with_missing_table_folder).unwrap();
362 create_in_memory_meta_file(&ledger_with_missing_table_folder);
363 create_fake_files(
364 &ledger_with_missing_table_folder,
365 &[LedgerStateSnapshot::IN_MEMORY_STATE],
366 );
367
368 let result = LedgerStateSnapshot::list_all_in_dir(&target_dir).unwrap();
369
370 assert_eq!(Vec::<LedgerStateSnapshot>::new(), result);
371 }
372
373 #[test]
374 fn list_all_ledger_state_should_not_include_utxo_hd_folder_with_wrong_meta_backend() {
375 let target_dir = temp_dir_create!();
376 let ledger_dir = create_ledger_dir(&target_dir);
377
378 let ledger_with_wrong_backend = ledger_dir.join("100");
379 create_dir_all(ledger_with_wrong_backend.join(LedgerStateSnapshot::IN_MEMORY_TABLES))
380 .unwrap();
381 let mut meta =
382 File::create(ledger_with_wrong_backend.join(LedgerStateSnapshot::IN_MEMORY_META))
383 .unwrap();
384 write!(meta, r#"{{"backend": "utxohd-lmdb"}}"#).unwrap();
385 create_fake_files(
386 &ledger_with_wrong_backend,
387 &[LedgerStateSnapshot::IN_MEMORY_STATE],
388 );
389 create_fake_files(
390 &ledger_with_wrong_backend.join(LedgerStateSnapshot::IN_MEMORY_TABLES),
391 &[LedgerStateSnapshot::IN_MEMORY_TVAR],
392 );
393
394 let result = LedgerStateSnapshot::list_all_in_dir(&target_dir).unwrap();
395
396 assert_eq!(Vec::<LedgerStateSnapshot>::new(), result);
397 }
398
399 #[test]
400 fn list_all_ledger_state_should_not_include_utxo_hd_folder_with_non_json_meta() {
401 let target_dir = temp_dir_create!();
402 let ledger_dir = create_ledger_dir(&target_dir);
403
404 let ledger_with_non_json_meta = ledger_dir.join("200");
405 create_dir_all(ledger_with_non_json_meta.join(LedgerStateSnapshot::IN_MEMORY_TABLES))
406 .unwrap();
407 create_fake_files(
408 &ledger_with_non_json_meta,
409 &[
410 LedgerStateSnapshot::IN_MEMORY_META,
411 LedgerStateSnapshot::IN_MEMORY_STATE,
412 ],
413 );
414 create_fake_files(
415 &ledger_with_non_json_meta.join(LedgerStateSnapshot::IN_MEMORY_TABLES),
416 &[LedgerStateSnapshot::IN_MEMORY_TVAR],
417 );
418
419 let result = LedgerStateSnapshot::list_all_in_dir(&target_dir).unwrap();
420
421 assert!(result.is_empty());
422 }
423
424 #[test]
425 fn list_all_ledger_state_with_valid_up_to_10_6_folder_structure() {
426 let target_dir = temp_dir_create!();
427 let ledger_dir = create_ledger_dir(&target_dir);
428
429 let ledger_state = ledger_dir.join("200");
430 create_dir_all(ledger_state.join(LedgerStateSnapshot::IN_MEMORY_TABLES)).unwrap();
431 create_in_memory_meta_file(&ledger_state);
432 create_fake_files(&ledger_state, &[LedgerStateSnapshot::IN_MEMORY_STATE]);
433 create_fake_files(
434 &ledger_state.join(LedgerStateSnapshot::IN_MEMORY_TABLES),
435 &[LedgerStateSnapshot::IN_MEMORY_TVAR],
436 );
437
438 let result = LedgerStateSnapshot::list_all_in_dir(&target_dir).unwrap();
439
440 assert_eq!(
441 vec![LedgerStateSnapshot::InMemoryUpTo10_6 {
442 path: ledger_state,
443 slot_number: SlotNumber(200),
444 folder_name: "200".into(),
445 }],
446 result
447 );
448 }
449
450 #[test]
451 fn list_all_ledger_state_with_valid_from_10_7_folder_structure() {
452 let target_dir = temp_dir_create!();
453 let ledger_dir = create_ledger_dir(&target_dir);
454
455 let ledger_state = ledger_dir.join("200");
456 create_dir(&ledger_state).unwrap();
457 create_in_memory_meta_file(&ledger_state);
458 create_fake_files(
459 &ledger_state,
460 &[
461 LedgerStateSnapshot::IN_MEMORY_STATE,
462 LedgerStateSnapshot::IN_MEMORY_TABLES,
463 ],
464 );
465
466 let result = LedgerStateSnapshot::list_all_in_dir(&target_dir).unwrap();
467
468 assert_eq!(
469 vec![LedgerStateSnapshot::InMemoryFrom10_7 {
470 path: ledger_state,
471 slot_number: SlotNumber(200),
472 folder_name: "200".into(),
473 }],
474 result
475 );
476 }
477
478 #[test]
479 fn get_relative_path_for_up_to_10_6_lists_meta_state_and_tvar() {
480 let ledger_state = LedgerStateSnapshot::InMemoryUpTo10_6 {
481 path: PathBuf::from("/tmp/050"),
482 slot_number: SlotNumber(50),
483 folder_name: "050".into(),
484 };
485
486 assert_eq!(
487 vec![
488 PathBuf::from("050").join(LedgerStateSnapshot::IN_MEMORY_META),
489 PathBuf::from("050").join(LedgerStateSnapshot::IN_MEMORY_STATE),
490 PathBuf::from("050")
491 .join(LedgerStateSnapshot::IN_MEMORY_TABLES)
492 .join(LedgerStateSnapshot::IN_MEMORY_TVAR)
493 ],
494 ledger_state.get_files_relative_path(),
495 )
496 }
497
498 #[test]
499 fn get_relative_path_for_from_10_7_lists_meta_state_and_tables_file() {
500 let ledger_state = LedgerStateSnapshot::InMemoryFrom10_7 {
501 path: PathBuf::from("/tmp/050"),
502 slot_number: SlotNumber(50),
503 folder_name: "050".into(),
504 };
505
506 assert_eq!(
507 vec![
508 PathBuf::from("050").join(LedgerStateSnapshot::IN_MEMORY_META),
509 PathBuf::from("050").join(LedgerStateSnapshot::IN_MEMORY_STATE),
510 PathBuf::from("050").join(LedgerStateSnapshot::IN_MEMORY_TABLES),
511 ],
512 ledger_state.get_files_relative_path(),
513 )
514 }
515 }
516}