1use std::collections::BTreeSet;
2use std::str::FromStr;
3use std::time::Duration;
4
5use anyhow::anyhow;
6use digest::Update;
7use serde::{Deserialize, Serialize};
8use sha2::Sha256;
9use strum::{AsRefStr, Display, EnumDiscriminants, EnumIter, EnumString, IntoEnumIterator};
10
11use crate::{
12 StdResult,
13 crypto_helper::{TryFromBytes, TryToBytes},
14};
15
16use super::{BlockNumber, CardanoDbBeacon, Epoch};
17
18const ENTITY_TYPE_MITHRIL_STAKE_DISTRIBUTION: usize = 0;
20
21const ENTITY_TYPE_CARDANO_STAKE_DISTRIBUTION: usize = 1;
23
24const ENTITY_TYPE_CARDANO_IMMUTABLE_FILES_FULL: usize = 2;
26
27const ENTITY_TYPE_CARDANO_TRANSACTIONS: usize = 3;
29
30const ENTITY_TYPE_CARDANO_DATABASE: usize = 4;
32
33#[derive(Display, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, EnumDiscriminants)]
41#[strum(serialize_all = "PascalCase")]
42#[strum_discriminants(doc = "The discriminants of the SignedEntityType enum.")]
43#[strum_discriminants(derive(
44 Display,
45 EnumString,
46 AsRefStr,
47 Serialize,
48 Deserialize,
49 PartialOrd,
50 Ord,
51 EnumIter,
52))]
53pub enum SignedEntityType {
54 MithrilStakeDistribution(Epoch),
56
57 CardanoStakeDistribution(Epoch),
59
60 CardanoImmutableFilesFull(CardanoDbBeacon),
62
63 CardanoDatabase(CardanoDbBeacon),
65
66 CardanoTransactions(Epoch, BlockNumber),
68}
69
70impl SignedEntityType {
71 pub fn genesis(epoch: Epoch) -> Self {
73 Self::MithrilStakeDistribution(epoch)
74 }
75
76 pub fn get_epoch(&self) -> Epoch {
78 match self {
79 Self::CardanoImmutableFilesFull(b) | Self::CardanoDatabase(b) => b.epoch,
80 Self::CardanoStakeDistribution(e)
81 | Self::MithrilStakeDistribution(e)
82 | Self::CardanoTransactions(e, _) => *e,
83 }
84 }
85
86 pub fn get_epoch_when_signed_entity_type_is_signed(&self) -> Epoch {
88 match self {
89 Self::CardanoImmutableFilesFull(beacon) | Self::CardanoDatabase(beacon) => beacon.epoch,
90 Self::CardanoStakeDistribution(epoch) => epoch.next(),
91 Self::MithrilStakeDistribution(epoch) | Self::CardanoTransactions(epoch, _) => *epoch,
92 }
93 }
94
95 pub fn index(&self) -> usize {
97 match self {
98 Self::MithrilStakeDistribution(_) => ENTITY_TYPE_MITHRIL_STAKE_DISTRIBUTION,
99 Self::CardanoStakeDistribution(_) => ENTITY_TYPE_CARDANO_STAKE_DISTRIBUTION,
100 Self::CardanoImmutableFilesFull(_) => ENTITY_TYPE_CARDANO_IMMUTABLE_FILES_FULL,
101 Self::CardanoTransactions(_, _) => ENTITY_TYPE_CARDANO_TRANSACTIONS,
102 Self::CardanoDatabase(_) => ENTITY_TYPE_CARDANO_DATABASE,
103 }
104 }
105
106 pub fn get_json_beacon(&self) -> StdResult<String> {
108 let value = match self {
109 Self::CardanoImmutableFilesFull(value) | Self::CardanoDatabase(value) => {
110 serde_json::to_string(value)?
111 }
112 Self::CardanoStakeDistribution(value) | Self::MithrilStakeDistribution(value) => {
113 serde_json::to_string(value)?
114 }
115 Self::CardanoTransactions(epoch, block_number) => {
116 let json = serde_json::json!({
117 "epoch": epoch,
118 "block_number": block_number,
119 });
120 serde_json::to_string(&json)?
121 }
122 };
123
124 Ok(value)
125 }
126
127 pub fn get_open_message_timeout(&self) -> Option<Duration> {
129 match self {
130 Self::MithrilStakeDistribution(_) => Some(Duration::from_secs(3600)),
131 Self::CardanoImmutableFilesFull(_) => Some(Duration::from_secs(600)),
132 Self::CardanoStakeDistribution(_) => Some(Duration::from_secs(1800)),
133 Self::CardanoTransactions(_, _) => Some(Duration::from_secs(600)),
134 Self::CardanoDatabase(_) => Some(Duration::from_secs(600)),
135 }
136 }
137
138 pub(crate) fn feed_hash(&self, hasher: &mut Sha256) {
139 match self {
140 SignedEntityType::MithrilStakeDistribution(epoch)
141 | SignedEntityType::CardanoStakeDistribution(epoch) => {
142 hasher.update(&epoch.to_be_bytes())
143 }
144 SignedEntityType::CardanoImmutableFilesFull(db_beacon)
145 | SignedEntityType::CardanoDatabase(db_beacon) => {
146 hasher.update(&db_beacon.epoch.to_be_bytes());
147 hasher.update(&db_beacon.immutable_file_number.to_be_bytes());
148 }
149 SignedEntityType::CardanoTransactions(epoch, block_number) => {
150 hasher.update(&epoch.to_be_bytes());
151 hasher.update(&block_number.to_be_bytes())
152 }
153 }
154 }
155}
156
157impl TryFromBytes for SignedEntityType {
158 fn try_from_bytes(bytes: &[u8]) -> StdResult<Self> {
159 let (res, _) =
160 bincode::serde::decode_from_slice::<Self, _>(bytes, bincode::config::standard())?;
161
162 Ok(res)
163 }
164}
165
166impl TryToBytes for SignedEntityType {
167 fn to_bytes_vec(&self) -> StdResult<Vec<u8>> {
168 bincode::serde::encode_to_vec(self, bincode::config::standard()).map_err(|e| e.into())
169 }
170}
171
172impl SignedEntityTypeDiscriminants {
173 pub fn all() -> BTreeSet<Self> {
175 SignedEntityTypeDiscriminants::iter().collect()
176 }
177
178 pub fn index(&self) -> usize {
180 match self {
181 Self::MithrilStakeDistribution => ENTITY_TYPE_MITHRIL_STAKE_DISTRIBUTION,
182 Self::CardanoStakeDistribution => ENTITY_TYPE_CARDANO_STAKE_DISTRIBUTION,
183 Self::CardanoImmutableFilesFull => ENTITY_TYPE_CARDANO_IMMUTABLE_FILES_FULL,
184 Self::CardanoTransactions => ENTITY_TYPE_CARDANO_TRANSACTIONS,
185 Self::CardanoDatabase => ENTITY_TYPE_CARDANO_DATABASE,
186 }
187 }
188
189 pub fn from_id(signed_entity_type_id: usize) -> StdResult<SignedEntityTypeDiscriminants> {
191 match signed_entity_type_id {
192 ENTITY_TYPE_MITHRIL_STAKE_DISTRIBUTION => Ok(Self::MithrilStakeDistribution),
193 ENTITY_TYPE_CARDANO_STAKE_DISTRIBUTION => Ok(Self::CardanoStakeDistribution),
194 ENTITY_TYPE_CARDANO_IMMUTABLE_FILES_FULL => Ok(Self::CardanoImmutableFilesFull),
195 ENTITY_TYPE_CARDANO_TRANSACTIONS => Ok(Self::CardanoTransactions),
196 ENTITY_TYPE_CARDANO_DATABASE => Ok(Self::CardanoDatabase),
197 index => Err(anyhow!("Invalid entity_type_id {index}.")),
198 }
199 }
200
201 pub fn parse_list<T: AsRef<str>>(discriminants_string: T) -> StdResult<BTreeSet<Self>> {
206 let mut discriminants = BTreeSet::new();
207 let mut invalid_discriminants = Vec::new();
208
209 for name in discriminants_string
210 .as_ref()
211 .split(',')
212 .map(str::trim)
213 .filter(|s| !s.is_empty())
214 {
215 match Self::from_str(name) {
216 Ok(discriminant) => {
217 discriminants.insert(discriminant);
218 }
219 Err(_) => {
220 invalid_discriminants.push(name);
221 }
222 }
223 }
224
225 if invalid_discriminants.is_empty() {
226 Ok(discriminants)
227 } else {
228 Err(anyhow!(Self::format_parse_list_error(
229 invalid_discriminants
230 )))
231 }
232 }
233
234 fn format_parse_list_error(invalid_discriminants: Vec<&str>) -> String {
235 format!(
236 r#"Invalid signed entity types discriminants: {}.
237
238Accepted values are (case-sensitive): {}."#,
239 invalid_discriminants.join(", "),
240 Self::accepted_discriminants()
241 )
242 }
243
244 fn accepted_discriminants() -> String {
245 Self::iter().map(|d| d.to_string()).collect::<Vec<_>>().join(", ")
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use digest::Digest;
252
253 use crate::test::assert_same_json;
254
255 use super::*;
256
257 #[test]
258 fn get_epoch_when_signed_entity_type_is_signed_for_cardano_stake_distribution_return_epoch_with_offset()
259 {
260 let signed_entity_type = SignedEntityType::CardanoStakeDistribution(Epoch(3));
261
262 assert_eq!(
263 signed_entity_type.get_epoch_when_signed_entity_type_is_signed(),
264 Epoch(4)
265 );
266 }
267
268 #[test]
269 fn get_epoch_when_signed_entity_type_is_signed_for_mithril_stake_distribution_return_epoch_stored_in_signed_entity_type()
270 {
271 let signed_entity_type = SignedEntityType::MithrilStakeDistribution(Epoch(3));
272 assert_eq!(
273 signed_entity_type.get_epoch_when_signed_entity_type_is_signed(),
274 Epoch(3)
275 );
276 }
277
278 #[test]
279 fn get_epoch_when_signed_entity_type_is_signed_for_cardano_immutable_files_full_return_epoch_stored_in_signed_entity_type()
280 {
281 let signed_entity_type =
282 SignedEntityType::CardanoImmutableFilesFull(CardanoDbBeacon::new(3, 100));
283 assert_eq!(
284 signed_entity_type.get_epoch_when_signed_entity_type_is_signed(),
285 Epoch(3)
286 );
287 }
288
289 #[test]
290 fn get_epoch_when_signed_entity_type_is_signed_for_cardano_transactions_return_epoch_stored_in_signed_entity_type()
291 {
292 let signed_entity_type = SignedEntityType::CardanoTransactions(Epoch(3), BlockNumber(77));
293 assert_eq!(
294 signed_entity_type.get_epoch_when_signed_entity_type_is_signed(),
295 Epoch(3)
296 );
297 }
298
299 #[test]
300 fn get_epoch_when_signed_entity_type_is_signed_for_cardano_database_return_epoch_stored_in_signed_entity_type()
301 {
302 let signed_entity_type = SignedEntityType::CardanoDatabase(CardanoDbBeacon::new(12, 987));
303 assert_eq!(
304 signed_entity_type.get_epoch_when_signed_entity_type_is_signed(),
305 Epoch(12)
306 );
307 }
308
309 #[test]
310 fn verify_signed_entity_type_properties_are_included_in_computed_hash() {
311 fn hash(signed_entity_type: SignedEntityType) -> String {
312 let mut hasher = Sha256::new();
313 signed_entity_type.feed_hash(&mut hasher);
314 hex::encode(hasher.finalize())
315 }
316
317 let reference_hash = hash(SignedEntityType::MithrilStakeDistribution(Epoch(5)));
318 assert_ne!(
319 reference_hash,
320 hash(SignedEntityType::MithrilStakeDistribution(Epoch(15)))
321 );
322
323 let reference_hash = hash(SignedEntityType::CardanoStakeDistribution(Epoch(5)));
324 assert_ne!(
325 reference_hash,
326 hash(SignedEntityType::CardanoStakeDistribution(Epoch(15)))
327 );
328
329 let reference_hash = hash(SignedEntityType::CardanoImmutableFilesFull(
330 CardanoDbBeacon::new(5, 100),
331 ));
332 assert_ne!(
333 reference_hash,
334 hash(SignedEntityType::CardanoImmutableFilesFull(
335 CardanoDbBeacon::new(20, 100)
336 ))
337 );
338 assert_ne!(
339 reference_hash,
340 hash(SignedEntityType::CardanoImmutableFilesFull(
341 CardanoDbBeacon::new(5, 507)
342 ))
343 );
344
345 let reference_hash = hash(SignedEntityType::CardanoTransactions(
346 Epoch(35),
347 BlockNumber(77),
348 ));
349 assert_ne!(
350 reference_hash,
351 hash(SignedEntityType::CardanoTransactions(
352 Epoch(3),
353 BlockNumber(77)
354 ))
355 );
356 assert_ne!(
357 reference_hash,
358 hash(SignedEntityType::CardanoTransactions(
359 Epoch(35),
360 BlockNumber(98765)
361 ))
362 );
363
364 let reference_hash = hash(SignedEntityType::CardanoDatabase(CardanoDbBeacon::new(
365 12, 987,
366 )));
367 assert_ne!(
368 reference_hash,
369 hash(SignedEntityType::CardanoDatabase(CardanoDbBeacon::new(
370 98, 987
371 )))
372 );
373 assert_ne!(
374 reference_hash,
375 hash(SignedEntityType::CardanoDatabase(CardanoDbBeacon::new(
376 12, 123
377 )))
378 );
379 }
380
381 #[test]
382 fn get_open_message_timeout() {
383 assert_eq!(
384 SignedEntityType::MithrilStakeDistribution(Epoch(1)).get_open_message_timeout(),
385 Some(Duration::from_secs(3600))
386 );
387 assert_eq!(
388 SignedEntityType::CardanoImmutableFilesFull(CardanoDbBeacon::new(1, 1))
389 .get_open_message_timeout(),
390 Some(Duration::from_secs(600))
391 );
392 assert_eq!(
393 SignedEntityType::CardanoStakeDistribution(Epoch(1)).get_open_message_timeout(),
394 Some(Duration::from_secs(1800))
395 );
396 assert_eq!(
397 SignedEntityType::CardanoTransactions(Epoch(1), BlockNumber(1))
398 .get_open_message_timeout(),
399 Some(Duration::from_secs(600))
400 );
401 assert_eq!(
402 SignedEntityType::CardanoDatabase(CardanoDbBeacon::new(1, 1))
403 .get_open_message_timeout(),
404 Some(Duration::from_secs(600))
405 );
406 }
407
408 #[test]
409 fn serialize_beacon_to_json() {
410 let cardano_stake_distribution_json = SignedEntityType::CardanoStakeDistribution(Epoch(25))
411 .get_json_beacon()
412 .unwrap();
413 assert_same_json!("25", &cardano_stake_distribution_json);
414
415 let cardano_transactions_json =
416 SignedEntityType::CardanoTransactions(Epoch(35), BlockNumber(77))
417 .get_json_beacon()
418 .unwrap();
419 assert_same_json!(
420 r#"{"epoch":35,"block_number":77}"#,
421 &cardano_transactions_json
422 );
423
424 let cardano_immutable_files_full_json =
425 SignedEntityType::CardanoImmutableFilesFull(CardanoDbBeacon::new(5, 100))
426 .get_json_beacon()
427 .unwrap();
428 assert_same_json!(
429 r#"{"epoch":5,"immutable_file_number":100}"#,
430 &cardano_immutable_files_full_json
431 );
432
433 let msd_json = SignedEntityType::MithrilStakeDistribution(Epoch(15))
434 .get_json_beacon()
435 .unwrap();
436 assert_same_json!("15", &msd_json);
437
438 let cardano_database_full_json =
439 SignedEntityType::CardanoDatabase(CardanoDbBeacon::new(12, 987))
440 .get_json_beacon()
441 .unwrap();
442 assert_same_json!(
443 r#"{"epoch":12,"immutable_file_number":987}"#,
444 &cardano_database_full_json
445 );
446 }
447
448 #[test]
449 fn bytes_encoding() {
450 let cardano_stake_distribution = SignedEntityType::CardanoStakeDistribution(Epoch(25));
451 let cardano_stake_distribution_bytes = cardano_stake_distribution.to_bytes_vec().unwrap();
452 let cardano_stake_distribution_from_bytes =
453 SignedEntityType::try_from_bytes(&cardano_stake_distribution_bytes).unwrap();
454
455 assert_eq!(
456 cardano_stake_distribution,
457 cardano_stake_distribution_from_bytes
458 );
459 }
460
461 #[test]
464 fn ordering_discriminant() {
465 let mut list = vec![
466 SignedEntityTypeDiscriminants::CardanoStakeDistribution,
467 SignedEntityTypeDiscriminants::CardanoDatabase,
468 SignedEntityTypeDiscriminants::CardanoTransactions,
469 SignedEntityTypeDiscriminants::CardanoImmutableFilesFull,
470 SignedEntityTypeDiscriminants::MithrilStakeDistribution,
471 ];
472 list.sort();
473
474 assert_eq!(
475 list,
476 vec![
477 SignedEntityTypeDiscriminants::MithrilStakeDistribution,
478 SignedEntityTypeDiscriminants::CardanoStakeDistribution,
479 SignedEntityTypeDiscriminants::CardanoImmutableFilesFull,
480 SignedEntityTypeDiscriminants::CardanoDatabase,
481 SignedEntityTypeDiscriminants::CardanoTransactions,
482 ]
483 );
484 }
485
486 #[test]
487 fn ordering_discriminant_with_duplicate() {
488 let mut list = vec![
489 SignedEntityTypeDiscriminants::CardanoDatabase,
490 SignedEntityTypeDiscriminants::CardanoStakeDistribution,
491 SignedEntityTypeDiscriminants::CardanoDatabase,
492 SignedEntityTypeDiscriminants::MithrilStakeDistribution,
493 SignedEntityTypeDiscriminants::CardanoTransactions,
494 SignedEntityTypeDiscriminants::CardanoStakeDistribution,
495 SignedEntityTypeDiscriminants::CardanoImmutableFilesFull,
496 SignedEntityTypeDiscriminants::MithrilStakeDistribution,
497 SignedEntityTypeDiscriminants::MithrilStakeDistribution,
498 ];
499 list.sort();
500
501 assert_eq!(
502 list,
503 vec![
504 SignedEntityTypeDiscriminants::MithrilStakeDistribution,
505 SignedEntityTypeDiscriminants::MithrilStakeDistribution,
506 SignedEntityTypeDiscriminants::MithrilStakeDistribution,
507 SignedEntityTypeDiscriminants::CardanoStakeDistribution,
508 SignedEntityTypeDiscriminants::CardanoStakeDistribution,
509 SignedEntityTypeDiscriminants::CardanoImmutableFilesFull,
510 SignedEntityTypeDiscriminants::CardanoDatabase,
511 SignedEntityTypeDiscriminants::CardanoDatabase,
512 SignedEntityTypeDiscriminants::CardanoTransactions,
513 ]
514 );
515 }
516
517 #[test]
518 fn parse_signed_entity_types_discriminants_discriminant_without_values() {
519 let discriminants_str = "";
520 let discriminants = SignedEntityTypeDiscriminants::parse_list(discriminants_str).unwrap();
521
522 assert_eq!(BTreeSet::new(), discriminants);
523
524 let discriminants_str = " ";
525 let discriminants = SignedEntityTypeDiscriminants::parse_list(discriminants_str).unwrap();
526
527 assert_eq!(BTreeSet::new(), discriminants);
528 }
529
530 #[test]
531 fn parse_signed_entity_types_discriminants_with_correctly_formed_values() {
532 let discriminants_str = "MithrilStakeDistribution,CardanoImmutableFilesFull";
533 let discriminants = SignedEntityTypeDiscriminants::parse_list(discriminants_str).unwrap();
534
535 assert_eq!(
536 BTreeSet::from([
537 SignedEntityTypeDiscriminants::MithrilStakeDistribution,
538 SignedEntityTypeDiscriminants::CardanoImmutableFilesFull,
539 ]),
540 discriminants
541 );
542 }
543
544 #[test]
545 fn parse_signed_entity_types_discriminants_should_trim_values() {
546 let discriminants_str =
547 "MithrilStakeDistribution , CardanoImmutableFilesFull , CardanoTransactions ";
548 let discriminants = SignedEntityTypeDiscriminants::parse_list(discriminants_str).unwrap();
549
550 assert_eq!(
551 BTreeSet::from([
552 SignedEntityTypeDiscriminants::MithrilStakeDistribution,
553 SignedEntityTypeDiscriminants::CardanoTransactions,
554 SignedEntityTypeDiscriminants::CardanoImmutableFilesFull,
555 ]),
556 discriminants
557 );
558 }
559
560 #[test]
561 fn parse_signed_entity_types_discriminants_should_remove_duplicates() {
562 let discriminants_str =
563 "CardanoTransactions,CardanoTransactions,CardanoTransactions,CardanoTransactions";
564 let discriminant = SignedEntityTypeDiscriminants::parse_list(discriminants_str).unwrap();
565
566 assert_eq!(
567 BTreeSet::from([SignedEntityTypeDiscriminants::CardanoTransactions]),
568 discriminant
569 );
570 }
571
572 #[test]
573 fn parse_signed_entity_types_discriminants_should_be_case_sensitive() {
574 let discriminants_str = "mithrilstakedistribution,CARDANOIMMUTABLEFILESFULL";
575 let error = SignedEntityTypeDiscriminants::parse_list(discriminants_str).unwrap_err();
576
577 assert_eq!(
578 SignedEntityTypeDiscriminants::format_parse_list_error(vec![
579 "mithrilstakedistribution",
580 "CARDANOIMMUTABLEFILESFULL"
581 ]),
582 error.to_string()
583 );
584 }
585
586 #[test]
587 fn parse_signed_entity_types_discriminants_should_not_return_unknown_signed_entity_types() {
588 let discriminants_str = "Unknown";
589 let error = SignedEntityTypeDiscriminants::parse_list(discriminants_str).unwrap_err();
590
591 assert_eq!(
592 SignedEntityTypeDiscriminants::format_parse_list_error(vec!["Unknown"]),
593 error.to_string()
594 );
595 }
596
597 #[test]
598 fn parse_signed_entity_types_discriminants_should_fail_if_there_is_at_least_one_invalid_value()
599 {
600 let discriminants_str = "CardanoTransactions,Invalid,MithrilStakeDistribution";
601 let error = SignedEntityTypeDiscriminants::parse_list(discriminants_str).unwrap_err();
602
603 assert_eq!(
604 SignedEntityTypeDiscriminants::format_parse_list_error(vec!["Invalid"]),
605 error.to_string()
606 );
607 }
608
609 #[test]
610 fn parse_list_error_format_to_an_useful_message() {
611 let invalid_discriminants = vec!["Unknown", "Invalid"];
612 let error = SignedEntityTypeDiscriminants::format_parse_list_error(invalid_discriminants);
613
614 assert_eq!(
615 format!(
616 r#"Invalid signed entity types discriminants: Unknown, Invalid.
617
618Accepted values are (case-sensitive): {}."#,
619 SignedEntityTypeDiscriminants::accepted_discriminants()
620 ),
621 error
622 );
623 }
624}