xmtp_mls_common/app_data/components/
inbox_id_set.rs1use openmls::messages::proposals::AppDataUpdateOperation;
13use std::collections::HashMap;
14use tls_codec::{Deserialize, Serialize};
15use xmtp_proto::xmtp::mls::message_contents::ComponentType;
16
17use crate::{
18 app_data::{
19 component_id::ComponentId,
20 component_registry::ComponentOp,
21 typed::{Component, ComponentTypedError, ExpandedComponentChange},
22 },
23 inbox_id::InboxId,
24 tls_set::{TlsKeyHash, TlsSet, TlsSetDelta, TlsSetError, TlsSetMutation},
25};
26
27pub(crate) fn apply_inbox_id_set_delta(
42 payload: &[u8],
43 prior: Option<&[u8]>,
44) -> Result<Vec<u8>, ComponentTypedError> {
45 let delta = TlsSetDelta::<InboxId>::tls_deserialize_exact(payload)?;
46 let mut set: TlsSet<InboxId> = match prior {
47 Some(bytes) => TlsSet::<InboxId>::tls_deserialize_exact(bytes)?,
48 None => TlsSet::new(),
49 };
50 set.apply_delta(delta)?;
51 Ok(set.tls_serialize_detached()?)
52}
53
54pub(crate) fn expand_inbox_id_set_changes(
63 op: &AppDataUpdateOperation,
64 prior: Option<&[u8]>,
65) -> Result<Vec<ExpandedComponentChange>, ComponentTypedError> {
66 match op {
67 AppDataUpdateOperation::Remove => Ok(vec![ExpandedComponentChange {
68 op: ComponentOp::Delete,
69 value: None,
70 }]),
71 AppDataUpdateOperation::Update(payload) => {
72 let delta = TlsSetDelta::<InboxId>::tls_deserialize_exact(payload.as_slice())?;
73
74 let needs_index = delta
90 .mutations
91 .iter()
92 .any(|m| matches!(m, TlsSetMutation::RemoveByHash(_)));
93 let hash_index: Option<HashMap<TlsKeyHash, InboxId>> = if needs_index {
94 match prior {
95 Some(bytes) => {
96 let prior_set = TlsSet::<InboxId>::tls_deserialize_exact(bytes)?;
97 let mut idx = HashMap::with_capacity(prior_set.len());
98 for key in prior_set.iter() {
99 if idx.insert(TlsKeyHash::of(key)?, *key).is_some() {
112 return Err(ComponentTypedError::TlsSetApply(
113 TlsSetError::DuplicateHash,
114 ));
115 }
116 }
117 Some(idx)
118 }
119 None => None,
123 }
124 } else {
125 None
126 };
127
128 let mut out = Vec::with_capacity(delta.mutations.len());
129 for mutation in delta.mutations {
130 match mutation {
131 TlsSetMutation::Insert(key) => out.push(ExpandedComponentChange {
132 op: ComponentOp::Insert,
133 value: Some(key.into_bytes().to_vec()),
134 }),
135 TlsSetMutation::Remove(key) => out.push(ExpandedComponentChange {
136 op: ComponentOp::Delete,
137 value: Some(key.into_bytes().to_vec()),
138 }),
139 TlsSetMutation::RemoveByHash(target) => {
140 let resolved = hash_index
141 .as_ref()
142 .and_then(|idx| idx.get(&target))
143 .map(|id| id.as_bytes().to_vec());
144 out.push(ExpandedComponentChange {
145 op: ComponentOp::Delete,
146 value: resolved,
147 });
148 }
149 }
150 }
151 Ok(out)
152 }
153 }
154}
155
156macro_rules! inbox_id_set_component {
157 ($struct_name:ident, $id:expr) => {
158 pub struct $struct_name;
159
160 impl Component for $struct_name {
161 const ID: ComponentId = $id;
162 const COMPONENT_TYPE: ComponentType = ComponentType::TlsSetInboxId;
163 type Value = TlsSet<InboxId>;
164 type Mutation = TlsSetDelta<InboxId>;
169
170 fn decode_value(bytes: &[u8]) -> Result<Self::Value, ComponentTypedError> {
171 TlsSet::<InboxId>::tls_deserialize_exact(bytes).map_err(Into::into)
172 }
173
174 fn encode_value(value: &Self::Value) -> Result<Vec<u8>, ComponentTypedError> {
175 value.tls_serialize_detached().map_err(Into::into)
176 }
177
178 fn encode_mutation(mutation: &Self::Mutation) -> Result<Vec<u8>, ComponentTypedError> {
179 mutation.tls_serialize_detached().map_err(Into::into)
180 }
181
182 fn apply_update_payload(
183 payload: &[u8],
184 prior: Option<&[u8]>,
185 ) -> Result<Vec<u8>, ComponentTypedError> {
186 apply_inbox_id_set_delta(payload, prior)
187 }
188
189 fn expand_to_changes(
190 op: &AppDataUpdateOperation,
191 prior: Option<&[u8]>,
192 ) -> Result<Vec<ExpandedComponentChange>, ComponentTypedError> {
193 expand_inbox_id_set_changes(op, prior)
194 }
195 }
196 };
197}
198
199inbox_id_set_component!(AdminListComponent, ComponentId::ADMIN_LIST);
200inbox_id_set_component!(SuperAdminListComponent, ComponentId::SUPER_ADMIN_LIST);
201inbox_id_set_component!(DmMembersComponent, ComponentId::DM_MEMBERS);
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 fn fixture_inbox_id(seed: u8) -> InboxId {
208 let mut bytes = [0u8; 32];
209 bytes[0] = seed;
210 InboxId::from_bytes(bytes)
211 }
212
213 #[xmtp_common::test(unwrap_try = true)]
214 fn round_trip_admin_list_value() {
215 let mut set = TlsSet::<InboxId>::new();
216 set.insert(fixture_inbox_id(1)).unwrap();
217 set.insert(fixture_inbox_id(2)).unwrap();
218 let bytes = AdminListComponent::encode_value(&set).unwrap();
219 let decoded = AdminListComponent::decode_value(&bytes).unwrap();
220 assert_eq!(decoded.len(), 2);
221 assert!(decoded.contains(&fixture_inbox_id(1)));
222 assert!(decoded.contains(&fixture_inbox_id(2)));
223 }
224
225 #[xmtp_common::test(unwrap_try = true)]
226 fn encode_mutation_serializes_full_delta() {
227 let delta = TlsSetDelta::<InboxId>::new().insert(fixture_inbox_id(7));
228 let bytes = AdminListComponent::encode_mutation(&delta).unwrap();
229 let round_trip = TlsSetDelta::<InboxId>::tls_deserialize_exact(&bytes).unwrap();
230 assert_eq!(round_trip.mutations.len(), 1);
231 match &round_trip.mutations[0] {
232 TlsSetMutation::Insert(id) => assert_eq!(*id, fixture_inbox_id(7)),
233 other => panic!("unexpected mutation: {other:?}"),
234 }
235 }
236
237 #[xmtp_common::test(unwrap_try = true)]
238 fn encode_mutation_supports_batched_delta() {
239 let delta = TlsSetDelta::<InboxId>::new()
243 .insert(fixture_inbox_id(1))
244 .insert(fixture_inbox_id(2))
245 .remove(fixture_inbox_id(3));
246 let bytes = AdminListComponent::encode_mutation(&delta).unwrap();
247 let round_trip = TlsSetDelta::<InboxId>::tls_deserialize_exact(&bytes).unwrap();
248 assert_eq!(round_trip.mutations.len(), 3);
249 }
250
251 #[xmtp_common::test(unwrap_try = true)]
252 fn apply_insert_against_empty_prior() {
253 let delta = TlsSetDelta::<InboxId>::new().insert(fixture_inbox_id(3));
254 let payload = AdminListComponent::encode_mutation(&delta).unwrap();
255 let new_bytes = AdminListComponent::apply_update_payload(&payload, None).unwrap();
256 let new = AdminListComponent::decode_value(&new_bytes).unwrap();
257 assert_eq!(new.len(), 1);
258 assert!(new.contains(&fixture_inbox_id(3)));
259 }
260
261 #[xmtp_common::test(unwrap_try = true)]
262 fn apply_remove_against_existing_prior() {
263 let mut prior_set = TlsSet::<InboxId>::new();
265 prior_set.insert(fixture_inbox_id(1)).unwrap();
266 prior_set.insert(fixture_inbox_id(2)).unwrap();
267 let prior_bytes = SuperAdminListComponent::encode_value(&prior_set).unwrap();
268
269 let delta = TlsSetDelta::<InboxId>::new().remove(fixture_inbox_id(1));
271 let payload = SuperAdminListComponent::encode_mutation(&delta).unwrap();
272
273 let new_bytes =
274 SuperAdminListComponent::apply_update_payload(&payload, Some(&prior_bytes)).unwrap();
275 let new = SuperAdminListComponent::decode_value(&new_bytes).unwrap();
276 assert_eq!(new.len(), 1);
277 assert!(new.contains(&fixture_inbox_id(2)));
278 }
279
280 #[xmtp_common::test(unwrap_try = true)]
281 fn apply_batched_delta_atomically() {
282 let mut prior_set = TlsSet::<InboxId>::new();
284 prior_set.insert(fixture_inbox_id(3)).unwrap();
285 let prior_bytes = AdminListComponent::encode_value(&prior_set).unwrap();
286
287 let delta = TlsSetDelta::<InboxId>::new()
288 .insert(fixture_inbox_id(1))
289 .insert(fixture_inbox_id(2))
290 .remove(fixture_inbox_id(3));
291 let payload = AdminListComponent::encode_mutation(&delta).unwrap();
292
293 let new_bytes =
294 AdminListComponent::apply_update_payload(&payload, Some(&prior_bytes)).unwrap();
295 let new = AdminListComponent::decode_value(&new_bytes).unwrap();
296 assert_eq!(new.len(), 2);
297 assert!(new.contains(&fixture_inbox_id(1)));
298 assert!(new.contains(&fixture_inbox_id(2)));
299 assert!(!new.contains(&fixture_inbox_id(3)));
300 }
301
302 #[xmtp_common::test(unwrap_try = true)]
303 fn expand_insert_yields_single_change() {
304 let delta = TlsSetDelta::<InboxId>::new().insert(fixture_inbox_id(5));
305 let payload = AdminListComponent::encode_mutation(&delta).unwrap();
306 let op = AppDataUpdateOperation::Update(payload.into());
307 let changes = AdminListComponent::expand_to_changes(&op, None).unwrap();
308 assert_eq!(changes.len(), 1);
309 assert_eq!(changes[0].op, ComponentOp::Insert);
310 assert_eq!(
311 changes[0].value.as_deref(),
312 Some(&fixture_inbox_id(5).as_bytes()[..])
313 );
314 }
315
316 #[xmtp_common::test(unwrap_try = true)]
317 fn expand_batched_delta_yields_one_change_per_mutation() {
318 let delta = TlsSetDelta::<InboxId>::new()
322 .insert(fixture_inbox_id(10))
323 .insert(fixture_inbox_id(11))
324 .remove(fixture_inbox_id(12));
325 let payload = AdminListComponent::encode_mutation(&delta).unwrap();
326 let op = AppDataUpdateOperation::Update(payload.into());
327 let changes = AdminListComponent::expand_to_changes(&op, None).unwrap();
328 assert_eq!(changes.len(), 3);
329 assert_eq!(changes[0].op, ComponentOp::Insert);
330 assert_eq!(changes[1].op, ComponentOp::Insert);
331 assert_eq!(changes[2].op, ComponentOp::Delete);
332 }
333
334 #[xmtp_common::test(unwrap_try = true)]
335 fn expand_remove_by_hash_resolves_against_prior() {
336 let target = fixture_inbox_id(9);
337 let mut prior_set = TlsSet::<InboxId>::new();
338 prior_set.insert(target).unwrap();
339 let prior_bytes = AdminListComponent::encode_value(&prior_set).unwrap();
340
341 let target_hash = TlsKeyHash::of(&target).unwrap();
342 let delta = TlsSetDelta::<InboxId>::new().remove_by_hash(target_hash);
343 let payload = AdminListComponent::encode_mutation(&delta).unwrap();
344 let op = AppDataUpdateOperation::Update(payload.into());
345
346 let changes = AdminListComponent::expand_to_changes(&op, Some(&prior_bytes)).unwrap();
347 assert_eq!(changes.len(), 1);
348 assert_eq!(changes[0].op, ComponentOp::Delete);
349 assert_eq!(changes[0].value.as_deref(), Some(&target.as_bytes()[..]));
350 }
351
352 #[xmtp_common::test(unwrap_try = true)]
353 fn expand_remove_by_hash_with_no_prior_yields_unresolved() {
354 let target_hash = TlsKeyHash::of(&fixture_inbox_id(9)).unwrap();
355 let delta = TlsSetDelta::<InboxId>::new().remove_by_hash(target_hash);
356 let payload = AdminListComponent::encode_mutation(&delta).unwrap();
357 let op = AppDataUpdateOperation::Update(payload.into());
358
359 let changes = AdminListComponent::expand_to_changes(&op, None).unwrap();
360 assert_eq!(changes.len(), 1);
361 assert_eq!(changes[0].op, ComponentOp::Delete);
362 assert!(changes[0].value.is_none());
363 }
364
365 #[xmtp_common::test(unwrap_try = true)]
366 fn apply_rejects_non_delta_payload() {
367 let mut set = TlsSet::<InboxId>::new();
372 set.insert(fixture_inbox_id(1)).unwrap();
373 let raw_set_bytes = set.tls_serialize_detached().unwrap();
374 let err = AdminListComponent::apply_update_payload(&raw_set_bytes, None).unwrap_err();
375 assert!(
376 matches!(err, ComponentTypedError::TlsCodec(_)),
377 "expected TlsCodec decode error for non-delta payload, got {err:?}"
378 );
379 }
380
381 #[xmtp_common::test(unwrap_try = true)]
382 fn dm_members_uses_same_codec() {
383 let mut set = TlsSet::<InboxId>::new();
386 set.insert(fixture_inbox_id(42)).unwrap();
387 let bytes = DmMembersComponent::encode_value(&set).unwrap();
388 let decoded = DmMembersComponent::decode_value(&bytes).unwrap();
389 assert!(decoded.contains(&fixture_inbox_id(42)));
390 assert_eq!(
391 DmMembersComponent::COMPONENT_TYPE,
392 ComponentType::TlsSetInboxId
393 );
394 }
395}