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, GroupMembership as GroupMembershipProto, PolicySet as PolicySetProto,
15};
16
17use crate::app_data::creation::{
18 build_registry, encode_conversation_type, encode_dm_members, encode_inbox_id_set,
19 encode_metadata_attribute_value, metadata_field_registry_mapping,
20};
21use crate::{
22 app_data::{
23 component_id::ComponentId,
24 component_registry::{ComponentRegistry, ComponentRegistryError},
25 },
26 group_mutable_metadata::MetadataField,
27 inbox_id::{InboxId, InboxIdError},
28 tls_map::{TlsMap, TlsMapDelta, TlsMapMutation},
29};
30use xmtp_proto::xmtp::mls::message_contents::GroupMembershipEntry;
31
32#[derive(Debug, thiserror::Error)]
34pub enum MigrationError {
35 #[error("legacy PolicySet is missing required policy field: {0}")]
38 MissingPolicyField(&'static str),
39
40 #[error("update_metadata_policy references unknown metadata field: {0}")]
44 UnknownMetadataField(String),
45
46 #[error("ADMIN_LIST admin policy is not deny, admin, or super-admin (got base={0:?})")]
50 NonConstrainedAdminPolicy(Option<i32>),
51
52 #[error("component registry error: {0}")]
53 Registry(#[from] ComponentRegistryError),
54
55 #[error("legacy mutable-metadata extension missing from group")]
56 MissingMutableMetadataExtension,
57
58 #[error("legacy group-membership extension missing from group")]
59 MissingGroupMembershipExtension,
60
61 #[error("legacy GroupMembership extension decode error: {0}")]
62 GroupMembershipDecode(#[from] prost::DecodeError),
63
64 #[error("legacy GroupMutablePermissionsV1 decode error: {0}")]
67 GroupPermissionsDecode(prost::DecodeError),
68
69 #[error("legacy GroupMutableMetadata decode error: {0}")]
70 MutableMetadataDecode(#[from] crate::group_mutable_metadata::GroupMutableMetadataError),
71
72 #[error("legacy GroupMetadata decode error: {0}")]
73 GroupMetadataDecode(#[from] crate::group_metadata::GroupMetadataError),
74
75 #[error("TLS codec error: {0}")]
76 TlsCodec(#[from] tls_codec::Error),
77
78 #[error("invalid inbox id: {0}")]
79 InvalidInboxId(#[from] InboxIdError),
80
81 #[error("unrecognized GROUP_MEMBERSHIP policy (base={0:?})")]
85 UnknownMembershipPolicy(Option<i32>),
86
87 #[error("invalid CONVERSATION_TYPE payload length: expected 4, got {0}")]
88 ConversationTypePayloadLength(usize),
89
90 #[error("DmMembers self-reference: both slots contain inbox id {0}")]
95 DmMembersSelfReference(String),
96
97 #[error("GroupMembershipEntry envelope has unknown or unset version")]
101 GroupMembershipEntryUnknownVersion,
102
103 #[error("legacy failed_installations entry has invalid length: expected 32, got {0}")]
109 InvalidFailedInstallationLength(usize),
110
111 #[error("GROUP_MEMBERSHIP bootstrap delta carried a non-Insert mutation")]
115 GroupMembershipNonInsertBootstrapMutation,
116
117 #[error("GROUP_MEMBERSHIP bootstrap delta has duplicate Insert for inbox {0}")]
123 GroupMembershipDuplicateInbox(String),
124
125 #[error("legacy {field} attribute is not a base-10 i64 string (got {value:?}): {reason}")]
132 InvalidDisappearingTimestamp {
133 field: &'static str,
134 value: String,
135 reason: String,
136 },
137
138 #[error("legacy CommitLogSigner attribute is not valid hex: {reason}")]
140 InvalidCommitLogSignerHex { reason: String },
141
142 #[error("legacy CommitLogSigner length: expected {expected}, got {actual}")]
145 InvalidCommitLogSignerLength { expected: usize, actual: usize },
146}
147
148#[derive(Debug, Clone, PartialEq)]
182pub struct CanonicalBootstrapExpectation {
183 pub strict: BTreeMap<ComponentId, (AppDataUpdateOperationType, Vec<u8>)>,
184 pub expected_registry: BTreeMap<ComponentId, ComponentMetadata>,
185 pub membership_sequence_ids: BTreeMap<InboxId, u64>,
186 pub allowed_failed_installations: BTreeSet<[u8; 32]>,
187}
188
189pub fn synthesize_canonical_subset_for_validation(
204 mls_group: &OpenMlsGroup,
205) -> Result<CanonicalBootstrapExpectation, MigrationError> {
206 synthesize_canonical_subset_from_extensions(mls_group.extensions())
207}
208
209pub fn synthesize_canonical_subset_from_extensions(
212 extensions: &Extensions<GroupContext>,
213) -> Result<CanonicalBootstrapExpectation, MigrationError> {
214 let gmm = crate::group_mutable_metadata::GroupMutableMetadata::try_from(
215 crate::group_mutable_metadata::find_mutable_metadata_extension(extensions)
216 .ok_or(crate::group_mutable_metadata::GroupMutableMetadataError::MissingExtension)?,
217 )?;
218 let registry = synthesize_registry_from_extensions(extensions)?;
219 let legacy_membership = extract_legacy_group_membership(extensions)?;
220 let legacy_metadata = crate::group_metadata::GroupMetadata::try_from(
221 extensions
222 .immutable_metadata()
223 .ok_or(crate::group_metadata::GroupMetadataError::MissingExtension)?
224 .metadata(),
225 )?;
226
227 let mut strict: BTreeMap<ComponentId, (AppDataUpdateOperationType, Vec<u8>)> = BTreeMap::new();
228
229 let mut expected_registry: BTreeMap<ComponentId, ComponentMetadata> = BTreeMap::new();
233 for entry in registry.iter() {
234 let (id, meta) = entry?;
235 expected_registry.insert(id, meta);
236 }
237
238 for (field, component_id, _) in metadata_field_registry_mapping() {
245 if let Some(s) = gmm.attributes.get(field.as_str()) {
246 strict.insert(
247 *component_id,
248 (
249 AppDataUpdateOperationType::Update,
250 encode_metadata_attribute_value(*component_id, s)?,
251 ),
252 );
253 }
254 }
255
256 if let Some(hex_str) = gmm.attributes.get(MetadataField::CommitLogSigner.as_str()) {
260 let raw = hex::decode(hex_str).map_err(|e| MigrationError::InvalidCommitLogSignerHex {
261 reason: e.to_string(),
262 })?;
263 if raw.len() != xmtp_cryptography::configuration::ED25519_KEY_LENGTH {
264 return Err(MigrationError::InvalidCommitLogSignerLength {
265 actual: raw.len(),
266 expected: xmtp_cryptography::configuration::ED25519_KEY_LENGTH,
267 });
268 }
269 strict.insert(
270 ComponentId::COMMIT_LOG_SIGNER,
271 (AppDataUpdateOperationType::Update, raw),
272 );
273 }
274
275 strict.insert(
278 ComponentId::ADMIN_LIST,
279 (
280 AppDataUpdateOperationType::Update,
281 encode_inbox_id_set(&gmm.admin_list)?,
282 ),
283 );
284 strict.insert(
285 ComponentId::SUPER_ADMIN_LIST,
286 (
287 AppDataUpdateOperationType::Update,
288 encode_inbox_id_set(&gmm.super_admin_list)?,
289 ),
290 );
291
292 let conversation_type_proto: xmtp_proto::xmtp::mls::message_contents::ConversationType =
299 legacy_metadata.conversation_type.into();
300 strict.insert(
301 ComponentId::CONVERSATION_TYPE,
302 (
303 AppDataUpdateOperationType::Update,
304 encode_conversation_type(conversation_type_proto as i32),
305 ),
306 );
307 strict.insert(
312 ComponentId::CREATOR_INBOX_ID,
313 (
314 AppDataUpdateOperationType::Update,
315 InboxId::from_hex(&legacy_metadata.creator_inbox_id)?.tls_serialize_detached()?,
316 ),
317 );
318 if let Some(dm) = &legacy_metadata.dm_members {
319 strict.insert(
320 ComponentId::DM_MEMBERS,
321 (AppDataUpdateOperationType::Update, encode_dm_members(dm)?),
322 );
323 }
324 if let Some(oneshot) = &legacy_metadata.oneshot_message {
325 strict.insert(
326 ComponentId::ONESHOT_MESSAGE,
327 (AppDataUpdateOperationType::Update, oneshot.encode_to_vec()),
328 );
329 }
330
331 let mut membership_sequence_ids: BTreeMap<InboxId, u64> = BTreeMap::new();
334 for (inbox_id_hex, seq) in legacy_membership.members.iter() {
335 let inbox_id = InboxId::from_hex(inbox_id_hex)?;
336 membership_sequence_ids.insert(inbox_id, *seq);
337 }
338
339 let mut allowed_failed_installations: BTreeSet<[u8; 32]> = BTreeSet::new();
346 for raw in &legacy_membership.failed_installations {
347 let key: [u8; 32] = raw
348 .as_slice()
349 .try_into()
350 .map_err(|_| MigrationError::InvalidFailedInstallationLength(raw.len()))?;
351 allowed_failed_installations.insert(key);
352 }
353
354 Ok(CanonicalBootstrapExpectation {
355 strict,
356 expected_registry,
357 membership_sequence_ids,
358 allowed_failed_installations,
359 })
360}
361
362fn synthesize_registry_from_extensions(
367 extensions: &Extensions<GroupContext>,
368) -> Result<ComponentRegistry, MigrationError> {
369 let policy_set = extract_legacy_policy_set(extensions)?;
370
371 let legacy_metadata = crate::group_metadata::GroupMetadata::try_from(
372 extensions
373 .immutable_metadata()
374 .ok_or(crate::group_metadata::GroupMetadataError::MissingExtension)?
375 .metadata(),
376 )?;
377 build_registry(
378 &policy_set,
379 legacy_metadata.dm_members.is_some(),
380 legacy_metadata.oneshot_message.is_some(),
381 )
382}
383
384fn extract_legacy_policy_set(
385 extensions: &Extensions<GroupContext>,
386) -> Result<PolicySetProto, MigrationError> {
387 let policy_set_bytes = find_unknown_extension(
388 extensions,
389 xmtp_configuration::GROUP_PERMISSIONS_EXTENSION_ID,
390 )
391 .ok_or(MigrationError::MissingPolicyField(
392 "group_permissions extension",
393 ))?;
394 let permissions_proto =
395 xmtp_proto::xmtp::mls::message_contents::GroupMutablePermissionsV1::decode(
396 policy_set_bytes.as_slice(),
397 )
398 .map_err(MigrationError::GroupPermissionsDecode)?;
399 permissions_proto
400 .policies
401 .ok_or(MigrationError::MissingPolicyField("policies"))
402}
403
404fn find_unknown_extension(extensions: &Extensions<GroupContext>, id: u16) -> Option<&Vec<u8>> {
405 use openmls::extensions::{Extension, UnknownExtension};
406 extensions.iter().find_map(|extension| match extension {
407 Extension::Unknown(eid, UnknownExtension(data)) if *eid == id => Some(data),
408 _ => None,
409 })
410}
411
412fn extract_legacy_group_membership(
413 extensions: &Extensions<GroupContext>,
414) -> Result<GroupMembershipProto, MigrationError> {
415 let bytes = find_unknown_extension(
416 extensions,
417 xmtp_configuration::GROUP_MEMBERSHIP_EXTENSION_ID,
418 )
419 .ok_or(MigrationError::MissingGroupMembershipExtension)?;
420 Ok(GroupMembershipProto::decode(bytes.as_slice())?)
421}
422
423pub fn encode_group_membership_delta(
434 entries: &BTreeMap<InboxId, GroupMembershipEntry>,
435) -> Result<Vec<u8>, MigrationError> {
436 let mut delta: TlsMapDelta<InboxId, VLBytes> = TlsMapDelta::new();
437 for (inbox_id, entry) in entries {
438 delta = delta.insert(*inbox_id, VLBytes::new(entry.encode_to_vec()));
439 }
440 Ok(delta.tls_serialize_detached()?)
441}
442
443pub fn decode_group_membership_delta(
449 bytes: &[u8],
450) -> Result<BTreeMap<InboxId, GroupMembershipEntry>, MigrationError> {
451 let delta = TlsMapDelta::<InboxId, VLBytes>::tls_deserialize_exact(bytes)?;
452 let mut out: BTreeMap<InboxId, GroupMembershipEntry> = BTreeMap::new();
453 for mutation in delta.mutations {
454 let (key, value) = match mutation {
455 TlsMapMutation::Insert { key, value } => (key, value),
456 TlsMapMutation::Update { .. } | TlsMapMutation::Delete { .. } => {
457 return Err(MigrationError::GroupMembershipNonInsertBootstrapMutation);
458 }
459 };
460 let envelope = GroupMembershipEntry::decode(value.as_slice())?;
461 if envelope.version.is_none() {
462 return Err(MigrationError::GroupMembershipEntryUnknownVersion);
463 }
464 if out.insert(key, envelope).is_some() {
465 return Err(MigrationError::GroupMembershipDuplicateInbox(key.to_hex()));
466 }
467 }
468 Ok(out)
469}
470
471pub fn encode_group_membership_dict(
479 entries: &BTreeMap<InboxId, GroupMembershipEntry>,
480) -> Result<Vec<u8>, MigrationError> {
481 let mut map: TlsMap<InboxId, VLBytes> = TlsMap::new();
482 for (inbox_id, entry) in entries {
483 map.insert(*inbox_id, VLBytes::new(entry.encode_to_vec()))
484 .map_err(|e| {
485 MigrationError::TlsCodec(tls_codec::Error::EncodingError(e.to_string()))
486 })?;
487 }
488 Ok(map.tls_serialize_detached()?)
489}
490
491pub fn decode_group_membership_dict(
496 bytes: &[u8],
497) -> Result<BTreeMap<InboxId, GroupMembershipEntry>, MigrationError> {
498 let snapshot = TlsMap::<InboxId, VLBytes>::tls_deserialize_exact(bytes)?;
499 let mut out: BTreeMap<InboxId, GroupMembershipEntry> = BTreeMap::new();
500 for (key, value) in snapshot.into_iter() {
501 let envelope = GroupMembershipEntry::decode(value.as_slice())?;
502 if envelope.version.is_none() {
503 return Err(MigrationError::GroupMembershipEntryUnknownVersion);
504 }
505 out.insert(key, envelope);
506 }
507 Ok(out)
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513 use crate::inbox_id::INBOX_ID_BYTE_LEN;
514 use crate::{
515 app_data::creation::{
516 admin_list_policy_to_metadata_policy, decode_conversation_type,
517 membership_policy_to_metadata_policy, metadata_policy,
518 synthesize_registry_from_policy_set,
519 },
520 tls_set::TlsSetDelta,
521 };
522 use xmtp_proto::xmtp::mls::message_contents::{
523 ComponentType, MembershipPolicy as MembershipPolicyProto,
524 MetadataPolicy as MetadataPolicyProto,
525 PermissionsUpdatePolicy as PermissionsUpdatePolicyProto,
526 group_membership_entry::{
527 V1 as GroupMembershipEntryV1, Version as GroupMembershipEntryVersion,
528 },
529 membership_policy::{BasePolicy as MembershipBase, Kind as MembershipKind},
530 metadata_policy::{
531 AndCondition as MetadataAndCondition, AnyCondition as MetadataAnyCondition,
532 Kind as MetadataKind, MetadataBasePolicy,
533 },
534 permissions_update_policy::{Kind as PermissionsKind, PermissionsBasePolicy},
535 };
536
537 fn allow_metadata() -> MetadataPolicyProto {
538 MetadataPolicyProto {
539 kind: Some(MetadataKind::Base(MetadataBasePolicy::Allow as i32)),
540 }
541 }
542 fn allow_if_admin_metadata() -> MetadataPolicyProto {
543 MetadataPolicyProto {
544 kind: Some(MetadataKind::Base(MetadataBasePolicy::AllowIfAdmin as i32)),
545 }
546 }
547 fn allow_if_super_admin_metadata() -> MetadataPolicyProto {
548 MetadataPolicyProto {
549 kind: Some(MetadataKind::Base(
550 MetadataBasePolicy::AllowIfSuperAdmin as i32,
551 )),
552 }
553 }
554 fn admin_only_perms() -> PermissionsUpdatePolicyProto {
555 PermissionsUpdatePolicyProto {
556 kind: Some(PermissionsKind::Base(
557 PermissionsBasePolicy::AllowIfAdmin as i32,
558 )),
559 }
560 }
561 fn super_admin_only_perms() -> PermissionsUpdatePolicyProto {
562 PermissionsUpdatePolicyProto {
563 kind: Some(PermissionsKind::Base(
564 PermissionsBasePolicy::AllowIfSuperAdmin as i32,
565 )),
566 }
567 }
568 fn deny_perms() -> PermissionsUpdatePolicyProto {
569 PermissionsUpdatePolicyProto {
570 kind: Some(PermissionsKind::Base(PermissionsBasePolicy::Deny as i32)),
571 }
572 }
573 fn allow_membership() -> MembershipPolicyProto {
574 MembershipPolicyProto {
575 kind: Some(MembershipKind::Base(MembershipBase::Allow as i32)),
576 }
577 }
578
579 fn minimal_default_policy_set() -> PolicySetProto {
580 PolicySetProto {
581 add_member_policy: Some(allow_membership()),
582 remove_member_policy: Some(allow_membership()),
583 update_metadata_policy: std::collections::HashMap::new(),
584 add_admin_policy: Some(admin_only_perms()),
585 remove_admin_policy: Some(admin_only_perms()),
586 update_permissions_policy: Some(super_admin_only_perms()),
587 }
588 }
589
590 #[xmtp_common::test(unwrap_try = true)]
591 fn synthesizes_all_well_known_components() {
592 let registry = synthesize_registry_from_policy_set(&minimal_default_policy_set()).unwrap();
593 for (_, id, _) in metadata_field_registry_mapping() {
595 assert!(registry.contains(id), "missing {}", id);
596 }
597 assert!(registry.contains(&ComponentId::COMMIT_LOG_SIGNER));
599 assert!(registry.contains(&ComponentId::ADMIN_LIST));
601 assert!(registry.contains(&ComponentId::GROUP_MEMBERSHIP));
602 assert!(registry.contains(&ComponentId::CONVERSATION_TYPE));
604 assert!(registry.contains(&ComponentId::CREATOR_INBOX_ID));
605 assert!(registry.contains(&ComponentId::DM_MEMBERS));
606 assert!(registry.contains(&ComponentId::ONESHOT_MESSAGE));
607 assert!(!registry.contains(&ComponentId::SUPER_ADMIN_LIST));
609 assert!(!registry.contains(&ComponentId::COMPONENT_REGISTRY));
610 }
611
612 #[xmtp_common::test(unwrap_try = true)]
613 fn synthesis_defaults_to_admin_for_missing_metadata_fields() {
614 let registry = synthesize_registry_from_policy_set(&minimal_default_policy_set()).unwrap();
619 let meta = registry.get(&ComponentId::GROUP_NAME).unwrap().unwrap();
620 let perms = meta.permissions.unwrap();
621 assert_eq!(perms.insert_policy.unwrap(), allow_if_admin_metadata());
622 assert_eq!(perms.update_policy.unwrap(), allow_if_admin_metadata());
623 }
624
625 #[xmtp_common::test(unwrap_try = true)]
626 fn synthesis_defaults_min_version_floor_to_super_admin_when_missing() {
627 let registry = synthesize_registry_from_policy_set(&minimal_default_policy_set()).unwrap();
632 let meta = registry
633 .get(&ComponentId::MIN_SUPPORTED_PROTOCOL_VERSION)
634 .unwrap()
635 .unwrap();
636 let perms = meta.permissions.unwrap();
637 assert_eq!(
638 perms.insert_policy.unwrap(),
639 allow_if_super_admin_metadata()
640 );
641 assert_eq!(
642 perms.update_policy.unwrap(),
643 allow_if_super_admin_metadata()
644 );
645 }
646
647 #[xmtp_common::test(unwrap_try = true)]
648 fn synthesis_preserves_a_stored_min_version_policy() {
649 let mut ps = minimal_default_policy_set();
653 ps.update_metadata_policy.insert(
654 MetadataField::MinimumSupportedProtocolVersion
655 .as_str()
656 .to_string(),
657 allow_metadata(),
658 );
659 let registry = synthesize_registry_from_policy_set(&ps).unwrap();
660 let meta = registry
661 .get(&ComponentId::MIN_SUPPORTED_PROTOCOL_VERSION)
662 .unwrap()
663 .unwrap();
664 assert_eq!(
665 meta.permissions.unwrap().insert_policy.unwrap(),
666 allow_metadata()
667 );
668 }
669
670 #[xmtp_common::test(unwrap_try = true)]
671 fn synthesis_uses_per_field_policy_when_present() {
672 let mut ps = minimal_default_policy_set();
673 ps.update_metadata_policy
674 .insert("group_name".to_string(), allow_if_admin_metadata());
675 let registry = synthesize_registry_from_policy_set(&ps).unwrap();
676 let meta = registry.get(&ComponentId::GROUP_NAME).unwrap().unwrap();
677 assert_eq!(
678 meta.permissions.clone().unwrap().insert_policy.unwrap(),
679 allow_if_admin_metadata()
680 );
681 let desc = registry
683 .get(&ComponentId::GROUP_DESCRIPTION)
684 .unwrap()
685 .unwrap();
686 assert_eq!(
687 desc.permissions.unwrap().insert_policy.unwrap(),
688 allow_if_admin_metadata()
689 );
690 }
691
692 #[xmtp_common::test(unwrap_try = true)]
693 fn synthesis_rejects_unknown_metadata_field() {
694 let mut ps = minimal_default_policy_set();
695 ps.update_metadata_policy
696 .insert("something_new".to_string(), allow_metadata());
697 let err = synthesize_registry_from_policy_set(&ps).unwrap_err();
698 assert!(matches!(err, MigrationError::UnknownMetadataField(f) if f == "something_new"));
699 }
700
701 #[xmtp_common::test(unwrap_try = true)]
702 fn synthesis_admin_list_super_admin_only() {
703 let mut ps = minimal_default_policy_set();
704 ps.add_admin_policy = Some(super_admin_only_perms());
705 ps.remove_admin_policy = Some(super_admin_only_perms());
706 let registry = synthesize_registry_from_policy_set(&ps).unwrap();
707 let admin = registry.get(&ComponentId::ADMIN_LIST).unwrap().unwrap();
708 let perms = admin.permissions.unwrap();
709 assert_eq!(
710 perms.insert_policy.unwrap(),
711 MetadataPolicyProto {
712 kind: Some(MetadataKind::Base(
713 MetadataBasePolicy::AllowIfSuperAdmin as i32
714 ))
715 }
716 );
717 }
718
719 #[xmtp_common::test(unwrap_try = true)]
720 fn synthesis_admin_list_preserves_deny_policy() {
721 let mut ps = minimal_default_policy_set();
722 ps.add_admin_policy = Some(deny_perms());
723 ps.remove_admin_policy = Some(deny_perms());
724
725 let registry = synthesize_registry_from_policy_set(&ps)?;
726 let permissions = registry
727 .get(&ComponentId::ADMIN_LIST)?
728 .expect("ADMIN_LIST is registered")
729 .permissions
730 .expect("ADMIN_LIST has permissions");
731 assert_eq!(
732 permissions.insert_policy,
733 Some(metadata_policy(MetadataBasePolicy::Deny))
734 );
735 assert_eq!(
736 permissions.delete_policy,
737 Some(metadata_policy(MetadataBasePolicy::Deny))
738 );
739 }
740
741 #[xmtp_common::test(unwrap_try = true)]
742 fn metadata_field_mapping_agrees_with_dispatch_table() {
743 use crate::app_data::registry_table::lookup_component;
750 for (_field, component_id, expected_type) in metadata_field_registry_mapping() {
751 let dispatched = lookup_component(*component_id)
752 .unwrap_or_else(|| panic!("WELL_KNOWN missing entry for {component_id}"));
753 assert_eq!(
754 dispatched.component_type(),
755 *expected_type,
756 "metadata_field_registry_mapping disagrees with WELL_KNOWN for {component_id}"
757 );
758 }
759 }
760
761 #[xmtp_common::test(unwrap_try = true)]
762 fn synthesis_sets_correct_component_type_per_field() {
763 let registry = synthesize_registry_from_policy_set(&minimal_default_policy_set()).unwrap();
767 let expected: &[(ComponentId, ComponentType)] = &[
768 (ComponentId::GROUP_NAME, ComponentType::String),
769 (ComponentId::GROUP_DESCRIPTION, ComponentType::String),
770 (ComponentId::GROUP_IMAGE_URL, ComponentType::String),
771 (ComponentId::APP_DATA, ComponentType::String),
772 (
773 ComponentId::MIN_SUPPORTED_PROTOCOL_VERSION,
774 ComponentType::String,
775 ),
776 (ComponentId::MESSAGE_DISAPPEAR_FROM_NS, ComponentType::Bytes),
777 (ComponentId::MESSAGE_DISAPPEAR_IN_NS, ComponentType::Bytes),
778 ];
779 for (id, ty) in expected {
780 let meta = registry.get(id).unwrap().unwrap();
781 assert_eq!(
782 meta.component_type, *ty as i32,
783 "wrong component_type for {id}"
784 );
785 }
786 }
787
788 #[xmtp_common::test(unwrap_try = true)]
789 fn membership_policy_preserves_combinator_recursively() {
790 use xmtp_proto::xmtp::mls::message_contents::{
791 MembershipPolicy as MembershipPolicyProto,
792 membership_policy::{AndCondition as AndCondProto, Kind as MembershipKind},
793 };
794 let combinator = MembershipPolicyProto {
795 kind: Some(MembershipKind::AndCondition(AndCondProto {
796 policies: vec![MembershipPolicyProto {
797 kind: Some(MembershipKind::AnyCondition(
798 xmtp_proto::xmtp::mls::message_contents::membership_policy::AnyCondition {
799 policies: vec![allow_membership()],
800 },
801 )),
802 }],
803 })),
804 };
805 let translated = membership_policy_to_metadata_policy(&combinator).unwrap();
806 assert_eq!(
807 translated,
808 MetadataPolicyProto {
809 kind: Some(MetadataKind::AndCondition(MetadataAndCondition {
810 policies: vec![MetadataPolicyProto {
811 kind: Some(MetadataKind::AnyCondition(MetadataAnyCondition {
812 policies: vec![allow_metadata()],
813 })),
814 }],
815 })),
816 }
817 );
818 }
819
820 #[xmtp_common::test(unwrap_try = true)]
821 fn membership_policy_rejects_unknown_base() {
822 use xmtp_proto::xmtp::mls::message_contents::{
823 MembershipPolicy as MembershipPolicyProto, membership_policy::Kind as MembershipKind,
824 };
825 let unknown = MembershipPolicyProto {
826 kind: Some(MembershipKind::Base(9999)),
827 };
828 let err = membership_policy_to_metadata_policy(&unknown).unwrap_err();
829 assert!(matches!(
830 err,
831 MigrationError::UnknownMembershipPolicy(Some(9999))
832 ));
833 }
834
835 #[xmtp_common::test(unwrap_try = true)]
836 fn group_membership_encode_round_trip() {
837 let mut entries: BTreeMap<InboxId, GroupMembershipEntryV1> = BTreeMap::new();
838 entries.insert(
839 InboxId::from_bytes([0x01; 32]),
840 GroupMembershipEntryV1 {
841 sequence_id: 42,
842 failed_installations: vec![vec![0xAA; 16]],
843 },
844 );
845 entries.insert(
846 InboxId::from_bytes([0x02; 32]),
847 GroupMembershipEntryV1 {
848 sequence_id: 99,
849 failed_installations: vec![],
850 },
851 );
852 let entries = entries
853 .into_iter()
854 .map(|(k, v)| {
855 (
856 k,
857 GroupMembershipEntry {
858 version: Some(GroupMembershipEntryVersion::V1(v)),
859 },
860 )
861 })
862 .collect::<BTreeMap<_, _>>();
863 let wire_bytes = encode_group_membership_delta(&entries).unwrap();
866 let decoded = decode_group_membership_delta(&wire_bytes).unwrap();
867 assert_eq!(decoded, entries);
868 }
869
870 #[xmtp_common::test(unwrap_try = true)]
871 fn admin_list_policy_rejects_combinator() {
872 use xmtp_proto::xmtp::mls::message_contents::{
873 PermissionsUpdatePolicy as PermissionsUpdatePolicyProto,
874 permissions_update_policy::{AndCondition as AndCondProto, Kind as PermissionsKind},
875 };
876 let combinator = PermissionsUpdatePolicyProto {
877 kind: Some(PermissionsKind::AndCondition(AndCondProto {
878 policies: vec![],
879 })),
880 };
881 let err = admin_list_policy_to_metadata_policy(&combinator).unwrap_err();
882 assert!(matches!(
883 err,
884 MigrationError::NonConstrainedAdminPolicy(None)
885 ));
886 }
887
888 #[xmtp_common::test(unwrap_try = true)]
889 fn decode_wire_rejects_unset_version() {
890 let envelope = GroupMembershipEntry { version: None };
895 let delta = TlsMapDelta::<InboxId, VLBytes>::new().insert(
896 InboxId::from_bytes([0x04; 32]),
897 VLBytes::new(envelope.encode_to_vec()),
898 );
899 let bytes = delta.tls_serialize_detached().unwrap();
900 let err = decode_group_membership_delta(&bytes).unwrap_err();
901 assert!(matches!(
902 err,
903 MigrationError::GroupMembershipEntryUnknownVersion
904 ));
905 }
906
907 #[xmtp_common::test(unwrap_try = true)]
908 fn decode_wire_rejects_non_insert_mutation() {
909 let delta = TlsMapDelta::<InboxId, VLBytes>::new().delete(InboxId::from_bytes([0x05; 32]));
912 let bytes = delta.tls_serialize_detached().unwrap();
913 let err = decode_group_membership_delta(&bytes).unwrap_err();
914 assert!(matches!(
915 err,
916 MigrationError::GroupMembershipNonInsertBootstrapMutation
917 ));
918 }
919
920 #[xmtp_common::test(unwrap_try = true)]
921 fn decode_dict_round_trips() {
922 let mut snapshot: TlsMap<InboxId, VLBytes> = TlsMap::new();
926 let inbox = InboxId::from_bytes([0x06; 32]);
927 let envelope = GroupMembershipEntry {
928 version: Some(GroupMembershipEntryVersion::V1(GroupMembershipEntryV1 {
929 sequence_id: 7,
930 failed_installations: vec![],
931 })),
932 };
933 snapshot
934 .insert(inbox, VLBytes::new(envelope.encode_to_vec()))
935 .unwrap();
936 let bytes = snapshot.tls_serialize_detached().unwrap();
937 let decoded = decode_group_membership_dict(&bytes).unwrap();
938 assert_eq!(decoded.len(), 1);
939 assert!(decoded.contains_key(&inbox));
940 }
941
942 fn hex_inbox(tag: u8) -> String {
951 hex::encode([tag; INBOX_ID_BYTE_LEN])
952 }
953
954 #[xmtp_common::test(unwrap_try = true)]
955 fn encode_inbox_id_set_emits_bootstrap_wire_delta() {
956 use crate::tls_set::TlsSetMutation;
962 let ids = vec![hex_inbox(0xAA), hex_inbox(0xBB)];
963 let bytes = encode_inbox_id_set(&ids).unwrap();
964 let delta = TlsSetDelta::<InboxId>::tls_deserialize_exact(&bytes)
965 .expect("decodes as TlsSetDelta<InboxId>");
966 assert_eq!(delta.mutations.len(), 2);
967 for (mutation, expected_tag) in delta.mutations.iter().zip([0xAA, 0xBB]) {
969 match mutation {
970 TlsSetMutation::Insert(id) => {
971 assert_eq!(id.as_bytes(), &[expected_tag; INBOX_ID_BYTE_LEN]);
972 }
973 other => panic!("expected Insert, got {other:?}"),
974 }
975 }
976 }
977
978 #[xmtp_common::test(unwrap_try = true)]
979 fn encode_inbox_id_set_rejects_bad_hex() {
980 let err = encode_inbox_id_set(&["not-hex".to_string()]).unwrap_err();
981 assert!(matches!(err, MigrationError::InvalidInboxId(_)));
982 }
983
984 #[xmtp_common::test(unwrap_try = true)]
985 fn encode_dm_members_produces_two_insert_delta() {
986 use crate::tls_set::TlsSetMutation;
987 let dm = crate::group_metadata::DmMembers {
988 member_one_inbox_id: hex_inbox(0xCC),
989 member_two_inbox_id: hex_inbox(0xDD),
990 };
991 let bytes = encode_dm_members(&dm).unwrap();
992 let delta = TlsSetDelta::<InboxId>::tls_deserialize_exact(&bytes).unwrap();
993 assert_eq!(delta.mutations.len(), 2);
994 for (mutation, expected_tag) in delta.mutations.iter().zip([0xCC, 0xDD]) {
995 match mutation {
996 TlsSetMutation::Insert(id) => {
997 assert_eq!(id.as_bytes(), &[expected_tag; INBOX_ID_BYTE_LEN]);
998 }
999 other => panic!("expected Insert, got {other:?}"),
1000 }
1001 }
1002 }
1003
1004 #[xmtp_common::test(unwrap_try = true)]
1005 fn encode_dm_members_rejects_self_reference() {
1006 let id = hex_inbox(0xEE);
1010 let dm = crate::group_metadata::DmMembers {
1011 member_one_inbox_id: id.clone(),
1012 member_two_inbox_id: id.clone(),
1013 };
1014 let err = encode_dm_members(&dm).unwrap_err();
1015 let MigrationError::DmMembersSelfReference(msg) = err else {
1016 panic!("expected DmMembersSelfReference, got {err:?}");
1017 };
1018 assert!(msg.starts_with(&id), "missing canonical hex: {msg}");
1020 assert!(msg.contains(&format!("member_one={id}")), "{msg}");
1021 assert!(msg.contains(&format!("member_two={id}")), "{msg}");
1022 }
1023
1024 #[xmtp_common::test(unwrap_try = true)]
1025 fn encode_dm_members_rejects_case_divergent_self_reference() {
1026 let lower = hex_inbox(0xEE);
1033 let upper = lower.to_ascii_uppercase();
1034 assert_ne!(lower, upper, "test premise: strings must differ");
1035 let dm = crate::group_metadata::DmMembers {
1036 member_one_inbox_id: lower.clone(),
1037 member_two_inbox_id: upper.clone(),
1038 };
1039 let err = encode_dm_members(&dm).unwrap_err();
1040 let MigrationError::DmMembersSelfReference(msg) = err else {
1041 panic!("expected DmMembersSelfReference, got {err:?}");
1042 };
1043 assert!(msg.starts_with(&lower), "missing canonical hex: {msg}");
1046 assert!(msg.contains(&format!("member_one={lower}")), "{msg}");
1047 assert!(msg.contains(&format!("member_two={upper}")), "{msg}");
1048 }
1049
1050 #[xmtp_common::test(unwrap_try = true)]
1051 fn conversation_type_codec_round_trips() {
1052 for v in [0_i32, 1, 2, -1, i32::MAX, i32::MIN] {
1056 let bytes = encode_conversation_type(v);
1057 assert_eq!(bytes.len(), 4, "always 4 bytes");
1058 assert_eq!(decode_conversation_type(&bytes).unwrap(), v);
1059 }
1060 }
1061
1062 #[xmtp_common::test(unwrap_try = true)]
1063 fn conversation_type_decode_rejects_wrong_length() {
1064 let err = decode_conversation_type(&[0, 0, 0]).unwrap_err();
1065 assert!(matches!(
1066 err,
1067 MigrationError::ConversationTypePayloadLength(3)
1068 ));
1069 let err = decode_conversation_type(&[0; 8]).unwrap_err();
1070 assert!(matches!(
1071 err,
1072 MigrationError::ConversationTypePayloadLength(8)
1073 ));
1074 }
1075
1076 fn build_test_extensions(
1083 gmm: crate::group_mutable_metadata::GroupMutableMetadata,
1084 policy_set: PolicySetProto,
1085 membership: xmtp_proto::xmtp::mls::message_contents::GroupMembership,
1086 metadata: crate::group_metadata::GroupMetadata,
1087 ) -> Extensions<GroupContext> {
1088 use openmls::extensions::{Extension, Metadata, UnknownExtension};
1089 use xmtp_configuration::{
1090 GROUP_MEMBERSHIP_EXTENSION_ID, GROUP_PERMISSIONS_EXTENSION_ID,
1091 MUTABLE_METADATA_EXTENSION_ID,
1092 };
1093 use xmtp_proto::xmtp::mls::message_contents::GroupMutablePermissionsV1;
1094
1095 let gmm_bytes: Vec<u8> = gmm.try_into().unwrap();
1096 let permissions_bytes = GroupMutablePermissionsV1 {
1097 policies: Some(policy_set),
1098 }
1099 .encode_to_vec();
1100 let membership_bytes = membership.encode_to_vec();
1101 let metadata_bytes: Vec<u8> = metadata.try_into().unwrap();
1102
1103 Extensions::from_vec(vec![
1104 Extension::Unknown(MUTABLE_METADATA_EXTENSION_ID, UnknownExtension(gmm_bytes)),
1105 Extension::Unknown(
1106 GROUP_PERMISSIONS_EXTENSION_ID,
1107 UnknownExtension(permissions_bytes),
1108 ),
1109 Extension::Unknown(
1110 GROUP_MEMBERSHIP_EXTENSION_ID,
1111 UnknownExtension(membership_bytes),
1112 ),
1113 Extension::ImmutableMetadata(Metadata::new(metadata_bytes)),
1114 ])
1115 .unwrap()
1116 }
1117
1118 fn default_gmm() -> crate::group_mutable_metadata::GroupMutableMetadata {
1119 crate::group_mutable_metadata::GroupMutableMetadata::new(
1120 std::collections::HashMap::new(),
1121 Vec::new(),
1122 Vec::new(),
1123 )
1124 }
1125
1126 fn empty_membership() -> xmtp_proto::xmtp::mls::message_contents::GroupMembership {
1127 xmtp_proto::xmtp::mls::message_contents::GroupMembership {
1128 members: std::collections::HashMap::new(),
1129 failed_installations: vec![],
1130 }
1131 }
1132
1133 fn plain_group_metadata() -> crate::group_metadata::GroupMetadata {
1134 crate::group_metadata::GroupMetadata::new(
1135 xmtp_proto::types::ConversationType::Group,
1136 hex_inbox(0x11),
1137 None,
1138 None,
1139 )
1140 }
1141
1142 #[xmtp_common::test(unwrap_try = true)]
1143 fn canonical_subset_empty_group_omits_optional_seeds() {
1144 let exts = build_test_extensions(
1149 default_gmm(),
1150 minimal_default_policy_set(),
1151 empty_membership(),
1152 plain_group_metadata(),
1153 );
1154 let subset = synthesize_canonical_subset_from_extensions(&exts).unwrap();
1155
1156 assert!(!subset.strict.contains_key(&ComponentId::COMPONENT_REGISTRY));
1160 assert!(subset.strict.contains_key(&ComponentId::ADMIN_LIST));
1161 assert!(subset.strict.contains_key(&ComponentId::SUPER_ADMIN_LIST));
1162 assert!(subset.strict.contains_key(&ComponentId::CONVERSATION_TYPE));
1163 assert!(subset.strict.contains_key(&ComponentId::CREATOR_INBOX_ID));
1164
1165 assert!(!subset.strict.contains_key(&ComponentId::DM_MEMBERS));
1176 assert!(!subset.strict.contains_key(&ComponentId::ONESHOT_MESSAGE));
1177 assert!(!subset.strict.contains_key(&ComponentId::GROUP_NAME));
1178 assert!(
1179 !subset
1180 .strict
1181 .contains_key(&ComponentId::MESSAGE_DISAPPEAR_FROM_NS)
1182 );
1183 assert!(!subset.strict.contains_key(&ComponentId::COMMIT_LOG_SIGNER));
1184
1185 assert!(
1190 !subset
1191 .expected_registry
1192 .contains_key(&ComponentId::DM_MEMBERS)
1193 );
1194 assert!(
1195 !subset
1196 .expected_registry
1197 .contains_key(&ComponentId::ONESHOT_MESSAGE)
1198 );
1199
1200 assert!(subset.membership_sequence_ids.is_empty());
1201 }
1202
1203 #[xmtp_common::test(unwrap_try = true)]
1204 fn canonical_subset_dm_group_includes_dm_members() {
1205 let dm_members = crate::group_metadata::DmMembers {
1206 member_one_inbox_id: hex_inbox(0x22),
1207 member_two_inbox_id: hex_inbox(0x33),
1208 };
1209 let metadata = crate::group_metadata::GroupMetadata::new(
1210 xmtp_proto::types::ConversationType::Dm,
1211 hex_inbox(0x22),
1212 Some(dm_members.clone()),
1213 None,
1214 );
1215 let exts = build_test_extensions(
1216 default_gmm(),
1217 minimal_default_policy_set(),
1218 empty_membership(),
1219 metadata,
1220 );
1221 let subset = synthesize_canonical_subset_from_extensions(&exts).unwrap();
1222
1223 let (_, dm_bytes) = subset
1224 .strict
1225 .get(&ComponentId::DM_MEMBERS)
1226 .expect("DM group must include DM_MEMBERS in strict");
1227 assert_eq!(*dm_bytes, encode_dm_members(&dm_members).unwrap());
1228
1229 assert!(
1230 subset
1231 .expected_registry
1232 .contains_key(&ComponentId::DM_MEMBERS),
1233 "expected_registry must keep DM_MEMBERS for a DM group"
1234 );
1235 }
1236
1237 #[xmtp_common::test(unwrap_try = true)]
1238 fn canonical_subset_oneshot_group_includes_oneshot() {
1239 use xmtp_proto::xmtp::mls::message_contents::OneshotMessage;
1240 let oneshot = OneshotMessage { message_type: None };
1245 let metadata = crate::group_metadata::GroupMetadata::new(
1246 xmtp_proto::types::ConversationType::Group,
1247 hex_inbox(0x44),
1248 None,
1249 Some(oneshot.clone()),
1250 );
1251 let exts = build_test_extensions(
1252 default_gmm(),
1253 minimal_default_policy_set(),
1254 empty_membership(),
1255 metadata,
1256 );
1257 let subset = synthesize_canonical_subset_from_extensions(&exts).unwrap();
1258
1259 let (_, oneshot_bytes) = subset
1260 .strict
1261 .get(&ComponentId::ONESHOT_MESSAGE)
1262 .expect("oneshot group must include ONESHOT_MESSAGE");
1263 assert_eq!(*oneshot_bytes, oneshot.encode_to_vec());
1264
1265 assert!(
1266 subset
1267 .expected_registry
1268 .contains_key(&ComponentId::ONESHOT_MESSAGE)
1269 );
1270 }
1271
1272 #[xmtp_common::test(unwrap_try = true)]
1273 fn canonical_subset_membership_sequence_ids() {
1274 let mut members = std::collections::HashMap::new();
1275 members.insert(hex_inbox(0x55), 7_u64);
1276 members.insert(hex_inbox(0x66), 42_u64);
1277 let membership = xmtp_proto::xmtp::mls::message_contents::GroupMembership {
1278 members,
1279 failed_installations: vec![],
1280 };
1281
1282 let exts = build_test_extensions(
1283 default_gmm(),
1284 minimal_default_policy_set(),
1285 membership,
1286 plain_group_metadata(),
1287 );
1288 let subset = synthesize_canonical_subset_from_extensions(&exts).unwrap();
1289
1290 assert_eq!(subset.membership_sequence_ids.len(), 2);
1291 assert_eq!(
1292 subset
1293 .membership_sequence_ids
1294 .get(&InboxId::from_bytes([0x55; 32]))
1295 .copied(),
1296 Some(7)
1297 );
1298 assert_eq!(
1299 subset
1300 .membership_sequence_ids
1301 .get(&InboxId::from_bytes([0x66; 32]))
1302 .copied(),
1303 Some(42)
1304 );
1305 assert!(subset.allowed_failed_installations.is_empty());
1307 }
1308
1309 #[xmtp_common::test(unwrap_try = true)]
1310 fn canonical_subset_collects_legacy_failed_installations_into_allow_set() {
1311 let installation_a = [0xAA_u8; 32];
1316 let installation_b = [0xBB_u8; 32];
1317 let membership = xmtp_proto::xmtp::mls::message_contents::GroupMembership {
1318 members: std::collections::HashMap::new(),
1319 failed_installations: vec![
1320 installation_a.to_vec(),
1321 installation_b.to_vec(),
1322 installation_a.to_vec(), ],
1324 };
1325 let exts = build_test_extensions(
1326 default_gmm(),
1327 minimal_default_policy_set(),
1328 membership,
1329 plain_group_metadata(),
1330 );
1331 let subset = synthesize_canonical_subset_from_extensions(&exts).unwrap();
1332
1333 assert_eq!(subset.allowed_failed_installations.len(), 2);
1334 assert!(
1335 subset
1336 .allowed_failed_installations
1337 .contains(&installation_a)
1338 );
1339 assert!(
1340 subset
1341 .allowed_failed_installations
1342 .contains(&installation_b)
1343 );
1344 }
1345
1346 #[xmtp_common::test(unwrap_try = true)]
1347 fn canonical_subset_rejects_non_32_byte_failed_installation() {
1348 let membership = xmtp_proto::xmtp::mls::message_contents::GroupMembership {
1352 members: std::collections::HashMap::new(),
1353 failed_installations: vec![vec![0xCC; 16]], };
1355 let exts = build_test_extensions(
1356 default_gmm(),
1357 minimal_default_policy_set(),
1358 membership,
1359 plain_group_metadata(),
1360 );
1361 let err = synthesize_canonical_subset_from_extensions(&exts).unwrap_err();
1362 assert!(matches!(
1363 err,
1364 MigrationError::InvalidFailedInstallationLength(16)
1365 ));
1366 }
1367
1368 fn strict_lines(subset: &CanonicalBootstrapExpectation) -> Vec<String> {
1370 subset
1371 .strict
1372 .iter()
1373 .map(|(id, (op, bytes))| format!("{id} {op:?} {}", hex::encode(bytes)))
1374 .collect()
1375 }
1376
1377 fn golden_gmm() -> crate::group_mutable_metadata::GroupMutableMetadata {
1381 use crate::group_mutable_metadata::MetadataField;
1382 let mut attributes = std::collections::HashMap::new();
1383 attributes.insert(
1384 MetadataField::GroupName.to_string(),
1385 "Golden Group".to_string(),
1386 );
1387 attributes.insert(
1388 MetadataField::Description.to_string(),
1389 "golden description".to_string(),
1390 );
1391 attributes.insert(
1392 MetadataField::GroupImageUrlSquare.to_string(),
1393 "https://example.com/golden.png".to_string(),
1394 );
1395 attributes.insert(
1396 MetadataField::AppData.to_string(),
1397 "golden-app-data".to_string(),
1398 );
1399 attributes.insert(
1400 MetadataField::MinimumSupportedProtocolVersion.to_string(),
1401 "1.11.0".to_string(),
1402 );
1403 attributes.insert(
1404 MetadataField::MessageDisappearFromNS.to_string(),
1405 "1000000000".to_string(),
1406 );
1407 attributes.insert(
1408 MetadataField::MessageDisappearInNS.to_string(),
1409 "2000000000".to_string(),
1410 );
1411 attributes.insert(
1412 MetadataField::CommitLogSigner.to_string(),
1413 hex::encode([0xAB; 32]),
1414 );
1415 crate::group_mutable_metadata::GroupMutableMetadata::new(
1416 attributes,
1417 vec![hex_inbox(0x22)],
1418 vec![hex_inbox(0x33)],
1419 )
1420 }
1421
1422 #[xmtp_common::test(unwrap_try = true)]
1437 fn golden_bootstrap_synthesis_group() {
1438 let exts = build_test_extensions(
1439 golden_gmm(),
1440 minimal_default_policy_set(),
1441 empty_membership(),
1442 plain_group_metadata(),
1443 );
1444 let subset = synthesize_canonical_subset_from_extensions(&exts).unwrap();
1445
1446 let expected: Vec<String> = [
1447 "0x8001 Update 2200003333333333333333333333333333333333333333333333333333333333333333",
1450 "0x8002 Update 2200002222222222222222222222222222222222222222222222222222222222222222",
1452 "0x8004 Update 476f6c64656e2047726f7570",
1454 "0x8005 Update 676f6c64656e206465736372697074696f6e",
1456 "0x8006 Update 68747470733a2f2f6578616d706c652e636f6d2f676f6c64656e2e706e67",
1458 "0x8007 Update 000000003b9aca00",
1460 "0x8008 Update 0000000077359400",
1462 "0x8009 Update 676f6c64656e2d6170702d64617461",
1464 "0x800A Update 312e31312e30",
1466 "0x800B Update abababababababababababababababababababababababababababababababab",
1468 "0xBFFE Update 001111111111111111111111111111111111111111111111111111111111111111",
1470 "0xBFFF Update 00000001",
1472 ]
1473 .into_iter()
1474 .map(String::from)
1475 .collect();
1476
1477 assert_eq!(
1478 strict_lines(&subset),
1479 expected,
1480 "bootstrap synthesis encoder output changed — see the doc comment \
1481 on this test before touching the pins"
1482 );
1483 }
1484
1485 #[xmtp_common::test(unwrap_try = true)]
1489 fn golden_bootstrap_synthesis_dm_with_oneshot() {
1490 let dm_members = crate::group_metadata::DmMembers {
1491 member_one_inbox_id: hex_inbox(0x22),
1492 member_two_inbox_id: hex_inbox(0x33),
1493 };
1494 let oneshot =
1498 xmtp_proto::xmtp::mls::message_contents::OneshotMessage { message_type: None };
1499 let metadata = crate::group_metadata::GroupMetadata::new(
1500 xmtp_proto::types::ConversationType::Dm,
1501 hex_inbox(0x22),
1502 Some(dm_members),
1503 Some(oneshot),
1504 );
1505 let exts = build_test_extensions(
1506 golden_gmm(),
1507 minimal_default_policy_set(),
1508 empty_membership(),
1509 metadata,
1510 );
1511 let subset = synthesize_canonical_subset_from_extensions(&exts).unwrap();
1512
1513 let expected: Vec<String> = [
1514 "0x8001 Update 2200003333333333333333333333333333333333333333333333333333333333333333",
1515 "0x8002 Update 2200002222222222222222222222222222222222222222222222222222222222222222",
1516 "0x8004 Update 476f6c64656e2047726f7570",
1517 "0x8005 Update 676f6c64656e206465736372697074696f6e",
1518 "0x8006 Update 68747470733a2f2f6578616d706c652e636f6d2f676f6c64656e2e706e67",
1519 "0x8007 Update 000000003b9aca00",
1520 "0x8008 Update 0000000077359400",
1521 "0x8009 Update 676f6c64656e2d6170702d64617461",
1522 "0x800A Update 312e31312e30",
1523 "0x800B Update abababababababababababababababababababababababababababababababab",
1524 "0xBFFC Update ",
1527 "0xBFFD Update 40440000222222222222222222222222222222222222222222222222222222222222222200003333333333333333333333333333333333333333333333333333333333333333",
1530 "0xBFFE Update 002222222222222222222222222222222222222222222222222222222222222222",
1532 "0xBFFF Update 00000002",
1534 ]
1535 .into_iter()
1536 .map(String::from)
1537 .collect();
1538
1539 assert_eq!(
1540 strict_lines(&subset),
1541 expected,
1542 "bootstrap synthesis encoder output changed — see the doc comment \
1543 on golden_bootstrap_synthesis_group before touching the pins"
1544 );
1545 }
1546}