1use super::{
2 FailedInstallationIds, GroupError, HmacKey, MlsGroup,
3 change_callbacks::AppDataChange,
4 intents::{
5 CommitPendingProposalsIntentData, Installation, IntentError, PostCommitAction,
6 ProposeMemberUpdateIntentData, SendMessageIntentData, SendWelcomesAction,
7 UpdateAdminListIntentData, UpdateGroupMembershipIntentData, UpdatePermissionIntentData,
8 },
9 summary::{MessageIdentifier, MessageIdentifierBuilder, ProcessSummary, SyncSummary},
10 validated_commit::{CommitValidationError, validate_proposal},
11};
12use crate::{
13 client::ClientError,
14 context::XmtpSharedContext,
15 groups::{
16 group_membership::GroupMembership,
17 intents::MembershipDiffWithKeyPackages,
18 intents::{QueueIntent, ReaddInstallationsIntentData, UpdateMetadataIntentData},
19 mls_ext::{CommitLogStorer, MlsGroupReload},
20 mls_sync::update_group_membership::apply_readd_installations_intent,
21 validated_commit::ValidatedCommit,
22 },
23 identity::{IdentityError, parse_credential},
24 identity_updates::{IdentityUpdates, load_identity_updates_for_client},
25 intents::ProcessIntentError,
26 messages::{decoded_message::MessageBody, enrichment::EnrichMessageError},
27 mls_store::MlsStore,
28 traits::IntoWith,
29 utils::{
30 hash::sha256,
31 id::{calculate_message_id, calculate_message_id_for_intent},
32 time::hmac_epoch,
33 },
34};
35use hkdf::Hkdf;
36use hmac::{Hmac, Mac};
37use openmls::prelude::BasicCredentialError;
38use openmls::{
39 credentials::BasicCredential,
40 framing::ProtocolMessage,
41 group::{
42 CommitToPendingProposalsError, GroupContext, GroupEpoch, ProcessMessageError, StagedCommit,
43 ValidationError,
44 },
45 key_packages::KeyPackage,
46 messages::proposals::Proposal,
47 prelude::{
48 Extensions, LeafNodeIndex, MlsGroup as OpenMlsGroup, ProcessedMessage,
49 ProcessedMessageContent, ProposalType, Sender,
50 tls_codec::{Error as TlsCodecError, Serialize},
51 },
52 treesync::LeafNodeParameters,
53};
54use openmls_traits::OpenMlsProvider;
55use prost::Message;
56use prost::bytes::Bytes;
57use sha2::Sha256;
58use std::{
59 collections::{BTreeSet, HashMap, HashSet},
60 mem::{Discriminant, discriminant},
61 ops::RangeInclusive,
62 time::Duration,
63};
64use thiserror::Error;
65use tracing::debug;
66use update_group_membership::apply_update_group_membership_intent;
67use xmtp_api::PublishUnit;
68use xmtp_common::{Event, ExponentialBackoff, RetryableError, Strategy, log_event, time::now_ns};
69use xmtp_configuration::{
70 HMAC_SALT, MAX_GROUP_SYNC_RETRIES, MAX_PAST_EPOCHS, SYNC_BACKOFF_TOTAL_WAIT_MAX_SECS,
71 SYNC_BACKOFF_WAIT_MS, SYNC_JITTER_MS, SYNC_UPDATE_INSTALLATIONS_INTERVAL_NS,
72 WELCOME_HPKE_LABEL,
73};
74use xmtp_content_types::{CodecError, ContentCodec, group_updated::GroupUpdatedCodec};
75use xmtp_db::TransactionOutcome::{Continue, Rollback};
76use xmtp_db::XmtpMlsStorageProvider;
77use xmtp_db::group::GroupMembershipState;
78use xmtp_db::message_deletion::{QueryMessageDeletion, StoredMessageDeletion};
79use xmtp_db::{
80 Fetch, StorageError, StoreOrIgnore, TransactionOutcome,
81 group::ConversationType,
82 group_intent::{ID, IntentKind, IntentState, StoredGroupIntent},
83 group_message::{ContentType, DeliveryStatus, GroupMessageKind, StoredGroupMessage},
84 remote_commit_log::CommitResult,
85 sql_key_store,
86 user_preferences::StoredUserPreferences,
87};
88use xmtp_db::{NotFound, group_intent::IntentKind::MetadataUpdate};
89use xmtp_db::{XmtpOpenMlsProviderRef, prelude::*};
90use xmtp_db::{
91 group_message::MsgQueryArgs,
92 pending_remove::{PendingRemove, QueryPendingRemove},
93};
94use xmtp_events::{ClientEvent, EventContext, EventWriter};
95use xmtp_id::{InboxId, InboxIdRef};
96use xmtp_mls_common::app_data::component_id::ComponentId;
97use xmtp_mls_common::group_mutable_metadata::MetadataField;
98use xmtp_mls_common::libxmtp_version::LibXMTPVersion;
99use xmtp_mls_common::mls_ext::payload_encryption::{
100 WrapPayloadError, wrap_payload_hpke, wrap_payload_symmetric,
101};
102use xmtp_mls_validation::commit::{
103 CommitRuleError, Inbox, MetadataChanges, extract_group_membership,
104};
105use xmtp_proto::backend_v1::{
106 ClientEnvelope, GroupMessage as BackendGroupMessage, WelcomeMessage as WelcomeMessageInput,
107 client_envelope::Payload,
108 welcome_message::{
109 V1 as WelcomeMessageInputV1, Version as WelcomeMessageInputVersion,
110 WelcomePointer as WelcomePointerInput,
111 },
112};
113use xmtp_proto::types::GroupId;
114use xmtp_proto::xmtp::mls::message_contents::content_types::DeleteMessage;
115use xmtp_proto::xmtp::mls::{
116 database::{ProcessPendingSelfRemove, Task as TaskProto, task::Task as TaskKind},
117 message_contents::{
118 GroupUpdated, PlaintextEnvelope, WelcomeMetadata, WelcomePointer as WelcomePointerProto,
119 group_updated,
120 plaintext_envelope::{Content, V1, V2},
121 },
122};
123use xmtp_proto::{
124 GroupUpdateDeduper,
125 types::{Cursor, GroupMessage},
126};
127use xmtp_proto::{ShortHex, xmtp::mls::message_contents::EncodedContent};
128use zeroize::Zeroizing;
129
130mod envelope_and_app_data;
131mod external_message;
132mod leave_and_delete;
133mod membership_helpers;
134mod own_message;
135mod post_commit;
136mod processing;
137mod receive;
138mod sync_driver;
139pub(in crate::groups) use membership_helpers::*;
140mod processing_policy;
141pub mod update_group_membership;
142pub(crate) use processing::GroupHeadOutcome;
143pub(crate) mod publish;
144
145impl<Context: XmtpSharedContext> MlsGroup<Context> {
146 fn metadata_changed_event(&self, commit: &ValidatedCommit) -> Option<ClientEvent> {
147 let changed: BTreeSet<_> = commit
148 .metadata_component_ids
149 .iter()
150 .copied()
151 .map(ComponentId::from)
152 .map(ComponentId::event_name)
153 .collect();
154 (!changed.is_empty()).then(|| {
155 ClientEvent::ConversationMetadataChanged(xmtp_events::MetadataChanged {
156 group_id: self.group_id.to_vec(),
157 changed: changed.into_iter().collect(),
158 })
159 })
160 }
161
162 pub(in crate::groups) fn emit_message_status_changed(
163 &self,
164 message_id: Vec<u8>,
165 previous: xmtp_events::MessageStatus,
166 current: xmtp_events::MessageStatus,
167 writer: &impl EventWriter<crate::subscriptions::internal::InternalEvent>,
168 ) {
169 if self.conversation_type.is_virtual() || previous == current {
170 return;
171 }
172 writer.emit_with_context(
173 Some(ClientEvent::MessageStatusChanged(
174 xmtp_events::MessageStatusChanged {
175 group_id: self.group_id.to_vec(),
176 message_id,
177 previous,
178 current,
179 },
180 )),
181 None,
182 EventContext {
183 dm_identifier: self.dm_id.as_ref().map(|id| id.as_bytes().to_vec()),
184 references_own_messages: false,
185 ..Default::default()
186 },
187 );
188 }
189
190 fn emit_commit_events(
192 &self,
193 commit: &ValidatedCommit,
194 group_active: bool,
195 storage: &impl XmtpMlsStorageProvider,
196 writer: &impl EventWriter<crate::subscriptions::internal::InternalEvent>,
197 ) -> Result<(), GroupMessageProcessingError> {
198 if self.conversation_type.is_virtual() {
199 return Ok(());
200 }
201 let context = EventContext {
202 dm_identifier: self.dm_id.as_ref().map(|id| id.as_bytes().to_vec()),
203 references_own_messages: false,
204 ..Default::default()
205 };
206 if !group_active {
207 let left = storage
208 .db()
209 .get_pending_remove_users(&self.group_id)?
210 .contains(&self.context.inbox_id().to_string());
211 writer.emit_with_context(
212 Some(ClientEvent::ConversationRemoved(
213 xmtp_events::ConversationRemoved {
214 group_id: self.group_id.to_vec(),
215 cause: if left {
216 xmtp_events::RemovalCause::Left
217 } else {
218 xmtp_events::RemovalCause::Removed
219 },
220 },
221 )),
222 None,
223 context.clone(),
224 );
225 }
226 if !commit.added_inboxes.is_empty() || !commit.removed_inboxes.is_empty() {
227 writer.emit_with_context(
228 Some(ClientEvent::ConversationMembershipChanged(
229 xmtp_events::MembershipChanged {
230 group_id: self.group_id.to_vec(),
231 added_inbox_ids: commit
232 .added_inboxes
233 .iter()
234 .map(|inbox| inbox.inbox_id.clone())
235 .collect(),
236 removed_inbox_ids: commit
237 .removed_inboxes
238 .iter()
239 .map(|inbox| inbox.inbox_id.clone())
240 .collect(),
241 },
242 )),
243 None,
244 context.clone(),
245 );
246 }
247 if let Some(event) = self.metadata_changed_event(commit) {
248 writer.emit_with_context(Some(event), None, context);
249 }
250 Ok(())
251 }
252}
253
254#[derive(Debug, Error)]
255pub enum GroupMessageProcessingError {
256 #[error("intent already processed")]
257 IntentAlreadyProcessed,
258 #[error("message with cursor [{}] for group [{}] already processed", _0.cursor, xmtp_common::fmt::debug_hex(_0.group_id)
259 )]
260 MessageAlreadyProcessed(MessageIdentifier),
261 #[error("message identifier not found")]
262 MessageIdentifierNotFound,
263 #[error("welcome with cursor [{0}] already processed")]
264 WelcomeAlreadyProcessed(u64),
265 #[error("[{message_time_ns:?}] invalid sender with credential: {credential:?}")]
266 InvalidSender {
267 message_time_ns: u64,
268 credential: Vec<u8>,
269 },
270 #[error("invalid payload")]
271 InvalidPayload,
272 #[error("incoming group head changed")]
274 IncomingHeadChanged,
275 #[error("stored incoming envelope is corrupt: {0}")]
277 CorruptIncomingEnvelope(prost::DecodeError),
278 #[error("unsupported MLS wire version")]
280 UnsupportedMlsVersion,
281 #[error("a committed proposal was received but is missing from local state")]
285 LostProposalState,
286 #[error(transparent)]
288 Envelope(xmtp_api_backend::envelope::EnvelopeError),
289 #[error("own envelope has no prepared attempt")]
291 OwnMessageWithoutAttempt,
292 #[error("own intent kind for payload {0} is not supported by this version")]
303 UnsupportedOwnIntentKind(String),
304 #[error("prepared attempt state: {0}")]
306 PreparedAttempt(#[source] Box<GroupError>),
307 #[error("intent rejected: {0}")]
309 RejectedIntent(&'static str),
310 #[error("storage error: {0}")]
311 Storage(#[from] xmtp_db::StorageError),
312 #[error(transparent)]
313 Identity(#[from] IdentityError),
314 #[error("openmls process message error: {0}")]
315 OpenMlsProcessMessage(
316 #[from] openmls::prelude::ProcessMessageError<sql_key_store::SqlKeyStoreError>,
317 ),
318 #[error("app-data process message error: {0}")]
326 OpenMlsProcessMessageWithAppData(
327 #[from] super::app_data::ProcessMessageWithAppDataError<sql_key_store::SqlKeyStoreError>,
328 ),
329 #[error("merge staged commit: {0}")]
330 MergeStagedCommit(#[from] openmls::group::MergeCommitError<sql_key_store::SqlKeyStoreError>),
331 #[error("TLS Codec error: {0}")]
332 TlsError(#[from] TlsCodecError),
333 #[error("unsupported message type: {0:?}")]
334 UnsupportedMessageType(Discriminant<ProtocolMessage>),
335 #[error("unexpected processed message content: {0}")]
341 UnexpectedProcessedContent(&'static str),
342 #[error("commit validation: {0}")]
343 CommitValidation(#[from] CommitValidationError),
344 #[error("epoch increment not allowed")]
345 EpochIncrementNotAllowed,
346 #[error("clear pending commit error: {0}")]
347 ClearPendingCommit(#[from] sql_key_store::SqlKeyStoreError),
348 #[error("Serialization/Deserialization Error {0}")]
349 Serde(#[from] serde_json::Error),
350 #[error("intent is missing staged_commit field")]
351 IntentMissingStagedCommit,
352 #[error("encode proto: {0}")]
353 EncodeProto(#[from] prost::EncodeError),
354 #[error("proto decode error: {0}")]
355 DecodeProto(#[from] prost::DecodeError),
356 #[error(transparent)]
357 Intent(#[from] IntentError),
358 #[error(transparent)]
359 Codec(#[from] CodecError),
360 #[error("wrong credential type")]
361 WrongCredentialType(#[from] BasicCredentialError),
362 #[error(transparent)]
363 ProcessIntent(#[from] ProcessIntentError),
364 #[error(transparent)]
365 AssociationDeserialization(#[from] xmtp_id::associations::DeserializationError),
366 #[error(transparent)]
367 Client(#[from] ClientError),
368 #[error("Group paused due to minimum protocol version requirement")]
369 GroupPaused,
370 #[error("group is inactive")]
372 GroupInactive,
373 #[error("Message epoch [{0}] is too old [{1}]")]
374 OldEpoch(u64, u64),
375 #[error("Message epoch [{0}] is greater than group epoch [{1}]")]
376 FutureEpoch(u64, u64),
377 #[error(transparent)]
378 Db(#[from] xmtp_db::ConnectionError),
379 #[error(transparent)]
380 Builder(#[from] derive_builder::UninitializedFieldError),
381 #[error(transparent)]
382 Diesel(#[from] xmtp_db::diesel::result::Error),
383 #[error(transparent)]
384 EnrichMessage(#[from] EnrichMessageError),
385 #[error("pre-commit proposal phase complete, re-queuing intent")]
386 PreCommitProposalPhaseComplete,
387 #[error(transparent)]
388 Conversion(#[from] xmtp_proto::ConversionError),
389 #[error(
396 "staged commit merge for group [{group_id}] at sequence [{commit_sequence_id}] reached \
397 epoch [{epoch}] without advancing the epoch authenticator; refusing to record corrupt \
398 commit log entry"
399 )]
400 EpochAuthenticatorNotAdvanced {
401 group_id: GroupId,
402 commit_sequence_id: i64,
403 epoch: u64,
404 },
405}
406
407impl From<xmtp_mls_validation::commit::CommitRuleError> for GroupMessageProcessingError {
408 fn from(error: xmtp_mls_validation::commit::CommitRuleError) -> Self {
409 Self::CommitValidation(error.into())
410 }
411}
412
413impl RetryableError for GroupMessageProcessingError {
414 fn is_retryable(&self) -> bool {
415 match self {
416 Self::IncomingHeadChanged => true,
417 Self::PreparedAttempt(error) => error.is_retryable(),
418 Self::CorruptIncomingEnvelope(_)
419 | Self::UnsupportedMlsVersion
420 | Self::LostProposalState
421 | Self::Envelope(_)
422 | Self::OwnMessageWithoutAttempt
423 | Self::UnsupportedOwnIntentKind(_) => false,
424 Self::RejectedIntent(_) => false,
425 Self::Storage(err) => err.is_retryable(),
426 Self::Diesel(err) => err.is_retryable(),
427 Self::Identity(err) => err.is_retryable(),
428 Self::OpenMlsProcessMessage(err) => err.is_retryable(),
429 Self::OpenMlsProcessMessageWithAppData(err) => match err {
430 super::app_data::ProcessMessageWithAppDataError::OpenMls(e) => e.is_retryable(),
431 super::app_data::ProcessMessageWithAppDataError::AppDataDecode(_) => false,
434 super::app_data::ProcessMessageWithAppDataError::ProtocolVersionTooLow {
439 ..
440 } => false,
441 super::app_data::ProcessMessageWithAppDataError::ResolveAppDataCommit(_) => false,
446 },
447 Self::MergeStagedCommit(err) => err.is_retryable(),
448 Self::ProcessIntent(err) => err.is_retryable(),
449 Self::CommitValidation(err) => err.is_retryable(),
450 Self::ClearPendingCommit(err) => err.is_retryable(),
451 Self::Client(err) => err.is_retryable(),
452 Self::Db(e) => e.is_retryable(),
453 Self::EnrichMessage(e) => e.is_retryable(),
454 Self::IntentAlreadyProcessed
455 | Self::MessageIdentifierNotFound
456 | Self::WrongCredentialType(_)
457 | Self::Codec(_)
458 | Self::MessageAlreadyProcessed(_)
459 | Self::WelcomeAlreadyProcessed(_)
460 | Self::InvalidSender { .. }
461 | Self::DecodeProto(_)
462 | Self::InvalidPayload
463 | Self::Intent(_)
464 | Self::UnexpectedProcessedContent(_)
465 | Self::EpochIncrementNotAllowed
466 | Self::EncodeProto(_)
467 | Self::IntentMissingStagedCommit
468 | Self::Serde(_)
469 | Self::AssociationDeserialization(_)
470 | Self::TlsError(_)
471 | Self::UnsupportedMessageType(_)
472 | Self::GroupPaused
473 | Self::GroupInactive
474 | Self::FutureEpoch(_, _)
475 | Self::OldEpoch(_, _)
476 | Self::PreCommitProposalPhaseComplete => false,
477 Self::Builder(_) => false,
478 Self::Conversion(_) => false,
479 Self::EpochAuthenticatorNotAdvanced { .. } => true,
483 }
484 }
485}
486
487impl crate::worker::NeedsDbReconnect for GroupMessageProcessingError {
488 fn needs_db_reconnect(&self) -> bool {
489 match self {
490 Self::Storage(error) => error.db_needs_connection(),
491 Self::Db(error) => error.db_needs_connection(),
492 Self::Client(error) => error.db_needs_connection(),
493 Self::Identity(error) => error.needs_db_reconnect(),
494 Self::PreparedAttempt(error) => error.needs_db_reconnect(),
495 Self::CommitValidation(error) => error.needs_db_reconnect(),
496 _ => false,
497 }
498 }
499}
500
501impl GroupMessageProcessingError {
502 fn from_app_data_processing(
514 err: super::app_data::ProcessMessageWithAppDataError<sql_key_store::SqlKeyStoreError>,
515 ) -> Self {
516 match err {
517 super::app_data::ProcessMessageWithAppDataError::ProtocolVersionTooLow {
518 min_version,
519 ..
520 } => Self::CommitValidation(CommitValidationError::Rule(
521 CommitRuleError::ProtocolVersionTooLow(min_version),
522 )),
523 other => other.into(),
524 }
525 }
526
527 pub(crate) fn commit_result(&self) -> CommitResult {
528 use super::app_data::ProcessMessageWithAppDataError;
529 match self {
530 GroupMessageProcessingError::OpenMlsProcessMessage(
531 ProcessMessageError::ValidationError(ValidationError::WrongEpoch),
532 ) => CommitResult::WrongEpoch,
533 GroupMessageProcessingError::OpenMlsProcessMessageWithAppData(
540 ProcessMessageWithAppDataError::OpenMls(ProcessMessageError::ValidationError(
541 ValidationError::WrongEpoch,
542 )),
543 ) => CommitResult::WrongEpoch,
544 GroupMessageProcessingError::OpenMlsProcessMessageWithAppData(
545 ProcessMessageWithAppDataError::OpenMls(_),
546 ) => CommitResult::Undecryptable,
547 GroupMessageProcessingError::OpenMlsProcessMessageWithAppData(
548 ProcessMessageWithAppDataError::AppDataDecode(_),
549 ) => CommitResult::Invalid,
550 GroupMessageProcessingError::OldEpoch(_, _) => CommitResult::WrongEpoch,
551 GroupMessageProcessingError::FutureEpoch(_, _) => CommitResult::WrongEpoch,
552 GroupMessageProcessingError::CommitValidation(_) => CommitResult::Invalid,
553 GroupMessageProcessingError::OpenMlsProcessMessage(_) => CommitResult::Undecryptable,
554 _ => CommitResult::Unknown,
555 }
556 }
557}
558
559#[derive(Debug, Error)]
560pub struct IntentResolutionError {
561 processing_error: GroupMessageProcessingError,
562}
563
564impl std::fmt::Display for IntentResolutionError {
565 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
566 write!(f, "IntentValidationError: {}", self.processing_error)
567 }
568}
569
570impl RetryableError for IntentResolutionError {
571 fn is_retryable(&self) -> bool {
572 self.processing_error.is_retryable()
573 }
574}
575
576#[derive(Debug)]
577pub(crate) struct PublishIntentData {
578 pub(crate) staged_commit: Option<Vec<u8>>,
579 pub(crate) post_commit_action: Option<Vec<u8>>,
580 pub(crate) payloads_to_publish: Vec<Vec<u8>>,
583 pub(crate) should_send_push_notification: bool,
584 pub(crate) group_epoch: u64,
585}
586
587#[cfg(any(test, feature = "test-utils"))]
588impl PublishIntentData {
589 #[allow(dead_code)]
590 pub fn post_commit_data(&self) -> Option<Vec<u8>> {
591 self.post_commit_action.clone()
592 }
593
594 #[allow(dead_code)]
595 pub fn staged_commit(&self) -> Option<Vec<u8>> {
596 self.staged_commit.clone()
597 }
598}
599
600#[derive(Debug)]
602pub(crate) struct ProcessedMessageOutcome {
603 pub(crate) group_active: bool,
606 pub(crate) app_data_change: Option<AppDataChange>,
615}
616
617impl ProcessedMessageOutcome {
618 fn new(group_active: bool) -> Self {
620 Self {
621 group_active,
622 app_data_change: None,
623 }
624 }
625}
626
627#[cfg(test)]
628pub(crate) mod tests {
629
630 use super::*;
631 use crate::{builder::ClientBuilder, utils::TestMlsGroup};
632 use std::sync::Arc;
633 use xmtp_cryptography::utils::generate_local_wallet;
634
635 #[xmtp_common::test(unwrap_try = true)]
636 async fn publish_stores_envelope_metadata_without_sync() {
637 use crate::tester;
638 use xmtp_proto::types::Topic;
639
640 tester!(alix, disable_workers);
641 let group = alix.create_group(None, None)?;
643 group.key_update().await?;
644 let id = group.send_message_optimistic(b"publish metadata", Default::default())?;
645 group.publish_intents().await?;
646 let published = alix.context.db().find_group_intents(
647 group.group_id,
648 Some(vec![IntentState::Published]),
649 Some(vec![IntentKind::SendMessage]),
650 )?;
651 assert_eq!(published.len(), 1);
652
653 let stored: StoredGroupMessage = alix.context.db().fetch(&id)?.unwrap();
655 assert_eq!(stored.sequence_id, 0);
656 assert!(
657 stored.envelope_hash.is_some(),
658 "publish left envelope_hash NULL"
659 );
660 assert!(stored.expiry_ns.is_some(), "publish left expiry_ns NULL");
661
662 let envelopes = alix
664 .context
665 .api()
666 .query_all(
667 [(Topic::new_group_message(group.group_id), Cursor(0))].into(),
668 alix.context.api().limits().max_query_limit as u32,
669 )
670 .await?;
671 let envelope = envelopes.last().unwrap();
672 let canonical = xmtp_mls_validation::parse_envelope(envelope.envelope.clone().unwrap())?;
673 assert_eq!(
674 stored.envelope_hash,
675 Some(canonical.canonical.hash.to_vec())
676 );
677 assert_eq!(
678 stored.expiry_ns,
679 Some(i64::try_from(envelope.meta.as_ref().unwrap().expiry_ns)?)
680 );
681 }
682
683 #[xmtp_common::test(unwrap_try = true)]
684 async fn partial_envelope_metadata_preserves_stored_fields() {
685 use crate::tester;
686 use xmtp_proto::types::Topic;
687
688 tester!(alix, disable_workers);
689 let group = alix.create_group(None, None)?;
690 let id = group
691 .send_message(b"partial metadata", Default::default())
692 .await?;
693 let original: StoredGroupMessage = alix.context.db().fetch(&id)?.unwrap();
694 assert!(original.envelope_hash.is_some());
695 assert!(original.expiry_ns.is_some());
696 let mut envelopes = alix
697 .context
698 .api()
699 .query_all(
700 [(Topic::new_group_message(group.group_id), Cursor(0))].into(),
701 alix.context.api().limits().max_query_limit as u32,
702 )
703 .await?;
704 let mut envelope =
705 xmtp_api_backend::envelope::decode_group_message(envelopes.pop().unwrap())?;
706 envelope.envelope_hash = None;
707 TestMlsGroup::save_envelope_metadata_with_db(&alix.context.db(), &envelope)?;
708 let stored: StoredGroupMessage = alix.context.db().fetch(&id)?.unwrap();
709 assert_eq!(stored.envelope_hash, original.envelope_hash);
710 assert_eq!(stored.expiry_ns, original.expiry_ns);
711
712 envelope.envelope_hash = original.envelope_hash.clone();
713 envelope.expiry_ns = None;
714 TestMlsGroup::save_envelope_metadata_with_db(&alix.context.db(), &envelope)?;
715 let stored: StoredGroupMessage = alix.context.db().fetch(&id)?.unwrap();
716 assert_eq!(stored.envelope_hash, original.envelope_hash);
717 assert_eq!(stored.expiry_ns, original.expiry_ns);
718 }
719
720 #[cfg_attr(
722 not(target_arch = "wasm32"),
723 tokio::test(flavor = "multi_thread", worker_threads = 10)
724 )]
725 #[cfg(not(target_family = "wasm"))]
726 async fn publish_intents_worst_case_scenario() {
727 use crate::tester;
728
729 tester!(amal_a, triggers);
730 let amal_group_a: Arc<MlsGroup<_>> =
731 Arc::new(amal_a.create_group(None, Default::default()).unwrap());
732
733 let db = amal_a.context.db();
734
735 amal_group_a.sync().await.unwrap();
737 assert_eq!(db.intents_processed(), 1);
738
739 for _ in 0..100 {
740 use crate::groups::send_message_opts::SendMessageOpts;
741
742 let s = xmtp_common::rand_string::<100>();
743 amal_group_a
744 .send_message_optimistic(s.as_bytes(), SendMessageOpts::default())
745 .unwrap();
746 }
747
748 let mut set = tokio::task::JoinSet::new();
749 for _ in 0..50 {
750 let g = amal_group_a.clone();
751 set.spawn(async move { g.publish_intents().await });
752 }
753
754 let res = set.join_all().await;
755 let errs: Vec<&Result<_, _>> = res.iter().filter(|r| r.is_err()).collect();
756 errs.iter().for_each(|e| {
757 tracing::error!("{}", e.as_ref().unwrap_err());
758 });
759
760 let published = db.intents_published();
761 assert_eq!(published, 101);
762 let created = db.intents_created();
763 assert_eq!(created, 101);
764 if !errs.is_empty() {
765 panic!("Errors during publish");
766 }
767 }
768
769 #[xmtp_common::test]
770 async fn hmac_keys_work_as_expected() {
771 let wallet = generate_local_wallet();
772 let amal = Arc::new(ClientBuilder::new_test_client(&wallet).await);
773 let amal_group: Arc<TestMlsGroup> =
774 Arc::new(amal.create_group(None, Default::default()).unwrap());
775
776 let hmac_keys = amal_group.hmac_keys(-1..=1).unwrap();
777 let current_hmac_key = amal_group.hmac_keys(0..=0).unwrap().pop().unwrap();
778 assert_eq!(hmac_keys.len(), 3);
779 assert_eq!(hmac_keys[1].key, current_hmac_key.key);
780 assert_eq!(hmac_keys[1].epoch, current_hmac_key.epoch);
781
782 assert_ne!(hmac_keys[0].key, hmac_keys[1].key);
784 assert_ne!(hmac_keys[0].key, hmac_keys[2].key);
785 assert_ne!(hmac_keys[1].key, hmac_keys[2].key);
786
787 let current_epoch = hmac_epoch();
789 assert_eq!(hmac_keys[0].epoch, current_epoch - 1);
790 assert_eq!(hmac_keys[1].epoch, current_epoch);
791 assert_eq!(hmac_keys[2].epoch, current_epoch + 1);
792 }
793
794 #[xmtp_common::test(unwrap_try = true)]
798 async fn test_process_delete_message_malformed_encoded_content() {
799 use crate::tester;
800 use xmtp_db::group_message::{ContentType, DeliveryStatus, GroupMessageKind};
801
802 tester!(alix);
803 let alix_group = alix.create_group(None, None)?;
804
805 let malformed_message = xmtp_db::group_message::StoredGroupMessage {
807 id: vec![1, 2, 3],
808 group_id: alix_group.group_id,
809 decrypted_message_bytes: vec![0xFF, 0xFE, 0xFD], sent_at_ns: xmtp_common::time::now_ns(),
811 kind: GroupMessageKind::Application,
812 sender_installation_id: vec![1, 2, 3],
813 sender_inbox_id: alix.inbox_id().to_string(),
814 delivery_status: DeliveryStatus::Published,
815 content_type: ContentType::DeleteMessage,
816 version_major: 1,
817 version_minor: 0,
818 authority_id: "xmtp.org".to_string(),
819 reference_id: None,
820 expire_at_ns: None,
821 sequence_id: 1,
822 envelope_hash: None,
823 expiry_ns: None,
824 inserted_at_ns: 0,
825 should_push: false,
826 idempotency_key: String::new(),
827 };
828
829 let storage = alix.context.mls_storage();
831 let result: Result<(), crate::groups::GroupError> =
832 alix_group.load_mls_group_with_lock(storage, |mls_group| {
833 let inner_result = alix_group.process_delete_message(
834 &mls_group,
835 storage,
836 &malformed_message,
837 alix_group.context.events(),
838 );
839 match inner_result {
840 Ok(()) => Ok(()),
841 Err(_) => Err(crate::groups::GroupError::InvalidGroupMembership),
842 }
843 });
844
845 assert!(
846 result.is_ok(),
847 "Malformed EncodedContent should not cause error"
848 );
849 }
850
851 #[xmtp_common::test(unwrap_try = true)]
853 async fn test_process_delete_message_malformed_inner_proto() {
854 use crate::tester;
855 use prost::Message;
856 use xmtp_db::group_message::{ContentType, DeliveryStatus, GroupMessageKind};
857 use xmtp_proto::xmtp::mls::message_contents::EncodedContent;
858
859 tester!(alix);
860 let alix_group = alix.create_group(None, None)?;
861
862 let encoded_content = EncodedContent {
864 r#type: Some(xmtp_proto::xmtp::mls::message_contents::ContentTypeId {
865 authority_id: "xmtp.org".to_string(),
866 type_id: "deleteMessage".to_string(),
867 version_major: 1,
868 version_minor: 0,
869 }),
870 parameters: std::collections::HashMap::new(),
871 fallback: None,
872 compression: None,
873 content: vec![0xFF, 0xFE, 0xFD], };
875
876 let mut encoded_bytes = Vec::new();
877 encoded_content.encode(&mut encoded_bytes)?;
878
879 let malformed_message = xmtp_db::group_message::StoredGroupMessage {
880 id: vec![4, 5, 6],
881 group_id: alix_group.group_id,
882 decrypted_message_bytes: encoded_bytes,
883 sent_at_ns: xmtp_common::time::now_ns(),
884 kind: GroupMessageKind::Application,
885 sender_installation_id: vec![1, 2, 3],
886 sender_inbox_id: alix.inbox_id().to_string(),
887 delivery_status: DeliveryStatus::Published,
888 content_type: ContentType::DeleteMessage,
889 version_major: 1,
890 version_minor: 0,
891 authority_id: "xmtp.org".to_string(),
892 reference_id: None,
893 expire_at_ns: None,
894 sequence_id: 2,
895 envelope_hash: None,
896 expiry_ns: None,
897 inserted_at_ns: 0,
898 should_push: false,
899 idempotency_key: String::new(),
900 };
901
902 let storage = alix.context.mls_storage();
903 let result: Result<(), crate::groups::GroupError> =
904 alix_group.load_mls_group_with_lock(storage, |mls_group| {
905 let inner_result = alix_group.process_delete_message(
906 &mls_group,
907 storage,
908 &malformed_message,
909 alix_group.context.events(),
910 );
911 match inner_result {
912 Ok(()) => Ok(()),
913 Err(_) => Err(crate::groups::GroupError::InvalidGroupMembership),
914 }
915 });
916
917 assert!(
918 result.is_ok(),
919 "Malformed inner DeleteMessage proto should not cause error"
920 );
921 }
922
923 #[xmtp_common::test(unwrap_try = true)]
925 async fn test_process_delete_message_invalid_hex_message_id() {
926 use crate::tester;
927 use prost::Message;
928 use xmtp_db::group_message::{ContentType, DeliveryStatus, GroupMessageKind};
929 use xmtp_proto::xmtp::mls::message_contents::EncodedContent;
930 use xmtp_proto::xmtp::mls::message_contents::content_types::DeleteMessage;
931
932 tester!(alix);
933 let alix_group = alix.create_group(None, None)?;
934
935 let delete_msg = DeleteMessage {
937 message_id: "not_valid_hex!!!".to_string(), };
939
940 let mut delete_bytes = Vec::new();
941 delete_msg.encode(&mut delete_bytes)?;
942
943 let encoded_content = EncodedContent {
944 r#type: Some(xmtp_proto::xmtp::mls::message_contents::ContentTypeId {
945 authority_id: "xmtp.org".to_string(),
946 type_id: "deleteMessage".to_string(),
947 version_major: 1,
948 version_minor: 0,
949 }),
950 parameters: std::collections::HashMap::new(),
951 fallback: None,
952 compression: None,
953 content: delete_bytes,
954 };
955
956 let mut encoded_bytes = Vec::new();
957 encoded_content.encode(&mut encoded_bytes)?;
958
959 let message_with_bad_hex = xmtp_db::group_message::StoredGroupMessage {
960 id: vec![7, 8, 9],
961 group_id: alix_group.group_id,
962 decrypted_message_bytes: encoded_bytes,
963 sent_at_ns: xmtp_common::time::now_ns(),
964 kind: GroupMessageKind::Application,
965 sender_installation_id: vec![1, 2, 3],
966 sender_inbox_id: alix.inbox_id().to_string(),
967 delivery_status: DeliveryStatus::Published,
968 content_type: ContentType::DeleteMessage,
969 version_major: 1,
970 version_minor: 0,
971 authority_id: "xmtp.org".to_string(),
972 reference_id: None,
973 expire_at_ns: None,
974 sequence_id: 3,
975 envelope_hash: None,
976 expiry_ns: None,
977 inserted_at_ns: 0,
978 should_push: false,
979 idempotency_key: String::new(),
980 };
981
982 let storage = alix.context.mls_storage();
983 let result: Result<(), crate::groups::GroupError> =
984 alix_group.load_mls_group_with_lock(storage, |mls_group| {
985 let inner_result = alix_group.process_delete_message(
986 &mls_group,
987 storage,
988 &message_with_bad_hex,
989 alix_group.context.events(),
990 );
991 match inner_result {
992 Ok(()) => Ok(()),
993 Err(_) => Err(crate::groups::GroupError::InvalidGroupMembership),
994 }
995 });
996
997 assert!(
998 result.is_ok(),
999 "Invalid hex message_id should not cause error"
1000 );
1001 }
1002
1003 #[test]
1010 fn process_message_with_app_data_error_commit_result_mapping() {
1011 use super::super::app_data::ProcessMessageWithAppDataError;
1012 use openmls::group::ValidationError;
1013 use xmtp_mls_common::app_data::component_source::ComponentSourceError;
1014
1015 let wrong_epoch = GroupMessageProcessingError::OpenMlsProcessMessageWithAppData(
1016 ProcessMessageWithAppDataError::OpenMls(ProcessMessageError::ValidationError(
1017 ValidationError::WrongEpoch,
1018 )),
1019 );
1020 assert_eq!(wrong_epoch.commit_result(), CommitResult::WrongEpoch);
1021
1022 let other_openmls = GroupMessageProcessingError::OpenMlsProcessMessageWithAppData(
1023 ProcessMessageWithAppDataError::OpenMls(ProcessMessageError::IncompatibleWireFormat),
1024 );
1025 assert_eq!(other_openmls.commit_result(), CommitResult::Undecryptable);
1026
1027 let decode_failure = GroupMessageProcessingError::OpenMlsProcessMessageWithAppData(
1031 ProcessMessageWithAppDataError::AppDataDecode(ComponentSourceError::UnknownComponent(
1032 xmtp_mls_common::app_data::component_id::ComponentId::from(0u16),
1033 )),
1034 );
1035 assert_eq!(decode_failure.commit_result(), CommitResult::Invalid);
1036 assert!(
1037 !decode_failure.is_retryable(),
1038 "wire-format violations must not be retriable"
1039 );
1040 }
1041}