mithril_common/messages/
cardano_database.rs

1use anyhow::anyhow;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4
5use crate::entities::{
6    AncillaryLocation, AncillaryLocations, CardanoDbBeacon, CompressionAlgorithm, DigestLocation,
7    DigestsLocations, Epoch, ImmutablesLocation, ImmutablesLocations, MultiFilesUri, TemplateUri,
8};
9use crate::StdResult;
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
159impl CardanoDatabaseSnapshotMessage {
160    /// Return a dummy test entity (test-only).
161    pub fn dummy() -> Self {
162        Self {
163            hash: "d4071d518a3ace0f6c04a9c0745b9e9560e3e2af1b373bafc4e0398423e9abfb".to_string(),
164            merkle_root: "c8224920b9f5ad7377594eb8a15f34f08eb3103cc5241d57cafc5638403ec7c6"
165                .to_string(),
166            network: "preview".to_string(),
167            beacon: CardanoDbBeacon {
168                epoch: Epoch(123),
169                immutable_file_number: 2345,
170            },
171            certificate_hash: "f6c01b373bafc4e039844071d5da3ace4a9c0745b9e9560e3e2af01823e9abfb"
172                .to_string(),
173            total_db_size_uncompressed: 800796318,
174            created_at: DateTime::parse_from_rfc3339("2023-01-19T13:43:05.618857482Z")
175                .unwrap()
176                .with_timezone(&Utc),
177            digests: DigestsMessagePart {
178                size_uncompressed: 1024,
179                locations: vec![DigestLocation::Aggregator {
180                    uri: "https://host-1/digest-1".to_string(),
181                }],
182            },
183            immutables: ImmutablesMessagePart {
184                average_size_uncompressed: 512,
185                locations: vec![
186                    ImmutablesLocation::CloudStorage {
187                        uri: MultiFilesUri::Template(TemplateUri(
188                            "https://host-1/immutables-2".to_string(),
189                        )),
190                        compression_algorithm: Some(CompressionAlgorithm::Gzip),
191                    },
192                    ImmutablesLocation::CloudStorage {
193                        uri: MultiFilesUri::Template(TemplateUri(
194                            "https://host-2/immutables-2".to_string(),
195                        )),
196                        compression_algorithm: Some(CompressionAlgorithm::Gzip),
197                    },
198                ],
199            },
200            ancillary: AncillaryMessagePart {
201                size_uncompressed: 2048,
202                locations: vec![AncillaryLocation::CloudStorage {
203                    uri: "https://host-1/ancillary-3".to_string(),
204                    compression_algorithm: Some(CompressionAlgorithm::Gzip),
205                }],
206            },
207            cardano_node_version: "0.0.1".to_string(),
208        }
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    const CURRENT_JSON: &str = r#"
217    {
218        "hash": "d4071d518a3ace0f6c04a9c0745b9e9560e3e2af1b373bafc4e0398423e9abfb",
219        "merkle_root": "c8224920b9f5ad7377594eb8a15f34f08eb3103cc5241d57cafc5638403ec7c6",
220        "network": "preview",
221        "beacon": {
222            "epoch": 123,
223            "immutable_file_number": 2345
224        },
225        "certificate_hash": "f6c01b373bafc4e039844071d5da3ace4a9c0745b9e9560e3e2af01823e9abfb",
226        "total_db_size_uncompressed": 800796318,
227        "digests": {
228            "size_uncompressed": 1024,
229            "locations": [
230                {
231                    "type": "aggregator",
232                    "uri": "https://host-1/digest-1"
233                }
234            ]
235        },
236        "immutables": {
237            "average_size_uncompressed": 2048,
238            "locations": [
239                {
240                    "type": "cloud_storage",
241                    "uri": {
242                        "Template": "https://host-1/immutables-{immutable_file_number}"
243                    },
244                    "compression_algorithm": "gzip"
245                },
246                {
247                    "type": "cloud_storage",
248                    "uri": {
249                        "Template": "https://host-2/immutables-{immutable_file_number}"
250                    }
251                }
252            ]
253        },
254        "ancillary": {
255            "size_uncompressed": 4096,
256            "locations": [
257                {
258                    "type": "cloud_storage",
259                    "uri": "https://host-1/ancillary-3",
260                    "compression_algorithm": "gzip"
261                }
262            ]
263        },
264        "cardano_node_version": "0.0.1",
265        "created_at": "2023-01-19T13:43:05.618857482Z"
266    }"#;
267
268    fn golden_current_message() -> CardanoDatabaseSnapshotMessage {
269        CardanoDatabaseSnapshotMessage {
270            hash: "d4071d518a3ace0f6c04a9c0745b9e9560e3e2af1b373bafc4e0398423e9abfb".to_string(),
271            merkle_root: "c8224920b9f5ad7377594eb8a15f34f08eb3103cc5241d57cafc5638403ec7c6"
272                .to_string(),
273            network: "preview".to_string(),
274            beacon: CardanoDbBeacon {
275                epoch: Epoch(123),
276                immutable_file_number: 2345,
277            },
278            certificate_hash: "f6c01b373bafc4e039844071d5da3ace4a9c0745b9e9560e3e2af01823e9abfb"
279                .to_string(),
280            total_db_size_uncompressed: 800796318,
281            created_at: DateTime::parse_from_rfc3339("2023-01-19T13:43:05.618857482Z")
282                .unwrap()
283                .with_timezone(&Utc),
284            digests: DigestsMessagePart {
285                size_uncompressed: 1024,
286                locations: vec![DigestLocation::Aggregator {
287                    uri: "https://host-1/digest-1".to_string(),
288                }],
289            },
290            immutables: ImmutablesMessagePart {
291                average_size_uncompressed: 2048,
292                locations: vec![
293                    ImmutablesLocation::CloudStorage {
294                        uri: MultiFilesUri::Template(TemplateUri(
295                            "https://host-1/immutables-{immutable_file_number}".to_string(),
296                        )),
297                        compression_algorithm: Some(CompressionAlgorithm::Gzip),
298                    },
299                    ImmutablesLocation::CloudStorage {
300                        uri: MultiFilesUri::Template(TemplateUri(
301                            "https://host-2/immutables-{immutable_file_number}".to_string(),
302                        )),
303                        compression_algorithm: None,
304                    },
305                ],
306            },
307            ancillary: AncillaryMessagePart {
308                size_uncompressed: 4096,
309                locations: vec![AncillaryLocation::CloudStorage {
310                    uri: "https://host-1/ancillary-3".to_string(),
311                    compression_algorithm: Some(CompressionAlgorithm::Gzip),
312                }],
313            },
314            cardano_node_version: "0.0.1".to_string(),
315        }
316    }
317
318    #[test]
319    fn test_current_json_deserialized_into_current_message() {
320        let json = CURRENT_JSON;
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!(golden_current_message(), message);
326    }
327
328    #[test]
329    fn test_a_future_json_deserialized_with_unknown_location_types() {
330        let json = r#"
331        {
332            "hash": "d4071d518a3ace0f6c04a9c0745b9e9560e3e2af1b373bafc4e0398423e9abfb",
333            "merkle_root": "c8224920b9f5ad7377594eb8a15f34f08eb3103cc5241d57cafc5638403ec7c6",
334            "network": "preview",
335            "beacon": {
336                "epoch": 123,
337                "immutable_file_number": 2345
338            },
339            "certificate_hash": "f6c01b373bafc4e039844071d5da3ace4a9c0745b9e9560e3e2af01823e9abfb",
340            "total_db_size_uncompressed": 800796318,
341            "digests": {
342                "size_uncompressed": 1024,
343                "locations": [
344                    {
345                        "type": "whatever",
346                        "new_field": "digest-1"
347                    }
348                ]
349            },
350            "immutables": {
351                "average_size_uncompressed": 512,
352                "locations": [
353                    {
354                        "type": "whatever",
355                        "new_field": [123, 125]
356                    }
357                ]
358            },
359            "ancillary": {
360                "size_uncompressed": 4096,
361                "locations": [
362                    {
363                        "type": "whatever",
364                        "new_field": "ancillary-3"
365                    }
366                ]
367            },
368            "compression_algorithm": "gzip",
369            "cardano_node_version": "0.0.1",
370            "created_at": "2023-01-19T13:43:05.618857482Z"
371        }"#;
372        let message: CardanoDatabaseSnapshotMessage = serde_json::from_str(json).expect(
373            "This JSON is expected to be successfully parsed into a CardanoDatabaseSnapshotMessage instance.",
374        );
375
376        assert_eq!(message.digests.locations.len(), 1);
377        assert_eq!(DigestLocation::Unknown, message.digests.locations[0]);
378
379        assert_eq!(message.immutables.locations.len(), 1);
380        assert_eq!(ImmutablesLocation::Unknown, message.immutables.locations[0]);
381
382        assert_eq!(message.ancillary.locations.len(), 1);
383        assert_eq!(AncillaryLocation::Unknown, message.ancillary.locations[0]);
384    }
385
386    mod sanitize_immutable_locations {
387        use super::*;
388
389        #[test]
390        fn succeeds_and_leave_all_locations_intact_if_no_unknown_location() {
391            let immutable_locations = ImmutablesMessagePart {
392                locations: vec![ImmutablesLocation::CloudStorage {
393                    uri: MultiFilesUri::Template(TemplateUri(
394                        "http://whatever/{immutable_file_number}.tar.gz".to_string(),
395                    )),
396                    compression_algorithm: None,
397                }],
398                average_size_uncompressed: 512,
399            };
400
401            let sanitize_locations = immutable_locations
402                .sanitized_locations()
403                .expect("Should succeed since there are no unknown locations.");
404            assert_eq!(sanitize_locations, immutable_locations.locations);
405        }
406
407        #[test]
408        fn succeeds_and_remove_unknown_locations_if_some_locations_are_not_unknown() {
409            let immutable_locations = ImmutablesMessagePart {
410                locations: vec![
411                    ImmutablesLocation::CloudStorage {
412                        uri: MultiFilesUri::Template(TemplateUri(
413                            "http://whatever/{immutable_file_number}.tar.gz".to_string(),
414                        )),
415                        compression_algorithm: None,
416                    },
417                    ImmutablesLocation::Unknown,
418                ],
419                average_size_uncompressed: 512,
420            };
421
422            let sanitize_locations = immutable_locations
423                .sanitized_locations()
424                .expect("Should succeed since not all locations are unknown.");
425            assert_eq!(
426                sanitize_locations,
427                vec![ImmutablesLocation::CloudStorage {
428                    uri: MultiFilesUri::Template(TemplateUri(
429                        "http://whatever/{immutable_file_number}.tar.gz".to_string(),
430                    )),
431                    compression_algorithm: None,
432                }]
433            );
434        }
435
436        #[test]
437        fn fails_if_all_locations_are_unknown() {
438            ImmutablesMessagePart {
439                locations: vec![ImmutablesLocation::Unknown],
440                average_size_uncompressed: 512,
441            }
442            .sanitized_locations()
443            .expect_err("Should fail since all locations are unknown.");
444        }
445    }
446
447    mod sanitize_ancillary_locations {
448        use super::*;
449
450        #[test]
451        fn succeeds_and_leave_all_locations_intact_if_no_unknown_location() {
452            let ancillary_locations = AncillaryMessagePart {
453                locations: vec![AncillaryLocation::CloudStorage {
454                    uri: "http://whatever/ancillary.tar.gz".to_string(),
455                    compression_algorithm: None,
456                }],
457                size_uncompressed: 1024,
458            };
459
460            let sanitize_locations = ancillary_locations
461                .sanitized_locations()
462                .expect("Should succeed since there are no unknown locations.");
463            assert_eq!(sanitize_locations, ancillary_locations.locations);
464        }
465
466        #[test]
467        fn succeeds_and_remove_unknown_locations_if_some_locations_are_not_unknown() {
468            let ancillary_locations = AncillaryMessagePart {
469                locations: vec![
470                    AncillaryLocation::CloudStorage {
471                        uri: "http://whatever/digests.tar.gz".to_string(),
472                        compression_algorithm: None,
473                    },
474                    AncillaryLocation::Unknown,
475                ],
476                size_uncompressed: 512,
477            };
478
479            let sanitize_locations = ancillary_locations
480                .sanitized_locations()
481                .expect("Should succeed since not all locations are unknown.");
482            assert_eq!(
483                sanitize_locations,
484                vec![AncillaryLocation::CloudStorage {
485                    uri: "http://whatever/digests.tar.gz".to_string(),
486                    compression_algorithm: None,
487                }]
488            );
489        }
490
491        #[test]
492        fn fails_if_all_locations_are_unknown() {
493            AncillaryMessagePart {
494                locations: vec![AncillaryLocation::Unknown],
495                size_uncompressed: 512,
496            }
497            .sanitized_locations()
498            .expect_err("Should fail since all locations are unknown.");
499        }
500    }
501
502    mod sanitize_digests_locations {
503        use super::*;
504
505        #[test]
506        fn succeeds_and_leave_all_locations_intact_if_no_unknown_location() {
507            let digests_locations = DigestsMessagePart {
508                locations: vec![DigestLocation::CloudStorage {
509                    uri: "http://whatever/digests.tar.gz".to_string(),
510                    compression_algorithm: None,
511                }],
512                size_uncompressed: 512,
513            };
514
515            let sanitize_locations = digests_locations
516                .sanitized_locations()
517                .expect("Should succeed since there are no unknown locations.");
518            assert_eq!(sanitize_locations, digests_locations.locations);
519        }
520
521        #[test]
522        fn succeeds_and_remove_unknown_locations_if_some_locations_are_not_unknown() {
523            let digests_locations = DigestsMessagePart {
524                locations: vec![
525                    DigestLocation::CloudStorage {
526                        uri: "http://whatever/digests.tar.gz".to_string(),
527                        compression_algorithm: None,
528                    },
529                    DigestLocation::Unknown,
530                ],
531                size_uncompressed: 512,
532            };
533
534            let sanitize_locations = digests_locations
535                .sanitized_locations()
536                .expect("Should succeed since not all locations are unknown.");
537            assert_eq!(
538                sanitize_locations,
539                vec![DigestLocation::CloudStorage {
540                    uri: "http://whatever/digests.tar.gz".to_string(),
541                    compression_algorithm: None,
542                }]
543            );
544        }
545
546        #[test]
547        fn fails_if_all_locations_are_unknown() {
548            DigestsMessagePart {
549                locations: vec![DigestLocation::Unknown],
550                size_uncompressed: 512,
551            }
552            .sanitized_locations()
553            .expect_err("Should fail since all locations are unknown.");
554        }
555    }
556}