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