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