/ src / core / group_update_handle.rs
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  }