1use std::collections::BTreeMap;
18
19use openmls::{
20 extensions::Extensions,
21 group::{GroupContext, MlsGroup as OpenMlsGroup},
22 messages::proposals::{AppDataUpdateProposal, Proposal},
23 prelude::CommitMessageBundle,
24 storage::OpenMlsProvider,
25};
26use prost::Message as _;
27use tls_codec::{Serialize as _, VLBytes};
28use xmtp_mls_common::{
29 app_data::{
30 component_id::ComponentId,
31 component_registry::{ComponentRegistry, ComponentRegistryError},
32 migration::{self, CanonicalBootstrapExpectation, MigrationError as CommonMigrationError},
33 },
34 inbox_id::InboxId,
35 tls_map::TlsMapDelta,
36};
37use xmtp_proto::xmtp::mls::message_contents::{
38 GroupMembership as GroupMembershipProto, GroupMembershipEntry,
39 group_membership_entry::{
40 V1 as GroupMembershipEntryV1, Version as GroupMembershipEntryVersion,
41 },
42};
43
44use crate::{context::XmtpSharedContext, identity_updates::IdentityUpdates};
45
46#[derive(Debug, thiserror::Error)]
48pub enum BootstrapSynthesisError {
49 #[error(transparent)]
50 Common(#[from] CommonMigrationError),
51
52 #[error("identity-update lookup failed for inbox {inbox_id}: {source}")]
56 IdentityUpdateLookup {
57 inbox_id: String,
58 #[source]
59 source: crate::client::ClientError,
60 },
61
62 #[error("legacy GroupMembership decode: {0}")]
63 LegacyMembershipDecode(#[from] prost::DecodeError),
64
65 #[error("registry re-encode: {0}")]
73 RegistryReEncode(#[from] ComponentRegistryError),
74
75 #[error(
84 "sequence_id {sequence_id} for inbox {inbox_id} exceeds i64::MAX; cannot query identity-update history"
85 )]
86 SequenceIdOverflow { inbox_id: String, sequence_id: u64 },
87
88 #[error("wire delta encode: {0}")]
94 TlsCodec(#[from] tls_codec::Error),
95}
96
97impl xmtp_common::retry::RetryableError for BootstrapSynthesisError {
98 fn is_retryable(&self) -> bool {
106 match self {
107 Self::IdentityUpdateLookup { source, .. } => source.is_retryable(),
108 Self::Common(_)
109 | Self::LegacyMembershipDecode(_)
110 | Self::RegistryReEncode(_)
111 | Self::SequenceIdOverflow { .. }
112 | Self::TlsCodec(_) => false,
113 }
114 }
115}
116
117pub async fn synthesize_initial_component_values<C: XmtpSharedContext>(
126 context: &C,
127 mls_group: &OpenMlsGroup,
128) -> Result<BTreeMap<ComponentId, Vec<u8>>, BootstrapSynthesisError> {
129 synthesize_initial_component_values_from_extensions(context, mls_group.extensions()).await
130}
131
132pub async fn synthesize_initial_component_values_from_extensions<C: XmtpSharedContext>(
136 context: &C,
137 extensions: &Extensions<GroupContext>,
138) -> Result<BTreeMap<ComponentId, Vec<u8>>, BootstrapSynthesisError> {
139 use xmtp_mls_common::app_data::migration::synthesize_canonical_subset_from_extensions;
140
141 let canonical: CanonicalBootstrapExpectation =
143 synthesize_canonical_subset_from_extensions(extensions)?;
144
145 let mut out: BTreeMap<ComponentId, Vec<u8>> = BTreeMap::new();
146 for (id, (_op_type, bytes)) in canonical.strict.into_iter() {
147 out.insert(id, bytes);
148 }
149
150 let mut registry = ComponentRegistry::new();
165 for (id, meta) in canonical.expected_registry.iter() {
166 registry.set(*id, meta.clone())?;
167 }
168 let mut registry_delta: TlsMapDelta<ComponentId, VLBytes> = TlsMapDelta::new();
169 for (id, meta) in canonical.expected_registry.into_iter() {
170 registry_delta = registry_delta.insert(id, VLBytes::new(meta.encode_to_vec()));
171 }
172 out.insert(
173 ComponentId::COMPONENT_REGISTRY,
174 registry_delta.tls_serialize_detached()?,
175 );
176
177 let group_membership_bytes =
180 build_partitioned_group_membership(context, extensions, &canonical.membership_sequence_ids)
181 .await?;
182 out.insert(ComponentId::GROUP_MEMBERSHIP, group_membership_bytes);
183
184 Ok(out)
185}
186
187async fn build_partitioned_group_membership<C: XmtpSharedContext>(
192 context: &C,
193 extensions: &Extensions<GroupContext>,
194 sequence_ids: &BTreeMap<InboxId, u64>,
195) -> Result<Vec<u8>, BootstrapSynthesisError> {
196 use openmls::extensions::{Extension, UnknownExtension};
197 let legacy_bytes = extensions
198 .iter()
199 .find_map(|extension| match extension {
200 Extension::Unknown(
201 xmtp_configuration::GROUP_MEMBERSHIP_EXTENSION_ID,
202 UnknownExtension(data),
203 ) => Some(data),
204 _ => None,
205 })
206 .ok_or_else(|| {
207 BootstrapSynthesisError::Common(CommonMigrationError::MissingGroupMembershipExtension)
208 })?;
209 let legacy_proto = GroupMembershipProto::decode(legacy_bytes.as_slice())?;
210 if legacy_proto.failed_installations.is_empty() {
213 return encode_partitioned_group_membership(sequence_ids, BTreeMap::new());
214 }
215
216 let identity_updates = IdentityUpdates::new(context);
217 let db = context.db();
218
219 let mut install_owner: std::collections::HashMap<Vec<u8>, InboxId> =
223 std::collections::HashMap::new();
224 for (inbox_id, seq) in sequence_ids.iter() {
225 let inbox_id_hex = inbox_id.to_hex();
226 let signed_seq =
227 i64::try_from(*seq).map_err(|_| BootstrapSynthesisError::SequenceIdOverflow {
228 inbox_id: inbox_id_hex.clone(),
229 sequence_id: *seq,
230 })?;
231 let state = identity_updates
232 .get_association_state(&db, &inbox_id_hex, Some(signed_seq))
233 .await
234 .map_err(|source| BootstrapSynthesisError::IdentityUpdateLookup {
235 inbox_id: inbox_id_hex.clone(),
236 source,
237 })?;
238 for install_id in state.installation_ids() {
239 install_owner.insert(install_id, *inbox_id);
240 }
241 }
242
243 let mut per_inbox_failed: BTreeMap<InboxId, Vec<Vec<u8>>> = BTreeMap::new();
249 let mut dropped = 0usize;
250 for fi in &legacy_proto.failed_installations {
251 if let Some(owner) = install_owner.get(fi) {
252 per_inbox_failed.entry(*owner).or_default().push(fi.clone());
253 } else {
254 dropped += 1;
255 }
256 }
257 if dropped > 0 {
258 let total = legacy_proto.failed_installations.len();
259 if dropped * 2 > total {
266 tracing::warn!(
267 dropped,
268 total,
269 "bootstrap synthesis dropped a majority of failed_installation entries (hint only, but unusual)"
270 );
271 } else {
272 tracing::info!(
273 dropped,
274 total,
275 "bootstrap synthesis dropped unresolvable failed_installation entries (hint only, not correctness-critical)"
276 );
277 }
278 }
279
280 encode_partitioned_group_membership(sequence_ids, per_inbox_failed)
281}
282
283fn encode_partitioned_group_membership(
284 sequence_ids: &BTreeMap<InboxId, u64>,
285 mut per_inbox_failed: BTreeMap<InboxId, Vec<Vec<u8>>>,
286) -> Result<Vec<u8>, BootstrapSynthesisError> {
287 let mut entries: BTreeMap<InboxId, GroupMembershipEntry> = BTreeMap::new();
292 for (inbox_id, seq) in sequence_ids.iter() {
293 let failed = per_inbox_failed.remove(inbox_id).unwrap_or_default();
294 entries.insert(
295 *inbox_id,
296 GroupMembershipEntry {
297 version: Some(GroupMembershipEntryVersion::V1(GroupMembershipEntryV1 {
298 sequence_id: *seq,
299 failed_installations: failed,
300 })),
301 },
302 );
303 }
304
305 Ok(migration::encode_group_membership_delta(&entries)?)
306}
307
308#[derive(Debug, thiserror::Error)]
310pub enum BootstrapCommitError<StorageError: std::error::Error> {
311 #[error("commit create error: {0}")]
312 CreateCommit(#[from] openmls::group::CreateCommitError),
313 #[error("commit stage error: {0}")]
314 StageCommit(#[from] openmls::group::CommitBuilderStageError<StorageError>),
315 #[error("TLS codec error: {0}")]
316 TlsCodec(#[from] tls_codec::Error),
317 #[error("bootstrap precondition: new_extensions still carries legacy XMTP extension {0:#06x}")]
322 LegacyExtensionPresent(u16),
323 #[error("bootstrap precondition: RequiredCapabilities doesn't list AppDataDictionary")]
329 MissingAppDataDictionaryRequirement,
330 #[error("bootstrap precondition: dict apply failed: {0}")]
336 DictApply(#[from] super::component_source::ComponentSourceError),
337}
338
339pub fn stage_bootstrap_commit<Provider: OpenMlsProvider>(
361 mls_group: &mut OpenMlsGroup,
362 provider: &Provider,
363 signer: &impl openmls_traits::signatures::Signer,
364 component_values: &BTreeMap<ComponentId, Vec<u8>>,
365 new_extensions: Extensions<GroupContext>,
366) -> Result<CommitMessageBundle, BootstrapCommitError<Provider::StorageError>> {
367 use openmls::component::ComponentData;
368 use openmls::extensions::Extension;
369 use xmtp_configuration::{
370 GROUP_MEMBERSHIP_EXTENSION_ID, GROUP_PERMISSIONS_EXTENSION_ID,
371 MUTABLE_METADATA_EXTENSION_ID,
372 };
373
374 for ext in new_extensions.iter() {
379 match ext {
380 Extension::Unknown(MUTABLE_METADATA_EXTENSION_ID, _) => {
381 return Err(BootstrapCommitError::LegacyExtensionPresent(
382 MUTABLE_METADATA_EXTENSION_ID,
383 ));
384 }
385 Extension::Unknown(GROUP_PERMISSIONS_EXTENSION_ID, _) => {
386 return Err(BootstrapCommitError::LegacyExtensionPresent(
387 GROUP_PERMISSIONS_EXTENSION_ID,
388 ));
389 }
390 Extension::Unknown(GROUP_MEMBERSHIP_EXTENSION_ID, _) => {
391 return Err(BootstrapCommitError::LegacyExtensionPresent(
392 GROUP_MEMBERSHIP_EXTENSION_ID,
393 ));
394 }
395 Extension::ImmutableMetadata(_) => {
396 return Err(BootstrapCommitError::LegacyExtensionPresent(0xf000));
398 }
399 _ => {}
400 }
401 }
402 use openmls::extensions::ExtensionType;
408 let requires_app_data_dictionary = new_extensions
409 .required_capabilities()
410 .map(|rc| {
411 rc.extension_types()
412 .contains(&ExtensionType::AppDataDictionary)
413 })
414 .unwrap_or(false);
415 if !requires_app_data_dictionary {
416 return Err(BootstrapCommitError::MissingAppDataDictionaryRequirement);
417 }
418
419 let mut builder = mls_group
422 .commit_builder()
423 .propose_group_context_extensions(new_extensions)
424 .map_err(BootstrapCommitError::CreateCommit)?;
425 for (component_id, bytes) in component_values.iter() {
436 builder = builder.add_proposal(Proposal::AppDataUpdate(Box::new(
437 AppDataUpdateProposal::update(component_id.as_u16(), bytes.clone()),
438 )));
439 }
440
441 let mut stage = builder.load_psks(provider.storage())?;
442
443 let empty_registry = xmtp_mls_common::app_data::component_registry::ComponentRegistry::new();
462 let mut updater = stage.app_data_dictionary_updater();
463 for (component_id, wire_bytes) in component_values.iter() {
464 let dict_bytes = super::component_source::apply_app_data_update_payload(
465 *component_id,
466 wire_bytes,
467 None,
468 &empty_registry,
469 )
470 .map_err(BootstrapCommitError::DictApply)?;
471 updater.set(ComponentData::from_parts(
472 component_id.as_u16(),
473 dict_bytes.into(),
474 ));
475 }
476 stage.with_app_data_dictionary_updates(updater.changes());
477
478 let bundle = stage
479 .build(provider.rand(), provider.crypto(), signer, |_| true)?
480 .stage_commit(provider)?;
481 Ok(bundle)
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
490 use std::collections::HashMap;
491
492 use openmls::extensions::{Extension, Extensions, Metadata, UnknownExtension};
493 use xmtp_configuration::{
494 GROUP_MEMBERSHIP_EXTENSION_ID, GROUP_PERMISSIONS_EXTENSION_ID,
495 MUTABLE_METADATA_EXTENSION_ID,
496 };
497 use xmtp_mls_common::{
498 app_data::component_id::ComponentId, group_metadata::GroupMetadata,
499 group_mutable_metadata::GroupMutableMetadata,
500 };
501 use xmtp_proto::xmtp::mls::message_contents::{
502 GroupMembership as GroupMembershipProto, GroupMutablePermissionsV1,
503 MembershipPolicy as MembershipPolicyProto,
504 PermissionsUpdatePolicy as PermissionsUpdatePolicyProto, PolicySet as PolicySetProto,
505 membership_policy::{BasePolicy as MembershipBase, Kind as MembershipKind},
506 permissions_update_policy::{Kind as PermissionsKind, PermissionsBasePolicy},
507 };
508
509 use xmtp_db::identity_update::QueryIdentityUpdates;
510
511 use crate::{identity_updates::load_identity_updates, tester};
512
513 fn unwrap_v1(entry: &GroupMembershipEntry) -> &GroupMembershipEntryV1 {
518 match entry.version.as_ref().expect("entry missing version") {
519 GroupMembershipEntryVersion::V1(v1) => v1,
520 }
521 }
522
523 fn minimal_policy_set() -> PolicySetProto {
524 let allow = MembershipPolicyProto {
525 kind: Some(MembershipKind::Base(MembershipBase::Allow as i32)),
526 };
527 let admin_only = PermissionsUpdatePolicyProto {
528 kind: Some(PermissionsKind::Base(
529 PermissionsBasePolicy::AllowIfAdmin as i32,
530 )),
531 };
532 let super_admin_only = PermissionsUpdatePolicyProto {
533 kind: Some(PermissionsKind::Base(
534 PermissionsBasePolicy::AllowIfSuperAdmin as i32,
535 )),
536 };
537 PolicySetProto {
538 add_member_policy: Some(allow.clone()),
539 remove_member_policy: Some(allow),
540 update_metadata_policy: HashMap::new(),
541 add_admin_policy: Some(admin_only.clone()),
542 remove_admin_policy: Some(admin_only),
543 update_permissions_policy: Some(super_admin_only),
544 }
545 }
546
547 fn build_test_extensions(
548 gmm: GroupMutableMetadata,
549 membership: GroupMembershipProto,
550 metadata: GroupMetadata,
551 ) -> Extensions<openmls::group::GroupContext> {
552 let gmm_bytes: Vec<u8> = gmm.try_into().unwrap();
553 let permissions_bytes = GroupMutablePermissionsV1 {
554 policies: Some(minimal_policy_set()),
555 }
556 .encode_to_vec();
557 let membership_bytes = membership.encode_to_vec();
558 let metadata_bytes: Vec<u8> = metadata.try_into().unwrap();
559 Extensions::from_vec(vec![
560 Extension::Unknown(MUTABLE_METADATA_EXTENSION_ID, UnknownExtension(gmm_bytes)),
561 Extension::Unknown(
562 GROUP_PERMISSIONS_EXTENSION_ID,
563 UnknownExtension(permissions_bytes),
564 ),
565 Extension::Unknown(
566 GROUP_MEMBERSHIP_EXTENSION_ID,
567 UnknownExtension(membership_bytes),
568 ),
569 Extension::ImmutableMetadata(Metadata::new(metadata_bytes)),
570 ])
571 .expect("valid group-context extension set")
572 }
573
574 fn default_gmm() -> GroupMutableMetadata {
575 GroupMutableMetadata::new(HashMap::new(), Vec::new(), Vec::new())
576 }
577
578 fn default_metadata(creator_inbox_id: String) -> GroupMetadata {
579 GroupMetadata::new(
580 xmtp_db::group::ConversationType::Group,
581 creator_inbox_id,
582 None,
583 None,
584 )
585 }
586
587 #[xmtp_common::test(unwrap_try = true)]
588 async fn synthesize_preserves_zero_membership_without_failed_installations() {
589 let context = crate::test::mock::context();
590 let inbox = hex::encode([7; 32]);
591 let extensions = build_test_extensions(
592 default_gmm(),
593 GroupMembershipProto {
594 members: [(inbox.clone(), 0)].into(),
595 failed_installations: Vec::new(),
596 },
597 default_metadata(inbox.clone()),
598 );
599 let values =
600 synthesize_initial_component_values_from_extensions(&context, &extensions).await?;
601 let membership = migration::decode_group_membership_delta(
602 values.get(&ComponentId::GROUP_MEMBERSHIP).unwrap(),
603 )?;
604 assert_eq!(membership.len(), 1);
605 let entry = unwrap_v1(membership.get(&InboxId::from_hex(&inbox)?).unwrap());
606 assert_eq!(entry.sequence_id, 0);
607 assert!(entry.failed_installations.is_empty());
608 }
609
610 #[xmtp_common::test(unwrap_try = true)]
611 async fn synthesize_initial_component_values_is_deterministic() {
612 tester!(alix);
617 tester!(bo);
618 load_identity_updates(
619 alix.context.api(),
620 &alix.context.db(),
621 &[alix.inbox_id(), bo.inbox_id()],
622 )
623 .await?;
624 let alix_seq = alix
629 .context
630 .db()
631 .get_latest_sequence_id_for_inbox(alix.inbox_id())? as u64;
632 let bo_seq = alix
633 .context
634 .db()
635 .get_latest_sequence_id_for_inbox(bo.inbox_id())? as u64;
636
637 let mut members = HashMap::new();
638 members.insert(alix.inbox_id().to_string(), alix_seq);
639 members.insert(bo.inbox_id().to_string(), bo_seq);
640 let exts = build_test_extensions(
641 default_gmm(),
642 GroupMembershipProto {
643 members,
644 failed_installations: vec![],
645 },
646 default_metadata(alix.inbox_id().to_string()),
647 );
648
649 let a = synthesize_initial_component_values_from_extensions(&alix.context, &exts).await?;
650 let b = synthesize_initial_component_values_from_extensions(&alix.context, &exts).await?;
651 assert_eq!(a, b, "bootstrap synthesis must be byte-stable");
652 }
653
654 #[xmtp_common::test(unwrap_try = true)]
655 async fn synthesize_partitions_failed_installations_by_owner() {
656 tester!(alix);
660 tester!(bo);
661 load_identity_updates(
662 alix.context.api(),
663 &alix.context.db(),
664 &[alix.inbox_id(), bo.inbox_id()],
665 )
666 .await?;
667
668 let alix_install = alix.installation_id.to_vec();
669 let bo_install = bo.installation_id.to_vec();
670 let alix_inbox = InboxId::from_hex(alix.inbox_id())?;
671 let bo_inbox = InboxId::from_hex(bo.inbox_id())?;
672 let alix_seq = alix
673 .context
674 .db()
675 .get_latest_sequence_id_for_inbox(alix.inbox_id())? as u64;
676 let bo_seq = alix
677 .context
678 .db()
679 .get_latest_sequence_id_for_inbox(bo.inbox_id())? as u64;
680
681 let mut members = HashMap::new();
682 members.insert(alix.inbox_id().to_string(), alix_seq);
683 members.insert(bo.inbox_id().to_string(), bo_seq);
684 let exts = build_test_extensions(
685 default_gmm(),
686 GroupMembershipProto {
687 members,
688 failed_installations: vec![alix_install.clone(), bo_install.clone()],
689 },
690 default_metadata(alix.inbox_id().to_string()),
691 );
692
693 let out = synthesize_initial_component_values_from_extensions(&alix.context, &exts).await?;
694
695 let bytes = out.get(&ComponentId::GROUP_MEMBERSHIP).unwrap();
696 let decoded =
697 xmtp_mls_common::app_data::migration::decode_group_membership_delta(bytes).unwrap();
698 assert_eq!(decoded.len(), 2);
699 let v1_a = unwrap_v1(decoded.get(&alix_inbox).unwrap());
700 let v1_b = unwrap_v1(decoded.get(&bo_inbox).unwrap());
701 assert_eq!(v1_a.sequence_id, alix_seq);
702 assert_eq!(v1_a.failed_installations, vec![alix_install]);
703 assert_eq!(v1_b.sequence_id, bo_seq);
704 assert_eq!(v1_b.failed_installations, vec![bo_install]);
705 }
706
707 #[xmtp_common::test(unwrap_try = true)]
708 async fn synthesize_drops_unresolvable_failed_installations() {
709 tester!(alix);
712 load_identity_updates(alix.context.api(), &alix.context.db(), &[alix.inbox_id()]).await?;
713
714 let alix_inbox = InboxId::from_hex(alix.inbox_id())?;
715 let alix_seq = alix
716 .context
717 .db()
718 .get_latest_sequence_id_for_inbox(alix.inbox_id())? as u64;
719 let orphan_install = vec![0xDE; 32]; let mut members = HashMap::new();
722 members.insert(alix.inbox_id().to_string(), alix_seq);
723 let exts = build_test_extensions(
724 default_gmm(),
725 GroupMembershipProto {
726 members,
727 failed_installations: vec![orphan_install],
728 },
729 default_metadata(alix.inbox_id().to_string()),
730 );
731
732 let out = synthesize_initial_component_values_from_extensions(&alix.context, &exts).await?;
733 let bytes = out.get(&ComponentId::GROUP_MEMBERSHIP).unwrap();
734 let decoded =
735 xmtp_mls_common::app_data::migration::decode_group_membership_delta(bytes).unwrap();
736 let v1 = unwrap_v1(decoded.get(&alix_inbox).unwrap());
737 assert_eq!(
738 v1.failed_installations,
739 Vec::<Vec<u8>>::new(),
740 "orphan failed_installation must be dropped"
741 );
742 }
743
744 #[xmtp_common::test(unwrap_try = true)]
745 async fn synthesize_emits_expected_component_keys() {
746 tester!(alix);
750 let exts = build_test_extensions(
751 default_gmm(),
752 GroupMembershipProto::default(),
753 default_metadata(alix.inbox_id().to_string()),
754 );
755 let out = synthesize_initial_component_values_from_extensions(&alix.context, &exts).await?;
756 assert!(out.contains_key(&ComponentId::COMPONENT_REGISTRY));
757 assert!(out.contains_key(&ComponentId::GROUP_MEMBERSHIP));
758 assert!(out.contains_key(&ComponentId::ADMIN_LIST));
759 assert!(out.contains_key(&ComponentId::SUPER_ADMIN_LIST));
760 assert!(out.contains_key(&ComponentId::CREATOR_INBOX_ID));
761 assert!(out.contains_key(&ComponentId::CONVERSATION_TYPE));
762 assert!(!out.contains_key(&ComponentId::DM_MEMBERS));
763 assert!(!out.contains_key(&ComponentId::ONESHOT_MESSAGE));
764 }
765
766 #[xmtp_common::test(unwrap_try = true)]
767 async fn synthesize_partitions_against_snapshotted_view_not_latest() {
768 use std::collections::BTreeSet;
786
787 tester!(alix);
788 let alix2 = alix.new_installation().await;
789 tester!(bo);
790 let bo2 = bo.new_installation().await;
791
792 load_identity_updates(
795 alix.context.api(),
796 &alix.context.db(),
797 &[alix.inbox_id(), bo.inbox_id()],
798 )
799 .await?;
800
801 let alix_snapshot_seq =
806 alix.context
807 .db()
808 .get_latest_sequence_id_for_inbox(alix.inbox_id())? as u64;
809 let bo_snapshot_seq = alix
810 .context
811 .db()
812 .get_latest_sequence_id_for_inbox(bo.inbox_id())? as u64;
813
814 let alix1_install = alix.installation_id.to_vec();
815 let alix2_install = alix2.installation_id.to_vec();
816 let bo1_install = bo.installation_id.to_vec();
817 let bo2_install = bo2.installation_id.to_vec();
818
819 let alix3 = alix.new_installation().await;
822 let bo3 = bo.new_installation().await;
823 let alix3_install = alix3.installation_id.to_vec();
824 let bo3_install = bo3.installation_id.to_vec();
825
826 load_identity_updates(
830 alix.context.api(),
831 &alix.context.db(),
832 &[alix.inbox_id(), bo.inbox_id()],
833 )
834 .await?;
835
836 let alix_inbox = InboxId::from_hex(alix.inbox_id())?;
837 let bo_inbox = InboxId::from_hex(bo.inbox_id())?;
838
839 let bogus_install = vec![0xCA; 32];
844
845 let mut members = HashMap::new();
846 members.insert(alix.inbox_id().to_string(), alix_snapshot_seq);
847 members.insert(bo.inbox_id().to_string(), bo_snapshot_seq);
848 let exts = build_test_extensions(
849 default_gmm(),
850 GroupMembershipProto {
851 members,
852 failed_installations: vec![
853 alix1_install.clone(),
854 alix2_install.clone(),
855 alix3_install.clone(),
856 bo1_install.clone(),
857 bo2_install.clone(),
858 bo3_install.clone(),
859 bogus_install.clone(),
860 ],
861 },
862 default_metadata(alix.inbox_id().to_string()),
863 );
864
865 let out = synthesize_initial_component_values_from_extensions(&alix.context, &exts).await?;
866 let bytes = out.get(&ComponentId::GROUP_MEMBERSHIP).unwrap();
867 let decoded =
868 xmtp_mls_common::app_data::migration::decode_group_membership_delta(bytes).unwrap();
869
870 let alix_v1 = unwrap_v1(decoded.get(&alix_inbox).unwrap());
871 let bo_v1 = unwrap_v1(decoded.get(&bo_inbox).unwrap());
872
873 assert_eq!(alix_v1.sequence_id, alix_snapshot_seq);
874 assert_eq!(bo_v1.sequence_id, bo_snapshot_seq);
875
876 let alix_failed: BTreeSet<Vec<u8>> = alix_v1.failed_installations.iter().cloned().collect();
877 let bo_failed: BTreeSet<Vec<u8>> = bo_v1.failed_installations.iter().cloned().collect();
878
879 assert!(alix_failed.contains(&alix1_install));
881 assert!(alix_failed.contains(&alix2_install));
882 assert!(bo_failed.contains(&bo1_install));
883 assert!(bo_failed.contains(&bo2_install));
884
885 assert!(!alix_failed.contains(&alix3_install));
888 assert!(!bo_failed.contains(&alix3_install));
889 assert!(!alix_failed.contains(&bo3_install));
890 assert!(!bo_failed.contains(&bo3_install));
891
892 assert!(!alix_failed.contains(&bogus_install));
894 assert!(!bo_failed.contains(&bogus_install));
895
896 assert!(!alix_failed.contains(&bo1_install));
898 assert!(!alix_failed.contains(&bo2_install));
899 assert!(!bo_failed.contains(&alix1_install));
900 assert!(!bo_failed.contains(&alix2_install));
901
902 let again =
906 synthesize_initial_component_values_from_extensions(&alix.context, &exts).await?;
907 assert_eq!(out, again, "bootstrap synthesis must be byte-stable");
908 }
909}