/ src / core / types.rs
types.rs
  1  //! Core types for group operations.
  2  //!
  3  //! This module defines the key data types used throughout the DE-MLS core:
  4  //!
  5  //! - [`ProcessResult`] - Outcome of processing an inbound message
  6  //! - Various `From` implementations for protobuf message conversions
  7  
  8  use hashgraph_like_consensus::{
  9      protos::consensus::v1::{Proposal, Vote},
 10      types::ConsensusEvent,
 11  };
 12  
 13  use crate::{
 14      core::CoreError,
 15      mls_crypto::parse_wallet_to_bytes,
 16      protos::de_mls::messages::v1::{
 17          AppMessage, BanRequest, CommitCandidate, ConversationMessage, GroupUpdateRequest,
 18          InvitationToJoin, Outcome, ProposalAdded, RemoveMember, UserKeyPackage, UserVote,
 19          ViolationEvidence, VotePayload, WelcomeMessage, app_message, group_update_request,
 20          welcome_message,
 21      },
 22  };
 23  
 24  /// Result of processing an inbound packet.
 25  ///
 26  /// This enum represents all possible outcomes from [`process_inbound`](super::process_inbound).
 27  /// Match it directly in your application layer to handle each variant.
 28  ///
 29  /// # Variants
 30  ///
 31  /// - `AppMessage` - A chat message or other application-level message
 32  /// - `Proposal` / `Vote` - Consensus messages that need forwarding
 33  /// - `GetUpdateRequest` - Steward received a membership change request
 34  /// - `JoinedGroup` - Successfully joined via welcome message
 35  /// - `GroupUpdated` - MLS state changed (batch commit applied)
 36  /// - `LeaveGroup` - User was removed from the group
 37  /// - `ViolationDetected` - Steward violation detected during commit validation
 38  /// - `Noop` - Nothing to do (message not for us, already processed, etc.)
 39  #[derive(Debug, Clone)]
 40  pub enum ProcessResult {
 41      /// An application message was received (chat message, etc.).
 42      ///
 43      /// The message has been decrypted and is ready for display.
 44      AppMessage(AppMessage),
 45  
 46      /// A consensus proposal was received from another peer.
 47      ///
 48      /// Should be forwarded to the consensus service via
 49      /// `crate::app::forward_incoming_proposal`.
 50      Proposal(Proposal),
 51  
 52      /// A consensus vote was received from another peer.
 53      ///
 54      /// Should be forwarded to the consensus service via
 55      /// `crate::app::forward_incoming_vote`.
 56      Vote(Vote),
 57  
 58      /// The user was removed from the group.
 59      ///
 60      /// Application should clean up group state and notify the UI.
 61      LeaveGroup,
 62  
 63      /// Steward received a membership change request (key package or ban).
 64      ///
 65      /// Application should start a consensus vote for this request.
 66      GetUpdateRequest(GroupUpdateRequest),
 67  
 68      /// The user successfully joined a group via welcome message.
 69      ///
 70      /// Contains the group name. Application should transition state
 71      /// from PendingJoin to Working.
 72      JoinedGroup(String),
 73  
 74      /// Group MLS state was updated (batch commit applied).
 75      ///
 76      /// Application should transition state back to Working.
 77      GroupUpdated,
 78  
 79      /// A steward violation was detected during commit validation.
 80      ///
 81      /// Contains evidence of the violation. The application should start
 82      /// an emergency criteria proposal vote for this evidence.
 83      ViolationDetected(ViolationEvidence),
 84  
 85      /// A remote commit candidate was successfully buffered in the freeze round.
 86      CandidateBuffered,
 87  
 88      /// No action needed.
 89      ///
 90      /// The message was not for us, was a duplicate, or required no action.
 91      Noop,
 92  }
 93  
 94  // ── ViolationEvidence constructors ────────────────────────────────
 95  
 96  use crate::protos::de_mls::messages::v1::ViolationType;
 97  
 98  impl ViolationEvidence {
 99      /// Steward included different proposal IDs than what was voted on,
100      /// or IDs match but content digest differs.
101      pub fn broken_commit(target: Vec<u8>, epoch: u64, payload: impl Into<Vec<u8>>) -> Self {
102          Self {
103              violation_type: ViolationType::BrokenCommit as i32,
104              target_member_id: target,
105              evidence_payload: payload.into(),
106              epoch,
107          }
108      }
109  
110      /// MLS payload count doesn't match proposal count,
111      /// or an MLS proposal failed to decrypt/store correctly.
112      pub fn broken_mls_proposal(target: Vec<u8>, epoch: u64, payload: impl Into<Vec<u8>>) -> Self {
113          Self {
114              violation_type: ViolationType::BrokenMlsProposal as i32,
115              target_member_id: target,
116              evidence_payload: payload.into(),
117              epoch,
118          }
119      }
120  
121      /// Steward didn't commit within the threshold duration.
122      pub fn censorship_inactivity(target: Vec<u8>, epoch: u64) -> Self {
123          Self {
124              violation_type: ViolationType::CensorshipInactivity as i32,
125              target_member_id: target,
126              evidence_payload: Vec::new(),
127              epoch,
128          }
129      }
130  }
131  
132  // WELCOME MESSAGE SUBTOPIC
133  
134  pub fn invitation_from_bytes(mls_bytes: Vec<u8>) -> WelcomeMessage {
135      let invitation = InvitationToJoin {
136          mls_message_out_bytes: mls_bytes,
137      };
138  
139      WelcomeMessage {
140          payload: Some(welcome_message::Payload::InvitationToJoin(invitation)),
141      }
142  }
143  
144  impl From<UserKeyPackage> for WelcomeMessage {
145      fn from(user_key_package: UserKeyPackage) -> Self {
146          WelcomeMessage {
147              payload: Some(welcome_message::Payload::UserKeyPackage(user_key_package)),
148          }
149      }
150  }
151  
152  // APPLICATION MESSAGE SUBTOPIC
153  
154  impl From<VotePayload> for AppMessage {
155      fn from(vote_payload: VotePayload) -> Self {
156          AppMessage {
157              payload: Some(app_message::Payload::VotePayload(vote_payload)),
158          }
159      }
160  }
161  
162  impl From<UserVote> for AppMessage {
163      fn from(user_vote: UserVote) -> Self {
164          AppMessage {
165              payload: Some(app_message::Payload::UserVote(user_vote)),
166          }
167      }
168  }
169  
170  impl From<ConversationMessage> for AppMessage {
171      fn from(conversation_message: ConversationMessage) -> Self {
172          AppMessage {
173              payload: Some(app_message::Payload::ConversationMessage(
174                  conversation_message,
175              )),
176          }
177      }
178  }
179  
180  impl From<CommitCandidate> for AppMessage {
181      fn from(commit_candidate: CommitCandidate) -> Self {
182          AppMessage {
183              payload: Some(app_message::Payload::CommitCandidate(commit_candidate)),
184          }
185      }
186  }
187  
188  impl From<BanRequest> for AppMessage {
189      fn from(ban_request: BanRequest) -> Self {
190          AppMessage {
191              payload: Some(app_message::Payload::BanRequest(ban_request)),
192          }
193      }
194  }
195  
196  impl From<Proposal> for AppMessage {
197      fn from(proposal: Proposal) -> Self {
198          AppMessage {
199              payload: Some(app_message::Payload::Proposal(proposal)),
200          }
201      }
202  }
203  
204  impl From<Vote> for AppMessage {
205      fn from(vote: Vote) -> Self {
206          AppMessage {
207              payload: Some(app_message::Payload::Vote(vote)),
208          }
209      }
210  }
211  
212  impl From<ProposalAdded> for AppMessage {
213      fn from(proposal_added: ProposalAdded) -> Self {
214          AppMessage {
215              payload: Some(app_message::Payload::ProposalAdded(proposal_added)),
216          }
217      }
218  }
219  
220  impl From<ConsensusEvent> for Outcome {
221      fn from(consensus_event: ConsensusEvent) -> Self {
222          match consensus_event {
223              ConsensusEvent::ConsensusReached {
224                  proposal_id: _,
225                  result: true,
226                  timestamp: _,
227              } => Outcome::Accepted,
228              ConsensusEvent::ConsensusReached {
229                  proposal_id: _,
230                  result: false,
231                  timestamp: _,
232              } => Outcome::Rejected,
233              ConsensusEvent::ConsensusFailed {
234                  proposal_id: _,
235                  timestamp: _,
236              } => Outcome::Unspecified,
237          }
238      }
239  }
240  
241  impl TryFrom<AppMessage> for ProcessResult {
242      type Error = CoreError;
243      fn try_from(value: AppMessage) -> Result<Self, Self::Error> {
244          match &value.payload {
245              Some(app_message::Payload::ConversationMessage(_)) => {
246                  Ok(ProcessResult::AppMessage(value))
247              }
248              Some(app_message::Payload::Proposal(proposal)) => {
249                  Ok(ProcessResult::Proposal(proposal.clone()))
250              }
251              Some(app_message::Payload::Vote(vote)) => Ok(ProcessResult::Vote(vote.clone())),
252              Some(app_message::Payload::BanRequest(ban_request)) => {
253                  Ok(ProcessResult::GetUpdateRequest(GroupUpdateRequest {
254                      payload: Some(group_update_request::Payload::RemoveMember(RemoveMember {
255                          identity: parse_wallet_to_bytes(ban_request.user_to_ban.as_str())?,
256                      })),
257                  }))
258              }
259              _ => Ok(ProcessResult::Noop),
260          }
261      }
262  }