mithril_common/messages/
cardano_database.rs

1use anyhow::anyhow;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4
5use crate::StdResult;
6use crate::entities::{
7    AncillaryLocation, AncillaryLocations, CardanoDbBeacon, DigestLocation, DigestsLocations,
8    ImmutablesLocation, ImmutablesLocations,
9};
10
11/// The message part that represents the locations of the Cardano database digests.
12#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
13pub struct DigestsMessagePart {
14    /// Size of the uncompressed digests file.
15    pub size_uncompressed: u64,
16
17    /// Locations of the digests.
18    pub locations: Vec<DigestLocation>,
19}
20
21impl DigestsMessagePart {
22    /// Return the list of locations without the unknown locations, failing if all locations are unknown.
23    pub fn sanitized_locations(&self) -> StdResult<Vec<DigestLocation>> {
24        let sanitized_locations: Vec<_> = self
25            .locations
26            .iter()
27            .filter(|l| !matches!(l, DigestLocation::Unknown))
28            .cloned()
29            .collect();
30
31        if sanitized_locations.is_empty() {
32            Err(anyhow!("All digests locations are unknown."))
33        } else {
34            Ok(sanitized_locations)
35        }
36    }
37}
38
39/// The message part that represents the locations of the Cardano database immutables.
40#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
41pub struct ImmutablesMessagePart {
42    /// Average size for one immutable file.
43    pub average_size_uncompressed: u64,
44
45    /// Locations of the immutable files.
46    pub locations: Vec<ImmutablesLocation>,
47}
48
49impl ImmutablesMessagePart {
50    /// Return the list of locations without the unknown locations, failing if all locations are unknown.
51    pub fn sanitized_locations(&self) -> StdResult<Vec<ImmutablesLocation>> {
52        let sanitized_locations: Vec<_> = self
53            .locations
54            .iter()
55            .filter(|l| !matches!(l, ImmutablesLocation::Unknown))
56            .cloned()
57            .collect();
58
59        if sanitized_locations.is_empty() {
60            Err(anyhow!("All locations are unknown."))
61        } else {
62            Ok(sanitized_locations)
63        }
64    }
65}
66
67/// The message part that represents the locations of the Cardano database ancillary.
68#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
69pub struct AncillaryMessagePart {
70    /// Size of the uncompressed ancillary file.
71    pub size_uncompressed: u64,
72
73    /// Locations of the ancillary files.
74    pub locations: Vec<AncillaryLocation>,
75}
76
77impl AncillaryMessagePart {
78    /// Return the list of locations without the unknown locations, failing if all locations are unknown.
79    pub fn sanitized_locations(&self) -> StdResult<Vec<AncillaryLocation>> {
80        let sanitized_locations: Vec<_> = self
81            .locations
82            .iter()
83            .filter(|l| !matches!(l, AncillaryLocation::Unknown))
84            .cloned()
85            .collect();
86
87        if sanitized_locations.is_empty() {
88            Err(anyhow!("All locations are unknown."))
89        } else {
90            Ok(sanitized_locations)
91        }
92    }
93}
94
95impl From<DigestsLocations> for DigestsMessagePart {
96    fn from(part: DigestsLocations) -> Self {
97        Self {
98            size_uncompressed: part.size_uncompressed,
99            locations: part.locations,
100        }
101    }
102}
103
104impl From<ImmutablesLocations> for ImmutablesMessagePart {
105    fn from(part: ImmutablesLocations) -> Self {
106        Self {
107            average_size_uncompressed: part.average_size_uncompressed,
108            locations: part.locations,
109        }
110    }
111}
112
113impl From<AncillaryLocations> for AncillaryMessagePart {
114    fn from(part: AncillaryLocations) -> Self {
115        Self {
116            size_uncompressed: part.size_uncompressed,
117            locations: part.locations,
118        }
119    }
120}
121
122/// Cardano database snapshot.
123#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
124pub struct CardanoDatabaseSnapshotMessage {
125    /// Hash of the Cardano database snapshot.
126    pub hash: String,
127
128    /// Merkle root of the Cardano database snapshot.
129    pub merkle_root: String,
130
131    /// Cardano network
132    pub network: String,
133
134    /// Mithril beacon on the Cardano chain.
135    pub beacon: CardanoDbBeacon,
136
137    /// Hash of the associated certificate
138    pub certificate_hash: String,
139
140    /// Size of the uncompressed Cardano database files.
141    pub total_db_size_uncompressed: u64,
142
143    /// Locations of the the immutable file digests.
144    pub digests: DigestsMessagePart,
145
146    /// Locations of the immutable files.
147    pub immutables: ImmutablesMessagePart,
148
149    /// Locations of the ancillary files.
150    pub ancillary: AncillaryMessagePart,
151
152    /// Version of the Cardano node used to create the snapshot.
153    pub cardano_node_version: String,
154
155    /// Date and time at which the snapshot was created
156    pub created_at: DateTime<Utc>,
157}
158
159#[cfg(test)]
160mod tests {
161    use crate::entities::{CompressionAlgorithm, Epoch, MultiFilesUri, TemplateUri};
162
163    use super::*;
164
165    const CURRENT_JSON: &str = r#"
166    {
167        "hash": "d4071d518a3ace0f6c04a9c0745b9e9560e3e2af1b373bafc4e0398423e9abfb",
168        "merkle_root": "c8224920b9f5ad7377594eb8a15f34f08eb3103cc5241d57cafc5638403ec7c6",
169        "network": "preview",
170        "beacon": {
171            "epoch": 123,
172            "immutable_file_number": 2345
173        },
174        "certificate_hash": "f6c01b373bafc4e039844071d5da3ace4a9c0745b9e9560e3e2af01823e9abfb",
175        "total_db_size_uncompressed": 800796318,
176        "digests": {
177            "size_uncompressed": 1024,
178            "locations": [
179                {
180                    "type": "aggregator",
181                    "uri": "https://host-1/digest-1"
182                }
183            ]
184        },
185        "immutables": {
186            "average_size_uncompressed": 2048,
187            "locations": [
188                {
189                    "type": "cloud_storage",
190                    "uri": {
191                        "Template": "https://host-1/immutables-{immutable_file_number}"
192                    },
193                    "compression_algorithm": "gzip"
194                },
195                {
196                    "type": "cloud_storage",
197                    "uri": {
198                        "Template": "https://host-2/immutables-{immutable_file_number}"
199                    }
200                }
201            ]
202        },
203        "ancillary": {
204            "size_uncompressed": 4096,
205            "locations": [
206                {
207                    "type": "cloud_storage",
208                    "uri": "https://host-1/ancillary-3",
209                    "compression_algorithm": "gzip"
210                }
211            ]
212        },
213        "cardano_node_version": "0.0.1",
214        "created_at": "2023-01-19T13:43:05.618857482Z"
215    }"#;
216
217    fn golden_current_message() -> CardanoDatabaseSnapshotMessage {
218        CardanoDatabaseSnapshotMessage {
219            hash: "d4071d518a3ace0f6c04a9c0745b9e9560e3e2af1b373bafc4e0398423e9abfb".to_string(),
220            merkle_root: "c8224920b9f5ad7377594eb8a15f34f08eb3103cc5241d57cafc5638403ec7c6"
221                .to_string(),
222            network: "preview".to_string(),
223            beacon: CardanoDbBeacon {
224                epoch: Epoch(123),
225                immutable_file_number: 2345,
226            },
227            certificate_hash: "f6c01b373bafc4e039844071d5da3ace4a9c0745b9e9560e3e2af01823e9abfb"
228                .to_string(),
229            total_db_size_uncompressed: 800796318,
230            created_at: DateTime::parse_from_rfc3339("2023-01-19T13:43:05.618857482Z")
231                .unwrap()
232                .with_timezone(&Utc),
233            digests: DigestsMessagePart {
234                size_uncompressed: 1024,
235                locations: vec![DigestLocation::Aggregator {
236                    uri: "https://host-1/digest-1".to_string(),
237                }],
238            },
239            immutables: ImmutablesMessagePart {
240                average_size_uncompressed: 2048,
241                locations: vec![
242                    ImmutablesLocation::CloudStorage {
243                        uri: MultiFilesUri::Template(TemplateUri(
244                            "https://host-1/immutables-{immutable_file_number}".to_string(),
245                        )),
246                        compression_algorithm: Some(CompressionAlgorithm::Gzip),
247                    },
248                    ImmutablesLocation::CloudStorage {
249                        uri: MultiFilesUri::Template(TemplateUri(
250                            "https://host-2/immutables-{immutable_file_number}".to_string(),
251                        )),
252                        compression_algorithm: None,
253                    },
254                ],
255            },
256            ancillary: AncillaryMessagePart {
257                size_uncompressed: 4096,
258                locations: vec![AncillaryLocation::CloudStorage {
259                    uri: "https://host-1/ancillary-3".to_string(),
260                    compression_algorithm: Some(CompressionAlgorithm::Gzip),
261                }],
262            },
263            cardano_node_version: "0.0.1".to_string(),
264        }
265    }
266
267    #[test]
268    fn test_current_json_deserialized_into_current_message() {
269        let json = CURRENT_JSON;
270        let message: CardanoDatabaseSnapshotMessage = serde_json::from_str(json).expect(
271            "This JSON is expected to be successfully parsed into a CardanoDatabaseSnapshotMessage instance.",
272        );
273
274        assert_eq!(golden_current_message(), message);
275    }
276
277    #[test]
278    fn test_a_future_json_deserialized_with_unknown_location_types() {
279        let json = r#"
280        {
281            "hash": "d4071d518a3ace0f6c04a9c0745b9e9560e3e2af1b373bafc4e0398423e9abfb",
282            "merkle_root": "c8224920b9f5ad7377594eb8a15f34f08eb3103cc5241d57cafc5638403ec7c6",
283            "network": "preview",
284            "beacon": {
285                "epoch": 123,
286                "immutable_file_number": 2345
287            },
288            "certificate_hash": "f6c01b373bafc4e039844071d5da3ace4a9c0745b9e9560e3e2af01823e9abfb",
289            "total_db_size_uncompressed": 800796318,
290            "digests": {
291                "size_uncompressed": 1024,
292                "locations": [
293                    {
294                        "type": "whatever",
295                        "new_field": "digest-1"
296                    }
297                ]
298            },
299            "immutables": {
300                "average_size_uncompressed": 512,
301                "locations": [
302                    {
303                        "type": "whatever",
304                        "new_field": [123, 125]
305                    }
306                ]
307            },
308            "ancillary": {
309                "size_uncompressed": 4096,
310                "locations": [
311                    {
312                        "type": "whatever",
313                        "new_field": "ancillary-3"
314                    }
315                ]
316            },
317            "compression_algorithm": "gzip",
318            "cardano_node_version": "0.0.1",
319            "created_at": "2023-01-19T13:43:05.618857482Z"
320        }"#;
321        let message: CardanoDatabaseSnapshotMessage = serde_json::from_str(json).expect(
322            "This JSON is expected to be successfully parsed into a CardanoDatabaseSnapshotMessage instance.",
323        );
324
325        assert_eq!(message.digests.locations.len(), 1);
326        assert_eq!(DigestLocation::Unknown, message.digests.locations[0]);
327
328        assert_eq!(message.immutables.locations.len(), 1);
329        assert_eq!(ImmutablesLocation::Unknown, message.immutables.locations[0]);
330
331        assert_eq!(message.ancillary.locations.len(), 1);
332        assert_eq!(AncillaryLocation::Unknown, message.ancillary.locations[0]);
333    }
334
335    mod sanitize_immutable_locations {
336        use super::*;
337
338        #[test]
339        fn succeeds_and_leave_all_locations_intact_if_no_unknown_location() {
340            let immutable_locations = ImmutablesMessagePart {
341                locations: vec![ImmutablesLocation::CloudStorage {
342                    uri: MultiFilesUri::Template(TemplateUri(
343                        "http://whatever/{immutable_file_number}.tar.gz".to_string(),
344                    )),
345                    compression_algorithm: None,
346                }],
347                average_size_uncompressed: 512,
348            };
349
350            let sanitize_locations = immutable_locations
351                .sanitized_locations()
352                .expect("Should succeed since there are no unknown locations.");
353            assert_eq!(sanitize_locations, immutable_locations.locations);
354        }
355
356        #[test]
357        fn succeeds_and_remove_unknown_locations_if_some_locations_are_not_unknown() {
358            let immutable_locations = ImmutablesMessagePart {
359                locations: vec![
360                    ImmutablesLocation::CloudStorage {
361                        uri: MultiFilesUri::Template(TemplateUri(
362                            "http://whatever/{immutable_file_number}.tar.gz".to_string(),
363                        )),
364                        compression_algorithm: None,
365                    },
366                    ImmutablesLocation::Unknown,
367                ],
368                average_size_uncompressed: 512,
369            };
370
371            let sanitize_locations = immutable_locations
372                .sanitized_locations()
373                .expect("Should succeed since not all locations are unknown.");
374            assert_eq!(
375                sanitize_locations,
376                vec![ImmutablesLocation::CloudStorage {
377                    uri: MultiFilesUri::Template(TemplateUri(
378                        "http://whatever/{immutable_file_number}.tar.gz".to_string(),
379                    )),
380                    compression_algorithm: None,
381                }]
382            );
383        }
384
385        #[test]
386        fn fails_if_all_locations_are_unknown() {
387            ImmutablesMessagePart {
388                locations: vec![ImmutablesLocation::Unknown],
389                average_size_uncompressed: 512,
390            }
391            .sanitized_locations()
392            .expect_err("Should fail since all locations are unknown.");
393        }
394    }
395
396    mod sanitize_ancillary_locations {
397        use super::*;
398
399        #[test]
400        fn succeeds_and_leave_all_locations_intact_if_no_unknown_location() {
401            let ancillary_locations = AncillaryMessagePart {
402                locations: vec![AncillaryLocation::CloudStorage {
403                    uri: "http://whatever/ancillary.tar.gz".to_string(),
404                    compression_algorithm: None,
405                }],
406                size_uncompressed: 1024,
407            };
408
409            let sanitize_locations = ancillary_locations
410                .sanitized_locations()
411                .expect("Should succeed since there are no unknown locations.");
412            assert_eq!(sanitize_locations, ancillary_locations.locations);
413        }
414
415        #[test]
416        fn succeeds_and_remove_unknown_locations_if_some_locations_are_not_unknown() {
417            let ancillary_locations = AncillaryMessagePart {
418                locations: vec![
419                    AncillaryLocation::CloudStorage {
420                        uri: "http://whatever/digests.tar.gz".to_string(),
421                        compression_algorithm: None,
422                    },
423                    AncillaryLocation::Unknown,
424                ],
425                size_uncompressed: 512,
426            };
427
428            let sanitize_locations = ancillary_locations
429                .sanitized_locations()
430                .expect("Should succeed since not all locations are unknown.");
431            assert_eq!(
432                sanitize_locations,
433                vec![AncillaryLocation::CloudStorage {
434                    uri: "http://whatever/digests.tar.gz".to_string(),
435                    compression_algorithm: None,
436                }]
437            );
438        }
439
440        #[test]
441        fn fails_if_all_locations_are_unknown() {
442            AncillaryMessagePart {
443                locations: vec![AncillaryLocation::Unknown],
444                size_uncompressed: 512,
445            }
446            .sanitized_locations()
447            .expect_err("Should fail since all locations are unknown.");
448        }
449    }
450
451    mod sanitize_digests_locations {
452        use super::*;
453
454        #[test]
455        fn succeeds_and_leave_all_locations_intact_if_no_unknown_location() {
456            let digests_locations = DigestsMessagePart {
457                locations: vec![DigestLocation::CloudStorage {
458                    uri: "http://whatever/digests.tar.gz".to_string(),
459                    compression_algorithm: None,
460                }],
461                size_uncompressed: 512,
462            };
463
464            let sanitize_locations = digests_locations
465                .sanitized_locations()
466                .expect("Should succeed since there are no unknown locations.");
467            assert_eq!(sanitize_locations, digests_locations.locations);
468        }
469
470        #[test]
471        fn succeeds_and_remove_unknown_locations_if_some_locations_are_not_unknown() {
472            let digests_locations = DigestsMessagePart {
473                locations: vec![
474                    DigestLocation::CloudStorage {
475                        uri: "http://whatever/digests.tar.gz".to_string(),
476                        compression_algorithm: None,
477                    },
478                    DigestLocation::Unknown,
479                ],
480                size_uncompressed: 512,
481            };
482
483            let sanitize_locations = digests_locations
484                .sanitized_locations()
485                .expect("Should succeed since not all locations are unknown.");
486            assert_eq!(
487                sanitize_locations,
488                vec![DigestLocation::CloudStorage {
489                    uri: "http://whatever/digests.tar.gz".to_string(),
490                    compression_algorithm: None,
491                }]
492            );
493        }
494
495        #[test]
496        fn fails_if_all_locations_are_unknown() {
497            DigestsMessagePart {
498                locations: vec![DigestLocation::Unknown],
499                size_uncompressed: 512,
500            }
501            .sanitized_locations()
502            .expect_err("Should fail since all locations are unknown.");
503        }
504    }
505}