mithril_common/digesters/
immutable_digester.rs

1use async_trait::async_trait;
2use sha2::Sha256;
3use slog::{info, Logger};
4use std::{
5    collections::BTreeMap,
6    io,
7    ops::RangeInclusive,
8    path::{Path, PathBuf},
9};
10use thiserror::Error;
11
12use crate::{
13    crypto_helper::{MKTree, MKTreeStoreInMemory},
14    digesters::ImmutableFileListingError,
15    entities::{CardanoDbBeacon, HexEncodedDigest, ImmutableFileName, ImmutableFileNumber},
16    StdError,
17};
18
19use super::ImmutableFile;
20
21/// A digester than can compute the digest used for mithril signatures
22///
23/// If you want to mock it using mockall:
24/// ```
25/// mod test {
26///     use async_trait::async_trait;
27///     use mithril_common::digesters::{ComputedImmutablesDigests, ImmutableDigester, ImmutableDigesterError};
28///     use mithril_common::entities::{CardanoDbBeacon, ImmutableFileNumber};
29///     use mithril_common::crypto_helper::{MKTree, MKTreeStoreInMemory};
30///     use anyhow::anyhow;
31///     use mockall::mock;
32///     use std::ops::RangeInclusive;
33///     use std::path::Path;
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("At least two immutable chunks should exist in directory '{db_dir}': expected {expected_number} but found {found_number:?}.")]
110    NotEnoughImmutable {
111        /// Expected last [ImmutableFileNumber].
112        expected_number: ImmutableFileNumber,
113        /// Last [ImmutableFileNumber] found when listing [ImmutableFiles][crate::digesters::ImmutableFile].
114        found_number: Option<ImmutableFileNumber>,
115        /// A cardano node DB directory
116        db_dir: PathBuf,
117    },
118
119    /// Error raised when the digest computation failed.
120    #[error("Digest computation failed")]
121    DigestComputationError(#[from] io::Error),
122
123    /// Error raised when the Merkle tree computation failed.
124    #[error("Merkle tree computation failed")]
125    MerkleTreeComputationError(StdError),
126}
127
128/// Computed immutables digests
129pub struct ComputedImmutablesDigests {
130    /// A map of [ImmutableFile] to their respective digest.
131    pub entries: BTreeMap<ImmutableFile, HexEncodedDigest>,
132    pub(super) new_cached_entries: Vec<ImmutableFileName>,
133}
134
135impl ComputedImmutablesDigests {
136    pub(super) fn compute_immutables_digests(
137        entries: BTreeMap<ImmutableFile, Option<HexEncodedDigest>>,
138        logger: Logger,
139    ) -> Result<ComputedImmutablesDigests, io::Error> {
140        let mut new_cached_entries = Vec::new();
141        let mut progress = Progress {
142            index: 0,
143            total: entries.len(),
144        };
145
146        let mut digests = BTreeMap::new();
147
148        for (ix, (entry, cache)) in entries.into_iter().enumerate() {
149            let hash = match cache {
150                None => {
151                    new_cached_entries.push(entry.filename.clone());
152                    hex::encode(entry.compute_raw_hash::<Sha256>()?)
153                }
154                Some(digest) => digest,
155            };
156            digests.insert(entry, hash);
157
158            if progress.report(ix) {
159                info!(logger, "Hashing: {progress}");
160            }
161        }
162
163        Ok(ComputedImmutablesDigests {
164            entries: digests,
165            new_cached_entries,
166        })
167    }
168}
169
170pub(super) struct Progress {
171    pub(super) index: usize,
172    pub(super) total: usize,
173}
174
175impl Progress {
176    pub(super) fn report(&mut self, ix: usize) -> bool {
177        self.index = ix;
178        (20 * ix) % self.total == 0
179    }
180
181    pub(super) fn percent(&self) -> f64 {
182        (self.index as f64 * 100.0 / self.total as f64).ceil()
183    }
184}
185
186impl std::fmt::Display for Progress {
187    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
188        write!(f, "{}/{} ({}%)", self.index, self.total, self.percent())
189    }
190}
191
192#[cfg(test)]
193mod tests {
194
195    use super::*;
196
197    #[test]
198    fn reports_progress_every_5_percent() {
199        let mut progress = Progress {
200            index: 0,
201            total: 7000,
202        };
203
204        assert!(!progress.report(1));
205        assert!(!progress.report(4));
206        assert!(progress.report(350));
207        assert!(!progress.report(351));
208    }
209
210    #[test]
211    fn reports_progress_when_total_lower_than_20() {
212        let mut progress = Progress {
213            index: 0,
214            total: 16,
215        };
216
217        assert!(progress.report(4));
218        assert!(progress.report(12));
219        assert!(!progress.report(3));
220        assert!(!progress.report(15));
221    }
222}