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