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