use hashgraph_like_consensus::protos::consensus::v1::Proposal;
use prost::Message;
use tracing::{info, warn};
use crate::{
core::{
conversation::Conversation,
error::CoreError,
freeze::buffer_commit_candidate,
process_result::{NoopReason, ProcessResult},
},
mls_crypto::{DecryptResult, MlsService},
protos::de_mls::messages::v1::{
AppMessage, ConversationUpdateRequest, app_message, conversation_update_request,
},
};
fn authorize_fast_path_proposal(proposal: &Proposal, mls_sender: &[u8]) -> bool {
if proposal.expected_voters_count != 1 {
return true;
}
if proposal.proposal_owner != mls_sender {
return false;
}
let Ok(request) = ConversationUpdateRequest::decode(proposal.payload.as_slice()) else {
return false;
};
matches!(
request.payload,
Some(conversation_update_request::Payload::RemoveMember(ref r)) if r.identity == mls_sender
)
}
pub fn process_inbound<M: MlsService>(
conversation: &mut Conversation,
mls: &mut M,
payload: &[u8],
) -> Result<ProcessResult, CoreError> {
if let Ok(app_message) = AppMessage::decode(payload) {
if let Some(app_message::Payload::CommitCandidate(candidate)) = app_message.payload {
return buffer_commit_candidate(conversation, mls, candidate);
}
}
let res = mls.decrypt_application_only(payload)?;
match res {
DecryptResult::Application(app_bytes, sender) => {
let app_msg = AppMessage::decode(app_bytes.as_ref())?;
if let Some(app_message::Payload::Proposal(proposal)) = &app_msg.payload
&& !authorize_fast_path_proposal(proposal, &sender)
{
warn!(
conversation = conversation.name(),
proposal_id = proposal.proposal_id,
sender = ?sender,
owner = ?proposal.proposal_owner,
"fast-path proposal rejected: sender is not the self-removal target"
);
return Ok(ProcessResult::Noop(NoopReason::FastPathRejected));
}
if let Some(app_message::Payload::BanRequest(ban)) = &app_msg.payload
&& !mls.is_member(&ban.user_to_ban)
{
info!(
conversation = conversation.name(),
target = ?ban.user_to_ban,
"ban request skipped: target not a member"
);
return Ok(ProcessResult::Noop(NoopReason::BanTargetNotMember));
}
app_msg.try_into()
}
DecryptResult::Removed(_) => Ok(ProcessResult::LeaveConversation),
DecryptResult::Ignored => {
tracing::debug!(
conversation = conversation.name(),
"app message ignored (wrong epoch/conversation)"
);
Ok(ProcessResult::Noop(NoopReason::DecryptIgnored))
}
_ => {
warn!(
conversation = conversation.name(),
"unexpected MLS message type on app subtopic"
);
Ok(ProcessResult::Noop(NoopReason::UnexpectedMlsType))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::conversation::self_leave_proposal_id;
use crate::protos::de_mls::messages::v1::RemoveMember;
fn member(id: u8) -> Vec<u8> {
vec![id; 20]
}
fn remove_payload(identity: &[u8]) -> Vec<u8> {
ConversationUpdateRequest {
payload: Some(conversation_update_request::Payload::RemoveMember(
RemoveMember {
identity: identity.to_vec(),
},
)),
}
.encode_to_vec()
}
fn proposal_for_self_remove(sender: &[u8], expected_voters: u32) -> Proposal {
Proposal {
name: "test".into(),
payload: remove_payload(sender),
proposal_id: self_leave_proposal_id(sender),
proposal_owner: sender.to_vec(),
votes: Vec::new(),
expected_voters_count: expected_voters,
round: 1,
timestamp: 0,
expiration_timestamp: u64::MAX,
liveness_criteria_yes: true,
}
}
#[test]
fn fast_path_allows_self_removal_matching_sender() {
let sender = member(1);
let proposal = proposal_for_self_remove(&sender, 1);
assert!(authorize_fast_path_proposal(&proposal, &sender));
}
#[test]
fn fast_path_rejects_target_other_than_sender() {
let sender = member(1);
let victim = member(2);
let mut proposal = proposal_for_self_remove(&victim, 1);
proposal.proposal_owner = sender.clone();
assert!(!authorize_fast_path_proposal(&proposal, &sender));
}
#[test]
fn fast_path_rejects_owner_mismatch() {
let sender = member(1);
let imposter = member(3);
let mut proposal = proposal_for_self_remove(&sender, 1);
proposal.proposal_owner = imposter;
assert!(!authorize_fast_path_proposal(&proposal, &sender));
}
#[test]
fn fast_path_rejects_non_remove_payload() {
let sender = member(1);
let mut proposal = proposal_for_self_remove(&sender, 1);
proposal.payload = vec![0xff; 8]; assert!(!authorize_fast_path_proposal(&proposal, &sender));
}
#[test]
fn expected_voters_gt_one_bypasses_authz() {
let sender = member(1);
let victim = member(2);
let mut proposal = proposal_for_self_remove(&victim, 5);
proposal.proposal_owner = sender.clone();
assert!(authorize_fast_path_proposal(&proposal, &sender));
}
}