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