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