mithril_common/digesters/
immutable_digester.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
use async_trait::async_trait;
use sha2::Sha256;
use slog::{info, Logger};
use std::{
    collections::BTreeMap,
    io,
    ops::RangeInclusive,
    path::{Path, PathBuf},
};
use thiserror::Error;

use crate::{
    crypto_helper::{MKTree, MKTreeStoreInMemory},
    digesters::ImmutableFileListingError,
    entities::{CardanoDbBeacon, HexEncodedDigest, ImmutableFileName, ImmutableFileNumber},
    StdError,
};

use super::ImmutableFile;

/// A digester than can compute the digest used for mithril signatures
///
/// If you want to mock it using mockall:
/// ```
/// mod test {
///     use async_trait::async_trait;
///     use mithril_common::digesters::{ComputedImmutablesDigests, ImmutableDigester, ImmutableDigesterError};
///     use mithril_common::entities::{CardanoDbBeacon, ImmutableFileNumber};
///     use mithril_common::crypto_helper::{MKTree, MKTreeStoreInMemory};
///     use anyhow::anyhow;
///     use mockall::mock;
///     use std::ops::RangeInclusive;
///     use std::path::Path;
///
///     mock! {
///         pub ImmutableDigesterImpl { }
///
///         #[async_trait]
///         impl ImmutableDigester for ImmutableDigesterImpl {
///             async fn compute_digest(
///               &self,
///               dirpath: &Path,
///               beacon: &CardanoDbBeacon,
///             ) -> Result<String, ImmutableDigesterError>;
///
///             async fn compute_digests_for_range(
///               &self,
///               dirpath: &Path,
///               range: &RangeInclusive<ImmutableFileNumber>,
///             ) -> Result<ComputedImmutablesDigests, ImmutableDigesterError>;
///
///            async fn compute_merkle_tree(
///               &self,
///              dirpath: &Path,
///              beacon: &CardanoDbBeacon,
///           ) -> Result<MKTree<MKTreeStoreInMemory>, ImmutableDigesterError>;
///         }
///     }
///
///     #[test]
///     fn test_mock() {
///         let mut mock = MockDigesterImpl::new();
///         mock.expect_compute_digest().return_once(|_, _| {
///             Err(ImmutableDigesterError::NotEnoughImmutable {
///                 expected_number: 3,
///                 found_number: None,
///                 db_dir: PathBuff::new(),
///             })
///         });
///         mock.expect_compute_merkle_tree().return_once(|_, _| {
///            Err(ImmutableDigesterError::MerkleTreeComputationError(anyhow!("Error")))
///         });
///     }
/// }
/// ```
#[async_trait]
pub trait ImmutableDigester: Sync + Send {
    /// Compute the digest
    async fn compute_digest(
        &self,
        dirpath: &Path,
        beacon: &CardanoDbBeacon,
    ) -> Result<String, ImmutableDigesterError>;

    /// Compute the digests for a range of immutable files
    async fn compute_digests_for_range(
        &self,
        dirpath: &Path,
        range: &RangeInclusive<ImmutableFileNumber>,
    ) -> Result<ComputedImmutablesDigests, ImmutableDigesterError>;

    /// Compute the digests merkle tree
    async fn compute_merkle_tree(
        &self,
        dirpath: &Path,
        beacon: &CardanoDbBeacon,
    ) -> Result<MKTree<MKTreeStoreInMemory>, ImmutableDigesterError>;
}

/// [ImmutableDigester] related Errors.
#[derive(Error, Debug)]
pub enum ImmutableDigesterError {
    /// Error raised when the files listing failed.
    #[error("Immutable files listing failed")]
    ListImmutablesError(#[from] ImmutableFileListingError),

    /// Error raised when there's less than the required number of completed immutables in
    /// the cardano database or even no immutable at all.
    #[error("At least two immutable chunks should exist in directory '{db_dir}': expected {expected_number} but found {found_number:?}.")]
    NotEnoughImmutable {
        /// Expected last [ImmutableFileNumber].
        expected_number: ImmutableFileNumber,
        /// Last [ImmutableFileNumber] found when listing [ImmutableFiles][crate::digesters::ImmutableFile].
        found_number: Option<ImmutableFileNumber>,
        /// A cardano node DB directory
        db_dir: PathBuf,
    },

    /// Error raised when the digest computation failed.
    #[error("Digest computation failed")]
    DigestComputationError(#[from] io::Error),

    /// Error raised when the Merkle tree computation failed.
    #[error("Merkle tree computation failed")]
    MerkleTreeComputationError(StdError),
}

/// Computed immutables digests
pub struct ComputedImmutablesDigests {
    /// A map of [ImmutableFile] to their respective digest.
    pub entries: BTreeMap<ImmutableFile, HexEncodedDigest>,
    pub(super) new_cached_entries: Vec<ImmutableFileName>,
}

impl ComputedImmutablesDigests {
    pub(super) fn compute_immutables_digests(
        entries: BTreeMap<ImmutableFile, Option<HexEncodedDigest>>,
        logger: Logger,
    ) -> Result<ComputedImmutablesDigests, io::Error> {
        let mut new_cached_entries = Vec::new();
        let mut progress = Progress {
            index: 0,
            total: entries.len(),
        };

        let mut digests = BTreeMap::new();

        for (ix, (entry, cache)) in entries.into_iter().enumerate() {
            let hash = match cache {
                None => {
                    new_cached_entries.push(entry.filename.clone());
                    hex::encode(entry.compute_raw_hash::<Sha256>()?)
                }
                Some(digest) => digest,
            };
            digests.insert(entry, hash);

            if progress.report(ix) {
                info!(logger, "Hashing: {progress}");
            }
        }

        Ok(ComputedImmutablesDigests {
            entries: digests,
            new_cached_entries,
        })
    }
}

pub(super) struct Progress {
    pub(super) index: usize,
    pub(super) total: usize,
}

impl Progress {
    pub(super) fn report(&mut self, ix: usize) -> bool {
        self.index = ix;
        (20 * ix) % self.total == 0
    }

    pub(super) fn percent(&self) -> f64 {
        (self.index as f64 * 100.0 / self.total as f64).ceil()
    }
}

impl std::fmt::Display for Progress {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
        write!(f, "{}/{} ({}%)", self.index, self.total, self.percent())
    }
}

#[cfg(test)]
mod tests {

    use super::*;

    #[test]
    fn reports_progress_every_5_percent() {
        let mut progress = Progress {
            index: 0,
            total: 7000,
        };

        assert!(!progress.report(1));
        assert!(!progress.report(4));
        assert!(progress.report(350));
        assert!(!progress.report(351));
    }

    #[test]
    fn reports_progress_when_total_lower_than_20() {
        let mut progress = Progress {
            index: 0,
            total: 16,
        };

        assert!(progress.report(4));
        assert!(progress.report(12));
        assert!(!progress.report(3));
        assert!(!progress.report(15));
    }
}