1use std::collections::{BTreeMap, BTreeSet};
5
6use openmls::{
7 extensions::Extensions,
8 group::{GroupContext, MlsGroup as OpenMlsGroup},
9 messages::proposals::AppDataUpdateOperationType,
10};
11use prost::Message as _;
12use tls_codec::{Deserialize, Serialize, VLBytes};
13use xmtp_proto::xmtp::mls::message_contents::{
14 ComponentMetadata, ComponentPermissions, ComponentType,
15 GroupMembership as GroupMembershipProto, MembershipPolicy as MembershipPolicyProto,
16 MetadataPolicy as MetadataPolicyProto, PermissionsUpdatePolicy as PermissionsUpdatePolicyProto,
17 PolicySet as PolicySetProto,
18 membership_policy::{BasePolicy as MembershipBasePolicy, Kind as MembershipPolicyKind},
19 metadata_policy::{Kind as MetadataPolicyKind, MetadataBasePolicy},
20 permissions_update_policy::{Kind as PermissionsPolicyKind, PermissionsBasePolicy},
21};
22
23use crate::{
24 app_data::{
25 component_id::ComponentId,
26 component_registry::{ComponentRegistry, ComponentRegistryError, new_component_metadata},
27 },
28 group_mutable_metadata::MetadataField,
29 inbox_id::{InboxId, InboxIdError},
30 tls_map::{TlsMap, TlsMapDelta, TlsMapMutation},
31 tls_set::TlsSetDelta,
32};
33use xmtp_proto::xmtp::mls::message_contents::GroupMembershipEntry;
34#[derive(Debug, thiserror::Error)]
36pub enum MigrationError {
37 #[error("legacy PolicySet is missing required policy field: {0}")]
40 MissingPolicyField(&'static str),
41
42 #[error("update_metadata_policy references unknown metadata field: {0}")]
46 UnknownMetadataField(String),
47
48 #[error("ADMIN_LIST admin policy is not admin-or-super-admin (got base={0:?})")]
52 NonConstrainedAdminPolicy(Option<i32>),
53
54 #[error("update_permissions_policy must be AllowIfSuperAdmin (got {0:?})")]
58 UpdatePermissionsNotSuperAdmin(Option<i32>),
59
60 #[error("component registry error: {0}")]
61 Registry(#[from] ComponentRegistryError),
62
63 #[error("legacy mutable-metadata extension missing from group")]
64 MissingMutableMetadataExtension,
65
66 #[error("legacy group-membership extension missing from group")]
67 MissingGroupMembershipExtension,
68
69 #[error("legacy GroupMembership extension decode error: {0}")]
70 GroupMembershipDecode(#[from] prost::DecodeError),
71
72 #[error("legacy GroupMutablePermissionsV1 decode error: {0}")]
75 GroupPermissionsDecode(prost::DecodeError),
76
77 #[error("legacy GroupMutableMetadata decode error: {0}")]
78 MutableMetadataDecode(#[from] crate::group_mutable_metadata::GroupMutableMetadataError),
79
80 #[error("legacy GroupMetadata decode error: {0}")]
81 GroupMetadataDecode(#[from] crate::group_metadata::GroupMetadataError),
82
83 #[error("TLS codec error: {0}")]
84 TlsCodec(#[from] tls_codec::Error),
85
86 #[error("invalid inbox id: {0}")]
87 InvalidInboxId(#[from] InboxIdError),
88
89 #[error("unrecognized GROUP_MEMBERSHIP policy (base={0:?})")]
93 UnknownMembershipPolicy(Option<i32>),
94
95 #[error("invalid CONVERSATION_TYPE payload length: expected 4, got {0}")]
96 ConversationTypePayloadLength(usize),
97
98 #[error("DmMembers self-reference: both slots contain inbox id {0}")]
103 DmMembersSelfReference(String),
104
105 #[error("GroupMembershipEntry envelope has unknown or unset version")]
109 GroupMembershipEntryUnknownVersion,
110
111 #[error("legacy failed_installations entry has invalid length: expected 32, got {0}")]
117 InvalidFailedInstallationLength(usize),
118
119 #[error("GROUP_MEMBERSHIP bootstrap delta carried a non-Insert mutation")]
123 GroupMembershipNonInsertBootstrapMutation,
124
125 #[error("GROUP_MEMBERSHIP bootstrap delta has duplicate Insert for inbox {0}")]
131 GroupMembershipDuplicateInbox(String),
132
133 #[error("legacy {field} attribute is not a base-10 i64 string (got {value:?}): {reason}")]
140 InvalidDisappearingTimestamp {
141 field: &'static str,
142 value: String,
143 reason: String,
144 },
145
146 #[error("legacy CommitLogSigner attribute is not valid hex: {reason}")]
148 InvalidCommitLogSignerHex { reason: String },
149
150 #[error("legacy CommitLogSigner length: expected {expected}, got {actual}")]
153 InvalidCommitLogSignerLength { expected: usize, actual: usize },
154}
155
156pub fn synthesize_registry_from_policy_set(
179 policy_set: &PolicySetProto,
180) -> Result<ComponentRegistry, MigrationError> {
181 build_registry(policy_set, true, true)
186}
187
188fn build_registry(
189 policy_set: &PolicySetProto,
190 include_dm_members: bool,
191 include_oneshot_message: bool,
192) -> Result<ComponentRegistry, MigrationError> {
193 let mut registry = ComponentRegistry::new();
194
195 let add_member = policy_set
197 .add_member_policy
198 .as_ref()
199 .ok_or(MigrationError::MissingPolicyField("add_member_policy"))?;
200 let remove_member = policy_set
201 .remove_member_policy
202 .as_ref()
203 .ok_or(MigrationError::MissingPolicyField("remove_member_policy"))?;
204 let add_admin = policy_set
205 .add_admin_policy
206 .as_ref()
207 .ok_or(MigrationError::MissingPolicyField("add_admin_policy"))?;
208 let remove_admin = policy_set
209 .remove_admin_policy
210 .as_ref()
211 .ok_or(MigrationError::MissingPolicyField("remove_admin_policy"))?;
212 let update_permissions =
213 policy_set
214 .update_permissions_policy
215 .as_ref()
216 .ok_or(MigrationError::MissingPolicyField(
217 "update_permissions_policy",
218 ))?;
219
220 validate_update_permissions_is_super_admin(update_permissions)?;
224
225 let known: std::collections::HashSet<&'static str> = metadata_field_registry_mapping()
229 .iter()
230 .map(|(f, _, _)| f.as_str())
231 .collect();
232 for key in policy_set.update_metadata_policy.keys() {
233 if !known.contains(key.as_str()) {
234 return Err(MigrationError::UnknownMetadataField(key.clone()));
235 }
236 }
237
238 for (field, component_id, component_type) in metadata_field_registry_mapping() {
254 let default_base = if matches!(field, MetadataField::MinimumSupportedProtocolVersion) {
255 MetadataBasePolicy::AllowIfSuperAdmin
256 } else {
257 MetadataBasePolicy::AllowIfAdmin
258 };
259 let policy = policy_set
260 .update_metadata_policy
261 .get(field.as_str())
262 .cloned()
263 .unwrap_or_else(|| metadata_policy(default_base));
264
265 registry.set(
266 *component_id,
267 new_component_metadata(
268 ComponentPermissions {
269 insert_policy: Some(policy.clone()),
270 update_policy: Some(policy),
271 delete_policy: Some(metadata_policy(MetadataBasePolicy::AllowIfSuperAdmin)),
272 },
273 *component_type,
274 ),
275 )?;
276 }
277
278 let super_admin = metadata_policy(MetadataBasePolicy::AllowIfSuperAdmin);
291 registry.set(
292 ComponentId::COMMIT_LOG_SIGNER,
293 new_component_metadata(
294 ComponentPermissions {
295 insert_policy: Some(super_admin.clone()),
296 update_policy: Some(super_admin.clone()),
297 delete_policy: Some(super_admin.clone()),
298 },
299 ComponentType::Bytes,
300 ),
301 )?;
302
303 let admin_policy = admin_list_policy_to_metadata_policy(add_admin)?;
305 let remove_admin_policy = admin_list_policy_to_metadata_policy(remove_admin)?;
306 registry.set(
307 ComponentId::ADMIN_LIST,
308 new_component_metadata(
309 ComponentPermissions {
310 insert_policy: Some(admin_policy.clone()),
311 update_policy: Some(admin_policy),
312 delete_policy: Some(remove_admin_policy),
313 },
314 ComponentType::TlsSetInboxId,
315 ),
316 )?;
317
318 registry.set(
320 ComponentId::GROUP_MEMBERSHIP,
321 new_component_metadata(
322 ComponentPermissions {
323 insert_policy: Some(membership_policy_to_metadata_policy(add_member)?),
324 update_policy: Some(metadata_policy(MetadataBasePolicy::Allow)),
325 delete_policy: Some(membership_policy_to_metadata_policy(remove_member)?),
326 },
327 ComponentType::TlsMapInboxIdBytes,
328 ),
329 )?;
330
331 let immutable_permissions = ComponentPermissions {
338 insert_policy: Some(metadata_policy(MetadataBasePolicy::AllowIfSuperAdmin)),
339 update_policy: Some(metadata_policy(MetadataBasePolicy::Deny)),
340 delete_policy: Some(metadata_policy(MetadataBasePolicy::Deny)),
341 };
342 for id in [
343 ComponentId::CONVERSATION_TYPE,
344 ComponentId::CREATOR_INBOX_ID,
345 ] {
346 registry.set(
347 id,
348 new_component_metadata(immutable_permissions.clone(), ComponentType::Bytes),
349 )?;
350 }
351 if include_oneshot_message {
352 registry.set(
353 ComponentId::ONESHOT_MESSAGE,
354 new_component_metadata(immutable_permissions.clone(), ComponentType::Bytes),
355 )?;
356 }
357 if include_dm_members {
358 registry.set(
359 ComponentId::DM_MEMBERS,
360 new_component_metadata(immutable_permissions, ComponentType::TlsSetInboxId),
361 )?;
362 }
363
364 Ok(registry)
365}
366
367fn metadata_field_registry_mapping() -> &'static [(MetadataField, ComponentId, ComponentType)] {
384 &[
385 (
386 MetadataField::GroupName,
387 ComponentId::GROUP_NAME,
388 ComponentType::String,
389 ),
390 (
391 MetadataField::Description,
392 ComponentId::GROUP_DESCRIPTION,
393 ComponentType::String,
394 ),
395 (
396 MetadataField::GroupImageUrlSquare,
397 ComponentId::GROUP_IMAGE_URL,
398 ComponentType::String,
399 ),
400 (
401 MetadataField::MessageDisappearFromNS,
402 ComponentId::MESSAGE_DISAPPEAR_FROM_NS,
403 ComponentType::Bytes,
404 ),
405 (
406 MetadataField::MessageDisappearInNS,
407 ComponentId::MESSAGE_DISAPPEAR_IN_NS,
408 ComponentType::Bytes,
409 ),
410 (
411 MetadataField::AppData,
412 ComponentId::APP_DATA,
413 ComponentType::String,
414 ),
415 (
416 MetadataField::MinimumSupportedProtocolVersion,
417 ComponentId::MIN_SUPPORTED_PROTOCOL_VERSION,
418 ComponentType::String,
419 ),
420 ]
421}
422
423fn metadata_policy(base: MetadataBasePolicy) -> MetadataPolicyProto {
424 MetadataPolicyProto {
425 kind: Some(MetadataPolicyKind::Base(base as i32)),
426 }
427}
428
429fn admin_list_policy_to_metadata_policy(
436 p: &PermissionsUpdatePolicyProto,
437) -> Result<MetadataPolicyProto, MigrationError> {
438 match &p.kind {
439 Some(PermissionsPolicyKind::Base(base)) => match PermissionsBasePolicy::try_from(*base) {
440 Ok(PermissionsBasePolicy::AllowIfAdmin) => {
441 Ok(metadata_policy(MetadataBasePolicy::AllowIfAdmin))
442 }
443 Ok(PermissionsBasePolicy::AllowIfSuperAdmin) => {
444 Ok(metadata_policy(MetadataBasePolicy::AllowIfSuperAdmin))
445 }
446 _ => Err(MigrationError::NonConstrainedAdminPolicy(Some(*base))),
447 },
448 Some(PermissionsPolicyKind::AndCondition(_))
449 | Some(PermissionsPolicyKind::AnyCondition(_))
450 | None => Err(MigrationError::NonConstrainedAdminPolicy(None)),
451 }
452}
453
454fn membership_policy_to_metadata_policy(
460 p: &MembershipPolicyProto,
461) -> Result<MetadataPolicyProto, MigrationError> {
462 match &p.kind {
463 Some(MembershipPolicyKind::Base(base)) => {
464 let mapped = match MembershipBasePolicy::try_from(*base) {
465 Ok(MembershipBasePolicy::Allow) => MetadataBasePolicy::Allow,
466 Ok(MembershipBasePolicy::Deny) => MetadataBasePolicy::Deny,
467 Ok(MembershipBasePolicy::AllowIfAdminOrSuperAdmin) => {
468 MetadataBasePolicy::AllowIfAdmin
469 }
470 Ok(MembershipBasePolicy::AllowIfSuperAdmin) => {
471 MetadataBasePolicy::AllowIfSuperAdmin
472 }
473 _ => return Err(MigrationError::UnknownMembershipPolicy(Some(*base))),
474 };
475 Ok(metadata_policy(mapped))
476 }
477 Some(MembershipPolicyKind::AndCondition(_))
478 | Some(MembershipPolicyKind::AnyCondition(_))
479 | None => Err(MigrationError::UnknownMembershipPolicy(None)),
480 }
481}
482
483fn validate_update_permissions_is_super_admin(
484 p: &PermissionsUpdatePolicyProto,
485) -> Result<(), MigrationError> {
486 match &p.kind {
487 Some(PermissionsPolicyKind::Base(base)) => match PermissionsBasePolicy::try_from(*base) {
488 Ok(PermissionsBasePolicy::AllowIfSuperAdmin) => Ok(()),
489 _ => Err(MigrationError::UpdatePermissionsNotSuperAdmin(Some(*base))),
490 },
491 _ => Err(MigrationError::UpdatePermissionsNotSuperAdmin(None)),
492 }
493}
494
495#[derive(Debug, Clone, PartialEq)]
529pub struct CanonicalBootstrapExpectation {
530 pub strict: BTreeMap<ComponentId, (AppDataUpdateOperationType, Vec<u8>)>,
531 pub expected_registry: BTreeMap<ComponentId, ComponentMetadata>,
532 pub membership_sequence_ids: BTreeMap<InboxId, u64>,
533 pub allowed_failed_installations: BTreeSet<[u8; 32]>,
534}
535
536pub fn synthesize_canonical_subset_for_validation(
551 mls_group: &OpenMlsGroup,
552) -> Result<CanonicalBootstrapExpectation, MigrationError> {
553 synthesize_canonical_subset_from_extensions(mls_group.extensions())
554}
555
556pub fn synthesize_canonical_subset_from_extensions(
559 extensions: &Extensions<GroupContext>,
560) -> Result<CanonicalBootstrapExpectation, MigrationError> {
561 let gmm: crate::group_mutable_metadata::GroupMutableMetadata = extensions.try_into()?;
562 let registry = synthesize_registry_from_extensions(extensions)?;
563 let legacy_membership = extract_legacy_group_membership(extensions)?;
564 let legacy_metadata = crate::group_metadata::GroupMetadata::try_from(extensions)?;
565
566 let mut strict: BTreeMap<ComponentId, (AppDataUpdateOperationType, Vec<u8>)> = BTreeMap::new();
567
568 let mut expected_registry: BTreeMap<ComponentId, ComponentMetadata> = BTreeMap::new();
572 for entry in registry.iter() {
573 let (id, meta) = entry?;
574 expected_registry.insert(id, meta);
575 }
576
577 for (field, component_id, _) in metadata_field_registry_mapping() {
584 if let Some(s) = gmm.attributes.get(field.as_str()) {
585 strict.insert(
586 *component_id,
587 (
588 AppDataUpdateOperationType::Update,
589 encode_metadata_attribute_value(*component_id, s)?,
590 ),
591 );
592 }
593 }
594
595 if let Some(hex_str) = gmm.attributes.get(MetadataField::CommitLogSigner.as_str()) {
599 let raw = hex::decode(hex_str).map_err(|e| MigrationError::InvalidCommitLogSignerHex {
600 reason: e.to_string(),
601 })?;
602 if raw.len() != xmtp_cryptography::configuration::ED25519_KEY_LENGTH {
603 return Err(MigrationError::InvalidCommitLogSignerLength {
604 actual: raw.len(),
605 expected: xmtp_cryptography::configuration::ED25519_KEY_LENGTH,
606 });
607 }
608 strict.insert(
609 ComponentId::COMMIT_LOG_SIGNER,
610 (AppDataUpdateOperationType::Update, raw),
611 );
612 }
613
614 strict.insert(
617 ComponentId::ADMIN_LIST,
618 (
619 AppDataUpdateOperationType::Update,
620 encode_inbox_id_set(&gmm.admin_list)?,
621 ),
622 );
623 strict.insert(
624 ComponentId::SUPER_ADMIN_LIST,
625 (
626 AppDataUpdateOperationType::Update,
627 encode_inbox_id_set(&gmm.super_admin_list)?,
628 ),
629 );
630
631 let conversation_type_proto: xmtp_proto::xmtp::mls::message_contents::ConversationType =
638 legacy_metadata.conversation_type.into();
639 strict.insert(
640 ComponentId::CONVERSATION_TYPE,
641 (
642 AppDataUpdateOperationType::Update,
643 encode_conversation_type(conversation_type_proto as i32),
644 ),
645 );
646 strict.insert(
651 ComponentId::CREATOR_INBOX_ID,
652 (
653 AppDataUpdateOperationType::Update,
654 InboxId::from_hex(&legacy_metadata.creator_inbox_id)?.tls_serialize_detached()?,
655 ),
656 );
657 if let Some(dm) = &legacy_metadata.dm_members {
658 strict.insert(
659 ComponentId::DM_MEMBERS,
660 (AppDataUpdateOperationType::Update, encode_dm_members(dm)?),
661 );
662 }
663 if let Some(oneshot) = &legacy_metadata.oneshot_message {
664 strict.insert(
665 ComponentId::ONESHOT_MESSAGE,
666 (AppDataUpdateOperationType::Update, oneshot.encode_to_vec()),
667 );
668 }
669
670 let mut membership_sequence_ids: BTreeMap<InboxId, u64> = BTreeMap::new();
673 for (inbox_id_hex, seq) in legacy_membership.members.iter() {
674 let inbox_id = InboxId::from_hex(inbox_id_hex)?;
675 membership_sequence_ids.insert(inbox_id, *seq);
676 }
677
678 let mut allowed_failed_installations: BTreeSet<[u8; 32]> = BTreeSet::new();
685 for raw in &legacy_membership.failed_installations {
686 let key: [u8; 32] = raw
687 .as_slice()
688 .try_into()
689 .map_err(|_| MigrationError::InvalidFailedInstallationLength(raw.len()))?;
690 allowed_failed_installations.insert(key);
691 }
692
693 Ok(CanonicalBootstrapExpectation {
694 strict,
695 expected_registry,
696 membership_sequence_ids,
697 allowed_failed_installations,
698 })
699}
700
701fn synthesize_registry_from_extensions(
706 extensions: &Extensions<GroupContext>,
707) -> Result<ComponentRegistry, MigrationError> {
708 let policy_set_bytes = find_unknown_extension(
709 extensions,
710 xmtp_configuration::GROUP_PERMISSIONS_EXTENSION_ID,
711 )
712 .ok_or(MigrationError::MissingPolicyField(
713 "group_permissions extension",
714 ))?;
715 let permissions_proto =
716 xmtp_proto::xmtp::mls::message_contents::GroupMutablePermissionsV1::decode(
717 policy_set_bytes.as_slice(),
718 )
719 .map_err(MigrationError::GroupPermissionsDecode)?;
720 let policy_set = permissions_proto
721 .policies
722 .ok_or(MigrationError::MissingPolicyField("policies"))?;
723
724 let legacy_metadata = crate::group_metadata::GroupMetadata::try_from(extensions)?;
725 build_registry(
726 &policy_set,
727 legacy_metadata.dm_members.is_some(),
728 legacy_metadata.oneshot_message.is_some(),
729 )
730}
731
732fn find_unknown_extension(extensions: &Extensions<GroupContext>, id: u16) -> Option<&Vec<u8>> {
733 use openmls::extensions::{Extension, UnknownExtension};
734 extensions.iter().find_map(|extension| match extension {
735 Extension::Unknown(eid, UnknownExtension(data)) if *eid == id => Some(data),
736 _ => None,
737 })
738}
739
740fn extract_legacy_group_membership(
741 extensions: &Extensions<GroupContext>,
742) -> Result<GroupMembershipProto, MigrationError> {
743 let bytes = find_unknown_extension(
744 extensions,
745 xmtp_configuration::GROUP_MEMBERSHIP_EXTENSION_ID,
746 )
747 .ok_or(MigrationError::MissingGroupMembershipExtension)?;
748 Ok(GroupMembershipProto::decode(bytes.as_slice())?)
749}
750
751fn encode_metadata_attribute_value(
759 component_id: ComponentId,
760 legacy_value: &str,
761) -> Result<Vec<u8>, MigrationError> {
762 match component_id {
763 ComponentId::MESSAGE_DISAPPEAR_FROM_NS => {
764 parse_disappearing_i64("messageDisappearFromNS", legacy_value)
765 }
766 ComponentId::MESSAGE_DISAPPEAR_IN_NS => {
767 parse_disappearing_i64("messageDisappearInNS", legacy_value)
768 }
769 _ => Ok(legacy_value.as_bytes().to_vec()),
770 }
771}
772
773fn parse_disappearing_i64(
774 field: &'static str,
775 legacy_value: &str,
776) -> Result<Vec<u8>, MigrationError> {
777 let n: i64 = legacy_value
778 .parse()
779 .map_err(
780 |err: std::num::ParseIntError| MigrationError::InvalidDisappearingTimestamp {
781 field,
782 value: legacy_value.to_string(),
783 reason: err.to_string(),
784 },
785 )?;
786 Ok(n.to_be_bytes().to_vec())
787}
788
789fn encode_inbox_id_set(inbox_ids_hex: &[String]) -> Result<Vec<u8>, MigrationError> {
799 let ids: Vec<InboxId> = inbox_ids_hex
800 .iter()
801 .map(|s| InboxId::from_hex(s))
802 .collect::<Result<Vec<_>, _>>()?;
803 let mut sorted_ids: Vec<InboxId> = ids;
808 sorted_ids.sort();
809 sorted_ids.dedup();
810 let mut delta: TlsSetDelta<InboxId> = TlsSetDelta::new();
811 for id in sorted_ids {
812 delta = delta.insert(id);
813 }
814 Ok(delta.tls_serialize_detached()?)
815}
816
817fn encode_dm_members(
822 dm: &crate::group_metadata::DmMembers<xmtp_id::InboxId>,
823) -> Result<Vec<u8>, MigrationError> {
824 let one_str: &str = dm.member_one_inbox_id.as_ref();
830 let two_str: &str = dm.member_two_inbox_id.as_ref();
831 let one = InboxId::from_hex(one_str)?;
832 let two = InboxId::from_hex(two_str)?;
833 if one == two {
834 return Err(MigrationError::DmMembersSelfReference(format!(
838 "{} (member_one={one_str}, member_two={two_str})",
839 one.to_hex(),
840 )));
841 }
842 let (a, b) = if one <= two { (one, two) } else { (two, one) };
844 let delta = TlsSetDelta::<InboxId>::new().insert(a).insert(b);
845 Ok(delta.tls_serialize_detached()?)
846}
847
848pub(crate) fn encode_conversation_type(value: i32) -> Vec<u8> {
852 value.to_be_bytes().to_vec()
853}
854
855#[cfg(test)]
865pub(crate) fn decode_conversation_type(bytes: &[u8]) -> Result<i32, MigrationError> {
866 let arr: [u8; 4] = bytes
867 .try_into()
868 .map_err(|_| MigrationError::ConversationTypePayloadLength(bytes.len()))?;
869 Ok(i32::from_be_bytes(arr))
870}
871
872pub fn encode_group_membership_delta(
883 entries: &BTreeMap<InboxId, GroupMembershipEntry>,
884) -> Result<Vec<u8>, MigrationError> {
885 let mut delta: TlsMapDelta<InboxId, VLBytes> = TlsMapDelta::new();
886 for (inbox_id, entry) in entries {
887 delta = delta.insert(*inbox_id, VLBytes::new(entry.encode_to_vec()));
888 }
889 Ok(delta.tls_serialize_detached()?)
890}
891
892pub fn decode_group_membership_delta(
898 bytes: &[u8],
899) -> Result<BTreeMap<InboxId, GroupMembershipEntry>, MigrationError> {
900 let delta = TlsMapDelta::<InboxId, VLBytes>::tls_deserialize_exact(bytes)?;
901 let mut out: BTreeMap<InboxId, GroupMembershipEntry> = BTreeMap::new();
902 for mutation in delta.mutations {
903 let (key, value) = match mutation {
904 TlsMapMutation::Insert { key, value } => (key, value),
905 TlsMapMutation::Update { .. } | TlsMapMutation::Delete { .. } => {
906 return Err(MigrationError::GroupMembershipNonInsertBootstrapMutation);
907 }
908 };
909 let envelope = GroupMembershipEntry::decode(value.as_slice())?;
910 if envelope.version.is_none() {
911 return Err(MigrationError::GroupMembershipEntryUnknownVersion);
912 }
913 if out.insert(key, envelope).is_some() {
914 return Err(MigrationError::GroupMembershipDuplicateInbox(key.to_hex()));
915 }
916 }
917 Ok(out)
918}
919
920pub fn encode_group_membership_dict(
928 entries: &BTreeMap<InboxId, GroupMembershipEntry>,
929) -> Result<Vec<u8>, MigrationError> {
930 let mut map: TlsMap<InboxId, VLBytes> = TlsMap::new();
931 for (inbox_id, entry) in entries {
932 map.insert(*inbox_id, VLBytes::new(entry.encode_to_vec()))
933 .map_err(|e| {
934 MigrationError::TlsCodec(tls_codec::Error::EncodingError(e.to_string()))
935 })?;
936 }
937 Ok(map.tls_serialize_detached()?)
938}
939
940pub fn decode_group_membership_dict(
945 bytes: &[u8],
946) -> Result<BTreeMap<InboxId, GroupMembershipEntry>, MigrationError> {
947 let snapshot = TlsMap::<InboxId, VLBytes>::tls_deserialize_exact(bytes)?;
948 let mut out: BTreeMap<InboxId, GroupMembershipEntry> = BTreeMap::new();
949 for (key, value) in snapshot.into_iter() {
950 let envelope = GroupMembershipEntry::decode(value.as_slice())?;
951 if envelope.version.is_none() {
952 return Err(MigrationError::GroupMembershipEntryUnknownVersion);
953 }
954 out.insert(key, envelope);
955 }
956 Ok(out)
957}
958
959#[cfg(test)]
960mod tests {
961 use super::*;
962 use crate::inbox_id::INBOX_ID_BYTE_LEN;
963 use xmtp_proto::xmtp::mls::message_contents::{
964 MembershipPolicy as MembershipPolicyProto, MetadataPolicy as MetadataPolicyProto,
965 PermissionsUpdatePolicy as PermissionsUpdatePolicyProto,
966 group_membership_entry::{
967 V1 as GroupMembershipEntryV1, Version as GroupMembershipEntryVersion,
968 },
969 membership_policy::{BasePolicy as MembershipBase, Kind as MembershipKind},
970 metadata_policy::Kind as MetadataKind,
971 permissions_update_policy::Kind as PermissionsKind,
972 };
973
974 fn allow_metadata() -> MetadataPolicyProto {
975 MetadataPolicyProto {
976 kind: Some(MetadataKind::Base(MetadataBasePolicy::Allow as i32)),
977 }
978 }
979 fn allow_if_admin_metadata() -> MetadataPolicyProto {
980 MetadataPolicyProto {
981 kind: Some(MetadataKind::Base(MetadataBasePolicy::AllowIfAdmin as i32)),
982 }
983 }
984 fn allow_if_super_admin_metadata() -> MetadataPolicyProto {
985 MetadataPolicyProto {
986 kind: Some(MetadataKind::Base(
987 MetadataBasePolicy::AllowIfSuperAdmin as i32,
988 )),
989 }
990 }
991 fn admin_only_perms() -> PermissionsUpdatePolicyProto {
992 PermissionsUpdatePolicyProto {
993 kind: Some(PermissionsKind::Base(
994 PermissionsBasePolicy::AllowIfAdmin as i32,
995 )),
996 }
997 }
998 fn super_admin_only_perms() -> PermissionsUpdatePolicyProto {
999 PermissionsUpdatePolicyProto {
1000 kind: Some(PermissionsKind::Base(
1001 PermissionsBasePolicy::AllowIfSuperAdmin as i32,
1002 )),
1003 }
1004 }
1005 fn allow_membership() -> MembershipPolicyProto {
1006 MembershipPolicyProto {
1007 kind: Some(MembershipKind::Base(MembershipBase::Allow as i32)),
1008 }
1009 }
1010
1011 fn minimal_default_policy_set() -> PolicySetProto {
1012 PolicySetProto {
1013 add_member_policy: Some(allow_membership()),
1014 remove_member_policy: Some(allow_membership()),
1015 update_metadata_policy: std::collections::HashMap::new(),
1016 add_admin_policy: Some(admin_only_perms()),
1017 remove_admin_policy: Some(admin_only_perms()),
1018 update_permissions_policy: Some(super_admin_only_perms()),
1019 }
1020 }
1021
1022 #[test]
1023 fn synthesizes_all_well_known_components() {
1024 let registry = synthesize_registry_from_policy_set(&minimal_default_policy_set()).unwrap();
1025 for (_, id, _) in metadata_field_registry_mapping() {
1027 assert!(registry.contains(id), "missing {}", id);
1028 }
1029 assert!(registry.contains(&ComponentId::COMMIT_LOG_SIGNER));
1031 assert!(registry.contains(&ComponentId::ADMIN_LIST));
1033 assert!(registry.contains(&ComponentId::GROUP_MEMBERSHIP));
1034 assert!(registry.contains(&ComponentId::CONVERSATION_TYPE));
1036 assert!(registry.contains(&ComponentId::CREATOR_INBOX_ID));
1037 assert!(registry.contains(&ComponentId::DM_MEMBERS));
1038 assert!(registry.contains(&ComponentId::ONESHOT_MESSAGE));
1039 assert!(!registry.contains(&ComponentId::SUPER_ADMIN_LIST));
1041 assert!(!registry.contains(&ComponentId::COMPONENT_REGISTRY));
1042 }
1043
1044 #[test]
1045 fn synthesis_defaults_to_admin_for_missing_metadata_fields() {
1046 let registry = synthesize_registry_from_policy_set(&minimal_default_policy_set()).unwrap();
1051 let meta = registry.get(&ComponentId::GROUP_NAME).unwrap().unwrap();
1052 let perms = meta.permissions.unwrap();
1053 assert_eq!(perms.insert_policy.unwrap(), allow_if_admin_metadata());
1054 assert_eq!(perms.update_policy.unwrap(), allow_if_admin_metadata());
1055 }
1056
1057 #[test]
1058 fn synthesis_defaults_min_version_floor_to_super_admin_when_missing() {
1059 let registry = synthesize_registry_from_policy_set(&minimal_default_policy_set()).unwrap();
1064 let meta = registry
1065 .get(&ComponentId::MIN_SUPPORTED_PROTOCOL_VERSION)
1066 .unwrap()
1067 .unwrap();
1068 let perms = meta.permissions.unwrap();
1069 assert_eq!(
1070 perms.insert_policy.unwrap(),
1071 allow_if_super_admin_metadata()
1072 );
1073 assert_eq!(
1074 perms.update_policy.unwrap(),
1075 allow_if_super_admin_metadata()
1076 );
1077 }
1078
1079 #[test]
1080 fn synthesis_preserves_a_stored_min_version_policy() {
1081 let mut ps = minimal_default_policy_set();
1085 ps.update_metadata_policy.insert(
1086 MetadataField::MinimumSupportedProtocolVersion
1087 .as_str()
1088 .to_string(),
1089 allow_metadata(),
1090 );
1091 let registry = synthesize_registry_from_policy_set(&ps).unwrap();
1092 let meta = registry
1093 .get(&ComponentId::MIN_SUPPORTED_PROTOCOL_VERSION)
1094 .unwrap()
1095 .unwrap();
1096 assert_eq!(
1097 meta.permissions.unwrap().insert_policy.unwrap(),
1098 allow_metadata()
1099 );
1100 }
1101
1102 #[test]
1103 fn synthesis_uses_per_field_policy_when_present() {
1104 let mut ps = minimal_default_policy_set();
1105 ps.update_metadata_policy
1106 .insert("group_name".to_string(), allow_if_admin_metadata());
1107 let registry = synthesize_registry_from_policy_set(&ps).unwrap();
1108 let meta = registry.get(&ComponentId::GROUP_NAME).unwrap().unwrap();
1109 assert_eq!(
1110 meta.permissions.clone().unwrap().insert_policy.unwrap(),
1111 allow_if_admin_metadata()
1112 );
1113 let desc = registry
1115 .get(&ComponentId::GROUP_DESCRIPTION)
1116 .unwrap()
1117 .unwrap();
1118 assert_eq!(
1119 desc.permissions.unwrap().insert_policy.unwrap(),
1120 allow_if_admin_metadata()
1121 );
1122 }
1123
1124 #[test]
1125 fn synthesis_rejects_unknown_metadata_field() {
1126 let mut ps = minimal_default_policy_set();
1127 ps.update_metadata_policy
1128 .insert("something_new".to_string(), allow_metadata());
1129 let err = synthesize_registry_from_policy_set(&ps).unwrap_err();
1130 assert!(matches!(err, MigrationError::UnknownMetadataField(f) if f == "something_new"));
1131 }
1132
1133 #[test]
1134 fn synthesis_rejects_non_super_admin_update_permissions() {
1135 let mut ps = minimal_default_policy_set();
1136 ps.update_permissions_policy = Some(admin_only_perms());
1137 let err = synthesize_registry_from_policy_set(&ps).unwrap_err();
1138 assert!(matches!(
1139 err,
1140 MigrationError::UpdatePermissionsNotSuperAdmin(_)
1141 ));
1142 }
1143
1144 #[test]
1145 fn synthesis_admin_list_super_admin_only() {
1146 let mut ps = minimal_default_policy_set();
1147 ps.add_admin_policy = Some(super_admin_only_perms());
1148 ps.remove_admin_policy = Some(super_admin_only_perms());
1149 let registry = synthesize_registry_from_policy_set(&ps).unwrap();
1150 let admin = registry.get(&ComponentId::ADMIN_LIST).unwrap().unwrap();
1151 let perms = admin.permissions.unwrap();
1152 assert_eq!(
1153 perms.insert_policy.unwrap(),
1154 MetadataPolicyProto {
1155 kind: Some(MetadataKind::Base(
1156 MetadataBasePolicy::AllowIfSuperAdmin as i32
1157 ))
1158 }
1159 );
1160 }
1161
1162 #[xmtp_common::test(unwrap_try = true)]
1163 fn metadata_field_mapping_agrees_with_dispatch_table() {
1164 use crate::app_data::registry_table::lookup_component;
1171 for (_field, component_id, expected_type) in metadata_field_registry_mapping() {
1172 let dispatched = lookup_component(*component_id)
1173 .unwrap_or_else(|| panic!("WELL_KNOWN missing entry for {component_id}"));
1174 assert_eq!(
1175 dispatched.component_type(),
1176 *expected_type,
1177 "metadata_field_registry_mapping disagrees with WELL_KNOWN for {component_id}"
1178 );
1179 }
1180 }
1181
1182 #[test]
1183 fn synthesis_sets_correct_component_type_per_field() {
1184 let registry = synthesize_registry_from_policy_set(&minimal_default_policy_set()).unwrap();
1188 let expected: &[(ComponentId, ComponentType)] = &[
1189 (ComponentId::GROUP_NAME, ComponentType::String),
1190 (ComponentId::GROUP_DESCRIPTION, ComponentType::String),
1191 (ComponentId::GROUP_IMAGE_URL, ComponentType::String),
1192 (ComponentId::APP_DATA, ComponentType::String),
1193 (
1194 ComponentId::MIN_SUPPORTED_PROTOCOL_VERSION,
1195 ComponentType::String,
1196 ),
1197 (ComponentId::MESSAGE_DISAPPEAR_FROM_NS, ComponentType::Bytes),
1198 (ComponentId::MESSAGE_DISAPPEAR_IN_NS, ComponentType::Bytes),
1199 ];
1200 for (id, ty) in expected {
1201 let meta = registry.get(id).unwrap().unwrap();
1202 assert_eq!(
1203 meta.component_type, *ty as i32,
1204 "wrong component_type for {id}"
1205 );
1206 }
1207 }
1208
1209 #[test]
1210 fn synthesis_deterministic_bytes() {
1211 let a = synthesize_registry_from_policy_set(&minimal_default_policy_set()).unwrap();
1214 let b = synthesize_registry_from_policy_set(&minimal_default_policy_set()).unwrap();
1215 assert_eq!(a.to_bytes().unwrap(), b.to_bytes().unwrap());
1216 }
1217
1218 #[test]
1219 fn membership_policy_rejects_combinator() {
1220 use xmtp_proto::xmtp::mls::message_contents::{
1221 MembershipPolicy as MembershipPolicyProto,
1222 membership_policy::{AndCondition as AndCondProto, Kind as MembershipKind},
1223 };
1224 let combinator = MembershipPolicyProto {
1225 kind: Some(MembershipKind::AndCondition(AndCondProto {
1226 policies: vec![],
1227 })),
1228 };
1229 let err = membership_policy_to_metadata_policy(&combinator).unwrap_err();
1230 assert!(matches!(err, MigrationError::UnknownMembershipPolicy(None)));
1231 }
1232
1233 #[test]
1234 fn membership_policy_rejects_unknown_base() {
1235 use xmtp_proto::xmtp::mls::message_contents::{
1236 MembershipPolicy as MembershipPolicyProto, membership_policy::Kind as MembershipKind,
1237 };
1238 let unknown = MembershipPolicyProto {
1239 kind: Some(MembershipKind::Base(9999)),
1240 };
1241 let err = membership_policy_to_metadata_policy(&unknown).unwrap_err();
1242 assert!(matches!(
1243 err,
1244 MigrationError::UnknownMembershipPolicy(Some(9999))
1245 ));
1246 }
1247
1248 #[test]
1249 fn group_membership_encode_round_trip() {
1250 let mut entries: BTreeMap<InboxId, GroupMembershipEntryV1> = BTreeMap::new();
1251 entries.insert(
1252 InboxId::from_bytes([0x01; 32]),
1253 GroupMembershipEntryV1 {
1254 sequence_id: 42,
1255 failed_installations: vec![vec![0xAA; 16]],
1256 },
1257 );
1258 entries.insert(
1259 InboxId::from_bytes([0x02; 32]),
1260 GroupMembershipEntryV1 {
1261 sequence_id: 99,
1262 failed_installations: vec![],
1263 },
1264 );
1265 let entries = entries
1266 .into_iter()
1267 .map(|(k, v)| {
1268 (
1269 k,
1270 GroupMembershipEntry {
1271 version: Some(GroupMembershipEntryVersion::V1(v)),
1272 },
1273 )
1274 })
1275 .collect::<BTreeMap<_, _>>();
1276 let wire_bytes = encode_group_membership_delta(&entries).unwrap();
1279 let decoded = decode_group_membership_delta(&wire_bytes).unwrap();
1280 assert_eq!(decoded, entries);
1281 }
1282
1283 #[test]
1284 fn admin_list_policy_rejects_combinator() {
1285 use xmtp_proto::xmtp::mls::message_contents::{
1286 PermissionsUpdatePolicy as PermissionsUpdatePolicyProto,
1287 permissions_update_policy::{AndCondition as AndCondProto, Kind as PermissionsKind},
1288 };
1289 let combinator = PermissionsUpdatePolicyProto {
1290 kind: Some(PermissionsKind::AndCondition(AndCondProto {
1291 policies: vec![],
1292 })),
1293 };
1294 let err = admin_list_policy_to_metadata_policy(&combinator).unwrap_err();
1295 assert!(matches!(
1296 err,
1297 MigrationError::NonConstrainedAdminPolicy(None)
1298 ));
1299 }
1300
1301 #[test]
1302 fn decode_wire_rejects_unset_version() {
1303 let envelope = GroupMembershipEntry { version: None };
1308 let delta = TlsMapDelta::<InboxId, VLBytes>::new().insert(
1309 InboxId::from_bytes([0x04; 32]),
1310 VLBytes::new(envelope.encode_to_vec()),
1311 );
1312 let bytes = delta.tls_serialize_detached().unwrap();
1313 let err = decode_group_membership_delta(&bytes).unwrap_err();
1314 assert!(matches!(
1315 err,
1316 MigrationError::GroupMembershipEntryUnknownVersion
1317 ));
1318 }
1319
1320 #[test]
1321 fn decode_wire_rejects_non_insert_mutation() {
1322 let delta = TlsMapDelta::<InboxId, VLBytes>::new().delete(InboxId::from_bytes([0x05; 32]));
1325 let bytes = delta.tls_serialize_detached().unwrap();
1326 let err = decode_group_membership_delta(&bytes).unwrap_err();
1327 assert!(matches!(
1328 err,
1329 MigrationError::GroupMembershipNonInsertBootstrapMutation
1330 ));
1331 }
1332
1333 #[test]
1334 fn decode_dict_round_trips() {
1335 let mut snapshot: TlsMap<InboxId, VLBytes> = TlsMap::new();
1339 let inbox = InboxId::from_bytes([0x06; 32]);
1340 let envelope = GroupMembershipEntry {
1341 version: Some(GroupMembershipEntryVersion::V1(GroupMembershipEntryV1 {
1342 sequence_id: 7,
1343 failed_installations: vec![],
1344 })),
1345 };
1346 snapshot
1347 .insert(inbox, VLBytes::new(envelope.encode_to_vec()))
1348 .unwrap();
1349 let bytes = snapshot.tls_serialize_detached().unwrap();
1350 let decoded = decode_group_membership_dict(&bytes).unwrap();
1351 assert_eq!(decoded.len(), 1);
1352 assert!(decoded.contains_key(&inbox));
1353 }
1354
1355 fn hex_inbox(tag: u8) -> String {
1364 hex::encode([tag; INBOX_ID_BYTE_LEN])
1365 }
1366
1367 #[test]
1368 fn encode_inbox_id_set_emits_bootstrap_wire_delta() {
1369 use crate::tls_set::TlsSetMutation;
1375 let ids = vec![hex_inbox(0xAA), hex_inbox(0xBB)];
1376 let bytes = encode_inbox_id_set(&ids).unwrap();
1377 let delta = TlsSetDelta::<InboxId>::tls_deserialize_exact(&bytes)
1378 .expect("decodes as TlsSetDelta<InboxId>");
1379 assert_eq!(delta.mutations.len(), 2);
1380 for (mutation, expected_tag) in delta.mutations.iter().zip([0xAA, 0xBB]) {
1382 match mutation {
1383 TlsSetMutation::Insert(id) => {
1384 assert_eq!(id.as_bytes(), &[expected_tag; INBOX_ID_BYTE_LEN]);
1385 }
1386 other => panic!("expected Insert, got {other:?}"),
1387 }
1388 }
1389 }
1390
1391 #[test]
1392 fn encode_inbox_id_set_rejects_bad_hex() {
1393 let err = encode_inbox_id_set(&["not-hex".to_string()]).unwrap_err();
1394 assert!(matches!(err, MigrationError::InvalidInboxId(_)));
1395 }
1396
1397 #[test]
1398 fn encode_dm_members_produces_two_insert_delta() {
1399 use crate::tls_set::TlsSetMutation;
1400 let dm = crate::group_metadata::DmMembers {
1401 member_one_inbox_id: hex_inbox(0xCC),
1402 member_two_inbox_id: hex_inbox(0xDD),
1403 };
1404 let bytes = encode_dm_members(&dm).unwrap();
1405 let delta = TlsSetDelta::<InboxId>::tls_deserialize_exact(&bytes).unwrap();
1406 assert_eq!(delta.mutations.len(), 2);
1407 for (mutation, expected_tag) in delta.mutations.iter().zip([0xCC, 0xDD]) {
1408 match mutation {
1409 TlsSetMutation::Insert(id) => {
1410 assert_eq!(id.as_bytes(), &[expected_tag; INBOX_ID_BYTE_LEN]);
1411 }
1412 other => panic!("expected Insert, got {other:?}"),
1413 }
1414 }
1415 }
1416
1417 #[test]
1418 fn encode_dm_members_rejects_self_reference() {
1419 let id = hex_inbox(0xEE);
1423 let dm = crate::group_metadata::DmMembers {
1424 member_one_inbox_id: id.clone(),
1425 member_two_inbox_id: id.clone(),
1426 };
1427 let err = encode_dm_members(&dm).unwrap_err();
1428 let MigrationError::DmMembersSelfReference(msg) = err else {
1429 panic!("expected DmMembersSelfReference, got {err:?}");
1430 };
1431 assert!(msg.starts_with(&id), "missing canonical hex: {msg}");
1433 assert!(msg.contains(&format!("member_one={id}")), "{msg}");
1434 assert!(msg.contains(&format!("member_two={id}")), "{msg}");
1435 }
1436
1437 #[test]
1438 fn encode_dm_members_rejects_case_divergent_self_reference() {
1439 let lower = hex_inbox(0xEE);
1446 let upper = lower.to_ascii_uppercase();
1447 assert_ne!(lower, upper, "test premise: strings must differ");
1448 let dm = crate::group_metadata::DmMembers {
1449 member_one_inbox_id: lower.clone(),
1450 member_two_inbox_id: upper.clone(),
1451 };
1452 let err = encode_dm_members(&dm).unwrap_err();
1453 let MigrationError::DmMembersSelfReference(msg) = err else {
1454 panic!("expected DmMembersSelfReference, got {err:?}");
1455 };
1456 assert!(msg.starts_with(&lower), "missing canonical hex: {msg}");
1459 assert!(msg.contains(&format!("member_one={lower}")), "{msg}");
1460 assert!(msg.contains(&format!("member_two={upper}")), "{msg}");
1461 }
1462
1463 #[test]
1464 fn conversation_type_codec_round_trips() {
1465 for v in [0_i32, 1, 2, -1, i32::MAX, i32::MIN] {
1469 let bytes = encode_conversation_type(v);
1470 assert_eq!(bytes.len(), 4, "always 4 bytes");
1471 assert_eq!(decode_conversation_type(&bytes).unwrap(), v);
1472 }
1473 }
1474
1475 #[test]
1476 fn conversation_type_decode_rejects_wrong_length() {
1477 let err = decode_conversation_type(&[0, 0, 0]).unwrap_err();
1478 assert!(matches!(
1479 err,
1480 MigrationError::ConversationTypePayloadLength(3)
1481 ));
1482 let err = decode_conversation_type(&[0; 8]).unwrap_err();
1483 assert!(matches!(
1484 err,
1485 MigrationError::ConversationTypePayloadLength(8)
1486 ));
1487 }
1488
1489 fn build_test_extensions(
1496 gmm: crate::group_mutable_metadata::GroupMutableMetadata,
1497 policy_set: PolicySetProto,
1498 membership: xmtp_proto::xmtp::mls::message_contents::GroupMembership,
1499 metadata: crate::group_metadata::GroupMetadata,
1500 ) -> Extensions<GroupContext> {
1501 use openmls::extensions::{Extension, Metadata, UnknownExtension};
1502 use xmtp_configuration::{
1503 GROUP_MEMBERSHIP_EXTENSION_ID, GROUP_PERMISSIONS_EXTENSION_ID,
1504 MUTABLE_METADATA_EXTENSION_ID,
1505 };
1506 use xmtp_proto::xmtp::mls::message_contents::GroupMutablePermissionsV1;
1507
1508 let gmm_bytes: Vec<u8> = gmm.try_into().unwrap();
1509 let permissions_bytes = GroupMutablePermissionsV1 {
1510 policies: Some(policy_set),
1511 }
1512 .encode_to_vec();
1513 let membership_bytes = membership.encode_to_vec();
1514 let metadata_bytes: Vec<u8> = metadata.try_into().unwrap();
1515
1516 Extensions::from_vec(vec![
1517 Extension::Unknown(MUTABLE_METADATA_EXTENSION_ID, UnknownExtension(gmm_bytes)),
1518 Extension::Unknown(
1519 GROUP_PERMISSIONS_EXTENSION_ID,
1520 UnknownExtension(permissions_bytes),
1521 ),
1522 Extension::Unknown(
1523 GROUP_MEMBERSHIP_EXTENSION_ID,
1524 UnknownExtension(membership_bytes),
1525 ),
1526 Extension::ImmutableMetadata(Metadata::new(metadata_bytes)),
1527 ])
1528 .unwrap()
1529 }
1530
1531 fn default_gmm() -> crate::group_mutable_metadata::GroupMutableMetadata {
1532 crate::group_mutable_metadata::GroupMutableMetadata::new(
1533 std::collections::HashMap::new(),
1534 Vec::new(),
1535 Vec::new(),
1536 )
1537 }
1538
1539 fn empty_membership() -> xmtp_proto::xmtp::mls::message_contents::GroupMembership {
1540 xmtp_proto::xmtp::mls::message_contents::GroupMembership {
1541 members: std::collections::HashMap::new(),
1542 failed_installations: vec![],
1543 }
1544 }
1545
1546 fn plain_group_metadata() -> crate::group_metadata::GroupMetadata {
1547 crate::group_metadata::GroupMetadata::new(
1548 xmtp_proto::types::ConversationType::Group,
1549 hex_inbox(0x11),
1550 None,
1551 None,
1552 )
1553 }
1554
1555 #[test]
1556 fn canonical_subset_empty_group_omits_optional_seeds() {
1557 let exts = build_test_extensions(
1562 default_gmm(),
1563 minimal_default_policy_set(),
1564 empty_membership(),
1565 plain_group_metadata(),
1566 );
1567 let subset = synthesize_canonical_subset_from_extensions(&exts).unwrap();
1568
1569 assert!(!subset.strict.contains_key(&ComponentId::COMPONENT_REGISTRY));
1573 assert!(subset.strict.contains_key(&ComponentId::ADMIN_LIST));
1574 assert!(subset.strict.contains_key(&ComponentId::SUPER_ADMIN_LIST));
1575 assert!(subset.strict.contains_key(&ComponentId::CONVERSATION_TYPE));
1576 assert!(subset.strict.contains_key(&ComponentId::CREATOR_INBOX_ID));
1577
1578 assert!(!subset.strict.contains_key(&ComponentId::DM_MEMBERS));
1589 assert!(!subset.strict.contains_key(&ComponentId::ONESHOT_MESSAGE));
1590 assert!(!subset.strict.contains_key(&ComponentId::GROUP_NAME));
1591 assert!(
1592 !subset
1593 .strict
1594 .contains_key(&ComponentId::MESSAGE_DISAPPEAR_FROM_NS)
1595 );
1596 assert!(!subset.strict.contains_key(&ComponentId::COMMIT_LOG_SIGNER));
1597
1598 assert!(
1603 !subset
1604 .expected_registry
1605 .contains_key(&ComponentId::DM_MEMBERS)
1606 );
1607 assert!(
1608 !subset
1609 .expected_registry
1610 .contains_key(&ComponentId::ONESHOT_MESSAGE)
1611 );
1612
1613 assert!(subset.membership_sequence_ids.is_empty());
1614 }
1615
1616 #[test]
1617 fn canonical_subset_dm_group_includes_dm_members() {
1618 let dm_members = crate::group_metadata::DmMembers {
1619 member_one_inbox_id: hex_inbox(0x22),
1620 member_two_inbox_id: hex_inbox(0x33),
1621 };
1622 let metadata = crate::group_metadata::GroupMetadata::new(
1623 xmtp_proto::types::ConversationType::Dm,
1624 hex_inbox(0x22),
1625 Some(dm_members.clone()),
1626 None,
1627 );
1628 let exts = build_test_extensions(
1629 default_gmm(),
1630 minimal_default_policy_set(),
1631 empty_membership(),
1632 metadata,
1633 );
1634 let subset = synthesize_canonical_subset_from_extensions(&exts).unwrap();
1635
1636 let (_, dm_bytes) = subset
1637 .strict
1638 .get(&ComponentId::DM_MEMBERS)
1639 .expect("DM group must include DM_MEMBERS in strict");
1640 assert_eq!(*dm_bytes, encode_dm_members(&dm_members).unwrap());
1641
1642 assert!(
1643 subset
1644 .expected_registry
1645 .contains_key(&ComponentId::DM_MEMBERS),
1646 "expected_registry must keep DM_MEMBERS for a DM group"
1647 );
1648 }
1649
1650 #[test]
1651 fn canonical_subset_oneshot_group_includes_oneshot() {
1652 use xmtp_proto::xmtp::mls::message_contents::OneshotMessage;
1653 let oneshot = OneshotMessage { message_type: None };
1658 let metadata = crate::group_metadata::GroupMetadata::new(
1659 xmtp_proto::types::ConversationType::Group,
1660 hex_inbox(0x44),
1661 None,
1662 Some(oneshot.clone()),
1663 );
1664 let exts = build_test_extensions(
1665 default_gmm(),
1666 minimal_default_policy_set(),
1667 empty_membership(),
1668 metadata,
1669 );
1670 let subset = synthesize_canonical_subset_from_extensions(&exts).unwrap();
1671
1672 let (_, oneshot_bytes) = subset
1673 .strict
1674 .get(&ComponentId::ONESHOT_MESSAGE)
1675 .expect("oneshot group must include ONESHOT_MESSAGE");
1676 assert_eq!(*oneshot_bytes, oneshot.encode_to_vec());
1677
1678 assert!(
1679 subset
1680 .expected_registry
1681 .contains_key(&ComponentId::ONESHOT_MESSAGE)
1682 );
1683 }
1684
1685 #[test]
1686 fn canonical_subset_membership_sequence_ids() {
1687 let mut members = std::collections::HashMap::new();
1688 members.insert(hex_inbox(0x55), 7_u64);
1689 members.insert(hex_inbox(0x66), 42_u64);
1690 let membership = xmtp_proto::xmtp::mls::message_contents::GroupMembership {
1691 members,
1692 failed_installations: vec![],
1693 };
1694
1695 let exts = build_test_extensions(
1696 default_gmm(),
1697 minimal_default_policy_set(),
1698 membership,
1699 plain_group_metadata(),
1700 );
1701 let subset = synthesize_canonical_subset_from_extensions(&exts).unwrap();
1702
1703 assert_eq!(subset.membership_sequence_ids.len(), 2);
1704 assert_eq!(
1705 subset
1706 .membership_sequence_ids
1707 .get(&InboxId::from_bytes([0x55; 32]))
1708 .copied(),
1709 Some(7)
1710 );
1711 assert_eq!(
1712 subset
1713 .membership_sequence_ids
1714 .get(&InboxId::from_bytes([0x66; 32]))
1715 .copied(),
1716 Some(42)
1717 );
1718 assert!(subset.allowed_failed_installations.is_empty());
1720 }
1721
1722 #[test]
1723 fn canonical_subset_collects_legacy_failed_installations_into_allow_set() {
1724 let installation_a = [0xAA_u8; 32];
1729 let installation_b = [0xBB_u8; 32];
1730 let membership = xmtp_proto::xmtp::mls::message_contents::GroupMembership {
1731 members: std::collections::HashMap::new(),
1732 failed_installations: vec![
1733 installation_a.to_vec(),
1734 installation_b.to_vec(),
1735 installation_a.to_vec(), ],
1737 };
1738 let exts = build_test_extensions(
1739 default_gmm(),
1740 minimal_default_policy_set(),
1741 membership,
1742 plain_group_metadata(),
1743 );
1744 let subset = synthesize_canonical_subset_from_extensions(&exts).unwrap();
1745
1746 assert_eq!(subset.allowed_failed_installations.len(), 2);
1747 assert!(
1748 subset
1749 .allowed_failed_installations
1750 .contains(&installation_a)
1751 );
1752 assert!(
1753 subset
1754 .allowed_failed_installations
1755 .contains(&installation_b)
1756 );
1757 }
1758
1759 #[test]
1760 fn canonical_subset_rejects_non_32_byte_failed_installation() {
1761 let membership = xmtp_proto::xmtp::mls::message_contents::GroupMembership {
1765 members: std::collections::HashMap::new(),
1766 failed_installations: vec![vec![0xCC; 16]], };
1768 let exts = build_test_extensions(
1769 default_gmm(),
1770 minimal_default_policy_set(),
1771 membership,
1772 plain_group_metadata(),
1773 );
1774 let err = synthesize_canonical_subset_from_extensions(&exts).unwrap_err();
1775 assert!(matches!(
1776 err,
1777 MigrationError::InvalidFailedInstallationLength(16)
1778 ));
1779 }
1780
1781 fn strict_lines(subset: &CanonicalBootstrapExpectation) -> Vec<String> {
1783 subset
1784 .strict
1785 .iter()
1786 .map(|(id, (op, bytes))| format!("{id} {op:?} {}", hex::encode(bytes)))
1787 .collect()
1788 }
1789
1790 fn golden_gmm() -> crate::group_mutable_metadata::GroupMutableMetadata {
1794 use crate::group_mutable_metadata::MetadataField;
1795 let mut attributes = std::collections::HashMap::new();
1796 attributes.insert(
1797 MetadataField::GroupName.to_string(),
1798 "Golden Group".to_string(),
1799 );
1800 attributes.insert(
1801 MetadataField::Description.to_string(),
1802 "golden description".to_string(),
1803 );
1804 attributes.insert(
1805 MetadataField::GroupImageUrlSquare.to_string(),
1806 "https://example.com/golden.png".to_string(),
1807 );
1808 attributes.insert(
1809 MetadataField::AppData.to_string(),
1810 "golden-app-data".to_string(),
1811 );
1812 attributes.insert(
1813 MetadataField::MinimumSupportedProtocolVersion.to_string(),
1814 "1.11.0".to_string(),
1815 );
1816 attributes.insert(
1817 MetadataField::MessageDisappearFromNS.to_string(),
1818 "1000000000".to_string(),
1819 );
1820 attributes.insert(
1821 MetadataField::MessageDisappearInNS.to_string(),
1822 "2000000000".to_string(),
1823 );
1824 attributes.insert(
1825 MetadataField::CommitLogSigner.to_string(),
1826 hex::encode([0xAB; 32]),
1827 );
1828 crate::group_mutable_metadata::GroupMutableMetadata::new(
1829 attributes,
1830 vec![hex_inbox(0x22)],
1831 vec![hex_inbox(0x33)],
1832 )
1833 }
1834
1835 #[test]
1850 fn golden_bootstrap_synthesis_group() {
1851 let exts = build_test_extensions(
1852 golden_gmm(),
1853 minimal_default_policy_set(),
1854 empty_membership(),
1855 plain_group_metadata(),
1856 );
1857 let subset = synthesize_canonical_subset_from_extensions(&exts).unwrap();
1858
1859 let expected: Vec<String> = [
1860 "0x8001 Update 2200003333333333333333333333333333333333333333333333333333333333333333",
1863 "0x8002 Update 2200002222222222222222222222222222222222222222222222222222222222222222",
1865 "0x8004 Update 476f6c64656e2047726f7570",
1867 "0x8005 Update 676f6c64656e206465736372697074696f6e",
1869 "0x8006 Update 68747470733a2f2f6578616d706c652e636f6d2f676f6c64656e2e706e67",
1871 "0x8007 Update 000000003b9aca00",
1873 "0x8008 Update 0000000077359400",
1875 "0x8009 Update 676f6c64656e2d6170702d64617461",
1877 "0x800A Update 312e31312e30",
1879 "0x800B Update abababababababababababababababababababababababababababababababab",
1881 "0xBFFE Update 001111111111111111111111111111111111111111111111111111111111111111",
1883 "0xBFFF Update 00000001",
1885 ]
1886 .into_iter()
1887 .map(String::from)
1888 .collect();
1889
1890 assert_eq!(
1891 strict_lines(&subset),
1892 expected,
1893 "bootstrap synthesis encoder output changed — see the doc comment \
1894 on this test before touching the pins"
1895 );
1896 }
1897
1898 #[test]
1902 fn golden_bootstrap_synthesis_dm_with_oneshot() {
1903 let dm_members = crate::group_metadata::DmMembers {
1904 member_one_inbox_id: hex_inbox(0x22),
1905 member_two_inbox_id: hex_inbox(0x33),
1906 };
1907 let oneshot =
1911 xmtp_proto::xmtp::mls::message_contents::OneshotMessage { message_type: None };
1912 let metadata = crate::group_metadata::GroupMetadata::new(
1913 xmtp_proto::types::ConversationType::Dm,
1914 hex_inbox(0x22),
1915 Some(dm_members),
1916 Some(oneshot),
1917 );
1918 let exts = build_test_extensions(
1919 golden_gmm(),
1920 minimal_default_policy_set(),
1921 empty_membership(),
1922 metadata,
1923 );
1924 let subset = synthesize_canonical_subset_from_extensions(&exts).unwrap();
1925
1926 let expected: Vec<String> = [
1927 "0x8001 Update 2200003333333333333333333333333333333333333333333333333333333333333333",
1928 "0x8002 Update 2200002222222222222222222222222222222222222222222222222222222222222222",
1929 "0x8004 Update 476f6c64656e2047726f7570",
1930 "0x8005 Update 676f6c64656e206465736372697074696f6e",
1931 "0x8006 Update 68747470733a2f2f6578616d706c652e636f6d2f676f6c64656e2e706e67",
1932 "0x8007 Update 000000003b9aca00",
1933 "0x8008 Update 0000000077359400",
1934 "0x8009 Update 676f6c64656e2d6170702d64617461",
1935 "0x800A Update 312e31312e30",
1936 "0x800B Update abababababababababababababababababababababababababababababababab",
1937 "0xBFFC Update ",
1940 "0xBFFD Update 40440000222222222222222222222222222222222222222222222222222222222222222200003333333333333333333333333333333333333333333333333333333333333333",
1943 "0xBFFE Update 002222222222222222222222222222222222222222222222222222222222222222",
1945 "0xBFFF Update 00000002",
1947 ]
1948 .into_iter()
1949 .map(String::from)
1950 .collect();
1951
1952 assert_eq!(
1953 strict_lines(&subset),
1954 expected,
1955 "bootstrap synthesis encoder output changed — see the doc comment \
1956 on golden_bootstrap_synthesis_group before touching the pins"
1957 );
1958 }
1959
1960 #[test]
1961 fn canonical_subset_deterministic_across_calls() {
1962 let make = || {
1966 build_test_extensions(
1967 default_gmm(),
1968 minimal_default_policy_set(),
1969 empty_membership(),
1970 plain_group_metadata(),
1971 )
1972 };
1973 let a = synthesize_canonical_subset_from_extensions(&make()).unwrap();
1974 let b = synthesize_canonical_subset_from_extensions(&make()).unwrap();
1975 assert_eq!(a, b);
1976 }
1977}