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    DenyRuleSet, Digest, MisbehaviorReportDigest, ObjectReference, SenderSignedTransaction,
18    TransactionDigest, 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    message_envelope::{Envelope, Message, VerifiedEnvelope},
28    messages_checkpoint::{CheckpointSequenceNumber, CheckpointSignatureMessage},
29    supported_protocol_versions::{
30        Chain, SupportedProtocolVersions, SupportedProtocolVersionsWithHashes,
31    },
32    transaction::{CertifiedTransaction, TransactionEnvelope},
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<TransactionEnvelope>),
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 TransactionEnvelope) -> 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 transaction of the underlying certified or raw user
344    /// transaction, or `None` for internal consensus messages.
345    pub fn as_sender_signed_transaction(&self) -> Option<&SenderSignedTransaction> {
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<&TransactionEnvelope> {
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    V2(MisbehaviorObservationsV2),
436}
437
438impl MisbehaviorObservations {
439    /// Verifies the payload shape against the committee size.
440    pub fn verify(&self, committee_size: usize) -> bool {
441        match self {
442            Self::V1(payload) => payload.verify(committee_size),
443            Self::V2(payload) => payload.verify(committee_size),
444        }
445    }
446}
447
448impl VersionedMisbehaviorReport {
449    pub fn new_v1(
450        authority: AuthorityName,
451        generation: u64,
452        observations: MisbehaviorObservationsV1,
453    ) -> Self {
454        Self {
455            authority,
456            payload: MisbehaviorObservations::V1(observations),
457            generation,
458            digest: OnceCell::new(),
459        }
460    }
461
462    pub fn new_v2(
463        authority: AuthorityName,
464        generation: u64,
465        observations: MisbehaviorObservationsV2,
466    ) -> Self {
467        Self {
468            authority,
469            payload: MisbehaviorObservations::V2(observations),
470            generation,
471            digest: OnceCell::new(),
472        }
473    }
474
475    /// Returns the digest of the misbehavior report, caching it if it has not
476    /// been computed yet.
477    pub fn digest(&self) -> &MisbehaviorReportDigest {
478        self.digest
479            .get_or_init(|| MisbehaviorReportDigest::new(default_hash(self)))
480    }
481
482    /// Returns the summary of the misbehavior report, defined as the sum of all
483    /// metrics for all authorities.
484    pub fn summary(&self) -> u64 {
485        let summary = match &self.payload {
486            MisbehaviorObservations::V1(report) => [
487                &report.faulty_blocks_provable,
488                &report.faulty_blocks_unprovable,
489                &report.missing_proposals,
490                &report.equivocations,
491            ]
492            .into_iter()
493            .flatten()
494            .fold(0u64, |acc, metric| acc.saturating_add(*metric)),
495            MisbehaviorObservations::V2(report) => [
496                &report.faulty_blocks_provable,
497                &report.faulty_blocks_unprovable,
498                &report.missing_proposals,
499                &report.equivocations,
500                &report.invalid_bundle_parts,
501            ]
502            .into_iter()
503            .flatten()
504            .fold(0u64, |acc, metric| acc.saturating_add(*metric)),
505        };
506        if summary == u64::MAX {
507            warn!("MisbehaviorReport summary reached its maximum value.");
508        }
509        summary
510    }
511}
512
513/// V1 misbehavior observations: per-authority counts for each tracked
514/// misbehavior category (faulty blocks, equivocations, missing proposals).
515/// Field order is part of the wire format — BCS serializes named struct
516/// fields in declaration order. This first version does not include any
517/// type of proof.
518#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
519pub struct MisbehaviorObservationsV1 {
520    pub faulty_blocks_provable: Vec<u64>,
521    pub faulty_blocks_unprovable: Vec<u64>,
522    pub missing_proposals: Vec<u64>,
523    /// Per-authority count of rounds in which the authority signed more than
524    /// one block header, however many extra headers each round held.
525    pub equivocations: Vec<u64>,
526}
527
528impl MisbehaviorObservationsV1 {
529    pub fn verify(&self, committee_size: usize) -> bool {
530        // This version of reports are valid as long as they contain the counts for all
531        // authorities. Future versions may contain proofs that need verification.
532        // However, since the validity of a proof is deeply coupled with the protocol
533        // version and the consensus mechanism being used, we cannot verify it here. In
534        // the future, reports should be unwrapped (or translated) to a type verifiable
535        // by the starfish crate, which means that the verification logic will probably
536        // move out of this crate.
537        if (self.faulty_blocks_provable.len() != committee_size)
538            || (self.faulty_blocks_unprovable.len() != committee_size)
539            || (self.equivocations.len() != committee_size)
540            || (self.missing_proposals.len() != committee_size)
541        {
542            return false;
543        }
544        true
545    }
546}
547
548/// V2 misbehavior observations: the V1 categories plus a dedicated
549/// per-authority count of invalid bundle parts (counted under
550/// `faulty_blocks_unprovable` in the V1 format). Field order is part of the
551/// wire format — BCS serializes named struct fields in declaration order.
552#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
553pub struct MisbehaviorObservationsV2 {
554    pub faulty_blocks_provable: Vec<u64>,
555    pub faulty_blocks_unprovable: Vec<u64>,
556    pub missing_proposals: Vec<u64>,
557    /// Per-authority count of rounds in which the authority signed more than
558    /// one block header, however many extra headers each round held.
559    pub equivocations: Vec<u64>,
560    pub invalid_bundle_parts: Vec<u64>,
561}
562
563impl MisbehaviorObservationsV2 {
564    pub fn verify(&self, committee_size: usize) -> bool {
565        self.faulty_blocks_provable.len() == committee_size
566            && self.faulty_blocks_unprovable.len() == committee_size
567            && self.missing_proposals.len() == committee_size
568            && self.equivocations.len() == committee_size
569            && self.invalid_bundle_parts.len() == committee_size
570    }
571}
572
573#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
574pub enum VersionedDkgMessage {
575    V1(dkg_v1::Message<bls12381::G2Element, bls12381::G2Element>),
576}
577
578#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
579pub enum VersionedDkgConfirmation {
580    V1(dkg_v1::Confirmation<bls12381::G2Element>),
581}
582
583impl Debug for VersionedDkgMessage {
584    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
585        match self {
586            VersionedDkgMessage::V1(msg) => write!(
587                f,
588                "DKG V1 Message with sender={}, vss_pk.degree={}, encrypted_shares.len()={}",
589                msg.sender,
590                msg.vss_pk.degree(),
591                msg.encrypted_shares.len(),
592            ),
593        }
594    }
595}
596
597impl VersionedDkgMessage {
598    pub fn sender(&self) -> u16 {
599        match self {
600            VersionedDkgMessage::V1(msg) => msg.sender,
601        }
602    }
603
604    pub fn create(
605        dkg_version: u64,
606        party: Arc<dkg_v1::Party<bls12381::G2Element, bls12381::G2Element>>,
607    ) -> FastCryptoResult<VersionedDkgMessage> {
608        assert_eq!(dkg_version, 1, "BUG: invalid DKG version");
609        let msg = party.create_message(&mut rand::thread_rng())?;
610        Ok(VersionedDkgMessage::V1(msg))
611    }
612
613    pub fn unwrap_v1(self) -> dkg_v1::Message<bls12381::G2Element, bls12381::G2Element> {
614        match self {
615            VersionedDkgMessage::V1(msg) => msg,
616        }
617    }
618
619    pub fn is_valid_version(&self, dkg_version: u64) -> bool {
620        matches!((self, dkg_version), (VersionedDkgMessage::V1(_), 1))
621    }
622}
623
624impl VersionedDkgConfirmation {
625    pub fn sender(&self) -> u16 {
626        match self {
627            VersionedDkgConfirmation::V1(msg) => msg.sender,
628        }
629    }
630
631    pub fn num_of_complaints(&self) -> usize {
632        match self {
633            VersionedDkgConfirmation::V1(msg) => msg.complaints.len(),
634        }
635    }
636
637    pub fn unwrap_v1(&self) -> &dkg_v1::Confirmation<bls12381::G2Element> {
638        match self {
639            VersionedDkgConfirmation::V1(msg) => msg,
640        }
641    }
642
643    pub fn is_valid_version(&self, dkg_version: u64) -> bool {
644        matches!((self, dkg_version), (VersionedDkgConfirmation::V1(_), 1))
645    }
646}
647
648impl ConsensusTransaction {
649    pub fn new_certificate_message(
650        authority: &AuthorityName,
651        certificate: CertifiedTransaction,
652    ) -> Self {
653        let mut hasher = DefaultHasher::new();
654        let tx_digest = certificate.digest();
655        tx_digest.hash(&mut hasher);
656        authority.hash(&mut hasher);
657        let tracking_id = hasher.finish().to_le_bytes();
658        Self {
659            tracking_id,
660            kind: ConsensusTransactionKind::CertifiedTransaction(Box::new(certificate)),
661        }
662    }
663
664    pub fn new_checkpoint_signature_message(data: CheckpointSignatureMessage) -> Self {
665        let mut hasher = DefaultHasher::new();
666        data.summary.auth_sig().signature.hash(&mut hasher);
667        let tracking_id = hasher.finish().to_le_bytes();
668        Self {
669            tracking_id,
670            kind: ConsensusTransactionKind::CheckpointSignature(Box::new(data)),
671        }
672    }
673
674    pub fn new_end_of_publish(authority: AuthorityName) -> Self {
675        let mut hasher = DefaultHasher::new();
676        authority.hash(&mut hasher);
677        let tracking_id = hasher.finish().to_le_bytes();
678        Self {
679            tracking_id,
680            kind: ConsensusTransactionKind::EndOfPublish(authority),
681        }
682    }
683
684    pub fn new_capability_notification_v1(capabilities: AuthorityCapabilitiesV1) -> Self {
685        let mut hasher = DefaultHasher::new();
686        capabilities.hash(&mut hasher);
687        let tracking_id = hasher.finish().to_le_bytes();
688        Self {
689            tracking_id,
690            kind: ConsensusTransactionKind::CapabilityNotificationV1(capabilities),
691        }
692    }
693
694    pub fn new_signed_capability_notification_v1(
695        signed_capabilities: SignedAuthorityCapabilitiesV1,
696    ) -> Self {
697        let mut hasher = DefaultHasher::new();
698        signed_capabilities.data().hash(&mut hasher);
699        signed_capabilities.auth_sig().hash(&mut hasher);
700        let tracking_id = hasher.finish().to_le_bytes();
701        Self {
702            tracking_id,
703            kind: ConsensusTransactionKind::SignedCapabilityNotificationV1(signed_capabilities),
704        }
705    }
706
707    pub fn new_randomness_dkg_message(
708        authority: AuthorityName,
709        versioned_message: &VersionedDkgMessage,
710    ) -> Self {
711        let message =
712            bcs::to_bytes(versioned_message).expect("message serialization should not fail");
713        let mut hasher = DefaultHasher::new();
714        message.hash(&mut hasher);
715        let tracking_id = hasher.finish().to_le_bytes();
716        Self {
717            tracking_id,
718            kind: ConsensusTransactionKind::RandomnessDkgMessage(authority, message),
719        }
720    }
721    pub fn new_randomness_dkg_confirmation(
722        authority: AuthorityName,
723        versioned_confirmation: &VersionedDkgConfirmation,
724    ) -> Self {
725        let confirmation =
726            bcs::to_bytes(versioned_confirmation).expect("message serialization should not fail");
727        let mut hasher = DefaultHasher::new();
728        confirmation.hash(&mut hasher);
729        let tracking_id = hasher.finish().to_le_bytes();
730        Self {
731            tracking_id,
732            kind: ConsensusTransactionKind::RandomnessDkgConfirmation(authority, confirmation),
733        }
734    }
735
736    pub fn new_misbehavior_report(report: VersionedMisbehaviorReport) -> Self {
737        let serialized_report =
738            bcs::to_bytes(&report).expect("report serialization should not fail");
739        let mut hasher = DefaultHasher::new();
740        serialized_report.hash(&mut hasher);
741        let tracking_id = hasher.finish().to_le_bytes();
742        Self {
743            tracking_id,
744            kind: ConsensusTransactionKind::MisbehaviorReport(report),
745        }
746    }
747
748    pub fn new_user_transaction(transaction: TransactionEnvelope) -> Self {
749        let mut hasher = DefaultHasher::new();
750        let tx_digest = transaction.digest();
751        tx_digest.hash(&mut hasher);
752        let tracking_id = hasher.finish().to_le_bytes();
753        Self {
754            tracking_id,
755            kind: ConsensusTransactionKind::UserTransactionV1(Box::new(transaction)),
756        }
757    }
758
759    pub fn new_overload_notification_v1(
760        authority: AuthorityName,
761        load_shedding_percentage: u8,
762    ) -> Self {
763        // Wall-clock millis-since-epoch is used purely as a unique-per-submission
764        // disambiguator in the consensus transaction key, mirroring
765        // `AuthorityCapabilitiesV1::new`, because `consensus_message_processed`
766        // dedups by key for the full epoch.
767        // The receive side uses the percentage value directly; the generation is only
768        // for key uniqueness, not for ordering (consensus already orders deliveries).
769        let generation: u64 = SystemTime::now()
770            .duration_since(UNIX_EPOCH)
771            .expect("IOTA did not exist prior to 1970")
772            .as_millis()
773            .try_into()
774            .expect("This build of iota is not supported in the year 500,000,000");
775        let mut hasher = DefaultHasher::new();
776        authority.hash(&mut hasher);
777        generation.hash(&mut hasher);
778        load_shedding_percentage.hash(&mut hasher);
779        let tracking_id = hasher.finish().to_le_bytes();
780        Self {
781            tracking_id,
782            kind: ConsensusTransactionKind::OverloadNotificationV1(
783                authority,
784                generation,
785                load_shedding_percentage,
786            ),
787        }
788    }
789
790    pub fn new_transaction_deny_rule_proposal(proposal: TransactionDenyRuleProposal) -> Self {
791        let mut hasher = DefaultHasher::new();
792        proposal.hash(&mut hasher);
793        let tracking_id = hasher.finish().to_le_bytes();
794        Self {
795            tracking_id,
796            kind: ConsensusTransactionKind::TransactionDenyRuleProposal(proposal),
797        }
798    }
799
800    pub fn get_tracking_id(&self) -> u64 {
801        (&self.tracking_id[..])
802            .read_u64::<BigEndian>()
803            .unwrap_or_default()
804    }
805
806    pub fn key(&self) -> ConsensusTransactionKey {
807        match &self.kind {
808            ConsensusTransactionKind::CertifiedTransaction(cert) => {
809                ConsensusTransactionKey::Certificate(*cert.digest())
810            }
811            ConsensusTransactionKind::CheckpointSignature(data) => {
812                ConsensusTransactionKey::CheckpointSignature(
813                    data.summary.auth_sig().authority,
814                    data.summary.sequence_number,
815                )
816            }
817            ConsensusTransactionKind::EndOfPublish(authority) => {
818                ConsensusTransactionKey::EndOfPublish(*authority)
819            }
820            ConsensusTransactionKind::CapabilityNotificationV1(cap) => {
821                ConsensusTransactionKey::CapabilityNotification(cap.authority, cap.generation)
822            }
823            ConsensusTransactionKind::SignedCapabilityNotificationV1(signed_cap) => {
824                ConsensusTransactionKey::CapabilityNotification(
825                    signed_cap.authority,
826                    signed_cap.generation,
827                )
828            }
829
830            #[allow(deprecated)]
831            ConsensusTransactionKind::NewJWKFetchedDeprecated => {
832                ConsensusTransactionKey::NewJWKFetchedDeprecated
833            }
834            ConsensusTransactionKind::RandomnessDkgMessage(authority, _) => {
835                ConsensusTransactionKey::RandomnessDkgMessage(*authority)
836            }
837            ConsensusTransactionKind::RandomnessDkgConfirmation(authority, _) => {
838                ConsensusTransactionKey::RandomnessDkgConfirmation(*authority)
839            }
840            ConsensusTransactionKind::MisbehaviorReport(report) => {
841                ConsensusTransactionKey::MisbehaviorReport(
842                    report.authority,
843                    *report.digest(),
844                    report.generation,
845                )
846            }
847            ConsensusTransactionKind::UserTransactionV1(tx) => {
848                ConsensusTransactionKey::UserTransaction(*tx.digest())
849            }
850            ConsensusTransactionKind::OverloadNotificationV1(authority, generation, _) => {
851                ConsensusTransactionKey::OverloadNotificationV1(*authority, *generation)
852            }
853            ConsensusTransactionKind::TransactionDenyRuleProposal(proposal) => {
854                ConsensusTransactionKey::TransactionDenyRuleProposal(
855                    proposal.authority,
856                    proposal.generation,
857                )
858            }
859        }
860    }
861
862    pub fn is_user_certificate(&self) -> bool {
863        matches!(self.kind, ConsensusTransactionKind::CertifiedTransaction(_))
864    }
865
866    pub fn is_end_of_publish(&self) -> bool {
867        matches!(self.kind, ConsensusTransactionKind::EndOfPublish(_))
868    }
869}
870
871#[cfg(test)]
872mod tests {
873    use super::*;
874
875    /// Pre-refactor wire shape of `VersionedMisbehaviorReport` — only `payload`
876    /// crossed the wire (the digest cache was `#[serde(skip)]`). Used to pin
877    /// post-refactor bytes against the legacy encoding.
878    #[derive(Serialize)]
879    struct LegacyVersionedMisbehaviorReport<'a> {
880        payload: &'a MisbehaviorObservations,
881    }
882
883    fn sample_payload() -> MisbehaviorObservations {
884        MisbehaviorObservations::V1(MisbehaviorObservationsV1 {
885            faulty_blocks_provable: vec![1, 2, 3],
886            faulty_blocks_unprovable: vec![4, 5, 6],
887            missing_proposals: vec![7, 8, 9],
888            equivocations: vec![10, 11, 12],
889        })
890    }
891
892    /// Pins the BCS encoding of `VersionedMisbehaviorReport` against the
893    /// pre-refactor 3-tuple layout `(AuthorityName, { payload }, u64)`. Testnet
894    /// is running the legacy format; if the bytes ever drift, validators on
895    /// the new build will reject reports from validators on the old build (or
896    /// vice versa) and consensus halts. Reordering struct fields, adding a
897    /// non-`skip` field, or renaming a field's serde tag will all trip this
898    /// test.
899    #[test]
900    fn misbehavior_report_wire_format_unchanged() {
901        let authority = AuthorityName::default();
902        let generation: u64 = 42;
903        let payload = sample_payload();
904
905        let legacy_bytes = bcs::to_bytes(&(
906            authority,
907            LegacyVersionedMisbehaviorReport { payload: &payload },
908            generation,
909        ))
910        .unwrap();
911
912        let new = VersionedMisbehaviorReport {
913            authority,
914            payload,
915            generation,
916            digest: OnceCell::new(),
917        };
918        let new_bytes = bcs::to_bytes(&new).unwrap();
919
920        assert_eq!(
921            legacy_bytes, new_bytes,
922            "VersionedMisbehaviorReport wire format must not change — testnet is live"
923        );
924    }
925
926    /// Pins the BCS encoding of the `MisbehaviorObservations::V2` payload:
927    /// variant tag 1 (ULEB128 of the declaration index) followed by the five
928    /// per-authority vectors in declaration order.
929    #[test]
930    fn misbehavior_observations_v2_wire_format() {
931        let payload = MisbehaviorObservations::V2(MisbehaviorObservationsV2 {
932            faulty_blocks_provable: vec![1, 2, 3],
933            faulty_blocks_unprovable: vec![4, 5, 6],
934            missing_proposals: vec![7, 8, 9],
935            equivocations: vec![10, 11, 12],
936            invalid_bundle_parts: vec![13, 14, 15],
937        });
938
939        let mut expected = vec![1u8];
940        expected.extend(
941            bcs::to_bytes(&(
942                vec![1u64, 2, 3],
943                vec![4u64, 5, 6],
944                vec![7u64, 8, 9],
945                vec![10u64, 11, 12],
946                vec![13u64, 14, 15],
947            ))
948            .unwrap(),
949        );
950
951        assert_eq!(
952            bcs::to_bytes(&payload).unwrap(),
953            expected,
954            "MisbehaviorObservations::V2 wire format must not change"
955        );
956    }
957
958    /// `ConsensusTransactionKind::MisbehaviorReport`'s variant tag is its
959    /// position in the enum (BCS encodes enum variants as ULEB128 of the
960    /// declaration index). Reordering variants — even if the new wrapping
961    /// layout is byte-identical otherwise — would shift the tag and break
962    /// every node still on the old build. This test catches that and also
963    /// confirms the post-tag bytes equal the legacy 3-tuple encoding.
964    #[test]
965    fn misbehavior_report_consensus_kind_wire_format_unchanged() {
966        let authority = AuthorityName::default();
967        let generation: u64 = 7;
968        let payload = sample_payload();
969
970        let new_kind = ConsensusTransactionKind::MisbehaviorReport(VersionedMisbehaviorReport {
971            authority,
972            payload: payload.clone(),
973            generation,
974            digest: OnceCell::new(),
975        });
976        let new_bytes = bcs::to_bytes(&new_kind).unwrap();
977
978        // Legacy encoding: variant tag (8 = position of MisbehaviorReport in
979        // the enum, ULEB128 single byte) followed by the 3-tuple body.
980        let mut legacy_bytes = vec![8u8];
981        legacy_bytes.extend(
982            bcs::to_bytes(&(
983                authority,
984                LegacyVersionedMisbehaviorReport { payload: &payload },
985                generation,
986            ))
987            .unwrap(),
988        );
989
990        assert_eq!(
991            legacy_bytes, new_bytes,
992            "ConsensusTransactionKind::MisbehaviorReport wire format must not change — testnet is live"
993        );
994    }
995
996    /// Pins `ConsensusTransactionKind::TransactionDenyRuleProposal`'s variant
997    /// tag (11) and body layout: `(authority, generation, DenyRuleSet)`
998    /// with the rule set's fields in declaration order. Reordering enum
999    /// variants or `DenyRuleSet` fields breaks nodes on the old build.
1000    #[test]
1001    fn deny_rule_proposal_consensus_kind_wire_format_unchanged() {
1002        use iota_sdk_types::{Address, DenyRuleSet, ObjectId};
1003
1004        let authority = AuthorityName::default();
1005        let address = Address::new([7u8; 32]);
1006        let object = ObjectId::new([8u8; 32]);
1007        let package = ObjectId::new([9u8; 32]);
1008
1009        // Six one-hot switch patterns: a single sample can't pin the order of
1010        // equal-valued booleans, so pin each switch position separately. The
1011        // deny lists get distinct contents for the same reason.
1012        for hot in 0..6usize {
1013            let switch = |i: usize| i == hot;
1014            let proposal = TransactionDenyRuleProposal {
1015                authority,
1016                generation: 42,
1017                proposed_rules: DenyRuleSet {
1018                    denied_addresses: [address].into(),
1019                    denied_objects: [object].into(),
1020                    denied_packages: [package].into(),
1021                    package_publish_disabled: switch(0),
1022                    package_upgrade_disabled: switch(1),
1023                    shared_object_disabled: switch(2),
1024                    user_transaction_disabled: switch(3),
1025                    receiving_objects_disabled: switch(4),
1026                    move_authenticator_disabled: switch(5),
1027                },
1028            };
1029            let new_bytes = bcs::to_bytes(&ConsensusTransactionKind::TransactionDenyRuleProposal(
1030                proposal,
1031            ))
1032            .unwrap();
1033
1034            let mut legacy_bytes = vec![11u8];
1035            legacy_bytes.extend(
1036                bcs::to_bytes(&(
1037                    authority,
1038                    42u64,
1039                    (
1040                        vec![address],
1041                        vec![object],
1042                        vec![package],
1043                        switch(0), // package_publish_disabled
1044                        switch(1), // package_upgrade_disabled
1045                        switch(2), // shared_object_disabled
1046                        switch(3), // user_transaction_disabled
1047                        switch(4), // receiving_objects_disabled
1048                        switch(5), // move_authenticator_disabled
1049                    ),
1050                ))
1051                .unwrap(),
1052            );
1053
1054            assert_eq!(
1055                legacy_bytes, new_bytes,
1056                "ConsensusTransactionKind::TransactionDenyRuleProposal wire format must not \
1057                 change (switch {hot})"
1058            );
1059        }
1060    }
1061
1062    /// The proposal round-trips through BCS unchanged.
1063    #[test]
1064    fn deny_rule_proposal_bcs_round_trip() {
1065        use iota_sdk_types::{Address, DenyRuleSet, ObjectId};
1066
1067        let proposal = TransactionDenyRuleProposal {
1068            authority: AuthorityName::default(),
1069            generation: 42,
1070            proposed_rules: DenyRuleSet {
1071                denied_addresses: [Address::new([1u8; 32])].into(),
1072                denied_objects: [ObjectId::new([2u8; 32])].into(),
1073                denied_packages: [ObjectId::new([3u8; 32])].into(),
1074                package_publish_disabled: true,
1075                shared_object_disabled: true,
1076                receiving_objects_disabled: true,
1077                ..Default::default()
1078            },
1079        };
1080        let bytes = bcs::to_bytes(&proposal).unwrap();
1081        assert_eq!(proposal, bcs::from_bytes(&bytes).unwrap());
1082    }
1083
1084    /// The generation is wall-clock time, but never regresses below a
1085    /// recorded proposal's generation even if the clock stepped backward.
1086    #[test]
1087    fn proposal_generation_supersedes_last_generation() {
1088        use iota_sdk_types::DenyRuleSet;
1089
1090        let authority = AuthorityName::default();
1091        let fresh = TransactionDenyRuleProposal::new(authority, DenyRuleSet::default(), None);
1092        assert!(fresh.generation > 0);
1093
1094        let far_future = fresh.generation + 1_000_000_000;
1095        let successor =
1096            TransactionDenyRuleProposal::new(authority, DenyRuleSet::default(), Some(far_future));
1097        assert_eq!(successor.generation, far_future + 1);
1098    }
1099}