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 KeyPackages 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.