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::{
17    Digest, MisbehaviorReportDigest, ObjectReference, TransactionDigest, crypto::IntentScope,
18};
19use once_cell::sync::OnceCell;
20use serde::{Deserialize, Serialize};
21use tracing::warn;
22
23use crate::{
24    base_types::{AuthorityName, ConciseableName},
25    crypto::{AuthoritySignature, DefaultHash, default_hash},
26    deny_rule_governance::DenyRuleSet,
27    message_envelope::{Envelope, Message, VerifiedEnvelope},
28    messages_checkpoint::{CheckpointSequenceNumber, CheckpointSignatureMessage},
29    supported_protocol_versions::{
30        Chain, SupportedProtocolVersions, SupportedProtocolVersionsWithHashes,
31    },
32    transaction::{CertifiedTransaction, SenderSignedData, Transaction},
33};
34
35#[derive(Serialize, Deserialize, Clone, Debug)]
36pub struct ConsensusTransaction {
37    /// Encodes an u64 unique tracking id to allow us trace a message between
38    /// IOTA and consensus. Use an byte array instead of u64 to ensure stable
39    /// serialization.
40    pub tracking_id: [u8; 8],
41    pub kind: ConsensusTransactionKind,
42}
43
44#[derive(Serialize, Deserialize, Clone, Hash, PartialEq, Eq, Ord, PartialOrd)]
45pub enum ConsensusTransactionKey {
46    Certificate(TransactionDigest),
47    CheckpointSignature(AuthorityName, CheckpointSequenceNumber),
48    EndOfPublish(AuthorityName),
49    CapabilityNotification(AuthorityName, u64 /* generation */),
50    #[deprecated(note = "Authenticator state (JWK) is deprecated and was never enabled on IOTA")]
51    NewJWKFetchedDeprecated,
52    RandomnessDkgMessage(AuthorityName),
53    RandomnessDkgConfirmation(AuthorityName),
54    MisbehaviorReport(
55        AuthorityName,
56        MisbehaviorReportDigest,
57        CheckpointSequenceNumber,
58    ),
59    /// P-COOL user transaction key (by transaction digest).
60    UserTransaction(TransactionDigest),
61    OverloadNotificationV1(AuthorityName, u64 /* generation */),
62    TransactionDenyRuleProposal(AuthorityName, u64 /* generation */),
63    // New entries should be added at the end to preserve serialization compatibility. DO NOT
64    // CHANGE THE ORDER OF EXISTING ENTRIES!
65}
66
67impl Debug for ConsensusTransactionKey {
68    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
69        match self {
70            Self::Certificate(digest) => write!(f, "Certificate({digest})"),
71            Self::CheckpointSignature(name, seq) => {
72                write!(f, "CheckpointSignature({:?}, {:?})", name.concise(), seq)
73            }
74            Self::EndOfPublish(name) => write!(f, "EndOfPublish({:?})", name.concise()),
75            Self::CapabilityNotification(name, generation) => write!(
76                f,
77                "CapabilityNotification({:?}, {:?})",
78                name.concise(),
79                generation
80            ),
81            #[allow(deprecated)]
82            Self::NewJWKFetchedDeprecated => {
83                write!(
84                    f,
85                    "NewJWKFetched(deprecated: Authenticator state (JWK) is deprecated and was never enabled on IOTA)"
86                )
87            }
88            Self::RandomnessDkgMessage(name) => {
89                write!(f, "RandomnessDkgMessage({:?})", name.concise())
90            }
91            Self::RandomnessDkgConfirmation(name) => {
92                write!(f, "RandomnessDkgConfirmation({:?})", name.concise())
93            }
94            Self::MisbehaviorReport(name, digest, checkpoint_seq) => {
95                write!(
96                    f,
97                    "MisbehaviorReport({:?}, {:?}, {:?})",
98                    name.concise(),
99                    digest,
100                    checkpoint_seq
101                )
102            }
103            Self::UserTransaction(digest) => write!(f, "UserTransaction({digest:?})"),
104            Self::OverloadNotificationV1(name, generation) => {
105                write!(
106                    f,
107                    "OverloadNotificationV1({:?}, gen={generation:?})",
108                    name.concise()
109                )
110            }
111            Self::TransactionDenyRuleProposal(name, generation) => {
112                write!(
113                    f,
114                    "TransactionDenyRuleProposal({:?}, gen={generation:?})",
115                    name.concise()
116                )
117            }
118        }
119    }
120}
121
122pub type SignedAuthorityCapabilitiesV1 = Envelope<AuthorityCapabilitiesV1, AuthoritySignature>;
123
124pub type VerifiedAuthorityCapabilitiesV1 =
125    VerifiedEnvelope<AuthorityCapabilitiesV1, AuthoritySignature>;
126
127#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
128pub struct AuthorityCapabilitiesDigest(Digest);
129
130impl AuthorityCapabilitiesDigest {
131    pub const fn new(digest: [u8; 32]) -> Self {
132        Self(Digest::new(digest))
133    }
134}
135
136impl Debug for AuthorityCapabilitiesDigest {
137    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
138        f.debug_tuple("AuthorityCapabilitiesDigest")
139            .field(&self.0)
140            .finish()
141    }
142}
143
144/// Used to advertise capabilities of each authority via consensus. This allows
145/// validators to negotiate the creation of the ChangeEpoch transaction.
146#[derive(Serialize, Deserialize, Clone, Hash)]
147pub struct AuthorityCapabilitiesV1 {
148    /// Originating authority - must match transaction source authority from
149    /// consensus or the signature of a non-committee active validator.
150    pub authority: AuthorityName,
151    /// Generation number set by sending authority. Used to determine which of
152    /// multiple AuthorityCapabilities messages from the same authority is
153    /// the most recent.
154    ///
155    /// (Currently, we just set this to the current time in milliseconds since
156    /// the epoch, but this should not be interpreted as a timestamp.)
157    pub generation: u64,
158
159    /// ProtocolVersions that the authority supports, including the hash of the
160    /// serialized ProtocolConfig of that authority per version.
161    pub supported_protocol_versions: SupportedProtocolVersionsWithHashes,
162
163    /// The ObjectRefs of all versions of system packages that the validator
164    /// possesses. Used to determine whether to do a framework/movestdlib
165    /// upgrade.
166    pub available_system_packages: Vec<ObjectReference>,
167}
168
169impl Message for AuthorityCapabilitiesV1 {
170    type DigestType = AuthorityCapabilitiesDigest;
171    const SCOPE: IntentScope = IntentScope::AuthorityCapabilities;
172
173    fn digest(&self) -> Self::DigestType {
174        // Ensure deterministic serialization for digest
175        let mut hasher = DefaultHash::new();
176        let serialized = bcs::to_bytes(&self).expect("BCS should not fail");
177        hasher.update(&serialized);
178        AuthorityCapabilitiesDigest::new(<[u8; 32]>::from(hasher.finalize()))
179    }
180}
181
182impl Debug for AuthorityCapabilitiesV1 {
183    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
184        f.debug_struct("AuthorityCapabilities")
185            .field("authority", &self.authority.concise())
186            .field("generation", &self.generation)
187            .field(
188                "supported_protocol_versions",
189                &self.supported_protocol_versions,
190            )
191            .field("available_system_packages", &self.available_system_packages)
192            .finish()
193    }
194}
195
196impl AuthorityCapabilitiesV1 {
197    pub fn new(
198        authority: AuthorityName,
199        chain: Chain,
200        supported_protocol_versions: SupportedProtocolVersions,
201        available_system_packages: Vec<ObjectReference>,
202    ) -> Self {
203        let generation = SystemTime::now()
204            .duration_since(UNIX_EPOCH)
205            .expect("IOTA did not exist prior to 1970")
206            .as_millis()
207            .try_into()
208            .expect("This build of iota is not supported in the year 500,000,000");
209        Self {
210            authority,
211            generation,
212            supported_protocol_versions:
213                SupportedProtocolVersionsWithHashes::from_supported_versions(
214                    supported_protocol_versions,
215                    chain,
216                ),
217            available_system_packages,
218        }
219    }
220}
221
222impl SignedAuthorityCapabilitiesV1 {
223    pub fn cache_digest(&self, epoch: u64) -> AuthorityCapabilitiesDigest {
224        // Create a tuple that includes both the capabilities data and the epoch
225        let data_with_epoch = (self.data(), epoch);
226
227        // Ensure deterministic serialization for digest
228        let mut hasher = DefaultHash::new();
229        let serialized = bcs::to_bytes(&data_with_epoch).expect("BCS should not fail");
230        hasher.update(&serialized);
231        AuthorityCapabilitiesDigest::new(<[u8; 32]>::from(hasher.finalize()))
232    }
233}
234
235/// A validator's full-state proposal for the network transaction deny rules,
236/// announced through consensus.
237///
238/// Each proposal carries the authority's complete proposed rule set; the latest
239/// generation per authority supersedes earlier ones. The active rule set the
240/// network enforces is the stake-weighted aggregate of all current proposals.
241#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
242pub struct TransactionDenyRuleProposal {
243    /// The authority announcing this proposal.
244    pub authority: AuthorityName,
245    /// Per-authority counter used to deduplicate proposals; a higher generation
246    /// supersedes earlier proposals from the same authority.
247    pub generation: u64,
248    /// The complete set of rules this authority proposes.
249    pub proposed_rules: DenyRuleSet,
250}
251
252impl TransactionDenyRuleProposal {
253    /// Creates a proposal with a wall-clock generation, so a resubmission
254    /// supersedes this authority's earlier proposals. Pass the generation of
255    /// this authority's currently recorded proposal (if any) so the new one
256    /// stays newer even after a backward wall-clock step.
257    pub fn new(
258        authority: AuthorityName,
259        proposed_rules: DenyRuleSet,
260        last_generation: Option<u64>,
261    ) -> Self {
262        let now: u64 = SystemTime::now()
263            .duration_since(UNIX_EPOCH)
264            .expect("IOTA did not exist prior to 1970")
265            .as_millis()
266            .try_into()
267            .expect("This build of iota is not supported in the year 500,000,000");
268        Self {
269            authority,
270            generation: now.max(last_generation.map_or(0, |g| g.saturating_add(1))),
271            proposed_rules,
272        }
273    }
274}
275
276#[derive(Serialize, Deserialize, Clone, Debug)]
277pub enum ConsensusTransactionKind {
278    CertifiedTransaction(Box<CertifiedTransaction>),
279    CheckpointSignature(Box<CheckpointSignatureMessage>),
280    EndOfPublish(AuthorityName),
281
282    CapabilityNotificationV1(AuthorityCapabilitiesV1),
283    SignedCapabilityNotificationV1(SignedAuthorityCapabilitiesV1),
284
285    #[deprecated(note = "Authenticator state (JWK) is deprecated and was never enabled on IOTA")]
286    NewJWKFetchedDeprecated,
287
288    // DKG is used to generate keys for use in the random beacon protocol.
289    // `RandomnessDkgMessage` is sent out at start-of-epoch to initiate the process.
290    // Contents are a serialized `fastcrypto_tbls::dkg::Message`.
291    RandomnessDkgMessage(AuthorityName, Vec<u8>),
292    // `RandomnessDkgConfirmation` is the second DKG message, sent as soon as a threshold amount
293    // of `RandomnessDkgMessages` have been received locally, to complete the key generation
294    // process. Contents are a serialized `fastcrypto_tbls::dkg::Confirmation`.
295    RandomnessDkgConfirmation(AuthorityName, Vec<u8>),
296    MisbehaviorReport(VersionedMisbehaviorReport),
297    /// P-COOL user transaction. Raw, uncertified transaction submitted
298    /// directly to consensus without pre-consensus object locking.
299    /// Conflicts are resolved post-consensus.
300    UserTransactionV1(Box<Transaction>),
301    OverloadNotificationV1(
302        AuthorityName,
303        u64, // generation
304        u8,  // percentage
305    ),
306    /// A validator's full-state deny rule proposal. Unsigned: the sender is
307    /// authenticated as the consensus block author and must match
308    /// `TransactionDenyRuleProposal::authority`.
309    TransactionDenyRuleProposal(TransactionDenyRuleProposal),
310    // New entries should be added at the end to preserve serialization compatibility. DO NOT
311    // CHANGE THE ORDER OF EXISTING ENTRIES!
312}
313
314impl ConsensusTransactionKind {
315    // NOTE: Keep every match in this impl exhaustive (no `_` arm) so a new
316    // `ConsensusTransactionKind` variant must be classified here rather
317    // than being silently mishandled.
318
319    /// Helper that applies the matching projection to the underlying certified
320    /// or raw user transaction, or `None` for internal consensus messages.
321    fn map_cert_or_raw_user_tx<'a, R>(
322        &'a self,
323        certified: impl FnOnce(&'a CertifiedTransaction) -> R,
324        raw: impl FnOnce(&'a Transaction) -> R,
325    ) -> Option<R> {
326        match self {
327            Self::CertifiedTransaction(c) => Some(certified(c)),
328            Self::UserTransactionV1(t) => Some(raw(t)),
329            Self::CheckpointSignature(_)
330            | Self::EndOfPublish(_)
331            | Self::CapabilityNotificationV1(_)
332            | Self::SignedCapabilityNotificationV1(_)
333            | Self::RandomnessDkgMessage(..)
334            | Self::RandomnessDkgConfirmation(..)
335            | Self::MisbehaviorReport(_)
336            | Self::OverloadNotificationV1(..)
337            | Self::TransactionDenyRuleProposal(_) => None,
338            #[allow(deprecated)]
339            Self::NewJWKFetchedDeprecated => None,
340        }
341    }
342
343    /// The signed data of the underlying certified or raw user transaction,
344    /// or `None` for internal consensus messages.
345    pub fn as_sender_signed_data(&self) -> Option<&SenderSignedData> {
346        self.map_cert_or_raw_user_tx(|c| c.data(), |t| t.data())
347    }
348
349    /// The (cached) transaction digest of the underlying certified or raw
350    /// user transaction, or `None` for internal consensus messages.
351    pub fn transaction_digest(&self) -> Option<TransactionDigest> {
352        self.map_cert_or_raw_user_tx(|c| *c.digest(), |t| *t.digest())
353    }
354
355    /// Returns the raw, uncertified user transaction (`UserTransactionV1`)
356    /// submitted directly to consensus, or `None` for any other kind. Certified
357    /// user transactions are not included here.
358    pub fn as_user_transaction(&self) -> Option<&Transaction> {
359        match self {
360            Self::UserTransactionV1(tx) => Some(tx),
361            Self::CertifiedTransaction(_)
362            | Self::CheckpointSignature(_)
363            | Self::EndOfPublish(_)
364            | Self::CapabilityNotificationV1(_)
365            | Self::SignedCapabilityNotificationV1(_)
366            | Self::RandomnessDkgMessage(..)
367            | Self::RandomnessDkgConfirmation(..)
368            | Self::MisbehaviorReport(_)
369            | Self::OverloadNotificationV1(..)
370            | Self::TransactionDenyRuleProposal(_) => None,
371            #[allow(deprecated)]
372            Self::NewJWKFetchedDeprecated => None,
373        }
374    }
375
376    /// Returns `true` only for a raw, uncertified user transaction
377    /// (`UserTransactionV1`) submitted directly to consensus. Certified user
378    /// transactions are not included here.
379    pub fn is_user_transaction(&self) -> bool {
380        self.as_user_transaction().is_some()
381    }
382
383    /// Returns `true` for the randomness DKG messages
384    /// (`RandomnessDkgMessage` and `RandomnessDkgConfirmation`).
385    pub fn is_dkg(&self) -> bool {
386        match self {
387            Self::RandomnessDkgMessage(_, _) | Self::RandomnessDkgConfirmation(_, _) => true,
388            Self::UserTransactionV1(_)
389            | Self::CertifiedTransaction(_)
390            | Self::CheckpointSignature(_)
391            | Self::EndOfPublish(_)
392            | Self::CapabilityNotificationV1(_)
393            | Self::SignedCapabilityNotificationV1(_)
394            | Self::MisbehaviorReport(_)
395            | Self::OverloadNotificationV1(..)
396            | Self::TransactionDenyRuleProposal(_) => false,
397            #[allow(deprecated)]
398            Self::NewJWKFetchedDeprecated => false,
399        }
400    }
401}
402
403/// A misbehavior report carrying a versioned payload plus a memoized digest.
404///
405/// Wire format is BCS over the `Serialize`-derived fields in declaration order:
406/// `authority || payload || generation`. This exactly matches the pre-refactor
407/// `ConsensusTransactionKind::MisbehaviorReport(AuthorityName,
408/// VersionedMisbehaviorReport { payload }, CheckpointSequenceNumber)` 3-tuple
409/// — see `tests::misbehavior_report_wire_format_unchanged` which pins the
410/// equivalence. Reordering or inserting any non-`skip` field here would change
411/// the consensus wire format and halt a running testnet.
412#[derive(Debug, Clone, Serialize, Deserialize)]
413pub struct VersionedMisbehaviorReport {
414    /// Originating authority — must match the transaction source authority
415    /// from consensus. Verified at the consensus boundary.
416    pub authority: AuthorityName,
417    /// Versioned payload of the misbehavior report.
418    pub payload: MisbehaviorObservations,
419    /// Generation number set by the sending authority. Used to identify the
420    /// most recent report from each authority. Currently set to the
421    /// checkpoint sequence number at which the report was generated.
422    pub generation: u64,
423    #[serde(skip)]
424    digest: OnceCell<MisbehaviorReportDigest>,
425}
426
427/// Versioned per-authority misbehavior observations. New variants get their
428/// own named-field payload type (`MisbehaviorObservationsV2`,
429/// `MisbehaviorObservationsV3`, ...) so the wire schema stays compile-time
430/// checked. Also serves as the in-memory representation in
431/// `MisbehaviorMonitor` / `ReportAggregator`.
432#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
433pub enum MisbehaviorObservations {
434    V1(MisbehaviorObservationsV1),
435}
436
437impl VersionedMisbehaviorReport {
438    pub fn new_v1(
439        authority: AuthorityName,
440        generation: u64,
441        observations: MisbehaviorObservationsV1,
442    ) -> Self {
443        Self {
444            authority,
445            payload: MisbehaviorObservations::V1(observations),
446            generation,
447            digest: OnceCell::new(),
448        }
449    }
450
451    /// Returns the digest of the misbehavior report, caching it if it has not
452    /// been computed yet.
453    pub fn digest(&self) -> &MisbehaviorReportDigest {
454        self.digest
455            .get_or_init(|| MisbehaviorReportDigest::new(default_hash(self)))
456    }
457
458    /// Returns the summary of the misbehavior report, defined as the sum of all
459    /// metrics for all authorities.
460    pub fn summary(&self) -> u64 {
461        let summary = match &self.payload {
462            MisbehaviorObservations::V1(report) => [
463                &report.faulty_blocks_provable,
464                &report.faulty_blocks_unprovable,
465                &report.missing_proposals,
466                &report.equivocations,
467            ]
468            .into_iter()
469            .flatten()
470            .fold(0u64, |acc, metric| acc.saturating_add(*metric)),
471        };
472        if summary == u64::MAX {
473            warn!("MisbehaviorReport summary reached its maximum value.");
474        }
475        summary
476    }
477}
478
479/// V1 misbehavior observations: per-authority counts for each tracked
480/// misbehavior category (faulty blocks, equivocations, missing proposals).
481/// Field order is part of the wire format — BCS serializes named struct
482/// fields in declaration order. This first version does not include any
483/// type of proof.
484#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
485pub struct MisbehaviorObservationsV1 {
486    pub faulty_blocks_provable: Vec<u64>,
487    pub faulty_blocks_unprovable: Vec<u64>,
488    pub missing_proposals: Vec<u64>,
489    pub equivocations: Vec<u64>,
490}
491
492impl MisbehaviorObservationsV1 {
493    pub fn verify(&self, committee_size: usize) -> bool {
494        // This version of reports are valid as long as they contain the counts for all
495        // authorities. Future versions may contain proofs that need verification.
496        // However, since the validity of a proof is deeply coupled with the protocol
497        // version and the consensus mechanism being used, we cannot verify it here. In
498        // the future, reports should be unwrapped (or translated) to a type verifiable
499        // by the starfish crate, which means that the verification logic will probably
500        // move out of this crate.
501        if (self.faulty_blocks_provable.len() != committee_size)
502            || (self.faulty_blocks_unprovable.len() != committee_size)
503            || (self.equivocations.len() != committee_size)
504            || (self.missing_proposals.len() != committee_size)
505        {
506            return false;
507        }
508        true
509    }
510}
511
512#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
513pub enum VersionedDkgMessage {
514    V1(dkg_v1::Message<bls12381::G2Element, bls12381::G2Element>),
515}
516
517#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
518pub enum VersionedDkgConfirmation {
519    V1(dkg_v1::Confirmation<bls12381::G2Element>),
520}
521
522impl Debug for VersionedDkgMessage {
523    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
524        match self {
525            VersionedDkgMessage::V1(msg) => write!(
526                f,
527                "DKG V1 Message with sender={}, vss_pk.degree={}, encrypted_shares.len()={}",
528                msg.sender,
529                msg.vss_pk.degree(),
530                msg.encrypted_shares.len(),
531            ),
532        }
533    }
534}
535
536impl VersionedDkgMessage {
537    pub fn sender(&self) -> u16 {
538        match self {
539            VersionedDkgMessage::V1(msg) => msg.sender,
540        }
541    }
542
543    pub fn create(
544        dkg_version: u64,
545        party: Arc<dkg_v1::Party<bls12381::G2Element, bls12381::G2Element>>,
546    ) -> FastCryptoResult<VersionedDkgMessage> {
547        assert_eq!(dkg_version, 1, "BUG: invalid DKG version");
548        let msg = party.create_message(&mut rand::thread_rng())?;
549        Ok(VersionedDkgMessage::V1(msg))
550    }
551
552    pub fn unwrap_v1(self) -> dkg_v1::Message<bls12381::G2Element, bls12381::G2Element> {
553        match self {
554            VersionedDkgMessage::V1(msg) => msg,
555        }
556    }
557
558    pub fn is_valid_version(&self, dkg_version: u64) -> bool {
559        matches!((self, dkg_version), (VersionedDkgMessage::V1(_), 1))
560    }
561}
562
563impl VersionedDkgConfirmation {
564    pub fn sender(&self) -> u16 {
565        match self {
566            VersionedDkgConfirmation::V1(msg) => msg.sender,
567        }
568    }
569
570    pub fn num_of_complaints(&self) -> usize {
571        match self {
572            VersionedDkgConfirmation::V1(msg) => msg.complaints.len(),
573        }
574    }
575
576    pub fn unwrap_v1(&self) -> &dkg_v1::Confirmation<bls12381::G2Element> {
577        match self {
578            VersionedDkgConfirmation::V1(msg) => msg,
579        }
580    }
581
582    pub fn is_valid_version(&self, dkg_version: u64) -> bool {
583        matches!((self, dkg_version), (VersionedDkgConfirmation::V1(_), 1))
584    }
585}
586
587impl ConsensusTransaction {
588    pub fn new_certificate_message(
589        authority: &AuthorityName,
590        certificate: CertifiedTransaction,
591    ) -> Self {
592        let mut hasher = DefaultHasher::new();
593        let tx_digest = certificate.digest();
594        tx_digest.hash(&mut hasher);
595        authority.hash(&mut hasher);
596        let tracking_id = hasher.finish().to_le_bytes();
597        Self {
598            tracking_id,
599            kind: ConsensusTransactionKind::CertifiedTransaction(Box::new(certificate)),
600        }
601    }
602
603    pub fn new_checkpoint_signature_message(data: CheckpointSignatureMessage) -> Self {
604        let mut hasher = DefaultHasher::new();
605        data.summary.auth_sig().signature.hash(&mut hasher);
606        let tracking_id = hasher.finish().to_le_bytes();
607        Self {
608            tracking_id,
609            kind: ConsensusTransactionKind::CheckpointSignature(Box::new(data)),
610        }
611    }
612
613    pub fn new_end_of_publish(authority: AuthorityName) -> Self {
614        let mut hasher = DefaultHasher::new();
615        authority.hash(&mut hasher);
616        let tracking_id = hasher.finish().to_le_bytes();
617        Self {
618            tracking_id,
619            kind: ConsensusTransactionKind::EndOfPublish(authority),
620        }
621    }
622
623    pub fn new_capability_notification_v1(capabilities: AuthorityCapabilitiesV1) -> Self {
624        let mut hasher = DefaultHasher::new();
625        capabilities.hash(&mut hasher);
626        let tracking_id = hasher.finish().to_le_bytes();
627        Self {
628            tracking_id,
629            kind: ConsensusTransactionKind::CapabilityNotificationV1(capabilities),
630        }
631    }
632
633    pub fn new_signed_capability_notification_v1(
634        signed_capabilities: SignedAuthorityCapabilitiesV1,
635    ) -> Self {
636        let mut hasher = DefaultHasher::new();
637        signed_capabilities.data().hash(&mut hasher);
638        signed_capabilities.auth_sig().hash(&mut hasher);
639        let tracking_id = hasher.finish().to_le_bytes();
640        Self {
641            tracking_id,
642            kind: ConsensusTransactionKind::SignedCapabilityNotificationV1(signed_capabilities),
643        }
644    }
645
646    pub fn new_randomness_dkg_message(
647        authority: AuthorityName,
648        versioned_message: &VersionedDkgMessage,
649    ) -> Self {
650        let message =
651            bcs::to_bytes(versioned_message).expect("message serialization should not fail");
652        let mut hasher = DefaultHasher::new();
653        message.hash(&mut hasher);
654        let tracking_id = hasher.finish().to_le_bytes();
655        Self {
656            tracking_id,
657            kind: ConsensusTransactionKind::RandomnessDkgMessage(authority, message),
658        }
659    }
660    pub fn new_randomness_dkg_confirmation(
661        authority: AuthorityName,
662        versioned_confirmation: &VersionedDkgConfirmation,
663    ) -> Self {
664        let confirmation =
665            bcs::to_bytes(versioned_confirmation).expect("message serialization should not fail");
666        let mut hasher = DefaultHasher::new();
667        confirmation.hash(&mut hasher);
668        let tracking_id = hasher.finish().to_le_bytes();
669        Self {
670            tracking_id,
671            kind: ConsensusTransactionKind::RandomnessDkgConfirmation(authority, confirmation),
672        }
673    }
674
675    pub fn new_misbehavior_report(report: VersionedMisbehaviorReport) -> Self {
676        let serialized_report =
677            bcs::to_bytes(&report).expect("report serialization should not fail");
678        let mut hasher = DefaultHasher::new();
679        serialized_report.hash(&mut hasher);
680        let tracking_id = hasher.finish().to_le_bytes();
681        Self {
682            tracking_id,
683            kind: ConsensusTransactionKind::MisbehaviorReport(report),
684        }
685    }
686
687    pub fn new_user_transaction(transaction: Transaction) -> Self {
688        let mut hasher = DefaultHasher::new();
689        let tx_digest = transaction.digest();
690        tx_digest.hash(&mut hasher);
691        let tracking_id = hasher.finish().to_le_bytes();
692        Self {
693            tracking_id,
694            kind: ConsensusTransactionKind::UserTransactionV1(Box::new(transaction)),
695        }
696    }
697
698    pub fn new_overload_notification_v1(
699        authority: AuthorityName,
700        load_shedding_percentage: u8,
701    ) -> Self {
702        // Wall-clock millis-since-epoch is used purely as a unique-per-submission
703        // disambiguator in the consensus transaction key, mirroring
704        // `AuthorityCapabilitiesV1::new`, because `consensus_message_processed`
705        // dedups by key for the full epoch.
706        // The receive side uses the percentage value directly; the generation is only
707        // for key uniqueness, not for ordering (consensus already orders deliveries).
708        let generation: u64 = SystemTime::now()
709            .duration_since(UNIX_EPOCH)
710            .expect("IOTA did not exist prior to 1970")
711            .as_millis()
712            .try_into()
713            .expect("This build of iota is not supported in the year 500,000,000");
714        let mut hasher = DefaultHasher::new();
715        authority.hash(&mut hasher);
716        generation.hash(&mut hasher);
717        load_shedding_percentage.hash(&mut hasher);
718        let tracking_id = hasher.finish().to_le_bytes();
719        Self {
720            tracking_id,
721            kind: ConsensusTransactionKind::OverloadNotificationV1(
722                authority,
723                generation,
724                load_shedding_percentage,
725            ),
726        }
727    }
728
729    pub fn new_transaction_deny_rule_proposal(proposal: TransactionDenyRuleProposal) -> Self {
730        let mut hasher = DefaultHasher::new();
731        proposal.hash(&mut hasher);
732        let tracking_id = hasher.finish().to_le_bytes();
733        Self {
734            tracking_id,
735            kind: ConsensusTransactionKind::TransactionDenyRuleProposal(proposal),
736        }
737    }
738
739    pub fn get_tracking_id(&self) -> u64 {
740        (&self.tracking_id[..])
741            .read_u64::<BigEndian>()
742            .unwrap_or_default()
743    }
744
745    pub fn key(&self) -> ConsensusTransactionKey {
746        match &self.kind {
747            ConsensusTransactionKind::CertifiedTransaction(cert) => {
748                ConsensusTransactionKey::Certificate(*cert.digest())
749            }
750            ConsensusTransactionKind::CheckpointSignature(data) => {
751                ConsensusTransactionKey::CheckpointSignature(
752                    data.summary.auth_sig().authority,
753                    data.summary.sequence_number,
754                )
755            }
756            ConsensusTransactionKind::EndOfPublish(authority) => {
757                ConsensusTransactionKey::EndOfPublish(*authority)
758            }
759            ConsensusTransactionKind::CapabilityNotificationV1(cap) => {
760                ConsensusTransactionKey::CapabilityNotification(cap.authority, cap.generation)
761            }
762            ConsensusTransactionKind::SignedCapabilityNotificationV1(signed_cap) => {
763                ConsensusTransactionKey::CapabilityNotification(
764                    signed_cap.authority,
765                    signed_cap.generation,
766                )
767            }
768
769            #[allow(deprecated)]
770            ConsensusTransactionKind::NewJWKFetchedDeprecated => {
771                ConsensusTransactionKey::NewJWKFetchedDeprecated
772            }
773            ConsensusTransactionKind::RandomnessDkgMessage(authority, _) => {
774                ConsensusTransactionKey::RandomnessDkgMessage(*authority)
775            }
776            ConsensusTransactionKind::RandomnessDkgConfirmation(authority, _) => {
777                ConsensusTransactionKey::RandomnessDkgConfirmation(*authority)
778            }
779            ConsensusTransactionKind::MisbehaviorReport(report) => {
780                ConsensusTransactionKey::MisbehaviorReport(
781                    report.authority,
782                    *report.digest(),
783                    report.generation,
784                )
785            }
786            ConsensusTransactionKind::UserTransactionV1(tx) => {
787                ConsensusTransactionKey::UserTransaction(*tx.digest())
788            }
789            ConsensusTransactionKind::OverloadNotificationV1(authority, generation, _) => {
790                ConsensusTransactionKey::OverloadNotificationV1(*authority, *generation)
791            }
792            ConsensusTransactionKind::TransactionDenyRuleProposal(proposal) => {
793                ConsensusTransactionKey::TransactionDenyRuleProposal(
794                    proposal.authority,
795                    proposal.generation,
796                )
797            }
798        }
799    }
800
801    pub fn is_user_certificate(&self) -> bool {
802        matches!(self.kind, ConsensusTransactionKind::CertifiedTransaction(_))
803    }
804
805    pub fn is_end_of_publish(&self) -> bool {
806        matches!(self.kind, ConsensusTransactionKind::EndOfPublish(_))
807    }
808}
809
810#[cfg(test)]
811mod tests {
812    use super::*;
813
814    /// Pre-refactor wire shape of `VersionedMisbehaviorReport` — only `payload`
815    /// crossed the wire (the digest cache was `#[serde(skip)]`). Used to pin
816    /// post-refactor bytes against the legacy encoding.
817    #[derive(Serialize)]
818    struct LegacyVersionedMisbehaviorReport<'a> {
819        payload: &'a MisbehaviorObservations,
820    }
821
822    fn sample_payload() -> MisbehaviorObservations {
823        MisbehaviorObservations::V1(MisbehaviorObservationsV1 {
824            faulty_blocks_provable: vec![1, 2, 3],
825            faulty_blocks_unprovable: vec![4, 5, 6],
826            missing_proposals: vec![7, 8, 9],
827            equivocations: vec![10, 11, 12],
828        })
829    }
830
831    /// Pins the BCS encoding of `VersionedMisbehaviorReport` against the
832    /// pre-refactor 3-tuple layout `(AuthorityName, { payload }, u64)`. Testnet
833    /// is running the legacy format; if the bytes ever drift, validators on
834    /// the new build will reject reports from validators on the old build (or
835    /// vice versa) and consensus halts. Reordering struct fields, adding a
836    /// non-`skip` field, or renaming a field's serde tag will all trip this
837    /// test.
838    #[test]
839    fn misbehavior_report_wire_format_unchanged() {
840        let authority = AuthorityName::default();
841        let generation: u64 = 42;
842        let payload = sample_payload();
843
844        let legacy_bytes = bcs::to_bytes(&(
845            authority,
846            LegacyVersionedMisbehaviorReport { payload: &payload },
847            generation,
848        ))
849        .unwrap();
850
851        let new = VersionedMisbehaviorReport {
852            authority,
853            payload,
854            generation,
855            digest: OnceCell::new(),
856        };
857        let new_bytes = bcs::to_bytes(&new).unwrap();
858
859        assert_eq!(
860            legacy_bytes, new_bytes,
861            "VersionedMisbehaviorReport wire format must not change — testnet is live"
862        );
863    }
864
865    /// `ConsensusTransactionKind::MisbehaviorReport`'s variant tag is its
866    /// position in the enum (BCS encodes enum variants as ULEB128 of the
867    /// declaration index). Reordering variants — even if the new wrapping
868    /// layout is byte-identical otherwise — would shift the tag and break
869    /// every node still on the old build. This test catches that and also
870    /// confirms the post-tag bytes equal the legacy 3-tuple encoding.
871    #[test]
872    fn misbehavior_report_consensus_kind_wire_format_unchanged() {
873        let authority = AuthorityName::default();
874        let generation: u64 = 7;
875        let payload = sample_payload();
876
877        let new_kind = ConsensusTransactionKind::MisbehaviorReport(VersionedMisbehaviorReport {
878            authority,
879            payload: payload.clone(),
880            generation,
881            digest: OnceCell::new(),
882        });
883        let new_bytes = bcs::to_bytes(&new_kind).unwrap();
884
885        // Legacy encoding: variant tag (8 = position of MisbehaviorReport in
886        // the enum, ULEB128 single byte) followed by the 3-tuple body.
887        let mut legacy_bytes = vec![8u8];
888        legacy_bytes.extend(
889            bcs::to_bytes(&(
890                authority,
891                LegacyVersionedMisbehaviorReport { payload: &payload },
892                generation,
893            ))
894            .unwrap(),
895        );
896
897        assert_eq!(
898            legacy_bytes, new_bytes,
899            "ConsensusTransactionKind::MisbehaviorReport wire format must not change — testnet is live"
900        );
901    }
902
903    /// Pins `ConsensusTransactionKind::TransactionDenyRuleProposal`'s variant
904    /// tag (11) and body layout: `(authority, generation, DenyRuleSet)`
905    /// with the rule set's fields in declaration order. Reordering enum
906    /// variants or `DenyRuleSet` fields breaks nodes on the old build.
907    #[test]
908    fn deny_rule_proposal_consensus_kind_wire_format_unchanged() {
909        use std::collections::BTreeSet;
910
911        use iota_sdk_types::{Address, ObjectId};
912
913        use crate::deny_rule_governance::DenyRuleSet;
914
915        let authority = AuthorityName::default();
916        let address = Address::new([7u8; 32]);
917        let object = ObjectId::new([8u8; 32]);
918        let package = ObjectId::new([9u8; 32]);
919
920        // Six one-hot switch patterns: a single sample can't pin the order of
921        // equal-valued booleans, so pin each switch position separately. The
922        // deny lists get distinct contents for the same reason.
923        for hot in 0..6usize {
924            let switch = |i: usize| i == hot;
925            let proposal = TransactionDenyRuleProposal {
926                authority,
927                generation: 42,
928                proposed_rules: DenyRuleSet {
929                    denied_addresses: [address].into(),
930                    denied_objects: [object].into(),
931                    denied_packages: [package].into(),
932                    package_publish_disabled: switch(0),
933                    package_upgrade_disabled: switch(1),
934                    shared_object_disabled: switch(2),
935                    user_transaction_disabled: switch(3),
936                    receiving_objects_disabled: switch(4),
937                    move_authenticator_disabled: switch(5),
938                },
939            };
940            let new_bytes = bcs::to_bytes(&ConsensusTransactionKind::TransactionDenyRuleProposal(
941                proposal,
942            ))
943            .unwrap();
944
945            let mut legacy_bytes = vec![11u8];
946            legacy_bytes.extend(
947                bcs::to_bytes(&(
948                    authority,
949                    42u64,
950                    (
951                        BTreeSet::from([address]),
952                        BTreeSet::from([object]),
953                        BTreeSet::from([package]),
954                        switch(0), // package_publish_disabled
955                        switch(1), // package_upgrade_disabled
956                        switch(2), // shared_object_disabled
957                        switch(3), // user_transaction_disabled
958                        switch(4), // receiving_objects_disabled
959                        switch(5), // move_authenticator_disabled
960                    ),
961                ))
962                .unwrap(),
963            );
964
965            assert_eq!(
966                legacy_bytes, new_bytes,
967                "ConsensusTransactionKind::TransactionDenyRuleProposal wire format must not \
968                 change (switch {hot})"
969            );
970        }
971    }
972
973    /// The proposal round-trips through BCS unchanged.
974    #[test]
975    fn deny_rule_proposal_bcs_round_trip() {
976        use iota_sdk_types::{Address, ObjectId};
977
978        use crate::deny_rule_governance::DenyRuleSet;
979
980        let proposal = TransactionDenyRuleProposal {
981            authority: AuthorityName::default(),
982            generation: 42,
983            proposed_rules: DenyRuleSet {
984                denied_addresses: [Address::new([1u8; 32])].into(),
985                denied_objects: [ObjectId::new([2u8; 32])].into(),
986                denied_packages: [ObjectId::new([3u8; 32])].into(),
987                package_publish_disabled: true,
988                shared_object_disabled: true,
989                receiving_objects_disabled: true,
990                ..Default::default()
991            },
992        };
993        let bytes = bcs::to_bytes(&proposal).unwrap();
994        assert_eq!(proposal, bcs::from_bytes(&bytes).unwrap());
995    }
996
997    /// The generation is wall-clock time, but never regresses below a
998    /// recorded proposal's generation even if the clock stepped backward.
999    #[test]
1000    fn proposal_generation_supersedes_last_generation() {
1001        use crate::deny_rule_governance::DenyRuleSet;
1002
1003        let authority = AuthorityName::default();
1004        let fresh = TransactionDenyRuleProposal::new(authority, DenyRuleSet::default(), None);
1005        assert!(fresh.generation > 0);
1006
1007        let far_future = fresh.generation + 1_000_000_000;
1008        let successor =
1009            TransactionDenyRuleProposal::new(authority, DenyRuleSet::default(), Some(far_future));
1010        assert_eq!(successor.generation, far_future + 1);
1011    }
1012}