xmtp_mls/groups/mls_sync/
post_commit.rs1use super::*;
4
5impl<Context> MlsGroup<Context>
6where
7 Context: XmtpSharedContext,
8{
9 #[tracing::instrument(skip_all)]
10 pub(crate) async fn post_commit(&self) -> Result<(), GroupError> {
11 self.publish_required_welcomes().await
12 }
13
14 pub async fn maybe_update_installations(
15 &self,
16 update_interval_ns: Option<i64>,
17 ) -> Result<(), GroupError> {
18 let db = self.context.db();
19 let Some(stored_group) = db.find_group(&self.group_id)? else {
20 return Err(GroupError::NotFound(NotFound::GroupById(self.group_id)));
21 };
22 if stored_group.conversation_type.is_virtual() {
23 return Ok(());
24 }
25
26 let interval_ns = update_interval_ns.unwrap_or(SYNC_UPDATE_INSTALLATIONS_INTERVAL_NS);
28
29 let now_ns = xmtp_common::time::now_ns();
30 let last_ns = db.get_installations_time_checked(&self.group_id)?;
31 let elapsed_ns = now_ns - last_ns;
32 if elapsed_ns > interval_ns && self.is_active()? {
33 self.add_missing_installations().await?;
34 db.update_installations_time_checked(&self.group_id)?;
35 }
36
37 Ok(())
38 }
39
40 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(level = "info", fields(inbox_id = %self.context.inbox_id()), skip_all))]
49 #[cfg_attr(
50 not(any(test, feature = "test-utils")),
51 tracing::instrument(level = "trace", skip_all)
52 )]
53 pub(crate) async fn add_missing_installations(&self) -> Result<(), GroupError> {
54 let intent_data = self.get_membership_update_intent(&[], &[]).await?;
55
56 if intent_data.is_empty() {
58 return Ok(());
59 }
60
61 debug!(
62 inbox_id = self.context.inbox_id(),
63 installation_id = %self.context.installation_id(),
64 "Adding missing installations {:?}",
65 intent_data
66 );
67
68 let intent = QueueIntent::update_group_membership()
69 .data(intent_data)
70 .queue(self)?;
71
72 let _ = self.sync_until_intent_resolved(intent.id).await?;
73 Ok(())
74 }
75
76 #[tracing::instrument(level = "trace", skip_all)]
77 pub(crate) async fn get_membership_update_intent(
84 &self,
85 inbox_ids_to_add: &[InboxIdRef<'_>],
86 inbox_ids_to_remove: &[InboxIdRef<'_>],
87 ) -> Result<UpdateGroupMembershipIntentData, GroupError> {
88 let existing_group_membership = self.with_group_snapshot(|group| {
89 extract_group_membership(group.extensions()).map_err(Into::into)
90 })?;
91 {
92 let mut inbox_ids = existing_group_membership.inbox_ids();
94 inbox_ids.extend_from_slice(inbox_ids_to_add);
95 let conn = self.context.db();
96 load_identity_updates(self.context.api(), &conn, &inbox_ids).await?;
98
99 let latest_sequence_id_map = conn.get_latest_sequence_id(&inbox_ids as &[&str])?;
100
101 let changed_inbox_ids =
103 inbox_ids
104 .iter()
105 .try_fold(HashMap::new(), |mut updates, inbox_id| {
106 match (
107 latest_sequence_id_map.get(inbox_id as &str),
108 existing_group_membership.get(inbox_id),
109 ) {
110 (Some(latest_sequence_id), Some(current_sequence_id)) => {
112 let latest_sequence_id_u64 = *latest_sequence_id as u64;
113 if latest_sequence_id_u64.gt(current_sequence_id) {
114 updates.insert(inbox_id.to_string(), latest_sequence_id_u64);
115 }
116 }
117 (Some(latest_sequence_id), None) => {
119 updates.insert(inbox_id.to_string(), *latest_sequence_id as u64);
121 }
122 (_, _) => {
123 tracing::warn!(
124 "Could not find existing sequence ID for inbox {}",
125 inbox_id
126 );
127 return Err(GroupError::MissingSequenceId);
128 }
129 }
130
131 Ok(updates)
132 })?;
133 let old_group_membership = existing_group_membership.clone();
134 let mut new_membership = old_group_membership.clone();
135 for (inbox_id, sequence_id) in changed_inbox_ids.iter() {
136 new_membership.add(inbox_id.clone(), *sequence_id);
137 }
138 for inbox_id in inbox_ids_to_remove {
139 new_membership.remove(inbox_id);
140 }
141
142 let changes_with_kps = calculate_membership_changes_with_keypackages(
143 &self.context,
144 &self.group_id,
145 &new_membership,
146 &old_group_membership,
147 )
148 .await?;
149
150 if !inbox_ids_to_add.is_empty()
153 && !changes_with_kps.failed_installations.is_empty()
154 && changes_with_kps.new_installations.is_empty()
155 {
156 return Err(GroupError::FailedToVerifyInstallations(
157 FailedInstallationIds(changes_with_kps.failed_installations.clone()),
158 ));
159 }
160
161 Ok(UpdateGroupMembershipIntentData::new(
162 changed_inbox_ids,
163 inbox_ids_to_remove
164 .iter()
165 .map(|s| s.to_string())
166 .collect::<Vec<String>>(),
167 changes_with_kps.failed_installations,
168 ))
169 }
170 }
171
172 #[cfg(test)]
173 pub(in crate::groups) async fn send_welcomes(
174 &self,
175 action: SendWelcomesAction,
176 message_cursor: Option<i64>,
177 ) -> Result<(), GroupError> {
178 let message_cursor = u64::try_from(message_cursor.unwrap_or(0))
179 .map_err(|_| xmtp_proto::ConversionError::Unspecified("negative Welcome cursor"))?;
180 let units = crate::state_tx::state_write(self.context.mls_storage(), |_tx| {
181 self.prepare_welcome_envelopes(action, message_cursor)?
182 .into_iter()
183 .map(PublishUnit::single)
184 .collect::<Result<Vec<_>, _>>()
185 .map(Continue)
186 .map_err(GroupError::from)
187 })?
188 .into_continued();
189 self.context.api().publish_units(units).await?;
190 Ok(())
191 }
192
193 #[tracing::instrument(level = "trace", skip_all)]
197 pub fn hmac_keys(
198 &self,
199 epoch_delta_range: RangeInclusive<i64>,
200 ) -> Result<Vec<HmacKey>, StorageError> {
201 crate::state_tx::state_write(self.context.mls_storage(), |tx| {
202 self.hmac_keys_in(tx.storage().db(), epoch_delta_range)
203 .map(Continue)
204 })
205 .map(TransactionOutcome::into_continued)
206 }
207
208 pub(crate) fn hmac_keys_in(
209 &self,
210 conn: impl xmtp_db::DbQuery,
211 epoch_delta_range: RangeInclusive<i64>,
212 ) -> Result<Vec<HmacKey>, StorageError> {
213 let preferences = StoredUserPreferences::load(&conn)?;
214 let mut ikm = match preferences.hmac_key {
215 Some(ikm) => ikm,
216 None => {
217 let key = HmacKey::random_key();
218 StoredUserPreferences::store_hmac_key(&conn, &key, None)?;
219 key
220 }
221 };
222 ikm.extend_from_slice(self.group_id.as_ref());
223 let hkdf = Hkdf::<Sha256>::new(Some(HMAC_SALT), &ikm);
224
225 let mut result = vec![];
226 let current_epoch = hmac_epoch();
227 for delta in epoch_delta_range {
228 let epoch = current_epoch + delta;
229
230 let mut info = self.group_id.to_vec();
231 info.extend(&epoch.to_le_bytes());
232
233 let mut key = [0; 42];
234 hkdf.expand(&info, &mut key).expect("Length is correct");
235
236 result.push(HmacKey { key, epoch });
237 }
238
239 Ok(result)
240 }
241
242 #[cfg(test)]
243 #[tracing::instrument(level = "trace", skip_all)]
244 pub(in crate::groups) fn prepare_group_messages(
245 &self,
246 payloads: Vec<(&[u8], bool)>,
247 ) -> Result<Vec<PublishUnit>, GroupError> {
248 crate::state_tx::state_write(self.context.mls_storage(), |tx| {
249 let envelopes = self.prepare_group_envelopes_in(tx.storage().db(), payloads)?;
250 Ok::<_, GroupError>(Continue(vec![PublishUnit::new(envelopes)?]))
251 })
252 .map(TransactionOutcome::into_continued)
253 }
254
255 pub(super) fn prepare_group_envelopes_in(
256 &self,
257 conn: impl xmtp_db::DbQuery,
258 payloads: Vec<(&[u8], bool)>,
259 ) -> Result<Vec<ClientEnvelope>, GroupError> {
260 let hmac_key = self
261 .hmac_keys_in(conn, 0..=0)?
262 .pop()
263 .expect("Range of count 1 was provided.");
264 let sender_hmac =
265 Hmac::<Sha256>::new_from_slice(&hmac_key.key).expect("HMAC can take key of any size");
266
267 let mut result = vec![];
268 for (payload, should_push) in payloads {
269 let mut sender_hmac = sender_hmac.clone();
270 sender_hmac.update(payload);
271 let sender_hmac = sender_hmac.finalize();
272
273 result.push(ClientEnvelope {
274 payload: Some(Payload::GroupMessage(BackendGroupMessage {
275 data: payload.to_vec(),
276 sender_hmac: sender_hmac.into_bytes().to_vec(),
277 should_push,
278 })),
279 });
280 }
281
282 Ok(result)
283 }
284}