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