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] xmtp_mls_common::app_data::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 =
465 xmtp_mls_common::app_data::component_source::apply_app_data_update_payload(
466 *component_id,
467 wire_bytes,
468 None,
469 &empty_registry,
470 )
471 .map_err(BootstrapCommitError::DictApply)?;
472 updater.set(ComponentData::from_parts(
473 component_id.as_u16(),
474 dict_bytes.into(),
475 ));
476 }
477 stage.with_app_data_dictionary_updates(updater.changes());
478
479 let bundle = stage
480 .build(provider.rand(), provider.crypto(), signer, |_| true)?
481 .stage_commit(provider)?;
482 Ok(bundle)
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
491 use std::collections::HashMap;
492
493 use openmls::extensions::{Extension, Extensions, Metadata, UnknownExtension};
494 use xmtp_configuration::{
495 GROUP_MEMBERSHIP_EXTENSION_ID, GROUP_PERMISSIONS_EXTENSION_ID,
496 MUTABLE_METADATA_EXTENSION_ID,
497 };
498 use xmtp_mls_common::{
499 app_data::component_id::ComponentId, group_metadata::GroupMetadata,
500 group_mutable_metadata::GroupMutableMetadata,
501 };
502 use xmtp_proto::xmtp::mls::message_contents::{
503 GroupMembership as GroupMembershipProto, GroupMutablePermissionsV1,
504 MembershipPolicy as MembershipPolicyProto,
505 PermissionsUpdatePolicy as PermissionsUpdatePolicyProto, PolicySet as PolicySetProto,
506 membership_policy::{BasePolicy as MembershipBase, Kind as MembershipKind},
507 permissions_update_policy::{Kind as PermissionsKind, PermissionsBasePolicy},
508 };
509
510 use xmtp_db::identity_update::QueryIdentityUpdates;
511
512 use crate::{identity_updates::load_identity_updates, tester};
513
514 fn unwrap_v1(entry: &GroupMembershipEntry) -> &GroupMembershipEntryV1 {
519 match entry.version.as_ref().expect("entry missing version") {
520 GroupMembershipEntryVersion::V1(v1) => v1,
521 }
522 }
523
524 fn minimal_policy_set() -> PolicySetProto {
525 let allow = MembershipPolicyProto {
526 kind: Some(MembershipKind::Base(MembershipBase::Allow as i32)),
527 };
528 let admin_only = PermissionsUpdatePolicyProto {
529 kind: Some(PermissionsKind::Base(
530 PermissionsBasePolicy::AllowIfAdmin as i32,
531 )),
532 };
533 let super_admin_only = PermissionsUpdatePolicyProto {
534 kind: Some(PermissionsKind::Base(
535 PermissionsBasePolicy::AllowIfSuperAdmin as i32,
536 )),
537 };
538 PolicySetProto {
539 add_member_policy: Some(allow.clone()),
540 remove_member_policy: Some(allow),
541 update_metadata_policy: HashMap::new(),
542 add_admin_policy: Some(admin_only.clone()),
543 remove_admin_policy: Some(admin_only),
544 update_permissions_policy: Some(super_admin_only),
545 }
546 }
547
548 fn build_test_extensions(
549 gmm: GroupMutableMetadata,
550 membership: GroupMembershipProto,
551 metadata: GroupMetadata,
552 ) -> Extensions<openmls::group::GroupContext> {
553 let gmm_bytes: Vec<u8> = gmm.try_into().unwrap();
554 let permissions_bytes = GroupMutablePermissionsV1 {
555 policies: Some(minimal_policy_set()),
556 }
557 .encode_to_vec();
558 let membership_bytes = membership.encode_to_vec();
559 let metadata_bytes: Vec<u8> = metadata.try_into().unwrap();
560 Extensions::from_vec(vec![
561 Extension::Unknown(MUTABLE_METADATA_EXTENSION_ID, UnknownExtension(gmm_bytes)),
562 Extension::Unknown(
563 GROUP_PERMISSIONS_EXTENSION_ID,
564 UnknownExtension(permissions_bytes),
565 ),
566 Extension::Unknown(
567 GROUP_MEMBERSHIP_EXTENSION_ID,
568 UnknownExtension(membership_bytes),
569 ),
570 Extension::ImmutableMetadata(Metadata::new(metadata_bytes)),
571 ])
572 .expect("valid group-context extension set")
573 }
574
575 fn default_gmm() -> GroupMutableMetadata {
576 GroupMutableMetadata::new(HashMap::new(), Vec::new(), Vec::new())
577 }
578
579 fn default_metadata(creator_inbox_id: String) -> GroupMetadata {
580 GroupMetadata::new(
581 xmtp_db::group::ConversationType::Group,
582 creator_inbox_id,
583 None,
584 None,
585 )
586 }
587
588 #[xmtp_common::test(unwrap_try = true)]
589 async fn synthesize_preserves_zero_membership_without_failed_installations() {
590 let context = crate::test::mock::context();
591 let inbox = hex::encode([7; 32]);
592 let extensions = build_test_extensions(
593 default_gmm(),
594 GroupMembershipProto {
595 members: [(inbox.clone(), 0)].into(),
596 failed_installations: Vec::new(),
597 },
598 default_metadata(inbox.clone()),
599 );
600 let values =
601 synthesize_initial_component_values_from_extensions(&context, &extensions).await?;
602 let membership = migration::decode_group_membership_delta(
603 values.get(&ComponentId::GROUP_MEMBERSHIP).unwrap(),
604 )?;
605 assert_eq!(membership.len(), 1);
606 let entry = unwrap_v1(membership.get(&InboxId::from_hex(&inbox)?).unwrap());
607 assert_eq!(entry.sequence_id, 0);
608 assert!(entry.failed_installations.is_empty());
609 }
610
611 #[xmtp_common::test(unwrap_try = true)]
612 async fn synthesize_initial_component_values_is_deterministic() {
613 tester!(alix);
618 tester!(bo);
619 load_identity_updates(
620 alix.context.api(),
621 &alix.context.db(),
622 &[alix.inbox_id(), bo.inbox_id()],
623 )
624 .await?;
625 let alix_seq = alix
630 .context
631 .db()
632 .get_latest_sequence_id_for_inbox(alix.inbox_id())? as u64;
633 let bo_seq = alix
634 .context
635 .db()
636 .get_latest_sequence_id_for_inbox(bo.inbox_id())? as u64;
637
638 let mut members = HashMap::new();
639 members.insert(alix.inbox_id().to_string(), alix_seq);
640 members.insert(bo.inbox_id().to_string(), bo_seq);
641 let exts = build_test_extensions(
642 default_gmm(),
643 GroupMembershipProto {
644 members,
645 failed_installations: vec![],
646 },
647 default_metadata(alix.inbox_id().to_string()),
648 );
649
650 let a = synthesize_initial_component_values_from_extensions(&alix.context, &exts).await?;
651 let b = synthesize_initial_component_values_from_extensions(&alix.context, &exts).await?;
652 assert_eq!(a, b, "bootstrap synthesis must be byte-stable");
653 }
654
655 #[xmtp_common::test(unwrap_try = true)]
656 async fn synthesize_partitions_failed_installations_by_owner() {
657 tester!(alix);
661 tester!(bo);
662 load_identity_updates(
663 alix.context.api(),
664 &alix.context.db(),
665 &[alix.inbox_id(), bo.inbox_id()],
666 )
667 .await?;
668
669 let alix_install = alix.installation_id.to_vec();
670 let bo_install = bo.installation_id.to_vec();
671 let alix_inbox = InboxId::from_hex(alix.inbox_id())?;
672 let bo_inbox = InboxId::from_hex(bo.inbox_id())?;
673 let alix_seq = alix
674 .context
675 .db()
676 .get_latest_sequence_id_for_inbox(alix.inbox_id())? as u64;
677 let bo_seq = alix
678 .context
679 .db()
680 .get_latest_sequence_id_for_inbox(bo.inbox_id())? as u64;
681
682 let mut members = HashMap::new();
683 members.insert(alix.inbox_id().to_string(), alix_seq);
684 members.insert(bo.inbox_id().to_string(), bo_seq);
685 let exts = build_test_extensions(
686 default_gmm(),
687 GroupMembershipProto {
688 members,
689 failed_installations: vec![alix_install.clone(), bo_install.clone()],
690 },
691 default_metadata(alix.inbox_id().to_string()),
692 );
693
694 let out = synthesize_initial_component_values_from_extensions(&alix.context, &exts).await?;
695
696 let bytes = out.get(&ComponentId::GROUP_MEMBERSHIP).unwrap();
697 let decoded =
698 xmtp_mls_common::app_data::migration::decode_group_membership_delta(bytes).unwrap();
699 assert_eq!(decoded.len(), 2);
700 let v1_a = unwrap_v1(decoded.get(&alix_inbox).unwrap());
701 let v1_b = unwrap_v1(decoded.get(&bo_inbox).unwrap());
702 assert_eq!(v1_a.sequence_id, alix_seq);
703 assert_eq!(v1_a.failed_installations, vec![alix_install]);
704 assert_eq!(v1_b.sequence_id, bo_seq);
705 assert_eq!(v1_b.failed_installations, vec![bo_install]);
706 }
707
708 #[xmtp_common::test(unwrap_try = true)]
709 async fn synthesize_drops_unresolvable_failed_installations() {
710 tester!(alix);
713 load_identity_updates(alix.context.api(), &alix.context.db(), &[alix.inbox_id()]).await?;
714
715 let alix_inbox = InboxId::from_hex(alix.inbox_id())?;
716 let alix_seq = alix
717 .context
718 .db()
719 .get_latest_sequence_id_for_inbox(alix.inbox_id())? as u64;
720 let orphan_install = vec![0xDE; 32]; let mut members = HashMap::new();
723 members.insert(alix.inbox_id().to_string(), alix_seq);
724 let exts = build_test_extensions(
725 default_gmm(),
726 GroupMembershipProto {
727 members,
728 failed_installations: vec![orphan_install],
729 },
730 default_metadata(alix.inbox_id().to_string()),
731 );
732
733 let out = synthesize_initial_component_values_from_extensions(&alix.context, &exts).await?;
734 let bytes = out.get(&ComponentId::GROUP_MEMBERSHIP).unwrap();
735 let decoded =
736 xmtp_mls_common::app_data::migration::decode_group_membership_delta(bytes).unwrap();
737 let v1 = unwrap_v1(decoded.get(&alix_inbox).unwrap());
738 assert_eq!(
739 v1.failed_installations,
740 Vec::<Vec<u8>>::new(),
741 "orphan failed_installation must be dropped"
742 );
743 }
744
745 #[xmtp_common::test(unwrap_try = true)]
746 async fn synthesize_emits_expected_component_keys() {
747 tester!(alix);
751 let exts = build_test_extensions(
752 default_gmm(),
753 GroupMembershipProto::default(),
754 default_metadata(alix.inbox_id().to_string()),
755 );
756 let out = synthesize_initial_component_values_from_extensions(&alix.context, &exts).await?;
757 assert!(out.contains_key(&ComponentId::COMPONENT_REGISTRY));
758 assert!(out.contains_key(&ComponentId::GROUP_MEMBERSHIP));
759 assert!(out.contains_key(&ComponentId::ADMIN_LIST));
760 assert!(out.contains_key(&ComponentId::SUPER_ADMIN_LIST));
761 assert!(out.contains_key(&ComponentId::CREATOR_INBOX_ID));
762 assert!(out.contains_key(&ComponentId::CONVERSATION_TYPE));
763 assert!(!out.contains_key(&ComponentId::DM_MEMBERS));
764 assert!(!out.contains_key(&ComponentId::ONESHOT_MESSAGE));
765 }
766
767 #[xmtp_common::test(unwrap_try = true)]
768 async fn synthesize_partitions_against_snapshotted_view_not_latest() {
769 use std::collections::BTreeSet;
787
788 tester!(alix);
789 let alix2 = alix.new_installation().await;
790 tester!(bo);
791 let bo2 = bo.new_installation().await;
792
793 load_identity_updates(
796 alix.context.api(),
797 &alix.context.db(),
798 &[alix.inbox_id(), bo.inbox_id()],
799 )
800 .await?;
801
802 let alix_snapshot_seq =
807 alix.context
808 .db()
809 .get_latest_sequence_id_for_inbox(alix.inbox_id())? as u64;
810 let bo_snapshot_seq = alix
811 .context
812 .db()
813 .get_latest_sequence_id_for_inbox(bo.inbox_id())? as u64;
814
815 let alix1_install = alix.installation_id.to_vec();
816 let alix2_install = alix2.installation_id.to_vec();
817 let bo1_install = bo.installation_id.to_vec();
818 let bo2_install = bo2.installation_id.to_vec();
819
820 let alix3 = alix.new_installation().await;
823 let bo3 = bo.new_installation().await;
824 let alix3_install = alix3.installation_id.to_vec();
825 let bo3_install = bo3.installation_id.to_vec();
826
827 load_identity_updates(
831 alix.context.api(),
832 &alix.context.db(),
833 &[alix.inbox_id(), bo.inbox_id()],
834 )
835 .await?;
836
837 let alix_inbox = InboxId::from_hex(alix.inbox_id())?;
838 let bo_inbox = InboxId::from_hex(bo.inbox_id())?;
839
840 let bogus_install = vec![0xCA; 32];
845
846 let mut members = HashMap::new();
847 members.insert(alix.inbox_id().to_string(), alix_snapshot_seq);
848 members.insert(bo.inbox_id().to_string(), bo_snapshot_seq);
849 let exts = build_test_extensions(
850 default_gmm(),
851 GroupMembershipProto {
852 members,
853 failed_installations: vec![
854 alix1_install.clone(),
855 alix2_install.clone(),
856 alix3_install.clone(),
857 bo1_install.clone(),
858 bo2_install.clone(),
859 bo3_install.clone(),
860 bogus_install.clone(),
861 ],
862 },
863 default_metadata(alix.inbox_id().to_string()),
864 );
865
866 let out = synthesize_initial_component_values_from_extensions(&alix.context, &exts).await?;
867 let bytes = out.get(&ComponentId::GROUP_MEMBERSHIP).unwrap();
868 let decoded =
869 xmtp_mls_common::app_data::migration::decode_group_membership_delta(bytes).unwrap();
870
871 let alix_v1 = unwrap_v1(decoded.get(&alix_inbox).unwrap());
872 let bo_v1 = unwrap_v1(decoded.get(&bo_inbox).unwrap());
873
874 assert_eq!(alix_v1.sequence_id, alix_snapshot_seq);
875 assert_eq!(bo_v1.sequence_id, bo_snapshot_seq);
876
877 let alix_failed: BTreeSet<Vec<u8>> = alix_v1.failed_installations.iter().cloned().collect();
878 let bo_failed: BTreeSet<Vec<u8>> = bo_v1.failed_installations.iter().cloned().collect();
879
880 assert!(alix_failed.contains(&alix1_install));
882 assert!(alix_failed.contains(&alix2_install));
883 assert!(bo_failed.contains(&bo1_install));
884 assert!(bo_failed.contains(&bo2_install));
885
886 assert!(!alix_failed.contains(&alix3_install));
889 assert!(!bo_failed.contains(&alix3_install));
890 assert!(!alix_failed.contains(&bo3_install));
891 assert!(!bo_failed.contains(&bo3_install));
892
893 assert!(!alix_failed.contains(&bogus_install));
895 assert!(!bo_failed.contains(&bogus_install));
896
897 assert!(!alix_failed.contains(&bo1_install));
899 assert!(!alix_failed.contains(&bo2_install));
900 assert!(!bo_failed.contains(&alix1_install));
901 assert!(!bo_failed.contains(&alix2_install));
902
903 let again =
907 synthesize_initial_component_values_from_extensions(&alix.context, &exts).await?;
908 assert_eq!(out, again, "bootstrap synthesis must be byte-stable");
909 }
910}