Change:
vector contains any Update or Remove proposals, or if it's empty. Otherwise, the "path" value MAY be omitted. <del> If the "path" value is populated: Process the "path" value using the provisional ratchet tree and GroupContext, to generate the new ratchet tree and the "commit_secret": </del> <ins> If the "path" value is populated, validate it and apply it to the tree: If this is an external commit, assign the sender the leftmost blank leaf node in the new ratchet tree.  If there are no blank leaf nodes in the new ratchet tree, add a blank leaf to the right side of the new ratchet tree and assign it to the sender. </ins> Validate the LeafNode as specified in leaf-node-validation. The "leaf_node_source" field MUST be set to "commit".

Discussion:
via option (3)
Bringing up the issue mentioned in the comments to : this can be inconvenient for implementations because this way the update path can't be generated and encrypted at the same time. From security perspective, I think it can't break anything, since we include strictly more in the context. On the other hand, it's not clear to me what it buys.
I sympathize with the idea that this constraints applications. MLSpp does exactly what you're talking about, updating and encrypting at the same time. But I'm not really seeing why that refactoring matters all that much. It's not any more net work, aside from maybe computing a filtered direct path twice.
In , we removed the requirement for an external Commit to have an Add proposal, under the theory that the required already has a leaf node for the new joiner. When I went to implement this, however, I noticed some ambiguities. First, the Commit processing section doesn't mention when the new joiner is added. The obvious approach is to do the add as part of the processing, but that processing presumes that the sender is already in the tree (for example, there is no provision for extending the tree). Second, the UpdatePath for an external commit needs to be done in a specific order in order to avoid a circular dependency. The TreeKEM encapsulation done by the joiner uses the GroupContext a context parameter, including the hash of the tree being used. But the LeafNode in the has to have a parent hash in it, which can only be computed after the encapsulation operation. This circularity can be resolved, but only by preparing the UpdatePath with the following specific steps: Compute the path secrets Compute the parent hash and the new leaf node Compute the new tree hash Encrypt the path secrets to the rest of the group That rules out some implementations that compute path secrets and encrypt them in one traversal of the direct path (this is what MLSpp does now). The options here are basically: Re-add the Add proposal, thus having a throw-away temporary LeafNode for the joiner Specify that the UpdatePath must be computed in the specific order above To me, (2) seems like a pretty clear winner.

Actually, it seems like there's a simpler option here: Extend the definition of an ExternalInit proposal to say that part of how it is applied is to ensure there is a blank leaf in the tree, extending the tree if necessary. (This implies that ExternalInit must be done after any Remove.) The leftmost blank leaf is the sender's leaf for purposes of UpdatePath processing. The provisional tree used in encrypting path secrets is still the provisional tree, in the sense that all proposals have been applied. This tree has a blank leaf for the UpdatePath sender. If we clarify these points, I think everything else works unmodified.
I agree with the first point. Even if the Add is not explicitly present, the external commit effectively emulates an Add anyway. To the second point: The solution is only simpler if that's how you want to implement it . I haven't tried it, but if you have chosen a different order of implementation, i.e. perform all changes to the provisional tree (including the update path and parent hash), then compute the tree hash and then encrypt the path, you now have to go and compute an additional tree hash before applying the update path, then apply the update path and keep the partial tree hash around for later encryption/decryption. This was not a part of MLS that I have looked at from a provable security perspective, but I'm wondering what the consequences are from a security perspective if we're including a parent hash for an incomplete provisional tree.
It seems like the main question here is what tree is provided to TreeKEM encryptions of path secrets. I'll call this the "context tree" below. It sounds like you have in mind that the tree provided to TreeKEM has the UpdatePath applied as well as the proposals, but that's not what the spec says right now: The starting point for this issue is that the "provisional" tree right now doesn't really faithfully capture the tree you're encrypting to, at least in the sense that it has a different number of leaves. That seemed wrong to me. It seems like we have the following options here: Do nothing. The context tree doesn't necessarily have a blank for the joiner.

Maximal divergence between the new tree and the context tree (among these options) TreeKEM encryption operates on a different tree than the context tree Ensure the provisional tree has a blank leaf for the joiner (as up-thread). Context tree has a blank leaf for the joiner, but direct path doesn't match new tree TreeKEM encryption can operate on the context tree, because the structure is correct Use the "new" ratchet tree as the TreeKEM context. Context tree is exactly the new tree -- could probably eliminate provisional Requires separate steps for (1) updating tree nodes and (2) encrypting path secrets ... when these are currently described together as effectively one operation Personally, it seems like the answer is (2) or (3), and I probably lean toward (3). I don't think (1) is insecure, because the context tree does capture all of the nodes to which path secrets are encrypted. But the divergence is worrisome and it's cheap to do better. Between (2) and (3), there's a trade-off between fidelity and spec change size. The impact of (2) is more localized (just the ExternalInit section). Updating the spec to do (3) will require amending the description of update paths as well as the Commit creation / processing rules (to separate update/encrypt and remove provisional, respectively). But honestly, removing the provisional tree seems like a worthwhile simplification, so between that and the greater fidelity of the context tree, (3) seems worth the cost.
Actually, as I look into implementing this: We still need a provisional GroupContext, because the will differ. But we can at least have two trees instead of three.