Skip to main content

de_mls/core/
proposal_kind.rs

1//! Classification of [`ConversationUpdateRequest`]s by protocol role.
2//!
3//! `ProposalKind` is the canonical classifier for membership-vs-governance
4//! proposals. The ordinal order encodes RFC partial-freeze priority
5//! (Commit < StewardElection < Emergency), used by
6//! [`Conversation::partial_freeze_blocks`](crate::core::Conversation::partial_freeze_blocks).
7
8use crate::protos::de_mls::messages::v1::{
9    ConversationUpdateRequest, conversation_update_request::Payload,
10};
11
12/// Protocol role of a `ConversationUpdateRequest`. Ordinal order is RFC priority
13/// (higher variant beats lower when both are in flight).
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
15pub enum ProposalKind {
16    /// Membership change (invite / remove member). Lowest priority.
17    Commit = 0,
18    /// Steward rotation. Between commit and emergency.
19    StewardElection = 1,
20    /// Emergency criteria — highest priority; partially freezes lower kinds.
21    Emergency = 2,
22}
23
24impl ProposalKind {
25    /// Classify a `ConversationUpdateRequest`. `None` / unknown payloads map to [`Self::Commit`].
26    pub fn of(req: &ConversationUpdateRequest) -> Self {
27        match &req.payload {
28            Some(Payload::EmergencyCriteria(_)) => Self::Emergency,
29            Some(Payload::StewardElection(_)) => Self::StewardElection,
30            _ => Self::Commit,
31        }
32    }
33
34    pub fn is_emergency(self) -> bool {
35        self == Self::Emergency
36    }
37
38    pub fn is_steward_election(self) -> bool {
39        self == Self::StewardElection
40    }
41
42    /// True for kinds that produce an MLS proposal (invite / remove member).
43    /// Emergency and election are consensus-only — they never land in an MLS commit.
44    pub fn is_mls_producing(self) -> bool {
45        self == Self::Commit
46    }
47
48    /// True for emergency or steward-election kinds (non-commit governance).
49    pub fn is_governance(self) -> bool {
50        !self.is_mls_producing()
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57    use crate::protos::de_mls::messages::v1::{
58        EmergencyCriteriaProposal, InviteMember, RemoveMember, StewardElectionProposal,
59        ViolationEvidence,
60    };
61
62    fn req(payload: Payload) -> ConversationUpdateRequest {
63        ConversationUpdateRequest {
64            payload: Some(payload),
65        }
66    }
67
68    #[test]
69    fn invite_and_remove_are_commit() {
70        assert_eq!(
71            ProposalKind::of(&req(Payload::InviteMember(InviteMember::default()))),
72            ProposalKind::Commit,
73        );
74        assert_eq!(
75            ProposalKind::of(&req(Payload::RemoveMember(RemoveMember::default()))),
76            ProposalKind::Commit,
77        );
78    }
79
80    #[test]
81    fn emergency_classified() {
82        let r = req(Payload::EmergencyCriteria(EmergencyCriteriaProposal {
83            evidence: Some(ViolationEvidence::broken_commit(vec![], 0, vec![])),
84        }));
85        let k = ProposalKind::of(&r);
86        assert_eq!(k, ProposalKind::Emergency);
87        assert!(k.is_emergency());
88        assert!(k.is_governance());
89        assert!(!k.is_mls_producing());
90    }
91
92    #[test]
93    fn steward_election_classified() {
94        let r = req(Payload::StewardElection(StewardElectionProposal {
95            proposed_stewards: vec![vec![1], vec![2]],
96            election_epoch: 5,
97            retry_round: 0,
98        }));
99        let k = ProposalKind::of(&r);
100        assert_eq!(k, ProposalKind::StewardElection);
101        assert!(k.is_steward_election());
102        assert!(k.is_governance());
103    }
104
105    /// RFC partial-freeze priority: Emergency > StewardElection > Commit.
106    /// [`Conversation::partial_freeze_blocks`] and any future cross-kind
107    /// priority check rely on this ordering.
108    #[test]
109    fn priority_ordering() {
110        assert!(ProposalKind::Emergency > ProposalKind::StewardElection);
111        assert!(ProposalKind::StewardElection > ProposalKind::Commit);
112        assert!(ProposalKind::Emergency > ProposalKind::Commit);
113    }
114}