Skip to main content

de_mls/conversation/
query.rs

1//! Read-only queries over a conversation's state.
2
3use crate::{
4    ConsensusPlugin, Conversation, ConversationError, ConversationState, MemberRole,
5    PeerScoringPlugin, StewardListPlugin, mls_crypto::MlsService,
6    protos::de_mls::messages::v1::ConversationUpdateRequest,
7};
8
9impl<C, Sc, St> Conversation<C, Sc, St>
10where
11    C: ConsensusPlugin,
12    Sc: PeerScoringPlugin,
13    St: StewardListPlugin,
14{
15    /// Current state of the conversation's state machine.
16    pub fn state(&self) -> ConversationState {
17        self.current_state()
18    }
19
20    /// Name of this conversation. Identifies it in the integrator's
21    /// registry and on every [`crate::Outbound`] it produces.
22    pub fn id(&self) -> &str {
23        &self.conversation_id
24    }
25
26    /// Identity bytes of the local member in this conversation.
27    pub fn member_id_bytes(&self) -> &[u8] {
28        &self.self_member_id
29    }
30
31    /// App id this conversation tags on outbound packets and uses for self-echo
32    /// filtering in [`Conversation::process_inbound`].
33    pub fn app_id(&self) -> &[u8] {
34        &self.app_id
35    }
36
37    /// Current MLS epoch + reelection retry round. Intended for UI status
38    /// display.
39    pub fn epoch_and_retry(&self) -> Result<(u64, u32), ConversationError> {
40        let epoch = self.mls().current_epoch()?;
41        Ok((epoch, self.services.steward_list.next_retry_round()))
42    }
43
44    /// Count of buffered pending membership updates. Used by tests and the UI
45    /// to verify buffer hygiene (e.g., that a joiner's buffer is empty right
46    /// after they receive the welcome).
47    pub fn pending_update_count(&self) -> usize {
48        self.queues.pending_update_count()
49    }
50
51    /// Freeze round progress: `(received, expected)`. Returns `(0, 0)` if not
52    /// in freeze or no steward list is known.
53    pub fn freeze_candidate_count(&self) -> (usize, usize) {
54        let received = self.queues.freeze_candidate_count();
55        let expected = self
56            .services
57            .steward_list
58            .current_list()
59            .map(|l| l.len())
60            .unwrap_or(0);
61        (received, expected)
62    }
63
64    pub fn is_steward(&self) -> bool {
65        self.services.steward_list.is_steward(&self.self_member_id)
66    }
67
68    /// `true` if the local member is the **primary** steward designated for the
69    /// current epoch — the one that should commit and sponsor joiners first.
70    /// Unlike [`Self::is_steward`] (true for any member on the list, backups
71    /// included), this is true for exactly one member per epoch, so it gates the
72    /// single-actor paths: backups defer to the primary and only step in after
73    /// the recovery window. Eligibility is the same live rotation
74    /// [`Self::member_roles`] uses, so all members agree on who it is.
75    pub fn is_epoch_steward(&self) -> Result<bool, ConversationError> {
76        let mls = self.mls();
77        let epoch = mls.current_epoch()?;
78        let members = mls.members()?;
79        let eligible = self.queues.steward_eligibility(&members);
80        let (epoch_steward, _backup) = self
81            .services
82            .steward_list
83            .epoch_and_backup(epoch, &eligible);
84        Ok(epoch_steward == Some(self.self_member_id.as_ref()))
85    }
86
87    /// Identity bytes of every current member of this conversation, as
88    /// reported by MLS.
89    pub fn members(&self) -> Result<Vec<Vec<u8>>, ConversationError> {
90        Ok(self.mls().members()?)
91    }
92
93    pub fn member_scores(&self) -> Vec<(Vec<u8>, i64)> {
94        self.services.scoring.all_members_with_scores()
95    }
96
97    pub fn member_score(&self, member_id: &[u8]) -> Option<i64> {
98        self.services.scoring.score_for(member_id)
99    }
100
101    /// Identities that have an in-flight self-leave request. Used by the UI
102    /// to render a "pending leave" indicator.
103    pub fn pending_leave_member_ids(&self) -> Result<Vec<Vec<u8>>, ConversationError> {
104        let members = self.mls().members()?;
105        Ok(members
106            .into_iter()
107            .filter(|id| self.queues.is_pending_self_leave(id))
108            .collect())
109    }
110
111    /// Steward role for each member. Uses live rotation so removed or
112    /// pending-leave stewards are skipped in role display.
113    pub fn member_roles(&self) -> Result<Vec<(Vec<u8>, MemberRole)>, ConversationError> {
114        let mls = self.mls();
115        let epoch = mls.current_epoch()?;
116        let members = mls.members()?;
117
118        let eligible = self.queues.steward_eligibility(&members);
119        let (live_epoch, live_backup) = self
120            .services
121            .steward_list
122            .epoch_and_backup(epoch, &eligible);
123        let live_epoch = live_epoch.map(|s| s.to_vec());
124        let live_backup = live_backup.map(|s| s.to_vec());
125        let exhausted = self.services.steward_list.is_exhausted(epoch);
126        let has_list = self.services.steward_list.current_list().is_some();
127        let roles = members
128            .iter()
129            .cloned()
130            .map(|id| {
131                let role = if has_list && !exhausted {
132                    if live_epoch.as_deref().is_some_and(|es| es == id) {
133                        MemberRole::EpochSteward
134                    } else if live_backup.as_deref().is_some_and(|bs| bs == id) {
135                        MemberRole::BackupSteward
136                    } else if self.services.steward_list.is_steward(&id) {
137                        MemberRole::Steward
138                    } else {
139                        MemberRole::Member
140                    }
141                } else if has_list && self.services.steward_list.is_steward(&id) {
142                    MemberRole::Steward
143                } else {
144                    MemberRole::Member
145                };
146                (id, role)
147            })
148            .collect();
149        Ok(roles)
150    }
151
152    pub fn approved_proposals_for_current_epoch(&self) -> Vec<ConversationUpdateRequest> {
153        self.queues.approved_proposals().values().cloned().collect()
154    }
155}