mithril_common/entities/
signed_entity_type.rs

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
18/// Database representation of the SignedEntityType::MithrilStakeDistribution value
19const ENTITY_TYPE_MITHRIL_STAKE_DISTRIBUTION: usize = 0;
20
21/// Database representation of the SignedEntityType::CardanoStakeDistribution value
22const ENTITY_TYPE_CARDANO_STAKE_DISTRIBUTION: usize = 1;
23
24/// Database representation of the SignedEntityType::CardanoImmutableFilesFull value
25const ENTITY_TYPE_CARDANO_IMMUTABLE_FILES_FULL: usize = 2;
26
27/// Database representation of the SignedEntityType::CardanoTransactions value
28const ENTITY_TYPE_CARDANO_TRANSACTIONS: usize = 3;
29
30/// Database representation of the SignedEntityType::CardanoDatabase value
31const ENTITY_TYPE_CARDANO_DATABASE: usize = 4;
32
33/// Database representation of the SignedEntityType::CardanoBlocksTransactions value
34const ENTITY_TYPE_CARDANO_BLOCKS_TRANSACTIONS: usize = 5;
35
36/// The signed entity type that represents a type of data signed by the Mithril
37/// protocol Note: Each variant of this enum must be associated to an entry in
38/// the `signed_entity_type` table of the signer/aggregator nodes. The variant
39/// are identified by their discriminant (i.e. index in the enum), thus the
40/// modification of this type should only ever consist of appending new
41/// variants.
42// Important note: The order of the variants is important as it is used for the derived Ord trait.
43#[derive(Display, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, EnumDiscriminants)]
44#[strum(serialize_all = "PascalCase")]
45#[strum_discriminants(doc = "The discriminants of the SignedEntityType enum.")]
46#[strum_discriminants(derive(
47    Display,
48    EnumString,
49    AsRefStr,
50    Serialize,
51    Deserialize,
52    PartialOrd,
53    Ord,
54    EnumIter,
55))]
56pub enum SignedEntityType {
57    /// Mithril stake distribution
58    MithrilStakeDistribution(Epoch),
59
60    /// Cardano Stake Distribution
61    CardanoStakeDistribution(Epoch),
62
63    /// Full Cardano Immutable Files
64    CardanoImmutableFilesFull(CardanoDbBeacon),
65
66    /// Cardano Database
67    CardanoDatabase(CardanoDbBeacon),
68
69    /// Cardano Transactions
70    CardanoTransactions(Epoch, BlockNumber),
71
72    /// Cardano Blocks and Transactions
73    CardanoBlocksTransactions(Epoch, BlockNumber),
74}
75
76impl SignedEntityType {
77    /// Create a new signed entity type for a genesis certificate (a [Self::MithrilStakeDistribution])
78    pub fn genesis(epoch: Epoch) -> Self {
79        Self::MithrilStakeDistribution(epoch)
80    }
81
82    /// Return the epoch from the signed entity.
83    pub fn get_epoch(&self) -> Epoch {
84        match self {
85            Self::CardanoImmutableFilesFull(b) | Self::CardanoDatabase(b) => b.epoch,
86            Self::CardanoStakeDistribution(e)
87            | Self::MithrilStakeDistribution(e)
88            | Self::CardanoTransactions(e, _)
89            | Self::CardanoBlocksTransactions(e, _) => *e,
90        }
91    }
92
93    /// Return the epoch at which the signed entity type is signed.
94    pub fn get_epoch_when_signed_entity_type_is_signed(&self) -> Epoch {
95        match self {
96            Self::CardanoImmutableFilesFull(beacon) | Self::CardanoDatabase(beacon) => beacon.epoch,
97            Self::CardanoStakeDistribution(epoch) => epoch.next(),
98            Self::MithrilStakeDistribution(epoch)
99            | Self::CardanoTransactions(epoch, _)
100            | Self::CardanoBlocksTransactions(epoch, _) => *epoch,
101        }
102    }
103
104    /// Get the database value from enum's instance
105    pub fn index(&self) -> usize {
106        match self {
107            Self::MithrilStakeDistribution(..) => ENTITY_TYPE_MITHRIL_STAKE_DISTRIBUTION,
108            Self::CardanoStakeDistribution(..) => ENTITY_TYPE_CARDANO_STAKE_DISTRIBUTION,
109            Self::CardanoImmutableFilesFull(..) => ENTITY_TYPE_CARDANO_IMMUTABLE_FILES_FULL,
110            Self::CardanoTransactions(..) => ENTITY_TYPE_CARDANO_TRANSACTIONS,
111            Self::CardanoBlocksTransactions(..) => ENTITY_TYPE_CARDANO_BLOCKS_TRANSACTIONS,
112            Self::CardanoDatabase(..) => ENTITY_TYPE_CARDANO_DATABASE,
113        }
114    }
115
116    /// Return a JSON serialized value of the internal beacon
117    pub fn get_json_beacon(&self) -> StdResult<String> {
118        let value = match self {
119            Self::CardanoImmutableFilesFull(value) | Self::CardanoDatabase(value) => {
120                serde_json::to_string(value)?
121            }
122            Self::CardanoStakeDistribution(value) | Self::MithrilStakeDistribution(value) => {
123                serde_json::to_string(value)?
124            }
125            Self::CardanoTransactions(epoch, block_number)
126            | Self::CardanoBlocksTransactions(epoch, block_number) => {
127                let json = serde_json::json!({
128                    "epoch": epoch,
129                    "block_number": block_number,
130                });
131                serde_json::to_string(&json)?
132            }
133        };
134
135        Ok(value)
136    }
137
138    /// Return the associated open message timeout
139    pub fn get_open_message_timeout(&self) -> Option<Duration> {
140        match self {
141            Self::MithrilStakeDistribution(..) => Some(Duration::from_secs(3600)),
142            Self::CardanoImmutableFilesFull(..) => Some(Duration::from_secs(600)),
143            Self::CardanoStakeDistribution(..) => Some(Duration::from_secs(1800)),
144            Self::CardanoTransactions(..) => Some(Duration::from_secs(600)),
145            Self::CardanoBlocksTransactions(..) => Some(Duration::from_secs(600)),
146            Self::CardanoDatabase(..) => Some(Duration::from_secs(600)),
147        }
148    }
149
150    pub(crate) fn feed_hash(&self, hasher: &mut Sha256) {
151        // TODO: feed discriminant index to the hasher for all signed entity types (a migration of existing hashes will be needed)
152        if matches!(self, Self::CardanoBlocksTransactions(..)) {
153            // Leverage the database index value to differentiate types that share the same beacon
154            hasher.update(&self.index().to_be_bytes());
155        }
156
157        match self {
158            SignedEntityType::MithrilStakeDistribution(epoch)
159            | SignedEntityType::CardanoStakeDistribution(epoch) => {
160                hasher.update(&epoch.to_be_bytes())
161            }
162            SignedEntityType::CardanoImmutableFilesFull(db_beacon)
163            | SignedEntityType::CardanoDatabase(db_beacon) => {
164                hasher.update(&db_beacon.epoch.to_be_bytes());
165                hasher.update(&db_beacon.immutable_file_number.to_be_bytes());
166            }
167            SignedEntityType::CardanoTransactions(epoch, block_number)
168            | SignedEntityType::CardanoBlocksTransactions(epoch, block_number) => {
169                hasher.update(&epoch.to_be_bytes());
170                hasher.update(&block_number.to_be_bytes())
171            }
172        }
173    }
174}
175
176impl TryFromBytes for SignedEntityType {
177    fn try_from_bytes(bytes: &[u8]) -> StdResult<Self> {
178        let (res, _) =
179            bincode::serde::decode_from_slice::<Self, _>(bytes, bincode::config::standard())?;
180
181        Ok(res)
182    }
183}
184
185impl TryToBytes for SignedEntityType {
186    fn to_bytes_vec(&self) -> StdResult<Vec<u8>> {
187        bincode::serde::encode_to_vec(self, bincode::config::standard()).map_err(|e| e.into())
188    }
189}
190
191impl SignedEntityTypeDiscriminants {
192    /// Get all the discriminants
193    pub fn all() -> BTreeSet<Self> {
194        Self::iter_all().collect()
195    }
196
197    fn iter_all() -> impl Iterator<Item = Self> {
198        // Temporarily exclude CardanoBlocksTransactions until it can be derived from a TimePoint in SignedEntityConfig
199        Self::iter().filter(|&d| d != Self::CardanoBlocksTransactions)
200    }
201
202    /// Get the database value from enum's instance
203    pub fn index(&self) -> usize {
204        match self {
205            Self::MithrilStakeDistribution => ENTITY_TYPE_MITHRIL_STAKE_DISTRIBUTION,
206            Self::CardanoStakeDistribution => ENTITY_TYPE_CARDANO_STAKE_DISTRIBUTION,
207            Self::CardanoImmutableFilesFull => ENTITY_TYPE_CARDANO_IMMUTABLE_FILES_FULL,
208            Self::CardanoTransactions => ENTITY_TYPE_CARDANO_TRANSACTIONS,
209            Self::CardanoBlocksTransactions => ENTITY_TYPE_CARDANO_BLOCKS_TRANSACTIONS,
210            Self::CardanoDatabase => ENTITY_TYPE_CARDANO_DATABASE,
211        }
212    }
213
214    /// Get the discriminant associated with the given id
215    pub fn from_id(signed_entity_type_id: usize) -> StdResult<SignedEntityTypeDiscriminants> {
216        match signed_entity_type_id {
217            ENTITY_TYPE_MITHRIL_STAKE_DISTRIBUTION => Ok(Self::MithrilStakeDistribution),
218            ENTITY_TYPE_CARDANO_STAKE_DISTRIBUTION => Ok(Self::CardanoStakeDistribution),
219            ENTITY_TYPE_CARDANO_IMMUTABLE_FILES_FULL => Ok(Self::CardanoImmutableFilesFull),
220            ENTITY_TYPE_CARDANO_TRANSACTIONS => Ok(Self::CardanoTransactions),
221            ENTITY_TYPE_CARDANO_BLOCKS_TRANSACTIONS => Ok(Self::CardanoBlocksTransactions),
222            ENTITY_TYPE_CARDANO_DATABASE => Ok(Self::CardanoDatabase),
223            index => Err(anyhow!("Invalid entity_type_id {index}.")),
224        }
225    }
226
227    /// Parse the deduplicated list of signed entity types discriminants from a comma separated
228    /// string.
229    ///
230    /// Unknown or incorrectly formed values are ignored.
231    pub fn parse_list<T: AsRef<str>>(discriminants_string: T) -> StdResult<BTreeSet<Self>> {
232        let mut discriminants = BTreeSet::new();
233        let mut invalid_discriminants = Vec::new();
234
235        for name in discriminants_string
236            .as_ref()
237            .split(',')
238            .map(str::trim)
239            .filter(|s| !s.is_empty())
240        {
241            match Self::from_str(name) {
242                Ok(discriminant) => {
243                    discriminants.insert(discriminant);
244                }
245                Err(_) => {
246                    invalid_discriminants.push(name);
247                }
248            }
249        }
250
251        if invalid_discriminants.is_empty() {
252            Ok(discriminants)
253        } else {
254            Err(anyhow!(Self::format_parse_list_error(
255                invalid_discriminants
256            )))
257        }
258    }
259
260    fn format_parse_list_error(invalid_discriminants: Vec<&str>) -> String {
261        format!(
262            r#"Invalid signed entity types discriminants: {}.
263
264Accepted values are (case-sensitive): {}."#,
265            invalid_discriminants.join(", "),
266            Self::accepted_discriminants()
267        )
268    }
269
270    fn accepted_discriminants() -> String {
271        Self::iter_all().map(|d| d.to_string()).collect::<Vec<_>>().join(", ")
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use digest::Digest;
278
279    use crate::test::assert_same_json;
280
281    use super::*;
282
283    fn hash(signed_entity_type: SignedEntityType) -> String {
284        let mut hasher = Sha256::new();
285        signed_entity_type.feed_hash(&mut hasher);
286        hex::encode(hasher.finalize())
287    }
288
289    #[test]
290    fn get_epoch_when_signed_entity_type_is_signed_for_cardano_stake_distribution_return_epoch_with_offset()
291     {
292        let signed_entity_type = SignedEntityType::CardanoStakeDistribution(Epoch(3));
293
294        assert_eq!(
295            signed_entity_type.get_epoch_when_signed_entity_type_is_signed(),
296            Epoch(4)
297        );
298    }
299
300    #[test]
301    fn get_epoch_when_signed_entity_type_is_signed_for_mithril_stake_distribution_return_epoch_stored_in_signed_entity_type()
302     {
303        let signed_entity_type = SignedEntityType::MithrilStakeDistribution(Epoch(3));
304        assert_eq!(
305            signed_entity_type.get_epoch_when_signed_entity_type_is_signed(),
306            Epoch(3)
307        );
308    }
309
310    #[test]
311    fn get_epoch_when_signed_entity_type_is_signed_for_cardano_immutable_files_full_return_epoch_stored_in_signed_entity_type()
312     {
313        let signed_entity_type =
314            SignedEntityType::CardanoImmutableFilesFull(CardanoDbBeacon::new(3, 100));
315        assert_eq!(
316            signed_entity_type.get_epoch_when_signed_entity_type_is_signed(),
317            Epoch(3)
318        );
319    }
320
321    #[test]
322    fn get_epoch_when_signed_entity_type_is_signed_for_cardano_transactions_return_epoch_stored_in_signed_entity_type()
323     {
324        let signed_entity_type = SignedEntityType::CardanoTransactions(Epoch(3), BlockNumber(77));
325        assert_eq!(
326            signed_entity_type.get_epoch_when_signed_entity_type_is_signed(),
327            Epoch(3)
328        );
329    }
330
331    #[test]
332    fn get_epoch_when_signed_entity_type_is_signed_for_cardano_blocks_transactions_return_epoch_stored_in_signed_entity_type()
333     {
334        let signed_entity_type =
335            SignedEntityType::CardanoBlocksTransactions(Epoch(5), BlockNumber(77));
336        assert_eq!(
337            signed_entity_type.get_epoch_when_signed_entity_type_is_signed(),
338            Epoch(5)
339        );
340    }
341
342    #[test]
343    fn get_epoch_when_signed_entity_type_is_signed_for_cardano_database_return_epoch_stored_in_signed_entity_type()
344     {
345        let signed_entity_type = SignedEntityType::CardanoDatabase(CardanoDbBeacon::new(12, 987));
346        assert_eq!(
347            signed_entity_type.get_epoch_when_signed_entity_type_is_signed(),
348            Epoch(12)
349        );
350    }
351
352    #[test]
353    fn verify_signed_entity_type_properties_are_included_in_computed_hash() {
354        let reference_hash = hash(SignedEntityType::MithrilStakeDistribution(Epoch(5)));
355        assert_ne!(
356            reference_hash,
357            hash(SignedEntityType::MithrilStakeDistribution(Epoch(15)))
358        );
359
360        let reference_hash = hash(SignedEntityType::CardanoStakeDistribution(Epoch(5)));
361        assert_ne!(
362            reference_hash,
363            hash(SignedEntityType::CardanoStakeDistribution(Epoch(15)))
364        );
365
366        let reference_hash = hash(SignedEntityType::CardanoImmutableFilesFull(
367            CardanoDbBeacon::new(5, 100),
368        ));
369        assert_ne!(
370            reference_hash,
371            hash(SignedEntityType::CardanoImmutableFilesFull(
372                CardanoDbBeacon::new(20, 100)
373            ))
374        );
375        assert_ne!(
376            reference_hash,
377            hash(SignedEntityType::CardanoImmutableFilesFull(
378                CardanoDbBeacon::new(5, 507)
379            ))
380        );
381
382        let reference_hash = hash(SignedEntityType::CardanoTransactions(
383            Epoch(35),
384            BlockNumber(77),
385        ));
386        assert_ne!(
387            reference_hash,
388            hash(SignedEntityType::CardanoTransactions(
389                Epoch(3),
390                BlockNumber(77)
391            ))
392        );
393        assert_ne!(
394            reference_hash,
395            hash(SignedEntityType::CardanoTransactions(
396                Epoch(35),
397                BlockNumber(98765)
398            ))
399        );
400
401        let reference_hash = hash(SignedEntityType::CardanoBlocksTransactions(
402            Epoch(35),
403            BlockNumber(77),
404        ));
405        assert_ne!(
406            reference_hash,
407            hash(SignedEntityType::CardanoBlocksTransactions(
408                Epoch(3),
409                BlockNumber(77)
410            ))
411        );
412        assert_ne!(
413            reference_hash,
414            hash(SignedEntityType::CardanoBlocksTransactions(
415                Epoch(35),
416                BlockNumber(98765)
417            ))
418        );
419
420        let reference_hash = hash(SignedEntityType::CardanoDatabase(CardanoDbBeacon::new(
421            12, 987,
422        )));
423        assert_ne!(
424            reference_hash,
425            hash(SignedEntityType::CardanoDatabase(CardanoDbBeacon::new(
426                98, 987
427            )))
428        );
429        assert_ne!(
430            reference_hash,
431            hash(SignedEntityType::CardanoDatabase(CardanoDbBeacon::new(
432                12, 123
433            )))
434        );
435    }
436
437    #[test]
438    fn signed_entity_with_shared_beacons_computes_different_hashes() {
439        assert_ne!(
440            hash(SignedEntityType::CardanoTransactions(
441                Epoch(3),
442                BlockNumber(77)
443            )),
444            hash(SignedEntityType::CardanoBlocksTransactions(
445                Epoch(3),
446                BlockNumber(77)
447            ))
448        );
449
450        // TODO: uncomment those tests cases when feeding database index is generalized to all types
451        // assert_ne!(
452        //     hash(SignedEntityType::MithrilStakeDistribution(Epoch(15))),
453        //     hash(SignedEntityType::CardanoStakeDistribution(Epoch(15)))
454        // );
455        //
456        // assert_ne!(
457        //     hash(SignedEntityType::CardanoImmutableFilesFull(
458        //         CardanoDbBeacon::new(98, 987)
459        //     )),
460        //     hash(SignedEntityType::CardanoDatabase(CardanoDbBeacon::new(
461        //         98, 987
462        //     )))
463        // );
464    }
465
466    #[test]
467    fn get_open_message_timeout() {
468        assert_eq!(
469            SignedEntityType::MithrilStakeDistribution(Epoch(1)).get_open_message_timeout(),
470            Some(Duration::from_secs(3600))
471        );
472        assert_eq!(
473            SignedEntityType::CardanoImmutableFilesFull(CardanoDbBeacon::new(1, 1))
474                .get_open_message_timeout(),
475            Some(Duration::from_secs(600))
476        );
477        assert_eq!(
478            SignedEntityType::CardanoStakeDistribution(Epoch(1)).get_open_message_timeout(),
479            Some(Duration::from_secs(1800))
480        );
481        assert_eq!(
482            SignedEntityType::CardanoTransactions(Epoch(1), BlockNumber(1))
483                .get_open_message_timeout(),
484            Some(Duration::from_secs(600))
485        );
486        assert_eq!(
487            SignedEntityType::CardanoBlocksTransactions(Epoch(1), BlockNumber(1))
488                .get_open_message_timeout(),
489            Some(Duration::from_secs(600))
490        );
491        assert_eq!(
492            SignedEntityType::CardanoDatabase(CardanoDbBeacon::new(1, 1))
493                .get_open_message_timeout(),
494            Some(Duration::from_secs(600))
495        );
496    }
497
498    #[test]
499    fn serialize_beacon_to_json() {
500        let cardano_stake_distribution_json = SignedEntityType::CardanoStakeDistribution(Epoch(25))
501            .get_json_beacon()
502            .unwrap();
503        assert_same_json!("25", &cardano_stake_distribution_json);
504
505        let cardano_transactions_json =
506            SignedEntityType::CardanoTransactions(Epoch(35), BlockNumber(77))
507                .get_json_beacon()
508                .unwrap();
509        assert_same_json!(
510            r#"{"epoch":35,"block_number":77}"#,
511            &cardano_transactions_json
512        );
513
514        let cardano_transactions_json =
515            SignedEntityType::CardanoBlocksTransactions(Epoch(35), BlockNumber(77))
516                .get_json_beacon()
517                .unwrap();
518        assert_same_json!(
519            r#"{"epoch":35,"block_number":77}"#,
520            &cardano_transactions_json
521        );
522
523        let cardano_immutable_files_full_json =
524            SignedEntityType::CardanoImmutableFilesFull(CardanoDbBeacon::new(5, 100))
525                .get_json_beacon()
526                .unwrap();
527        assert_same_json!(
528            r#"{"epoch":5,"immutable_file_number":100}"#,
529            &cardano_immutable_files_full_json
530        );
531
532        let msd_json = SignedEntityType::MithrilStakeDistribution(Epoch(15))
533            .get_json_beacon()
534            .unwrap();
535        assert_same_json!("15", &msd_json);
536
537        let cardano_database_full_json =
538            SignedEntityType::CardanoDatabase(CardanoDbBeacon::new(12, 987))
539                .get_json_beacon()
540                .unwrap();
541        assert_same_json!(
542            r#"{"epoch":12,"immutable_file_number":987}"#,
543            &cardano_database_full_json
544        );
545    }
546
547    #[test]
548    fn bytes_encoding() {
549        let cardano_stake_distribution = SignedEntityType::CardanoStakeDistribution(Epoch(25));
550        let cardano_stake_distribution_bytes = cardano_stake_distribution.to_bytes_vec().unwrap();
551        let cardano_stake_distribution_from_bytes =
552            SignedEntityType::try_from_bytes(&cardano_stake_distribution_bytes).unwrap();
553
554        assert_eq!(
555            cardano_stake_distribution,
556            cardano_stake_distribution_from_bytes
557        );
558    }
559
560    #[test]
561    fn iter_all_discriminants() {
562        let discriminants: Vec<SignedEntityTypeDiscriminants> =
563            SignedEntityTypeDiscriminants::iter_all().collect();
564
565        assert_eq!(
566            discriminants,
567            vec![
568                SignedEntityTypeDiscriminants::MithrilStakeDistribution,
569                SignedEntityTypeDiscriminants::CardanoStakeDistribution,
570                SignedEntityTypeDiscriminants::CardanoImmutableFilesFull,
571                SignedEntityTypeDiscriminants::CardanoDatabase,
572                SignedEntityTypeDiscriminants::CardanoTransactions,
573            ]
574        );
575    }
576
577    // Expected ord:
578    // MithrilStakeDistribution < CardanoStakeDistribution < CardanoImmutableFilesFull < CardanoDatabase < CardanoTransactions
579    #[test]
580    fn ordering_discriminant() {
581        let mut list = vec![
582            SignedEntityTypeDiscriminants::CardanoStakeDistribution,
583            SignedEntityTypeDiscriminants::CardanoDatabase,
584            SignedEntityTypeDiscriminants::CardanoTransactions,
585            SignedEntityTypeDiscriminants::CardanoImmutableFilesFull,
586            SignedEntityTypeDiscriminants::CardanoBlocksTransactions,
587            SignedEntityTypeDiscriminants::MithrilStakeDistribution,
588        ];
589        list.sort();
590
591        assert_eq!(
592            list,
593            vec![
594                SignedEntityTypeDiscriminants::MithrilStakeDistribution,
595                SignedEntityTypeDiscriminants::CardanoStakeDistribution,
596                SignedEntityTypeDiscriminants::CardanoImmutableFilesFull,
597                SignedEntityTypeDiscriminants::CardanoDatabase,
598                SignedEntityTypeDiscriminants::CardanoTransactions,
599                SignedEntityTypeDiscriminants::CardanoBlocksTransactions,
600            ]
601        );
602    }
603
604    #[test]
605    fn ordering_discriminant_with_duplicate() {
606        let mut list = vec![
607            SignedEntityTypeDiscriminants::CardanoDatabase,
608            SignedEntityTypeDiscriminants::CardanoStakeDistribution,
609            SignedEntityTypeDiscriminants::CardanoDatabase,
610            SignedEntityTypeDiscriminants::MithrilStakeDistribution,
611            SignedEntityTypeDiscriminants::CardanoTransactions,
612            SignedEntityTypeDiscriminants::CardanoStakeDistribution,
613            SignedEntityTypeDiscriminants::CardanoBlocksTransactions,
614            SignedEntityTypeDiscriminants::CardanoImmutableFilesFull,
615            SignedEntityTypeDiscriminants::MithrilStakeDistribution,
616            SignedEntityTypeDiscriminants::MithrilStakeDistribution,
617        ];
618        list.sort();
619
620        assert_eq!(
621            list,
622            vec![
623                SignedEntityTypeDiscriminants::MithrilStakeDistribution,
624                SignedEntityTypeDiscriminants::MithrilStakeDistribution,
625                SignedEntityTypeDiscriminants::MithrilStakeDistribution,
626                SignedEntityTypeDiscriminants::CardanoStakeDistribution,
627                SignedEntityTypeDiscriminants::CardanoStakeDistribution,
628                SignedEntityTypeDiscriminants::CardanoImmutableFilesFull,
629                SignedEntityTypeDiscriminants::CardanoDatabase,
630                SignedEntityTypeDiscriminants::CardanoDatabase,
631                SignedEntityTypeDiscriminants::CardanoTransactions,
632                SignedEntityTypeDiscriminants::CardanoBlocksTransactions,
633            ]
634        );
635    }
636
637    #[test]
638    fn parse_signed_entity_types_discriminants_discriminant_without_values() {
639        let discriminants_str = "";
640        let discriminants = SignedEntityTypeDiscriminants::parse_list(discriminants_str).unwrap();
641
642        assert_eq!(BTreeSet::new(), discriminants);
643
644        let discriminants_str = "     ";
645        let discriminants = SignedEntityTypeDiscriminants::parse_list(discriminants_str).unwrap();
646
647        assert_eq!(BTreeSet::new(), discriminants);
648    }
649
650    #[test]
651    fn parse_signed_entity_types_discriminants_with_correctly_formed_values() {
652        let discriminants_str = "MithrilStakeDistribution,CardanoImmutableFilesFull";
653        let discriminants = SignedEntityTypeDiscriminants::parse_list(discriminants_str).unwrap();
654
655        assert_eq!(
656            BTreeSet::from([
657                SignedEntityTypeDiscriminants::MithrilStakeDistribution,
658                SignedEntityTypeDiscriminants::CardanoImmutableFilesFull,
659            ]),
660            discriminants
661        );
662    }
663
664    #[test]
665    fn parse_signed_entity_types_discriminants_should_trim_values() {
666        let discriminants_str =
667            "MithrilStakeDistribution    ,  CardanoImmutableFilesFull  ,   CardanoTransactions   ";
668        let discriminants = SignedEntityTypeDiscriminants::parse_list(discriminants_str).unwrap();
669
670        assert_eq!(
671            BTreeSet::from([
672                SignedEntityTypeDiscriminants::MithrilStakeDistribution,
673                SignedEntityTypeDiscriminants::CardanoTransactions,
674                SignedEntityTypeDiscriminants::CardanoImmutableFilesFull,
675            ]),
676            discriminants
677        );
678    }
679
680    #[test]
681    fn parse_signed_entity_types_discriminants_should_remove_duplicates() {
682        let discriminants_str =
683            "CardanoTransactions,CardanoTransactions,CardanoTransactions,CardanoTransactions";
684        let discriminant = SignedEntityTypeDiscriminants::parse_list(discriminants_str).unwrap();
685
686        assert_eq!(
687            BTreeSet::from([SignedEntityTypeDiscriminants::CardanoTransactions]),
688            discriminant
689        );
690    }
691
692    #[test]
693    fn parse_signed_entity_types_discriminants_should_be_case_sensitive() {
694        let discriminants_str = "mithrilstakedistribution,CARDANOIMMUTABLEFILESFULL";
695        let error = SignedEntityTypeDiscriminants::parse_list(discriminants_str).unwrap_err();
696
697        assert_eq!(
698            SignedEntityTypeDiscriminants::format_parse_list_error(vec![
699                "mithrilstakedistribution",
700                "CARDANOIMMUTABLEFILESFULL"
701            ]),
702            error.to_string()
703        );
704    }
705
706    #[test]
707    fn parse_signed_entity_types_discriminants_should_not_return_unknown_signed_entity_types() {
708        let discriminants_str = "Unknown";
709        let error = SignedEntityTypeDiscriminants::parse_list(discriminants_str).unwrap_err();
710
711        assert_eq!(
712            SignedEntityTypeDiscriminants::format_parse_list_error(vec!["Unknown"]),
713            error.to_string()
714        );
715    }
716
717    #[test]
718    fn parse_signed_entity_types_discriminants_should_fail_if_there_is_at_least_one_invalid_value()
719    {
720        let discriminants_str = "CardanoTransactions,Invalid,MithrilStakeDistribution";
721        let error = SignedEntityTypeDiscriminants::parse_list(discriminants_str).unwrap_err();
722
723        assert_eq!(
724            SignedEntityTypeDiscriminants::format_parse_list_error(vec!["Invalid"]),
725            error.to_string()
726        );
727    }
728
729    #[test]
730    fn parse_list_error_format_to_an_useful_message() {
731        let invalid_discriminants = vec!["Unknown", "Invalid"];
732        let error = SignedEntityTypeDiscriminants::format_parse_list_error(invalid_discriminants);
733
734        assert_eq!(
735            format!(
736                r#"Invalid signed entity types discriminants: Unknown, Invalid.
737
738Accepted values are (case-sensitive): {}."#,
739                SignedEntityTypeDiscriminants::accepted_discriminants()
740            ),
741            error
742        );
743    }
744
745    #[test]
746    fn discriminants_index_returns_correct_database_value() {
747        assert_eq!(
748            SignedEntityTypeDiscriminants::MithrilStakeDistribution.index(),
749            ENTITY_TYPE_MITHRIL_STAKE_DISTRIBUTION
750        );
751        assert_eq!(
752            SignedEntityTypeDiscriminants::CardanoStakeDistribution.index(),
753            ENTITY_TYPE_CARDANO_STAKE_DISTRIBUTION
754        );
755        assert_eq!(
756            SignedEntityTypeDiscriminants::CardanoImmutableFilesFull.index(),
757            ENTITY_TYPE_CARDANO_IMMUTABLE_FILES_FULL
758        );
759        assert_eq!(
760            SignedEntityTypeDiscriminants::CardanoTransactions.index(),
761            ENTITY_TYPE_CARDANO_TRANSACTIONS
762        );
763        assert_eq!(
764            SignedEntityTypeDiscriminants::CardanoBlocksTransactions.index(),
765            ENTITY_TYPE_CARDANO_BLOCKS_TRANSACTIONS
766        );
767        assert_eq!(
768            SignedEntityTypeDiscriminants::CardanoDatabase.index(),
769            ENTITY_TYPE_CARDANO_DATABASE
770        );
771    }
772}