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, ComponentInvariantError, 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 key: None,
70 value: None,
71 }]),
72 AppDataUpdateOperation::Update(payload) => {
73 let delta = TlsSetDelta::<InboxId>::tls_deserialize_exact(payload.as_slice())?;
74
75 let needs_index = delta
91 .mutations
92 .iter()
93 .any(|m| matches!(m, TlsSetMutation::RemoveByHash(_)));
94 let hash_index: Option<HashMap<TlsKeyHash, InboxId>> = if needs_index {
95 match prior {
96 Some(bytes) => {
97 let prior_set = TlsSet::<InboxId>::tls_deserialize_exact(bytes)?;
98 let mut idx = HashMap::with_capacity(prior_set.len());
99 for key in prior_set.iter() {
100 if idx.insert(TlsKeyHash::of(key)?, *key).is_some() {
113 return Err(ComponentTypedError::TlsSetApply(
114 TlsSetError::DuplicateHash,
115 ));
116 }
117 }
118 Some(idx)
119 }
120 None => None,
124 }
125 } else {
126 None
127 };
128
129 let mut out = Vec::with_capacity(delta.mutations.len());
130 for mutation in delta.mutations {
131 match mutation {
132 TlsSetMutation::Insert(key) => out.push(ExpandedComponentChange {
133 op: ComponentOp::Insert,
134 key: None,
135 value: Some(key.into_bytes().to_vec()),
136 }),
137 TlsSetMutation::Remove(key) => out.push(ExpandedComponentChange {
138 op: ComponentOp::Delete,
139 key: None,
140 value: Some(key.into_bytes().to_vec()),
141 }),
142 TlsSetMutation::RemoveByHash(target) => {
143 let resolved = hash_index
144 .as_ref()
145 .and_then(|idx| idx.get(&target))
146 .map(|id| id.as_bytes().to_vec());
147 out.push(ExpandedComponentChange {
148 op: ComponentOp::Delete,
149 key: None,
150 value: resolved,
151 });
152 }
153 }
154 }
155 Ok(out)
156 }
157 }
158}
159
160macro_rules! inbox_id_set_component {
161 ($struct_name:ident, $id:expr) => {
162 pub struct $struct_name;
163
164 impl Component for $struct_name {
165 const ID: ComponentId = $id;
166 const COMPONENT_TYPE: ComponentType = ComponentType::TlsSetInboxId;
167 type Value = TlsSet<InboxId>;
168 type Mutation = TlsSetDelta<InboxId>;
173
174 fn decode_value(bytes: &[u8]) -> Result<Self::Value, ComponentTypedError> {
175 TlsSet::<InboxId>::tls_deserialize_exact(bytes).map_err(Into::into)
176 }
177
178 fn encode_value(value: &Self::Value) -> Result<Vec<u8>, ComponentTypedError> {
179 value.tls_serialize_detached().map_err(Into::into)
180 }
181
182 fn encode_mutation(mutation: &Self::Mutation) -> Result<Vec<u8>, ComponentTypedError> {
183 mutation.tls_serialize_detached().map_err(Into::into)
184 }
185
186 fn apply_update_payload(
187 payload: &[u8],
188 prior: Option<&[u8]>,
189 ) -> Result<Vec<u8>, ComponentTypedError> {
190 apply_inbox_id_set_delta(payload, prior)
191 }
192
193 fn expand_to_changes(
194 op: &AppDataUpdateOperation,
195 prior: Option<&[u8]>,
196 ) -> Result<Vec<ExpandedComponentChange>, ComponentTypedError> {
197 expand_inbox_id_set_changes(op, prior)
198 }
199 }
200 };
201}
202
203inbox_id_set_component!(AdminListComponent, ComponentId::ADMIN_LIST);
204
205pub struct SuperAdminListComponent;
206
207impl Component for SuperAdminListComponent {
208 const ID: ComponentId = ComponentId::SUPER_ADMIN_LIST;
209 const COMPONENT_TYPE: ComponentType = ComponentType::TlsSetInboxId;
210 type Value = TlsSet<InboxId>;
211 type Mutation = TlsSetDelta<InboxId>;
212
213 fn decode_value(bytes: &[u8]) -> Result<Self::Value, ComponentTypedError> {
214 TlsSet::<InboxId>::tls_deserialize_exact(bytes).map_err(Into::into)
215 }
216
217 fn encode_value(value: &Self::Value) -> Result<Vec<u8>, ComponentTypedError> {
218 value.tls_serialize_detached().map_err(Into::into)
219 }
220
221 fn encode_mutation(mutation: &Self::Mutation) -> Result<Vec<u8>, ComponentTypedError> {
222 mutation.tls_serialize_detached().map_err(Into::into)
223 }
224
225 fn apply_update_payload(
226 payload: &[u8],
227 prior: Option<&[u8]>,
228 ) -> Result<Vec<u8>, ComponentTypedError> {
229 apply_inbox_id_set_delta(payload, prior)
230 }
231
232 fn expand_to_changes(
233 op: &AppDataUpdateOperation,
234 prior: Option<&[u8]>,
235 ) -> Result<Vec<ExpandedComponentChange>, ComponentTypedError> {
236 expand_inbox_id_set_changes(op, prior)
237 }
238
239 fn validate_invariant(
240 change: &crate::app_data::validation::ComponentChange<'_>,
241 _registry: &crate::app_data::component_registry::ComponentRegistry,
242 ) -> Result<(), ComponentInvariantError> {
243 let prior = match change.old_value {
248 Some(bytes) => TlsSet::<InboxId>::tls_deserialize_exact(bytes).map_err(|_| {
249 ComponentInvariantError::Violation {
250 component_id: Self::ID,
251 reason: "pre-state is not a valid inbox-id set".into(),
252 }
253 })?,
254 None => return Ok(()),
255 };
256 if prior.is_empty() {
257 return Ok(());
258 }
259 let post_is_empty = match change.new_value {
260 Some(bytes) => TlsSet::<InboxId>::tls_deserialize_exact(bytes)
261 .map_err(|_| ComponentInvariantError::Violation {
262 component_id: Self::ID,
263 reason: "post-state is not a valid inbox-id set".into(),
264 })?
265 .is_empty(),
266 None => true,
267 };
268 if post_is_empty {
269 return Err(ComponentInvariantError::Violation {
270 component_id: Self::ID,
271 reason: "a non-empty super-admin list cannot transition to empty".into(),
272 });
273 }
274 Ok(())
275 }
276}
277
278inbox_id_set_component!(DmMembersComponent, ComponentId::DM_MEMBERS);
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283 use crate::app_data::{component_registry::ComponentRegistry, validation::ActorAuthority};
284
285 fn fixture_inbox_id(seed: u8) -> InboxId {
286 let mut bytes = [0u8; 32];
287 bytes[0] = seed;
288 InboxId::from_bytes(bytes)
289 }
290
291 #[xmtp_common::test(unwrap_try = true)]
292 fn round_trip_admin_list_value() {
293 let mut set = TlsSet::<InboxId>::new();
294 set.insert(fixture_inbox_id(1)).unwrap();
295 set.insert(fixture_inbox_id(2)).unwrap();
296 let bytes = AdminListComponent::encode_value(&set).unwrap();
297 let decoded = AdminListComponent::decode_value(&bytes).unwrap();
298 assert_eq!(decoded.len(), 2);
299 assert!(decoded.contains(&fixture_inbox_id(1)));
300 assert!(decoded.contains(&fixture_inbox_id(2)));
301 }
302
303 #[xmtp_common::test(unwrap_try = true)]
304 fn encode_mutation_serializes_full_delta() {
305 let delta = TlsSetDelta::<InboxId>::new().insert(fixture_inbox_id(7));
306 let bytes = AdminListComponent::encode_mutation(&delta).unwrap();
307 let round_trip = TlsSetDelta::<InboxId>::tls_deserialize_exact(&bytes).unwrap();
308 assert_eq!(round_trip.mutations.len(), 1);
309 match &round_trip.mutations[0] {
310 TlsSetMutation::Insert(id) => assert_eq!(*id, fixture_inbox_id(7)),
311 other => panic!("unexpected mutation: {other:?}"),
312 }
313 }
314
315 #[xmtp_common::test(unwrap_try = true)]
316 fn encode_mutation_supports_batched_delta() {
317 let delta = TlsSetDelta::<InboxId>::new()
321 .insert(fixture_inbox_id(1))
322 .insert(fixture_inbox_id(2))
323 .remove(fixture_inbox_id(3));
324 let bytes = AdminListComponent::encode_mutation(&delta).unwrap();
325 let round_trip = TlsSetDelta::<InboxId>::tls_deserialize_exact(&bytes).unwrap();
326 assert_eq!(round_trip.mutations.len(), 3);
327 }
328
329 #[xmtp_common::test(unwrap_try = true)]
330 fn apply_insert_against_empty_prior() {
331 let delta = TlsSetDelta::<InboxId>::new().insert(fixture_inbox_id(3));
332 let payload = AdminListComponent::encode_mutation(&delta).unwrap();
333 let new_bytes = AdminListComponent::apply_update_payload(&payload, None).unwrap();
334 let new = AdminListComponent::decode_value(&new_bytes).unwrap();
335 assert_eq!(new.len(), 1);
336 assert!(new.contains(&fixture_inbox_id(3)));
337 }
338
339 #[xmtp_common::test(unwrap_try = true)]
340 fn apply_remove_against_existing_prior() {
341 let mut prior_set = TlsSet::<InboxId>::new();
343 prior_set.insert(fixture_inbox_id(1)).unwrap();
344 prior_set.insert(fixture_inbox_id(2)).unwrap();
345 let prior_bytes = SuperAdminListComponent::encode_value(&prior_set).unwrap();
346
347 let delta = TlsSetDelta::<InboxId>::new().remove(fixture_inbox_id(1));
349 let payload = SuperAdminListComponent::encode_mutation(&delta).unwrap();
350
351 let new_bytes =
352 SuperAdminListComponent::apply_update_payload(&payload, Some(&prior_bytes)).unwrap();
353 let new = SuperAdminListComponent::decode_value(&new_bytes).unwrap();
354 assert_eq!(new.len(), 1);
355 assert!(new.contains(&fixture_inbox_id(2)));
356 }
357
358 #[xmtp_common::test(unwrap_try = true)]
359 fn super_admin_list_rejects_transition_from_non_empty_to_empty() {
360 let id = fixture_inbox_id(1);
361 let mut prior = TlsSet::<InboxId>::new();
362 prior.insert(id)?;
363 let prior = SuperAdminListComponent::encode_value(&prior)?;
364 let post = SuperAdminListComponent::encode_value(&TlsSet::new())?;
365 let change = crate::app_data::validation::ComponentChange::builder()
366 .component_id(ComponentId::SUPER_ADMIN_LIST)
367 .op(ComponentOp::Update)
368 .actor(ActorAuthority {
369 is_admin: true,
370 is_super_admin: true,
371 })
372 .old_value(&prior)
373 .new_value(&post)
374 .build();
375
376 let err = SuperAdminListComponent::validate_invariant(&change, &ComponentRegistry::new())
377 .unwrap_err();
378 assert!(matches!(err, ComponentInvariantError::Violation { .. }));
379 }
380
381 #[xmtp_common::test(unwrap_try = true)]
382 fn super_admin_list_allows_empty_creation_state() {
383 let empty = SuperAdminListComponent::encode_value(&TlsSet::<InboxId>::new())?;
384 let change = crate::app_data::validation::ComponentChange::builder()
385 .component_id(ComponentId::SUPER_ADMIN_LIST)
386 .op(ComponentOp::Insert)
387 .actor(ActorAuthority {
388 is_admin: true,
389 is_super_admin: true,
390 })
391 .new_value(&empty)
392 .build();
393
394 SuperAdminListComponent::validate_invariant(&change, &ComponentRegistry::new())?;
395 }
396
397 #[xmtp_common::test(unwrap_try = true)]
398 fn apply_batched_delta_atomically() {
399 let mut prior_set = TlsSet::<InboxId>::new();
401 prior_set.insert(fixture_inbox_id(3)).unwrap();
402 let prior_bytes = AdminListComponent::encode_value(&prior_set).unwrap();
403
404 let delta = TlsSetDelta::<InboxId>::new()
405 .insert(fixture_inbox_id(1))
406 .insert(fixture_inbox_id(2))
407 .remove(fixture_inbox_id(3));
408 let payload = AdminListComponent::encode_mutation(&delta).unwrap();
409
410 let new_bytes =
411 AdminListComponent::apply_update_payload(&payload, Some(&prior_bytes)).unwrap();
412 let new = AdminListComponent::decode_value(&new_bytes).unwrap();
413 assert_eq!(new.len(), 2);
414 assert!(new.contains(&fixture_inbox_id(1)));
415 assert!(new.contains(&fixture_inbox_id(2)));
416 assert!(!new.contains(&fixture_inbox_id(3)));
417 }
418
419 #[xmtp_common::test(unwrap_try = true)]
420 fn expand_insert_yields_single_change() {
421 let delta = TlsSetDelta::<InboxId>::new().insert(fixture_inbox_id(5));
422 let payload = AdminListComponent::encode_mutation(&delta).unwrap();
423 let op = AppDataUpdateOperation::Update(payload.into());
424 let changes = AdminListComponent::expand_to_changes(&op, None).unwrap();
425 assert_eq!(changes.len(), 1);
426 assert_eq!(changes[0].op, ComponentOp::Insert);
427 assert_eq!(
428 changes[0].value.as_deref(),
429 Some(&fixture_inbox_id(5).as_bytes()[..])
430 );
431 }
432
433 #[xmtp_common::test(unwrap_try = true)]
434 fn expand_batched_delta_yields_one_change_per_mutation() {
435 let delta = TlsSetDelta::<InboxId>::new()
439 .insert(fixture_inbox_id(10))
440 .insert(fixture_inbox_id(11))
441 .remove(fixture_inbox_id(12));
442 let payload = AdminListComponent::encode_mutation(&delta).unwrap();
443 let op = AppDataUpdateOperation::Update(payload.into());
444 let changes = AdminListComponent::expand_to_changes(&op, None).unwrap();
445 assert_eq!(changes.len(), 3);
446 assert_eq!(changes[0].op, ComponentOp::Insert);
447 assert_eq!(changes[1].op, ComponentOp::Insert);
448 assert_eq!(changes[2].op, ComponentOp::Delete);
449 }
450
451 #[xmtp_common::test(unwrap_try = true)]
452 fn expand_remove_by_hash_resolves_against_prior() {
453 let target = fixture_inbox_id(9);
454 let mut prior_set = TlsSet::<InboxId>::new();
455 prior_set.insert(target).unwrap();
456 let prior_bytes = AdminListComponent::encode_value(&prior_set).unwrap();
457
458 let target_hash = TlsKeyHash::of(&target).unwrap();
459 let delta = TlsSetDelta::<InboxId>::new().remove_by_hash(target_hash);
460 let payload = AdminListComponent::encode_mutation(&delta).unwrap();
461 let op = AppDataUpdateOperation::Update(payload.into());
462
463 let changes = AdminListComponent::expand_to_changes(&op, Some(&prior_bytes)).unwrap();
464 assert_eq!(changes.len(), 1);
465 assert_eq!(changes[0].op, ComponentOp::Delete);
466 assert_eq!(changes[0].value.as_deref(), Some(&target.as_bytes()[..]));
467 }
468
469 #[xmtp_common::test(unwrap_try = true)]
470 fn expand_remove_by_hash_with_no_prior_yields_unresolved() {
471 let target_hash = TlsKeyHash::of(&fixture_inbox_id(9)).unwrap();
472 let delta = TlsSetDelta::<InboxId>::new().remove_by_hash(target_hash);
473 let payload = AdminListComponent::encode_mutation(&delta).unwrap();
474 let op = AppDataUpdateOperation::Update(payload.into());
475
476 let changes = AdminListComponent::expand_to_changes(&op, None).unwrap();
477 assert_eq!(changes.len(), 1);
478 assert_eq!(changes[0].op, ComponentOp::Delete);
479 assert!(changes[0].value.is_none());
480 }
481
482 #[xmtp_common::test(unwrap_try = true)]
483 fn apply_rejects_non_delta_payload() {
484 let mut set = TlsSet::<InboxId>::new();
489 set.insert(fixture_inbox_id(1)).unwrap();
490 let raw_set_bytes = set.tls_serialize_detached().unwrap();
491 let err = AdminListComponent::apply_update_payload(&raw_set_bytes, None).unwrap_err();
492 assert!(
493 matches!(err, ComponentTypedError::TlsCodec(_)),
494 "expected TlsCodec decode error for non-delta payload, got {err:?}"
495 );
496 }
497
498 #[xmtp_common::test(unwrap_try = true)]
499 fn dm_members_uses_same_codec() {
500 let mut set = TlsSet::<InboxId>::new();
503 set.insert(fixture_inbox_id(42)).unwrap();
504 let bytes = DmMembersComponent::encode_value(&set).unwrap();
505 let decoded = DmMembersComponent::decode_value(&bytes).unwrap();
506 assert!(decoded.contains(&fixture_inbox_id(42)));
507 assert_eq!(
508 DmMembersComponent::COMPONENT_TYPE,
509 ComponentType::TlsSetInboxId
510 );
511 }
512}