Removing members from a group
Immediate operation
Members can be removed from the group atomically with the .remove_members()
function, which takes the KeyPackageRef
of group member as input. References to the KeyPackage
s of group members can be obtained using the .members()
function, from which one can in turn compute the KeyPackageRef
using their .hash_ref()
function.
let (mls_message_out, welcome_option, _group_info) = charlie_group
.remove_members(provider, &charlie_signature_keys, &[bob_member.index])
.expect("Could not remove Bob from group.");
The function returns the tuple (MlsMessageOut, Option<Welcome>)
. The MlsMessageOut
contains a Commit message that needs to be fanned out to existing group members.
Even though members were removed in this operation, the Commit message could potentially also cover Add Proposals previously received in the epoch. Therefore the function can also optionally return a Welcome
message. The Welcome
message must be sent to the newly added members.
Proposal
Members can also be removed as a proposal (without the corresponding Commit message) by using the .propose_remove_member()
function:
let (mls_message_out, _proposal_ref) = alice_group
.propose_remove_member(
provider,
&alice_signature_keys,
charlie_group.own_leaf_index(),
)
.expect("Could not create proposal to remove Charlie.");
In this case, the function returns an MlsMessageOut
that needs to be fanned out to existing group members.
Getting removed from a group
A member is removed from a group if another member commits to a remove proposal targeting the member's leaf. Once the to-be-removed member merges that commit via merge_staged_commit()
, all other proposals in that commit will still be applied, but the group will be marked as inactive afterward. The group remains usable, e.g., to examine the membership list after the final commit was processed, but it won't be possible to create or process new messages.
if let ProcessedMessageContent::StagedCommitMessage(staged_commit) =
bob_processed_message.into_content()
{
let remove_proposal = staged_commit
.remove_proposals()
.next()
.expect("An unexpected error occurred.");
// We construct a RemoveOperation enum to help us interpret the remove operation
let remove_operation = RemoveOperation::new(remove_proposal, &bob_group)
.expect("An unexpected Error occurred.");
match remove_operation {
RemoveOperation::WeLeft => unreachable!(),
// We expect this variant, since Bob was removed by Charlie
RemoveOperation::WeWereRemovedBy(member) => {
assert!(matches!(member, Sender::Member(member) if member == charlies_leaf_index));
}
RemoveOperation::TheyLeft(_) => unreachable!(),
RemoveOperation::TheyWereRemovedBy(_) => unreachable!(),
RemoveOperation::WeRemovedThem(_) => unreachable!(),
}
// Merge staged Commit
bob_group
.merge_staged_commit(provider, *staged_commit)
.expect("Error merging staged commit.");
} else {
unreachable!("Expected a StagedCommit.");
}
// Check we didn't receive a Welcome message
assert!(welcome_option.is_none());
// Check that Bob's group is no longer active
assert!(!bob_group.is_active());
let members = bob_group.members().collect::<Vec<Member>>();
assert_eq!(members.len(), 2);
let credential0 = members[0].credential.serialized_content();
let credential1 = members[1].credential.serialized_content();
assert_eq!(credential0, b"Alice");
assert_eq!(credential1, b"Charlie");
External Proposal
Parties outside the group can also make proposals to remove members as long as they are registered as part of the ExternalSendersExtension
extension.
Since those proposals are crafted by outsiders, they are always public messages.
let proposal = ExternalProposal::new_remove::<Provider>(
bob_index,
alice_group.group_id().clone(),
alice_group.epoch(),
&ds_signature_keys,
SenderExtensionIndex::new(0),
)
.expect("Could not create external Remove proposal");
It is then up to one of the group members to process the proposal and commit it.