massa_models/
denunciation.rs

1//! Copyright (c) 2022 MASSA LABS <info@massa.net>
2
3//! Denunciation intro
4//!
5//! Currently, nothing prevents a user from producing multiple blocks (for the same Slot) or endorsing a block multiple times.
6//! If an invalid Denunciation will just be ignored, a valid Denunciation will slash some locked rolls.
7//! Note that this proposal aims at dissuading 'rational' users from causing damage but does help against 'byzantine' actors
8//! that just want to disturb or break the blockclique (at any cost).
9//!
10//! Denunciation structure
11//!
12//! A denunciation embeds some information such as
13//! Slot, the slot from the block header or the endorsements (+ index)
14//! A public key: the public key of the secure share endorsement or secured header
15//! 2 Hashes & 2 Signatures
16//!
17//! All of this constitutes a proof (and is verifiable) that a user produced multiple (at least 2) blocks or endorsements
18//!
19//! Denunciation creation
20//!
21//! Denunciations are created in a Denunciation pool, receiving new blocks & block headers & endorsements from various places.
22//! The denunciation pool is also responsible of returning a list of denunciations to insert into a new block header.
23//! After execution, the denunciation is kept for some time inside `executed_denunciations`
24//! in order to prevent multiple executions. Note that this structure is part of the final state hash and thus is bootstrapped.
25//!
26//! Denunciation execution
27//!
28//! When a Denunciation is proven valid, we then need to ensure that the user has sufficient funds. If we cannot deduct the funds on node
29//! balance or staked rolls, we will use 'locked' rolls` (or Deferred credits).
30//! Selling a roll will lock it for some time (== 4 cycles). Note that it restricts the time,
31//! a denunciation can be produced (A constant value will be defined for this).
32//!
33//! Punishment model
34//!
35//! A valid denunciation slashes locked rolls only. It does not forfeit slot rewards
36//! (`EndorsementCreatorReward`, `EndorsementTargetReward`, `BlockCreatorReward`) for the
37//! denouncing block. A validator denounced in a block may still receive those rewards in the
38//! same slot. That is intentional: excluding denounced addresses from same-slot rewards would
39//! make the effective penalty depend on whether they were also selected by draws for that slot,
40//! which is not a coherent punishment model.
41
42#![allow(missing_docs)]
43
44use std::cmp::Ordering;
45use std::ops::Bound::{Excluded, Included};
46
47use nom::{
48    error::{context, ContextError, ParseError},
49    sequence::tuple,
50    IResult, Parser,
51};
52use num_enum::{IntoPrimitive, TryFromPrimitive};
53use serde::{Deserialize, Serialize};
54use thiserror::Error;
55
56use crate::block_header::{BlockHeaderDenunciationData, SecuredHeader};
57use crate::endorsement::{EndorsementDenunciationData, SecureShareEndorsement};
58use crate::slot::{Slot, SlotDeserializer, SlotSerializer};
59
60use crate::secure_share::Id;
61use massa_hash::{Hash, HashDeserializer, HashSerializer};
62use massa_serialization::{
63    Deserializer, SerializeError, Serializer, U32VarIntDeserializer, U32VarIntSerializer,
64};
65use massa_signature::{
66    MassaSignatureError, PublicKey, PublicKeyDeserializer, Signature, SignatureDeserializer,
67};
68use variant_count::VariantCount;
69
70/// A Variant of Denunciation enum for endorsement
71#[allow(dead_code)]
72#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
73pub struct EndorsementDenunciation {
74    public_key: PublicKey,
75    slot: Slot,
76    index: u32,
77    hash_1: Hash,
78    hash_2: Hash,
79    signature_1: Signature,
80    signature_2: Signature,
81}
82
83impl EndorsementDenunciation {
84    /// Rebuild full hash of SecureShareEndorsement from given arguments.
85    ///
86    /// `sig_chain_id` selects the signed-hash layout: `None` reproduces the legacy
87    /// (chain-agnostic) layout, `Some(chain_id)` reproduces the chain-scoped layout
88    /// used post MIP-0002 `Execution` v2 activation. It must match the layout used to
89    /// sign the endorsement.
90    fn compute_hash_for_sig_verif(
91        public_key: &PublicKey,
92        slot: &Slot,
93        index: &u32,
94        content_hash: &Hash,
95        sig_chain_id: Option<u64>,
96    ) -> Hash {
97        let mut hash_data = Vec::new();
98        // Public key
99        hash_data.extend(public_key.to_bytes());
100        // Ser slot & index (+ optional chain id)
101        let denunciation_data = EndorsementDenunciationData::new(*slot, *index, sig_chain_id);
102        hash_data.extend(&denunciation_data.to_bytes());
103        // Add content hash
104        hash_data.extend(content_hash.to_bytes());
105        Hash::compute_from(&hash_data)
106    }
107
108    // Getters (for GRPC From)
109    pub fn get_public_key(&self) -> &PublicKey {
110        &self.public_key
111    }
112    pub fn get_slot(&self) -> &Slot {
113        &self.slot
114    }
115    pub fn get_index(&self) -> &u32 {
116        &self.index
117    }
118    pub fn get_hash_1(&self) -> &Hash {
119        &self.hash_1
120    }
121    pub fn get_hash_2(&self) -> &Hash {
122        &self.hash_2
123    }
124    pub fn get_signature_1(&self) -> &Signature {
125        &self.signature_1
126    }
127    pub fn get_signature_2(&self) -> &Signature {
128        &self.signature_2
129    }
130}
131
132/// A Variant of Denunciation enum for block header
133#[allow(dead_code)]
134#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
135pub struct BlockHeaderDenunciation {
136    public_key: PublicKey,
137    slot: Slot,
138    hash_1: Hash,
139    hash_2: Hash,
140    signature_1: Signature,
141    signature_2: Signature,
142}
143
144impl BlockHeaderDenunciation {
145    /// Rebuild full hash of SecuredHeader from given arguments.
146    ///
147    /// `sig_chain_id` selects the signed-hash layout: `None` reproduces the legacy
148    /// (chain-agnostic) layout, `Some(chain_id)` reproduces the chain-scoped layout
149    /// used post MIP-0002 `Execution` v2 activation. It must match the layout used to
150    /// sign the block header.
151    fn compute_hash_for_sig_verif(
152        public_key: &PublicKey,
153        slot: &Slot,
154        content_hash: &Hash,
155        sig_chain_id: Option<u64>,
156    ) -> Hash {
157        let mut hash_data = Vec::new();
158        // Public key
159        hash_data.extend(public_key.to_bytes());
160        // Ser slot (+ optional chain id)
161        let de_data = BlockHeaderDenunciationData::new(*slot, sig_chain_id);
162        hash_data.extend(de_data.to_bytes());
163        // Add content hash
164        hash_data.extend(content_hash.to_bytes());
165        Hash::compute_from(&hash_data)
166    }
167
168    // Getters (for GRPC From)
169    pub fn get_public_key(&self) -> &PublicKey {
170        &self.public_key
171    }
172    pub fn get_slot(&self) -> &Slot {
173        &self.slot
174    }
175    pub fn get_hash_1(&self) -> &Hash {
176        &self.hash_1
177    }
178    pub fn get_hash_2(&self) -> &Hash {
179        &self.hash_2
180    }
181    pub fn get_signature_1(&self) -> &Signature {
182        &self.signature_1
183    }
184    pub fn get_signature_2(&self) -> &Signature {
185        &self.signature_2
186    }
187}
188
189/// A denunciation enum
190#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
191#[allow(missing_docs)]
192pub enum Denunciation {
193    Endorsement(EndorsementDenunciation),
194    BlockHeader(BlockHeaderDenunciation),
195}
196
197#[allow(dead_code)]
198impl Denunciation {
199    /// Check if it is a Denunciation of several endorsements
200    pub fn is_for_endorsement(&self) -> bool {
201        matches!(self, Denunciation::Endorsement(_))
202    }
203
204    /// Check if it is a Denunciation of several block headers
205    pub fn is_for_block_header(&self) -> bool {
206        matches!(self, Denunciation::BlockHeader(_))
207    }
208
209    /// Check if it is a Denunciation for this endorsement
210    ///
211    /// `sig_chain_id` selects the signed-hash layout used to reconstruct the
212    /// signature hash (see [`EndorsementDenunciation::compute_hash_for_sig_verif`]).
213    pub fn is_also_for_endorsement(
214        &self,
215        s_endorsement: &SecureShareEndorsement,
216        sig_chain_id: Option<u64>,
217    ) -> Result<bool, DenunciationError> {
218        match self {
219            Denunciation::BlockHeader(_) => Ok(false),
220            Denunciation::Endorsement(endo_de) => {
221                let content_hash = s_endorsement.id.get_hash();
222
223                let hash = EndorsementDenunciation::compute_hash_for_sig_verif(
224                    &endo_de.public_key,
225                    &endo_de.slot,
226                    &endo_de.index,
227                    content_hash,
228                    sig_chain_id,
229                );
230
231                Ok(endo_de.slot == s_endorsement.content.slot
232                    && endo_de.index == s_endorsement.content.index
233                    && endo_de.public_key == s_endorsement.content_creator_pub_key
234                    && endo_de.hash_1 != *content_hash
235                    && endo_de.hash_2 != *content_hash
236                    && endo_de
237                        .public_key
238                        .verify_signature(&hash, &s_endorsement.signature)
239                        .is_ok())
240            }
241        }
242    }
243
244    /// Check if it is a Denunciation for this block header
245    ///
246    /// `sig_chain_id` selects the signed-hash layout used to reconstruct the
247    /// signature hash (see [`BlockHeaderDenunciation::compute_hash_for_sig_verif`]).
248    pub fn is_also_for_block_header(
249        &self,
250        s_block_header: &SecuredHeader,
251        sig_chain_id: Option<u64>,
252    ) -> Result<bool, DenunciationError> {
253        match self {
254            Denunciation::Endorsement(_) => Ok(false),
255            Denunciation::BlockHeader(endo_bh) => {
256                let content_hash = s_block_header.id.get_hash();
257
258                let hash = BlockHeaderDenunciation::compute_hash_for_sig_verif(
259                    &endo_bh.public_key,
260                    &endo_bh.slot,
261                    content_hash,
262                    sig_chain_id,
263                );
264
265                Ok(endo_bh.slot == s_block_header.content.slot
266                    && endo_bh.public_key == s_block_header.content_creator_pub_key
267                    && endo_bh.hash_1 != *content_hash
268                    && endo_bh.hash_2 != *content_hash
269                    && endo_bh
270                        .public_key
271                        .verify_signature(&hash, &s_block_header.signature)
272                        .is_ok())
273            }
274        }
275    }
276
277    /// Check if Denunciation is valid using the legacy (chain-agnostic) signed-hash
278    /// layout. Kept for backward compatibility with tests / pre-activation data.
279    /// Production code should use [`Self::is_valid_with_chain`] with the
280    /// `sig_chain_id` derived from the denounced slot's network-agreed active
281    /// version.
282    pub fn is_valid(&self) -> bool {
283        self.is_valid_with_chain(None)
284    }
285
286    /// Check if Denunciation is valid, reconstructing signatures with the
287    /// chain-scoped signed-hash layout selected by `sig_chain_id` (`None` = legacy,
288    /// `Some(chain_id)` = chain-scoped).
289    ///
290    /// This is the chain-aware variant that prevents cross-chain replay of
291    /// endorsements / block headers as denunciations (F90 / PDF #11): when
292    /// `sig_chain_id` is `Some(local_chain_id)`, a denunciation combining two
293    /// messages signed on different chains no longer verifies, because the second
294    /// message's signature was produced over a hash folding in a different chain id.
295    pub fn is_valid_with_chain(&self, sig_chain_id: Option<u64>) -> bool {
296        let (signature_1, signature_2, hash_1, hash_2, public_key) = match self {
297            Denunciation::Endorsement(de) => {
298                let hash_1 = EndorsementDenunciation::compute_hash_for_sig_verif(
299                    &de.public_key,
300                    &de.slot,
301                    &de.index,
302                    &de.hash_1,
303                    sig_chain_id,
304                );
305                let hash_2 = EndorsementDenunciation::compute_hash_for_sig_verif(
306                    &de.public_key,
307                    &de.slot,
308                    &de.index,
309                    &de.hash_2,
310                    sig_chain_id,
311                );
312
313                (
314                    de.signature_1,
315                    de.signature_2,
316                    hash_1,
317                    hash_2,
318                    de.public_key,
319                )
320            }
321            Denunciation::BlockHeader(de) => {
322                let hash_1 = BlockHeaderDenunciation::compute_hash_for_sig_verif(
323                    &de.public_key,
324                    &de.slot,
325                    &de.hash_1,
326                    sig_chain_id,
327                );
328                let hash_2 = BlockHeaderDenunciation::compute_hash_for_sig_verif(
329                    &de.public_key,
330                    &de.slot,
331                    &de.hash_2,
332                    sig_chain_id,
333                );
334
335                (
336                    de.signature_1,
337                    de.signature_2,
338                    hash_1,
339                    hash_2,
340                    de.public_key,
341                )
342            }
343        };
344
345        hash_1 != hash_2
346            && public_key.verify_signature(&hash_1, &signature_1).is_ok()
347            && public_key.verify_signature(&hash_2, &signature_2).is_ok()
348    }
349
350    /// Get Denunciation slot ref
351    pub fn get_slot(&self) -> &Slot {
352        match self {
353            Denunciation::Endorsement(de) => &de.slot,
354            Denunciation::BlockHeader(de) => &de.slot,
355        }
356    }
357
358    /// Get field: index (return None for a block header denunciation)
359    pub fn get_index(&self) -> Option<&u32> {
360        match self {
361            Denunciation::BlockHeader(_) => None,
362            Denunciation::Endorsement(de) => Some(&de.index),
363        }
364    }
365
366    /// Get Denunciation public key ref
367    pub fn get_public_key(&self) -> &PublicKey {
368        match self {
369            Denunciation::Endorsement(de) => &de.public_key,
370            Denunciation::BlockHeader(de) => &de.public_key,
371        }
372    }
373
374    /// Check if denunciation has expired given a slot period
375    /// Note that slot_period can be:
376    /// * A final slot period (for example in order to cleanup denunciation pool caches)
377    /// * A block slot period (in execution (execute_denunciation(...)))
378    pub fn is_expired(
379        denunciation_slot_period: &u64,
380        slot_period: &u64,
381        denunciation_expire_periods: &u64,
382    ) -> bool {
383        slot_period.checked_sub(*denunciation_slot_period) > Some(*denunciation_expire_periods)
384    }
385}
386
387/// Create a new Denunciation from 2 SecureShareEndorsement (legacy, chain-agnostic
388/// signed-hash layout). Kept for backward compatibility with tests and
389/// pre-activation data. Production code should use the chain-aware tuple variant.
390impl TryFrom<(&SecureShareEndorsement, &SecureShareEndorsement)> for Denunciation {
391    type Error = DenunciationError;
392
393    fn try_from(
394        (s_e1, s_e2): (&SecureShareEndorsement, &SecureShareEndorsement),
395    ) -> Result<Self, Self::Error> {
396        Denunciation::try_from((s_e1, s_e2, None))
397    }
398}
399
400/// Create a new Denunciation from 2 SecureShareEndorsement using the chain-scoped
401/// signed-hash layout selected by `sig_chain_id` (`None` = legacy, `Some(chain_id)`
402/// = chain-scoped). The same `sig_chain_id` must be used to sign the endorsements
403/// and to verify the resulting denunciation.
404impl
405    TryFrom<(
406        &SecureShareEndorsement,
407        &SecureShareEndorsement,
408        Option<u64>,
409    )> for Denunciation
410{
411    type Error = DenunciationError;
412
413    fn try_from(
414        (s_e1, s_e2, sig_chain_id): (
415            &SecureShareEndorsement,
416            &SecureShareEndorsement,
417            Option<u64>,
418        ),
419    ) -> Result<Self, Self::Error> {
420        // In order to create a Denunciation, there should be the same
421        // slot, index & public key
422        if s_e1.content.slot != s_e2.content.slot
423            || s_e1.content.index != s_e2.content.index
424            || s_e1.content_creator_pub_key != s_e2.content_creator_pub_key
425            || s_e1.id == s_e2.id
426        {
427            return Err(DenunciationError::InvalidInput(format!(
428                "Not the same slot, index or public key or same hash for {:?} & {:?}",
429                s_e1, s_e2
430            )));
431        }
432
433        // Check sig of s_e1 with s_e1.public_key, s_e1.slot, s_e1.index
434        let s_e1_hash_content = s_e1.id.get_hash();
435        let s_e1_hash = EndorsementDenunciation::compute_hash_for_sig_verif(
436            &s_e1.content_creator_pub_key,
437            &s_e1.content.slot,
438            &s_e1.content.index,
439            s_e1_hash_content,
440            sig_chain_id,
441        );
442        // Check sig of s_e2 but with s_e1.public_key, s_e1.slot, s_e1.index
443        let s_e2_hash_content = s_e2.id.get_hash();
444        let s_e2_hash = EndorsementDenunciation::compute_hash_for_sig_verif(
445            &s_e1.content_creator_pub_key,
446            &s_e1.content.slot,
447            &s_e1.content.index,
448            s_e2_hash_content,
449            sig_chain_id,
450        );
451
452        s_e1.content_creator_pub_key
453            .verify_signature(&s_e1_hash, &s_e1.signature)?;
454        s_e1.content_creator_pub_key
455            .verify_signature(&s_e2_hash, &s_e2.signature)?;
456
457        Ok(Denunciation::Endorsement(EndorsementDenunciation {
458            public_key: s_e1.content_creator_pub_key,
459            slot: s_e1.content.slot,
460            index: s_e1.content.index,
461            signature_1: s_e1.signature,
462            signature_2: s_e2.signature,
463            hash_1: *s_e1_hash_content,
464            hash_2: *s_e2_hash_content,
465        }))
466    }
467}
468
469/// Create a new Denunciation from 2 SecureHeader (legacy, chain-agnostic signed-hash
470/// layout). Kept for backward compatibility with tests and pre-activation data.
471/// Production code should use the chain-aware tuple variant.
472impl TryFrom<(&SecuredHeader, &SecuredHeader)> for Denunciation {
473    type Error = DenunciationError;
474
475    fn try_from((s_bh1, s_bh2): (&SecuredHeader, &SecuredHeader)) -> Result<Self, Self::Error> {
476        Denunciation::try_from((s_bh1, s_bh2, None))
477    }
478}
479
480/// Create a new Denunciation from 2 SecureHeader using the chain-scoped signed-hash
481/// layout selected by `sig_chain_id` (`None` = legacy, `Some(chain_id)` =
482/// chain-scoped).
483impl TryFrom<(&SecuredHeader, &SecuredHeader, Option<u64>)> for Denunciation {
484    type Error = DenunciationError;
485
486    fn try_from(
487        (s_bh1, s_bh2, sig_chain_id): (&SecuredHeader, &SecuredHeader, Option<u64>),
488    ) -> Result<Self, Self::Error> {
489        // Cannot use the same block header twice
490        // In order to create a Denunciation, there should be the same slot & public key
491        if s_bh1.content.slot != s_bh2.content.slot
492            || s_bh1.content_creator_pub_key != s_bh2.content_creator_pub_key
493            || s_bh1.id == s_bh2.id
494        {
495            return Err(DenunciationError::InvalidInput(format!(
496                "Not the same slot or public key or same hash for {:?} & {:?}",
497                s_bh1, s_bh2
498            )));
499        }
500
501        // Check sig of s_bh2 but with s_bh1.public_key, s_bh1.slot, s_bh1.index
502        let s_bh1_hash_content = s_bh1.id.get_hash();
503        let s_bh1_hash = BlockHeaderDenunciation::compute_hash_for_sig_verif(
504            &s_bh1.content_creator_pub_key,
505            &s_bh1.content.slot,
506            s_bh1_hash_content,
507            sig_chain_id,
508        );
509        let s_bh2_hash_content = s_bh2.id.get_hash();
510        let s_bh2_hash = BlockHeaderDenunciation::compute_hash_for_sig_verif(
511            &s_bh1.content_creator_pub_key,
512            &s_bh1.content.slot,
513            s_bh2_hash_content,
514            sig_chain_id,
515        );
516
517        s_bh1
518            .content_creator_pub_key
519            .verify_signature(&s_bh1_hash, &s_bh1.signature)?;
520        s_bh1
521            .content_creator_pub_key
522            .verify_signature(&s_bh2_hash, &s_bh2.signature)?;
523
524        Ok(Denunciation::BlockHeader(BlockHeaderDenunciation {
525            public_key: s_bh1.content_creator_pub_key,
526            slot: s_bh1.content.slot,
527            signature_1: s_bh1.signature,
528            signature_2: s_bh2.signature,
529            hash_1: *s_bh1_hash_content,
530            hash_2: *s_bh2_hash_content,
531        }))
532    }
533}
534
535#[allow(missing_docs)]
536#[derive(IntoPrimitive, Debug, TryFromPrimitive, VariantCount)]
537#[repr(u32)]
538pub enum DenunciationTypeId {
539    BlockHeader = 0,
540    Endorsement = 1,
541}
542
543impl From<&Denunciation> for DenunciationTypeId {
544    fn from(value: &Denunciation) -> Self {
545        match value {
546            Denunciation::Endorsement(_) => DenunciationTypeId::Endorsement,
547            Denunciation::BlockHeader(_) => DenunciationTypeId::BlockHeader,
548        }
549    }
550}
551
552/// Denunciation error
553#[allow(missing_docs)]
554#[derive(Error, Debug)]
555pub enum DenunciationError {
556    #[error("Invalid endorsements or block headers, cannot create denunciation: {0}")]
557    InvalidInput(String),
558    #[error("signature error: {0}")]
559    Signature(#[from] MassaSignatureError),
560    #[error("serialization error: {0}")]
561    Serialization(#[from] SerializeError),
562}
563
564// Serialization / Deserialization
565
566/// Serializer for `EndorsementDenunciation`
567struct EndorsementDenunciationSerializer {
568    slot_serializer: SlotSerializer,
569    u32_serializer: U32VarIntSerializer,
570    hash_serializer: HashSerializer,
571}
572
573impl EndorsementDenunciationSerializer {
574    /// Creates a new `EndorsementDenunciationSerializer`
575    const fn new() -> Self {
576        Self {
577            slot_serializer: SlotSerializer::new(),
578            u32_serializer: U32VarIntSerializer::new(),
579            hash_serializer: HashSerializer::new(),
580        }
581    }
582}
583
584impl Default for EndorsementDenunciationSerializer {
585    fn default() -> Self {
586        Self::new()
587    }
588}
589
590impl Serializer<EndorsementDenunciation> for EndorsementDenunciationSerializer {
591    fn serialize(
592        &self,
593        value: &EndorsementDenunciation,
594        buffer: &mut Vec<u8>,
595    ) -> Result<(), SerializeError> {
596        buffer.extend(value.public_key.to_bytes());
597        self.slot_serializer.serialize(&value.slot, buffer)?;
598        self.u32_serializer.serialize(&value.index, buffer)?;
599        self.hash_serializer.serialize(&value.hash_1, buffer)?;
600        self.hash_serializer.serialize(&value.hash_2, buffer)?;
601        buffer.extend(value.signature_1.to_bytes());
602        buffer.extend(value.signature_2.to_bytes());
603        Ok(())
604    }
605}
606
607/// Deserializer for `EndorsementDenunciation`
608struct EndorsementDenunciationDeserializer {
609    slot_deserializer: SlotDeserializer,
610    index_deserializer: U32VarIntDeserializer,
611    hash_deserializer: HashDeserializer,
612    pubkey_deserializer: PublicKeyDeserializer,
613    signature_deserializer: SignatureDeserializer,
614}
615
616impl EndorsementDenunciationDeserializer {
617    /// Creates a new `EndorsementDeserializer`
618    const fn new(thread_count: u8, endorsement_count: u32) -> Self {
619        EndorsementDenunciationDeserializer {
620            slot_deserializer: SlotDeserializer::new(
621                (Included(0), Included(u64::MAX)),
622                (Included(0), Excluded(thread_count)),
623            ),
624            index_deserializer: U32VarIntDeserializer::new(
625                Included(0),
626                Excluded(endorsement_count),
627            ),
628            hash_deserializer: HashDeserializer::new(),
629            pubkey_deserializer: PublicKeyDeserializer::new(),
630            signature_deserializer: SignatureDeserializer::new(),
631        }
632    }
633}
634
635impl Deserializer<EndorsementDenunciation> for EndorsementDenunciationDeserializer {
636    fn deserialize<'a, E: ParseError<&'a [u8]> + ContextError<&'a [u8]>>(
637        &self,
638        buffer: &'a [u8],
639    ) -> IResult<&'a [u8], EndorsementDenunciation, E> {
640        context(
641            "Failed Endorsement Denunciation deserialization",
642            tuple((
643                context("Failed public key deserialization", |input| {
644                    self.pubkey_deserializer.deserialize(input)
645                }),
646                context("Failed slot deserialization", |input| {
647                    self.slot_deserializer.deserialize(input)
648                }),
649                context("Failed slot deserialization", |input| {
650                    self.index_deserializer.deserialize(input)
651                }),
652                context("Failed hash 1 deserialization", |input| {
653                    self.hash_deserializer.deserialize(input)
654                }),
655                context("Failed hash 2 deserialization", |input| {
656                    self.hash_deserializer.deserialize(input)
657                }),
658                context("Failed signature 1 deserialization", |input| {
659                    self.signature_deserializer.deserialize(input)
660                }),
661                context("Failed signature 2 deserialization", |input| {
662                    self.signature_deserializer.deserialize(input)
663                }),
664            )),
665        )
666        .map(
667            |(public_key, slot, index, hash_1, hash_2, signature_1, signature_2)| {
668                EndorsementDenunciation {
669                    public_key,
670                    slot,
671                    index,
672                    hash_1,
673                    hash_2,
674                    signature_1,
675                    signature_2,
676                }
677            },
678        )
679        .parse(buffer)
680    }
681}
682
683/// Serializer for `BlockHeaderDenunciation`
684struct BlockHeaderDenunciationSerializer {
685    slot_serializer: SlotSerializer,
686    hash_serializer: HashSerializer,
687}
688
689impl BlockHeaderDenunciationSerializer {
690    /// Creates a new `BlockHeaderDenunciationSerializer`
691    const fn new() -> Self {
692        Self {
693            slot_serializer: SlotSerializer::new(),
694            hash_serializer: HashSerializer::new(),
695        }
696    }
697}
698
699impl Default for BlockHeaderDenunciationSerializer {
700    fn default() -> Self {
701        Self::new()
702    }
703}
704
705impl Serializer<BlockHeaderDenunciation> for BlockHeaderDenunciationSerializer {
706    fn serialize(
707        &self,
708        value: &BlockHeaderDenunciation,
709        buffer: &mut Vec<u8>,
710    ) -> Result<(), SerializeError> {
711        buffer.extend(value.public_key.to_bytes());
712        self.slot_serializer.serialize(&value.slot, buffer)?;
713        self.hash_serializer.serialize(&value.hash_1, buffer)?;
714        self.hash_serializer.serialize(&value.hash_2, buffer)?;
715        buffer.extend(value.signature_1.to_bytes());
716        buffer.extend(value.signature_2.to_bytes());
717        Ok(())
718    }
719}
720
721/// Deserializer for `BlockHeaderDenunciation`
722struct BlockHeaderDenunciationDeserializer {
723    slot_deserializer: SlotDeserializer,
724    hash_deserializer: HashDeserializer,
725    pubkey_deserializer: PublicKeyDeserializer,
726    signature_deserializer: SignatureDeserializer,
727}
728
729impl BlockHeaderDenunciationDeserializer {
730    /// Creates a new `BlockHeaderDenunciationDeserializer`
731    pub const fn new(thread_count: u8) -> Self {
732        Self {
733            slot_deserializer: SlotDeserializer::new(
734                (Included(0), Included(u64::MAX)),
735                (Included(0), Excluded(thread_count)),
736            ),
737            hash_deserializer: HashDeserializer::new(),
738            pubkey_deserializer: PublicKeyDeserializer::new(),
739            signature_deserializer: SignatureDeserializer::new(),
740        }
741    }
742}
743
744impl Deserializer<BlockHeaderDenunciation> for BlockHeaderDenunciationDeserializer {
745    fn deserialize<'a, E: ParseError<&'a [u8]> + ContextError<&'a [u8]>>(
746        &self,
747        buffer: &'a [u8],
748    ) -> IResult<&'a [u8], BlockHeaderDenunciation, E> {
749        context(
750            "Failed BlockHeader Denunciation deserialization",
751            tuple((
752                context("Failed public key deserialization", |input| {
753                    self.pubkey_deserializer.deserialize(input)
754                }),
755                context("Failed slot deserialization", |input| {
756                    self.slot_deserializer.deserialize(input)
757                }),
758                context("Failed hash 1 deserialization", |input| {
759                    self.hash_deserializer.deserialize(input)
760                }),
761                context("Failed hash 2 deserialization", |input| {
762                    self.hash_deserializer.deserialize(input)
763                }),
764                context("Failed signature 1 deserialization", |input| {
765                    self.signature_deserializer.deserialize(input)
766                }),
767                context("Failed signature 2 deserialization", |input| {
768                    self.signature_deserializer.deserialize(input)
769                }),
770            )),
771        )
772        .map(
773            |(public_key, slot, hash_1, hash_2, signature_1, signature_2)| {
774                BlockHeaderDenunciation {
775                    public_key,
776                    slot,
777                    hash_1,
778                    hash_2,
779                    signature_1,
780                    signature_2,
781                }
782            },
783        )
784        .parse(buffer)
785    }
786}
787
788/// Serializer for `Denunciation`
789pub struct DenunciationSerializer {
790    endo_de_serializer: EndorsementDenunciationSerializer,
791    blkh_de_serializer: BlockHeaderDenunciationSerializer,
792    type_id_serializer: U32VarIntSerializer,
793}
794
795impl DenunciationSerializer {
796    /// Creates a new `BlockHeaderDenunciationSerializer`
797    pub const fn new() -> Self {
798        Self {
799            endo_de_serializer: EndorsementDenunciationSerializer::new(),
800            blkh_de_serializer: BlockHeaderDenunciationSerializer::new(),
801            type_id_serializer: U32VarIntSerializer::new(),
802        }
803    }
804}
805
806impl Default for DenunciationSerializer {
807    fn default() -> Self {
808        Self::new()
809    }
810}
811
812impl Serializer<Denunciation> for DenunciationSerializer {
813    fn serialize(&self, value: &Denunciation, buffer: &mut Vec<u8>) -> Result<(), SerializeError> {
814        let de_type_id = DenunciationTypeId::from(value);
815        self.type_id_serializer
816            .serialize(&u32::from(de_type_id), buffer)?;
817        match value {
818            Denunciation::Endorsement(de) => {
819                self.endo_de_serializer.serialize(de, buffer)?;
820            }
821            Denunciation::BlockHeader(de) => {
822                self.blkh_de_serializer.serialize(de, buffer)?;
823            }
824        }
825        Ok(())
826    }
827}
828
829/// Deserializer for `Denunciation`
830pub struct DenunciationDeserializer {
831    endo_de_deserializer: EndorsementDenunciationDeserializer,
832    blkh_de_deserializer: BlockHeaderDenunciationDeserializer,
833    type_id_deserializer: U32VarIntDeserializer,
834}
835
836impl DenunciationDeserializer {
837    /// Creates a new `DenunciationDeserializer`
838    pub const fn new(thread_count: u8, endorsement_count: u32) -> Self {
839        Self {
840            endo_de_deserializer: EndorsementDenunciationDeserializer::new(
841                thread_count,
842                endorsement_count,
843            ),
844            blkh_de_deserializer: BlockHeaderDenunciationDeserializer::new(thread_count),
845            type_id_deserializer: U32VarIntDeserializer::new(
846                Included(0),
847                Excluded(DenunciationTypeId::VARIANT_COUNT as u32),
848            ),
849        }
850    }
851}
852
853impl Deserializer<Denunciation> for DenunciationDeserializer {
854    fn deserialize<'a, E: ParseError<&'a [u8]> + ContextError<&'a [u8]>>(
855        &self,
856        buffer: &'a [u8],
857    ) -> IResult<&'a [u8], Denunciation, E> {
858        let (rem, de_type_id_) = context("Failed Denunciation type id deserialization", |input| {
859            self.type_id_deserializer.deserialize(input)
860        })
861        .parse(buffer)?;
862
863        let de_type_id = DenunciationTypeId::try_from(de_type_id_).map_err(|_| {
864            nom::Err::Error(ParseError::from_error_kind(
865                buffer,
866                nom::error::ErrorKind::Fail,
867            ))
868        })?;
869
870        match de_type_id {
871            DenunciationTypeId::Endorsement => {
872                let (rem2, endo_de) = self.endo_de_deserializer.deserialize(rem)?;
873                IResult::Ok((rem2, Denunciation::Endorsement(endo_de)))
874            }
875            DenunciationTypeId::BlockHeader => {
876                let (rem2, blkh_de) = self.blkh_de_deserializer.deserialize(rem)?;
877                IResult::Ok((rem2, Denunciation::BlockHeader(blkh_de)))
878            }
879        }
880    }
881}
882
883// End Ser / Der
884
885// Denunciation Index
886
887#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
888/// Index for Denunciations in collections (e.g. like a HashMap...)
889pub enum DenunciationIndex {
890    /// Variant for Block header denunciation index
891    BlockHeader {
892        /// de slot
893        slot: Slot,
894    },
895    /// Variant for Endorsement denunciation index
896    Endorsement {
897        /// de slot
898        slot: Slot,
899        /// de index
900        index: u32,
901    },
902}
903
904impl DenunciationIndex {
905    /// Get field: slot
906    pub fn get_slot(&self) -> &Slot {
907        match self {
908            DenunciationIndex::BlockHeader { slot } => slot,
909            DenunciationIndex::Endorsement { slot, .. } => slot,
910        }
911    }
912
913    /// Get field: index (return None for a block header denunciation index)
914    pub fn get_index(&self) -> Option<&u32> {
915        match self {
916            DenunciationIndex::BlockHeader { .. } => None,
917            DenunciationIndex::Endorsement { slot: _, index } => Some(index),
918        }
919    }
920
921    /// Compute the hash
922    pub fn get_hash(&self) -> Hash {
923        let mut buffer = u32::from(DenunciationIndexTypeId::from(self))
924            .to_le_bytes()
925            .to_vec();
926        match self {
927            DenunciationIndex::BlockHeader { slot } => buffer.extend(slot.to_bytes_key()),
928            DenunciationIndex::Endorsement { slot, index } => {
929                buffer.extend(slot.to_bytes_key());
930                buffer.extend(index.to_le_bytes());
931            }
932        }
933        Hash::compute_from(&buffer)
934    }
935}
936
937/// Create a `DenunciationIndex` from a `Denunciation`
938impl From<&Denunciation> for DenunciationIndex {
939    fn from(value: &Denunciation) -> Self {
940        match value {
941            Denunciation::Endorsement(endo_de) => DenunciationIndex::Endorsement {
942                slot: endo_de.slot,
943                index: endo_de.index,
944            },
945            Denunciation::BlockHeader(blkh_de) => {
946                DenunciationIndex::BlockHeader { slot: blkh_de.slot }
947            }
948        }
949    }
950}
951
952/// Create a `DenunciationIndex` from a `DenunciationPrecursor`
953impl From<&DenunciationPrecursor> for DenunciationIndex {
954    fn from(value: &DenunciationPrecursor) -> Self {
955        match value {
956            DenunciationPrecursor::Endorsement(de_p) => DenunciationIndex::Endorsement {
957                slot: de_p.slot,
958                index: de_p.index,
959            },
960            DenunciationPrecursor::BlockHeader(de_p) => {
961                DenunciationIndex::BlockHeader { slot: de_p.slot }
962            }
963        }
964    }
965}
966
967impl Ord for DenunciationIndex {
968    fn cmp(&self, other: &Self) -> Ordering {
969        // key ends with type id so that we avoid prioritizing one type over the other
970        // when filling block headers (comparison of tuples is from left to right).
971        let self_key = (
972            self.get_slot(),
973            self.get_index().unwrap_or(&0),
974            u32::from(DenunciationIndexTypeId::from(self)),
975        );
976        let other_key = (
977            other.get_slot(),
978            other.get_index().unwrap_or(&0),
979            u32::from(DenunciationIndexTypeId::from(other)),
980        );
981        self_key.cmp(&other_key)
982    }
983}
984
985impl PartialOrd for DenunciationIndex {
986    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
987        Some(self.cmp(other))
988    }
989}
990
991// End Denunciation Index
992
993// Denunciation Index ser der
994
995#[derive(IntoPrimitive, Debug, Eq, PartialEq, TryFromPrimitive)]
996#[repr(u32)]
997enum DenunciationIndexTypeId {
998    BlockHeader = 0,
999    Endorsement = 1,
1000}
1001
1002impl From<&DenunciationIndex> for DenunciationIndexTypeId {
1003    fn from(value: &DenunciationIndex) -> Self {
1004        match value {
1005            DenunciationIndex::BlockHeader { .. } => DenunciationIndexTypeId::BlockHeader,
1006            DenunciationIndex::Endorsement { .. } => DenunciationIndexTypeId::Endorsement,
1007        }
1008    }
1009}
1010
1011#[derive(Clone)]
1012/// Serializer for `DenunciationIndex`
1013pub struct DenunciationIndexSerializer {
1014    u32_serializer: U32VarIntSerializer,
1015    slot_serializer: SlotSerializer,
1016    index_serializer: U32VarIntSerializer,
1017}
1018
1019impl DenunciationIndexSerializer {
1020    /// Creates a new `DenunciationIndexSerializer`
1021    pub const fn new() -> Self {
1022        Self {
1023            u32_serializer: U32VarIntSerializer::new(),
1024            slot_serializer: SlotSerializer::new(),
1025            index_serializer: U32VarIntSerializer::new(),
1026        }
1027    }
1028}
1029
1030impl Default for DenunciationIndexSerializer {
1031    fn default() -> Self {
1032        Self::new()
1033    }
1034}
1035
1036impl Serializer<DenunciationIndex> for DenunciationIndexSerializer {
1037    fn serialize(
1038        &self,
1039        value: &DenunciationIndex,
1040        buffer: &mut Vec<u8>,
1041    ) -> Result<(), SerializeError> {
1042        match value {
1043            DenunciationIndex::BlockHeader { slot } => {
1044                self.u32_serializer
1045                    .serialize(&u32::from(DenunciationIndexTypeId::BlockHeader), buffer)?;
1046                self.slot_serializer.serialize(slot, buffer)?;
1047            }
1048            DenunciationIndex::Endorsement { slot, index } => {
1049                self.u32_serializer
1050                    .serialize(&u32::from(DenunciationIndexTypeId::Endorsement), buffer)?;
1051                self.slot_serializer.serialize(slot, buffer)?;
1052                self.index_serializer.serialize(index, buffer)?;
1053            }
1054        }
1055        Ok(())
1056    }
1057}
1058
1059#[derive(Clone)]
1060/// Deserializer for `DenunciationIndex`
1061pub struct DenunciationIndexDeserializer {
1062    id_deserializer: U32VarIntDeserializer,
1063    slot_deserializer: SlotDeserializer,
1064    index_deserializer: U32VarIntDeserializer,
1065}
1066
1067impl DenunciationIndexDeserializer {
1068    /// Creates a new `DenunciationIndexDeserializer`
1069    pub const fn new(thread_count: u8, endorsement_count: u32) -> Self {
1070        Self {
1071            id_deserializer: U32VarIntDeserializer::new(Included(0), Included(u32::MAX)),
1072            slot_deserializer: SlotDeserializer::new(
1073                (Included(0), Included(u64::MAX)),
1074                (Included(0), Excluded(thread_count)),
1075            ),
1076            index_deserializer: U32VarIntDeserializer::new(
1077                Included(0),
1078                Included(endorsement_count),
1079            ),
1080        }
1081    }
1082}
1083
1084impl Deserializer<DenunciationIndex> for DenunciationIndexDeserializer {
1085    fn deserialize<'a, E: ParseError<&'a [u8]> + ContextError<&'a [u8]>>(
1086        &self,
1087        buffer: &'a [u8],
1088    ) -> IResult<&'a [u8], DenunciationIndex, E> {
1089        let (input, id) = self.id_deserializer.deserialize(buffer)?;
1090        let id = DenunciationIndexTypeId::try_from(id).map_err(|_| {
1091            nom::Err::Error(ParseError::from_error_kind(
1092                buffer,
1093                nom::error::ErrorKind::Eof,
1094            ))
1095        })?;
1096
1097        match id {
1098            DenunciationIndexTypeId::BlockHeader => {
1099                context("Failed slot deserialization", |input| {
1100                    self.slot_deserializer.deserialize(input)
1101                })
1102                .map(|slot| DenunciationIndex::BlockHeader { slot })
1103                // .parse(&buffer[1..])
1104                .parse(input)
1105            }
1106            DenunciationIndexTypeId::Endorsement => context(
1107                "Failed Endorsement denunciation index",
1108                tuple((
1109                    context("Failed slot deserialization", |input| {
1110                        self.slot_deserializer.deserialize(input)
1111                    }),
1112                    context("Failed index deserialization", |input| {
1113                        self.index_deserializer.deserialize(input)
1114                    }),
1115                )),
1116            )
1117            .map(|(slot, index)| DenunciationIndex::Endorsement { slot, index })
1118            .parse(input),
1119        }
1120    }
1121}
1122
1123// End Denunciation Index ser der
1124
1125// Denunciation interest
1126
1127/// DenunciationPrecursor variant for endorsement
1128#[derive(Debug, Clone, PartialEq)]
1129pub struct EndorsementDenunciationPrecursor {
1130    /// secure share endorsement public key
1131    pub public_key: PublicKey,
1132    /// endorsement slot
1133    pub slot: Slot,
1134    /// endorsement index
1135    pub index: u32,
1136    /// secured header partial hash
1137    hash: Hash,
1138    /// secured header signature
1139    signature: Signature,
1140}
1141
1142/// DenunciationPrecursor variant for block header
1143#[derive(Debug, Clone, PartialEq)]
1144pub struct BlockHeaderDenunciationPrecursor {
1145    /// secured header public key
1146    pub public_key: PublicKey,
1147    /// block header slot
1148    pub slot: Slot,
1149    /// secured header partial hash
1150    hash: Hash,
1151    /// secured header signature
1152    signature: Signature,
1153}
1154
1155/// Lightweight data for Denunciation creation
1156/// (avoid storing heavyweight secured header or secure share endorsement, see denunciation pool)
1157#[derive(Debug, Clone, PartialEq)]
1158pub enum DenunciationPrecursor {
1159    /// Endorsement variant
1160    Endorsement(EndorsementDenunciationPrecursor),
1161    /// Block header variant
1162    BlockHeader(BlockHeaderDenunciationPrecursor),
1163}
1164
1165impl DenunciationPrecursor {
1166    /// Get field: slot
1167    pub fn get_slot(&self) -> &Slot {
1168        match self {
1169            DenunciationPrecursor::Endorsement(endo_de_p) => &endo_de_p.slot,
1170            DenunciationPrecursor::BlockHeader(blkh_de_p) => &blkh_de_p.slot,
1171        }
1172    }
1173
1174    /// Get field: pub key
1175    pub fn get_public_key(&self) -> &PublicKey {
1176        match self {
1177            DenunciationPrecursor::Endorsement(endo_de_p) => &endo_de_p.public_key,
1178            DenunciationPrecursor::BlockHeader(blkh_de_p) => &blkh_de_p.public_key,
1179        }
1180    }
1181}
1182
1183impl From<&SecureShareEndorsement> for DenunciationPrecursor {
1184    fn from(value: &SecureShareEndorsement) -> Self {
1185        DenunciationPrecursor::Endorsement(EndorsementDenunciationPrecursor {
1186            public_key: value.content_creator_pub_key,
1187            slot: value.content.slot,
1188            index: value.content.index,
1189            hash: *value.id.get_hash(),
1190            signature: value.signature,
1191        })
1192    }
1193}
1194
1195impl From<&SecuredHeader> for DenunciationPrecursor {
1196    fn from(value: &SecuredHeader) -> Self {
1197        DenunciationPrecursor::BlockHeader(BlockHeaderDenunciationPrecursor {
1198            public_key: value.content_creator_pub_key,
1199            slot: value.content.slot,
1200            hash: *value.id.get_hash(),
1201            signature: value.signature,
1202        })
1203    }
1204}
1205
1206/// Create a new Denunciation from 2 DenunciationPrecursor (legacy, chain-agnostic
1207/// signed-hash layout). Kept for backward compatibility. Production code should use
1208/// the chain-aware tuple variant.
1209impl TryFrom<(&DenunciationPrecursor, &DenunciationPrecursor)> for Denunciation {
1210    type Error = DenunciationError;
1211
1212    fn try_from(
1213        (de_p_1, de_p_2): (&DenunciationPrecursor, &DenunciationPrecursor),
1214    ) -> Result<Self, Self::Error> {
1215        Denunciation::try_from((de_p_1, de_p_2, None))
1216    }
1217}
1218
1219/// Create a new Denunciation from 2 DenunciationPrecursor using the chain-scoped
1220/// signed-hash layout selected by `sig_chain_id` (`None` = legacy, `Some(chain_id)`
1221/// = chain-scoped).
1222impl TryFrom<(&DenunciationPrecursor, &DenunciationPrecursor, Option<u64>)> for Denunciation {
1223    type Error = DenunciationError;
1224
1225    fn try_from(
1226        (de_p_1, de_p_2, sig_chain_id): (
1227            &DenunciationPrecursor,
1228            &DenunciationPrecursor,
1229            Option<u64>,
1230        ),
1231    ) -> Result<Self, Self::Error> {
1232        match (de_p_1, de_p_2) {
1233            (
1234                DenunciationPrecursor::BlockHeader(de_p_blkh_1),
1235                DenunciationPrecursor::BlockHeader(de_p_blkh_2),
1236            ) => {
1237                // Cannot use the same block header (here: block header denunciation precursor) twice
1238                if de_p_blkh_1.slot != de_p_blkh_2.slot
1239                    || de_p_blkh_1.public_key != de_p_blkh_2.public_key
1240                    || de_p_blkh_1.hash == de_p_blkh_2.hash
1241                {
1242                    return Err(DenunciationError::InvalidInput(
1243                        format!("Not the same slot or public key or same hash for de precursor: {:?} & {:?}", de_p_blkh_1, de_p_blkh_2)
1244                    ));
1245                }
1246
1247                // Check sig
1248                let de_p_blkh_1_hash = BlockHeaderDenunciation::compute_hash_for_sig_verif(
1249                    &de_p_blkh_1.public_key,
1250                    &de_p_blkh_1.slot,
1251                    &de_p_blkh_1.hash,
1252                    sig_chain_id,
1253                );
1254                let de_p_blkh_2_hash = BlockHeaderDenunciation::compute_hash_for_sig_verif(
1255                    &de_p_blkh_2.public_key,
1256                    &de_p_blkh_2.slot,
1257                    &de_p_blkh_2.hash,
1258                    sig_chain_id,
1259                );
1260
1261                de_p_blkh_1
1262                    .public_key
1263                    .verify_signature(&de_p_blkh_1_hash, &de_p_blkh_1.signature)?;
1264                de_p_blkh_1
1265                    .public_key
1266                    .verify_signature(&de_p_blkh_2_hash, &de_p_blkh_2.signature)?;
1267
1268                Ok(Denunciation::BlockHeader(BlockHeaderDenunciation {
1269                    public_key: de_p_blkh_1.public_key,
1270                    slot: de_p_blkh_1.slot,
1271                    signature_1: de_p_blkh_1.signature,
1272                    signature_2: de_p_blkh_2.signature,
1273                    hash_1: de_p_blkh_1.hash,
1274                    hash_2: de_p_blkh_2.hash,
1275                }))
1276            }
1277            (
1278                DenunciationPrecursor::Endorsement(de_p_endo_1),
1279                DenunciationPrecursor::Endorsement(de_p_endo_2),
1280            ) => {
1281                // Cannot use the same endorsement (here: endorsement denunciation) twice
1282                if de_p_endo_1.slot != de_p_endo_2.slot
1283                    || de_p_endo_1.index != de_p_endo_2.index
1284                    || de_p_endo_1.public_key != de_p_endo_2.public_key
1285                    || de_p_endo_1.hash == de_p_endo_2.hash
1286                {
1287                    return Err(DenunciationError::InvalidInput(
1288                        format!("Not the same slot, index or public key or same hash for de precursor: {:?} & {:?}", de_p_endo_1, de_p_endo_2)
1289                    ));
1290                }
1291
1292                // Check sig
1293                let de_p_endo_1_hash = EndorsementDenunciation::compute_hash_for_sig_verif(
1294                    &de_p_endo_1.public_key,
1295                    &de_p_endo_1.slot,
1296                    &de_p_endo_1.index,
1297                    &de_p_endo_1.hash,
1298                    sig_chain_id,
1299                );
1300                let de_p_endo_2_hash = EndorsementDenunciation::compute_hash_for_sig_verif(
1301                    &de_p_endo_2.public_key,
1302                    &de_p_endo_2.slot,
1303                    &de_p_endo_2.index,
1304                    &de_p_endo_2.hash,
1305                    sig_chain_id,
1306                );
1307
1308                de_p_endo_1
1309                    .public_key
1310                    .verify_signature(&de_p_endo_1_hash, &de_p_endo_1.signature)?;
1311                de_p_endo_1
1312                    .public_key
1313                    .verify_signature(&de_p_endo_2_hash, &de_p_endo_2.signature)?;
1314
1315                Ok(Denunciation::Endorsement(EndorsementDenunciation {
1316                    public_key: de_p_endo_1.public_key,
1317                    slot: de_p_endo_1.slot,
1318                    index: de_p_endo_1.index,
1319                    signature_1: de_p_endo_1.signature,
1320                    signature_2: de_p_endo_2.signature,
1321                    hash_1: de_p_endo_1.hash,
1322                    hash_2: de_p_endo_2.hash,
1323                }))
1324            }
1325            _ => {
1326                // Different enum variants - this is invalid
1327                Err(DenunciationError::InvalidInput(
1328                    "Mixed endorsement and block header denunciation precursors".to_string(),
1329                ))
1330            }
1331        }
1332    }
1333}
1334
1335// End Denunciation interest
1336
1337// test-exports
1338
1339#[cfg(any(test, feature = "test-exports"))]
1340impl Denunciation {
1341    /// Used under testing conditions to validate an instance of Self
1342    pub fn check_invariants(&self) -> Result<(), Box<dyn std::error::Error>> {
1343        if !self.is_valid_with_chain(Some(*crate::config::CHAINID)) {
1344            return Err(format!("Denunciation is invalid: {:?}", self).into());
1345        }
1346        Ok(())
1347    }
1348}
1349
1350// end test-exports
1351
1352#[cfg(test)]
1353mod tests {
1354    use super::*;
1355
1356    use massa_hash::Hash;
1357    use massa_serialization::DeserializeError;
1358    use massa_signature::KeyPair;
1359
1360    use crate::block_id::BlockId;
1361    use crate::config::{CHAINID, ENDORSEMENT_COUNT, THREAD_COUNT};
1362    use crate::endorsement::{Endorsement, EndorsementSerializer, SecureShareEndorsement};
1363    use crate::secure_share::{Id, SecureShareContent};
1364
1365    use crate::test_exports::{
1366        gen_block_headers_for_denunciation, gen_endorsements_for_denunciation,
1367    };
1368
1369    #[test]
1370    fn test_endorsement_denunciation() {
1371        // Create an endorsement denunciation and check if it is valid
1372        let (_slot, _keypair, s_endorsement_1, s_endorsement_2, _s_endorsement_3) =
1373            gen_endorsements_for_denunciation(None, None);
1374        let denunciation: Denunciation = (&s_endorsement_1, &s_endorsement_2, Some(*CHAINID))
1375            .try_into()
1376            .unwrap();
1377
1378        assert!(denunciation.is_for_endorsement());
1379        assert!(denunciation.is_valid_with_chain(Some(*CHAINID)));
1380    }
1381
1382    #[test]
1383    fn test_endorsement_denunciation_invalid_1() {
1384        let (slot, keypair, s_endorsement_1, _s_endorsement_2, _s_endorsement_3) =
1385            gen_endorsements_for_denunciation(None, None);
1386
1387        // Try to create a denunciation from 2 endorsements @ != index
1388        let endorsement_4 = Endorsement {
1389            slot,
1390            index: 9,
1391            endorsed_block: BlockId::generate_from_hash(Hash::compute_from("foo".as_bytes())),
1392        };
1393        let s_endorsement_4 = Endorsement::new_verifiable(
1394            endorsement_4,
1395            EndorsementSerializer::new(),
1396            &keypair,
1397            *CHAINID,
1398            Some(*CHAINID),
1399        )
1400        .unwrap();
1401
1402        let denunciation =
1403            Denunciation::try_from((&s_endorsement_1, &s_endorsement_4, Some(*CHAINID)));
1404
1405        assert!(matches!(
1406            denunciation,
1407            Err(DenunciationError::InvalidInput(..))
1408        ));
1409
1410        // Try to create a denunciation from only 1 endorsement
1411        let denunciation =
1412            Denunciation::try_from((&s_endorsement_1, &s_endorsement_1, Some(*CHAINID)));
1413
1414        assert!(matches!(
1415            denunciation,
1416            Err(DenunciationError::InvalidInput(..))
1417        ));
1418    }
1419
1420    #[test]
1421    fn test_endorsement_denunciation_is_for() {
1422        let (slot, keypair, s_endorsement_1, s_endorsement_2, s_endorsement_3) =
1423            gen_endorsements_for_denunciation(None, None);
1424
1425        let denunciation: Denunciation = (&s_endorsement_1, &s_endorsement_2, Some(*CHAINID))
1426            .try_into()
1427            .unwrap();
1428
1429        assert!(denunciation.is_for_endorsement());
1430        assert!(denunciation.is_valid_with_chain(Some(*CHAINID)));
1431
1432        // Try to create a denunciation from 2 endorsements @ != index
1433        let endorsement_4 = Endorsement {
1434            slot,
1435            index: 9,
1436            endorsed_block: BlockId::generate_from_hash(Hash::compute_from("foo".as_bytes())),
1437        };
1438        let s_endorsement_4 = Endorsement::new_verifiable(
1439            endorsement_4,
1440            EndorsementSerializer::new(),
1441            &keypair,
1442            *CHAINID,
1443            Some(*CHAINID),
1444        )
1445        .unwrap();
1446
1447        assert!(!denunciation
1448            .is_also_for_endorsement(&s_endorsement_4, Some(*CHAINID))
1449            .unwrap());
1450        assert!(denunciation
1451            .is_also_for_endorsement(&s_endorsement_3, Some(*CHAINID))
1452            .unwrap());
1453        assert!(denunciation.is_valid_with_chain(Some(*CHAINID)));
1454    }
1455
1456    #[test]
1457    fn test_block_header_denunciation() {
1458        // Create an block header denunciation and check if it is valid
1459        let (_slot, _keypair, s_block_header_1, s_block_header_2, s_block_header_3) =
1460            gen_block_headers_for_denunciation(None, None);
1461        let denunciation: Denunciation = (&s_block_header_1, &s_block_header_2, Some(*CHAINID))
1462            .try_into()
1463            .unwrap();
1464
1465        assert!(denunciation.is_for_block_header());
1466        assert!(denunciation.is_valid_with_chain(Some(*CHAINID)));
1467        assert!(denunciation
1468            .is_also_for_block_header(&s_block_header_3, Some(*CHAINID))
1469            .unwrap());
1470    }
1471
1472    #[test]
1473    fn test_forge_invalid_denunciation() {
1474        let keypair = KeyPair::generate(0).unwrap();
1475        let slot_1 = Slot::new(4, 2);
1476        let slot_2 = Slot::new(3, 7);
1477
1478        let endorsement_1 = Endorsement {
1479            slot: slot_1,
1480            index: 0,
1481            endorsed_block: BlockId::generate_from_hash(Hash::compute_from("blk1".as_bytes())),
1482        };
1483
1484        let s_endorsement_1: SecureShareEndorsement = Endorsement::new_verifiable(
1485            endorsement_1,
1486            EndorsementSerializer::new(),
1487            &keypair,
1488            *CHAINID,
1489            Some(*CHAINID),
1490        )
1491        .unwrap();
1492
1493        let endorsement_2 = Endorsement {
1494            slot: slot_2,
1495            index: 0,
1496            endorsed_block: BlockId::generate_from_hash(Hash::compute_from("blk2".as_bytes())),
1497        };
1498
1499        let s_endorsement_2: SecureShareEndorsement = Endorsement::new_verifiable(
1500            endorsement_2,
1501            EndorsementSerializer::new(),
1502            &keypair,
1503            *CHAINID,
1504            Some(*CHAINID),
1505        )
1506        .unwrap();
1507
1508        // from an attacker - building manually a Denunciation object
1509        let de_forged_1 = Denunciation::Endorsement(EndorsementDenunciation {
1510            public_key: keypair.get_public_key(),
1511            slot: slot_1,
1512            index: 0,
1513            hash_1: *s_endorsement_1.id.get_hash(), // use only data from s_endorsement_1
1514            hash_2: *s_endorsement_1.id.get_hash(),
1515            signature_1: s_endorsement_1.signature,
1516            signature_2: s_endorsement_1.signature,
1517        });
1518
1519        // hash_1 == hash_2 -> this is invalid
1520        assert!(!de_forged_1.is_valid_with_chain(Some(*CHAINID)));
1521
1522        // from an attacker - building manually a Denunciation object
1523        let de_forged_2 = Denunciation::Endorsement(EndorsementDenunciation {
1524            public_key: keypair.get_public_key(),
1525            slot: slot_2,
1526            index: 0,
1527            hash_1: *s_endorsement_1.id.get_hash(),
1528            hash_2: *s_endorsement_2.id.get_hash(),
1529            signature_1: s_endorsement_1.signature,
1530            signature_2: s_endorsement_2.signature,
1531        });
1532
1533        // An attacker uses an old s_endorsement_1 to forge a Denunciation object @ slot_2
1534        // This has to be detected if Denunciation are send via the network
1535        assert!(!de_forged_2.is_valid_with_chain(Some(*CHAINID)));
1536    }
1537
1538    // SER / DER
1539    #[test]
1540    fn test_endorsement_denunciation_ser_der() {
1541        let (_, _, s_endorsement_1, s_endorsement_2, _) =
1542            gen_endorsements_for_denunciation(None, None);
1543
1544        let denunciation =
1545            Denunciation::try_from((&s_endorsement_1, &s_endorsement_2, Some(*CHAINID))).unwrap();
1546
1547        let mut buffer = Vec::new();
1548        let de_ser = EndorsementDenunciationSerializer::new();
1549
1550        match denunciation {
1551            Denunciation::Endorsement(de) => {
1552                de_ser.serialize(&de, &mut buffer).unwrap();
1553                let de_der =
1554                    EndorsementDenunciationDeserializer::new(THREAD_COUNT, ENDORSEMENT_COUNT);
1555
1556                let (rem, de_der_res) = de_der.deserialize::<DeserializeError>(&buffer).unwrap();
1557
1558                assert!(rem.is_empty());
1559                assert_eq!(de, de_der_res);
1560            }
1561            Denunciation::BlockHeader(_) => {
1562                unimplemented!()
1563            }
1564        }
1565    }
1566
1567    #[test]
1568    fn test_block_header_denunciation_ser_der() {
1569        let (_, _, s_block_header_1, s_block_header_2, _) =
1570            gen_block_headers_for_denunciation(None, None);
1571        let denunciation: Denunciation = (&s_block_header_1, &s_block_header_2, Some(*CHAINID))
1572            .try_into()
1573            .unwrap();
1574
1575        let mut buffer = Vec::new();
1576        let de_ser = BlockHeaderDenunciationSerializer::new();
1577
1578        match denunciation {
1579            Denunciation::Endorsement(_) => {
1580                unimplemented!()
1581            }
1582            Denunciation::BlockHeader(de) => {
1583                de_ser.serialize(&de, &mut buffer).unwrap();
1584                let de_der = BlockHeaderDenunciationDeserializer::new(THREAD_COUNT);
1585
1586                let (rem, de_der_res) = de_der.deserialize::<DeserializeError>(&buffer).unwrap();
1587
1588                assert!(rem.is_empty());
1589                assert_eq!(de, de_der_res);
1590            }
1591        }
1592    }
1593
1594    #[test]
1595    fn test_denunciation_ser_der() {
1596        let (_, _, s_block_header_1, s_block_header_2, _) =
1597            gen_block_headers_for_denunciation(None, None);
1598        let denunciation: Denunciation = (&s_block_header_1, &s_block_header_2, Some(*CHAINID))
1599            .try_into()
1600            .unwrap();
1601
1602        let mut buffer = Vec::new();
1603        let de_ser = DenunciationSerializer::new();
1604
1605        de_ser.serialize(&denunciation, &mut buffer).unwrap();
1606        let de_der = DenunciationDeserializer::new(THREAD_COUNT, ENDORSEMENT_COUNT);
1607
1608        let (rem, de_der_res) = de_der.deserialize::<DeserializeError>(&buffer).unwrap();
1609
1610        assert!(rem.is_empty());
1611        assert_eq!(denunciation, de_der_res);
1612
1613        let (_, _, s_endorsement_1, s_endorsement_2, _) =
1614            gen_endorsements_for_denunciation(None, None);
1615        let denunciation =
1616            Denunciation::try_from((&s_endorsement_1, &s_endorsement_2, Some(*CHAINID))).unwrap();
1617        buffer.clear();
1618
1619        de_ser.serialize(&denunciation, &mut buffer).unwrap();
1620        let (rem, de_der_res) = de_der.deserialize::<DeserializeError>(&buffer).unwrap();
1621        assert!(rem.is_empty());
1622        assert_eq!(denunciation, de_der_res);
1623    }
1624
1625    #[test]
1626    fn test_denunciation_precursor() {
1627        let (_, _, s_block_header_1, s_block_header_2, _) =
1628            gen_block_headers_for_denunciation(None, None);
1629        let denunciation: Denunciation = (&s_block_header_1, &s_block_header_2, Some(*CHAINID))
1630            .try_into()
1631            .unwrap();
1632
1633        let de_p_1 = DenunciationPrecursor::from(&s_block_header_1);
1634        let de_p_2 = DenunciationPrecursor::from(&s_block_header_2);
1635        let denunciation_2: Denunciation = (&de_p_1, &de_p_2, Some(*CHAINID)).try_into().unwrap();
1636
1637        assert_eq!(denunciation, denunciation_2);
1638
1639        let (_, _, s_endorsement_1, s_endorsement_2, _) =
1640            gen_endorsements_for_denunciation(None, None);
1641        let denunciation_3 =
1642            Denunciation::try_from((&s_endorsement_1, &s_endorsement_2, Some(*CHAINID))).unwrap();
1643
1644        let de_p_3 = DenunciationPrecursor::from(&s_endorsement_1);
1645        let de_p_4 = DenunciationPrecursor::from(&s_endorsement_2);
1646        let denunciation_4: Denunciation = (&de_p_3, &de_p_4, Some(*CHAINID)).try_into().unwrap();
1647
1648        assert_eq!(denunciation_3, denunciation_4);
1649    }
1650
1651    #[test]
1652    fn test_denunciation_index_ser_der() {
1653        let (_, _, s_block_header_1, s_block_header_2, _) =
1654            gen_block_headers_for_denunciation(None, None);
1655        let denunciation_1: Denunciation = (&s_block_header_1, &s_block_header_2, Some(*CHAINID))
1656            .try_into()
1657            .unwrap();
1658        let denunciation_index_1 = DenunciationIndex::from(&denunciation_1);
1659
1660        let (_, _, s_endorsement_1, s_endorsement_2, _) =
1661            gen_endorsements_for_denunciation(None, None);
1662        let denunciation_2 =
1663            Denunciation::try_from((&s_endorsement_1, &s_endorsement_2, Some(*CHAINID))).unwrap();
1664        let denunciation_index_2 = DenunciationIndex::from(&denunciation_2);
1665
1666        let mut buffer = Vec::new();
1667        let de_idx_ser = DenunciationIndexSerializer::new();
1668        de_idx_ser
1669            .serialize(&denunciation_index_1, &mut buffer)
1670            .unwrap();
1671        let de_idx_der = DenunciationIndexDeserializer::new(THREAD_COUNT, ENDORSEMENT_COUNT);
1672        let (rem, de_idx_der_res) = de_idx_der.deserialize::<DeserializeError>(&buffer).unwrap();
1673
1674        assert!(rem.is_empty());
1675        assert_eq!(denunciation_index_1, de_idx_der_res);
1676
1677        let mut buffer = Vec::new();
1678        let de_idx_ser = DenunciationIndexSerializer::new();
1679        de_idx_ser
1680            .serialize(&denunciation_index_2, &mut buffer)
1681            .unwrap();
1682        let de_idx_der = DenunciationIndexDeserializer::new(THREAD_COUNT, ENDORSEMENT_COUNT);
1683        let (rem, de_idx_der_res) = de_idx_der.deserialize::<DeserializeError>(&buffer).unwrap();
1684
1685        assert!(rem.is_empty());
1686        assert_eq!(denunciation_index_2, de_idx_der_res);
1687    }
1688
1689    /// F90 / PDF #11: cross-chain replay of endorsements as denunciations.
1690    ///
1691    /// Validators commonly run the same key on several chains (e.g. MAINNET + LABNET).
1692    /// Pre-fix, the endorsement signed hash was chain-agnostic, so two legitimate
1693    /// same-(slot, index) endorsements from two different chains could be combined
1694    /// into a denunciation on either chain and slash the validator. Folding
1695    /// `chain_id` into the signed hash (gated on MIP-0002 `Execution` v2) makes the
1696    /// cross-chain combination fail to verify, while same-chain denunciations and
1697    /// legacy (pre-activation) denunciations still work.
1698    fn make_endorsement(
1699        keypair: &KeyPair,
1700        slot: Slot,
1701        endorsed_block: &str,
1702        chain_id: u64,
1703        sig_chain_id: Option<u64>,
1704    ) -> SecureShareEndorsement {
1705        Endorsement::new_verifiable(
1706            Endorsement {
1707                slot,
1708                index: 0,
1709                endorsed_block: BlockId::generate_from_hash(Hash::compute_from(
1710                    endorsed_block.as_bytes(),
1711                )),
1712            },
1713            EndorsementSerializer::new(),
1714            keypair,
1715            chain_id,
1716            sig_chain_id,
1717        )
1718        .unwrap()
1719    }
1720
1721    #[test]
1722    fn test_endorsement_denunciation_cross_chain_rejected() {
1723        let keypair = KeyPair::generate(0).unwrap();
1724        let slot = Slot::new(3, 7);
1725        let chain_a: u64 = 77658377; // MAINNET
1726        let chain_b: u64 = 77658376; // LABNET
1727
1728        // Two same-chain endorsements (signed with chain_a, chain-scoped layout).
1729        let e_a1 = make_endorsement(&keypair, slot, "blk_a1", chain_a, Some(chain_a));
1730        let e_a2 = make_endorsement(&keypair, slot, "blk_a2", chain_a, Some(chain_a));
1731
1732        // Same-chain denunciation still verifies with the chain-scoped layout.
1733        let de_same =
1734            Denunciation::try_from((&e_a1, &e_a2, Some(chain_a))).expect("same-chain denunciation");
1735        assert!(de_same.is_valid_with_chain(Some(chain_a)));
1736
1737        // A second endorsement produced by the same key on a different chain.
1738        let e_b2 = make_endorsement(&keypair, slot, "blk_b2", chain_b, Some(chain_b));
1739
1740        // Cross-chain combination no longer verifies on chain A: the second
1741        // signature was produced over a hash folding in chain_b, not chain_a.
1742        let de_cross = Denunciation::try_from((&e_a1, &e_b2, Some(chain_a)));
1743        assert!(de_cross.is_err(), "cross-chain denunciation must not build");
1744        // And it does not verify either.
1745        assert!(!Denunciation::try_from((&e_a1, &e_b2, None))
1746            .map(|d| d.is_valid_with_chain(Some(chain_a)))
1747            .unwrap_or(false));
1748    }
1749
1750    /// Migration / no-partition check across the activation boundary.
1751    ///
1752    /// Pre-activation endorsements are signed with the legacy (chain-agnostic)
1753    /// layout (`sig_chain_id = None`); post-activation with the chain-scoped layout
1754    /// (`sig_chain_id = Some(chain_id)`). A legacy denunciation must keep verifying
1755    /// with the legacy layout, and a chain-scoped denunciation must verify with the
1756    /// chain-scoped layout — so nodes flip layouts together at the deterministic
1757    /// activation slot without partitioning on signature validity.
1758    #[test]
1759    fn test_endorsement_denunciation_migration_no_partition() {
1760        let keypair = KeyPair::generate(0).unwrap();
1761        let slot = Slot::new(3, 7);
1762        let chain: u64 = *CHAINID;
1763
1764        // Legacy (pre-activation) endorsements and denunciation.
1765        let e_l1 = make_endorsement(&keypair, slot, "blk_l1", chain, None);
1766        let e_l2 = make_endorsement(&keypair, slot, "blk_l2", chain, None);
1767        let de_legacy = Denunciation::try_from((&e_l1, &e_l2, None)).expect("legacy denunciation");
1768        assert!(de_legacy.is_valid_with_chain(None));
1769        // A legacy denunciation does NOT verify under the chain-scoped layout: the
1770        // signatures were produced over a chain-agnostic hash. This is expected and
1771        // is why the layout is gated on the denounced slot's network version.
1772        assert!(!de_legacy.is_valid_with_chain(Some(chain)));
1773
1774        // Post-activation endorsements and denunciation.
1775        let e_n1 = make_endorsement(&keypair, slot, "blk_n1", chain, Some(chain));
1776        let e_n2 = make_endorsement(&keypair, slot, "blk_n2", chain, Some(chain));
1777        let de_scoped =
1778            Denunciation::try_from((&e_n1, &e_n2, Some(chain))).expect("scoped denunciation");
1779        assert!(de_scoped.is_valid_with_chain(Some(chain)));
1780        assert!(!de_scoped.is_valid_with_chain(None));
1781    }
1782
1783    fn make_header(
1784        keypair: &KeyPair,
1785        slot: Slot,
1786        merkle: &str,
1787        chain_id: u64,
1788        sig_chain_id: Option<u64>,
1789    ) -> crate::block_header::SecuredHeader {
1790        crate::block_header::BlockHeader::new_verifiable(
1791            crate::block_header::BlockHeader {
1792                current_version: 0,
1793                announced_version: None,
1794                slot,
1795                parents: Vec::new(),
1796                operation_merkle_root: Hash::compute_from(merkle.as_bytes()),
1797                endorsements: Vec::new(),
1798                denunciations: Vec::new(),
1799            },
1800            crate::block_header::BlockHeaderSerializer::new(),
1801            keypair,
1802            chain_id,
1803            sig_chain_id,
1804        )
1805        .unwrap()
1806    }
1807
1808    #[test]
1809    fn test_block_header_denunciation_cross_chain_rejected() {
1810        let keypair = KeyPair::generate(0).unwrap();
1811        let slot = Slot::new(3, 7);
1812        let chain_a: u64 = 77658377;
1813        let chain_b: u64 = 77658376;
1814
1815        let h_a1 = make_header(&keypair, slot, "hdr_a1", chain_a, Some(chain_a));
1816        let h_a2 = make_header(&keypair, slot, "hdr_a2", chain_a, Some(chain_a));
1817        let de_same =
1818            Denunciation::try_from((&h_a1, &h_a2, Some(chain_a))).expect("same-chain header de");
1819        assert!(de_same.is_valid_with_chain(Some(chain_a)));
1820
1821        let h_b2 = make_header(&keypair, slot, "hdr_b2", chain_b, Some(chain_b));
1822        assert!(
1823            Denunciation::try_from((&h_a1, &h_b2, Some(chain_a))).is_err(),
1824            "cross-chain header denunciation must not build"
1825        );
1826    }
1827}