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) = charlie_group
        .remove_members(backend, &[bob_kp_ref])
        .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 members of the group. Despite the fact that members were only removed in this operation, the Commit message could potentially also cover Add Proposals that were previously received in the epoch. Therefore the function can also optionally return a Welcome message. The Welcome message needs to 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 = alice_group
        .propose_remove_member(
            backend,
            charlie_group
                .key_package_ref()
                .expect("An unexpected error occurred."),
        )
        .expect("Could not create proposal to remove Charlie.");

In this case the 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 ProcessedMessage::StagedCommitMessage(staged_commit) = bob_processed_message {
        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_old_kpr));
            }
            RemoveOperation::TheyLeft(_) => unreachable!(),
            RemoveOperation::TheyWereRemovedBy(_) => unreachable!(),
            RemoveOperation::WeRemovedThem(_) => unreachable!(),
        }

        // Merge staged Commit
        bob_group
            .merge_staged_commit(*staged_commit)
            .expect("Could not merge 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();
    assert_eq!(members.len(), 2);
    assert_eq!(members[0].credential().identity(), b"Alice");
    assert_eq!(members[1].credential().identity(), b"Charlie");