mithril_cardano_node_internal_database/entities/
ledger_state_snapshot.rs

1use std::{
2    cmp::Ordering,
3    ffi::OsString,
4    path::{Path, PathBuf},
5};
6use thiserror::Error;
7use walkdir::WalkDir;
8
9use mithril_common::entities::SlotNumber;
10
11use crate::LEDGER_DIR;
12
13/// Walk the given path and return the first directory named "ledger" it finds
14fn find_ledger_dir(path_to_walk: &Path) -> Option<PathBuf> {
15    WalkDir::new(path_to_walk)
16        .into_iter()
17        .filter_entry(|e| e.file_type().is_dir())
18        .filter_map(|e| e.ok())
19        .find(|f| f.file_name() == LEDGER_DIR)
20        .map(|e| e.into_path())
21}
22
23fn is_ledger_state_snapshot(path: &Path) -> bool {
24    if path.is_dir() {
25        path.join(LedgerStateSnapshot::IN_MEMORY_META).exists()
26            && path.join(LedgerStateSnapshot::IN_MEMORY_STATE).exists()
27            && path.join(LedgerStateSnapshot::IN_MEMORY_TABLES).exists()
28            && path
29                .join(LedgerStateSnapshot::IN_MEMORY_TABLES)
30                .join(LedgerStateSnapshot::IN_MEMORY_TVAR)
31                .exists()
32    } else {
33        path.is_file()
34    }
35}
36
37/// Represent an ledger file in a Cardano node database directory
38#[derive(Debug, PartialEq, Eq, Clone)]
39pub enum LedgerStateSnapshot {
40    /// Snapshot of a legacy ledger state (before UTxO-HD)
41    Legacy {
42        /// The path to the ledger file
43        path: PathBuf,
44        /// The ledger file slot number
45        slot_number: SlotNumber,
46        /// The filename
47        filename: OsString,
48    },
49    /// Snapshot of an UTxO-HD in-memory ledger state
50    InMemory {
51        /// The path to the ledger file
52        path: PathBuf,
53        /// The ledger file slot number
54        slot_number: SlotNumber,
55        /// Name of the ledger state folder
56        folder_name: OsString,
57    },
58}
59
60/// [LedgerStateSnapshot::list_all_in_dir] related errors.
61#[derive(Error, Debug)]
62pub enum LedgerStateSnapshotListingError {
63    /// Raised when the "ledger" folder could not be found in a file structure.
64    #[error("Couldn't find the 'ledger' folder in '{0:?}'")]
65    MissingLedgerFolder(PathBuf),
66}
67
68impl LedgerStateSnapshot {
69    /// Filename of the in-memory ledger snapshot 'meta' file
70    pub const IN_MEMORY_META: &'static str = "meta";
71    /// Filename of the in-memory ledger snapshot 'state' file
72    pub const IN_MEMORY_STATE: &'static str = "state";
73    /// Directory name of the in-memory ledger snapshot 'tables' folder
74    pub const IN_MEMORY_TABLES: &'static str = "tables";
75    /// Filename of the in-memory ledger snapshot 'tables/tvar' file
76    pub const IN_MEMORY_TVAR: &'static str = "tvar";
77
78    /// `LedgerStateSnapshot::Legacy` factory
79    pub fn legacy(path: PathBuf, slot_number: SlotNumber, filename: OsString) -> Self {
80        Self::Legacy {
81            path,
82            slot_number,
83            filename,
84        }
85    }
86
87    /// `LedgerStateSnapshot::InMemory` factory
88    pub fn in_memory(path: PathBuf, slot_number: SlotNumber, folder_name: OsString) -> Self {
89        Self::InMemory {
90            path,
91            slot_number,
92            folder_name,
93        }
94    }
95
96    /// Convert a path to a [LedgerStateSnapshot] if it satisfies the constraints.
97    ///
98    /// The constraints are:
99    /// - legacy state snapshot: the path must be a file, the filename should only contain a number (no
100    ///   extension).
101    pub fn from_path(path: &Path) -> Option<LedgerStateSnapshot> {
102        path.file_name().and_then(|filename| {
103            filename
104                .to_string_lossy()
105                .parse::<u64>()
106                .map(|number| {
107                    if path.is_dir() {
108                        Self::in_memory(
109                            path.to_path_buf(),
110                            SlotNumber(number),
111                            filename.to_os_string(),
112                        )
113                    } else {
114                        Self::legacy(
115                            path.to_path_buf(),
116                            SlotNumber(number),
117                            filename.to_os_string(),
118                        )
119                    }
120                })
121                .ok()
122        })
123    }
124
125    /// List all [`LedgerStateSnapshot`] in a given directory.
126    pub fn list_all_in_dir(
127        dir: &Path,
128    ) -> Result<Vec<LedgerStateSnapshot>, LedgerStateSnapshotListingError> {
129        let ledger_dir = find_ledger_dir(dir).ok_or(
130            LedgerStateSnapshotListingError::MissingLedgerFolder(dir.to_path_buf()),
131        )?;
132        let mut files: Vec<LedgerStateSnapshot> = vec![];
133
134        for path in WalkDir::new(ledger_dir)
135            .min_depth(1)
136            .max_depth(1)
137            .into_iter()
138            .filter_entry(|e| is_ledger_state_snapshot(e.path()))
139            .filter_map(|file| file.ok())
140        {
141            if let Some(ledger_file) = LedgerStateSnapshot::from_path(path.path()) {
142                files.push(ledger_file);
143            }
144        }
145        files.sort();
146
147        Ok(files)
148    }
149
150    /// Return paths to all files that constitute this snapshot
151    ///
152    /// Returned path are relative to the cardano node database ledger dir
153    pub fn get_files_relative_path(&self) -> Vec<PathBuf> {
154        match self {
155            LedgerStateSnapshot::Legacy { filename, .. } => vec![PathBuf::from(filename)],
156            LedgerStateSnapshot::InMemory { folder_name, .. } => {
157                vec![
158                    PathBuf::from(folder_name).join(Self::IN_MEMORY_META),
159                    PathBuf::from(folder_name).join(Self::IN_MEMORY_STATE),
160                    PathBuf::from(folder_name)
161                        .join(Self::IN_MEMORY_TABLES)
162                        .join(Self::IN_MEMORY_TVAR),
163                ]
164            }
165        }
166    }
167
168    /// Return the slot number when this snapshot was taken
169    pub fn slot_number(&self) -> SlotNumber {
170        match self {
171            LedgerStateSnapshot::Legacy { slot_number, .. }
172            | LedgerStateSnapshot::InMemory { slot_number, .. } => *slot_number,
173        }
174    }
175}
176
177impl PartialOrd for LedgerStateSnapshot {
178    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
179        Some(self.cmp(other))
180    }
181}
182
183impl Ord for LedgerStateSnapshot {
184    fn cmp(&self, other: &Self) -> Ordering {
185        self.slot_number().cmp(&other.slot_number())
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use std::fs::{File, create_dir};
192    use std::io::prelude::*;
193
194    use mithril_common::test_utils::temp_dir_create;
195
196    use super::*;
197
198    fn create_ledger_dir(parent_dir: &Path) -> PathBuf {
199        let ledger_dir = parent_dir.join(LEDGER_DIR);
200        create_dir(&ledger_dir).unwrap();
201        ledger_dir
202    }
203
204    fn create_fake_files(parent_dir: &Path, child_filenames: &[&str]) {
205        for filename in child_filenames {
206            let file = parent_dir.join(Path::new(filename));
207            let mut source_file = File::create(file).unwrap();
208            write!(source_file, "This is a test file named '{filename}'").unwrap();
209        }
210    }
211
212    fn extract_filenames(ledger_files: &[LedgerStateSnapshot]) -> Vec<String> {
213        ledger_files
214            .iter()
215            .flat_map(|i| i.get_files_relative_path())
216            .map(|p| p.file_name().unwrap().to_string_lossy().to_string())
217            .collect()
218    }
219
220    #[test]
221    fn list_all_ledger_file_fail_if_not_in_ledger_dir() {
222        let target_dir = temp_dir_create!();
223
224        LedgerStateSnapshot::list_all_in_dir(&target_dir)
225            .expect_err("LedgerStateSnapshot::list_all_in_dir should have Failed");
226    }
227
228    #[test]
229    fn list_all_ledger_file_should_works_in_a_empty_folder() {
230        let target_dir = temp_dir_create!();
231        create_ledger_dir(&target_dir);
232        let result = LedgerStateSnapshot::list_all_in_dir(&target_dir)
233            .expect("LedgerStateSnapshot::list_all_in_dir should work in a empty folder");
234
235        assert_eq!(Vec::<LedgerStateSnapshot>::new(), result);
236    }
237
238    mod legacy_ledger_state {
239        use super::*;
240
241        #[test]
242        fn list_all_ledger_file_order_should_be_deterministic() {
243            let target_dir = temp_dir_create!();
244            let ledger_dir = create_ledger_dir(&target_dir);
245            let entries = vec!["424", "123", "124", "00125", "21", "223", "0423"];
246            create_fake_files(&ledger_dir, &entries);
247            let ledger_files = LedgerStateSnapshot::list_all_in_dir(&target_dir)
248                .expect("LedgerStateSnapshot::list_all_in_dir Failed");
249
250            assert_eq!(
251                vec!["21", "123", "124", "00125", "223", "0423", "424"],
252                extract_filenames(&ledger_files)
253            );
254        }
255
256        #[test]
257        fn list_all_ledger_file_should_work_with_non_ledger_files() {
258            let target_dir = temp_dir_create!();
259            let ledger_dir = create_ledger_dir(&target_dir);
260            let entries = vec!["123", "124", "README.md", "124.back"];
261            create_fake_files(&ledger_dir, &entries);
262            let ledger_files = LedgerStateSnapshot::list_all_in_dir(&target_dir)
263                .expect("LedgerStateSnapshot::list_all_in_dir Failed");
264
265            assert_eq!(vec!["123", "124"], extract_filenames(&ledger_files));
266        }
267    }
268
269    // UTxO-HD in-memory rules:
270    // - a folder named after the slot number at which the snapshots are taken (same naming convention as
271    // legacy state snapshot)
272    // - contains three files, with one in a subfolder:
273    //   - "/meta"
274    //   - "/state"
275    //   - "/tables/tvar"
276    mod utxo_hd_in_memory_ledger_state {
277        use std::fs::create_dir_all;
278
279        use super::*;
280
281        #[test]
282        fn list_all_ledger_state_should_not_include_utxo_hd_folder_that_does_not_contains_meta_state_or_tvar_files()
283         {
284            let target_dir = temp_dir_create!();
285            let ledger_dir = create_ledger_dir(&target_dir);
286
287            let ledger_empty_dir = ledger_dir.join("000");
288            create_dir(&ledger_empty_dir).unwrap();
289
290            let ledger_with_missing_meta_files = ledger_dir.join("100");
291            create_dir_all(
292                ledger_with_missing_meta_files.join(LedgerStateSnapshot::IN_MEMORY_TABLES),
293            )
294            .unwrap();
295            create_fake_files(
296                &ledger_with_missing_meta_files,
297                &[LedgerStateSnapshot::IN_MEMORY_STATE],
298            );
299            create_fake_files(
300                &ledger_with_missing_meta_files.join(LedgerStateSnapshot::IN_MEMORY_TABLES),
301                &[LedgerStateSnapshot::IN_MEMORY_TVAR],
302            );
303
304            let ledger_with_missing_state_files = ledger_dir.join("200");
305            create_dir_all(
306                ledger_with_missing_state_files.join(LedgerStateSnapshot::IN_MEMORY_TABLES),
307            )
308            .unwrap();
309            create_fake_files(
310                &ledger_with_missing_state_files,
311                &[LedgerStateSnapshot::IN_MEMORY_META],
312            );
313            create_fake_files(
314                &ledger_with_missing_meta_files.join(LedgerStateSnapshot::IN_MEMORY_TABLES),
315                &[LedgerStateSnapshot::IN_MEMORY_TVAR],
316            );
317
318            let ledger_with_missing_tvar_files = ledger_dir.join("300");
319            create_dir_all(
320                ledger_with_missing_tvar_files.join(LedgerStateSnapshot::IN_MEMORY_TABLES),
321            )
322            .unwrap();
323            create_fake_files(
324                &ledger_with_missing_tvar_files,
325                &[
326                    LedgerStateSnapshot::IN_MEMORY_STATE,
327                    LedgerStateSnapshot::IN_MEMORY_META,
328                ],
329            );
330
331            let ledger_with_missing_table_folder = ledger_dir.join("400");
332            create_dir(&ledger_with_missing_table_folder).unwrap();
333            create_fake_files(
334                &ledger_with_missing_table_folder,
335                &[
336                    LedgerStateSnapshot::IN_MEMORY_STATE,
337                    LedgerStateSnapshot::IN_MEMORY_META,
338                ],
339            );
340
341            let result = LedgerStateSnapshot::list_all_in_dir(&target_dir).unwrap();
342
343            assert_eq!(Vec::<LedgerStateSnapshot>::new(), result);
344        }
345
346        #[test]
347        fn list_all_ledger_state_with_valid_utxo_hd_folder_structure() {
348            let target_dir = temp_dir_create!();
349            let ledger_dir = create_ledger_dir(&target_dir);
350
351            let ledger_state = ledger_dir.join("200");
352            create_dir_all(ledger_state.join(LedgerStateSnapshot::IN_MEMORY_TABLES)).unwrap();
353            create_fake_files(
354                &ledger_state,
355                &[
356                    LedgerStateSnapshot::IN_MEMORY_META,
357                    LedgerStateSnapshot::IN_MEMORY_STATE,
358                ],
359            );
360            create_fake_files(
361                &ledger_state.join(LedgerStateSnapshot::IN_MEMORY_TABLES),
362                &[LedgerStateSnapshot::IN_MEMORY_TVAR],
363            );
364
365            let result = LedgerStateSnapshot::list_all_in_dir(&target_dir).unwrap();
366
367            assert_eq!(
368                vec![LedgerStateSnapshot::in_memory(
369                    ledger_state,
370                    SlotNumber(200),
371                    "200".into()
372                )],
373                result
374            );
375        }
376
377        #[test]
378        fn get_relative_path_only_list_meta_state_and_tvar_files_even_if_there_are_other_files_in_the_folder()
379         {
380            let target_dir = temp_dir_create!();
381            create_dir_all(target_dir.join("050").join(LedgerStateSnapshot::IN_MEMORY_TABLES))
382                .unwrap();
383            let ledger_state = LedgerStateSnapshot::in_memory(
384                target_dir.join("050"),
385                SlotNumber(50),
386                "050".into(),
387            );
388
389            assert_eq!(
390                vec![
391                    PathBuf::from("050").join(LedgerStateSnapshot::IN_MEMORY_META),
392                    PathBuf::from("050").join(LedgerStateSnapshot::IN_MEMORY_STATE),
393                    PathBuf::from("050")
394                        .join(LedgerStateSnapshot::IN_MEMORY_TABLES)
395                        .join(LedgerStateSnapshot::IN_MEMORY_TVAR)
396                ],
397                ledger_state.get_files_relative_path(),
398            )
399        }
400    }
401}