de_mls/core/group_update_handle.rs
1//! Proposal lifecycle management for group membership changes.
2//!
3//! This module tracks proposals through their lifecycle:
4//!
5//! ```text
6//! ┌─────────────┐ vote ┌─────────────┐ commit ┌─────────────┐
7//! │ Voting │ ──────────► │ Approved │ ─────────► │ Archived │
8//! │ Proposals │ (consensus)│ Proposals │ (steward) │ (history) │
9//! └─────────────┘ └─────────────┘ └─────────────┘
10//! │
11//! │ (rejected)
12//! ▼
13//! ┌─────────┐
14//! │ Removed │
15//! └─────────┘
16//! ```
17//!
18//! # Proposal Flow
19//!
20//! 1. **Voting**: Proposal created via `add_voting_proposal()`, waiting for votes
21//! 2. **Approved**: Consensus reached, moved via `move_proposal_to_approved()`
22//! 3. **Committed**: Steward batches proposals, clears via `clear_approved_proposals()`
23//! 4. **Archived**: Past batches stored in `epoch_history` for UI display
24
25use std::collections::{HashMap, VecDeque};
26
27use crate::protos::de_mls::messages::v1::GroupUpdateRequest;
28
29/// Consensus proposal identifier (assigned by the consensus service).
30pub type ProposalId = u32;
31
32/// Maximum number of past epoch batches to retain for UI display.
33const MAX_EPOCH_HISTORY: usize = 10;
34
35/// Tracks proposals through voting, approval, and commit lifecycle.
36///
37/// This is the internal state container for proposal management.
38/// Use [`GroupHandle`](crate::core::GroupHandle) methods for access.
39#[derive(Clone, Debug, Default)]
40pub struct CurrentEpochProposals {
41 /// Proposals waiting for consensus voting.
42 /// Key: proposal_id from consensus service
43 approved_proposals: HashMap<ProposalId, GroupUpdateRequest>,
44
45 /// Proposals that passed consensus, waiting for steward to commit.
46 voting_proposals: HashMap<ProposalId, GroupUpdateRequest>,
47
48 /// History of committed proposal batches (most recent last).
49 /// Limited to `MAX_EPOCH_HISTORY` entries for memory efficiency.
50 epoch_history: VecDeque<HashMap<ProposalId, GroupUpdateRequest>>,
51}
52
53impl CurrentEpochProposals {
54 /// Create a new steward with empty proposal queues.
55 pub fn new() -> Self {
56 Self {
57 approved_proposals: HashMap::new(),
58 voting_proposals: HashMap::new(),
59 epoch_history: VecDeque::new(),
60 }
61 }
62
63 /// Add a proposal to the approved proposals queue.
64 pub fn add_proposal(&mut self, proposal_id: ProposalId, proposal: GroupUpdateRequest) {
65 self.approved_proposals.insert(proposal_id, proposal);
66 }
67
68 /// Get the count of approved proposals waiting for voting.
69 pub fn approved_proposals_count(&self) -> usize {
70 self.approved_proposals.len()
71 }
72
73 /// Get a copy of the approved proposals.
74 pub fn approved_proposals(&self) -> HashMap<ProposalId, GroupUpdateRequest> {
75 self.approved_proposals.clone()
76 }
77 /// Add a proposal to the voting proposals queue.
78 ///
79 /// # Arguments
80 /// * `proposal_id` - The proposal ID
81 /// * `proposal` - The group update request to add
82 pub fn add_voting_proposal(&mut self, proposal_id: ProposalId, proposal: GroupUpdateRequest) {
83 self.voting_proposals.insert(proposal_id, proposal);
84 }
85
86 pub fn remove_voting_proposal(&mut self, proposal_id: ProposalId) {
87 self.voting_proposals.remove(&proposal_id);
88 }
89
90 /// Clear the approved proposals, archiving them to epoch history.
91 pub fn clear_approved_proposals(&mut self) {
92 if !self.approved_proposals.is_empty() {
93 let snapshot = std::mem::take(&mut self.approved_proposals);
94 if self.epoch_history.len() >= MAX_EPOCH_HISTORY {
95 self.epoch_history.pop_front();
96 }
97 self.epoch_history.push_back(snapshot);
98 }
99 }
100
101 /// Get the epoch history (past batches of approved proposals, most recent last).
102 pub fn epoch_history(&self) -> &VecDeque<HashMap<ProposalId, GroupUpdateRequest>> {
103 &self.epoch_history
104 }
105
106 pub fn move_proposal_to_approved(&mut self, proposal_id: ProposalId) {
107 if let Some(proposal) = self.voting_proposals.remove(&proposal_id) {
108 self.approved_proposals.insert(proposal_id, proposal);
109 }
110 }
111
112 pub fn is_owner_of_proposal(&self, proposal_id: ProposalId) -> bool {
113 self.voting_proposals.contains_key(&proposal_id)
114 }
115}