xmtp_mls/groups/mls_sync/
receive.rs1use super::*;
4use crate::messages::decoded_message::DecodedMessage;
5use xmtp_db::group_message::SortBy;
6use xmtp_db::prelude::QueryGroupMessage;
7
8const PAGE_SIZE: i64 = 100;
10
11impl<Context> MlsGroup<Context>
12where
13 Context: XmtpSharedContext,
14{
15 #[tracing::instrument(skip_all, level = "trace")]
17 pub async fn receive(&self) -> Result<ProcessSummary, GroupError> {
18 use xmtp_db::delivery::{DeliveryScope, QueryDelivery};
19 let db = self.context.db();
20 if db
21 .find_group(&self.group_id)?
22 .is_some_and(|group| group.membership_state == GroupMembershipState::Restored)
23 {
24 return Err(GroupError::GroupInactive);
25 }
26 let mut position = db.current_delivery_cursor()?;
27 crate::subscriptions::barrier::receive_through_current(
28 &self.context,
29 vec![xmtp_proto::types::Topic::new_group_message(self.group_id)],
30 )
31 .await?;
32 let upper = db.current_delivery_cursor()?;
33 let settings = self.context.incoming_runtime().policy();
34 let mut summary = ProcessSummary::default();
35 loop {
36 let rows = db.replay_delivery_messages_bounded(
37 position,
38 &DeliveryScope::Groups(vec![self.group_id]),
39 xmtp_common::time::now_ns(),
40 settings.max_local_read_rows,
41 settings.max_local_read_bytes,
42 )?;
43 if rows.is_empty() {
44 break;
45 }
46 let mut reached_upper = false;
47 for row in rows {
48 if row.cursor.delivery_sequence > upper.delivery_sequence {
49 reached_upper = true;
50 break;
51 }
52 position = row.cursor;
53 let message = row.message;
54 summary.add_id(message.cursor());
55 summary.add(MessageIdentifier {
56 cursor: message.cursor(),
57 group_id: message.group_id,
58 created_ns: chrono::DateTime::from_timestamp_nanos(message.sent_at_ns),
59 previously_processed: false,
60 internal_id: Some(message.id),
61 group_context: None,
62 intent_kind: None,
63 });
64 }
65 if reached_upper || position.delivery_sequence >= upper.delivery_sequence {
66 break;
67 }
68 }
69 Ok(summary)
70 }
71
72 #[tracing::instrument(skip_all, level = "trace")]
73 pub(super) fn maybe_update_cursor(
74 &self,
75 db: &impl DbQuery,
76 message: &xmtp_proto::types::GroupMessage,
77 ) -> Result<bool, StorageError> {
78 let updated = db.update_cursor(
79 message.group_id,
80 xmtp_db::refresh_state::EntityKind::ApplicationMessage,
81 message.cursor,
82 )?;
83 if updated {
84 log_event!(
85 Event::GroupCursorUpdate,
86 self.context.installation_id(),
87 group_id = message.group_id.as_slice(),
88 cursor = message.cursor.0,
89 );
90 } else {
91 tracing::debug!("no cursor update required");
92 }
93 Ok(updated)
94 }
95
96 pub(super) fn save_transcript_message(
98 &self,
99 validated_commit: ValidatedCommit,
100 timestamp_ns: u64,
101 cursor: Cursor,
102 storage: &impl XmtpMlsStorageProvider,
103 ) -> Result<Option<(StoredGroupMessage, GroupUpdated)>, GroupMessageProcessingError> {
104 if validated_commit.is_empty() {
105 return Ok(None);
106 }
107 let sender_installation_id = validated_commit.actor_installation_id();
108 let sender_inbox_id = validated_commit.actor_inbox_id();
109
110 let pending_remove_users = &storage.db().get_pending_remove_users(&self.group_id)?;
111 let payload: GroupUpdated = validated_commit.into_with(pending_remove_users);
112 tracing::info!("Storing transcript message");
113 let encoded_payload = GroupUpdatedCodec::encode(payload.clone())?;
114 let mut encoded_payload_bytes = Vec::new();
115 encoded_payload.encode(&mut encoded_payload_bytes)?;
116
117 let message_id = calculate_message_id(
118 self.group_id,
119 encoded_payload_bytes.as_slice(),
120 ×tamp_ns.to_string(),
121 );
122 let content_type = encoded_payload.r#type.unwrap_or_else(|| {
123 tracing::warn!("Missing content type in encoded payload, using default values");
124 xmtp_proto::xmtp::mls::message_contents::ContentTypeId {
126 authority_id: "unknown".to_string(),
127 type_id: "unknown".to_string(),
128 version_major: 0,
129 version_minor: 0,
130 }
131 });
132
133 self.handle_metadata_update_from_commit(&payload.metadata_field_changes, storage)?;
134
135 if self.update_already_exists(&payload, storage)? {
137 return Ok(None);
138 }
139
140 let msg = StoredGroupMessage {
141 id: message_id,
142 group_id: self.group_id,
143 decrypted_message_bytes: encoded_payload_bytes,
144 sent_at_ns: timestamp_ns as i64,
145 kind: GroupMessageKind::MembershipChange,
146 sender_installation_id,
147 sender_inbox_id,
148 delivery_status: DeliveryStatus::Published,
149 content_type: ContentType::from_identifier(
150 &content_type.authority_id,
151 &content_type.type_id,
152 content_type.version_major,
153 ),
154 version_major: content_type.version_major as i32,
155 version_minor: content_type.version_minor as i32,
156 authority_id: content_type.authority_id.to_string(),
157 reference_id: None,
158 sequence_id: cursor.0 as i64,
159 envelope_hash: None,
160 expiry_ns: None,
161 expire_at_ns: None,
162 inserted_at_ns: 0, should_push: true,
164 idempotency_key: timestamp_ns.to_string(),
166 };
167
168 msg.store_or_ignore(&storage.db())?;
169 Ok(Some((msg, payload)))
170 }
171
172 fn update_already_exists(
173 &self,
174 payload: &GroupUpdated,
175 storage: &impl XmtpMlsStorageProvider,
176 ) -> Result<bool, GroupMessageProcessingError> {
177 if self.dm_id.is_none() || payload.added_inboxes.is_empty() {
178 return Ok(false);
181 }
182
183 let mut deduper = GroupUpdateDeduper::default();
184 let mut inserted_after_ns = None;
185 let mut limit = PAGE_SIZE;
188 loop {
189 let rows = storage.db().get_group_messages(
205 &self.group_id,
206 &MsgQueryArgs {
207 content_types: Some(vec![ContentType::GroupUpdated]),
208 inserted_after_ns,
209 limit: Some(limit),
210 sort_by: Some(SortBy::InsertedAt),
211 ..Default::default()
212 },
213 )?;
214
215 let full_page = rows.len() >= limit as usize;
216 let last_value = rows.last().map(|row| row.inserted_at_ns);
217
218 let cutoff = last_value.filter(|_| full_page);
225 let consumed = rows
226 .into_iter()
227 .filter(|row| Some(row.inserted_at_ns) != cutoff);
228
229 let mut highest_consumed = None;
230 for row in consumed {
231 highest_consumed = Some(row.inserted_at_ns);
232 let MessageBody::GroupUpdated(update) = DecodedMessage::from(row).content else {
233 continue;
234 };
235
236 deduper.consume(&update);
237 }
238
239 match highest_consumed {
240 Some(value) => {
242 inserted_after_ns = Some(value);
243 limit = PAGE_SIZE;
244 }
245 None if full_page => limit = limit.saturating_mul(2),
248 None => break,
249 }
250 }
251
252 Ok(deduper.is_dupe(payload))
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259 use crate::tester;
260 use xmtp_common::{rand_vec, time::now_ns};
261 use xmtp_content_types::{ContentCodec, encryption::sha256, group_updated::GroupUpdatedCodec};
262 use xmtp_db::diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
263 use xmtp_db::group_message::{DeliveryStatus, GroupMessageKind};
264 use xmtp_db::user_preferences::StoredUserPreferences;
265 use xmtp_db::{ConnectionExt, DbConnection, Store};
266 use xmtp_proto::types::GroupId;
267 use xmtp_proto::xmtp::mls::message_contents::group_updated::Inbox;
268
269 const SEEDED_SENDER: &str = "seeded";
271
272 fn add_update(inbox_id: &str) -> GroupUpdated {
273 GroupUpdated {
274 added_inboxes: vec![Inbox {
275 inbox_id: inbox_id.to_string(),
276 }],
277 ..Default::default()
278 }
279 }
280
281 fn encode(update: &GroupUpdated) -> Vec<u8> {
282 let mut bytes = Vec::new();
283 GroupUpdatedCodec::encode(update.clone())
284 .unwrap()
285 .encode(&mut bytes)
286 .unwrap();
287 bytes
288 }
289
290 fn seed_tied_updates<C: ConnectionExt>(
294 db: &DbConnection<C>,
295 group_id: GroupId,
296 rows: Vec<Vec<u8>>,
297 ) {
298 for (i, bytes) in rows.into_iter().enumerate() {
299 StoredGroupMessage {
300 id: sha256(&rand_vec::<12>()),
301 group_id,
302 decrypted_message_bytes: bytes,
303 sent_at_ns: now_ns(),
304 kind: GroupMessageKind::MembershipChange,
305 sender_installation_id: vec![1, 2, 3],
306 sender_inbox_id: SEEDED_SENDER.to_string(),
307 delivery_status: DeliveryStatus::Published,
308 content_type: ContentType::GroupUpdated,
309 version_major: 0,
310 version_minor: 0,
311 authority_id: "unknown".to_string(),
312 reference_id: None,
313 sequence_id: i as i64 + 1,
314 envelope_hash: None,
315 expiry_ns: None,
316 expire_at_ns: None,
317 inserted_at_ns: 0,
318 should_push: true,
319 idempotency_key: String::new(),
320 }
321 .store(db)
322 .unwrap();
323 }
324
325 db.raw_query(|conn| {
330 use xmtp_db::schema::group_messages::dsl;
331 let newest: Option<i64> = dsl::group_messages
332 .filter(dsl::group_id.eq(group_id))
333 .select(xmtp_db::diesel::dsl::max(dsl::inserted_at_ns))
334 .first(conn)?;
335 let shared_inserted_at_ns = newest.unwrap_or_default() + 1_000_000;
336 xmtp_db::diesel::update(dsl::group_messages)
337 .filter(dsl::group_id.eq(group_id))
338 .filter(dsl::sender_inbox_id.eq(SEEDED_SENDER))
339 .set(dsl::inserted_at_ns.eq(shared_inserted_at_ns))
340 .execute(conn)
341 })
342 .unwrap();
343 }
344
345 async fn wait_for_startup_cleanup<C: ConnectionExt>(db: DbConnection<C>) {
348 xmtp_common::wait_for_eq(
349 || async {
350 StoredUserPreferences::load(&db)
351 .unwrap()
352 .dm_group_updates_migrated
353 },
354 true,
355 )
356 .await
357 .unwrap();
358 }
359
360 #[xmtp_common::test(unwrap_try = true)]
365 async fn test_update_already_exists_spans_rows_sharing_one_inserted_at() {
366 tester!(alix);
367 tester!(bo);
368 let (dm, _) = alix.test_talk_in_dm_with(&bo).await?;
369 wait_for_startup_cleanup(alix.db()).await;
370
371 let earlier = add_update("earlier");
372 let latest = add_update("latest");
373 let mut rows = vec![encode(&earlier); PAGE_SIZE as usize];
374 rows.extend(vec![encode(&latest); 25]);
375 seed_tied_updates(&alix.db(), dm.group_id, rows);
376
377 let storage = alix.context.mls_storage();
378 assert!(
379 dm.update_already_exists(&latest, storage)?,
380 "the newest add sits past the page edge and must still be seen"
381 );
382 assert!(
383 !dm.update_already_exists(&earlier, storage)?,
384 "an add that is no longer the newest is not a duplicate"
385 );
386 }
387
388 #[xmtp_common::test(unwrap_try = true)]
393 async fn test_update_already_exists_counts_undecodable_rows_toward_the_page() {
394 tester!(alix);
395 tester!(bo);
396 let (dm, _) = alix.test_talk_in_dm_with(&bo).await?;
397 wait_for_startup_cleanup(alix.db()).await;
398
399 let earlier = add_update("earlier");
400 let latest = add_update("latest");
401 let mut rows = vec![vec![0xFF, 0xFE, 0xFD]];
403 rows.extend(vec![encode(&earlier); PAGE_SIZE as usize - 1]);
404 rows.extend(vec![encode(&latest); 25]);
405 seed_tied_updates(&alix.db(), dm.group_id, rows);
406
407 let storage = alix.context.mls_storage();
408 assert!(
409 dm.update_already_exists(&latest, storage)?,
410 "an undecodable row must not shorten the page and hide the rows past its edge"
411 );
412 }
413}