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