1use crate::core::sync_tasks::schedule_newcomer_adds;
5use crate::groups::MetadataPermissionsError;
6use crate::groups::mls_ext::CommitLogStorer;
7use crate::groups::mls_ext::ResolvedWelcome;
8use crate::groups::oneshot::Oneshot;
9use crate::groups::welcomes::WelcomeMembership;
10use crate::identity_updates::{
11 IdentityDependencyError, IdentityRequirement, InstallationDiffError, require_association_state,
12};
13use crate::state_tx::{state_write, state_write_with_events};
14use crate::subscriptions::internal::{InternalEvent, dm_event_context, emit_root_key_changed};
15use crate::worker::device_sync::is_own_sync_group;
16use crate::{
17 context::XmtpSharedContext,
18 groups::{
19 GroupError, MlsGroup, ValidateGroupMembership, mls_ext::DecryptedWelcome, validate_dm_group,
20 },
21 intents::ProcessIntentError,
22};
23use derive_builder::Builder;
24use openmls::group::MlsGroup as OpenMlsGroup;
25use prost::Message;
26use xmtp_common::{snippet::Snippet, time::now_ns};
27use xmtp_content_types::ContentCodec;
28use xmtp_content_types::group_updated::GroupUpdatedCodec;
29use xmtp_db::TransactionOutcome::{Continue, Rollback};
30use xmtp_db::pending_remove::QueryPendingRemove;
31use xmtp_db::{
32 TransactionOutcome, XmtpOpenMlsProviderRef,
33 group::{ConversationType, GroupMembershipState, StoredGroup},
34 group_message::{ContentType, DeliveryStatus, GroupMessageKind, StoredGroupMessage},
35 incoming_envelope::{JoinAnchorMode, NetworkEntityKind, StoredIncomingEnvelope, StreamTopic},
36 prelude::*,
37 refresh_state::EntityKind,
38};
39use xmtp_events::{
40 ClientEvent, ConversationJoined, ConversationType as EventConversationType, EventBuffer,
41 EventWriter, JoinOrigin,
42};
43use xmtp_id::associations::MemberIdentifier;
44use xmtp_mls_group_state::app_data::group_root::check_group_root;
45use xmtp_mls_group_state::group_metadata::extract_group_metadata;
46use xmtp_mls_group_state::libxmtp_version::LibXMTPVersion;
47
48use xmtp_mls_group_state::app_data::component_source::extract_group_mutable_metadata_capability_aware;
49use xmtp_proto::types::Cursor;
50use xmtp_proto::xmtp::mls::message_contents::{ContentTypeId, GroupUpdated, group_updated::Inbox};
51
52use xmtp_proto::types::GroupId;
53#[derive(Builder)]
62#[builder(
63 pattern = "owned",
64 setter(strip_option),
65 build_fn(error = "GroupError", private)
66)]
67pub struct XmtpWelcome<'a, C, V> {
68 context: C,
69 welcome: &'a xmtp_proto::types::WelcomeMessage,
71 pending: StoredIncomingEnvelope,
73 validator: V,
74}
75
76impl<'a, C, V> XmtpWelcome<'a, C, V> {
77 pub fn builder() -> XmtpWelcomeBuilder<'a, C, V> {
78 Default::default()
79 }
80}
81
82enum CommitResult<C> {
84 FailedForever(GroupError),
86 Ok(Option<MlsGroup<C>>),
88}
89
90pub(crate) fn terminal_welcome_error(error: &GroupError) -> bool {
93 use openmls::prelude::WelcomeError;
94 matches!(
95 error,
96 GroupError::InvalidWelcomeMetadata
97 | GroupError::Storage(xmtp_db::StorageError::Stream(
99 xmtp_db::stream_storage::StreamStorageError::StaleJoinAnchor
100 ))
101 | GroupError::WelcomeGroupRoot(_)
102 | GroupError::InvalidGroupMembership
103 | GroupError::MetadataPermissionsError(_)
104 | GroupError::NoPSKSupport
105 | GroupError::TlsError(_)
106 | GroupError::CredentialError(_)
107 | GroupError::Identity(
108 crate::identity::IdentityError::Decode(_)
109 | crate::identity::IdentityError::BasicCredential(_)
110 )
111 | GroupError::ConversionError(_)
112 | GroupError::UnwrapWelcome(_)
113 | GroupError::ProcessIntent(ProcessIntentError::WelcomeAlreadyProcessed(_))
114 | GroupError::InstallationDiff(InstallationDiffError::IdentityDependency(
115 IdentityDependencyError::InvalidSequence(_)
116 | IdentityDependencyError::MissingReference(_)
117 ))
118 | GroupError::WelcomeError(
119 WelcomeError::GroupSecrets(_)
120 | WelcomeError::Psk(_)
122 | WelcomeError::CiphersuiteMismatch
123 | WelcomeError::GroupInfo(_)
124 | WelcomeError::JoinerSecretNotFound
125 | WelcomeError::MissingRatchetTree
126 | WelcomeError::ConfirmationTagMismatch
127 | WelcomeError::InvalidGroupInfoSignature
128 | WelcomeError::UnknownSender
129 | WelcomeError::NotAWelcomeMessage
130 | WelcomeError::MalformedWelcomeMessage
131 | WelcomeError::UnableToDecrypt
132 | WelcomeError::PublicTreeError(_)
133 | WelcomeError::LeafNodeValidation(_)
134 )
135 )
136}
137
138impl<C> CommitResult<C> {
139 fn into_result(self) -> Result<Option<MlsGroup<C>>, GroupError> {
140 match self {
141 Self::FailedForever(err) => Err(err),
142 Self::Ok(group) => Ok(group),
143 }
144 }
145}
146
147impl<'a, C, V> XmtpWelcomeBuilder<'a, C, V>
148where
149 C: XmtpSharedContext,
150 V: ValidateGroupMembership,
151{
152 #[tracing::instrument(skip_all, fields(operation = "mls.process_welcome"))]
157 pub async fn process(self) -> Result<Option<MlsGroup<C>>, GroupError> {
158 let this = self.build()?;
159 this.check_pending(&this.context.db())?;
160
161 let (resolved, membership) = match this.validate_membership().await {
162 Err(error) => return this.reject_or_retry(error),
163 Ok(validated) => validated,
164 };
165 let commit_result = this.commit_or_fail_forever(&resolved, Some(&membership), None)?;
166 commit_result.into_result()
167 }
168
169 pub(crate) fn process_resolved(
172 self,
173 resolved: &ResolvedWelcome,
174 missing_reference: Option<&IdentityRequirement>,
175 ) -> Result<Option<MlsGroup<C>>, GroupError> {
176 let this = self.build()?;
177 this.commit_or_fail_forever(resolved, None, missing_reference)?
178 .into_result()
179 }
180}
181
182impl<'a, C, V> XmtpWelcome<'a, C, V>
183where
184 C: XmtpSharedContext,
185 V: ValidateGroupMembership,
186 <C::MlsStorage as XmtpMlsStorageProvider>::Connection: xmtp_db::ConnectionExt,
187{
188 fn topic(&self) -> StreamTopic {
189 StreamTopic {
190 entity_id: self.context.installation_id().to_vec(),
191 kind: NetworkEntityKind::Welcome,
192 }
193 }
194
195 fn check_pending(&self, db: &impl DbQuery) -> Result<(), GroupError> {
197 let current = db.pending_envelope(&self.topic(), self.welcome.cursor)?;
198 if current.is_none_or(|row| row.envelope != self.pending.envelope)
199 || self.pending.sequence_id != self.welcome.cursor.0 as i64
200 || self.pending.entity_id != self.context.installation_id()
201 {
202 return Err(ProcessIntentError::WelcomeAlreadyProcessed(self.welcome.cursor).into());
203 }
204 Ok(())
205 }
206
207 fn reject_or_retry(&self, error: GroupError) -> Result<Option<MlsGroup<C>>, GroupError> {
208 if terminal_welcome_error(&error) {
209 state_write(self.context.mls_storage(), |tx| {
210 let storage = tx.storage();
211 let db = storage.db();
212 self.check_pending(&db)?;
213 db.record_terminal_rejection(
214 &self.topic(),
215 self.welcome.cursor,
216 "invalid_welcome",
217 )?;
218 db.complete_pending_envelope(&self.topic(), self.welcome.cursor)?;
219 Ok::<_, GroupError>(Continue(()))
220 })?;
221 }
222 Err(error)
223 }
224
225 async fn validate_membership(
228 &self,
229 ) -> Result<(ResolvedWelcome, WelcomeMembership), GroupError> {
230 let resolved = ResolvedWelcome::resolve(self.welcome, &self.context).await?;
231 let mut membership = None;
232 state_write(self.context.mls_storage(), |tx| {
233 let storage = tx.storage();
234 let decrypted = resolved.stage(self.welcome, &storage)?;
235 self.join_anchor(&decrypted)?;
236 membership = Some(WelcomeMembership::from_staged(&decrypted.staged_welcome)?);
237 Ok::<_, GroupError>(Rollback::<()>)
238 })?;
239 let membership = membership.ok_or(GroupError::UninitializedResult)?;
240 membership.validate_sequences(self.welcome.sequence_id())?;
241 self.validator.check_initial_membership(&membership).await?;
242 Ok((resolved, membership))
243 }
244
245 fn join_anchor(&self, decrypted: &DecryptedWelcome) -> Result<Cursor, GroupError> {
249 let metadata =
250 extract_group_metadata(decrypted.staged_welcome.group_context().extensions())
251 .map_err(MetadataPermissionsError::from)?;
252 let anchor = match &decrypted.welcome_metadata {
253 Some(metadata) => metadata.message_cursor,
254 None if metadata.conversation_type == ConversationType::Oneshot => 0,
255 None => return Err(GroupError::InvalidWelcomeMetadata),
256 };
257 let initial = metadata.conversation_type == ConversationType::Oneshot
258 || decrypted.staged_welcome.group_context().epoch().as_u64() == 0;
259 if anchor >= self.welcome.sequence_id() || (anchor == 0 && !initial) {
260 return Err(GroupError::InvalidWelcomeMetadata);
261 }
262 Ok(Cursor(anchor))
263 }
264
265 fn commit_or_fail_forever(
268 &self,
269 resolved: &ResolvedWelcome,
270 membership: Option<&WelcomeMembership>,
271 missing_reference: Option<&IdentityRequirement>,
272 ) -> Result<CommitResult<C>, GroupError> {
273 tracing::debug!("attempting to commit welcome={}", &self.welcome.cursor);
274 let commit_result = state_write_with_events(
275 self.context.mls_storage(),
276 self.context.events(),
277 |tx, event_buffer| {
278 let storage = tx.storage();
279 self.check_pending(&storage.db())?;
280 let result = event_buffer.savepoint(|event_buffer| {
282 storage.savepoint(|conn| {
283 self.commit(conn, resolved, membership, event_buffer)
284 .map(Continue)
285 })
286 });
287 let db = storage.db();
288 let result = result.map_err(|error| match (&error, missing_reference) {
290 (
291 GroupError::InstallationDiff(InstallationDiffError::IdentityDependency(
292 IdentityDependencyError::Need(required),
293 )),
294 Some(missing),
295 ) if required.contains(missing) => InstallationDiffError::IdentityDependency(
296 IdentityDependencyError::MissingReference(missing.clone()),
297 )
298 .into(),
299 _ => error,
300 });
301 match result {
302 Err(err) if terminal_welcome_error(&err) => {
303 db.record_terminal_rejection(
304 &self.topic(),
305 self.welcome.cursor,
306 "invalid_welcome",
307 )?;
308 db.complete_pending_envelope(&self.topic(), self.welcome.cursor)?;
309 Ok(Continue(CommitResult::FailedForever(err)))
311 }
312 Err(e) => Err(e),
314 Ok(Continue(group)) => {
315 db.complete_pending_envelope(&self.topic(), self.welcome.cursor)?;
316 crate::worker::key_package_maintenance::queue_key_rotation_in(&storage)?;
320 event_buffer.emit(None, Some(InternalEvent::TaskScheduled));
321 self.context.task_channels().mark_notification_changed();
322 Ok(Continue(CommitResult::Ok(group)))
323 }
324 Ok(Rollback) => {
325 unreachable!("savepoint never intentionally rolls back here")
326 }
327 }
328 },
329 )
330 .map(TransactionOutcome::into_continued)?;
331 Ok(commit_result)
332 }
333
334 fn commit(
339 &self,
340 tx: &mut impl TransactionalKeyStore,
341 resolved: &ResolvedWelcome,
342 expected_membership: Option<&WelcomeMembership>,
343 event_buffer: &EventBuffer<'_, InternalEvent>,
344 ) -> Result<Option<MlsGroup<C>>, GroupError> {
345 let Self {
346 welcome, context, ..
347 } = self;
348
349 let storage = tx.key_store();
350 let db = storage.db();
351 let provider = XmtpOpenMlsProviderRef::new(&storage);
352
353 self.check_pending(&db)?;
354 let decrypted = resolved.stage(welcome, &storage)?;
355 let anchor = self.join_anchor(&decrypted)?;
356 let membership = WelcomeMembership::from_staged(&decrypted.staged_welcome)?;
357 membership.validate_sequences(welcome.sequence_id())?;
358 if expected_membership.is_some_and(|expected| *expected != membership) {
359 return Err(GroupError::LockUnavailable);
360 }
361 let DecryptedWelcome {
362 staged_welcome,
363 added_by_inbox_id,
364 added_by_installation_id,
365 welcome_metadata: _,
366 } = decrypted;
367 let metadata = extract_group_metadata(staged_welcome.group_context().extensions())
368 .map_err(MetadataPermissionsError::from)?;
369 let commit_log_id = check_group_root(
376 staged_welcome.group_context().extensions(),
377 staged_welcome.group_context().group_id().as_slice(),
378 )
379 .map_err(GroupError::WelcomeGroupRoot)?
380 .commit_log_id();
381 if metadata.conversation_type == ConversationType::Sync
384 && !is_own_sync_group(
385 context.inbox_id(),
386 &added_by_inbox_id,
387 staged_welcome.members(),
388 )
389 {
390 tracing::warn!(
391 added_by_inbox_id,
392 "rejecting a sync group Welcome that names another inbox"
393 );
394 return Err(GroupError::InvalidGroupMembership);
395 }
396 self.validator.check_verified_membership(&membership, &db)?;
397 if added_by_inbox_id == context.inbox_id() {
402 let newest = IdentityRequirement {
403 inbox_id: added_by_inbox_id.clone(),
404 sequence_id: db.get_latest_sequence_id_for_inbox(&added_by_inbox_id)? as u64,
405 };
406 let own =
408 require_association_state(&db, &newest).map_err(InstallationDiffError::from)?;
409 let adder = MemberIdentifier::installation(added_by_installation_id.clone());
410 if own.get(&adder).is_none() {
411 tracing::warn!("rejecting a Welcome from a revoked own installation");
412 return Err(GroupError::InvalidGroupMembership);
413 }
414 }
415 let group_id = GroupId::try_from(staged_welcome.group_context().group_id())?;
421 let existing_group = db.find_group(&group_id)?;
422 if let Some(existing) = &existing_group {
423 if existing.conversation_type != metadata.conversation_type {
424 return Err(GroupError::InvalidWelcomeMetadata);
425 }
426 if existing.conversation_type == ConversationType::Dm {
427 let stored_pair = crate::groups::parse_canonical_dm_id(existing.dm_id.as_deref())?;
428 if metadata.dm_members.as_ref().map(String::from).as_deref()
429 != Some(stored_pair.to_string().as_str())
430 {
431 return Err(MetadataPermissionsError::from(
432 crate::groups::DmValidationError::StoredDmIdMismatch,
433 )
434 .into());
435 }
436 }
437 }
438 if metadata.conversation_type == ConversationType::Oneshot {
439 Oneshot::process_welcome(
440 &provider,
441 context.inbox_id(),
442 welcome.cursor,
443 added_by_installation_id,
444 metadata,
445 )?;
446 return Ok(None);
447 }
448
449 let mut anchor_mode = JoinAnchorMode::Advance;
450
451 if let Some(existing) = &existing_group
452 && existing.membership_state != GroupMembershipState::Restored
453 {
454 let mut current = OpenMlsGroup::load(&storage, &group_id.to_openmls())?
455 .ok_or(xmtp_db::NotFound::MlsGroup(group_id))?;
456 let processed = db.latest_cursor_for_id(group_id, &[EntityKind::ApplicationMessage])?;
457 let incoming_epoch = staged_welcome.group_context().epoch();
458 let active = current.is_active();
459 if processed == anchor
464 && !active
465 && current.epoch() == incoming_epoch
466 && current.public_group().group_context().tree_hash()
467 == staged_welcome.group_context().tree_hash()
468 {
469 anchor_mode = JoinAnchorMode::InactiveReadd;
470 }
471 if processed > anchor
475 || (processed == anchor && anchor_mode != JoinAnchorMode::InactiveReadd)
476 || current.epoch() > incoming_epoch
477 || (active && current.epoch() >= incoming_epoch)
478 {
479 return Err(ProcessIntentError::WelcomeAlreadyProcessed(welcome.cursor).into());
480 }
481 if active {
482 return Err(GroupError::WelcomeGroupPrefixPending {
483 group_id,
484 anchor: anchor.0,
485 });
486 }
487 current.clear_pending_proposals(&storage)?;
491 db.forget_received_proposals(group_id)?;
492 }
493
494 let is_readd_after_leaving = existing_group
497 .as_ref()
498 .is_some_and(|g| g.membership_state == GroupMembershipState::PendingRemove);
499
500 let prior_membership_cutoff = db.get_local_commit_log_cursor(&group_id)?.unwrap_or(0);
501 let mls_group = OpenMlsGroup::from_welcome_logged(
502 &provider,
503 staged_welcome,
504 &added_by_inbox_id,
505 &added_by_installation_id,
506 context.server_configuration().commit_log_enabled(),
507 )?;
508 let publisher_boundary =
512 i64::try_from(anchor.0).map_err(|_| xmtp_db::StorageError::DbSerialize)?;
513 let publisher_boundaries = xmtp_mls_validation::ForkActors::from_group(&mls_group)?
514 .publisher_keys(&mls_group)?
515 .into_iter()
516 .map(|author| (author, publisher_boundary))
517 .collect();
518 db.set_commit_log_publisher_boundaries(&group_id, &publisher_boundaries)?;
519 let dm_members = metadata.dm_members;
520 let conversation_type = metadata.conversation_type;
521 let mutable_metadata = Some(
523 extract_group_mutable_metadata_capability_aware(&mls_group)
524 .map_err(|_| GroupError::InvalidWelcomeMetadata)?,
525 );
526 let disappearing_settings = mutable_metadata.as_ref().and_then(|metadata| {
527 MlsGroup::<C>::conversation_message_disappearing_settings_from_extensions(metadata).ok()
528 });
529
530 if let Some(min_version) = mutable_metadata
531 .as_ref()
532 .and_then(MlsGroup::<C>::min_protocol_version_from_extensions)
533 {
534 let required = LibXMTPVersion::parse(&min_version)
535 .map_err(|_| GroupError::InvalidWelcomeMetadata)?;
536 if required > *context.version_info().pkg_semver() {
537 return Err(GroupError::UnsupportedWelcomeVersion(min_version));
538 }
539 }
540
541 {
544 let registry = crate::groups::app_data::load_component_registry(&mls_group)
545 .map_err(|_| GroupError::InvalidWelcomeMetadata)?;
546 xmtp_mls_group_state::app_data::policy_set::validate_registry_action_policies(
547 ®istry,
548 )
549 .map_err(|error| {
550 tracing::warn!(%error, "invalid registry action policy in Welcome");
551 GroupError::InvalidWelcomeMetadata
552 })?;
553 }
554
555 let leave_pending = is_readd_after_leaving
558 && db.get_user_pending_remove_status(&group_id, context.inbox_id())?;
559 let membership_state = if leave_pending {
560 GroupMembershipState::PendingRemove
561 } else if is_readd_after_leaving {
562 tracing::info!(
563 group_id = %group_id,
564 "User is being re-added after leaving/removal, setting membership state to ALLOWED"
565 );
566 GroupMembershipState::Allowed
567 } else {
568 tracing::debug!(
569 group_id = %group_id,
570 "User is being added to new group, setting membership state to PENDING"
571 );
572 GroupMembershipState::Pending
573 };
574
575 let joined_at_ns = if conversation_type == ConversationType::Sync {
577 xmtp_db::group::next_sync_join_time(&db)?
578 } else {
579 now_ns()
580 };
581 let mut group = StoredGroup::builder();
582 group
583 .id(group_id)
584 .created_at_ns(joined_at_ns)
585 .added_by_inbox_id(&added_by_inbox_id)
586 .cursor(welcome.cursor)
587 .conversation_type(conversation_type)
588 .dm_id(dm_members.map(String::from))
589 .message_disappear_from_ns(disappearing_settings.as_ref().map(|m| m.from_ns))
590 .message_disappear_in_ns(disappearing_settings.as_ref().map(|m| m.in_ns))
591 .commit_log_public_key(Some(commit_log_id.to_vec()))
592 .should_publish_commit_log(MlsGroup::<C>::check_should_publish_commit_log(
593 context.inbox_id().to_string(),
594 mutable_metadata,
595 ));
596
597 let to_store = match conversation_type {
598 ConversationType::Group => group.membership_state(membership_state).build()?,
599 ConversationType::Dm => {
600 validate_dm_group(context, &mls_group, &added_by_inbox_id)?;
601 group
602 .membership_state(membership_state)
603 .last_message_ns(welcome.timestamp())
604 .build()?
605 }
606 ConversationType::Sync => {
607 group
609 .membership_state(GroupMembershipState::Allowed)
610 .build()?
611 }
612 ConversationType::Oneshot => {
613 unreachable!("StagedWelcome of type Oneshot should already be handled")
614 }
615 };
616
617 tracing::debug!("storing group with welcome id {}", welcome.cursor);
618
619 if is_readd_after_leaving && let Some(ref existing) = existing_group {
622 tracing::info!(
623 group_id = %existing.id,
624 "Updating existing group membership state after readd"
625 );
626 db.update_group_membership(existing.id, membership_state)?;
627 }
628
629 if existing_group
631 .as_ref()
632 .is_some_and(|group| group.membership_state != GroupMembershipState::Restored)
633 {
634 db.set_group_commit_log_forked_status(&group_id, None)?;
635 }
636
637 let should_publish = to_store.should_publish_commit_log;
639 let mut stored_group = db.insert_or_replace_group(to_store)?;
640 db.set_should_publish_commit_log(&stored_group.id, should_publish)?;
644 db.update_cursor(
647 group_id,
648 EntityKind::CommitLogUpload,
649 Cursor(prior_membership_cutoff as u64),
650 )?;
651 db.update_cursor(group_id, EntityKind::CommitLogPublisherRecovery, Cursor(1))?;
652 db.set_commit_log_reader_epoch_floor(&group_id, 0)?;
654 stored_group.should_publish_commit_log = should_publish;
655 if stored_group.conversation_type == ConversationType::Sync {
660 schedule_newcomer_adds(&db, event_buffer)?;
661 if xmtp_db::sync_outbox::mark_all_owed(&db)? {
662 emit_root_key_changed(event_buffer, &db, true)?;
663 }
664 event_buffer.emit(None, Some(InternalEvent::OutboxChanged));
665 }
666
667 let current_inbox_id = context.inbox_id().to_string();
669 let added_payload = GroupUpdated {
670 initiated_by_inbox_id: added_by_inbox_id.clone(),
671 added_inboxes: vec![Inbox {
672 inbox_id: current_inbox_id.clone(),
673 }],
674 removed_inboxes: vec![],
675 metadata_field_changes: vec![],
676 left_inboxes: vec![],
677 added_admin_inboxes: vec![],
678 removed_admin_inboxes: vec![],
679 added_super_admin_inboxes: vec![],
680 removed_super_admin_inboxes: vec![],
681 };
682
683 let encoded_added_payload = GroupUpdatedCodec::encode(added_payload)?;
684 let mut encoded_added_payload_bytes = Vec::new();
685 encoded_added_payload.encode(&mut encoded_added_payload_bytes)?;
686
687 let added_idempotency_key = format!("{}_welcome_added", welcome.created_ns);
688 let added_message_id = crate::groups::message_id::calculate_message_id(
689 stored_group.id,
690 encoded_added_payload_bytes.as_slice(),
691 &added_idempotency_key,
692 );
693
694 let added_content_type = encoded_added_payload.r#type.unwrap_or_else(|| {
695 tracing::warn!("Missing content type in encoded added payload, using default values");
696 ContentTypeId {
697 authority_id: "unknown".to_string(),
698 type_id: "unknown".to_string(),
699 version_major: 0,
700 version_minor: 0,
701 }
702 });
703
704 let cursor = anchor.0 as i64;
705
706 let added_msg = StoredGroupMessage {
708 id: added_message_id,
709 group_id: stored_group.id,
710 decrypted_message_bytes: encoded_added_payload_bytes,
711 sent_at_ns: welcome.timestamp(),
712 kind: GroupMessageKind::MembershipChange,
713 sender_installation_id: added_by_installation_id,
714 sender_inbox_id: added_by_inbox_id.clone(),
715 delivery_status: DeliveryStatus::Published,
716 content_type: ContentType::from_identifier(
717 &added_content_type.authority_id,
718 &added_content_type.type_id,
719 added_content_type.version_major,
720 ),
721 version_major: added_content_type.version_major as i32,
722 version_minor: added_content_type.version_minor as i32,
723 authority_id: added_content_type.authority_id,
724 reference_id: None,
725 sequence_id: cursor,
726 envelope_hash: None,
727 expiry_ns: None,
728 expiry_unknown: false,
729 deleted_by_inbox_id: None,
730 expire_at_ns: None,
731 inserted_at_ns: 0, should_push: true,
733 idempotency_key: added_idempotency_key,
735 };
736
737 added_msg.store_or_ignore(&db)?;
738
739 tracing::debug!("created GroupUpdated message for welcome, inbox_id={current_inbox_id}");
740
741 let group = MlsGroup::new(
742 context.clone(),
743 stored_group.id,
744 stored_group.dm_id,
745 stored_group.conversation_type,
746 stored_group.created_at_ns,
747 );
748
749 db.install_group_anchor(group.group_id, anchor, anchor_mode)?;
751 db.record_welcome_discovery(group.group_id, welcome.cursor)?;
752 db.update_responded_at_sequence_id(
753 &group.group_id,
754 context.installation_id().as_slice(),
755 cursor,
756 )?;
757 event_buffer.emit_with_context(
758 (!stored_group.conversation_type.is_virtual()).then(|| {
759 ClientEvent::ConversationJoined(ConversationJoined {
760 group_id: group.group_id.to_vec(),
761 conversation_type: if stored_group.conversation_type == ConversationType::Dm {
762 EventConversationType::Dm
763 } else {
764 EventConversationType::Group
765 },
766 origin: JoinOrigin::Welcomed,
767 adder_inbox_id: Some(added_by_inbox_id.clone()),
768 })
769 }),
770 Some(InternalEvent::GroupJoined {
771 group_id: group.group_id,
772 is_sync: stored_group.conversation_type == ConversationType::Sync,
773 }),
774 dm_event_context(group.dm_id.as_deref()),
775 );
776
777 tracing::debug!(
778 inbox_id = %current_inbox_id,
779 installation_id = %self.context.installation_id().as_slice().snippet(),
780 group_id = %group.group_id,
781 welcome_id = welcome.cursor.0,
782
783 cursor = cursor,
784 "updated message cursor from welcome metadata"
785 );
786
787 Ok(Some(group))
788 }
789}
790
791#[cfg(test)]
792mod tests {
793 use super::*;
794 use crate::groups::InitialMembershipValidator;
795 use crate::prelude::*;
796 use crate::tester;
797 use crate::utils::test::MlsGroupExt;
798
799 struct UnavailableValidator;
800
801 impl ValidateGroupMembership for UnavailableValidator {
802 async fn check_initial_membership(
803 &self,
804 _welcome: &WelcomeMembership,
805 ) -> Result<(), GroupError> {
806 Err(GroupError::LockUnavailable)
807 }
808 }
809
810 #[xmtp_common::test(unwrap_try = true)]
811 async fn trial_validation_failure_preserves_welcome_keys() {
812 tester!(alix, disable_workers);
813 tester!(bo, disable_workers);
814 let alix_group = alix.create_group(None, None)?;
815 alix_group.invite(&bo).await?;
816 let welcome = bo
817 .context
818 .api()
819 .query_welcome_messages(bo.context.installation_id())
820 .await?
821 .pop()?;
822
823 let events = bo.context.events().subscribe(
824 xmtp_events::EventFilter::new([xmtp_events::EventKind::ConversationJoined]),
825 Some(10),
826 );
827 let result = XmtpWelcome::builder()
828 .context(bo.context.clone())
829 .welcome(&welcome)
830 .pending(
831 crate::groups::welcome_sync::pending_welcome_for_test(&bo.context, &welcome)
832 .await?,
833 )
834 .validator(UnavailableValidator)
835 .process()
836 .await;
837 assert!(matches!(result, Err(GroupError::LockUnavailable)));
838 assert!(events.drain().is_empty());
839 assert!(bo.context.db().find_group(&alix_group.group_id)?.is_none());
840 assert_eq!(
841 bo.context
842 .db()
843 .get_last_cursor(bo.context.installation_id(), EntityKind::Welcome)?,
844 Cursor(0)
845 );
846
847 let bo_group = bo.sync_welcomes().await?.pop()?;
848 assert!(matches!(
849 events.drain().pop().and_then(|event| event.client),
850 Some(xmtp_events::ClientEvent::ConversationJoined(joined)) if joined.group_id == bo_group.group_id.to_vec()
851 ));
852 alix_group.test_can_talk_with(&bo_group).await?;
853 }
854
855 struct JoinDuringValidation<C> {
856 context: C,
857 welcome: xmtp_proto::types::WelcomeMessage,
858 }
859
860 impl<C: XmtpSharedContext> ValidateGroupMembership for JoinDuringValidation<C> {
861 async fn check_initial_membership(
862 &self,
863 _welcome: &WelcomeMembership,
864 ) -> Result<(), GroupError> {
865 let group = XmtpWelcome::builder()
866 .context(self.context.clone())
867 .welcome(&self.welcome)
868 .pending(
869 crate::groups::welcome_sync::pending_welcome_for_test(
870 &self.context,
871 &self.welcome,
872 )
873 .await?,
874 )
875 .validator(InitialMembershipValidator::new(self.context.clone()))
876 .process()
877 .await?
878 .ok_or(GroupError::UninitializedResult)?;
879 group.sync_with_conn().await?;
880 Ok(())
881 }
882 }
883
884 #[xmtp_common::test(unwrap_try = true)]
885 async fn fresh_install_rejects_welcome_after_another_writer_advances_group() {
886 tester!(alix, disable_workers);
887 tester!(bo, disable_workers);
888 let alix_group = alix.create_group(None, None)?;
889 alix_group.invite(&bo).await?;
890 alix_group
891 .update_group_name("advanced during validation".into())
892 .await?;
893 let welcome = bo
894 .context
895 .api()
896 .query_welcome_messages(bo.context.installation_id())
897 .await?
898 .pop()?;
899
900 let result = XmtpWelcome::builder()
901 .context(bo.context.clone())
902 .welcome(&welcome)
903 .pending(
904 crate::groups::welcome_sync::pending_welcome_for_test(&bo.context, &welcome)
905 .await?,
906 )
907 .validator(JoinDuringValidation {
908 context: bo.context.clone(),
909 welcome: welcome.clone(),
910 })
911 .process()
912 .await;
913 assert!(matches!(
914 result,
915 Err(GroupError::ProcessIntent(
916 ProcessIntentError::WelcomeAlreadyProcessed(_)
917 ))
918 ));
919 let bo_group = bo.group(&alix_group.group_id)?;
920 assert_eq!(
921 alix_group.epoch_authenticator().await?,
922 bo_group.epoch_authenticator().await?
923 );
924 alix_group.test_can_talk_with(&bo_group).await?;
925 }
926
927 async fn process_newest_welcome(
929 joiner: &crate::utils::FullXmtpClient,
930 ) -> Result<Option<MlsGroup<crate::utils::TestXmtpMlsContext>>, GroupError> {
931 let welcome = joiner
932 .context
933 .api()
934 .query_welcome_messages(joiner.context.installation_id())
935 .await?
936 .into_iter()
937 .max_by_key(|welcome| welcome.cursor)
938 .ok_or(GroupError::UninitializedResult)?;
939 XmtpWelcome::builder()
940 .context(joiner.context.clone())
941 .welcome(&welcome)
942 .pending(
943 crate::groups::welcome_sync::pending_welcome_for_test(&joiner.context, &welcome)
944 .await?,
945 )
946 .validator(InitialMembershipValidator::new(joiner.context.clone()))
947 .process()
948 .await
949 }
950
951 #[xmtp_common::test(unwrap_try = true)]
958 async fn a_welcome_joins_only_when_its_root_names_the_group() {
959 use crate::utils::test::crafted_welcome::{
960 assert_welcome_rejected, send_crafted_welcome, send_crafted_welcome_of_type,
961 };
962 use xmtp_mls_group_keys::group_keys::{GroupKeys, GroupRoot};
963 use xmtp_mls_group_state::app_data::group_root::GroupRootError;
964
965 tester!(alix, disable_workers);
966 tester!(bo, disable_workers);
967 let keyed_id = |root: &GroupRoot| GroupKeys::derive(root).group_id().to_vec();
968 let root = GroupRoot::generate();
969 let other_root = GroupRoot::generate();
970 let legacy_id = xmtp_common::rand_vec::<{ xmtp_configuration::LEGACY_GROUP_ID_BYTES }>();
971
972 for (case, group_id, welcome_root, conversation_type, expected) in [
973 (
974 "no root",
975 keyed_id(&root),
976 None,
977 ConversationType::Group,
978 GroupRootError::Missing,
979 ),
980 (
981 "another group's root",
982 keyed_id(&root),
983 Some(&other_root),
984 ConversationType::Group,
985 GroupRootError::OtherGroup,
986 ),
987 (
988 "a 16-byte group",
989 legacy_id.clone(),
990 Some(&root),
991 ConversationType::Group,
992 GroupRootError::OtherGroup,
993 ),
994 (
995 "a one-shot group with no root",
996 keyed_id(&other_root),
997 None,
998 ConversationType::Oneshot,
999 GroupRootError::Missing,
1000 ),
1001 ] {
1002 send_crafted_welcome_of_type(&alix, &bo, &group_id, welcome_root, conversation_type)
1003 .await;
1004 let cursor = bo
1005 .context
1006 .api()
1007 .query_welcome_messages(bo.context.installation_id())
1008 .await?
1009 .into_iter()
1010 .map(|welcome| welcome.cursor)
1011 .max()
1012 .expect(case);
1013 let result = process_newest_welcome(&bo).await;
1014 assert!(
1015 matches!(&result, Err(GroupError::WelcomeGroupRoot(error)) if *error == expected),
1016 "{case}: {result:?}"
1017 );
1018 let group_id = GroupId::try_from(group_id.as_slice())?;
1019 assert_eq!(
1020 assert_welcome_rejected(&bo, &group_id),
1021 cursor,
1022 "{case}: the rejection is this Welcome's"
1023 );
1024 }
1025
1026 let joined_id = keyed_id(&root);
1027 send_crafted_welcome(&alix, &bo, &joined_id, Some(&root)).await;
1028 let joined = process_newest_welcome(&bo).await?.expect("a group");
1029 assert_eq!(joined.group_id.as_slice(), joined_id.as_slice());
1030 }
1031
1032 #[rstest::rstest]
1038 #[case::revoked_sync(ConversationType::Sync, true)]
1039 #[case::revoked_group(ConversationType::Group, true)]
1040 #[case::current_uncached(ConversationType::Group, false)]
1041 #[xmtp_common::test(unwrap_try = true)]
1042 async fn own_adder_must_be_current(#[case] kind: ConversationType, #[case] revoked: bool) {
1043 use crate::utils::test::crafted_welcome::{
1044 assert_welcome_rejected, send_crafted_welcome_of_type,
1045 };
1046 use diesel::prelude::*;
1047 use xmtp_db::{ConnectionExt, schema::association_state::dsl};
1048 use xmtp_id::associations::test_utils::add_wallet_signature;
1049 use xmtp_mls_group_keys::group_keys::{GroupKeys, GroupRoot};
1050
1051 tester!(alix1, disable_workers);
1052 tester!(alix2, from: alix1, disable_workers);
1053 let identity = alix1.identity_updates();
1054 let db = alix1.context.db();
1055 if revoked {
1056 let revoked = vec![alix2.context.installation_id().to_vec()];
1057 let mut revoke = identity.revoke_installations(revoked).await.unwrap();
1058 add_wallet_signature(&mut revoke, &alix1.builder.owner).await;
1059 identity.apply_signature_request(revoke).await.unwrap();
1060 } else {
1061 tester!(_alix3, from: alix1, disable_workers);
1063 let inbox_id = alix1.inbox_id();
1064 identity
1065 .get_latest_association_state(&db, inbox_id)
1066 .await
1067 .unwrap();
1068 let newest = db.get_latest_sequence_id_for_inbox(inbox_id).unwrap();
1069 let cached = dsl::association_state.filter(dsl::sequence_id.eq(newest));
1070 db.raw_query(|conn| diesel::delete(cached).execute(conn))
1071 .unwrap();
1072 }
1073
1074 let root = GroupRoot::generate();
1075 let group_id = GroupKeys::derive(&root).group_id().to_vec();
1076 send_crafted_welcome_of_type(&alix2, &alix1, &group_id, Some(&root), kind).await;
1077 let group_id = GroupId::try_from(group_id.as_slice()).unwrap();
1078 if !revoked {
1079 let joined = xmtp_common::wait_for_some(|| async {
1081 alix1.sync_welcomes().await.unwrap();
1082 db.find_group(&group_id).unwrap()
1083 });
1084 assert!(joined.await.is_some());
1085 return;
1086 }
1087
1088 let result = process_newest_welcome(&alix1).await;
1089 assert!(
1090 matches!(result, Err(GroupError::InvalidGroupMembership)),
1091 "{result:?}"
1092 );
1093 assert_welcome_rejected(&alix1, &group_id);
1094 let consent = db
1095 .consent_row(&xmtp_db::consent_record::ConsentKey::group(&group_id))
1096 .unwrap();
1097 assert!(consent.is_none(), "{consent:?}");
1098 }
1099
1100 #[xmtp_common::test(unwrap_try = true)]
1106 async fn inactive_group_rejects_stale_branch_and_keeps_pending_prefix() {
1107 use crate::utils::test::crafted_welcome::{CraftedWelcome, send_crafted_welcome_with};
1108 use xmtp_db::incoming_envelope::QueryIncomingEnvelope;
1109 use xmtp_mls_group_state::app_data::group_root::read_group_root;
1110 use xmtp_proto::types::Topic;
1111
1112 tester!(alix, disable_workers);
1113 tester!(bo, disable_workers);
1114 let alix_group = alix.create_group(None, None)?;
1115 let root = read_group_root(alix_group.load_group_context()?.extensions())?
1116 .expect("a new group carries its root");
1117 alix_group.invite(&bo).await?;
1118 let bo_group = bo.sync_welcomes().await?.pop().expect("initial Welcome");
1119 bo_group.sync().await?;
1120 alix_group.remove_members(&[bo.inbox_id()]).await?;
1121 let _ = bo_group.sync().await;
1122 assert!(!bo_group.is_active()?, "the removal must be applied");
1123
1124 let db = bo.context.db();
1125 let topic = StreamTopic::group(bo_group.group_id);
1126 let before = db.topic_progress(&topic)?;
1127 let old_epoch = bo_group.epoch().await?;
1128 assert!(old_epoch > 1, "the crafted branch joins at epoch one");
1129
1130 alix_group.send_msg(b"after removal").await;
1133 bo.mls_store()
1134 .receive_topics_once(
1135 &[Topic::new_group_message(bo_group.group_id)],
1136 bo.context
1137 .incoming_runtime()
1138 .policy()
1139 .incoming_limits(topic.kind),
1140 )
1141 .await?;
1142 let forged_anchor = db.topic_progress(&topic)?.received;
1143 assert!(before.processed < forged_anchor);
1144 assert!(db.pending_envelope(&topic, forged_anchor)?.is_some());
1145
1146 let options = CraftedWelcome {
1147 anchor: forged_anchor.0,
1148 ..Default::default()
1149 };
1150 send_crafted_welcome_with(
1151 &alix,
1152 &bo,
1153 alix_group.group_id.as_slice(),
1154 Some(&root),
1155 options,
1156 )
1157 .await;
1158 let result = process_newest_welcome(&bo).await;
1159 assert!(
1160 matches!(
1161 result,
1162 Err(GroupError::ProcessIntent(
1163 ProcessIntentError::WelcomeAlreadyProcessed(_)
1164 ))
1165 ),
1166 "{result:?}"
1167 );
1168 bo.assert_newest_welcome_rejected().await;
1169 send_crafted_welcome_with(
1170 &alix,
1171 &bo,
1172 alix_group.group_id.as_slice(),
1173 Some(&root),
1174 CraftedWelcome {
1175 anchor: before.processed.0,
1176 prior_epoch: old_epoch - 1,
1177 ..Default::default()
1178 },
1179 )
1180 .await;
1181 assert!(matches!(
1182 process_newest_welcome(&bo).await,
1183 Err(GroupError::ProcessIntent(
1184 ProcessIntentError::WelcomeAlreadyProcessed(_)
1185 ))
1186 ));
1187 bo.assert_newest_welcome_rejected().await;
1188 assert!(!bo_group.is_active()?);
1189 assert_eq!(bo_group.epoch().await?, old_epoch);
1190 assert_eq!(db.topic_progress(&topic)?.processed, before.processed);
1191 assert!(db.pending_envelope(&topic, forged_anchor)?.is_some());
1192 }
1193
1194 #[xmtp_common::test(unwrap_try = true)]
1200 async fn replacement_welcome_cannot_change_conversation_type() {
1201 use crate::utils::test::crafted_welcome::{
1202 CraftedWelcome, send_crafted_welcome_of_type, send_crafted_welcome_with,
1203 };
1204 use xmtp_mls_group_state::app_data::group_root::read_group_root;
1205
1206 tester!(alix, disable_workers);
1207 tester!(bo, disable_workers);
1208 tester!(caro, disable_workers);
1209
1210 let original = alix.create_group(None, None)?;
1211 original.invite(&caro).await?;
1212 let caro_group = caro.sync_welcomes().await?.pop().expect("Caro joined");
1213 let retained_root = read_group_root(caro_group.load_group_context()?.extensions())?
1214 .expect("the original group has a root");
1215 original.invite(&bo).await?;
1216 let bo_group = bo.sync_welcomes().await?.pop().expect("Bo joined");
1217 original.remove_members(&[caro.inbox_id()]).await?;
1218 original.remove_members(&[bo.inbox_id()]).await?;
1219 bo_group.sync_with_conn().await?;
1220 assert!(!bo_group.is_active()?);
1221 let row_before_forgery = bo
1222 .context
1223 .db()
1224 .find_group(&original.group_id)?
1225 .expect("stored");
1226 assert_eq!(
1227 row_before_forgery.conversation_type,
1228 ConversationType::Group
1229 );
1230 let anchor = bo_group.cursor().await?;
1231 let prior_epoch = bo_group.epoch().await?;
1232
1233 let options = CraftedWelcome {
1236 conversation_type: ConversationType::Dm,
1237 anchor: anchor.0,
1238 prior_epoch,
1239 ..Default::default()
1240 };
1241 send_crafted_welcome_with(
1242 &caro,
1243 &bo,
1244 original.group_id.as_slice(),
1245 Some(&retained_root),
1246 options,
1247 )
1248 .await;
1249 let result = process_newest_welcome(&bo).await;
1250 assert!(
1251 matches!(result, Err(GroupError::InvalidWelcomeMetadata)),
1252 "{result:?}"
1253 );
1254 bo.assert_newest_welcome_rejected().await;
1255
1256 send_crafted_welcome_of_type(
1257 &caro,
1258 &bo,
1259 original.group_id.as_slice(),
1260 Some(&retained_root),
1261 ConversationType::Oneshot,
1262 )
1263 .await;
1264 let result = process_newest_welcome(&bo).await;
1265 assert!(
1266 matches!(result, Err(GroupError::InvalidWelcomeMetadata)),
1267 "{result:?}"
1268 );
1269 bo.assert_newest_welcome_rejected().await;
1270
1271 assert_eq!(
1272 bo.context.db().find_group(&original.group_id)?,
1273 Some(row_before_forgery)
1274 );
1275 assert!(!bo_group.is_active()?);
1276 assert_eq!(bo_group.epoch().await?, prior_epoch);
1277 assert_eq!(bo_group.cursor().await?, anchor);
1278 }
1279
1280 #[xmtp_common::test(unwrap_try = true)]
1286 async fn live_dm_rejects_welcome_with_another_pair() {
1287 use crate::groups::DmValidationError;
1288 use crate::utils::test::crafted_welcome::{CraftedWelcome, send_crafted_welcome_with};
1289 use xmtp_mls_group_state::app_data::group_root::read_group_root;
1290
1291 tester!(alix, disable_workers);
1292 tester!(bo, disable_workers);
1293 tester!(caro, disable_workers);
1294 let alix_dm = alix.find_or_create_dm(bo.inbox_id(), None).await?;
1295 let bo_dm = bo.sync_welcomes().await?.pop().expect("Bo joined the DM");
1296 assert!(bo_dm.is_active()?);
1297 let root = read_group_root(alix_dm.load_group_context()?.extensions())?
1298 .expect("a new DM carries its root");
1299 let row_before = bo.context.db().find_group(&alix_dm.group_id)?;
1300 let cursor_before = bo_dm.cursor().await?;
1301 let epoch_before = bo_dm.epoch().await?;
1302
1303 let options = CraftedWelcome {
1304 conversation_type: ConversationType::Dm,
1305 anchor: cursor_before.0 + 1,
1306 prior_epoch: epoch_before,
1307 dm_peer: Some(caro.inbox_id()),
1308 ..Default::default()
1309 };
1310 send_crafted_welcome_with(
1311 &alix,
1312 &bo,
1313 alix_dm.group_id.as_slice(),
1314 Some(&root),
1315 options,
1316 )
1317 .await;
1318 let result = process_newest_welcome(&bo).await;
1319 assert!(
1320 matches!(
1321 result,
1322 Err(GroupError::MetadataPermissionsError(
1323 MetadataPermissionsError::DmValidation(DmValidationError::StoredDmIdMismatch)
1324 ))
1325 ),
1326 "{result:?}"
1327 );
1328 bo.assert_newest_welcome_rejected().await;
1329 assert_eq!(bo.context.db().find_group(&alix_dm.group_id)?, row_before);
1330 assert_eq!(bo_dm.cursor().await?, cursor_before);
1331 assert_eq!(bo_dm.epoch().await?, epoch_before);
1332 alix_dm.test_can_talk_with(&bo_dm).await?;
1333 }
1334
1335 #[xmtp_common::test(unwrap_try = true)]
1341 async fn dm_welcome_with_a_nonstandard_registry_is_rejected() {
1342 use crate::groups::DmValidationError;
1343 use crate::utils::test::crafted_welcome::{CraftedWelcome, send_crafted_welcome_with};
1344 use xmtp_mls_group_keys::group_keys::{GroupKeys, GroupRoot};
1345 use xmtp_mls_group_state::app_data::{
1346 component_id::ComponentId, dm_registry_profile::dm_registry_profiles,
1347 };
1348 use xmtp_proto::xmtp::mls::message_contents::{
1349 MetadataPolicy,
1350 metadata_policy::{Kind, MetadataBasePolicy},
1351 };
1352
1353 tester!(alix, disable_workers);
1354 tester!(bo, disable_workers);
1355 let mut registry = dm_registry_profiles().pop()?;
1356 let mut membership = registry.get(&ComponentId::GROUP_MEMBERSHIP)?.unwrap();
1357 membership.permissions.as_mut().unwrap().update_policy = Some(MetadataPolicy {
1358 kind: Some(Kind::Base(MetadataBasePolicy::Deny as i32)),
1359 });
1360 registry.set(ComponentId::GROUP_MEMBERSHIP, membership)?;
1361
1362 let crafted_dm = async |registry| {
1363 let root = GroupRoot::generate();
1364 let group_id = GroupKeys::derive(&root).group_id().to_vec();
1365 let options = CraftedWelcome {
1366 conversation_type: ConversationType::Dm,
1367 registry,
1368 ..Default::default()
1369 };
1370 send_crafted_welcome_with(&alix, &bo, &group_id, Some(&root), options).await;
1371 group_id
1372 };
1373
1374 let rejected_id = crafted_dm(Some(®istry)).await;
1375 let result = process_newest_welcome(&bo).await;
1376 assert!(
1377 matches!(
1378 result,
1379 Err(GroupError::MetadataPermissionsError(
1380 MetadataPermissionsError::DmValidation(DmValidationError::InvalidPermissions)
1381 ))
1382 ),
1383 "{result:?}"
1384 );
1385 bo.assert_newest_welcome_rejected().await;
1386 let rejected_id = GroupId::try_from(rejected_id.as_slice())?;
1387 assert!(bo.context.db().find_group(&rejected_id)?.is_none());
1388
1389 let joined_id = crafted_dm(None).await;
1390 let joined = process_newest_welcome(&bo)
1391 .await?
1392 .expect("the creator's registry joins");
1393 assert_eq!(joined.group_id.as_slice(), joined_id.as_slice());
1394 }
1395
1396 #[rstest::rstest]
1401 #[case::prior_denied(true)]
1402 #[case::no_record(false)]
1403 #[xmtp_common::test(unwrap_try = true)]
1404 async fn forged_creator_claim_keeps_prior_consent(#[case] prior_denied: bool) {
1405 use crate::utils::test::crafted_welcome::{CraftedWelcome, send_crafted_welcome_with};
1406 use xmtp_db::consent_record::{ConsentKey, ConsentState, ConsentType, StoredConsentRecord};
1407 use xmtp_mls_group_keys::group_keys::{GroupKeys, GroupRoot};
1408
1409 tester!(alix, disable_workers);
1410 tester!(bo, disable_workers);
1411 let root = GroupRoot::generate();
1412 let group_id = GroupKeys::derive(&root).group_id().to_vec();
1413 let entity = hex::encode(&group_id);
1414 let prior = prior_denied.then(|| {
1416 StoredConsentRecord::new(
1417 ConsentType::ConversationId,
1418 ConsentState::Denied,
1419 entity.clone(),
1420 )
1421 });
1422 if let Some(prior) = &prior {
1423 prior.merge_into(&bo.context.db()).unwrap();
1424 }
1425
1426 let options = CraftedWelcome {
1427 claimed_creator: Some(bo.inbox_id()),
1428 ..Default::default()
1429 };
1430 send_crafted_welcome_with(&alix, &bo, &group_id, Some(&root), options).await;
1431 let joined = process_newest_welcome(&bo)
1432 .await
1433 .unwrap()
1434 .expect("valid Welcome");
1435 assert_eq!(joined.added_by_inbox_id().unwrap(), alix.inbox_id());
1436 let stored = bo
1437 .context
1438 .db()
1439 .consent_row(&ConsentKey::group(&group_id))
1440 .unwrap();
1441 assert_eq!(
1442 stored.map(|record| (record.state, record.consented_at_ns)),
1443 prior.map(|record| (record.state, record.consented_at_ns))
1444 );
1445 }
1446
1447 #[xmtp_common::test(unwrap_try = true)]
1453 async fn joining_stores_no_state() {
1454 use crate::utils::test::crafted_welcome::{CraftedWelcome, send_crafted_welcome_with};
1455 use xmtp_db::consent_record::{ConsentKey, ConsentState};
1456 use xmtp_mls_group_keys::group_keys::{GroupKeys, GroupRoot};
1457
1458 tester!(alix, disable_workers);
1459 tester!(bo1, disable_workers);
1460 tester!(bo2, from: bo1, disable_workers);
1461 let root = GroupRoot::generate();
1462 let group_id = GroupKeys::derive(&root).group_id().to_vec();
1463 let options = CraftedWelcome {
1464 claimed_creator: Some(bo1.inbox_id()),
1465 ..Default::default()
1466 };
1467 send_crafted_welcome_with(&alix, &bo1, &group_id, Some(&root), options).await;
1468 let joined = process_newest_welcome(&bo1).await?.expect("valid Welcome");
1469 joined.update_installations().await?;
1471 bo2.sync_welcomes().await?;
1472 let own = bo1.create_group(None, None)?;
1473 own.update_installations().await?;
1474 bo2.sync_welcomes().await?;
1475 for bo in [&bo1, &bo2] {
1476 assert_eq!(
1477 bo.group(&joined.group_id)?.consent_state()?,
1478 ConsentState::Unknown
1479 );
1480 assert!(
1481 bo.db()
1482 .consent_row(&ConsentKey::group(&group_id))?
1483 .is_none()
1484 );
1485 }
1486 assert_eq!(
1487 bo1.group(&own.group_id)?.consent_state()?,
1488 ConsentState::Allowed
1489 );
1490 let own_key = ConsentKey::group(own.group_id.as_ref());
1491 assert!(
1492 bo2.db().consent_row(&own_key)?.is_none(),
1493 "the join stored nothing"
1494 );
1495 }
1496
1497 #[xmtp_common::test(unwrap_try = true)]
1502 async fn forged_dm_with_group_id_reads_its_builder() {
1503 use crate::utils::test::crafted_welcome::{CraftedWelcome, send_crafted_welcome_with};
1504 use xmtp_db::consent_record::{ConsentKey, ConsentState, ConsentType, StoredConsentRecord};
1505 use xmtp_mls_group_state::app_data::group_root::read_group_root;
1506
1507 tester!(alix, disable_workers);
1508 tester!(bo2, disable_workers);
1509 let group = alix.create_group(None, None)?;
1510 let root = read_group_root(group.load_group_context()?.extensions())?
1511 .expect("a new group carries its root");
1512 let g = ConsentKey::group(group.group_id.as_ref());
1514 StoredConsentRecord::new(
1515 ConsentType::ConversationId,
1516 ConsentState::Allowed,
1517 g.entity.clone(),
1518 )
1519 .merge_into(&bo2.db())?;
1520 let options = CraftedWelcome {
1521 conversation_type: ConversationType::Dm,
1522 ..Default::default()
1523 };
1524 send_crafted_welcome_with(&alix, &bo2, group.group_id.as_slice(), Some(&root), options)
1525 .await;
1526 let dm = process_newest_welcome(&bo2)
1527 .await?
1528 .expect("the forged DM joins");
1529 assert_eq!(dm.group_id, group.group_id);
1530 assert_eq!(dm.consent_state()?, ConsentState::Unknown);
1531 assert_eq!(
1532 bo2.db().consent_row(&g)?.map(|r| r.state),
1533 Some(ConsentState::Allowed)
1534 );
1535 assert!(
1536 bo2.db()
1537 .consent_row(&ConsentKey::inbox(alix.inbox_id()))?
1538 .is_none()
1539 );
1540 assert!(!xmtp_db::sync_outbox::has_owed(&bo2.db())?);
1541 }
1542
1543 #[rstest::rstest]
1545 #[case::late_active_prefix(false)]
1546 #[case::live_retired_controller(true)]
1547 #[xmtp_common::test(unwrap_try = true)]
1548 async fn rejoin_keeps_messages_before_removal(#[case] live_receiver: bool) {
1549 use crate::subscriptions::incoming::{
1550 IncomingCoordinator, IncomingRegistration, IncomingScope,
1551 };
1552 use xmtp_common::time::{Duration, timeout};
1553
1554 tester!(alix, disable_workers);
1555 tester!(bo, disable_workers);
1556 let alix_group = alix.create_group(None, None).unwrap();
1557 alix_group.invite(&bo).await.unwrap();
1558 let bo_group = bo.sync_welcomes().await.unwrap().pop().unwrap();
1559 let lease = live_receiver.then(|| {
1560 IncomingCoordinator::for_context(&bo.context).acquire(IncomingScope::AllGroups)
1561 });
1562 let topic = xmtp_proto::types::Topic::new_group_message(bo_group.group_id);
1563 alix_group.send_msg(b"before removal").await;
1564 alix_group.remove_members(&[bo.inbox_id()]).await.unwrap();
1565 if let Some(lease) = &lease {
1566 timeout(Duration::from_secs(10), async {
1567 loop {
1568 if lease.snapshot().topics.iter().any(|entry| {
1569 entry.topic == topic && entry.registration == IncomingRegistration::Removed
1570 }) {
1571 break;
1572 }
1573 lease.changed().await;
1574 }
1575 })
1576 .await
1577 .unwrap();
1578 }
1579 alix_group.invite(&bo).await.unwrap();
1580
1581 bo.sync_welcomes().await.unwrap();
1582 let messages = bo
1583 .context
1584 .db()
1585 .get_group_messages(&bo_group.group_id, &Default::default())
1586 .unwrap();
1587 assert!(
1588 messages
1589 .iter()
1590 .any(|message| message.decrypted_message_bytes == b"before removal")
1591 );
1592 assert_eq!(
1593 alix_group.epoch_authenticator().await.unwrap(),
1594 bo_group.epoch_authenticator().await.unwrap()
1595 );
1596 if let Some(lease) = &lease {
1597 alix_group.send_msg(b"after rejoin").await;
1598 timeout(Duration::from_secs(10), async {
1599 loop {
1600 let messages = bo
1601 .context
1602 .db()
1603 .get_group_messages(&bo_group.group_id, &Default::default())
1604 .unwrap();
1605 if messages
1606 .iter()
1607 .any(|message| message.decrypted_message_bytes == b"after rejoin")
1608 {
1609 return Ok::<_, xmtp_db::StorageError>(());
1610 }
1611 lease.changed().await;
1612 }
1613 })
1614 .await
1615 .unwrap()
1616 .unwrap();
1617 }
1618 alix_group.test_can_talk_with(&bo_group).await.unwrap();
1619 }
1620}