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