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 25 use std::collections::{HashMap, VecDeque}; 26 27 use crate::protos::de_mls::messages::v1::GroupUpdateRequest; 28 29 /// Consensus proposal identifier (assigned by the consensus service). 30 pub type ProposalId = u32; 31 32 /// Maximum number of past epoch batches to retain. 33 /// 34 /// TODO(M1): RFC §"Creating Voting Proposal" requires retaining finalized proposals 35 /// for at least `threshold_duration`. This count-based cap is a simplification; 36 /// replace with time-based expiry keyed on `GroupConfig::epoch_duration` when 37 /// peer scoring (M1) introduces `threshold_duration` as a first-class config value. 38 const MAX_EPOCH_HISTORY: usize = 10; 39 40 /// Tracks proposals through voting, approval, and commit lifecycle. 41 /// 42 /// This is the internal state container for proposal management. 43 /// Use [`GroupHandle`](crate::core::GroupHandle) methods for access. 44 #[derive(Clone, Debug, Default)] 45 pub struct CurrentEpochProposals { 46 /// Proposals waiting for consensus voting. 47 /// Key: proposal_id from consensus service 48 approved_proposals: HashMap<ProposalId, GroupUpdateRequest>, 49 50 /// Proposals that passed consensus, waiting for steward to commit. 51 voting_proposals: HashMap<ProposalId, GroupUpdateRequest>, 52 53 /// History of committed proposal batches (most recent last). 54 /// Limited to `MAX_EPOCH_HISTORY` entries for memory efficiency. 55 epoch_history: VecDeque<HashMap<ProposalId, GroupUpdateRequest>>, 56 } 57 58 impl CurrentEpochProposals { 59 /// Create a new steward with empty proposal queues. 60 pub fn new() -> Self { 61 Self { 62 approved_proposals: HashMap::new(), 63 voting_proposals: HashMap::new(), 64 epoch_history: VecDeque::new(), 65 } 66 } 67 68 /// Add a proposal to the approved proposals queue. 69 pub fn add_proposal(&mut self, proposal_id: ProposalId, proposal: GroupUpdateRequest) { 70 self.approved_proposals.insert(proposal_id, proposal); 71 } 72 73 /// Get the count of approved proposals waiting for voting. 74 pub fn approved_proposals_count(&self) -> usize { 75 self.approved_proposals.len() 76 } 77 78 /// Get a copy of the approved proposals. 79 pub fn approved_proposals(&self) -> HashMap<ProposalId, GroupUpdateRequest> { 80 self.approved_proposals.clone() 81 } 82 /// Add a proposal to the voting proposals queue. 83 /// 84 /// # Arguments 85 /// * `proposal_id` - The proposal ID 86 /// * `proposal` - The group update request to add 87 pub fn add_voting_proposal(&mut self, proposal_id: ProposalId, proposal: GroupUpdateRequest) { 88 self.voting_proposals.insert(proposal_id, proposal); 89 } 90 91 pub fn remove_voting_proposal(&mut self, proposal_id: ProposalId) { 92 self.voting_proposals.remove(&proposal_id); 93 } 94 95 /// Clear all voting proposals (used on freeze timeout with no candidate). 96 pub fn clear_voting_proposals(&mut self) { 97 self.voting_proposals.clear(); 98 } 99 100 /// Clear the approved proposals, archiving them to epoch history. 101 pub fn clear_approved_proposals(&mut self) { 102 if !self.approved_proposals.is_empty() { 103 let snapshot = std::mem::take(&mut self.approved_proposals); 104 if self.epoch_history.len() >= MAX_EPOCH_HISTORY { 105 self.epoch_history.pop_front(); 106 } 107 self.epoch_history.push_back(snapshot); 108 } 109 } 110 111 /// Get the epoch history (past batches of approved proposals, most recent last). 112 pub fn epoch_history(&self) -> &VecDeque<HashMap<ProposalId, GroupUpdateRequest>> { 113 &self.epoch_history 114 } 115 116 /// Remove a single proposal from the approved queue. 117 /// 118 /// Used for proposals that don't produce MLS operations (e.g., emergency criteria). 119 pub fn remove_approved_proposal(&mut self, proposal_id: ProposalId) { 120 self.approved_proposals.remove(&proposal_id); 121 } 122 123 pub fn has_approved_proposal(&self, proposal_id: ProposalId) -> bool { 124 self.approved_proposals.contains_key(&proposal_id) 125 } 126 127 pub fn move_proposal_to_approved(&mut self, proposal_id: ProposalId) { 128 if let Some(proposal) = self.voting_proposals.remove(&proposal_id) { 129 self.approved_proposals.insert(proposal_id, proposal); 130 } 131 } 132 133 pub fn is_owner_of_proposal(&self, proposal_id: ProposalId) -> bool { 134 self.voting_proposals.contains_key(&proposal_id) 135 } 136 }