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    /// Retrieve a dummy entity type (for test only)
71    pub fn dummy() -> Self {
72        Self::MithrilStakeDistribution(Epoch(5))
73    }
74
75    /// Create a new signed entity type for a genesis certificate (a [Self::MithrilStakeDistribution])
76    pub fn genesis(epoch: Epoch) -> Self {
77        Self::MithrilStakeDistribution(epoch)
78    }
79
80    /// Return the epoch from the signed entity.
81    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    /// Return the epoch at which the signed entity type is signed.
91    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    /// Get the database value from enum's instance
100    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    /// Return a JSON serialized value of the internal beacon
111    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    /// Return the associated open message timeout
132    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    /// Get all the discriminants
177    pub fn all() -> BTreeSet<Self> {
178        SignedEntityTypeDiscriminants::iter().collect()
179    }
180
181    /// Get the database value from enum's instance
182    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    /// Get the discriminant associated with the given id
193    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    /// Parse the deduplicated list of signed entity types discriminants from a comma separated
205    /// string.
206    ///
207    /// Unknown or incorrectly formed values are ignored.
208    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    // Expected ord:
438    // MithrilStakeDistribution < CardanoStakeDistribution < CardanoImmutableFilesFull < CardanoDatabase < CardanoTransactions
439    #[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}