mithril_cardano_node_internal_database/digesters/
immutable_digester.rs

1use async_trait::async_trait;
2use sha2::Sha256;
3use slog::{Logger, info};
4use std::{
5    collections::BTreeMap,
6    io,
7    ops::RangeInclusive,
8    path::{Path, PathBuf},
9};
10use thiserror::Error;
11
12use mithril_common::{
13    StdError,
14    crypto_helper::{MKTree, MKTreeStoreInMemory},
15    entities::{CardanoDbBeacon, HexEncodedDigest, ImmutableFileName, ImmutableFileNumber},
16};
17
18use crate::entities::{ImmutableFile, ImmutableFileListingError};
19
20/// A digester than can compute the digest used for mithril signatures
21///
22/// If you want to mock it using mockall:
23/// ```
24/// mod test {
25///     use anyhow::anyhow;
26///     use async_trait::async_trait;
27///     use mockall::mock;
28///     use std::ops::RangeInclusive;
29///     use std::path::Path;
30///
31///     use mithril_cardano_node_internal_database::digesters::{ComputedImmutablesDigests, ImmutableDigester, ImmutableDigesterError};
32///     use mithril_common::entities::{CardanoDbBeacon, ImmutableFileNumber};
33///     use mithril_common::crypto_helper::{MKTree, MKTreeStoreInMemory};
34///
35///     mock! {
36///         pub ImmutableDigesterImpl { }
37///
38///         #[async_trait]
39///         impl ImmutableDigester for ImmutableDigesterImpl {
40///             async fn compute_digest(
41///               &self,
42///               dirpath: &Path,
43///               beacon: &CardanoDbBeacon,
44///             ) -> Result<String, ImmutableDigesterError>;
45///
46///             async fn compute_digests_for_range(
47///               &self,
48///               dirpath: &Path,
49///               range: &RangeInclusive<ImmutableFileNumber>,
50///             ) -> Result<ComputedImmutablesDigests, ImmutableDigesterError>;
51///
52///            async fn compute_merkle_tree(
53///               &self,
54///              dirpath: &Path,
55///              beacon: &CardanoDbBeacon,
56///           ) -> Result<MKTree<MKTreeStoreInMemory>, ImmutableDigesterError>;
57///         }
58///     }
59///
60///     #[test]
61///     fn test_mock() {
62///         let mut mock = MockDigesterImpl::new();
63///         mock.expect_compute_digest().return_once(|_, _| {
64///             Err(ImmutableDigesterError::NotEnoughImmutable {
65///                 expected_number: 3,
66///                 found_number: None,
67///                 db_dir: PathBuff::new(),
68///             })
69///         });
70///         mock.expect_compute_merkle_tree().return_once(|_, _| {
71///            Err(ImmutableDigesterError::MerkleTreeComputationError(anyhow!("Error")))
72///         });
73///     }
74/// }
75/// ```
76#[async_trait]
77pub trait ImmutableDigester: Sync + Send {
78    /// Compute the digest
79    async fn compute_digest(
80        &self,
81        dirpath: &Path,
82        beacon: &CardanoDbBeacon,
83    ) -> Result<String, ImmutableDigesterError>;
84
85    /// Compute the digests for a range of immutable files
86    async fn compute_digests_for_range(
87        &self,
88        dirpath: &Path,
89        range: &RangeInclusive<ImmutableFileNumber>,
90    ) -> Result<ComputedImmutablesDigests, ImmutableDigesterError>;
91
92    /// Compute the digests merkle tree
93    async fn compute_merkle_tree(
94        &self,
95        dirpath: &Path,
96        beacon: &CardanoDbBeacon,
97    ) -> Result<MKTree<MKTreeStoreInMemory>, ImmutableDigesterError>;
98}
99
100/// [ImmutableDigester] related Errors.
101#[derive(Error, Debug)]
102pub enum ImmutableDigesterError {
103    /// Error raised when the files listing failed.
104    #[error("Immutable files listing failed")]
105    ListImmutablesError(#[from] ImmutableFileListingError),
106
107    /// Error raised when there's less than the required number of completed immutables in
108    /// the cardano database or even no immutable at all.
109    #[error(
110        "At least two immutable chunks should exist in directory '{db_dir}': expected {expected_number} but found {found_number:?}."
111    )]
112    NotEnoughImmutable {
113        /// Expected last [ImmutableFileNumber].
114        expected_number: ImmutableFileNumber,
115        /// Last [ImmutableFileNumber] found when listing [ImmutableFiles][crate::entities::ImmutableFile].
116        found_number: Option<ImmutableFileNumber>,
117        /// A cardano node DB directory
118        db_dir: PathBuf,
119    },
120
121    /// Error raised when the digest computation failed.
122    #[error("Digest computation failed")]
123    DigestComputationError(#[from] io::Error),
124
125    /// Error raised when the Merkle tree computation failed.
126    #[error("Merkle tree computation failed")]
127    MerkleTreeComputationError(StdError),
128}
129
130/// Computed immutables digests
131pub struct ComputedImmutablesDigests {
132    /// A map of [ImmutableFile] to their respective digest.
133    pub entries: BTreeMap<ImmutableFile, HexEncodedDigest>,
134    pub(super) new_cached_entries: Vec<ImmutableFileName>,
135}
136
137impl ComputedImmutablesDigests {
138    pub(crate) fn compute_immutables_digests(
139        entries: BTreeMap<ImmutableFile, Option<HexEncodedDigest>>,
140        logger: Logger,
141    ) -> Result<ComputedImmutablesDigests, io::Error> {
142        let mut new_cached_entries = Vec::new();
143        let mut progress = Progress {
144            index: 0,
145            total: entries.len(),
146        };
147
148        let mut digests = BTreeMap::new();
149
150        for (ix, (entry, cache)) in entries.into_iter().enumerate() {
151            let hash = match cache {
152                None => {
153                    new_cached_entries.push(entry.filename.clone());
154                    hex::encode(entry.compute_raw_hash::<Sha256>()?)
155                }
156                Some(digest) => digest,
157            };
158            digests.insert(entry, hash);
159
160            if progress.report(ix) {
161                info!(logger, "Hashing: {progress}");
162            }
163        }
164
165        Ok(ComputedImmutablesDigests {
166            entries: digests,
167            new_cached_entries,
168        })
169    }
170}
171
172pub(super) struct Progress {
173    pub(super) index: usize,
174    pub(super) total: usize,
175}
176
177impl Progress {
178    pub(super) fn report(&mut self, ix: usize) -> bool {
179        self.index = ix;
180        (20 * ix) % self.total == 0
181    }
182
183    pub(super) fn percent(&self) -> f64 {
184        (self.index as f64 * 100.0 / self.total as f64).ceil()
185    }
186}
187
188impl std::fmt::Display for Progress {
189    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
190        write!(f, "{}/{} ({}%)", self.index, self.total, self.percent())
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn reports_progress_every_5_percent() {
200        let mut progress = Progress {
201            index: 0,
202            total: 7000,
203        };
204
205        assert!(!progress.report(1));
206        assert!(!progress.report(4));
207        assert!(progress.report(350));
208        assert!(!progress.report(351));
209    }
210
211    #[test]
212    fn reports_progress_when_total_lower_than_20() {
213        let mut progress = Progress {
214            index: 0,
215            total: 16,
216        };
217
218        assert!(progress.report(4));
219        assert!(progress.report(12));
220        assert!(!progress.report(3));
221        assert!(!progress.report(15));
222    }
223}