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 }