massa_models/
endorsement.rs

1// Copyright (c) 2022 MASSA LABS <info@massa.net>
2
3use crate::block_id::{BlockIdDeserializer, BlockIdSerializer};
4use crate::prehash::PreHashed;
5use crate::secure_share::{Id, SecureShare, SecureShareContent};
6use crate::slot::{Slot, SlotDeserializer, SlotSerializer};
7use crate::{block_id::BlockId, error::ModelsError};
8use massa_hash::{Hash, HashDeserializer};
9use massa_serialization::{
10    DeserializeError, Deserializer, SerializeError, Serializer, U32VarIntDeserializer,
11    U32VarIntSerializer, U64VarIntDeserializer, U64VarIntSerializer,
12};
13use massa_signature::PublicKey;
14use nom::error::{context, ErrorKind};
15use nom::sequence::tuple;
16use nom::Parser;
17use nom::{
18    error::{ContextError, ParseError},
19    IResult,
20};
21use serde::{Deserialize, Serialize};
22use serde_with::{DeserializeFromStr, SerializeDisplay};
23use std::fmt::Formatter;
24use std::ops::Bound::{Excluded, Included};
25use std::{fmt::Display, str::FromStr};
26use transition::Versioned;
27
28/// Endorsement ID size in bytes
29pub const ENDORSEMENT_ID_SIZE_BYTES: usize = massa_hash::HASH_SIZE_BYTES;
30
31/// endorsement id
32#[allow(missing_docs)]
33#[transition::versioned(versions("0"))]
34#[derive(
35    Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, SerializeDisplay, DeserializeFromStr,
36)]
37pub struct EndorsementId(Hash);
38
39const ENDORSEMENTID_PREFIX: char = 'E';
40const ENDORSEMENTID_VERSION: u64 = 0;
41
42impl PreHashed for EndorsementId {}
43
44impl Id for EndorsementId {
45    fn new(hash: Hash) -> Self {
46        EndorsementId::EndorsementIdV0(EndorsementIdV0(hash))
47    }
48
49    fn get_hash(&self) -> &Hash {
50        match self {
51            EndorsementId::EndorsementIdV0(endorsement_id) => endorsement_id.get_hash(),
52        }
53    }
54}
55
56#[transition::impl_version(versions("0"))]
57impl EndorsementId {
58    fn get_hash(&self) -> &Hash {
59        &self.0
60    }
61}
62
63impl std::fmt::Display for EndorsementId {
64    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
65        match self {
66            EndorsementId::EndorsementIdV0(id) => write!(f, "{}", id),
67        }
68    }
69}
70
71#[transition::impl_version(versions("0"))]
72impl std::fmt::Display for EndorsementId {
73    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
74        let u64_serializer = U64VarIntSerializer::new();
75        // might want to allocate the vector with capacity in order to avoid re-allocation
76        let mut bytes: Vec<u8> = Vec::new();
77        u64_serializer
78            .serialize(&ENDORSEMENTID_VERSION, &mut bytes)
79            .map_err(|_| std::fmt::Error)?;
80        bytes.extend(self.0.to_bytes());
81        write!(
82            f,
83            "{}{}",
84            ENDORSEMENTID_PREFIX,
85            bs58::encode(bytes).with_check().into_string()
86        )
87    }
88}
89
90impl std::fmt::Debug for EndorsementId {
91    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
92        write!(f, "{}", self)
93    }
94}
95
96impl FromStr for EndorsementId {
97    type Err = ModelsError;
98    /// ## Example
99    /// ```rust
100    /// # use massa_hash::Hash;
101    /// # use std::str::FromStr;
102    /// # use massa_models::endorsement::EndorsementId;
103    /// # use crate::massa_models::secure_share::Id;
104    /// # let endo_id = EndorsementId::new(Hash::compute_from("endo_id".as_bytes()));
105    /// let ser = endo_id.to_string();
106    /// let res_endo_id = EndorsementId::from_str(&ser).unwrap();
107    /// assert_eq!(endo_id, res_endo_id);
108    /// ```
109    fn from_str(s: &str) -> Result<Self, Self::Err> {
110        let mut chars = s.chars();
111        match chars.next() {
112            Some(prefix) if prefix == ENDORSEMENTID_PREFIX => {
113                let data = chars.collect::<String>();
114                let decoded_bs58_check = bs58::decode(data)
115                    .with_check(None)
116                    .into_vec()
117                    .map_err(|_| ModelsError::EndorsementIdParseError)?;
118                let endorsement_id_deserializer = EndorsementIdDeserializer::new();
119                let (rest, endorsement_id) = endorsement_id_deserializer
120                    .deserialize::<DeserializeError>(&decoded_bs58_check[..])
121                    .map_err(|_| ModelsError::EndorsementIdParseError)?;
122                if rest.is_empty() {
123                    Ok(endorsement_id)
124                } else {
125                    Err(ModelsError::EndorsementIdParseError)
126                }
127            }
128            _ => Err(ModelsError::EndorsementIdParseError),
129        }
130    }
131}
132
133#[transition::impl_version(versions("0"))]
134impl FromStr for EndorsementId {
135    type Err = ModelsError;
136    fn from_str(s: &str) -> Result<Self, Self::Err> {
137        let mut chars = s.chars();
138        match chars.next() {
139            Some(prefix) if prefix == ENDORSEMENTID_PREFIX => {
140                let data = chars.collect::<String>();
141                let decoded_bs58_check = bs58::decode(data)
142                    .with_check(None)
143                    .into_vec()
144                    .map_err(|_| ModelsError::EndorsementIdParseError)?;
145                let endorsement_id_deserializer = EndorsementIdDeserializer::new();
146                let (rest, endorsement_id) = endorsement_id_deserializer
147                    .deserialize::<DeserializeError>(&decoded_bs58_check[..])
148                    .map_err(|_| ModelsError::EndorsementIdParseError)?;
149                if rest.is_empty() {
150                    Ok(endorsement_id)
151                } else {
152                    Err(ModelsError::EndorsementIdParseError)
153                }
154            }
155            _ => Err(ModelsError::EndorsementIdParseError),
156        }
157    }
158}
159
160struct EndorsementIdDeserializer {
161    version_deserializer: U64VarIntDeserializer,
162    hash_deserializer: HashDeserializer,
163}
164
165impl EndorsementIdDeserializer {
166    pub fn new() -> Self {
167        Self {
168            version_deserializer: U64VarIntDeserializer::new(Included(0), Included(u64::MAX)),
169            hash_deserializer: HashDeserializer::new(),
170        }
171    }
172}
173
174impl Deserializer<EndorsementId> for EndorsementIdDeserializer {
175    fn deserialize<'a, E: ParseError<&'a [u8]> + ContextError<&'a [u8]>>(
176        &self,
177        buffer: &'a [u8],
178    ) -> IResult<&'a [u8], EndorsementId, E> {
179        // Verify that we at least have a version and something else
180        if buffer.len() < 2 {
181            return Err(nom::Err::Error(E::from_error_kind(buffer, ErrorKind::Eof)));
182        }
183        let (rest, endorsement_id_version) = self
184            .version_deserializer
185            .deserialize(buffer)
186            .map_err(|_: nom::Err<E>| {
187                nom::Err::Error(E::from_error_kind(buffer, ErrorKind::Eof))
188            })?;
189        match endorsement_id_version {
190            <EndorsementId!["0"]>::VERSION => {
191                let (rest, endorsement_id) = self.deserialize(rest)?;
192                Ok((rest, EndorsementIdVariant!["0"](endorsement_id)))
193            }
194            _ => Err(nom::Err::Error(E::from_error_kind(buffer, ErrorKind::Eof))),
195        }
196    }
197}
198
199#[transition::impl_version(versions("0"), structures("EndorsementId"))]
200impl Deserializer<EndorsementId> for EndorsementIdDeserializer {
201    fn deserialize<'a, E: ParseError<&'a [u8]> + ContextError<&'a [u8]>>(
202        &self,
203        buffer: &'a [u8],
204    ) -> IResult<&'a [u8], EndorsementId, E> {
205        context("Failed OperationId deserialization", |input| {
206            self.hash_deserializer.deserialize(input)
207        })
208        .map(EndorsementId)
209        .parse(buffer)
210    }
211}
212impl Display for Endorsement {
213    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214        writeln!(
215            f,
216            "Endorsed block: {} at slot {}",
217            self.endorsed_block, self.slot
218        )?;
219        writeln!(f, "Index: {}", self.index)?;
220        Ok(())
221    }
222}
223
224/// an endorsement, as sent in the network
225#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
226pub struct Endorsement {
227    /// Slot in which the endorsement can be included
228    pub slot: Slot,
229    /// Endorsement index inside the including block
230    pub index: u32,
231    /// Hash of endorsed block.
232    /// This is the parent in thread `self.slot.thread` of the block in which the endorsement is included
233    pub endorsed_block: BlockId,
234}
235
236#[cfg(any(test, feature = "test-exports"))]
237impl SecureShareEndorsement {
238    // TODO: gh-issue #3398
239    /// Used under testing conditions to validate an instance of Self
240    pub fn check_invariants(&self) -> Result<(), Box<dyn std::error::Error>> {
241        if let Err(e) = self.verify_signature() {
242            return Err(e.into());
243        }
244        if self.content.slot.thread >= crate::config::THREAD_COUNT {
245            Err("Endorsement slot on non-existent thread".into())
246        } else if self.content.index >= crate::config::ENDORSEMENT_COUNT {
247            Err("Endorsement index out of range".into())
248        } else {
249            Ok(())
250        }
251    }
252}
253
254/// Wrapped endorsement
255pub type SecureShareEndorsement = SecureShare<Endorsement, EndorsementId>;
256
257impl SecureShareContent for Endorsement {
258    /// Compute the signed hash
259    fn compute_signed_hash(&self, public_key: &PublicKey, content_hash: &Hash) -> Hash {
260        let mut signed_data: Vec<u8> = Vec::new();
261        signed_data.extend(public_key.to_bytes());
262        signed_data.extend(EndorsementDenunciationData::new(self.slot, self.index).to_bytes());
263        signed_data.extend(content_hash.to_bytes());
264        Hash::compute_from(&signed_data)
265    }
266}
267
268/// Serializer for `Endorsement`
269#[derive(Clone)]
270pub struct EndorsementSerializer {
271    slot_serializer: SlotSerializer,
272    u32_serializer: U32VarIntSerializer,
273    block_id_serializer: BlockIdSerializer,
274}
275
276impl EndorsementSerializer {
277    /// Creates a new `EndorsementSerializer`
278    pub fn new() -> Self {
279        EndorsementSerializer {
280            slot_serializer: SlotSerializer::new(),
281            u32_serializer: U32VarIntSerializer::new(),
282            block_id_serializer: BlockIdSerializer::new(),
283        }
284    }
285}
286
287impl Default for EndorsementSerializer {
288    fn default() -> Self {
289        Self::new()
290    }
291}
292
293impl Serializer<Endorsement> for EndorsementSerializer {
294    /// ## Example:
295    /// ```rust
296    /// use massa_models::{slot::Slot, block_id::BlockId, endorsement::{Endorsement, EndorsementSerializer}};
297    /// use massa_serialization::Serializer;
298    /// use massa_hash::Hash;
299    ///
300    /// let endorsement = Endorsement {
301    ///   slot: Slot::new(1, 2),
302    ///   index: 0,
303    ///   endorsed_block: BlockId::generate_from_hash(Hash::compute_from("test".as_bytes()))
304    /// };
305    /// let mut buffer = Vec::new();
306    /// EndorsementSerializer::new().serialize(&endorsement, &mut buffer).unwrap();
307    /// ```
308    fn serialize(&self, value: &Endorsement, buffer: &mut Vec<u8>) -> Result<(), SerializeError> {
309        self.slot_serializer.serialize(&value.slot, buffer)?;
310        self.u32_serializer.serialize(&value.index, buffer)?;
311        self.block_id_serializer
312            .serialize(&value.endorsed_block, buffer)?;
313        Ok(())
314    }
315}
316
317/// Deserializer for `Endorsement`
318pub struct EndorsementDeserializer {
319    slot_deserializer: SlotDeserializer,
320    index_deserializer: U32VarIntDeserializer,
321    block_id_deserializer: BlockIdDeserializer,
322}
323
324impl EndorsementDeserializer {
325    /// Creates a new `EndorsementDeserializer`
326    pub fn new(thread_count: u8, endorsement_count: u32) -> Self {
327        EndorsementDeserializer {
328            slot_deserializer: SlotDeserializer::new(
329                (Included(0), Included(u64::MAX)),
330                (Included(0), Excluded(thread_count)),
331            ),
332            index_deserializer: U32VarIntDeserializer::new(
333                Included(0),
334                Excluded(endorsement_count),
335            ),
336            block_id_deserializer: BlockIdDeserializer::new(),
337        }
338    }
339}
340
341impl Deserializer<Endorsement> for EndorsementDeserializer {
342    /// ## Example:
343    /// ```rust
344    /// use massa_models::{slot::Slot, block_id::BlockId, endorsement::{Endorsement, EndorsementSerializer, EndorsementDeserializer}};
345    /// use massa_serialization::{Serializer, Deserializer, DeserializeError};
346    /// use massa_hash::Hash;
347    ///
348    /// let endorsement = Endorsement {
349    ///   slot: Slot::new(1, 2),
350    ///   index: 0,
351    ///   endorsed_block: BlockId::generate_from_hash(Hash::compute_from("test".as_bytes()))
352    /// };
353    /// let mut buffer = Vec::new();
354    /// EndorsementSerializer::new().serialize(&endorsement, &mut buffer).unwrap();
355    /// let (rest, deserialized) = EndorsementDeserializer::new(32, 10).deserialize::<DeserializeError>(&buffer).unwrap();
356    /// assert_eq!(rest.len(), 0);
357    /// assert_eq!(deserialized.slot, endorsement.slot);
358    /// assert_eq!(deserialized.index, endorsement.index);
359    /// assert_eq!(deserialized.endorsed_block, endorsement.endorsed_block);
360    /// ```
361    fn deserialize<'a, E: ParseError<&'a [u8]> + ContextError<&'a [u8]>>(
362        &self,
363        buffer: &'a [u8],
364    ) -> IResult<&'a [u8], Endorsement, E> {
365        context(
366            "Failed endorsement deserialization",
367            tuple((
368                context("Failed slot deserialization", |input| {
369                    self.slot_deserializer.deserialize(input)
370                }),
371                context("Failed index deserialization", |input| {
372                    self.index_deserializer.deserialize(input)
373                }),
374                context("Failed endorsed_block deserialization", |input| {
375                    self.block_id_deserializer.deserialize(input)
376                }),
377            )),
378        )
379        .map(|(slot, index, endorsed_block)| Endorsement {
380            slot,
381            index,
382            endorsed_block,
383        })
384        .parse(buffer)
385    }
386}
387
388/// Lightweight Serializer for `Endorsement`
389/// When included in a `BlockHeader`, we want to serialize only the index (optimization)
390pub struct EndorsementSerializerLW {
391    // slot_serializer: SlotSerializer,
392    u32_serializer: U32VarIntSerializer,
393}
394
395impl EndorsementSerializerLW {
396    /// Creates a new `EndorsementSerializerLW`
397    pub fn new() -> Self {
398        EndorsementSerializerLW {
399            u32_serializer: U32VarIntSerializer::new(),
400        }
401    }
402}
403
404impl Default for EndorsementSerializerLW {
405    fn default() -> Self {
406        Self::new()
407    }
408}
409
410impl Serializer<Endorsement> for EndorsementSerializerLW {
411    /// ## Example:
412    /// ```rust
413    /// use massa_models::{slot::Slot, block_id::BlockId, endorsement::{Endorsement, EndorsementSerializerLW}};
414    /// use massa_serialization::Serializer;
415    /// use massa_hash::Hash;
416    ///
417    /// let endorsement = Endorsement {
418    ///   slot: Slot::new(1, 2),
419    ///   index: 0,
420    ///   endorsed_block: BlockId::generate_from_hash(Hash::compute_from("test".as_bytes()))
421    /// };
422    /// let mut buffer = Vec::new();
423    /// EndorsementSerializerLW::new().serialize(&endorsement, &mut buffer).unwrap();
424    /// ```
425    fn serialize(&self, value: &Endorsement, buffer: &mut Vec<u8>) -> Result<(), SerializeError> {
426        self.u32_serializer.serialize(&value.index, buffer)?;
427        Ok(())
428    }
429}
430
431/// Lightweight Deserializer for `Endorsement`
432pub struct EndorsementDeserializerLW {
433    index_deserializer: U32VarIntDeserializer,
434    slot: Slot,
435    endorsed_block: BlockId,
436}
437
438impl EndorsementDeserializerLW {
439    /// Creates a new `EndorsementDeserializerLW`
440    pub const fn new(endorsement_count: u32, slot: Slot, endorsed_block: BlockId) -> Self {
441        EndorsementDeserializerLW {
442            index_deserializer: U32VarIntDeserializer::new(
443                Included(0),
444                Excluded(endorsement_count),
445            ),
446            slot,
447            endorsed_block,
448        }
449    }
450}
451
452impl Deserializer<Endorsement> for EndorsementDeserializerLW {
453    /// ## Example:
454    /// ```rust
455    /// use massa_models::{slot::Slot, block_id::BlockId, endorsement::{Endorsement, EndorsementSerializerLW, EndorsementDeserializerLW}};
456    /// use massa_serialization::{Serializer, Deserializer, DeserializeError};
457    /// use massa_hash::Hash;
458    ///
459    /// let slot = Slot::new(1, 2);
460    /// let endorsed_block = BlockId::generate_from_hash(Hash::compute_from("test".as_bytes()));
461    /// let endorsement = Endorsement {
462    ///   slot: slot,
463    ///   index: 0,
464    ///   endorsed_block: endorsed_block
465    /// };
466    /// let mut buffer = Vec::new();
467    /// EndorsementSerializerLW::new().serialize(&endorsement, &mut buffer).unwrap();
468    /// let (rest, deserialized) = EndorsementDeserializerLW::new(10, slot, endorsed_block).deserialize::<DeserializeError>(&buffer).unwrap();
469    /// assert_eq!(rest.len(), 0);
470    /// assert_eq!(deserialized.index, endorsement.index);
471    /// ```
472    fn deserialize<'a, E: ParseError<&'a [u8]> + ContextError<&'a [u8]>>(
473        &self,
474        buffer: &'a [u8],
475    ) -> IResult<&'a [u8], Endorsement, E> {
476        context("Failed index deserialization", |input| {
477            self.index_deserializer.deserialize(input)
478        })
479        .map(|index| Endorsement {
480            slot: self.slot,
481            index,
482            endorsed_block: self.endorsed_block,
483        })
484        .parse(buffer)
485    }
486}
487
488/// A denunciation data for endorsement
489#[derive(Debug)]
490pub struct EndorsementDenunciationData {
491    slot: Slot,
492    index: u32,
493}
494
495impl EndorsementDenunciationData {
496    /// Create a new denunciation data for endorsement
497    pub fn new(slot: Slot, index: u32) -> Self {
498        Self { slot, index }
499    }
500
501    /// Get byte array
502    pub fn to_bytes(&self) -> Vec<u8> {
503        let mut buf = Vec::new();
504        buf.extend(self.slot.to_bytes_key());
505        buf.extend(self.index.to_le_bytes());
506        buf
507    }
508}
509
510#[cfg(test)]
511mod tests {
512    use crate::secure_share::{SecureShareContent, SecureShareDeserializer, SecureShareSerializer};
513    use massa_signature::verify_signature_batch;
514    use serde_json::Value;
515
516    use super::*;
517    use crate::config::CHAINID;
518    use massa_serialization::DeserializeError;
519    use massa_signature::KeyPair;
520    use serial_test::serial;
521
522    #[test]
523    #[serial]
524    fn test_endorsement_serialization() {
525        let sender_keypair = KeyPair::generate(0).unwrap();
526        let content = Endorsement {
527            slot: Slot::new(10, 1),
528            index: 0,
529            endorsed_block: BlockId::generate_from_hash(Hash::compute_from("blk".as_bytes())),
530        };
531        let endorsement: SecureShareEndorsement = Endorsement::new_verifiable(
532            content,
533            EndorsementSerializer::new(),
534            &sender_keypair,
535            *CHAINID,
536        )
537        .unwrap();
538
539        let mut ser_endorsement: Vec<u8> = Vec::new();
540        let serializer = SecureShareSerializer::new();
541        serializer
542            .serialize(&endorsement, &mut ser_endorsement)
543            .unwrap();
544        let (_, res_endorsement): (&[u8], SecureShareEndorsement) =
545            SecureShareDeserializer::new(EndorsementDeserializer::new(32, 1), *CHAINID)
546                .deserialize::<DeserializeError>(&ser_endorsement)
547                .unwrap();
548        assert_eq!(res_endorsement, endorsement);
549    }
550
551    #[test]
552    #[serial]
553    fn test_endorsement_lightweight_serialization() {
554        let sender_keypair = KeyPair::generate(0).unwrap();
555        let content = Endorsement {
556            slot: Slot::new(10, 1),
557            index: 0,
558            endorsed_block: BlockId::generate_from_hash(Hash::compute_from("blk".as_bytes())),
559        };
560        let endorsement: SecureShareEndorsement = Endorsement::new_verifiable(
561            content,
562            EndorsementSerializerLW::new(),
563            &sender_keypair,
564            *CHAINID,
565        )
566        .unwrap();
567
568        let mut ser_endorsement: Vec<u8> = Vec::new();
569        let serializer = SecureShareSerializer::new();
570        serializer
571            .serialize(&endorsement, &mut ser_endorsement)
572            .unwrap();
573
574        let parent = BlockId::generate_from_hash(Hash::compute_from("blk".as_bytes()));
575
576        let (_, res_endorsement): (&[u8], SecureShareEndorsement) = SecureShareDeserializer::new(
577            EndorsementDeserializerLW::new(1, Slot::new(10, 1), parent),
578            *CHAINID,
579        )
580        .deserialize::<DeserializeError>(&ser_endorsement)
581        .unwrap();
582        // Test only endorsement index as with the lw ser. we only process this field
583        assert_eq!(res_endorsement.content.index, endorsement.content.index);
584    }
585
586    #[test]
587    fn test_verify_sig_batch() {
588        // test verify_signature_batch as we override SecureShareEndorsements compute_hash
589
590        let sender_keypair = KeyPair::generate(0).unwrap();
591        let content_1 = Endorsement {
592            slot: Slot::new(10, 1),
593            index: 0,
594            endorsed_block: BlockId::generate_from_hash(Hash::compute_from("blk1".as_bytes())),
595        };
596        let s_endorsement_1: SecureShareEndorsement = Endorsement::new_verifiable(
597            content_1,
598            EndorsementSerializer::new(),
599            &sender_keypair,
600            *CHAINID,
601        )
602        .unwrap();
603        let mut serialized = vec![];
604        SecureShareSerializer::new()
605            .serialize(&s_endorsement_1, &mut serialized)
606            .unwrap();
607        let (_, s_endorsement_1): (&[u8], SecureShare<Endorsement, EndorsementId>) =
608            SecureShareDeserializer::new(EndorsementDeserializer::new(32, 32), *CHAINID)
609                .deserialize::<DeserializeError>(&serialized)
610                .unwrap();
611        let sender_keypair = KeyPair::generate(0).unwrap();
612        let content_2 = Endorsement {
613            slot: Slot::new(2, 5),
614            index: 0,
615            endorsed_block: BlockId::generate_from_hash(Hash::compute_from("blk2".as_bytes())),
616        };
617        let s_endorsement_2: SecureShareEndorsement = Endorsement::new_verifiable(
618            content_2,
619            EndorsementSerializerLW::new(),
620            &sender_keypair,
621            *CHAINID,
622        )
623        .unwrap();
624
625        // Test with batch len == 1 (no // verif)
626        let batch_1 = [(
627            s_endorsement_1.compute_signed_hash(),
628            s_endorsement_1.signature,
629            s_endorsement_1.content_creator_pub_key,
630        )];
631        verify_signature_batch(&batch_1).unwrap();
632
633        // Test with batch len > 1 (// verif)
634        let batch_2 = [
635            (
636                s_endorsement_1.compute_signed_hash(),
637                s_endorsement_1.signature,
638                s_endorsement_1.content_creator_pub_key,
639            ),
640            (
641                s_endorsement_2.compute_signed_hash(),
642                s_endorsement_2.signature,
643                s_endorsement_2.content_creator_pub_key,
644            ),
645        ];
646        verify_signature_batch(&batch_2).unwrap();
647    }
648
649    #[test]
650    #[serial]
651    fn test_endorsement_id() {
652        let expected_endorsement_id =
653            EndorsementId::from_str("E12Uy7hrAUHpmHQTWu68p17v7VtZJ6syBTWEJH6jwMTWJB6fdSc7")
654                .unwrap();
655        let actual_endorsement_id = EndorsementId::new(Hash::compute_from("edm".as_bytes()));
656
657        assert_eq!(actual_endorsement_id, expected_endorsement_id);
658    }
659
660    #[test]
661    #[serial]
662    fn test_endorsement_id_errors() {
663        let actual_error = EndorsementId::from_str("SomeInvalidEndorsementId")
664            .unwrap_err()
665            .to_string();
666        let expected_error = "endorsement id parsing error".to_string();
667
668        assert_eq!(actual_error, expected_error);
669    }
670
671    #[test]
672    #[serial]
673    fn test_endorsement_serde() {
674        let orig_endorsement = Endorsement {
675            slot: Slot::new(10, 1),
676            index: 0,
677            endorsed_block: BlockId::generate_from_hash(Hash::compute_from("blk".as_bytes())),
678        };
679        let serialized_endorsement = serde_json::to_string(&orig_endorsement).unwrap();
680        let res_endorsement: Value = serde_json::from_str(&serialized_endorsement).unwrap();
681        // check equality
682        assert_eq!(
683            orig_endorsement.slot.period,
684            res_endorsement["slot"]["period"]
685        );
686        assert_eq!(
687            orig_endorsement.slot.thread,
688            res_endorsement["slot"]["thread"]
689        );
690        assert_eq!(orig_endorsement.index, res_endorsement["index"]);
691        assert_eq!(
692            orig_endorsement.endorsed_block.to_string(),
693            res_endorsement["endorsed_block"]
694        );
695    }
696}