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/// The signed entity type that represents a type of data signed by the Mithril
34/// protocol Note: Each variant of this enum must be associated to an entry in
35/// the `signed_entity_type` table of the signer/aggregator nodes. The variant
36/// are identified by their discriminant (i.e. index in the enum), thus the
37/// modification of this type should only ever consist of appending new
38/// variants.
39// Important note: The order of the variants is important as it is used for the derived Ord trait.
40#[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    /// Mithril stake distribution
54    MithrilStakeDistribution(Epoch),
55
56    /// Cardano Stake Distribution
57    CardanoStakeDistribution(Epoch),
58
59    /// Full Cardano Immutable Files
60    CardanoImmutableFilesFull(CardanoDbBeacon),
61
62    /// Cardano Database
63    CardanoDatabase(CardanoDbBeacon),
64
65    /// Cardano Transactions
66    CardanoTransactions(Epoch, BlockNumber),
67}
68
69impl SignedEntityType {
70    /// Create a new signed entity type for a genesis certificate (a [Self::MithrilStakeDistribution])
71    pub fn genesis(epoch: Epoch) -> Self {
72        Self::MithrilStakeDistribution(epoch)
73    }
74
75    /// Return the epoch from the signed entity.
76    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    /// Return the epoch at which the signed entity type is signed.
86    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    /// Get the database value from enum's instance
95    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    /// Return a JSON serialized value of the internal beacon
106    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    /// Return the associated open message timeout
127    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    /// Get all the discriminants
172    pub fn all() -> BTreeSet<Self> {
173        SignedEntityTypeDiscriminants::iter().collect()
174    }
175
176    /// Get the database value from enum's instance
177    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    /// Get the discriminant associated with the given id
188    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    /// Parse the deduplicated list of signed entity types discriminants from a comma separated
200    /// string.
201    ///
202    /// Unknown or incorrectly formed values are ignored.
203    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    // Expected ord:
433    // MithrilStakeDistribution < CardanoStakeDistribution < CardanoImmutableFilesFull < CardanoDatabase < CardanoTransactions
434    #[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}