mithril_aggregator/artifact_builder/
cardano_database.rs

1use std::sync::Arc;
2
3use anyhow::{anyhow, Context};
4use async_trait::async_trait;
5use semver::Version;
6
7use mithril_common::{
8    entities::{
9        AncillaryLocations, CardanoDatabaseSnapshot, CardanoDatabaseSnapshotArtifactData,
10        CardanoDbBeacon, Certificate, DigestsLocations, ImmutablesLocations,
11        ProtocolMessagePartKey, SignedEntityType,
12    },
13    CardanoNetwork, StdResult,
14};
15
16use crate::artifact_builder::{AncillaryArtifactBuilder, ArtifactBuilder};
17
18use super::{DigestArtifactBuilder, ImmutableArtifactBuilder};
19
20pub struct CardanoDatabaseArtifactBuilder {
21    network: CardanoNetwork,
22    cardano_node_version: Version,
23    ancillary_builder: Arc<AncillaryArtifactBuilder>,
24    immutable_builder: Arc<ImmutableArtifactBuilder>,
25    digest_builder: Arc<DigestArtifactBuilder>,
26}
27
28impl CardanoDatabaseArtifactBuilder {
29    pub fn new(
30        network: CardanoNetwork,
31        cardano_node_version: &Version,
32        ancillary_builder: Arc<AncillaryArtifactBuilder>,
33        immutable_builder: Arc<ImmutableArtifactBuilder>,
34        digest_builder: Arc<DigestArtifactBuilder>,
35    ) -> Self {
36        Self {
37            network,
38            cardano_node_version: cardano_node_version.clone(),
39            ancillary_builder,
40            immutable_builder,
41            digest_builder,
42        }
43    }
44}
45
46#[async_trait]
47impl ArtifactBuilder<CardanoDbBeacon, CardanoDatabaseSnapshot> for CardanoDatabaseArtifactBuilder {
48    async fn compute_artifact(
49        &self,
50        beacon: CardanoDbBeacon,
51        certificate: &Certificate,
52    ) -> StdResult<CardanoDatabaseSnapshot> {
53        let merkle_root = certificate
54            .protocol_message
55            .get_message_part(&ProtocolMessagePartKey::CardanoDatabaseMerkleRoot)
56            .ok_or(anyhow!(
57                "Can not find CardanoDatabaseMerkleRoot protocol message part in certificate"
58            ))
59            .with_context(|| {
60                format!(
61                    "Can not compute CardanoDatabase artifact for signed_entity: {:?}",
62                    SignedEntityType::CardanoDatabase(beacon.clone())
63                )
64            })?;
65
66        let ancillary_upload = self.ancillary_builder.upload(&beacon).await?;
67        let immutables_upload = self
68            .immutable_builder
69            .upload(beacon.immutable_file_number)
70            .await?;
71        let digest_upload = self.digest_builder.upload(&beacon).await?;
72
73        let content = CardanoDatabaseSnapshotArtifactData {
74            total_db_size_uncompressed: ancillary_upload.size + immutables_upload.total_size,
75            digests: DigestsLocations {
76                size_uncompressed: digest_upload.size,
77                locations: digest_upload.locations,
78            },
79            immutables: ImmutablesLocations {
80                average_size_uncompressed: immutables_upload.average_size,
81                locations: immutables_upload.locations,
82            },
83            ancillary: AncillaryLocations {
84                size_uncompressed: ancillary_upload.size,
85                locations: ancillary_upload.locations,
86            },
87        };
88
89        let cardano_database = CardanoDatabaseSnapshot::new(
90            merkle_root.to_string(),
91            self.network,
92            beacon,
93            content,
94            &self.cardano_node_version,
95        );
96
97        Ok(cardano_database)
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use std::path::Path;
104    use std::{collections::BTreeMap, path::PathBuf};
105
106    use mithril_common::{
107        digesters::{immutable_trio_names, DummyCardanoDbBuilder, IMMUTABLE_DIR, LEDGER_DIR},
108        entities::{
109            AncillaryFilesManifest, AncillaryLocation, CompressionAlgorithm, DigestLocation,
110            ImmutableFileNumber, ImmutablesLocation, MultiFilesUri, ProtocolMessage,
111            ProtocolMessagePartKey, TemplateUri,
112        },
113        test_utils::{fake_data, fake_keys, TempDir},
114        CardanoNetwork,
115    };
116    use mockall::{predicate, Predicate};
117
118    use crate::{
119        artifact_builder::{
120            DigestSnapshotter, MockAncillaryFileUploader, MockImmutableFilesUploader,
121        },
122        immutable_file_digest_mapper::MockImmutableFileDigestMapper,
123        services::ancillary_signer::MockAncillarySigner,
124        services::CompressedArchiveSnapshotter,
125        test_tools::TestLogger,
126        tools::{file_archiver::FileArchiver, url_sanitizer::SanitizedUrlWithTrailingSlash},
127    };
128
129    use super::*;
130
131    fn get_test_directory(dir_name: &str) -> PathBuf {
132        TempDir::create("cardano_database", dir_name)
133    }
134
135    async fn get_expected_manifest_size(
136        cardano_db_dir: &Path,
137        ancillary_immutable_file_number: ImmutableFileNumber,
138        ancillary_ledger_file_name: &str,
139        ancillary_manifest_signature: &str,
140    ) -> u64 {
141        let mut manifest_dummy = AncillaryFilesManifest::from_paths(
142            cardano_db_dir,
143            [
144                immutable_trio_names(ancillary_immutable_file_number)
145                    .into_iter()
146                    .map(|filename| PathBuf::from(IMMUTABLE_DIR).join(filename))
147                    .collect(),
148                vec![PathBuf::from(LEDGER_DIR).join(ancillary_ledger_file_name)],
149            ]
150            .concat(),
151        )
152        .await
153        .unwrap();
154        manifest_dummy.signature = Some(ancillary_manifest_signature.try_into().unwrap());
155
156        serde_json::to_string(&manifest_dummy).unwrap().len() as u64
157    }
158
159    #[tokio::test]
160    async fn should_compute_valid_artifact() {
161        let test_dir = get_test_directory("should_compute_valid_artifact");
162
163        let beacon = CardanoDbBeacon::new(123, 3);
164        let network = CardanoNetwork::DevNet(123);
165        let immutable_trio_file_size = 777;
166        let ledger_file_size = 6666;
167        let volatile_file_size = 99;
168        let cardano_db = DummyCardanoDbBuilder::new("cdb-should_compute_valid_artifact")
169            .with_immutables(&[1, 2, 3])
170            .append_immutable_trio()
171            .set_immutable_trio_file_size(immutable_trio_file_size)
172            .with_ledger_files(&["437"])
173            .set_ledger_file_size(ledger_file_size)
174            .with_volatile_files(&["blocks-0.dat"])
175            .set_volatile_file_size(volatile_file_size)
176            .build();
177
178        let ancillary_manifest_signature = fake_keys::signable_manifest_signature()[0];
179
180        // The ancillary archive also contains a signed manifest file whose size IS INCLUDED in
181        // the total size and ancillary artifact size
182        let manifest_size = get_expected_manifest_size(
183            cardano_db.get_dir(),
184            4,
185            "437",
186            ancillary_manifest_signature,
187        )
188        .await;
189        let expected_average_immutable_size = immutable_trio_file_size;
190        let expected_ancillary_size = immutable_trio_file_size + ledger_file_size;
191        let expected_total_size = 4 * immutable_trio_file_size + ledger_file_size + manifest_size;
192
193        let snapshotter = Arc::new(
194            CompressedArchiveSnapshotter::new(
195                cardano_db.get_dir().to_path_buf(),
196                test_dir.join("ongoing_snapshots"),
197                CompressionAlgorithm::Gzip,
198                Arc::new(FileArchiver::new_for_test(test_dir.join("verification"))),
199                Arc::new(MockAncillarySigner::that_succeeds_with_signature(
200                    ancillary_manifest_signature,
201                )),
202                TestLogger::stdout(),
203            )
204            .unwrap(),
205        );
206
207        let ancillary_artifact_builder = {
208            let mut ancillary_uploader = MockAncillaryFileUploader::new();
209            ancillary_uploader.expect_upload().return_once(|_, _| {
210                Ok(AncillaryLocation::CloudStorage {
211                    uri: "ancillary_uri".to_string(),
212                    compression_algorithm: Some(CompressionAlgorithm::Gzip),
213                })
214            });
215
216            AncillaryArtifactBuilder::new(
217                vec![Arc::new(ancillary_uploader)],
218                snapshotter.clone(),
219                network,
220                TestLogger::stdout(),
221            )
222            .unwrap()
223        };
224
225        fn predicate_length(length: u64) -> impl Predicate<[PathBuf]> {
226            predicate::function(move |p: &[_]| p.len() == length as usize)
227        }
228
229        let immutable_artifact_builder = {
230            let number_of_immutable_file_loaded = beacon.immutable_file_number;
231            let mut immutable_uploader = MockImmutableFilesUploader::new();
232            immutable_uploader
233                .expect_batch_upload()
234                .with(
235                    predicate_length(number_of_immutable_file_loaded),
236                    predicate::eq(Some(CompressionAlgorithm::Gzip)),
237                )
238                .return_once(|_, _| {
239                    Ok(ImmutablesLocation::CloudStorage {
240                        uri: MultiFilesUri::Template(TemplateUri(
241                            "immutable_template_uri".to_string(),
242                        )),
243                        compression_algorithm: Some(CompressionAlgorithm::Zstandard),
244                    })
245                });
246
247            ImmutableArtifactBuilder::new(
248                cardano_db.get_immutable_dir().to_path_buf(),
249                vec![Arc::new(immutable_uploader)],
250                snapshotter,
251                TestLogger::stdout(),
252            )
253            .unwrap()
254        };
255
256        let digest_artifact_builder = {
257            let mut immutable_file_digest_mapper = MockImmutableFileDigestMapper::new();
258
259            immutable_file_digest_mapper
260                .expect_get_immutable_file_digest_map()
261                .returning(|| Ok(BTreeMap::new()));
262
263            DigestArtifactBuilder::new(
264                SanitizedUrlWithTrailingSlash::parse("http://aggregator_uri").unwrap(),
265                vec![],
266                DigestSnapshotter {
267                    file_archiver: Arc::new(FileArchiver::new_for_test(
268                        test_dir.join("verification"),
269                    )),
270                    target_location: test_dir.clone(),
271                    compression_algorithm: CompressionAlgorithm::Gzip,
272                },
273                network,
274                test_dir.join("digests"),
275                Arc::new(immutable_file_digest_mapper),
276                TestLogger::stdout(),
277            )
278            .unwrap()
279        };
280
281        let cardano_database_artifact_builder = CardanoDatabaseArtifactBuilder::new(
282            network,
283            &Version::parse("1.0.0").unwrap(),
284            Arc::new(ancillary_artifact_builder),
285            Arc::new(immutable_artifact_builder),
286            Arc::new(digest_artifact_builder),
287        );
288
289        let certificate_with_merkle_root = {
290            let mut protocol_message = ProtocolMessage::new();
291            protocol_message.set_message_part(
292                ProtocolMessagePartKey::CardanoDatabaseMerkleRoot,
293                "merkleroot".to_string(),
294            );
295            Certificate {
296                protocol_message,
297                ..fake_data::certificate("certificate-123".to_string())
298            }
299        };
300
301        let artifact = cardano_database_artifact_builder
302            .compute_artifact(beacon.clone(), &certificate_with_merkle_root)
303            .await
304            .unwrap();
305
306        let expected_ancillary_locations = vec![AncillaryLocation::CloudStorage {
307            uri: "ancillary_uri".to_string(),
308            compression_algorithm: Some(CompressionAlgorithm::Gzip),
309        }];
310
311        let expected_immutables_locations = vec![ImmutablesLocation::CloudStorage {
312            uri: MultiFilesUri::Template(TemplateUri("immutable_template_uri".to_string())),
313            compression_algorithm: Some(CompressionAlgorithm::Zstandard),
314        }];
315
316        let expected_digest_locations = vec![DigestLocation::Aggregator {
317            uri: "http://aggregator_uri/artifact/cardano-database/digests".to_string(),
318        }];
319
320        let artifact_expected = CardanoDatabaseSnapshot::new(
321            "merkleroot".to_string(),
322            network,
323            beacon,
324            CardanoDatabaseSnapshotArtifactData {
325                total_db_size_uncompressed: expected_total_size,
326                digests: DigestsLocations {
327                    size_uncompressed: artifact.digests.size_uncompressed,
328                    locations: expected_digest_locations,
329                },
330                immutables: ImmutablesLocations {
331                    average_size_uncompressed: expected_average_immutable_size,
332                    locations: expected_immutables_locations,
333                },
334                ancillary: AncillaryLocations {
335                    size_uncompressed: expected_ancillary_size + manifest_size,
336                    locations: expected_ancillary_locations,
337                },
338            },
339            &Version::parse("1.0.0").unwrap(),
340        );
341
342        assert!(artifact.digests.size_uncompressed > 0);
343        assert_eq!(artifact_expected, artifact);
344    }
345}