1use semver::Version;
2use serde::{Deserialize, Serialize};
3use sha2::{Digest, Sha256};
4
5use crate::entities::{CardanoDbBeacon, CompressionAlgorithm};
6
7use super::{CardanoNetwork, MultiFilesUri};
8
9pub struct CardanoDatabaseSnapshotArtifactData {
11 pub total_db_size_uncompressed: u64,
13
14 pub digests: DigestsLocations,
16
17 pub immutables: ImmutablesLocations,
19
20 pub ancillary: AncillaryLocations,
22}
23
24#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
26pub struct CardanoDatabaseSnapshot {
27 pub hash: String,
29
30 pub merkle_root: String,
32
33 pub network: CardanoNetwork,
35
36 pub beacon: CardanoDbBeacon,
38
39 pub total_db_size_uncompressed: u64,
41
42 pub digests: DigestsLocations,
44
45 pub immutables: ImmutablesLocations,
47
48 pub ancillary: AncillaryLocations,
50
51 pub cardano_node_version: String,
53}
54
55impl CardanoDatabaseSnapshot {
56 pub fn new(
58 merkle_root: String,
59 network: CardanoNetwork,
60 beacon: CardanoDbBeacon,
61 content: CardanoDatabaseSnapshotArtifactData,
62 cardano_node_version: &Version,
63 ) -> Self {
64 let cardano_node_version = format!("{cardano_node_version}");
65 let mut cardano_database_snapshot = Self {
66 hash: "".to_string(),
67 merkle_root,
68 network,
69 beacon,
70 digests: content.digests,
71 immutables: content.immutables,
72 ancillary: content.ancillary,
73 total_db_size_uncompressed: content.total_db_size_uncompressed,
74 cardano_node_version,
75 };
76 cardano_database_snapshot.hash = cardano_database_snapshot.compute_hash();
77
78 cardano_database_snapshot
79 }
80
81 fn compute_hash(&self) -> String {
83 let mut hasher = Sha256::new();
84 hasher.update(self.beacon.epoch.to_be_bytes());
85 hasher.update(self.merkle_root.as_bytes());
86
87 hex::encode(hasher.finalize())
88 }
89}
90
91#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
93#[serde(rename_all = "snake_case", tag = "type")]
94pub enum DigestLocation {
95 CloudStorage {
97 uri: String,
99
100 #[serde(skip_serializing_if = "Option::is_none")]
102 compression_algorithm: Option<CompressionAlgorithm>,
103 },
104 Aggregator {
106 uri: String,
108 },
109 #[serde(other)]
111 Unknown,
112}
113
114#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
116#[serde(rename_all = "snake_case", tag = "type")]
117pub enum ImmutablesLocation {
118 CloudStorage {
120 uri: MultiFilesUri,
122
123 #[serde(skip_serializing_if = "Option::is_none")]
125 compression_algorithm: Option<CompressionAlgorithm>,
126 },
127 #[serde(other)]
129 Unknown,
130}
131
132#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
134#[serde(rename_all = "snake_case", tag = "type")]
135pub enum AncillaryLocation {
136 CloudStorage {
138 uri: String,
140
141 #[serde(skip_serializing_if = "Option::is_none")]
143 compression_algorithm: Option<CompressionAlgorithm>,
144 },
145 #[serde(other)]
147 Unknown,
148}
149
150#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
152pub struct DigestsLocations {
153 pub size_uncompressed: u64,
155
156 pub locations: Vec<DigestLocation>,
158}
159
160#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
162pub struct ImmutablesLocations {
163 pub average_size_uncompressed: u64,
165
166 pub locations: Vec<ImmutablesLocation>,
168}
169
170#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
172pub struct AncillaryLocations {
173 pub size_uncompressed: u64,
175
176 pub locations: Vec<AncillaryLocation>,
178}
179
180#[cfg(test)]
181mod tests {
182 use crate::entities::TemplateUri;
183
184 use super::*;
185
186 fn dummy() -> CardanoDatabaseSnapshot {
187 CardanoDatabaseSnapshot::new(
188 "mk-root-1111111111".to_string(),
189 CardanoNetwork::DevNet(87),
190 CardanoDbBeacon::new(2222, 55555),
191 CardanoDatabaseSnapshotArtifactData {
192 total_db_size_uncompressed: 0,
193 digests: DigestsLocations {
194 size_uncompressed: 0,
195 locations: vec![],
196 },
197 immutables: ImmutablesLocations {
198 average_size_uncompressed: 0,
199 locations: vec![],
200 },
201 ancillary: AncillaryLocations {
202 size_uncompressed: 0,
203 locations: vec![],
204 },
205 },
206 &Version::new(1, 0, 0),
207 )
208 }
209
210 mod cardano_database_snapshot_compute_hash {
211 use super::*;
212
213 #[test]
214 fn test_cardano_database_snapshot_compute_hash() {
215 let cardano_database_snapshot = CardanoDatabaseSnapshot {
216 merkle_root: "mk-root-123".to_string(),
217 beacon: CardanoDbBeacon::new(123, 98),
218 ..dummy()
219 };
220
221 assert_eq!(
222 "b1cc5e0deaa7856e8e811e349d6e639fa667aa70288602955f438c5893ce29c8",
223 cardano_database_snapshot.compute_hash()
224 );
225 }
226
227 #[test]
228 fn compute_hash_returns_same_hash_with_same_cardano_database_snapshot() {
229 assert_eq!(
230 CardanoDatabaseSnapshot {
231 merkle_root: "mk-root-123".to_string(),
232 beacon: CardanoDbBeacon::new(123, 98),
233 ..dummy()
234 }
235 .compute_hash(),
236 CardanoDatabaseSnapshot {
237 merkle_root: "mk-root-123".to_string(),
238 beacon: CardanoDbBeacon::new(123, 98),
239 ..dummy()
240 }
241 .compute_hash()
242 );
243 }
244
245 #[test]
246 fn compute_hash_returns_different_hash_with_different_merkle_root() {
247 assert_ne!(
248 CardanoDatabaseSnapshot {
249 merkle_root: "mk-root-123".to_string(),
250 beacon: CardanoDbBeacon::new(123, 98),
251 ..dummy()
252 }
253 .compute_hash(),
254 CardanoDatabaseSnapshot {
255 merkle_root: "mk-root-456".to_string(),
256 beacon: CardanoDbBeacon::new(123, 98),
257 ..dummy()
258 }
259 .compute_hash()
260 );
261 }
262
263 #[test]
264 fn compute_hash_returns_different_hash_with_same_epoch_in_beacon() {
265 assert_eq!(
266 CardanoDatabaseSnapshot {
267 merkle_root: "mk-root-123".to_string(),
268 beacon: CardanoDbBeacon::new(123, 98),
269 ..dummy()
270 }
271 .compute_hash(),
272 CardanoDatabaseSnapshot {
273 merkle_root: "mk-root-123".to_string(),
274 beacon: CardanoDbBeacon::new(123, 12),
275 ..dummy()
276 }
277 .compute_hash()
278 );
279 }
280
281 #[test]
282 fn compute_hash_returns_different_hash_with_different_beacon() {
283 assert_ne!(
284 CardanoDatabaseSnapshot {
285 merkle_root: "mk-root-123".to_string(),
286 beacon: CardanoDbBeacon::new(123, 98),
287 ..dummy()
288 }
289 .compute_hash(),
290 CardanoDatabaseSnapshot {
291 merkle_root: "mk-root-123".to_string(),
292 beacon: CardanoDbBeacon::new(456, 98),
293 ..dummy()
294 }
295 .compute_hash()
296 );
297 }
298 }
299
300 #[test]
301 fn should_not_display_compression_algorithm_in_json_ancillary_location_when_none() {
302 let json = serde_json::json!(AncillaryLocation::CloudStorage {
303 uri: "https://example.com".to_string(),
304 compression_algorithm: None,
305 });
306 assert_eq!(
307 json.to_string(),
308 r#"{"type":"cloud_storage","uri":"https://example.com"}"#
309 );
310 }
311
312 #[test]
313 fn should_not_display_compression_algorithm_in_json_digests_location_when_none() {
314 let json = serde_json::json!(DigestLocation::CloudStorage {
315 uri: "https://example.com".to_string(),
316 compression_algorithm: None,
317 });
318 assert_eq!(
319 json.to_string(),
320 r#"{"type":"cloud_storage","uri":"https://example.com"}"#
321 );
322 }
323
324 #[test]
325 fn should_not_display_compression_algorithm_in_json_immutable_location_when_none() {
326 let json = serde_json::json!(ImmutablesLocation::CloudStorage {
327 uri: MultiFilesUri::Template(TemplateUri("https://example.com".to_string())),
328 compression_algorithm: None,
329 });
330 assert_eq!(
331 json.to_string(),
332 r#"{"type":"cloud_storage","uri":{"Template":"https://example.com"}}"#
333 );
334 }
335}