Skip to main content

iota_types/
messages_consensus.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{
6    collections::hash_map::DefaultHasher,
7    fmt::{Debug, Formatter},
8    hash::{Hash, Hasher},
9    sync::Arc,
10    time::{SystemTime, UNIX_EPOCH},
11};
12
13use byteorder::{BigEndian, ReadBytesExt};
14use fastcrypto::{error::FastCryptoResult, groups::bls12381, hash::HashFunction};
15use fastcrypto_tbls::dkg_v1;
16use iota_sdk_types::crypto::IntentScope;
17use once_cell::sync::OnceCell;
18use serde::{Deserialize, Serialize};
19use tracing::warn;
20
21use crate::{
22    base_types::{AuthorityName, ConciseableName, ObjectRef, TransactionDigest},
23    crypto::{AuthoritySignature, DefaultHash, default_hash},
24    digests::{Digest, MisbehaviorReportDigest},
25    message_envelope::{Envelope, Message, VerifiedEnvelope},
26    messages_checkpoint::{CheckpointSequenceNumber, CheckpointSignatureMessage},
27    supported_protocol_versions::{
28        Chain, SupportedProtocolVersions, SupportedProtocolVersionsWithHashes,
29    },
30    transaction::CertifiedTransaction,
31};
32
33#[derive(Serialize, Deserialize, Clone, Debug)]
34pub struct ConsensusTransaction {
35    /// Encodes an u64 unique tracking id to allow us trace a message between
36    /// IOTA and consensus. Use an byte array instead of u64 to ensure stable
37    /// serialization.
38    pub tracking_id: [u8; 8],
39    pub kind: ConsensusTransactionKind,
40}
41
42#[derive(Serialize, Deserialize, Clone, Hash, PartialEq, Eq, Ord, PartialOrd)]
43pub enum ConsensusTransactionKey {
44    Certificate(TransactionDigest),
45    CheckpointSignature(AuthorityName, CheckpointSequenceNumber),
46    EndOfPublish(AuthorityName),
47    CapabilityNotification(AuthorityName, u64 /* generation */),
48    #[deprecated(note = "Authenticator state (JWK) is deprecated and was never enabled on IOTA")]
49    NewJWKFetchedDeprecated,
50    RandomnessDkgMessage(AuthorityName),
51    RandomnessDkgConfirmation(AuthorityName),
52    MisbehaviorReport(
53        AuthorityName,
54        MisbehaviorReportDigest,
55        CheckpointSequenceNumber,
56    ),
57    // New entries should be added at the end to preserve serialization compatibility. DO NOT
58    // CHANGE THE ORDER OF EXISTING ENTRIES!
59}
60
61impl Debug for ConsensusTransactionKey {
62    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
63        match self {
64            Self::Certificate(digest) => write!(f, "Certificate({digest})"),
65            Self::CheckpointSignature(name, seq) => {
66                write!(f, "CheckpointSignature({:?}, {:?})", name.concise(), seq)
67            }
68            Self::EndOfPublish(name) => write!(f, "EndOfPublish({:?})", name.concise()),
69            Self::CapabilityNotification(name, generation) => write!(
70                f,
71                "CapabilityNotification({:?}, {:?})",
72                name.concise(),
73                generation
74            ),
75            #[allow(deprecated)]
76            Self::NewJWKFetchedDeprecated => {
77                write!(
78                    f,
79                    "NewJWKFetched(deprecated: Authenticator state (JWK) is deprecated and was never enabled on IOTA)"
80                )
81            }
82            Self::RandomnessDkgMessage(name) => {
83                write!(f, "RandomnessDkgMessage({:?})", name.concise())
84            }
85            Self::RandomnessDkgConfirmation(name) => {
86                write!(f, "RandomnessDkgConfirmation({:?})", name.concise())
87            }
88            Self::MisbehaviorReport(name, digest, checkpoint_seq) => {
89                write!(
90                    f,
91                    "MisbehaviorReport({:?}, {:?}, {:?})",
92                    name.concise(),
93                    digest,
94                    checkpoint_seq
95                )
96            }
97        }
98    }
99}
100
101pub type SignedAuthorityCapabilitiesV1 = Envelope<AuthorityCapabilitiesV1, AuthoritySignature>;
102
103pub type VerifiedAuthorityCapabilitiesV1 =
104    VerifiedEnvelope<AuthorityCapabilitiesV1, AuthoritySignature>;
105
106#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
107pub struct AuthorityCapabilitiesDigest(Digest);
108
109impl AuthorityCapabilitiesDigest {
110    pub const fn new(digest: [u8; 32]) -> Self {
111        Self(Digest::new(digest))
112    }
113}
114
115impl Debug for AuthorityCapabilitiesDigest {
116    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
117        f.debug_tuple("AuthorityCapabilitiesDigest")
118            .field(&self.0)
119            .finish()
120    }
121}
122
123/// Used to advertise capabilities of each authority via consensus. This allows
124/// validators to negotiate the creation of the ChangeEpoch transaction.
125#[derive(Serialize, Deserialize, Clone, Hash)]
126pub struct AuthorityCapabilitiesV1 {
127    /// Originating authority - must match transaction source authority from
128    /// consensus or the signature of a non-committee active validator.
129    pub authority: AuthorityName,
130    /// Generation number set by sending authority. Used to determine which of
131    /// multiple AuthorityCapabilities messages from the same authority is
132    /// the most recent.
133    ///
134    /// (Currently, we just set this to the current time in milliseconds since
135    /// the epoch, but this should not be interpreted as a timestamp.)
136    pub generation: u64,
137
138    /// ProtocolVersions that the authority supports, including the hash of the
139    /// serialized ProtocolConfig of that authority per version.
140    pub supported_protocol_versions: SupportedProtocolVersionsWithHashes,
141
142    /// The ObjectRefs of all versions of system packages that the validator
143    /// possesses. Used to determine whether to do a framework/movestdlib
144    /// upgrade.
145    pub available_system_packages: Vec<ObjectRef>,
146}
147
148impl Message for AuthorityCapabilitiesV1 {
149    type DigestType = AuthorityCapabilitiesDigest;
150    const SCOPE: IntentScope = IntentScope::AuthorityCapabilities;
151
152    fn digest(&self) -> Self::DigestType {
153        // Ensure deterministic serialization for digest
154        let mut hasher = DefaultHash::new();
155        let serialized = bcs::to_bytes(&self).expect("BCS should not fail");
156        hasher.update(&serialized);
157        AuthorityCapabilitiesDigest::new(<[u8; 32]>::from(hasher.finalize()))
158    }
159}
160
161impl Debug for AuthorityCapabilitiesV1 {
162    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
163        f.debug_struct("AuthorityCapabilities")
164            .field("authority", &self.authority.concise())
165            .field("generation", &self.generation)
166            .field(
167                "supported_protocol_versions",
168                &self.supported_protocol_versions,
169            )
170            .field("available_system_packages", &self.available_system_packages)
171            .finish()
172    }
173}
174
175impl AuthorityCapabilitiesV1 {
176    pub fn new(
177        authority: AuthorityName,
178        chain: Chain,
179        supported_protocol_versions: SupportedProtocolVersions,
180        available_system_packages: Vec<ObjectRef>,
181    ) -> Self {
182        let generation = SystemTime::now()
183            .duration_since(UNIX_EPOCH)
184            .expect("IOTA did not exist prior to 1970")
185            .as_millis()
186            .try_into()
187            .expect("This build of iota is not supported in the year 500,000,000");
188        Self {
189            authority,
190            generation,
191            supported_protocol_versions:
192                SupportedProtocolVersionsWithHashes::from_supported_versions(
193                    supported_protocol_versions,
194                    chain,
195                ),
196            available_system_packages,
197        }
198    }
199}
200
201impl SignedAuthorityCapabilitiesV1 {
202    pub fn cache_digest(&self, epoch: u64) -> AuthorityCapabilitiesDigest {
203        // Create a tuple that includes both the capabilities data and the epoch
204        let data_with_epoch = (self.data(), epoch);
205
206        // Ensure deterministic serialization for digest
207        let mut hasher = DefaultHash::new();
208        let serialized = bcs::to_bytes(&data_with_epoch).expect("BCS should not fail");
209        hasher.update(&serialized);
210        AuthorityCapabilitiesDigest::new(<[u8; 32]>::from(hasher.finalize()))
211    }
212}
213
214#[derive(Serialize, Deserialize, Clone, Debug)]
215pub enum ConsensusTransactionKind {
216    CertifiedTransaction(Box<CertifiedTransaction>),
217    CheckpointSignature(Box<CheckpointSignatureMessage>),
218    EndOfPublish(AuthorityName),
219
220    CapabilityNotificationV1(AuthorityCapabilitiesV1),
221    SignedCapabilityNotificationV1(SignedAuthorityCapabilitiesV1),
222
223    #[deprecated(note = "Authenticator state (JWK) is deprecated and was never enabled on IOTA")]
224    NewJWKFetchedDeprecated,
225
226    // DKG is used to generate keys for use in the random beacon protocol.
227    // `RandomnessDkgMessage` is sent out at start-of-epoch to initiate the process.
228    // Contents are a serialized `fastcrypto_tbls::dkg::Message`.
229    RandomnessDkgMessage(AuthorityName, Vec<u8>),
230    // `RandomnessDkgConfirmation` is the second DKG message, sent as soon as a threshold amount
231    // of `RandomnessDkgMessages` have been received locally, to complete the key generation
232    // process. Contents are a serialized `fastcrypto_tbls::dkg::Confirmation`.
233    RandomnessDkgConfirmation(AuthorityName, Vec<u8>),
234    MisbehaviorReport(VersionedMisbehaviorReport),
235    // New entries should be added at the end to preserve serialization compatibility. DO NOT
236    // CHANGE THE ORDER OF EXISTING ENTRIES!
237}
238
239impl ConsensusTransactionKind {
240    pub fn is_dkg(&self) -> bool {
241        matches!(
242            self,
243            ConsensusTransactionKind::RandomnessDkgMessage(_, _)
244                | ConsensusTransactionKind::RandomnessDkgConfirmation(_, _)
245        )
246    }
247}
248
249/// A misbehavior report carrying a versioned payload plus a memoized digest.
250///
251/// Wire format is BCS over the `Serialize`-derived fields in declaration order:
252/// `authority || payload || generation`. This exactly matches the pre-refactor
253/// `ConsensusTransactionKind::MisbehaviorReport(AuthorityName,
254/// VersionedMisbehaviorReport { payload }, CheckpointSequenceNumber)` 3-tuple
255/// — see `tests::misbehavior_report_wire_format_unchanged` which pins the
256/// equivalence. Reordering or inserting any non-`skip` field here would change
257/// the consensus wire format and halt a running testnet.
258#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct VersionedMisbehaviorReport {
260    /// Originating authority — must match the transaction source authority
261    /// from consensus. Verified at the consensus boundary.
262    pub authority: AuthorityName,
263    /// Versioned payload of the misbehavior report.
264    pub payload: MisbehaviorObservations,
265    /// Generation number set by the sending authority. Used to identify the
266    /// most recent report from each authority. Currently set to the
267    /// checkpoint sequence number at which the report was generated.
268    pub generation: u64,
269    #[serde(skip)]
270    digest: OnceCell<MisbehaviorReportDigest>,
271}
272
273/// Versioned per-authority misbehavior observations. New variants get their
274/// own named-field payload type (`MisbehaviorObservationsV2`,
275/// `MisbehaviorObservationsV3`, ...) so the wire schema stays compile-time
276/// checked. Also serves as the in-memory representation in
277/// `MisbehaviorMonitor` / `ReportAggregator`.
278#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
279pub enum MisbehaviorObservations {
280    V1(MisbehaviorObservationsV1),
281}
282
283impl VersionedMisbehaviorReport {
284    pub fn new_v1(
285        authority: AuthorityName,
286        generation: u64,
287        observations: MisbehaviorObservationsV1,
288    ) -> Self {
289        Self {
290            authority,
291            payload: MisbehaviorObservations::V1(observations),
292            generation,
293            digest: OnceCell::new(),
294        }
295    }
296
297    /// Returns the digest of the misbehavior report, caching it if it has not
298    /// been computed yet.
299    pub fn digest(&self) -> &MisbehaviorReportDigest {
300        self.digest
301            .get_or_init(|| MisbehaviorReportDigest::new(default_hash(self)))
302    }
303
304    /// Returns the summary of the misbehavior report, defined as the sum of all
305    /// metrics for all authorities.
306    pub fn summary(&self) -> u64 {
307        let summary = match &self.payload {
308            MisbehaviorObservations::V1(report) => [
309                &report.faulty_blocks_provable,
310                &report.faulty_blocks_unprovable,
311                &report.missing_proposals,
312                &report.equivocations,
313            ]
314            .into_iter()
315            .flatten()
316            .fold(0u64, |acc, metric| acc.saturating_add(*metric)),
317        };
318        if summary == u64::MAX {
319            warn!("MisbehaviorReport summary reached its maximum value.");
320        }
321        summary
322    }
323}
324
325/// V1 misbehavior observations: per-authority counts for each tracked
326/// misbehavior category (faulty blocks, equivocations, missing proposals).
327/// Field order is part of the wire format — BCS serializes named struct
328/// fields in declaration order. This first version does not include any
329/// type of proof.
330#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
331pub struct MisbehaviorObservationsV1 {
332    pub faulty_blocks_provable: Vec<u64>,
333    pub faulty_blocks_unprovable: Vec<u64>,
334    pub missing_proposals: Vec<u64>,
335    pub equivocations: Vec<u64>,
336}
337
338impl MisbehaviorObservationsV1 {
339    pub fn verify(&self, committee_size: usize) -> bool {
340        // This version of reports are valid as long as they contain the counts for all
341        // authorities. Future versions may contain proofs that need verification.
342        // However, since the validity of a proof is deeply coupled with the protocol
343        // version and the consensus mechanism being used, we cannot verify it here. In
344        // the future, reports should be unwrapped (or translated) to a type verifiable
345        // by the starfish crate, which means that the verification logic will probably
346        // move out of this crate.
347        if (self.faulty_blocks_provable.len() != committee_size)
348            || (self.faulty_blocks_unprovable.len() != committee_size)
349            || (self.equivocations.len() != committee_size)
350            || (self.missing_proposals.len() != committee_size)
351        {
352            return false;
353        }
354        true
355    }
356}
357
358#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
359pub enum VersionedDkgMessage {
360    V1(dkg_v1::Message<bls12381::G2Element, bls12381::G2Element>),
361}
362
363#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
364pub enum VersionedDkgConfirmation {
365    V1(dkg_v1::Confirmation<bls12381::G2Element>),
366}
367
368impl Debug for VersionedDkgMessage {
369    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
370        match self {
371            VersionedDkgMessage::V1(msg) => write!(
372                f,
373                "DKG V1 Message with sender={}, vss_pk.degree={}, encrypted_shares.len()={}",
374                msg.sender,
375                msg.vss_pk.degree(),
376                msg.encrypted_shares.len(),
377            ),
378        }
379    }
380}
381
382impl VersionedDkgMessage {
383    pub fn sender(&self) -> u16 {
384        match self {
385            VersionedDkgMessage::V1(msg) => msg.sender,
386        }
387    }
388
389    pub fn create(
390        dkg_version: u64,
391        party: Arc<dkg_v1::Party<bls12381::G2Element, bls12381::G2Element>>,
392    ) -> FastCryptoResult<VersionedDkgMessage> {
393        assert_eq!(dkg_version, 1, "BUG: invalid DKG version");
394        let msg = party.create_message(&mut rand::thread_rng())?;
395        Ok(VersionedDkgMessage::V1(msg))
396    }
397
398    pub fn unwrap_v1(self) -> dkg_v1::Message<bls12381::G2Element, bls12381::G2Element> {
399        match self {
400            VersionedDkgMessage::V1(msg) => msg,
401        }
402    }
403
404    pub fn is_valid_version(&self, dkg_version: u64) -> bool {
405        matches!((self, dkg_version), (VersionedDkgMessage::V1(_), 1))
406    }
407}
408
409impl VersionedDkgConfirmation {
410    pub fn sender(&self) -> u16 {
411        match self {
412            VersionedDkgConfirmation::V1(msg) => msg.sender,
413        }
414    }
415
416    pub fn num_of_complaints(&self) -> usize {
417        match self {
418            VersionedDkgConfirmation::V1(msg) => msg.complaints.len(),
419        }
420    }
421
422    pub fn unwrap_v1(&self) -> &dkg_v1::Confirmation<bls12381::G2Element> {
423        match self {
424            VersionedDkgConfirmation::V1(msg) => msg,
425        }
426    }
427
428    pub fn is_valid_version(&self, dkg_version: u64) -> bool {
429        matches!((self, dkg_version), (VersionedDkgConfirmation::V1(_), 1))
430    }
431}
432
433impl ConsensusTransaction {
434    pub fn new_certificate_message(
435        authority: &AuthorityName,
436        certificate: CertifiedTransaction,
437    ) -> Self {
438        let mut hasher = DefaultHasher::new();
439        let tx_digest = certificate.digest();
440        tx_digest.hash(&mut hasher);
441        authority.hash(&mut hasher);
442        let tracking_id = hasher.finish().to_le_bytes();
443        Self {
444            tracking_id,
445            kind: ConsensusTransactionKind::CertifiedTransaction(Box::new(certificate)),
446        }
447    }
448
449    pub fn new_checkpoint_signature_message(data: CheckpointSignatureMessage) -> Self {
450        let mut hasher = DefaultHasher::new();
451        data.summary.auth_sig().signature.hash(&mut hasher);
452        let tracking_id = hasher.finish().to_le_bytes();
453        Self {
454            tracking_id,
455            kind: ConsensusTransactionKind::CheckpointSignature(Box::new(data)),
456        }
457    }
458
459    pub fn new_end_of_publish(authority: AuthorityName) -> Self {
460        let mut hasher = DefaultHasher::new();
461        authority.hash(&mut hasher);
462        let tracking_id = hasher.finish().to_le_bytes();
463        Self {
464            tracking_id,
465            kind: ConsensusTransactionKind::EndOfPublish(authority),
466        }
467    }
468
469    pub fn new_capability_notification_v1(capabilities: AuthorityCapabilitiesV1) -> Self {
470        let mut hasher = DefaultHasher::new();
471        capabilities.hash(&mut hasher);
472        let tracking_id = hasher.finish().to_le_bytes();
473        Self {
474            tracking_id,
475            kind: ConsensusTransactionKind::CapabilityNotificationV1(capabilities),
476        }
477    }
478
479    pub fn new_signed_capability_notification_v1(
480        signed_capabilities: SignedAuthorityCapabilitiesV1,
481    ) -> Self {
482        let mut hasher = DefaultHasher::new();
483        signed_capabilities.data().hash(&mut hasher);
484        signed_capabilities.auth_sig().hash(&mut hasher);
485        let tracking_id = hasher.finish().to_le_bytes();
486        Self {
487            tracking_id,
488            kind: ConsensusTransactionKind::SignedCapabilityNotificationV1(signed_capabilities),
489        }
490    }
491
492    pub fn new_randomness_dkg_message(
493        authority: AuthorityName,
494        versioned_message: &VersionedDkgMessage,
495    ) -> Self {
496        let message =
497            bcs::to_bytes(versioned_message).expect("message serialization should not fail");
498        let mut hasher = DefaultHasher::new();
499        message.hash(&mut hasher);
500        let tracking_id = hasher.finish().to_le_bytes();
501        Self {
502            tracking_id,
503            kind: ConsensusTransactionKind::RandomnessDkgMessage(authority, message),
504        }
505    }
506    pub fn new_randomness_dkg_confirmation(
507        authority: AuthorityName,
508        versioned_confirmation: &VersionedDkgConfirmation,
509    ) -> Self {
510        let confirmation =
511            bcs::to_bytes(versioned_confirmation).expect("message serialization should not fail");
512        let mut hasher = DefaultHasher::new();
513        confirmation.hash(&mut hasher);
514        let tracking_id = hasher.finish().to_le_bytes();
515        Self {
516            tracking_id,
517            kind: ConsensusTransactionKind::RandomnessDkgConfirmation(authority, confirmation),
518        }
519    }
520
521    pub fn new_misbehavior_report(report: VersionedMisbehaviorReport) -> Self {
522        let serialized_report =
523            bcs::to_bytes(&report).expect("report serialization should not fail");
524        let mut hasher = DefaultHasher::new();
525        serialized_report.hash(&mut hasher);
526        let tracking_id = hasher.finish().to_le_bytes();
527        Self {
528            tracking_id,
529            kind: ConsensusTransactionKind::MisbehaviorReport(report),
530        }
531    }
532
533    pub fn get_tracking_id(&self) -> u64 {
534        (&self.tracking_id[..])
535            .read_u64::<BigEndian>()
536            .unwrap_or_default()
537    }
538
539    pub fn key(&self) -> ConsensusTransactionKey {
540        match &self.kind {
541            ConsensusTransactionKind::CertifiedTransaction(cert) => {
542                ConsensusTransactionKey::Certificate(*cert.digest())
543            }
544            ConsensusTransactionKind::CheckpointSignature(data) => {
545                ConsensusTransactionKey::CheckpointSignature(
546                    data.summary.auth_sig().authority,
547                    data.summary.sequence_number,
548                )
549            }
550            ConsensusTransactionKind::EndOfPublish(authority) => {
551                ConsensusTransactionKey::EndOfPublish(*authority)
552            }
553            ConsensusTransactionKind::CapabilityNotificationV1(cap) => {
554                ConsensusTransactionKey::CapabilityNotification(cap.authority, cap.generation)
555            }
556            ConsensusTransactionKind::SignedCapabilityNotificationV1(signed_cap) => {
557                ConsensusTransactionKey::CapabilityNotification(
558                    signed_cap.authority,
559                    signed_cap.generation,
560                )
561            }
562
563            #[allow(deprecated)]
564            ConsensusTransactionKind::NewJWKFetchedDeprecated => {
565                ConsensusTransactionKey::NewJWKFetchedDeprecated
566            }
567            ConsensusTransactionKind::RandomnessDkgMessage(authority, _) => {
568                ConsensusTransactionKey::RandomnessDkgMessage(*authority)
569            }
570            ConsensusTransactionKind::RandomnessDkgConfirmation(authority, _) => {
571                ConsensusTransactionKey::RandomnessDkgConfirmation(*authority)
572            }
573            ConsensusTransactionKind::MisbehaviorReport(report) => {
574                ConsensusTransactionKey::MisbehaviorReport(
575                    report.authority,
576                    *report.digest(),
577                    report.generation,
578                )
579            }
580        }
581    }
582
583    pub fn is_user_certificate(&self) -> bool {
584        matches!(self.kind, ConsensusTransactionKind::CertifiedTransaction(_))
585    }
586
587    pub fn is_end_of_publish(&self) -> bool {
588        matches!(self.kind, ConsensusTransactionKind::EndOfPublish(_))
589    }
590}
591
592#[cfg(test)]
593mod tests {
594    use super::*;
595
596    /// Pre-refactor wire shape of `VersionedMisbehaviorReport` — only `payload`
597    /// crossed the wire (the digest cache was `#[serde(skip)]`). Used to pin
598    /// post-refactor bytes against the legacy encoding.
599    #[derive(Serialize)]
600    struct LegacyVersionedMisbehaviorReport<'a> {
601        payload: &'a MisbehaviorObservations,
602    }
603
604    fn sample_payload() -> MisbehaviorObservations {
605        MisbehaviorObservations::V1(MisbehaviorObservationsV1 {
606            faulty_blocks_provable: vec![1, 2, 3],
607            faulty_blocks_unprovable: vec![4, 5, 6],
608            missing_proposals: vec![7, 8, 9],
609            equivocations: vec![10, 11, 12],
610        })
611    }
612
613    /// Pins the BCS encoding of `VersionedMisbehaviorReport` against the
614    /// pre-refactor 3-tuple layout `(AuthorityName, { payload }, u64)`. Testnet
615    /// is running the legacy format; if the bytes ever drift, validators on
616    /// the new build will reject reports from validators on the old build (or
617    /// vice versa) and consensus halts. Reordering struct fields, adding a
618    /// non-`skip` field, or renaming a field's serde tag will all trip this
619    /// test.
620    #[test]
621    fn misbehavior_report_wire_format_unchanged() {
622        let authority = AuthorityName::default();
623        let generation: u64 = 42;
624        let payload = sample_payload();
625
626        let legacy_bytes = bcs::to_bytes(&(
627            authority,
628            LegacyVersionedMisbehaviorReport { payload: &payload },
629            generation,
630        ))
631        .unwrap();
632
633        let new = VersionedMisbehaviorReport {
634            authority,
635            payload,
636            generation,
637            digest: OnceCell::new(),
638        };
639        let new_bytes = bcs::to_bytes(&new).unwrap();
640
641        assert_eq!(
642            legacy_bytes, new_bytes,
643            "VersionedMisbehaviorReport wire format must not change — testnet is live"
644        );
645    }
646
647    /// `ConsensusTransactionKind::MisbehaviorReport`'s variant tag is its
648    /// position in the enum (BCS encodes enum variants as ULEB128 of the
649    /// declaration index). Reordering variants — even if the new wrapping
650    /// layout is byte-identical otherwise — would shift the tag and break
651    /// every node still on the old build. This test catches that and also
652    /// confirms the post-tag bytes equal the legacy 3-tuple encoding.
653    #[test]
654    fn misbehavior_report_consensus_kind_wire_format_unchanged() {
655        let authority = AuthorityName::default();
656        let generation: u64 = 7;
657        let payload = sample_payload();
658
659        let new_kind = ConsensusTransactionKind::MisbehaviorReport(VersionedMisbehaviorReport {
660            authority,
661            payload: payload.clone(),
662            generation,
663            digest: OnceCell::new(),
664        });
665        let new_bytes = bcs::to_bytes(&new_kind).unwrap();
666
667        // Legacy encoding: variant tag (8 = position of MisbehaviorReport in
668        // the enum, ULEB128 single byte) followed by the 3-tuple body.
669        let mut legacy_bytes = vec![8u8];
670        legacy_bytes.extend(
671            bcs::to_bytes(&(
672                authority,
673                LegacyVersionedMisbehaviorReport { payload: &payload },
674                generation,
675            ))
676            .unwrap(),
677        );
678
679        assert_eq!(
680            legacy_bytes, new_bytes,
681            "ConsensusTransactionKind::MisbehaviorReport wire format must not change — testnet is live"
682        );
683    }
684}