core_crypto/mls/conversation/
renew.rs

1use core_crypto_keystore::entities::MlsEncryptionKeyPair;
2use openmls::prelude::{LeafNode, LeafNodeIndex, Proposal, QueuedProposal, Sender, StagedCommit};
3use openmls_traits::OpenMlsCryptoProvider;
4
5use mls_crypto_provider::MlsCryptoProvider;
6
7use super::{Error, Result};
8use crate::{
9    KeystoreError, RecursiveError,
10    prelude::{MlsConversation, MlsProposalBundle, Session},
11};
12
13/// Marker struct holding methods responsible for restoring (renewing) proposals (or pending commit)
14/// in case another commit has been accepted by the backend instead of ours
15pub(crate) struct Renew;
16
17impl Renew {
18    /// Renews proposals:
19    /// * in pending_proposals but not in valid commit
20    /// * in pending_commit but not in valid commit
21    ///
22    /// NB: we do not deal with partial commit (commit which do not contain all pending proposals)
23    /// because they cannot be created at the moment by core-crypto
24    ///
25    /// * `self_index` - own client [KeyPackageRef] in current MLS group
26    /// * `pending_proposals` - local pending proposals in group's proposal store
27    /// * `pending_commit` - local pending commit which is now invalid
28    /// * `valid_commit` - commit accepted by the backend which will now supersede our local pending commit
29    pub(crate) fn renew<'a>(
30        self_index: &LeafNodeIndex,
31        pending_proposals: impl Iterator<Item = QueuedProposal> + 'a,
32        pending_commit: Option<&'a StagedCommit>,
33        valid_commit: &'a StagedCommit,
34    ) -> (Vec<QueuedProposal>, bool) {
35        // indicates if we need to renew an update proposal.
36        // true only if we have an empty pending commit or the valid commit does not contain one of our update proposal
37        // otherwise, local orphan update proposal will be renewed regularly, without this flag
38        let mut needs_update = false;
39
40        let renewed_pending_proposals = if let Some(pending_commit) = pending_commit {
41            // present in pending commit but not in valid commit
42            let commit_proposals = pending_commit.queued_proposals().cloned().collect::<Vec<_>>();
43
44            // if our own pending commit is empty it means we were attempting to update
45            let empty_commit = commit_proposals.is_empty();
46
47            // does the valid commit contains one of our update proposal ?
48            let valid_commit_has_own_update_proposal = valid_commit.update_proposals().any(|p| match p.sender() {
49                Sender::Member(sender_index) => self_index == sender_index,
50                _ => false,
51            });
52
53            // do we need to renew the update or has it already been committed
54            needs_update = !valid_commit_has_own_update_proposal && empty_commit;
55
56            // local proposals present in local pending commit but not in valid commit
57            commit_proposals
58                .into_iter()
59                .filter_map(|p| Self::is_proposal_renewable(p, Some(valid_commit)))
60                .collect::<Vec<_>>()
61        } else {
62            // local pending proposals present locally but not in valid commit
63            pending_proposals
64                .filter_map(|p| Self::is_proposal_renewable(p, Some(valid_commit)))
65                .collect::<Vec<_>>()
66        };
67        (renewed_pending_proposals, needs_update)
68    }
69
70    /// A proposal has to be renewed if it is absent from supplied commit
71    fn is_proposal_renewable(proposal: QueuedProposal, commit: Option<&StagedCommit>) -> Option<QueuedProposal> {
72        if let Some(commit) = commit {
73            let in_commit = match proposal.proposal() {
74                Proposal::Add(add) => commit.add_proposals().any(|p| {
75                    let commits_identity = p.add_proposal().key_package().leaf_node().credential().identity();
76                    let proposal_identity = add.key_package().leaf_node().credential().identity();
77                    commits_identity == proposal_identity
78                }),
79                Proposal::Remove(remove) => commit
80                    .remove_proposals()
81                    .any(|p| p.remove_proposal().removed() == remove.removed()),
82                Proposal::Update(update) => commit
83                    .update_proposals()
84                    .any(|p| p.update_proposal().leaf_node() == update.leaf_node()),
85                _ => true,
86            };
87            if in_commit { None } else { Some(proposal) }
88        } else {
89            // if proposal is orphan (not present in commit)
90            Some(proposal)
91        }
92    }
93}
94
95impl MlsConversation {
96    /// Given the proposals to renew, actually restore them by using associated methods in [MlsGroup].
97    /// This will also add them to the local proposal store
98    pub(crate) async fn renew_proposals_for_current_epoch(
99        &mut self,
100        client: &Session,
101        backend: &MlsCryptoProvider,
102        proposals: impl Iterator<Item = QueuedProposal>,
103        needs_update: bool,
104    ) -> Result<Vec<MlsProposalBundle>> {
105        let mut bundle = vec![];
106        let is_external = |p: &QueuedProposal| matches!(p.sender(), Sender::External(_) | Sender::NewMemberProposal);
107        let proposals = proposals.filter(|p| !is_external(p));
108        for proposal in proposals {
109            let msg = match proposal.proposal {
110                Proposal::Add(add) => self.propose_add_member(client, backend, add.key_package.into()).await?,
111                Proposal::Remove(remove) => self.propose_remove_member(client, backend, remove.removed()).await?,
112                Proposal::Update(update) => self.renew_update(client, backend, Some(update.leaf_node())).await?,
113                _ => return Err(Error::ProposalVariantCannotBeRenewed),
114            };
115            bundle.push(msg);
116        }
117        if needs_update {
118            let proposal = self.renew_update(client, backend, None).await?;
119            bundle.push(proposal);
120        }
121        Ok(bundle)
122    }
123
124    /// Renews an update proposal by considering the explicit LeafNode supplied in the proposal
125    /// by applying it to the current own LeafNode.
126    /// At this point, we have already verified we are only operating on proposals created by self.
127    async fn renew_update(
128        &mut self,
129        client: &Session,
130        backend: &MlsCryptoProvider,
131        leaf_node: Option<&LeafNode>,
132    ) -> Result<MlsProposalBundle> {
133        if let Some(leaf_node) = leaf_node {
134            // Creating an update rekeys the LeafNode everytime. Hence we need to clear the previous
135            // encryption key from the keystore otherwise we would have a leak
136            backend
137                .key_store()
138                .remove::<MlsEncryptionKeyPair, _>(leaf_node.encryption_key().as_slice())
139                .await
140                .map_err(KeystoreError::wrap("removing mls encryption keypair"))?;
141        }
142
143        let mut leaf_node = leaf_node
144            .or_else(|| self.group.own_leaf())
145            .cloned()
146            .ok_or(Error::MlsGroupInvalidState("own_leaf is None"))?;
147
148        let sc = self.signature_scheme();
149        let ct = self.own_credential_type()?;
150        let cb = client
151            .find_most_recent_credential_bundle(sc, ct)
152            .await
153            .map_err(RecursiveError::mls_client("finding most recent credential bundle"))?;
154
155        leaf_node.set_credential_with_key(cb.to_mls_credential_with_key());
156
157        self.propose_explicit_self_update(client, backend, Some(leaf_node))
158            .await
159    }
160
161    pub(crate) fn self_pending_proposals(&self) -> impl Iterator<Item = &QueuedProposal> {
162        self.group
163            .pending_proposals()
164            .filter(|&p| matches!(p.sender(), Sender::Member(i) if i == &self.group.own_leaf_index()))
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use wasm_bindgen_test::*;
171
172    use crate::test_utils::*;
173
174    mod update {
175        use super::*;
176
177        #[apply(all_cred_cipher)]
178        #[wasm_bindgen_test]
179        pub async fn renewable_when_created_by_self(case: TestContext) {
180            run_test_with_client_ids(
181                case.clone(),
182                ["alice", "bob"],
183                move |[mut alice_central, bob_central]| {
184                    Box::pin(async move {
185                        let id = conversation_id();
186                        alice_central
187                            .transaction
188                            .new_conversation(&id, case.credential_type, case.cfg.clone())
189                            .await
190                            .unwrap();
191                        alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
192
193                        assert!(alice_central.pending_proposals(&id).await.is_empty());
194                        alice_central.transaction.new_update_proposal(&id).await.unwrap();
195                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
196
197                        // Bob hasn't Alice's proposal but creates a commit
198                        bob_central
199                            .transaction
200                            .conversation(&id)
201                            .await
202                            .unwrap()
203                            .update_key_material()
204                            .await
205                            .unwrap();
206                        let commit = bob_central.mls_transport.latest_commit().await;
207
208                        let proposals = alice_central
209                            .transaction
210                            .conversation(&id)
211                            .await
212                            .unwrap()
213                            .decrypt_message(commit.to_bytes().unwrap())
214                            .await
215                            .unwrap()
216                            .proposals;
217                        // Alice should renew the proposal because its hers
218                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
219                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
220
221                        // It should also renew the proposal when in pending_commit
222                        bob_central
223                            .transaction
224                            .conversation(&id)
225                            .await
226                            .unwrap()
227                            .update_key_material()
228                            .await
229                            .unwrap();
230                        let commit = bob_central.mls_transport.latest_commit().await;
231                        let proposals = alice_central
232                            .transaction
233                            .conversation(&id)
234                            .await
235                            .unwrap()
236                            .decrypt_message(commit.to_bytes().unwrap())
237                            .await
238                            .unwrap()
239                            .proposals;
240                        // Alice should renew the proposal because its hers
241                        // It should also replace existing one
242                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
243                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
244                    })
245                },
246            )
247            .await
248        }
249
250        #[apply(all_cred_cipher)]
251        #[wasm_bindgen_test]
252        pub async fn not_renewable_when_in_valid_commit(case: TestContext) {
253            run_test_with_client_ids(
254                case.clone(),
255                ["alice", "bob"],
256                move |[mut alice_central, bob_central]| {
257                    Box::pin(async move {
258                        let id = conversation_id();
259                        alice_central
260                            .transaction
261                            .new_conversation(&id, case.credential_type, case.cfg.clone())
262                            .await
263                            .unwrap();
264                        alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
265
266                        assert!(alice_central.pending_proposals(&id).await.is_empty());
267                        let proposal = alice_central
268                            .transaction
269                            .new_update_proposal(&id)
270                            .await
271                            .unwrap()
272                            .proposal;
273                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
274
275                        // Bob has Alice's update proposal
276                        bob_central
277                            .transaction
278                            .conversation(&id)
279                            .await
280                            .unwrap()
281                            .decrypt_message(proposal.to_bytes().unwrap())
282                            .await
283                            .unwrap();
284
285                        bob_central
286                            .transaction
287                            .conversation(&id)
288                            .await
289                            .unwrap()
290                            .update_key_material()
291                            .await
292                            .unwrap();
293                        let commit = bob_central.mls_transport.latest_commit().await;
294
295                        // Bob's commit has Alice's proposal
296                        let proposals = alice_central
297                            .transaction
298                            .conversation(&id)
299                            .await
300                            .unwrap()
301                            .decrypt_message(commit.to_bytes().unwrap())
302                            .await
303                            .unwrap()
304                            .proposals;
305                        // Alice proposal should not be renew as it was in valid commit
306                        assert!(alice_central.pending_proposals(&id).await.is_empty());
307                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
308
309                        let proposal = alice_central
310                            .transaction
311                            .new_update_proposal(&id)
312                            .await
313                            .unwrap()
314                            .proposal;
315                        bob_central
316                            .transaction
317                            .conversation(&id)
318                            .await
319                            .unwrap()
320                            .decrypt_message(proposal.to_bytes().unwrap())
321                            .await
322                            .unwrap();
323                        bob_central
324                            .transaction
325                            .conversation(&id)
326                            .await
327                            .unwrap()
328                            .update_key_material()
329                            .await
330                            .unwrap();
331                        let commit = bob_central.mls_transport.latest_commit().await;
332                        let proposals = alice_central
333                            .transaction
334                            .conversation(&id)
335                            .await
336                            .unwrap()
337                            .decrypt_message(commit.to_bytes().unwrap())
338                            .await
339                            .unwrap()
340                            .proposals;
341                        // Alice should not be renew as it was in valid commit
342                        assert!(alice_central.pending_proposals(&id).await.is_empty());
343                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
344                    })
345                },
346            )
347            .await
348        }
349
350        #[apply(all_cred_cipher)]
351        #[wasm_bindgen_test]
352        pub async fn not_renewable_by_ref(case: TestContext) {
353            run_test_with_client_ids(
354                case.clone(),
355                ["alice", "bob", "charlie"],
356                move |[mut alice_central, bob_central, charlie_central]| {
357                    Box::pin(async move {
358                        let id = conversation_id();
359                        alice_central
360                            .transaction
361                            .new_conversation(&id, case.credential_type, case.cfg.clone())
362                            .await
363                            .unwrap();
364                        alice_central
365                            .invite_all(&case, &id, [&bob_central, &charlie_central])
366                            .await
367                            .unwrap();
368
369                        let proposal = bob_central.transaction.new_update_proposal(&id).await.unwrap().proposal;
370                        assert!(alice_central.pending_proposals(&id).await.is_empty());
371                        alice_central
372                            .transaction
373                            .conversation(&id)
374                            .await
375                            .unwrap()
376                            .decrypt_message(proposal.to_bytes().unwrap())
377                            .await
378                            .unwrap();
379                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
380
381                        // Charlie does not have other proposals, it creates a commit
382                        charlie_central
383                            .transaction
384                            .conversation(&id)
385                            .await
386                            .unwrap()
387                            .update_key_material()
388                            .await
389                            .unwrap();
390                        let commit = charlie_central.mls_transport.latest_commit().await;
391                        let proposals = alice_central
392                            .transaction
393                            .conversation(&id)
394                            .await
395                            .unwrap()
396                            .decrypt_message(commit.to_bytes().unwrap())
397                            .await
398                            .unwrap()
399                            .proposals;
400                        // Alice should not renew Bob's update proposal
401                        assert!(alice_central.pending_proposals(&id).await.is_empty());
402                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
403                    })
404                },
405            )
406            .await
407        }
408    }
409
410    mod add {
411        use super::*;
412
413        #[apply(all_cred_cipher)]
414        #[wasm_bindgen_test]
415        pub async fn not_renewable_when_valid_commit_adds_same(case: TestContext) {
416            run_test_with_client_ids(
417                case.clone(),
418                ["alice", "bob", "charlie"],
419                move |[mut alice_central, bob_central, charlie_central]| {
420                    Box::pin(async move {
421                        let id = conversation_id();
422                        alice_central
423                            .transaction
424                            .new_conversation(&id, case.credential_type, case.cfg.clone())
425                            .await
426                            .unwrap();
427                        alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
428
429                        let charlie_kp = charlie_central.get_one_key_package(&case).await;
430                        assert!(alice_central.pending_proposals(&id).await.is_empty());
431                        alice_central
432                            .transaction
433                            .new_add_proposal(&id, charlie_kp)
434                            .await
435                            .unwrap();
436                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
437
438                        let charlie = charlie_central.rand_key_package(&case).await;
439                        bob_central
440                            .transaction
441                            .conversation(&id)
442                            .await
443                            .unwrap()
444                            .add_members(vec![charlie])
445                            .await
446                            .unwrap();
447                        let commit = bob_central.mls_transport.latest_commit().await;
448                        let proposals = alice_central
449                            .transaction
450                            .conversation(&id)
451                            .await
452                            .unwrap()
453                            .decrypt_message(commit.to_bytes().unwrap())
454                            .await
455                            .unwrap()
456                            .proposals;
457                        // Alice proposal is not renewed since she also wanted to add Charlie
458                        assert!(alice_central.pending_proposals(&id).await.is_empty());
459                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
460                    })
461                },
462            )
463            .await
464        }
465
466        #[apply(all_cred_cipher)]
467        #[wasm_bindgen_test]
468        pub async fn not_renewable_in_pending_commit_when_valid_commit_adds_same(case: TestContext) {
469            run_test_with_client_ids(
470                case.clone(),
471                ["alice", "bob", "charlie"],
472                move |[mut alice_central, bob_central, charlie_central]| {
473                    Box::pin(async move {
474                        let id = conversation_id();
475                        alice_central
476                            .transaction
477                            .new_conversation(&id, case.credential_type, case.cfg.clone())
478                            .await
479                            .unwrap();
480                        alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
481
482                        let charlie_kp = charlie_central.get_one_key_package(&case).await;
483                        assert!(alice_central.pending_proposals(&id).await.is_empty());
484                        alice_central
485                            .transaction
486                            .new_add_proposal(&id, charlie_kp)
487                            .await
488                            .unwrap();
489                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
490
491                        // Here Alice also creates a commit
492                        alice_central.commit_pending_proposals_unmerged(&id).await;
493                        assert!(alice_central.pending_commit(&id).await.is_some());
494
495                        let charlie = charlie_central.rand_key_package(&case).await;
496                        bob_central
497                            .transaction
498                            .conversation(&id)
499                            .await
500                            .unwrap()
501                            .add_members(vec![charlie])
502                            .await
503                            .unwrap();
504                        let commit = bob_central.mls_transport.latest_commit().await;
505                        let proposals = alice_central
506                            .transaction
507                            .conversation(&id)
508                            .await
509                            .unwrap()
510                            .decrypt_message(commit.to_bytes().unwrap())
511                            .await
512                            .unwrap()
513                            .proposals;
514                        // Alice proposal is not renewed since she also wanted to add Charlie
515                        assert!(alice_central.pending_proposals(&id).await.is_empty());
516                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
517                    })
518                },
519            )
520            .await
521        }
522
523        #[apply(all_cred_cipher)]
524        #[wasm_bindgen_test]
525        pub async fn not_renewable_by_ref(case: TestContext) {
526            run_test_with_client_ids(
527                case.clone(),
528                ["alice", "bob", "charlie", "debbie"],
529                move |[mut alice_central, bob_central, charlie_central, debbie_central]| {
530                    Box::pin(async move {
531                        let id = conversation_id();
532                        alice_central
533                            .transaction
534                            .new_conversation(&id, case.credential_type, case.cfg.clone())
535                            .await
536                            .unwrap();
537                        alice_central
538                            .invite_all(&case, &id, [&bob_central, &charlie_central])
539                            .await
540                            .unwrap();
541
542                        // Bob will propose adding Debbie
543                        let debbie_kp = debbie_central.get_one_key_package(&case).await;
544                        let proposal = bob_central
545                            .transaction
546                            .new_add_proposal(&id, debbie_kp)
547                            .await
548                            .unwrap()
549                            .proposal;
550                        alice_central
551                            .transaction
552                            .conversation(&id)
553                            .await
554                            .unwrap()
555                            .decrypt_message(proposal.to_bytes().unwrap())
556                            .await
557                            .unwrap();
558                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
559
560                        // But Charlie will commit meanwhile
561                        charlie_central
562                            .transaction
563                            .conversation(&id)
564                            .await
565                            .unwrap()
566                            .update_key_material()
567                            .await
568                            .unwrap();
569                        let commit = charlie_central.mls_transport.latest_commit().await;
570                        let proposals = alice_central
571                            .transaction
572                            .conversation(&id)
573                            .await
574                            .unwrap()
575                            .decrypt_message(commit.to_bytes().unwrap())
576                            .await
577                            .unwrap()
578                            .proposals;
579                        // which Alice should not renew since it's not hers
580                        assert!(alice_central.pending_proposals(&id).await.is_empty());
581                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
582                    })
583                },
584            )
585            .await
586        }
587
588        #[apply(all_cred_cipher)]
589        #[wasm_bindgen_test]
590        pub async fn renewable_when_valid_commit_doesnt_adds_same(case: TestContext) {
591            run_test_with_client_ids(
592                case.clone(),
593                ["alice", "bob", "charlie"],
594                move |[mut alice_central, bob_central, charlie_central]| {
595                    Box::pin(async move {
596                        let id = conversation_id();
597                        alice_central
598                            .transaction
599                            .new_conversation(&id, case.credential_type, case.cfg.clone())
600                            .await
601                            .unwrap();
602                        alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
603
604                        // Alice proposes adding Charlie
605                        let charlie_kp = charlie_central.get_one_key_package(&case).await;
606                        assert!(alice_central.pending_proposals(&id).await.is_empty());
607                        alice_central
608                            .transaction
609                            .new_add_proposal(&id, charlie_kp)
610                            .await
611                            .unwrap();
612                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
613
614                        // But meanwhile Bob will create a commit without Alice's proposal
615                        bob_central
616                            .transaction
617                            .conversation(&id)
618                            .await
619                            .unwrap()
620                            .update_key_material()
621                            .await
622                            .unwrap();
623                        let commit = bob_central.mls_transport.latest_commit().await;
624                        let proposals = alice_central
625                            .transaction
626                            .conversation(&id)
627                            .await
628                            .unwrap()
629                            .decrypt_message(commit.to_bytes().unwrap())
630                            .await
631                            .unwrap()
632                            .proposals;
633                        // So Alice proposal should be renewed
634                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
635                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
636
637                        // And same should happen when proposal is in pending commit
638                        alice_central.commit_pending_proposals_unmerged(&id).await;
639                        assert!(alice_central.pending_commit(&id).await.is_some());
640                        bob_central
641                            .transaction
642                            .conversation(&id)
643                            .await
644                            .unwrap()
645                            .update_key_material()
646                            .await
647                            .unwrap();
648                        let commit = bob_central.mls_transport.latest_commit().await;
649                        let proposals = alice_central
650                            .transaction
651                            .conversation(&id)
652                            .await
653                            .unwrap()
654                            .decrypt_message(commit.to_bytes().unwrap())
655                            .await
656                            .unwrap()
657                            .proposals;
658                        // So Alice proposal should also be renewed
659                        // It should also replace existing one
660                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
661                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
662                    })
663                },
664            )
665            .await
666        }
667
668        #[apply(all_cred_cipher)]
669        #[wasm_bindgen_test]
670        pub async fn renews_pending_commit_when_valid_commit_doesnt_add_same(case: TestContext) {
671            run_test_with_client_ids(
672                case.clone(),
673                ["alice", "bob"],
674                move |[mut alice_central, bob_central]| {
675                    Box::pin(async move {
676                        let id = conversation_id();
677                        alice_central
678                            .transaction
679                            .new_conversation(&id, case.credential_type, case.cfg.clone())
680                            .await
681                            .unwrap();
682                        alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
683
684                        // Alice commits adding Charlie
685                        alice_central.create_unmerged_commit(&id).await;
686                        assert!(alice_central.pending_commit(&id).await.is_some());
687
688                        // But meanwhile Bob will create a commit
689                        bob_central
690                            .transaction
691                            .conversation(&id)
692                            .await
693                            .unwrap()
694                            .update_key_material()
695                            .await
696                            .unwrap();
697                        let commit = bob_central.mls_transport.latest_commit().await;
698                        let proposals = alice_central
699                            .transaction
700                            .conversation(&id)
701                            .await
702                            .unwrap()
703                            .decrypt_message(commit.to_bytes().unwrap())
704                            .await
705                            .unwrap()
706                            .proposals;
707                        // So Alice proposal should be renewed
708                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
709                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
710                    })
711                },
712            )
713            .await
714        }
715    }
716
717    mod remove {
718        use super::*;
719
720        #[apply(all_cred_cipher)]
721        #[wasm_bindgen_test]
722        pub async fn not_renewable_when_valid_commit_removes_same(case: TestContext) {
723            run_test_with_client_ids(
724                case.clone(),
725                ["alice", "bob", "charlie"],
726                move |[mut alice_central, bob_central, charlie_central]| {
727                    Box::pin(async move {
728                        let id = conversation_id();
729                        alice_central
730                            .transaction
731                            .new_conversation(&id, case.credential_type, case.cfg.clone())
732                            .await
733                            .unwrap();
734                        alice_central
735                            .invite_all(&case, &id, [&bob_central, &charlie_central])
736                            .await
737                            .unwrap();
738
739                        assert!(alice_central.pending_proposals(&id).await.is_empty());
740                        alice_central
741                            .transaction
742                            .new_remove_proposal(&id, charlie_central.get_client_id().await)
743                            .await
744                            .unwrap();
745                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
746
747                        bob_central
748                            .transaction
749                            .conversation(&id)
750                            .await
751                            .unwrap()
752                            .remove_members(&[charlie_central.get_client_id().await])
753                            .await
754                            .unwrap();
755                        let commit = bob_central.mls_transport.latest_commit().await;
756                        let proposals = alice_central
757                            .transaction
758                            .conversation(&id)
759                            .await
760                            .unwrap()
761                            .decrypt_message(commit.to_bytes().unwrap())
762                            .await
763                            .unwrap()
764                            .proposals;
765                        // Remove proposal is not renewed since commit does same
766                        assert!(alice_central.pending_proposals(&id).await.is_empty());
767                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
768                    })
769                },
770            )
771            .await
772        }
773
774        #[apply(all_cred_cipher)]
775        #[wasm_bindgen_test]
776        pub async fn not_renewable_by_ref(case: TestContext) {
777            run_test_with_client_ids(
778                case.clone(),
779                ["alice", "bob", "charlie"],
780                move |[mut alice_central, bob_central, charlie_central]| {
781                    Box::pin(async move {
782                        let id = conversation_id();
783                        alice_central
784                            .transaction
785                            .new_conversation(&id, case.credential_type, case.cfg.clone())
786                            .await
787                            .unwrap();
788                        alice_central
789                            .invite_all(&case, &id, [&bob_central, &charlie_central])
790                            .await
791                            .unwrap();
792
793                        let proposal = bob_central
794                            .transaction
795                            .new_remove_proposal(&id, charlie_central.get_client_id().await)
796                            .await
797                            .unwrap()
798                            .proposal;
799                        assert!(alice_central.pending_proposals(&id).await.is_empty());
800                        alice_central
801                            .transaction
802                            .conversation(&id)
803                            .await
804                            .unwrap()
805                            .decrypt_message(proposal.to_bytes().unwrap())
806                            .await
807                            .unwrap();
808                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
809
810                        charlie_central
811                            .transaction
812                            .conversation(&id)
813                            .await
814                            .unwrap()
815                            .update_key_material()
816                            .await
817                            .unwrap();
818                        let commit = charlie_central.mls_transport.latest_commit().await;
819                        let proposals = alice_central
820                            .transaction
821                            .conversation(&id)
822                            .await
823                            .unwrap()
824                            .decrypt_message(commit.to_bytes().unwrap())
825                            .await
826                            .unwrap()
827                            .proposals;
828                        // Remove proposal is not renewed since by ref
829                        assert!(alice_central.pending_proposals(&id).await.is_empty());
830                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
831                    })
832                },
833            )
834            .await
835        }
836
837        #[apply(all_cred_cipher)]
838        #[wasm_bindgen_test]
839        pub async fn renewable_when_valid_commit_doesnt_remove_same(case: TestContext) {
840            run_test_with_client_ids(
841                case.clone(),
842                ["alice", "bob", "charlie", "debbie"],
843                move |[mut alice_central, bob_central, charlie_central, debbie_central]| {
844                    Box::pin(async move {
845                        let id = conversation_id();
846                        alice_central
847                            .transaction
848                            .new_conversation(&id, case.credential_type, case.cfg.clone())
849                            .await
850                            .unwrap();
851                        alice_central
852                            .invite_all(&case, &id, [&bob_central, &charlie_central, &debbie_central])
853                            .await
854                            .unwrap();
855
856                        // Alice wants to remove Charlie
857                        assert!(alice_central.pending_proposals(&id).await.is_empty());
858                        alice_central
859                            .transaction
860                            .new_remove_proposal(&id, charlie_central.get_client_id().await)
861                            .await
862                            .unwrap();
863                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
864
865                        // Whereas Bob wants to remove Debbie
866                        bob_central
867                            .transaction
868                            .conversation(&id)
869                            .await
870                            .unwrap()
871                            .remove_members(&[debbie_central.get_client_id().await])
872                            .await
873                            .unwrap();
874                        let commit = bob_central.mls_transport.latest_commit().await;
875                        let proposals = alice_central
876                            .transaction
877                            .conversation(&id)
878                            .await
879                            .unwrap()
880                            .decrypt_message(commit.to_bytes().unwrap())
881                            .await
882                            .unwrap()
883                            .proposals;
884                        // Remove is renewed since valid commit removes another
885                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
886                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
887                    })
888                },
889            )
890            .await
891        }
892
893        #[apply(all_cred_cipher)]
894        #[wasm_bindgen_test]
895        pub async fn renews_pending_commit_when_commit_doesnt_remove_same(case: TestContext) {
896            run_test_with_client_ids(
897                case.clone(),
898                ["alice", "bob", "charlie", "debbie"],
899                move |[mut alice_central, bob_central, charlie_central, debbie_central]| {
900                    Box::pin(async move {
901                        let id = conversation_id();
902                        alice_central
903                            .transaction
904                            .new_conversation(&id, case.credential_type, case.cfg.clone())
905                            .await
906                            .unwrap();
907                        alice_central
908                            .invite_all(&case, &id, [&bob_central, &charlie_central, &debbie_central])
909                            .await
910                            .unwrap();
911
912                        // Alice wants to remove Charlie
913                        alice_central
914                            .transaction
915                            .new_remove_proposal(&id, charlie_central.get_client_id().await)
916                            .await
917                            .unwrap();
918                        alice_central.commit_pending_proposals_unmerged(&id).await;
919                        assert!(alice_central.pending_commit(&id).await.is_some());
920
921                        // Whereas Bob wants to remove Debbie
922                        bob_central
923                            .transaction
924                            .conversation(&id)
925                            .await
926                            .unwrap()
927                            .remove_members(&[debbie_central.get_client_id().await])
928                            .await
929                            .unwrap();
930                        let commit = bob_central.mls_transport.latest_commit().await;
931                        let proposals = alice_central
932                            .transaction
933                            .conversation(&id)
934                            .await
935                            .unwrap()
936                            .decrypt_message(commit.to_bytes().unwrap())
937                            .await
938                            .unwrap()
939                            .proposals;
940                        // Remove is renewed since valid commit removes another
941                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
942                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
943                    })
944                },
945            )
946            .await
947        }
948
949        #[apply(all_cred_cipher)]
950        #[wasm_bindgen_test]
951        pub async fn renews_pending_commit_from_proposal_when_commit_doesnt_remove_same(case: TestContext) {
952            run_test_with_client_ids(
953                case.clone(),
954                ["alice", "bob", "charlie", "debbie"],
955                move |[mut alice_central, bob_central, charlie_central, debbie_central]| {
956                    Box::pin(async move {
957                        let id = conversation_id();
958                        alice_central
959                            .transaction
960                            .new_conversation(&id, case.credential_type, case.cfg.clone())
961                            .await
962                            .unwrap();
963                        alice_central
964                            .invite_all(&case, &id, [&bob_central, &charlie_central, &debbie_central])
965                            .await
966                            .unwrap();
967
968                        // Alice wants to remove Charlie
969                        alice_central
970                            .transaction
971                            .new_remove_proposal(&id, charlie_central.get_client_id().await)
972                            .await
973                            .unwrap();
974                        alice_central.commit_pending_proposals_unmerged(&id).await;
975
976                        // Whereas Bob wants to remove Debbie
977                        bob_central
978                            .transaction
979                            .conversation(&id)
980                            .await
981                            .unwrap()
982                            .remove_members(&[debbie_central.get_client_id().await])
983                            .await
984                            .unwrap();
985                        let commit = bob_central.mls_transport.latest_commit().await;
986                        let proposals = alice_central
987                            .transaction
988                            .conversation(&id)
989                            .await
990                            .unwrap()
991                            .decrypt_message(commit.to_bytes().unwrap())
992                            .await
993                            .unwrap()
994                            .proposals;
995                        // Remove is renewed since valid commit removes another
996                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
997                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
998                    })
999                },
1000            )
1001            .await
1002        }
1003    }
1004}