validation.rs
1 use super::*; 2 3 // ─────────────────────────── Batch Validation ─────────────────────────── 4 5 /// Validate MLS actions from a commit against local voted proposals. 6 /// 7 /// Returns `Some(ProcessResult::ViolationDetected(...))` if a violation is found, 8 /// `None` if all checks pass. 9 pub(crate) fn validate_commit_candidate( 10 handle: &GroupHandle, 11 local_proposals: &HashMap<ProposalId, GroupUpdateRequest>, 12 sender_id: &[u8], 13 mls_actions: &[MlsProposalAction], 14 ) -> Result<Option<ProcessResult>, CoreError> { 15 let group_name = handle.group_name(); 16 17 let mut expected_actions: Vec<MlsProposalAction> = local_proposals 18 .values() 19 .filter_map(expected_action_for_request) 20 .collect(); 21 let mut actual_actions = mls_actions.to_vec(); 22 23 expected_actions.sort(); 24 actual_actions.sort(); 25 26 if actual_actions != expected_actions { 27 tracing::warn!( 28 "Violation: broken MLS proposal for group {} — \ 29 MLS actions {:?} don't match voted {:?}", 30 group_name, 31 actual_actions, 32 expected_actions, 33 ); 34 return Ok(Some(ProcessResult::ViolationDetected( 35 ViolationEvidence::broken_mls_proposal( 36 sender_id.to_vec(), 37 handle.current_epoch(), 38 format!("MLS actions {actual_actions:?} != voted {expected_actions:?}"), 39 ), 40 ))); 41 } 42 43 Ok(None) 44 } 45 46 /// Derive the expected [`MlsProposalAction`] from a voted [`GroupUpdateRequest`]. 47 fn expected_action_for_request(req: &GroupUpdateRequest) -> Option<MlsProposalAction> { 48 use crate::protos::de_mls::messages::v1::group_update_request::Payload; 49 match &req.payload { 50 Some(Payload::InviteMember(im)) => Some(MlsProposalAction::Add(im.identity.clone())), 51 Some(Payload::RemoveMember(rm)) => Some(MlsProposalAction::Remove(rm.identity.clone())), 52 // Emergency criteria proposals don't produce MLS proposals 53 Some(Payload::EmergencyCriteria(_)) | None => None, 54 } 55 }