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