Skip to main content

mithril_cardano_node_internal_database/entities/
ledger_state_snapshot.rs

1use 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
14/// Walk the given path and return the first directory named "ledger" it finds
15fn 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
24/// Returns true if the `meta` file in the given path exists and contains a JSON object
25/// with the `backend` field set to `"utxohd-mem"`.
26fn 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/// Represent a ledger file in a Cardano node database directory
48#[derive(Debug, PartialEq, Eq, Clone)]
49pub enum LedgerStateSnapshot {
50    /// Snapshot of a legacy ledger state (before UTxO-HD)
51    Legacy {
52        /// The path to the ledger file
53        path: PathBuf,
54        /// The ledger file slot number
55        slot_number: SlotNumber,
56        /// The filename
57        filename: OsString,
58    },
59    /// Snapshot of a UTxO-HD in-memory ledger state (Cardano node up to 10.6)
60    InMemoryUpTo10_6 {
61        /// The path to the ledger file
62        path: PathBuf,
63        /// The ledger file slot number
64        slot_number: SlotNumber,
65        /// Name of the ledger state folder
66        folder_name: OsString,
67    },
68    /// Snapshot of a UTxO-HD in-memory ledger state (Cardano node 10.7 and above)
69    InMemoryFrom10_7 {
70        /// The path to the ledger file
71        path: PathBuf,
72        /// The ledger file slot number
73        slot_number: SlotNumber,
74        /// Name of the ledger state folder
75        folder_name: OsString,
76    },
77}
78
79/// [LedgerStateSnapshot::list_all_in_dir] related errors.
80#[derive(Error, Debug)]
81pub enum LedgerStateSnapshotListingError {
82    /// Raised when the "ledger" folder could not be found in a file structure.
83    #[error("Couldn't find the 'ledger' folder in '{0:?}'")]
84    MissingLedgerFolder(PathBuf),
85}
86
87impl LedgerStateSnapshot {
88    /// Filename of the in-memory ledger snapshot 'meta' file
89    pub const IN_MEMORY_META: &'static str = "meta";
90    /// Filename of the in-memory ledger snapshot 'state' file
91    pub const IN_MEMORY_STATE: &'static str = "state";
92    /// Directory name of the in-memory ledger snapshot 'tables' folder
93    pub const IN_MEMORY_TABLES: &'static str = "tables";
94    /// Filename of the in-memory ledger snapshot 'tables/tvar' file
95    pub const IN_MEMORY_TVAR: &'static str = "tvar";
96
97    /// `LedgerStateSnapshot::Legacy` factory
98    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    /// Convert a path to a [LedgerStateSnapshot] if it satisfies the constraints.
107    ///
108    /// The constraints are:
109    /// - legacy state snapshot: the path must be a file, the filename should only contain a number (no
110    ///   extension).
111    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    /// List all [`LedgerStateSnapshot`] in a given directory.
145    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    /// Return paths to all files that constitute this snapshot
170    ///
171    /// Returned paths are relative to the cardano node database ledger dir
172    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    /// Return the slot number when this snapshot was taken
197    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    // UTxO-HD in-memory rules:
304    // - a folder named after the slot number at which the snapshots are taken (same naming convention as
305    // legacy state snapshot)
306    // - contains three files, with one in a subfolder:
307    //   - "/meta"
308    //   - "/state"
309    //   - "/tables/tvar" (Cardano node up to 10.6) or "/tables" as a file (Cardano node 10.7+)
310    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}