1use std::error::Error as StdError;
9
10use hashgraph_like_consensus::{error::ConsensusError, storage::ConsensusStorage};
11use openmls_traits::signatures::Signer;
12use openmls_traits::{OpenMlsProvider, storage::StorageProvider};
13use tracing::info;
14
15use crate::{
16 ConsensusPlugin, Conversation, ConversationError, ConversationEvent, ConversationState,
17 PeerScoringPlugin, ProposalKind, StewardListPlugin, SyncConsensusReceiver,
18 consensus::bridge::{ProposalParams, cast_vote, submit_proposal, submit_self_leave_proposal},
19 mls_crypto::MlsService,
20 protos::de_mls::messages::v1::{AppMessage, ConversationUpdateRequest},
21 self_leave_proposal_id,
22};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum CreatorVote {
27 Yes,
32 Deferred,
37}
38
39impl<C, Sc, St> Conversation<C, Sc, St>
40where
41 C: ConsensusPlugin,
42 Sc: PeerScoringPlugin,
43 St: StewardListPlugin,
44{
45 pub fn initiate_proposal<Pr>(
59 &mut self,
60 provider: &Pr,
61 request: ConversationUpdateRequest,
62 creator_vote: CreatorVote,
63 signer: &impl Signer,
64 ) -> Result<(), ConversationError>
65 where
66 Pr: OpenMlsProvider,
67 <Pr::StorageProvider as StorageProvider<1>>::Error: StdError + Send + Sync + 'static,
68 {
69 let kind = ProposalKind::of(&request);
70 let expected_voters = self.check_proposal_allowed(kind)?;
71
72 let liveness_criteria_yes = self.config.liveness_criteria_yes;
73 let consensus_timeout = self.config.consensus_timeout;
74 let voting_delay = self.config.voting_delay_for(kind);
75
76 let (proposal_id, unbundled) = submit_proposal::<C>(
77 &self.conversation_id,
78 &request,
79 &self.self_member_id,
80 &self.services.consensus,
81 ProposalParams {
82 expected_voters,
83 proposal_expiration: self.config.proposal_expiration,
84 consensus_timeout,
85 liveness_criteria_yes,
86 },
87 )?;
88
89 self.queues
90 .insert_voting_proposal(proposal_id, request.clone());
91 if kind.is_emergency() {
92 self.queues.insert_emergency(proposal_id);
93 }
94 self.register_consensus_timeout(proposal_id, consensus_timeout);
97
98 match creator_vote {
99 CreatorVote::Yes => {
100 let scope = C::Scope::from(self.conversation_id.clone());
104 let proposal = self.services.consensus.cast_vote_and_get_proposal(
105 &scope,
106 proposal_id,
107 true,
108 )?;
109 info!(
110 conversation = %self.conversation_id,
111 proposal_id,
112 actor = "owner",
113 "YES vote cast (bundled at submit)"
114 );
115 let outbound: AppMessage = proposal.into();
116 let payload = self.mls_mut().build_message(provider, signer, &outbound)?;
117 self.broadcast(payload);
118 self.emit_event(ConversationEvent::OwnProposalSubmitted {
119 proposal_id,
120 request,
121 });
122 }
123 CreatorVote::Deferred => {
124 let payload = self.mls_mut().build_message(provider, signer, &unbundled)?;
125 self.broadcast(payload);
126 self.emit_event(ConversationEvent::VoteRequested {
127 proposal_id,
128 request,
129 });
130 self.register_auto_vote(proposal_id, voting_delay, liveness_criteria_yes);
131 }
132 }
133
134 Ok(())
135 }
136
137 pub fn vote<Pr>(
142 &mut self,
143 provider: &Pr,
144 proposal_id: u32,
145 vote: bool,
146 signer: &impl Signer,
147 ) -> Result<(), ConversationError>
148 where
149 Pr: OpenMlsProvider,
150 <Pr::StorageProvider as StorageProvider<1>>::Error: StdError + Send + Sync + 'static,
151 {
152 let state = self.current_state();
153 if state == ConversationState::Freezing || state == ConversationState::Selection {
154 return Err(ConversationError::ConversationBlocked(state.to_string()));
155 }
156 self.cancel_auto_vote(proposal_id);
157 self.broadcast_vote(provider, proposal_id, vote, signer)
158 }
159
160 pub(crate) fn tick_deadlines<Pr>(&mut self, provider: &Pr, signer: &impl Signer)
164 where
165 Pr: OpenMlsProvider,
166 <Pr::StorageProvider as StorageProvider<1>>::Error: StdError + Send + Sync + 'static,
167 {
168 let now = std::time::Instant::now();
169 let auto_votes_due: Vec<(u32, bool)> = self
170 .timing
171 .pending_auto_votes
172 .iter()
173 .filter(|(_, e)| e.fire_at <= now)
174 .map(|(id, e)| (*id, e.vote))
175 .collect();
176 for (id, _) in &auto_votes_due {
177 self.timing.pending_auto_votes.remove(id);
178 }
179 let timeouts_due: Vec<u32> = self
180 .timing
181 .pending_consensus_timeouts
182 .iter()
183 .filter(|(_, fire_at)| **fire_at <= now)
184 .map(|(id, _)| *id)
185 .collect();
186 for id in &timeouts_due {
187 self.timing.pending_consensus_timeouts.remove(id);
188 }
189
190 for (proposal_id, vote) in auto_votes_due {
191 if let Err(e) = self.broadcast_vote(provider, proposal_id, vote, signer) {
192 tracing::debug!(
193 proposal_id,
194 error = %e,
195 "auto-vote skipped (already voted or session resolved)"
196 );
197 }
198 }
199 for proposal_id in timeouts_due {
200 self.resolve_on_timeout(proposal_id);
201 }
202
203 loop {
204 let Some((_scope, event)) =
207 <_ as SyncConsensusReceiver<_>>::try_recv(&mut self.services.consensus_rx)
208 else {
209 break;
210 };
211 if let Err(e) = self.apply_consensus_outcome(provider, event, signer) {
212 tracing::warn!(
213 conversation = %self.conversation_id,
214 error = %e,
215 "apply_consensus_outcome failed"
216 );
217 }
218 }
219 }
220
221 pub(crate) fn initiate_self_leave<Pr>(
231 &mut self,
232 provider: &Pr,
233 signer: &impl Signer,
234 ) -> Result<(), ConversationError>
235 where
236 Pr: OpenMlsProvider,
237 <Pr::StorageProvider as StorageProvider<1>>::Error: StdError + Send + Sync + 'static,
238 {
239 if self.queues.is_pending_self_leave(&self.self_member_id) {
240 info!(
241 conversation = %self.conversation_id,
242 "self-leave already in flight, ignoring duplicate"
243 );
244 return Ok(());
245 }
246
247 let request = ConversationUpdateRequest::remove_member(self.self_member_id.to_vec());
248 let proposal_id = self_leave_proposal_id(&self.self_member_id);
249
250 self.queues
254 .insert_voting_proposal(proposal_id, request.clone());
255
256 let submitted = submit_self_leave_proposal::<C>(
257 &self.conversation_id,
258 &self.self_member_id,
259 &self.services.consensus,
260 ProposalParams {
261 expected_voters: 1,
262 proposal_expiration: self.config.proposal_expiration,
263 consensus_timeout: self.config.consensus_timeout,
264 liveness_criteria_yes: true,
265 },
266 )?;
267
268 let Some((_proposal_id, app_msg)) = submitted else {
271 return Ok(());
272 };
273
274 let payload = self.mls_mut().build_message(provider, signer, &app_msg)?;
275 self.broadcast(payload);
276 Ok(())
277 }
278
279 fn check_proposal_allowed(&self, kind: ProposalKind) -> Result<u32, ConversationError> {
286 let state = self.current_state();
287
288 match state {
289 ConversationState::Reelection => {
290 if !kind.is_emergency() && !kind.is_steward_election() {
291 return Err(ConversationError::ConversationBlocked(state.to_string()));
292 }
293 if self.queues.partial_freeze_blocks(kind) {
294 return Err(ConversationError::PartialFreeze);
295 }
296 }
297 ConversationState::Freezing | ConversationState::Selection => {
298 return Err(ConversationError::ConversationBlocked(state.to_string()));
299 }
300 _ => {
301 if self.queues.partial_freeze_blocks(kind) {
302 return Err(ConversationError::PartialFreeze);
303 }
304 }
305 }
306
307 let members = self.mls().members()?;
308 Ok(members.len() as u32)
309 }
310
311 fn resolve_on_timeout(&self, proposal_id: u32) {
317 let scope = C::Scope::from(self.conversation_id.clone());
318 let still_active = self
319 .services
320 .consensus
321 .storage()
322 .get_active_proposals(&scope)
323 .map(|active| active.iter().any(|p| p.proposal_id == proposal_id))
324 .unwrap_or(false);
325 if !still_active {
326 return;
327 }
328 match self
329 .services
330 .consensus
331 .handle_consensus_timeout(&scope, proposal_id)
332 {
333 Ok(_) => {}
334 Err(ConsensusError::SessionNotFound) | Err(ConsensusError::SessionNotActive) => {
335 let resolved_locally = self.queues.is_consensus_outcome_applied(proposal_id);
336 if resolved_locally {
337 tracing::debug!(
338 conversation = %self.conversation_id,
339 proposal_id,
340 "timeout fired for already-resolved proposal: ignoring"
341 );
342 } else {
343 tracing::warn!(
344 conversation = %self.conversation_id,
345 proposal_id,
346 "timeout fired for unknown proposal id: no session and not in resolved cache"
347 );
348 }
349 }
350 Err(e) => {
351 info!(proposal_id, error = %e, "timeout resolution skipped");
352 }
353 }
354 }
355
356 fn broadcast_vote<Pr>(
360 &mut self,
361 provider: &Pr,
362 proposal_id: u32,
363 vote: bool,
364 signer: &impl Signer,
365 ) -> Result<(), ConversationError>
366 where
367 Pr: OpenMlsProvider,
368 <Pr::StorageProvider as StorageProvider<1>>::Error: StdError + Send + Sync + 'static,
369 {
370 let app_message = cast_vote::<C>(
371 &self.conversation_id,
372 proposal_id,
373 vote,
374 &self.services.consensus,
375 )?;
376 let payload = self
377 .mls_mut()
378 .build_message(provider, signer, &app_message)?;
379 self.broadcast(payload);
380 Ok(())
381 }
382}