1use crate::context::XmtpSharedContext;
10use crate::groups::InitialMembershipValidator;
11#[cfg(test)]
12use crate::groups::ValidateGroupMembership;
13use crate::groups::XmtpWelcome;
14use crate::groups::mls_ext::ResolvedWelcome;
15use crate::groups::{GroupError, MlsGroup};
16use crate::identity_updates::{
17 IdentityDependencyError, IdentityRequirement, InstallationDiffError,
18 resolve_identity_requirements,
19};
20#[cfg(test)]
21use crate::intents::ProcessIntentError;
22#[cfg(test)]
23use crate::mls_store::MlsStore;
24use futures::stream::{self, StreamExt};
25use prost::Message;
26use std::collections::{BTreeSet, HashSet};
27#[cfg(test)]
28use xmtp_common::Event;
29use xmtp_common::RetryableError;
30use xmtp_configuration::WELCOME_RETENTION_NS;
31use xmtp_db::incoming_envelope::{
32 IncomingRetry, NetworkEntityKind, StoredIncomingEnvelope, StreamTopic,
33};
34#[cfg(test)]
35use xmtp_db::refresh_state::EntityKind;
36use xmtp_db::{
37 consent_record::ConsentState,
38 group::{GroupMembershipState, GroupScope, StoredGroupRef},
39 prelude::*,
40};
41#[cfg(test)]
42use xmtp_macro::log_event;
43use xmtp_proto::types::Topic;
44use xmtp_proto::types::{Cursor, GroupId};
45
46const UNSUPPORTED_WELCOME_RETENTION_NS: i64 = xmtp_common::NS_IN_DAY;
51
52pub(crate) enum WelcomeRequirement {
54 Identity(BTreeSet<IdentityRequirement>),
56 Pointee,
58 GroupPrefix {
60 group_id: GroupId,
62 anchor: Cursor,
64 deadline_ns: i64,
67 },
68}
69
70pub(crate) enum WelcomeHeadOutcome<C> {
72 Idle { cursor: Cursor },
74 Waiting {
76 cursor: Cursor,
77 code: String,
78 blocked: bool,
80 },
81 Need {
83 cursor: Cursor,
84 requirement: WelcomeRequirement,
85 },
86 Progress {
88 cursor: Cursor,
89 result: Result<Option<MlsGroup<C>>, GroupError>,
90 },
91}
92
93fn unsupported_welcome_wire(wire: &xmtp_proto::backend_v1::ServerEnvelope) -> bool {
94 use xmtp_proto::backend_v1::{client_envelope::Payload, welcome_message::Version};
95 use xmtp_proto::xmtp::mls::message_contents::{
96 WelcomePointerWrapperAlgorithm, WelcomeWrapperAlgorithm,
97 };
98 match wire
99 .envelope
100 .as_ref()
101 .and_then(|envelope| envelope.payload.as_ref())
102 {
103 Some(Payload::WelcomeMessage(welcome)) => match &welcome.version {
104 None => true,
105 Some(Version::V1(message)) => {
106 WelcomeWrapperAlgorithm::try_from(message.wrapper_algorithm).is_err()
107 }
108 Some(Version::WelcomePointer(message)) => {
109 WelcomePointerWrapperAlgorithm::try_from(message.wrapper_algorithm).is_err()
110 }
111 },
112 _ => false,
113 }
114}
115
116fn welcome_timestamp_ns(pending: &StoredIncomingEnvelope) -> Result<i64, GroupError> {
118 let wire = xmtp_proto::backend_v1::ServerEnvelope::decode(pending.envelope.as_slice())
119 .map_err(|_| xmtp_db::StorageError::DbDeserialize)?;
120 let welcome = xmtp_api_backend::envelope::decode_welcome_message(wire)
121 .map_err(xmtp_api::ApiError::from)?;
122 Ok(welcome.timestamp())
123}
124
125#[derive(Debug, Clone)]
126pub struct GroupSyncSummary {
128 pub num_eligible: usize,
130 pub num_synced: usize,
132}
133
134impl GroupSyncSummary {
135 pub fn new(num_eligible: usize, num_synced: usize) -> Self {
136 Self {
137 num_eligible,
138 num_synced,
139 }
140 }
141}
142
143#[cfg(test)]
147enum WelcomeOutcome<Context> {
148 Processed(Option<MlsGroup<Context>>),
149 AlreadyProcessed(Cursor),
150}
151
152#[derive(Clone)]
153pub struct WelcomeService<Context> {
155 context: Context,
156}
157
158impl<Context> WelcomeService<Context> {
159 pub fn new(context: Context) -> Self {
160 Self { context }
161 }
162}
163
164impl<Context> WelcomeService<Context>
165where
166 Context: XmtpSharedContext,
167{
168 #[cfg(test)]
173 pub(crate) async fn process_new_welcome(
174 &self,
175 welcome: &xmtp_proto::types::WelcomeMessage,
176 validator: impl ValidateGroupMembership,
177 ) -> Result<Option<MlsGroup<Context>>, GroupError> {
178 match self.process_new_welcome_spanned(welcome, validator).await? {
179 WelcomeOutcome::Processed(group) => Ok(group),
180 WelcomeOutcome::AlreadyProcessed(cursor) => Err(GroupError::ProcessIntent(
181 ProcessIntentError::WelcomeAlreadyProcessed(cursor),
182 )),
183 }
184 }
185
186 #[cfg(test)]
187 #[tracing::instrument(err, skip_all, fields(operation = "mls.process_new_welcome"))]
188 async fn process_new_welcome_spanned(
189 &self,
190 welcome: &xmtp_proto::types::WelcomeMessage,
191 validator: impl ValidateGroupMembership,
192 ) -> Result<WelcomeOutcome<Context>, GroupError> {
193 let pending = self
194 .context
195 .db()
196 .pending_envelope(&self.topic(), welcome.cursor)?
197 .ok_or(ProcessIntentError::WelcomeAlreadyProcessed(welcome.cursor))?;
198 let result = XmtpWelcome::builder()
199 .context(self.context.clone())
200 .welcome(welcome)
201 .pending(pending)
202 .validator(validator)
203 .process()
204 .await;
205
206 match result {
207 Ok(mls_group) => {
208 if let Some(mls_group) = &mls_group {
209 if let (Ok(epoch), Ok(auth)) = (
210 mls_group.epoch().await,
211 mls_group.epoch_authenticator().await,
212 ) {
213 log_event!(
214 Event::ReceivedWelcome,
215 self.context.installation_id(),
216 group_id = mls_group.group_id.as_slice(),
217 conversation_type = %mls_group.conversation_type,
218 epoch,
219 epoch_auth = auth
220 );
221 } else {
222 tracing::warn!(
223 "Failed to lock the mls group for logging ProcessedWelcome."
224 );
225 }
226 }
227
228 Ok(WelcomeOutcome::Processed(mls_group))
229 }
230 Err(err) => {
231 use crate::DuplicateItem::*;
232 use crate::StorageError::*;
233
234 if matches!(err, GroupError::Storage(Duplicate(WelcomeId(_)))) {
235 tracing::warn!(
236 welcome_id = %welcome.cursor,
237 "Welcome ID already stored: {}",
238 err
239 );
240 return Ok(WelcomeOutcome::AlreadyProcessed(welcome.cursor));
241 } else if let GroupError::ProcessIntent(
242 ProcessIntentError::WelcomeAlreadyProcessed(cursor),
243 ) = err
244 {
245 tracing::warn!(
251 welcome_id = %welcome.cursor,
252 "welcome already processed, skipping: {}",
253 err
254 );
255 return Ok(WelcomeOutcome::AlreadyProcessed(cursor));
256 } else {
257 tracing::error!(
258 "failed to create group from welcome={} created at {}: {}",
259 welcome.cursor,
260 welcome.created_ns.timestamp(),
261 err
262 );
263 }
264
265 Err(err)
266 }
267 }
268 }
269
270 fn topic(&self) -> StreamTopic {
271 StreamTopic {
272 entity_id: self.context.installation_id().to_vec(),
273 kind: NetworkEntityKind::Welcome,
274 }
275 }
276
277 pub(crate) fn process_pending_welcomes_once(
280 &self,
281 excluded: &[Cursor],
282 ) -> Result<Vec<WelcomeHeadOutcome<Context>>, GroupError> {
283 let settings = self.context.incoming_runtime().policy();
284 self.context
285 .db()
286 .ready_welcomes_bounded(
287 xmtp_common::time::now_ns(),
288 settings.max_fetched_rows,
289 settings.max_fetched_bytes,
290 excluded,
291 )?
292 .into_iter()
293 .filter(|pending| pending.entity_id == self.context.installation_id())
294 .map(|pending| self.attempt_pending_welcome(pending, None, None))
295 .collect()
296 }
297
298 pub(crate) fn retry_pending_welcome(
301 &self,
302 cursor: Cursor,
303 missing_reference: Option<&IdentityRequirement>,
304 ) -> Result<WelcomeHeadOutcome<Context>, GroupError> {
305 let Some(pending) = self.context.db().pending_envelope(&self.topic(), cursor)? else {
306 return Ok(WelcomeHeadOutcome::Idle { cursor });
307 };
308 self.attempt_pending_welcome(pending, None, missing_reference)
309 }
310
311 pub(crate) fn retry_blocked_welcomes_after(
315 &self,
316 after: Cursor,
317 ) -> Result<Vec<WelcomeHeadOutcome<Context>>, GroupError> {
318 let settings = self.context.incoming_runtime().policy();
319 self.context
320 .db()
321 .blocked_welcomes_bounded(
322 &self.topic(),
323 after,
324 settings.max_fetched_rows,
325 settings.max_fetched_bytes,
326 )?
327 .into_iter()
328 .map(|pending| self.attempt_pending_welcome(pending, None, None))
329 .collect()
330 }
331
332 fn attempt_pending_welcome(
333 &self,
334 pending: StoredIncomingEnvelope,
335 resolved: Option<&ResolvedWelcome>,
336 missing_reference: Option<&IdentityRequirement>,
337 ) -> Result<WelcomeHeadOutcome<Context>, GroupError> {
338 let cursor = Cursor(pending.sequence_id as u64);
339 let wire = xmtp_proto::backend_v1::ServerEnvelope::decode(pending.envelope.as_slice())
340 .map_err(|_| xmtp_db::StorageError::DbDeserialize)?;
341 if unsupported_welcome_wire(&wire) {
342 return self.retain_unreadable_welcome(
343 &pending,
344 GroupError::UnsupportedWelcomeVersion("unsupported wrapper algorithm".into()),
345 "unsupported_welcome",
346 );
347 }
348 let welcome = match xmtp_api_backend::envelope::decode_welcome_message(wire) {
349 Ok(welcome) => welcome,
350 Err(error) => {
351 let error = GroupError::WrappedApi(xmtp_api::ApiError::Envelope(error));
352 self.complete_rejected_pending(&pending, "invalid_welcome_envelope")?;
353 return Ok(WelcomeHeadOutcome::Progress {
354 cursor,
355 result: Err(error),
356 });
357 }
358 };
359 if let Some(deadline) = pending.retry_expires_at_ns
362 && deadline <= xmtp_common::time::now_ns()
363 && pending.error_code.as_deref() == Some("welcome_pointee")
364 && pending.unreadable_expires_at_ns.is_none()
365 {
366 self.complete_rejected_pending(&pending, "welcome_pointer_expired")?;
367 return Ok(WelcomeHeadOutcome::Progress {
368 cursor,
369 result: Err(GroupError::WelcomeDataNotFound("expired pointer".into())),
370 });
371 }
372 let inline = ResolvedWelcome::inline(&welcome);
373 let Some(resolved) = resolved.or(inline.as_ref()) else {
374 return Ok(WelcomeHeadOutcome::Need {
377 cursor,
378 requirement: WelcomeRequirement::Pointee,
379 });
380 };
381 let result = XmtpWelcome::builder()
382 .context(self.context.clone())
383 .welcome(&welcome)
384 .pending(pending.clone())
385 .validator(InitialMembershipValidator::new(&self.context))
386 .process_resolved(resolved, missing_reference);
387 self.classify_pending_result(&pending, result)
388 }
389
390 fn classify_pending_result(
392 &self,
393 pending: &StoredIncomingEnvelope,
394 result: Result<Option<MlsGroup<Context>>, GroupError>,
395 ) -> Result<WelcomeHeadOutcome<Context>, GroupError> {
396 let cursor = Cursor(pending.sequence_id as u64);
397 match result {
398 Ok(group) => Ok(WelcomeHeadOutcome::Progress {
399 cursor,
400 result: Ok(group),
401 }),
402 Err(GroupError::InstallationDiff(InstallationDiffError::IdentityDependency(
403 IdentityDependencyError::Need(requirements),
404 ))) => {
405 self.defer_pending(pending, "identity_dependency", false, None, None)?;
406 Ok(WelcomeHeadOutcome::Need {
407 cursor,
408 requirement: WelcomeRequirement::Identity(requirements),
409 })
410 }
411 Err(GroupError::WelcomeGroupPrefixPending { group_id, anchor }) => {
416 let deadline_ns = match pending.retry_expires_at_ns {
417 Some(deadline) => deadline,
418 None => welcome_timestamp_ns(pending)?.saturating_add(WELCOME_RETENTION_NS),
419 };
420 self.defer_pending(pending, "group_prefix", false, Some(deadline_ns), None)?;
421 Ok(WelcomeHeadOutcome::Need {
422 cursor,
423 requirement: WelcomeRequirement::GroupPrefix {
424 group_id,
425 anchor: Cursor(anchor),
426 deadline_ns,
427 },
428 })
429 }
430 Err(
431 error @ (GroupError::WelcomeError(
432 openmls::prelude::WelcomeError::UnsupportedMlsVersion
433 | openmls::prelude::WelcomeError::UnsupportedExtensions
434 | openmls::prelude::WelcomeError::UnsupportedCapability
435 | openmls::prelude::WelcomeError::UnsupportedCiphersuite(_),
436 )
437 | GroupError::NotFound(
438 xmtp_db::NotFound::KeyPackageReference(_)
439 | xmtp_db::NotFound::KeyPackage(_)
440 | xmtp_db::NotFound::PostQuantumPrivateKey,
441 )),
442 ) => self.retain_unreadable_welcome(pending, error, "welcome_blocked"),
443 Err(error) => {
444 if crate::groups::welcomes::terminal_welcome_error(&error) {
445 self.complete_rejected_pending(pending, "invalid_welcome")?;
446 }
447 if self
448 .context
449 .db()
450 .pending_envelope(&self.topic(), cursor)?
451 .is_none()
452 {
453 return Ok(WelcomeHeadOutcome::Progress {
454 cursor,
455 result: Err(error),
456 });
457 }
458 let blocked = !error.is_retryable()
459 || matches!(error, GroupError::UnsupportedWelcomeVersion(_));
460 let code = if blocked {
461 "welcome_blocked"
462 } else {
463 "welcome_retry"
464 };
465 self.defer_pending(pending, code, blocked, None, error.rate_limit_delay())?;
466 tracing::warn!(sequence_id = cursor.0, code, error = %error, "Welcome remains pending");
467 Ok(WelcomeHeadOutcome::Waiting {
468 cursor,
469 code: code.into(),
470 blocked,
471 })
472 }
473 }
474 }
475
476 fn retain_unreadable_welcome(
479 &self,
480 pending: &StoredIncomingEnvelope,
481 error: GroupError,
482 code: &'static str,
483 ) -> Result<WelcomeHeadOutcome<Context>, GroupError> {
484 let cursor = Cursor(pending.sequence_id as u64);
485 let now = xmtp_common::time::now_ns();
486 let Some(deadline) = self.context.db().set_unreadable_welcome_deadline(
487 self.context.installation_id().as_slice(),
488 cursor,
489 now.saturating_add(UNSUPPORTED_WELCOME_RETENTION_NS),
490 )?
491 else {
492 return Ok(WelcomeHeadOutcome::Idle { cursor });
493 };
494 if deadline <= now {
495 self.complete_rejected_pending(pending, "unsupported_welcome_expired")?;
496 return Ok(WelcomeHeadOutcome::Progress {
497 cursor,
498 result: Err(error),
499 });
500 }
501 self.defer_pending(pending, code, true, None, None)?;
502 Ok(WelcomeHeadOutcome::Waiting {
503 cursor,
504 code: code.into(),
505 blocked: true,
506 })
507 }
508
509 fn defer_pending(
515 &self,
516 pending: &StoredIncomingEnvelope,
517 code: &'static str,
518 blocked: bool,
519 deadline: Option<i64>,
520 floor: Option<xmtp_common::time::Duration>,
521 ) -> Result<(), GroupError> {
522 let policy = self.context.incoming_runtime().policy();
523 let delay = policy
524 .active_database_poll_interval
525 .max(floor.unwrap_or_default());
526 self.context.db().set_incoming_retry(
527 &self.topic(),
528 Cursor(pending.sequence_id as u64),
529 &IncomingRetry {
530 retry_at_ns: xmtp_common::time::now_ns()
531 .saturating_add(i64::try_from(delay.as_nanos()).unwrap_or(i64::MAX))
532 .min(deadline.unwrap_or(i64::MAX)),
533 blocked,
534 error_code: Some(code.into()),
535 retry_expires_at_ns: deadline,
536 },
537 )?;
538 if blocked || deadline.is_some() || pending.unreadable_expires_at_ns.is_some() {
539 let allowance = policy.incoming_limits(NetworkEntityKind::Welcome).kind;
542 while let Some(oldest) = self
543 .context
544 .db()
545 .oldest_held_welcome_over(&self.topic(), allowance)?
546 {
547 self.complete_rejected_pending(&oldest, "held_capacity")?;
548 }
549 }
550 #[cfg(any(test, feature = "diagnostics"))]
551 crate::groups::diagnostics::record_welcome_retry(code);
552 Ok(())
553 }
554
555 fn complete_rejected_pending(
557 &self,
558 pending: &StoredIncomingEnvelope,
559 code: &'static str,
560 ) -> Result<(), GroupError> {
561 crate::state_tx::state_write(self.context.mls_storage(), |tx| {
562 let storage = tx.storage();
563 let db = storage.db();
564 let cursor = Cursor(pending.sequence_id as u64);
565 if db
566 .pending_envelope(&self.topic(), cursor)?
567 .is_some_and(|current| current.envelope == pending.envelope)
568 {
569 db.record_terminal_rejection(&self.topic(), cursor, code)?;
570 db.complete_pending_envelope(&self.topic(), cursor)?;
571 }
572 Ok::<_, GroupError>(xmtp_db::TransactionOutcome::Continue(()))
573 })?;
574 Ok(())
575 }
576
577 pub(crate) fn expire_group_prefix_welcome(
584 &self,
585 cursor: Cursor,
586 group_id: GroupId,
587 anchor: Cursor,
588 target: Cursor,
589 ) -> Result<WelcomeHeadOutcome<Context>, GroupError> {
590 enum Expiry {
591 Gone,
592 Kept { code: String, blocked: bool },
593 Rejected,
594 }
595 let now = xmtp_common::time::now_ns();
596 let expiry = crate::state_tx::state_write(self.context.mls_storage(), |tx| {
597 let storage = tx.storage();
598 let db = storage.db();
599 let Some(pending) = db.pending_envelope(&self.topic(), cursor)? else {
600 return Ok::<_, GroupError>(xmtp_db::TransactionOutcome::Continue(Expiry::Gone));
601 };
602 let processed = db.topic_progress(&StreamTopic::group(group_id))?.processed;
603 let expired = pending.error_code.as_deref() == Some("group_prefix")
604 && pending
605 .retry_expires_at_ns
606 .is_some_and(|deadline| deadline <= now)
607 && target <= processed
608 && processed < anchor
609 && openmls::group::MlsGroup::load(&storage, &group_id.to_openmls())?
612 .is_some_and(|group| group.is_active());
613 if !expired {
614 return Ok(xmtp_db::TransactionOutcome::Continue(Expiry::Kept {
615 code: pending.error_code.unwrap_or_default(),
616 blocked: pending.blocked,
617 }));
618 }
619 db.record_terminal_rejection(&self.topic(), cursor, "group_prefix_expired")?;
620 db.complete_pending_envelope(&self.topic(), cursor)?;
621 Ok(xmtp_db::TransactionOutcome::Continue(Expiry::Rejected))
622 })?
623 .into_continued();
624 Ok(match expiry {
625 Expiry::Gone => WelcomeHeadOutcome::Idle { cursor },
626 Expiry::Kept { code, blocked } => WelcomeHeadOutcome::Waiting {
627 cursor,
628 code,
629 blocked,
630 },
631 Expiry::Rejected => WelcomeHeadOutcome::Progress {
632 cursor,
633 result: Err(GroupError::WelcomeGroupPrefixPending {
634 group_id,
635 anchor: anchor.0,
636 }),
637 },
638 })
639 }
640
641 pub(crate) async fn resolve_pending_welcome(
644 &self,
645 cursor: Cursor,
646 ) -> Result<WelcomeHeadOutcome<Context>, GroupError> {
647 let Some(pending) = self.context.db().pending_envelope(&self.topic(), cursor)? else {
648 return Ok(WelcomeHeadOutcome::Idle { cursor });
649 };
650 if pending.error_code.as_deref() == Some("welcome_pointee")
651 && pending.unreadable_expires_at_ns.is_none()
652 && pending
653 .retry_expires_at_ns
654 .is_some_and(|deadline| deadline <= xmtp_common::time::now_ns())
655 {
656 return self.attempt_pending_welcome(pending, None, None);
657 }
658 let wire = xmtp_proto::backend_v1::ServerEnvelope::decode(pending.envelope.as_slice())
659 .map_err(|_| xmtp_db::StorageError::DbDeserialize)?;
660 let welcome = xmtp_api_backend::envelope::decode_welcome_message(wire)
661 .map_err(xmtp_api::ApiError::from)?;
662 let resolved = match ResolvedWelcome::resolve(&welcome, &self.context).await {
663 Ok(resolved) => resolved,
664 Err(error @ (GroupError::WelcomeDataNotFound(_) | GroupError::WrappedApi(_))) => {
668 if pending.unreadable_expires_at_ns.is_some() {
671 return self.retain_unreadable_welcome(&pending, error, "welcome_blocked");
672 }
673 let deadline = pending
674 .retry_expires_at_ns
675 .unwrap_or_else(|| welcome.timestamp().saturating_add(WELCOME_RETENTION_NS));
676 if deadline <= xmtp_common::time::now_ns() {
679 self.complete_rejected_pending(&pending, "welcome_pointer_expired")?;
680 return Ok(WelcomeHeadOutcome::Progress {
681 cursor,
682 result: Err(error),
683 });
684 }
685 let blocked = pending.error_code.as_deref() == Some("welcome_blocked")
688 || matches!(&error, GroupError::WrappedApi(api) if !api.is_retryable());
689 let code = if blocked {
690 "welcome_blocked"
691 } else {
692 "welcome_pointee"
693 };
694 self.defer_pending(
695 &pending,
696 code,
697 blocked,
698 Some(deadline),
699 error.rate_limit_delay(),
700 )?;
701 return Ok(WelcomeHeadOutcome::Waiting {
702 cursor,
703 code: code.into(),
704 blocked,
705 });
706 }
707 Err(error) => return self.classify_pending_result(&pending, Err(error)),
708 };
709 loop {
710 let outcome = self.attempt_pending_welcome(pending.clone(), Some(&resolved), None)?;
711 let WelcomeHeadOutcome::Need {
712 requirement: WelcomeRequirement::Identity(requirements),
713 ..
714 } = outcome
715 else {
716 return Ok(outcome);
717 };
718 match resolve_identity_requirements(&self.context, &requirements).await {
719 Ok(()) => {}
720 Err(IdentityDependencyError::MissingReference(requirement)) => {
721 return self.attempt_pending_welcome(
722 pending,
723 Some(&resolved),
724 Some(&requirement),
725 );
726 }
727 Err(error) => {
728 return self.classify_pending_result(
729 &pending,
730 Err(InstallationDiffError::from(error).into()),
731 );
732 }
733 }
734 }
735 }
736
737 pub async fn sync_welcomes(&self) -> Result<Vec<MlsGroup<Context>>, GroupError> {
739 let scope = GroupScope::listing(None);
740 let before: HashSet<_> = self
741 .context
742 .db()
743 .stored_group_refs(&scope)?
744 .into_iter()
745 .map(|group| group.id)
746 .collect();
747 crate::subscriptions::barrier::receive_through_current(
748 &self.context,
749 vec![Topic::new_welcome_message(self.context.installation_id())],
750 )
751 .await?;
752 Ok(self
753 .context
754 .db()
755 .stored_group_refs(&scope)?
756 .into_iter()
757 .filter(|group| !before.contains(&group.id))
758 .map(|group| self.mls_group(group))
759 .collect())
760 }
761
762 fn mls_group(&self, group: StoredGroupRef) -> MlsGroup<Context> {
763 MlsGroup::new(
764 self.context.clone(),
765 group.id,
766 group.dm_id,
767 group.conversation_type,
768 group.created_at_ns,
769 )
770 }
771
772 pub async fn sync_all_groups(
775 &self,
776 groups: Vec<MlsGroup<Context>>,
777 ) -> Result<GroupSyncSummary, GroupError> {
778 use crate::subscriptions::incoming::{IncomingCoordinator, IncomingScope};
779 use xmtp_common::time::Instant;
780
781 let deadline = Instant::now() + self.context.incoming_runtime().policy().barrier_timeout;
782 let mut selected = HashSet::new();
783 let selected: Vec<_> = groups
784 .into_iter()
785 .filter(|group| selected.insert(group.group_id))
786 .collect();
787 let db = self.context.db();
788 let num_eligible = selected.len();
789 let mut groups = Vec::new();
790 let mut restored_count = 0;
791 for group in selected {
792 if db
793 .find_group(&group.group_id)?
794 .is_some_and(|stored| stored.membership_state == GroupMembershipState::Restored)
795 {
796 restored_count += 1;
797 } else {
798 groups.push(group);
799 }
800 }
801 if groups.is_empty() && restored_count > 0 {
802 return Err(GroupError::GroupInactive);
803 }
804 let topics = group_sync_topics(&groups);
805 let _receipt = IncomingCoordinator::for_context(&self.context)
807 .acquire(IncomingScope::Topics(topics.clone()));
808 let concurrency = self
809 .context
810 .incoming_runtime()
811 .policy()
812 .max_dependency_requests;
813 let mut summary = super::summary::SyncSummary::default();
814 if restored_count > 0 {
815 add_group_sync_error(&mut summary, GroupError::GroupInactive);
816 }
817 let publish =
818 run_group_sync_work(&groups, GroupSyncWork::Publish, concurrency, deadline).await;
819 for error in publish.into_iter().filter_map(Result::err) {
820 summary.add_publish_err(error);
821 }
822 let received = crate::subscriptions::barrier::receive_through_current_until(
823 &self.context,
824 topics,
825 deadline,
826 )
827 .await;
828 let unfinished = unfinished_sync_topics(received.as_ref().err());
829 if let Err(error) = received {
830 add_group_sync_error(&mut summary, error.into());
831 }
832 let post_commit = run_group_sync_work(
833 &groups,
834 GroupSyncWork::PostCommit(&unfinished),
835 concurrency,
836 deadline,
837 )
838 .await;
839 finish_group_sync(summary, num_eligible, post_commit)
840 }
841
842 pub async fn unstick_paused_groups(&self) -> Result<usize, GroupError> {
858 use xmtp_mls_group_state::libxmtp_version::LibXMTPVersion;
859
860 let paused = self.context.db().get_paused_groups_with_versions()?;
861 if paused.is_empty() {
862 return Ok(0);
863 }
864 let own_version_str = self.context.version_info().pkg_version().to_string();
867 let own_v = self.context.version_info().pkg_semver();
868
869 let mut unstuck = 0usize;
870 for (group_id, required_str) in paused {
871 let Ok(required_v) = LibXMTPVersion::parse(&required_str) else {
875 tracing::warn!(
876 group_id = hex::encode(group_id.as_ref()),
877 required = %required_str,
878 "skipping unparseable paused_for_version while sweeping"
879 );
880 continue;
881 };
882 if required_v <= *own_v {
883 if let Err(err) = self.context.db().unpause_group(&group_id) {
888 tracing::warn!(
889 group_id = hex::encode(group_id.as_ref()),
890 required = %required_str,
891 error = %err,
892 "failed to unpause group during sweep; will retry on next sync"
893 );
894 continue;
895 }
896 tracing::debug!(
897 group_id = hex::encode(group_id.as_ref()),
898 required = %required_str,
899 own = %own_version_str,
900 "unstuck previously paused group: client version now satisfies floor"
901 );
902 unstuck += 1;
903 }
904 }
905 Ok(unstuck)
906 }
907
908 pub async fn sync_all_welcomes_and_groups(
911 &self,
912 consent_states: Option<Vec<ConsentState>>,
913 ) -> Result<GroupSyncSummary, GroupError> {
914 use crate::subscriptions::incoming::{IncomingCoordinator, IncomingScope};
915 use xmtp_common::time::Instant;
916
917 let deadline = Instant::now() + self.context.incoming_runtime().policy().barrier_timeout;
918 let groups: Vec<_> = self
920 .context
921 .db()
922 .stored_group_refs(&GroupScope::listing(consent_states.clone()).without_restored())?
923 .into_iter()
924 .map(|group| self.mls_group(group))
925 .collect();
926 let initial_ids = groups.iter().map(|group| group.group_id).collect();
927 let mut topics = group_sync_topics(&groups);
928 topics.push(Topic::new_welcome_message(self.context.installation_id()));
929 let _receipt =
930 IncomingCoordinator::for_context(&self.context).acquire(IncomingScope::Topics(topics));
931 let concurrency = self
932 .context
933 .incoming_runtime()
934 .policy()
935 .max_dependency_requests;
936 let mut summary = super::summary::SyncSummary::default();
937 if let Err(error) = self.unstick_paused_groups().await {
938 add_group_sync_error(&mut summary, error);
939 }
940 let publish =
941 run_group_sync_work(&groups, GroupSyncWork::Publish, concurrency, deadline).await;
942 for error in publish.into_iter().filter_map(Result::err) {
943 summary.add_publish_err(error);
944 }
945 let received = crate::subscriptions::barrier::receive_with_welcomes_until(
946 &self.context,
947 initial_ids,
948 consent_states,
949 deadline,
950 )
951 .await;
952 let unfinished = unfinished_sync_topics(received.result.as_ref().err());
953 if let Err(error) = received.result {
954 add_group_sync_error(&mut summary, error.into());
955 }
956 let mut enrolled: std::collections::HashMap<_, _> = groups
957 .into_iter()
958 .map(|group| (group.group_id, group))
959 .collect();
960 let mut enrolled_ids: HashSet<_> = enrolled.keys().copied().collect();
961 for group_id in received.group_ids {
962 if !enrolled_ids.insert(group_id) {
963 continue;
964 }
965 match MlsGroup::new_cached(self.context.clone(), &group_id) {
966 Ok((group, _)) => {
967 enrolled.insert(group_id, group);
968 }
969 Err(error) => add_group_sync_error(&mut summary, error.into()),
970 }
971 }
972 let num_eligible = enrolled_ids.len();
973 let groups: Vec<_> = enrolled.into_values().collect();
974 let post_commit = run_group_sync_work(
975 &groups,
976 GroupSyncWork::PostCommit(&unfinished),
977 concurrency,
978 deadline,
979 )
980 .await;
981 finish_group_sync(summary, num_eligible, post_commit)
982 }
983}
984
985#[derive(Clone, Copy)]
986enum GroupSyncWork<'a> {
987 Publish,
988 PostCommit(&'a HashSet<Topic>),
989}
990
991fn unfinished_sync_topics(
992 error: Option<&crate::subscriptions::barrier::BarrierError>,
993) -> HashSet<Topic> {
994 use crate::subscriptions::barrier::BarrierError;
995 let Some(BarrierError::Incomplete { unfinished, .. }) = error else {
996 return HashSet::new();
997 };
998 unfinished
999 .iter()
1000 .map(|status| status.topic.clone())
1001 .collect()
1002}
1003
1004fn group_sync_topics<Context: XmtpSharedContext>(groups: &[MlsGroup<Context>]) -> Vec<Topic> {
1005 groups
1006 .iter()
1007 .map(|group| Topic::new_group_message(group.group_id))
1008 .collect()
1009}
1010
1011async fn run_group_sync_work<Context: XmtpSharedContext>(
1013 groups: &[MlsGroup<Context>],
1014 work: GroupSyncWork<'_>,
1015 concurrency: usize,
1016 deadline: xmtp_common::time::Instant,
1017) -> Vec<Result<bool, GroupError>> {
1018 use xmtp_common::time::{Instant, timeout};
1019
1020 stream::iter(groups.iter().cloned())
1021 .map(|group| async move {
1022 let remaining = deadline.saturating_duration_since(Instant::now());
1023 let timed_out = || GroupError::SyncFailedToWait(Box::default());
1024 if remaining.is_zero() {
1025 return Err(timed_out());
1026 }
1027 let work = xmtp_common::retry::with_retry_deadline(deadline, async {
1030 match work {
1031 GroupSyncWork::Publish => {
1032 let active = group.is_active()?;
1033 if active {
1034 group.publish_intents().await?;
1035 }
1036 Ok(active)
1037 }
1038 GroupSyncWork::PostCommit(unfinished) => {
1039 group.post_commit().await?;
1040 if group.is_active()?
1041 && !unfinished.contains(&Topic::new_group_message(group.group_id))
1042 {
1043 group.maybe_update_installations(None).await?;
1046 }
1047 group.is_active()
1048 }
1049 }
1050 });
1051 match timeout(remaining, work).await {
1052 Ok(result) => result,
1053 Err(_) => Err(timed_out()),
1054 }
1055 })
1056 .buffer_unordered(concurrency)
1057 .collect()
1058 .await
1059}
1060
1061fn add_group_sync_error(summary: &mut super::summary::SyncSummary, error: GroupError) {
1062 let error = match summary.other.take() {
1063 Some(first) => combine_sync_errors(*first, error),
1064 None => error,
1065 };
1066 summary.add_other(error);
1067}
1068
1069fn finish_group_sync(
1070 mut summary: super::summary::SyncSummary,
1071 num_eligible: usize,
1072 post_commit: Vec<Result<bool, GroupError>>,
1073) -> Result<GroupSyncSummary, GroupError> {
1074 let mut num_synced = 0;
1075 for result in post_commit {
1076 match result {
1077 Ok(active) => num_synced += usize::from(active),
1078 Err(error) => summary.add_post_commit_err(error),
1079 }
1080 }
1081 if summary.is_errored() {
1082 Err(summary.into())
1083 } else {
1084 Ok(GroupSyncSummary::new(num_eligible, num_synced))
1085 }
1086}
1087
1088fn combine_sync_errors(first: GroupError, second: GroupError) -> GroupError {
1089 use crate::subscriptions::barrier::{BarrierError, BarrierFailure};
1090 match (first, second) {
1091 (
1092 GroupError::StreamBarrier(BarrierError::Incomplete {
1093 reason: a,
1094 mut unfinished,
1095 }),
1096 GroupError::StreamBarrier(BarrierError::Incomplete {
1097 reason: b,
1098 unfinished: next,
1099 }),
1100 ) => {
1101 unfinished.extend(next);
1102 let reason = if a == BarrierFailure::Cancelled || b == BarrierFailure::Cancelled {
1103 BarrierFailure::Cancelled
1104 } else if a == BarrierFailure::Deadline || b == BarrierFailure::Deadline {
1105 BarrierFailure::Deadline
1106 } else {
1107 BarrierFailure::Blocked
1108 };
1109 BarrierError::Incomplete { reason, unfinished }.into()
1110 }
1111 (first, second) => {
1112 let mut summary = super::summary::SyncSummary::other(first);
1113 summary.add_publish_err(second);
1114 GroupError::from(summary)
1115 }
1116 }
1117}
1118
1119#[cfg(test)]
1120pub(crate) async fn pending_welcome_for_test(
1121 context: &impl XmtpSharedContext,
1122 welcome: &xmtp_proto::types::WelcomeMessage,
1123) -> Result<StoredIncomingEnvelope, GroupError> {
1124 let topic = StreamTopic {
1125 entity_id: context.installation_id().to_vec(),
1126 kind: NetworkEntityKind::Welcome,
1127 };
1128 let store = MlsStore::new(context.clone());
1129 loop {
1130 if let Some(pending) = context.db().pending_envelope(&topic, welcome.cursor)? {
1131 return Ok(pending);
1132 }
1133 let page = store
1134 .receive_topics_once(
1135 &[Topic::new_welcome_message(context.installation_id())],
1136 context
1137 .incoming_runtime()
1138 .policy()
1139 .incoming_limits(NetworkEntityKind::Welcome),
1140 )
1141 .await?;
1142 if !page.has_more {
1143 return context
1144 .db()
1145 .pending_envelope(&topic, welcome.cursor)?
1146 .ok_or_else(|| ProcessIntentError::WelcomeAlreadyProcessed(welcome.cursor).into());
1147 }
1148 }
1149}
1150
1151#[cfg(test)]
1156pub(crate) struct HeldReadd<C> {
1157 pub(crate) alix_group: MlsGroup<C>,
1158 pub(crate) bo_group: MlsGroup<C>,
1159 pub(crate) welcome: xmtp_proto::types::WelcomeMessage,
1160 pub(crate) removal: xmtp_db::incoming_envelope::AdmissionResult,
1162}
1163
1164#[cfg(test)]
1165pub(crate) async fn held_readd<P: crate::groups::test_support::EnginePeer>(
1166 alix: &P,
1167 bo: &P,
1168) -> HeldReadd<P::Context> {
1169 use crate::context::CoreContext;
1170 let alix_group = alix.create_group(None, None).unwrap();
1171 alix_group
1172 .add_members(&[bo.context().inbox_id()])
1173 .await
1174 .unwrap();
1175 let bo_group = bo.sync_welcomes().await.unwrap().pop().unwrap();
1176 alix_group
1177 .remove_members(&[bo.context().inbox_id()])
1178 .await
1179 .unwrap();
1180 let page = MlsStore::new(bo.context().clone())
1181 .receive_topics_once(
1182 &[Topic::new_group_message(bo_group.group_id)],
1183 bo.context()
1184 .incoming_runtime()
1185 .policy()
1186 .incoming_limits(NetworkEntityKind::Group),
1187 )
1188 .await
1189 .unwrap();
1190 assert!(!page.has_more);
1191 let removal = page.admissions[0].1;
1192 assert!(removal.inserted > 0);
1193 alix_group
1194 .add_members(&[bo.context().inbox_id()])
1195 .await
1196 .unwrap();
1197 let welcome = bo
1198 .context()
1199 .api()
1200 .query_welcome_messages(bo.context().installation_id())
1201 .await
1202 .unwrap()
1203 .into_iter()
1204 .max_by_key(|welcome| welcome.cursor)
1205 .unwrap();
1206 pending_welcome_for_test(bo.context(), &welcome)
1207 .await
1208 .unwrap();
1209 HeldReadd {
1210 alix_group,
1211 bo_group,
1212 welcome,
1213 removal,
1214 }
1215}
1216
1217#[cfg(test)]
1218mod tests {
1219 use super::*;
1220 use crate::groups::WelcomeMembership;
1221 use crate::prelude::*;
1222 use crate::tester;
1223 use crate::utils::test::MlsGroupExt;
1224 use rstest::*;
1225
1226 struct RejectMembership {
1227 retryable: bool,
1228 }
1229
1230 #[derive(Clone, Copy)]
1231 enum UnreadableWelcome {
1232 Wrapper,
1233 MlsVersion,
1234 MissingKey,
1235 }
1236
1237 #[rstest]
1242 #[case::wrapper(UnreadableWelcome::Wrapper)]
1243 #[case::mls_version(UnreadableWelcome::MlsVersion)]
1244 #[case::missing_key(UnreadableWelcome::MissingKey)]
1245 #[xmtp_common::test(unwrap_try = true)]
1246 async fn an_unsupported_welcome_expires_after_a_final_attempt(
1247 #[case] failure: UnreadableWelcome,
1248 #[values(false, true)] previously_blocked: bool,
1249 ) {
1250 use crate::worker::key_package_maintenance::sweep_expired;
1251 use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
1252 use openmls_traits::storage::StorageProvider;
1253 use xmtp_db::ConnectionExt;
1254 use xmtp_db::incoming_envelope::{IncomingLimits, PendingBudget};
1255 use xmtp_db::schema::{incoming_envelopes, key_package_history};
1256 use xmtp_proto::backend_v1::{client_envelope::Payload, welcome_message::Version};
1257
1258 tester!(alix, disable_workers);
1259 tester!(bo, disable_workers);
1260 let group = alix.create_group(None, None).unwrap();
1261 group.invite(&bo).await.unwrap();
1262 let welcome = bo
1263 .context
1264 .api()
1265 .query_welcome_messages(bo.context.installation_id())
1266 .await
1267 .unwrap()
1268 .pop()
1269 .unwrap();
1270 let cursor = welcome.cursor;
1271 pending_welcome_for_test(&bo.context, &welcome)
1272 .await
1273 .unwrap();
1274 let service = WelcomeService::new(bo.context.clone());
1275 let db = bo.context.db();
1276
1277 let pending = db
1278 .pending_envelope(&service.topic(), cursor)
1279 .unwrap()
1280 .unwrap();
1281 let mut wire =
1282 xmtp_proto::backend_v1::ServerEnvelope::decode(pending.envelope.as_slice()).unwrap();
1283 let Payload::WelcomeMessage(message) =
1284 wire.envelope.as_mut().unwrap().payload.as_mut().unwrap()
1285 else {
1286 panic!("expected Welcome payload");
1287 };
1288 let Version::V1(message) = message.version.as_mut().unwrap() else {
1289 panic!("expected inline Welcome");
1290 };
1291 match failure {
1292 UnreadableWelcome::Wrapper => message.wrapper_algorithm = i32::MAX,
1293 UnreadableWelcome::MissingKey => message.hpke_public_key = vec![0; 32],
1294 UnreadableWelcome::MlsVersion => {
1295 (message.data, message.welcome_metadata) =
1296 xmtp_mls_group_keys::mls_ext::payload_encryption::wrap_payload_hpke(
1297 &[0, 2],
1298 &[],
1299 &message.hpke_public_key,
1300 xmtp_id::key_package::WrapperAlgorithm::try_from(message.wrapper_algorithm)
1301 .unwrap(),
1302 xmtp_configuration::WELCOME_HPKE_LABEL,
1303 )
1304 .unwrap();
1305 }
1306 }
1307 let row = || {
1308 incoming_envelopes::table.find((
1309 bo.context.installation_id().to_vec(),
1310 EntityKind::Welcome,
1311 cursor.0 as i64,
1312 ))
1313 };
1314 let envelope = wire.encode_to_vec();
1315 db.raw_query(|conn| {
1316 diesel::update(row())
1317 .set((
1318 incoming_envelopes::envelope_bytes.eq(envelope.len() as i64),
1319 incoming_envelopes::envelope.eq(&envelope),
1320 incoming_envelopes::blocked.eq(previously_blocked),
1321 incoming_envelopes::error_code
1322 .eq(previously_blocked.then_some("welcome_blocked")),
1323 ))
1324 .execute(conn)
1325 })
1326 .unwrap();
1327
1328 let before = xmtp_common::time::now_ns();
1330 if previously_blocked {
1331 service.retry_blocked_welcomes_after(Cursor(0)).unwrap();
1332 } else {
1333 service.process_pending_welcomes_once(&[]).unwrap();
1334 }
1335 let blocked = db
1336 .pending_envelope(&service.topic(), cursor)
1337 .unwrap()
1338 .unwrap();
1339 assert!(blocked.blocked);
1340 let deadline = blocked.unreadable_expires_at_ns.unwrap();
1341 assert!(deadline >= before + xmtp_common::NS_IN_DAY);
1342 assert!(deadline <= xmtp_common::time::now_ns() + xmtp_common::NS_IN_DAY);
1343 assert!(db.has_pending_welcomes().unwrap());
1344
1345 service.retry_blocked_welcomes_after(Cursor(0)).unwrap();
1347 let retained = db.pending_envelope(&service.topic(), cursor).unwrap();
1348 assert_eq!(
1349 retained.expect("retained").unreadable_expires_at_ns,
1350 Some(deadline),
1351 "a retry must not extend the first deadline"
1352 );
1353
1354 let history = db
1355 .find_key_package_history_entries_before_id(i32::MAX)
1356 .unwrap()
1357 .pop()
1358 .unwrap();
1359 let hash = crate::identity::deserialize_key_package_hash_ref(&history.key_package_hash_ref)
1360 .unwrap();
1361 db.raw_query(|conn| {
1362 diesel::update(key_package_history::table.find(history.id))
1363 .set(key_package_history::delete_at_ns.eq(0))
1364 .execute(conn)
1365 })
1366 .unwrap();
1367 sweep_expired(&bo.context).unwrap();
1368 let retained: Option<openmls::prelude::KeyPackageBundle> =
1369 bo.context.mls_storage().key_package(&hash).unwrap();
1370 assert!(retained.is_some());
1371
1372 let next_group = alix.create_group(None, None).unwrap();
1375 next_group.invite(&bo).await.unwrap();
1376 let budget = PendingBudget {
1377 rows: 1,
1378 bytes: u64::MAX,
1379 };
1380 let limits = IncomingLimits {
1381 batch: budget,
1382 topic: budget,
1383 kind: budget,
1384 };
1385 let store = MlsStore::new(bo.context.clone());
1386 let topics = [Topic::new_welcome_message(bo.context.installation_id())];
1387 store.receive_topics_once(&topics, limits).await.unwrap();
1388 assert!(db.topic_progress(&service.topic()).unwrap().received > cursor);
1389
1390 db.raw_query(|conn| {
1393 diesel::update(row())
1394 .set(incoming_envelopes::unreadable_expires_at_ns.eq(Some(1i64)))
1395 .execute(conn)
1396 })
1397 .unwrap();
1398 let outcomes = service.retry_blocked_welcomes_after(Cursor(0)).unwrap();
1399 let WelcomeHeadOutcome::Progress {
1400 result: Err(error), ..
1401 } = outcomes.into_iter().next().unwrap()
1402 else {
1403 panic!("expected terminal rejection");
1404 };
1405 match failure {
1406 UnreadableWelcome::Wrapper => {
1407 assert!(matches!(error, GroupError::UnsupportedWelcomeVersion(_)))
1408 }
1409 UnreadableWelcome::MlsVersion => assert!(matches!(
1410 error,
1411 GroupError::WelcomeError(openmls::prelude::WelcomeError::UnsupportedMlsVersion)
1412 )),
1413 UnreadableWelcome::MissingKey => assert!(matches!(
1414 error,
1415 GroupError::NotFound(xmtp_db::NotFound::KeyPackageReference(_))
1416 )),
1417 }
1418 assert_eq!(
1419 db.read_last_rejection(&service.topic())
1420 .unwrap()
1421 .unwrap()
1422 .code,
1423 "unsupported_welcome_expired"
1424 );
1425 assert!(
1426 db.pending_envelope(&service.topic(), cursor)
1427 .unwrap()
1428 .is_none()
1429 );
1430 assert!(db.topic_progress(&service.topic()).unwrap().processed >= cursor);
1431 assert!(
1432 db.welcome_barrier_complete(&service.topic(), cursor)
1433 .unwrap()
1434 );
1435
1436 let next = db
1437 .first_pending_envelope(&service.topic())
1438 .unwrap()
1439 .unwrap();
1440 assert!(matches!(
1441 service
1442 .resolve_pending_welcome(Cursor(next.sequence_id as u64))
1443 .await
1444 .unwrap(),
1445 WelcomeHeadOutcome::Progress {
1446 result: Ok(Some(_)),
1447 ..
1448 }
1449 ));
1450 assert!(db.find_group(&next_group.group_id).unwrap().is_some());
1451 sweep_expired(&bo.context).unwrap();
1452 let removed: Option<openmls::prelude::KeyPackageBundle> =
1453 bo.context.mls_storage().key_package(&hash).unwrap();
1454 assert!(removed.is_none());
1455 }
1456
1457 #[xmtp_common::test(unwrap_try = true)]
1465 async fn group_prefix_wait_expires_after_three_days() {
1466 use crate::worker::key_package_maintenance::sweep_expired;
1467 use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
1468 use openmls_traits::storage::StorageProvider;
1469 use xmtp_db::ConnectionExt;
1470 use xmtp_db::schema::{incoming_envelopes, key_package_history};
1471
1472 tester!(alix, disable_workers);
1473 tester!(bo, disable_workers);
1474 let HeldReadd {
1475 bo_group, welcome, ..
1476 } = held_readd(&alix, &bo).await;
1477 let service = WelcomeService::new(bo.context.clone());
1478 let db = bo.context.db();
1479 let held = || -> Result<bool, GroupError> {
1480 Ok(db
1481 .pending_envelope(&service.topic(), welcome.cursor)?
1482 .is_some())
1483 };
1484
1485 let WelcomeHeadOutcome::Need {
1486 requirement:
1487 WelcomeRequirement::GroupPrefix {
1488 group_id,
1489 anchor,
1490 deadline_ns,
1491 },
1492 ..
1493 } = service.retry_pending_welcome(welcome.cursor, None)?
1494 else {
1495 panic!("expected a group-prefix hold");
1496 };
1497 assert_eq!(group_id, bo_group.group_id);
1498 assert_eq!(deadline_ns, welcome.timestamp() + WELCOME_RETENTION_NS);
1499 let row = db
1500 .pending_envelope(&service.topic(), welcome.cursor)?
1501 .unwrap();
1502 assert_eq!(row.error_code.as_deref(), Some("group_prefix"));
1503 assert!(!row.blocked);
1504 assert_eq!(row.retry_expires_at_ns, Some(deadline_ns));
1505 assert!(!db.welcome_barrier_complete(&service.topic(), welcome.cursor)?);
1506 let processed = db.topic_progress(&StreamTopic::group(group_id))?.processed;
1507 assert!(processed < anchor);
1508 let expire = |anchor, target| {
1509 service.expire_group_prefix_welcome(welcome.cursor, group_id, anchor, target)
1510 };
1511
1512 let history = db
1513 .find_key_package_history_entries_before_id(i32::MAX)?
1514 .pop()
1515 .unwrap();
1516 let hash =
1517 crate::identity::deserialize_key_package_hash_ref(&history.key_package_hash_ref)?;
1518 db.raw_query(|conn| {
1519 diesel::update(
1520 key_package_history::table.filter(key_package_history::id.eq(history.id)),
1521 )
1522 .set(key_package_history::delete_at_ns.eq(0))
1523 .execute(conn)
1524 })?;
1525 sweep_expired(&bo.context)?;
1526 let retained: Option<openmls::prelude::KeyPackageBundle> =
1527 bo.context.mls_storage().key_package(&hash)?;
1528 assert!(
1529 retained.is_some(),
1530 "a prefix hold keeps expired private keys"
1531 );
1532
1533 assert!(matches!(
1535 expire(anchor, processed)?,
1536 WelcomeHeadOutcome::Waiting { .. }
1537 ));
1538 assert!(held()?);
1539
1540 db.raw_query(|conn| {
1543 diesel::update(incoming_envelopes::table.find((
1544 bo.context.installation_id().to_vec(),
1545 EntityKind::Welcome,
1546 welcome.cursor.0 as i64,
1547 )))
1548 .set((
1549 incoming_envelopes::retry_expires_at_ns.eq(Some(1i64)),
1550 incoming_envelopes::error_code.eq("identity_dependency"),
1551 ))
1552 .execute(conn)
1553 })?;
1554 assert!(matches!(
1555 expire(anchor, processed)?,
1556 WelcomeHeadOutcome::Waiting { .. }
1557 ));
1558 assert!(held()?);
1559
1560 assert!(matches!(
1563 service.retry_pending_welcome(welcome.cursor, None)?,
1564 WelcomeHeadOutcome::Need {
1565 requirement: WelcomeRequirement::GroupPrefix { deadline_ns: 1, .. },
1566 ..
1567 }
1568 ));
1569 assert_eq!(
1570 db.pending_envelope(&service.topic(), welcome.cursor)?
1571 .unwrap()
1572 .retry_expires_at_ns,
1573 Some(1)
1574 );
1575
1576 for (anchor, target) in [(anchor, Cursor(processed.0 + 1)), (processed, processed)] {
1578 assert!(matches!(
1579 expire(anchor, target)?,
1580 WelcomeHeadOutcome::Waiting { .. }
1581 ));
1582 assert!(held()?);
1583 }
1584
1585 let epoch = bo_group.epoch().await?;
1586 assert!(matches!(
1587 expire(anchor, processed)?,
1588 WelcomeHeadOutcome::Progress {
1589 result: Err(GroupError::WelcomeGroupPrefixPending { .. }),
1590 ..
1591 }
1592 ));
1593 assert!(db.welcome_barrier_complete(&service.topic(), welcome.cursor)?);
1594 assert_eq!(
1595 db.read_last_rejection(&service.topic())?.unwrap().code,
1596 "group_prefix_expired"
1597 );
1598 assert!(!held()?);
1599 assert!(bo_group.is_active()?);
1600 assert_eq!(bo_group.epoch().await?, epoch);
1601 sweep_expired(&bo.context)?;
1602 let retired: Option<openmls::prelude::KeyPackageBundle> =
1603 bo.context.mls_storage().key_package(&hash)?;
1604 assert!(retired.is_none(), "key is retired once the hold ends");
1605 }
1606
1607 enum WelcomeRetention {
1608 Unreadable,
1609 Held,
1610 Terminal,
1611 }
1612
1613 #[rstest]
1617 #[case::mls_version(|| openmls::prelude::WelcomeError::UnsupportedMlsVersion.into(), WelcomeRetention::Unreadable)]
1618 #[case::extensions(|| openmls::prelude::WelcomeError::UnsupportedExtensions.into(), WelcomeRetention::Unreadable)]
1619 #[case::capability(|| openmls::prelude::WelcomeError::UnsupportedCapability.into(), WelcomeRetention::Unreadable)]
1620 #[case::ciphersuite(|| openmls::prelude::WelcomeError::UnsupportedCiphersuite(openmls::prelude::Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519).into(), WelcomeRetention::Unreadable)]
1621 #[case::key_reference(|| xmtp_db::NotFound::KeyPackageReference(vec![]).into(), WelcomeRetention::Unreadable)]
1622 #[case::key_package(|| xmtp_db::NotFound::KeyPackage(vec![]).into(), WelcomeRetention::Unreadable)]
1623 #[case::post_quantum_key(|| xmtp_db::NotFound::PostQuantumPrivateKey.into(), WelcomeRetention::Unreadable)]
1624 #[case::group_version(|| GroupError::UnsupportedWelcomeVersion("999.0.0".into()), WelcomeRetention::Held)]
1625 #[case::local_failure(|| GroupError::LockUnavailable, WelcomeRetention::Held)]
1626 #[case::unrelated_missing_state(|| xmtp_db::NotFound::MlsGroup(GroupId::from([1; 16])).into(), WelcomeRetention::Held)]
1627 #[case::psk(|| openmls::prelude::WelcomeError::Psk(openmls::schedule::errors::PskError::KeyNotFound).into(), WelcomeRetention::Terminal)]
1628 #[case::stale_join_anchor(|| xmtp_db::StorageError::Stream(xmtp_db::stream_storage::StreamStorageError::StaleJoinAnchor).into(), WelcomeRetention::Terminal)]
1629 #[xmtp_common::test(unwrap_try = true)]
1630 async fn welcome_retention_is_limited_to_unreadable_input(
1631 #[case] error: fn() -> GroupError,
1632 #[case] expectation: WelcomeRetention,
1633 #[values(false, true)] prior_pointer_hold: bool,
1634 ) {
1635 use xmtp_db::ConnectionExt;
1636 use xmtp_db::incoming_envelope::NewIncomingEnvelope;
1637
1638 tester!(bo, disable_workers);
1639 let service = WelcomeService::new(bo.context.clone());
1640 let db = bo.context.db();
1641 db.admit_ordered_batch(
1642 &service.topic(),
1643 Cursor(0),
1644 &[NewIncomingEnvelope {
1645 sequence_id: Cursor(1),
1646 envelope: vec![1],
1647 }],
1648 bo.context
1649 .incoming_runtime()
1650 .policy()
1651 .incoming_limits(NetworkEntityKind::Welcome),
1652 )
1653 .unwrap();
1654 if prior_pointer_hold {
1655 db.set_incoming_retry(
1656 &service.topic(),
1657 Cursor(1),
1658 &IncomingRetry {
1659 retry_at_ns: 0,
1660 blocked: false,
1661 error_code: Some("welcome_pointee".into()),
1662 retry_expires_at_ns: Some(1),
1663 },
1664 )
1665 .unwrap();
1666 }
1667 let pending = db
1668 .pending_envelope(&service.topic(), Cursor(1))
1669 .unwrap()
1670 .unwrap();
1671 let outcome = service
1672 .classify_pending_result(&pending, Err(error()))
1673 .unwrap();
1674 if matches!(expectation, WelcomeRetention::Terminal) {
1675 assert!(matches!(
1676 outcome,
1677 WelcomeHeadOutcome::Progress { result: Err(_), .. }
1678 ));
1679 let rejection = db.read_last_rejection(&service.topic()).unwrap();
1680 assert_eq!(rejection.unwrap().code, "invalid_welcome");
1681 assert!(
1682 db.pending_envelope(&service.topic(), Cursor(1))
1683 .unwrap()
1684 .is_none()
1685 );
1686 assert!(!db.has_pending_welcomes().unwrap());
1687 return;
1688 }
1689 assert!(matches!(outcome, WelcomeHeadOutcome::Waiting { .. }));
1690 let pending = db
1691 .pending_envelope(&service.topic(), Cursor(1))
1692 .unwrap()
1693 .unwrap();
1694 let bounded = matches!(expectation, WelcomeRetention::Unreadable);
1695 assert_eq!(pending.unreadable_expires_at_ns.is_some(), bounded);
1696 if bounded {
1697 assert!(pending.blocked);
1698 assert!(pending.unreadable_expires_at_ns.unwrap() > xmtp_common::time::now_ns());
1699 db.raw_query(|conn| {
1701 use diesel::{ExpressionMethods, RunQueryDsl};
1702 use xmtp_db::schema::incoming_envelopes;
1703 diesel::update(incoming_envelopes::table)
1704 .set(incoming_envelopes::unreadable_expires_at_ns.eq(Some(1i64)))
1705 .execute(conn)
1706 })
1707 .unwrap();
1708 let pending = db
1709 .pending_envelope(&service.topic(), Cursor(1))
1710 .unwrap()
1711 .unwrap();
1712 assert!(matches!(
1713 service
1714 .classify_pending_result(&pending, Err(error()))
1715 .unwrap(),
1716 WelcomeHeadOutcome::Progress { result: Err(_), .. }
1717 ));
1718 assert!(!db.has_pending_welcomes().unwrap());
1719 }
1720 }
1721
1722 #[xmtp_common::test(unwrap_try = true)]
1726 async fn a_full_held_allowance_rejects_the_oldest_held_welcome() {
1727 use xmtp_db::incoming_envelope::NewIncomingEnvelope;
1728
1729 tester!(bo, disable_workers);
1730 let mut settings = bo.context.incoming_runtime().policy().clone();
1731 settings.welcome_pending.rows = 2;
1732 let client = crate::builder::ClientBuilder::from_client(bo.client.clone())
1733 .stream_policy(settings)
1734 .with_disable_workers(true)
1735 .with_allow_offline(Some(true))
1736 .build()
1737 .await?;
1738 let service = WelcomeService::new(client.context.clone());
1739 let db = client.context.db();
1740 let limits = client
1741 .context
1742 .incoming_runtime()
1743 .policy()
1744 .incoming_limits(NetworkEntityKind::Welcome);
1745 for sequence in 1..=3 {
1747 let envelope = NewIncomingEnvelope {
1748 sequence_id: Cursor(sequence),
1749 envelope: vec![1],
1750 };
1751 db.admit_ordered_batch(&service.topic(), Cursor(sequence - 1), &[envelope], limits)?;
1752 let pending = db
1753 .pending_envelope(&service.topic(), Cursor(sequence))?
1754 .unwrap();
1755 let floor = GroupError::UnsupportedWelcomeVersion("999.0.0".into());
1756 service.classify_pending_result(&pending, Err(floor))?;
1757 }
1758 assert!(db.pending_envelope(&service.topic(), Cursor(1))?.is_none());
1759 let rejection = db.read_last_rejection(&service.topic())?.unwrap();
1760 assert_eq!(
1761 (rejection.sequence_id, rejection.code.as_str()),
1762 (Cursor(1), "held_capacity")
1763 );
1764 for kept in [2, 3] {
1765 assert!(
1766 db.pending_envelope(&service.topic(), Cursor(kept))?
1767 .unwrap()
1768 .blocked
1769 );
1770 }
1771 }
1772
1773 #[xmtp_common::test(unwrap_try = true)]
1776 async fn a_rate_limited_welcome_retries_after_the_delay() {
1777 use xmtp_db::incoming_envelope::NewIncomingEnvelope;
1778 tester!(bo, disable_workers);
1779 let service = WelcomeService::new(bo.context.clone());
1780 let db = bo.context.db();
1781 db.admit_ordered_batch(
1782 &service.topic(),
1783 Cursor(0),
1784 &[NewIncomingEnvelope {
1785 sequence_id: Cursor(1),
1786 envelope: vec![1],
1787 }],
1788 bo.context
1789 .incoming_runtime()
1790 .policy()
1791 .incoming_limits(NetworkEntityKind::Welcome),
1792 )?;
1793 let pending = db.pending_envelope(&service.topic(), Cursor(1))?.unwrap();
1794 let delay = xmtp_common::time::Duration::from_secs(300);
1795 let refused = GroupError::WrappedApi(xmtp_api::ApiError::Api(
1796 xmtp_proto::api::NetworkError::new(xmtp_api_grpc::error::GrpcError::Status(
1797 xmtp_api_grpc::error::rate_limited_status("request", delay),
1798 )),
1799 ));
1800 let before = xmtp_common::time::now_ns();
1801 assert!(matches!(
1802 service.classify_pending_result(&pending, Err(refused))?,
1803 WelcomeHeadOutcome::Waiting { blocked: false, .. }
1804 ));
1805 let pending = db.pending_envelope(&service.topic(), Cursor(1))?.unwrap();
1806 assert!(pending.retry_at_ns >= before + delay.as_nanos() as i64);
1807 }
1808
1809 #[xmtp_common::test(unwrap_try = true)]
1813 async fn a_pointee_rate_limit_never_outlasts_its_retention() {
1814 use xmtp_db::incoming_envelope::NewIncomingEnvelope;
1815 tester!(bo, disable_workers);
1816 let service = WelcomeService::new(bo.context.clone());
1817 let db = bo.context.db();
1818 db.admit_ordered_batch(
1819 &service.topic(),
1820 Cursor(0),
1821 &[NewIncomingEnvelope {
1822 sequence_id: Cursor(1),
1823 envelope: vec![1],
1824 }],
1825 bo.context
1826 .incoming_runtime()
1827 .policy()
1828 .incoming_limits(NetworkEntityKind::Welcome),
1829 )?;
1830 let pending = db.pending_envelope(&service.topic(), Cursor(1))?.unwrap();
1831 let deadline = xmtp_common::time::now_ns() + xmtp_common::NS_IN_SEC;
1832 service.defer_pending(
1833 &pending,
1834 "welcome_pointee",
1835 false,
1836 Some(deadline),
1837 Some(xmtp_common::time::Duration::MAX),
1838 )?;
1839 let pending = db.pending_envelope(&service.topic(), Cursor(1))?.unwrap();
1840 assert!(pending.retry_at_ns <= deadline);
1841 }
1842
1843 #[rstest]
1847 #[case::wrapper("unsupported_welcome")]
1848 #[case::missing_key("welcome_blocked")]
1849 #[xmtp_common::test(unwrap_try = true)]
1850 async fn an_expired_unsupported_welcome_still_installs_once_it_is_supported(
1851 #[case] code: &'static str,
1852 #[values(false, true)] pointer: bool,
1853 ) {
1854 use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
1855 use openmls_traits::storage::StorageProvider;
1856 use xmtp_db::ConnectionExt;
1857 use xmtp_db::schema::incoming_envelopes;
1858
1859 tester!(alix, disable_workers);
1860 tester!(bo, disable_workers);
1861 let _other_installations = if pointer {
1862 tester!(bo2, from: bo, disable_workers);
1863 tester!(bo3, from: bo, disable_workers);
1864 Some((bo2, bo3))
1865 } else {
1866 None
1867 };
1868 let group = alix.create_group(None, None).unwrap();
1869 group.invite(&bo).await.unwrap();
1870 let welcome = bo
1871 .context
1872 .api()
1873 .query_welcome_messages(bo.context.installation_id())
1874 .await
1875 .unwrap()
1876 .pop()
1877 .unwrap();
1878 assert_eq!(
1879 matches!(
1880 welcome.variant,
1881 xmtp_proto::types::WelcomeMessageType::WelcomePointer(_)
1882 ),
1883 pointer
1884 );
1885 let cursor = welcome.cursor;
1886 pending_welcome_for_test(&bo.context, &welcome)
1887 .await
1888 .unwrap();
1889 let service = WelcomeService::new(bo.context.clone());
1890 let db = bo.context.db();
1891
1892 let missing_key = if code == "welcome_blocked" {
1893 let history = db
1894 .find_key_package_history_entries_before_id(i32::MAX)
1895 .unwrap()
1896 .pop()
1897 .unwrap();
1898 let hash_ref =
1899 crate::identity::deserialize_key_package_hash_ref(&history.key_package_hash_ref)
1900 .unwrap();
1901 let storage = bo.context.mls_storage();
1902 let bundle: openmls::prelude::KeyPackageBundle =
1903 storage.key_package(&hash_ref).unwrap().unwrap();
1904 storage.delete_key_package(&hash_ref).unwrap();
1905 assert!(matches!(
1906 service.resolve_pending_welcome(cursor).await.unwrap(),
1907 WelcomeHeadOutcome::Waiting { blocked: true, .. }
1908 ));
1909 assert!(
1910 db.pending_envelope(&service.topic(), cursor)
1911 .unwrap()
1912 .unwrap()
1913 .unreadable_expires_at_ns
1914 .unwrap()
1915 > xmtp_common::time::now_ns()
1916 );
1917 Some((hash_ref, bundle))
1918 } else {
1919 None
1920 };
1921
1922 db.raw_query(|conn| {
1926 diesel::update(incoming_envelopes::table.find((
1927 bo.context.installation_id().to_vec(),
1928 EntityKind::Welcome,
1929 cursor.0 as i64,
1930 )))
1931 .set((
1932 incoming_envelopes::blocked.eq(true),
1933 incoming_envelopes::error_code.eq(Some(code)),
1934 incoming_envelopes::unreadable_expires_at_ns.eq(Some(1i64)),
1935 ))
1936 .execute(conn)
1937 })
1938 .unwrap();
1939
1940 if let Some((hash_ref, bundle)) = missing_key {
1941 bo.context
1942 .mls_storage()
1943 .write_key_package(&hash_ref, &bundle)
1944 .unwrap();
1945 }
1946
1947 assert!(matches!(
1948 service
1949 .retry_blocked_welcomes_after(Cursor(0))
1950 .unwrap()
1951 .as_slice(),
1952 [WelcomeHeadOutcome::Need { .. }]
1953 ));
1954
1955 assert!(
1959 db.pending_envelope(&service.topic(), cursor)
1960 .unwrap()
1961 .is_some(),
1962 "a supported Welcome must not be deleted by a stale deadline"
1963 );
1964 assert!(db.topic_progress(&service.topic()).unwrap().processed < cursor);
1965 assert!(matches!(
1966 service.resolve_pending_welcome(cursor).await.unwrap(),
1967 WelcomeHeadOutcome::Progress {
1968 result: Ok(Some(_)),
1969 ..
1970 }
1971 ));
1972 assert!(db.find_group(&group.group_id).unwrap().is_some());
1973 assert!(!db.has_pending_welcomes().unwrap());
1974 assert!(db.read_last_rejection(&service.topic()).unwrap().is_none());
1975 }
1976
1977 #[rstest]
1983 #[case::recovered(false)]
1984 #[case::still_missing(true)]
1985 #[xmtp_common::test(unwrap_try = true)]
1986 async fn legacy_blocked_pointer_has_bounded_refetches(
1987 #[case] remains_missing: bool,
1988 #[values(false, true)] stored_deadline: bool,
1989 ) {
1990 use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
1991 use xmtp_db::{ConnectionExt, schema::incoming_envelopes};
1992
1993 tester!(alix, disable_workers);
1994 tester!(bo, disable_workers);
1995 tester!(_bo2, from: bo, disable_workers);
1996 tester!(_bo3, from: bo, disable_workers);
1997 let group = alix.create_group(None, None).unwrap();
1998 group.invite(&bo).await.unwrap();
1999 let welcome = bo
2000 .context
2001 .api()
2002 .query_welcome_messages(bo.context.installation_id())
2003 .await
2004 .unwrap()
2005 .pop()
2006 .unwrap();
2007 assert!(matches!(
2008 welcome.variant,
2009 xmtp_proto::types::WelcomeMessageType::WelcomePointer(_)
2010 ));
2011 pending_welcome_for_test(&bo.context, &welcome)
2012 .await
2013 .unwrap();
2014 let service = WelcomeService::new(bo.context.clone());
2015 let db = bo.context.db();
2016 let deadline = if stored_deadline {
2017 xmtp_common::time::now_ns() + xmtp_common::NS_IN_HOUR
2018 } else {
2019 welcome.timestamp() + WELCOME_RETENTION_NS
2020 };
2021 db.set_incoming_retry(
2022 &service.topic(),
2023 welcome.cursor,
2024 &IncomingRetry {
2025 retry_at_ns: 0,
2026 blocked: true,
2027 error_code: Some("welcome_blocked".into()),
2028 retry_expires_at_ns: stored_deadline.then_some(deadline),
2029 },
2030 )
2031 .unwrap();
2032
2033 let mut api = xmtp_api_backend::MockBackendClient::new();
2036 api.expect_query_raw_envelopes().returning(|_| {
2037 Ok(xmtp_proto::backend_v1::QueryResponse {
2038 envelopes: vec![],
2039 continuation: Some(xmtp_proto::backend_v1::Continuation { has_more: false }),
2040 }
2041 .into())
2042 });
2043 let missing_client = crate::builder::ClientBuilder::from_client(bo.client.clone())
2044 .api_client(std::sync::Arc::new(api))
2045 .config_provider(std::sync::Arc::new(
2046 xmtp_configuration::StaticConfigProvider::default(),
2047 ))
2048 .with_allow_offline(Some(true))
2049 .with_disable_workers(true)
2050 .build()
2051 .await
2052 .unwrap();
2053 for _ in 0..2 {
2054 let missing_service = WelcomeService::new(missing_client.context.clone());
2055 assert!(matches!(
2056 missing_service
2057 .retry_blocked_welcomes_after(Cursor(0))
2058 .unwrap()
2059 .as_slice(),
2060 [WelcomeHeadOutcome::Need { .. }]
2061 ));
2062 assert!(matches!(
2063 missing_service.resolve_pending_welcome(welcome.cursor).await.unwrap(),
2064 WelcomeHeadOutcome::Waiting { blocked: true, code, .. }
2065 if code == "welcome_blocked"
2066 ));
2067 let pending = db
2068 .pending_envelope(&service.topic(), welcome.cursor)
2069 .unwrap()
2070 .unwrap();
2071 assert!(pending.blocked);
2072 assert_eq!(pending.error_code.as_deref(), Some("welcome_blocked"));
2073 assert_eq!(pending.retry_expires_at_ns, Some(deadline));
2074 assert_eq!(pending.unreadable_expires_at_ns, None);
2075 assert!(db.read_last_rejection(&service.topic()).unwrap().is_none());
2076 assert!(db.find_group(&group.group_id).unwrap().is_none());
2077 }
2078
2079 db.raw_query(|conn| {
2080 diesel::update(incoming_envelopes::table.find((
2081 bo.context.installation_id().to_vec(),
2082 EntityKind::Welcome,
2083 welcome.cursor.0 as i64,
2084 )))
2085 .set(incoming_envelopes::retry_expires_at_ns.eq(Some(1i64)))
2086 .execute(conn)
2087 })
2088 .unwrap();
2089 assert!(matches!(
2090 service.retry_pending_welcome(welcome.cursor, None).unwrap(),
2091 WelcomeHeadOutcome::Need { .. }
2092 ));
2093
2094 if remains_missing {
2095 let service = WelcomeService::new(missing_client.context.clone());
2096 assert!(matches!(
2097 service
2098 .resolve_pending_welcome(welcome.cursor)
2099 .await
2100 .unwrap(),
2101 WelcomeHeadOutcome::Progress {
2102 result: Err(GroupError::WelcomeDataNotFound(_)),
2103 ..
2104 }
2105 ));
2106 assert!(!db.has_pending_welcomes().unwrap());
2107 assert!(db.find_group(&group.group_id).unwrap().is_none());
2108 assert_eq!(
2109 db.read_last_rejection(&service.topic())
2110 .unwrap()
2111 .unwrap()
2112 .code,
2113 "welcome_pointer_expired"
2114 );
2115 return;
2116 }
2117
2118 assert!(matches!(
2121 service
2122 .resolve_pending_welcome(welcome.cursor)
2123 .await
2124 .unwrap(),
2125 WelcomeHeadOutcome::Progress {
2126 result: Ok(Some(_)),
2127 ..
2128 }
2129 ));
2130 assert!(!db.has_pending_welcomes().unwrap());
2131 assert!(db.find_group(&group.group_id).unwrap().is_some());
2132 assert!(db.read_last_rejection(&service.topic()).unwrap().is_none());
2133 }
2134
2135 #[derive(Clone, Copy, Debug)]
2137 enum PointeeQueryFailure {
2138 Unavailable,
2139 Denied,
2140 BadMetadata,
2144 }
2145
2146 #[rstest]
2153 #[case::fresh_unavailable(None, false, PointeeQueryFailure::Unavailable)]
2154 #[case::fresh_denied(None, false, PointeeQueryFailure::Denied)]
2155 #[case::fresh_bad_metadata(None, false, PointeeQueryFailure::BadMetadata)]
2156 #[case::legacy_retry_unavailable(
2157 Some("welcome_retry"),
2158 false,
2159 PointeeQueryFailure::Unavailable
2160 )]
2161 #[case::legacy_retry_denied(Some("welcome_retry"), false, PointeeQueryFailure::Denied)]
2162 #[case::unreadable_unavailable(Some("welcome_blocked"), true, PointeeQueryFailure::Unavailable)]
2163 #[case::unreadable_denied(Some("welcome_blocked"), true, PointeeQueryFailure::Denied)]
2164 #[xmtp_common::test(unwrap_try = true)]
2165 async fn pointer_query_errors_have_bounded_retention(
2166 #[case] prior_code: Option<&'static str>,
2167 #[case] unreadable: bool,
2168 #[case] failure: PointeeQueryFailure,
2169 ) {
2170 use xmtp_proto::backend_v1 as wire;
2171 let retryable = matches!(failure, PointeeQueryFailure::Unavailable);
2172 use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
2173 use xmtp_db::{ConnectionExt, schema::incoming_envelopes};
2174
2175 tester!(alix, disable_workers);
2176 tester!(bo, disable_workers);
2177 tester!(_bo2, from: bo, disable_workers);
2178 tester!(_bo3, from: bo, disable_workers);
2179 let group = alix.create_group(None, None).unwrap();
2180 group.invite(&bo).await.unwrap();
2181 let welcome = bo
2182 .context
2183 .api()
2184 .query_welcome_messages(bo.context.installation_id())
2185 .await
2186 .unwrap()
2187 .pop()
2188 .unwrap();
2189 assert!(matches!(
2190 welcome.variant,
2191 xmtp_proto::types::WelcomeMessageType::WelcomePointer(_)
2192 ));
2193 pending_welcome_for_test(&bo.context, &welcome)
2194 .await
2195 .unwrap();
2196 let service = WelcomeService::new(bo.context.clone());
2197 let db = bo.context.db();
2198 let row = || {
2199 incoming_envelopes::table.find((
2200 bo.context.installation_id().to_vec(),
2201 EntityKind::Welcome,
2202 welcome.cursor.0 as i64,
2203 ))
2204 };
2205 let unreadable_deadline =
2206 unreadable.then(|| xmtp_common::time::now_ns() + xmtp_common::NS_IN_DAY);
2207 db.raw_query(|conn| {
2208 diesel::update(row())
2209 .set((
2210 incoming_envelopes::error_code.eq(prior_code),
2211 incoming_envelopes::blocked.eq(unreadable),
2212 incoming_envelopes::retry_expires_at_ns.eq(unreadable.then_some(1i64)),
2213 incoming_envelopes::unreadable_expires_at_ns.eq(unreadable_deadline),
2214 ))
2215 .execute(conn)
2216 })
2217 .unwrap();
2218
2219 let mut api = xmtp_api_backend::MockBackendClient::new();
2220 api.expect_query_raw_envelopes().returning(move |request| {
2221 let status = match failure {
2222 PointeeQueryFailure::Unavailable => {
2223 tonic::Status::unavailable("pointee query unavailable")
2224 }
2225 PointeeQueryFailure::Denied => {
2226 tonic::Status::permission_denied("pointee query denied")
2227 }
2228 PointeeQueryFailure::BadMetadata => {
2229 return Ok(wire::QueryResponse {
2231 envelopes: vec![wire::ServerEnvelope {
2232 meta: Some(wire::EnvelopeMeta {
2233 topic: request.queries[0].topic.clone(),
2234 cursor: Some(wire::Cursor { sequence_id: 1 }),
2235 message_hash: Some(wire::MessageHash {
2236 hash: Some(wire::message_hash::Hash::Sha256(vec![7; 31])),
2237 }),
2238 ..Default::default()
2239 }),
2240 envelope: None,
2241 }],
2242 continuation: Some(wire::Continuation { has_more: false }),
2243 }
2244 .into());
2245 }
2246 };
2247 Err(xmtp_proto::api::ApiClientError::client(
2248 xmtp_api_grpc::error::GrpcError::Status(status),
2249 ))
2250 });
2251 let failing_client = crate::builder::ClientBuilder::from_client(bo.client.clone())
2252 .api_client(std::sync::Arc::new(api))
2253 .config_provider(std::sync::Arc::new(
2254 xmtp_configuration::StaticConfigProvider::default(),
2255 ))
2256 .with_allow_offline(Some(true))
2257 .with_disable_workers(true)
2258 .build()
2259 .await
2260 .unwrap();
2261 let failing_service = WelcomeService::new(failing_client.context.clone());
2262 let blocked = unreadable || !retryable;
2263 assert!(matches!(
2264 failing_service.resolve_pending_welcome(welcome.cursor).await.unwrap(),
2265 WelcomeHeadOutcome::Waiting { blocked: actual, .. } if actual == blocked
2266 ));
2267 let pending = db
2268 .pending_envelope(&service.topic(), welcome.cursor)
2269 .unwrap()
2270 .unwrap();
2271 assert_eq!(
2272 pending.error_code.as_deref(),
2273 Some(if blocked {
2274 "welcome_blocked"
2275 } else {
2276 "welcome_pointee"
2277 })
2278 );
2279 assert_eq!(
2280 pending.retry_expires_at_ns,
2281 Some(if unreadable {
2282 1
2283 } else {
2284 welcome.timestamp() + WELCOME_RETENTION_NS
2285 })
2286 );
2287 assert_eq!(pending.unreadable_expires_at_ns, unreadable_deadline);
2288 assert!(db.read_last_rejection(&service.topic()).unwrap().is_none());
2289
2290 db.raw_query(|conn| {
2291 diesel::update(row())
2292 .set((
2293 incoming_envelopes::retry_expires_at_ns.eq(Some(1i64)),
2294 incoming_envelopes::unreadable_expires_at_ns.eq(unreadable.then_some(1i64)),
2295 ))
2296 .execute(conn)
2297 })
2298 .unwrap();
2299 let failing_service = WelcomeService::new(failing_client.context.clone());
2300 assert!(matches!(
2301 failing_service
2302 .resolve_pending_welcome(welcome.cursor)
2303 .await
2304 .unwrap(),
2305 WelcomeHeadOutcome::Progress { result: Err(_), .. }
2306 ));
2307 assert!(!db.has_pending_welcomes().unwrap());
2308 assert!(db.find_group(&group.group_id).unwrap().is_none());
2309 assert_eq!(
2310 db.read_last_rejection(&service.topic())
2311 .unwrap()
2312 .unwrap()
2313 .code,
2314 if unreadable {
2315 "unsupported_welcome_expired"
2316 } else {
2317 "welcome_pointer_expired"
2318 }
2319 );
2320 }
2321
2322 #[xmtp_common::test(unwrap_try = true)]
2323 async fn blocked_welcome_does_not_hold_an_independent_join() {
2324 use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
2325 use xmtp_db::ConnectionExt;
2326 use xmtp_db::schema::incoming_envelopes;
2327 use xmtp_proto::backend_v1::{client_envelope::Payload, welcome_message::Version};
2328
2329 tester!(alix, disable_workers);
2330 tester!(bo, disable_workers);
2331 let first_group = alix.create_group(None, None)?;
2332 first_group.invite(&bo).await?;
2333 let second_group = alix.create_group(None, None)?;
2334 second_group.invite(&bo).await?;
2335 let welcomes = bo
2336 .context
2337 .api()
2338 .query_welcome_messages(bo.context.installation_id())
2339 .await?;
2340 let first = welcomes.first()?.cursor;
2341 let last = welcomes.last()?.cursor;
2342 pending_welcome_for_test(&bo.context, welcomes.last()?).await?;
2343 let service = WelcomeService::new(bo.context.clone());
2344 let db = bo.context.db();
2345 let pending = db.pending_envelope(&service.topic(), first)?.unwrap();
2346 let mut wire = xmtp_proto::backend_v1::ServerEnvelope::decode(pending.envelope.as_slice())?;
2347 let Payload::WelcomeMessage(welcome) = wire.envelope.as_mut()?.payload.as_mut()? else {
2348 panic!("expected Welcome payload");
2349 };
2350 match welcome.version.as_mut()? {
2351 Version::V1(message) => message.wrapper_algorithm = i32::MAX,
2352 Version::WelcomePointer(message) => message.wrapper_algorithm = i32::MAX,
2353 }
2354 let envelope = wire.encode_to_vec();
2355 db.raw_query(|conn| {
2356 diesel::update(incoming_envelopes::table.find((
2357 bo.context.installation_id().to_vec(),
2358 EntityKind::Welcome,
2359 first.0 as i64,
2360 )))
2361 .set((
2362 incoming_envelopes::envelope_bytes.eq(envelope.len() as i64),
2363 incoming_envelopes::envelope.eq(&envelope),
2364 ))
2365 .execute(conn)
2366 })?;
2367
2368 let outcomes = service.process_pending_welcomes_once(&[])?;
2369 assert!(outcomes.iter().any(|outcome| matches!(outcome,
2370 WelcomeHeadOutcome::Waiting { cursor, blocked: true, .. } if *cursor == first)));
2371 assert!(outcomes.iter().any(|outcome| matches!(outcome,
2372 WelcomeHeadOutcome::Need { cursor, .. } | WelcomeHeadOutcome::Progress { cursor, .. } if *cursor == last)));
2373 if db.find_group(&second_group.group_id)?.is_none() {
2374 assert!(matches!(
2375 service.resolve_pending_welcome(last).await?,
2376 WelcomeHeadOutcome::Progress {
2377 result: Ok(Some(_)),
2378 ..
2379 }
2380 ));
2381 }
2382 assert!(db.find_group(&first_group.group_id)?.is_none());
2383 assert!(db.find_group(&second_group.group_id)?.is_some());
2384 let first_pending = db.pending_envelope(&service.topic(), first)?.unwrap();
2385 assert!(first_pending.blocked);
2386 assert_eq!(
2387 first_pending.error_code.as_deref(),
2388 Some("unsupported_welcome")
2389 );
2390 assert!(db.pending_envelope(&service.topic(), last)?.is_none());
2391 let progress = db.topic_progress(&service.topic())?;
2392 assert!(progress.processed < first);
2393 assert_eq!(progress.received, last);
2394 }
2395
2396 impl ValidateGroupMembership for RejectMembership {
2397 async fn check_initial_membership(
2398 &self,
2399 _welcome: &WelcomeMembership,
2400 ) -> Result<(), GroupError> {
2401 if self.retryable {
2402 Err(GroupError::LockUnavailable)
2403 } else {
2404 Err(GroupError::NoPSKSupport)
2405 }
2406 }
2407 }
2408
2409 #[rstest]
2410 #[case::terminal(false)]
2411 #[case::retry(true)]
2412 #[xmtp_common::test(unwrap_try = true)]
2413 async fn welcome_rejection_commits_only_terminal_progress(#[case] retryable: bool) {
2414 tester!(alix, disable_workers);
2415 tester!(bo, disable_workers);
2416 let group = alix.create_group(None, None).unwrap();
2417 group.invite(&bo).await.unwrap();
2418 let welcome = bo
2419 .context
2420 .api()
2421 .query_welcome_messages(bo.context.installation_id())
2422 .await
2423 .unwrap()
2424 .pop()
2425 .unwrap();
2426 pending_welcome_for_test(&bo.context, &welcome)
2427 .await
2428 .unwrap();
2429 let service = WelcomeService::new(bo.context.clone());
2430 let result = service
2431 .process_new_welcome(&welcome, RejectMembership { retryable })
2432 .await;
2433 assert!(result.is_err());
2434 assert!(
2435 bo.context
2436 .db()
2437 .find_group(&group.group_id)
2438 .unwrap()
2439 .is_none()
2440 );
2441 let expected = if !retryable {
2442 welcome.cursor
2443 } else {
2444 Cursor(0)
2445 };
2446 assert_eq!(
2447 bo.context
2448 .db()
2449 .get_last_cursor(bo.context.installation_id(), EntityKind::Welcome)
2450 .unwrap(),
2451 expected
2452 );
2453 }
2454
2455 #[xmtp_common::test(unwrap_try = true)]
2456 async fn later_welcome_completion_keeps_earlier_retry_pending() {
2457 tester!(alix, disable_workers);
2458 tester!(bo, disable_workers);
2459 let first_group = alix.create_group(None, None)?;
2460 first_group.invite(&bo).await?;
2461 let second_group = alix.create_group(None, None)?;
2462 second_group.invite(&bo).await?;
2463 let welcomes = bo
2464 .context
2465 .api()
2466 .query_welcome_messages(bo.context.installation_id())
2467 .await?;
2468 let first = welcomes.first()?.cursor;
2469 let last = welcomes.last()?.cursor;
2470 pending_welcome_for_test(&bo.context, welcomes.last()?).await?;
2471 let service = WelcomeService::new(bo.context.clone());
2472 for welcome in welcomes {
2473 let result = service
2474 .process_new_welcome(
2475 &welcome,
2476 RejectMembership {
2477 retryable: welcome.cursor == first,
2478 },
2479 )
2480 .await;
2481 assert!(result.is_err());
2482 }
2483 let db = bo.context.db();
2484 assert!(db.pending_envelope(&service.topic(), first)?.is_some());
2485 assert!(db.pending_envelope(&service.topic(), last)?.is_none());
2486 let progress = db.topic_progress(&service.topic())?;
2487 assert!(progress.processed < first);
2488 assert_eq!(progress.received, last);
2489 }
2490}