1use openmls::messages::proposals::AppDataUpdateOperation;
23use tls_codec::{Deserialize, Serialize, VLBytes};
24use xmtp_proto::xmtp::mls::message_contents::ComponentType;
25
26use crate::{
27 app_data::{
28 component_id::ComponentId,
29 component_registry::{ComponentOp, ComponentRegistry, ComponentRegistryError},
30 typed::{Component, ComponentTypedError, ExpandedComponentChange},
31 },
32 inbox_id::InboxId,
33 tls_map::{TlsMap, TlsMapDelta, TlsMapMutation},
34};
35
36pub(crate) fn apply_tls_map_delta<K>(
51 payload: &[u8],
52 prior: Option<&[u8]>,
53) -> Result<Vec<u8>, ComponentTypedError>
54where
55 K: tls_codec::Serialize
56 + tls_codec::Deserialize
57 + tls_codec::Size
58 + Ord
59 + Eq
60 + Clone
61 + std::fmt::Debug,
62{
63 let delta = TlsMapDelta::<K, VLBytes>::tls_deserialize_exact(payload)?;
64 let mut map: TlsMap<K, VLBytes> = match prior {
65 Some(bytes) => TlsMap::<K, VLBytes>::tls_deserialize_exact(bytes)?,
66 None => TlsMap::new(),
67 };
68 map.apply_delta(delta)?;
69 Ok(map.tls_serialize_detached()?)
70}
71
72pub(crate) fn expand_tls_map_changes<K>(
96 op: &AppDataUpdateOperation,
97 _prior: Option<&[u8]>,
98) -> Result<Vec<ExpandedComponentChange>, ComponentTypedError>
99where
100 K: tls_codec::Serialize + tls_codec::Deserialize + tls_codec::Size,
101{
102 match op {
103 AppDataUpdateOperation::Remove => Ok(vec![ExpandedComponentChange {
104 op: ComponentOp::Delete,
105 key: None,
106 value: None,
107 }]),
108 AppDataUpdateOperation::Update(payload) => {
109 let delta = TlsMapDelta::<K, VLBytes>::tls_deserialize_exact(payload.as_slice())?;
110 expand_decoded_tls_map_delta(delta)
111 }
112 }
113}
114
115fn expand_decoded_tls_map_delta<K>(
120 delta: TlsMapDelta<K, VLBytes>,
121) -> Result<Vec<ExpandedComponentChange>, ComponentTypedError>
122where
123 K: tls_codec::Serialize + tls_codec::Size,
124{
125 delta
126 .mutations
127 .into_iter()
128 .map(|mutation| {
129 let (op, key, value) = match mutation {
130 TlsMapMutation::Insert { key, value } => (ComponentOp::Insert, key, Some(value)),
131 TlsMapMutation::Update { key, value } => (ComponentOp::Update, key, Some(value)),
132 TlsMapMutation::Delete { key } => (ComponentOp::Delete, key, None),
133 };
134 Ok(ExpandedComponentChange {
135 op,
136 key: Some(key.tls_serialize_detached()?),
137 value: value.map(Vec::from),
138 })
139 })
140 .collect()
141}
142
143pub struct GroupMembershipComponent;
155
156impl Component for GroupMembershipComponent {
157 const ID: ComponentId = ComponentId::GROUP_MEMBERSHIP;
158 const COMPONENT_TYPE: ComponentType = ComponentType::TlsMapInboxIdBytes;
159 type Value = TlsMap<InboxId, VLBytes>;
160 type Mutation = TlsMapDelta<InboxId, VLBytes>;
165
166 fn decode_value(bytes: &[u8]) -> Result<Self::Value, ComponentTypedError> {
167 TlsMap::<InboxId, VLBytes>::tls_deserialize_exact(bytes).map_err(Into::into)
168 }
169
170 fn encode_value(value: &Self::Value) -> Result<Vec<u8>, ComponentTypedError> {
171 value.tls_serialize_detached().map_err(Into::into)
172 }
173
174 fn encode_mutation(mutation: &Self::Mutation) -> Result<Vec<u8>, ComponentTypedError> {
175 mutation.tls_serialize_detached().map_err(Into::into)
176 }
177
178 fn apply_update_payload(
179 payload: &[u8],
180 prior: Option<&[u8]>,
181 ) -> Result<Vec<u8>, ComponentTypedError> {
182 apply_tls_map_delta::<InboxId>(payload, prior)
183 }
184
185 fn expand_to_changes(
186 op: &AppDataUpdateOperation,
187 prior: Option<&[u8]>,
188 ) -> Result<Vec<ExpandedComponentChange>, ComponentTypedError> {
189 expand_tls_map_changes::<InboxId>(op, prior)
190 }
191}
192
193pub struct ComponentRegistryComponent;
213
214fn validate_registry_delta(
250 delta: &TlsMapDelta<ComponentId, VLBytes>,
251) -> Result<(), ComponentTypedError> {
252 for mutation in &delta.mutations {
253 match mutation {
254 TlsMapMutation::Insert { key, value } => {
255 ComponentRegistry::validate_entry(*key, value.as_slice())?;
256 }
257 TlsMapMutation::Update { key, value } => {
258 ComponentRegistry::validate_entry(*key, value.as_slice())?;
259 if key.is_immutable() {
260 return Err(ComponentRegistryError::ImmutableComponent(*key).into());
261 }
262 }
263 TlsMapMutation::Delete { key } => {
264 if !key.is_in_component_space() {
265 return Err(ComponentRegistryError::InvalidComponentId(*key).into());
266 }
267 if key.is_reserved() {
268 return Err(ComponentRegistryError::ReservedRange(*key).into());
269 }
270 if key.is_hardcoded() {
271 return Err(ComponentRegistryError::HardcodedComponent(*key).into());
272 }
273 if key.is_immutable() {
274 return Err(ComponentRegistryError::ImmutableComponent(*key).into());
275 }
276 }
277 }
278 }
279 Ok(())
280}
281
282impl Component for ComponentRegistryComponent {
283 const ID: ComponentId = ComponentId::COMPONENT_REGISTRY;
284 const COMPONENT_TYPE: ComponentType = ComponentType::TlsMapBytesBytes;
285 type Value = TlsMap<ComponentId, VLBytes>;
286 type Mutation = TlsMapDelta<ComponentId, VLBytes>;
290
291 fn decode_value(bytes: &[u8]) -> Result<Self::Value, ComponentTypedError> {
292 TlsMap::<ComponentId, VLBytes>::tls_deserialize_exact(bytes).map_err(Into::into)
293 }
294
295 fn encode_value(value: &Self::Value) -> Result<Vec<u8>, ComponentTypedError> {
296 value.tls_serialize_detached().map_err(Into::into)
297 }
298
299 fn encode_mutation(mutation: &Self::Mutation) -> Result<Vec<u8>, ComponentTypedError> {
300 mutation.tls_serialize_detached().map_err(Into::into)
301 }
302
303 fn apply_update_payload(
304 payload: &[u8],
305 prior: Option<&[u8]>,
306 ) -> Result<Vec<u8>, ComponentTypedError> {
307 let delta = TlsMapDelta::<ComponentId, VLBytes>::tls_deserialize_exact(payload)?;
308 validate_registry_delta(&delta)?;
309 let mut map = match prior {
310 Some(bytes) => TlsMap::<ComponentId, VLBytes>::tls_deserialize_exact(bytes)?,
311 None => TlsMap::new(),
312 };
313 map.apply_delta(delta)?;
314 Ok(map.tls_serialize_detached()?)
315 }
316
317 fn expand_to_changes(
318 op: &AppDataUpdateOperation,
319 _prior: Option<&[u8]>,
320 ) -> Result<Vec<ExpandedComponentChange>, ComponentTypedError> {
321 match op {
322 AppDataUpdateOperation::Remove => Err(ComponentRegistryError::HardcodedComponent(
331 ComponentId::COMPONENT_REGISTRY,
332 )
333 .into()),
334 AppDataUpdateOperation::Update(payload) => {
335 let delta =
336 TlsMapDelta::<ComponentId, VLBytes>::tls_deserialize_exact(payload.as_slice())?;
337 validate_registry_delta(&delta)?;
338 expand_decoded_tls_map_delta(delta)
339 }
340 }
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347 use crate::app_data::component_permissions::component_permissions;
348 use prost::Message;
349 use xmtp_proto::xmtp::mls::message_contents::{
350 MetadataPolicy as MetadataPolicyProto,
351 metadata_policy::{Kind as MetadataPolicyKind, MetadataBasePolicy},
352 };
353
354 fn fixture_inbox_id(seed: u8) -> InboxId {
355 let mut bytes = [0u8; 32];
356 bytes[0] = seed;
357 InboxId::from_bytes(bytes)
358 }
359
360 fn valid_meta_bytes() -> Vec<u8> {
364 let allow = MetadataPolicyProto {
365 kind: Some(MetadataPolicyKind::Base(MetadataBasePolicy::Allow as i32)),
366 };
367 crate::app_data::component_registry::new_component_metadata(
368 component_permissions()
369 .insert(allow.clone())
370 .update(allow.clone())
371 .delete(allow)
372 .call(),
373 ComponentType::Bytes,
374 )
375 .encode_to_vec()
376 }
377
378 #[xmtp_common::test(unwrap_try = true)]
379 fn group_membership_round_trip_value() {
380 let mut map: TlsMap<InboxId, VLBytes> = TlsMap::new();
381 map.insert(fixture_inbox_id(1), VLBytes::new(b"member1-blob".to_vec()))
382 .unwrap();
383 let bytes = GroupMembershipComponent::encode_value(&map).unwrap();
384 let decoded = GroupMembershipComponent::decode_value(&bytes).unwrap();
385 assert_eq!(decoded.len(), 1);
386 assert_eq!(
387 decoded.get(&fixture_inbox_id(1)).unwrap().as_slice(),
388 b"member1-blob"
389 );
390 }
391
392 #[xmtp_common::test(unwrap_try = true)]
393 fn group_membership_apply_insert_against_empty() {
394 let delta = TlsMapDelta::<InboxId, VLBytes>::new()
395 .insert(fixture_inbox_id(2), VLBytes::new(b"v".to_vec()));
396 let payload = GroupMembershipComponent::encode_mutation(&delta).unwrap();
397 let new_bytes = GroupMembershipComponent::apply_update_payload(&payload, None).unwrap();
398 let new = GroupMembershipComponent::decode_value(&new_bytes).unwrap();
399 assert_eq!(new.len(), 1);
400 assert_eq!(new.get(&fixture_inbox_id(2)).unwrap().as_slice(), b"v");
401 }
402
403 #[xmtp_common::test(unwrap_try = true)]
404 fn group_membership_apply_update_against_existing() {
405 let mut prior_map: TlsMap<InboxId, VLBytes> = TlsMap::new();
406 prior_map
407 .insert(fixture_inbox_id(3), VLBytes::new(b"old".to_vec()))
408 .unwrap();
409 let prior_bytes = GroupMembershipComponent::encode_value(&prior_map).unwrap();
410
411 let delta = TlsMapDelta::<InboxId, VLBytes>::new()
412 .update(fixture_inbox_id(3), VLBytes::new(b"new".to_vec()));
413 let payload = GroupMembershipComponent::encode_mutation(&delta).unwrap();
414 let new_bytes =
415 GroupMembershipComponent::apply_update_payload(&payload, Some(&prior_bytes)).unwrap();
416 let new = GroupMembershipComponent::decode_value(&new_bytes).unwrap();
417 assert_eq!(new.get(&fixture_inbox_id(3)).unwrap().as_slice(), b"new");
418 }
419
420 #[xmtp_common::test(unwrap_try = true)]
421 fn group_membership_apply_batched_delta_atomically() {
422 let mut prior_map: TlsMap<InboxId, VLBytes> = TlsMap::new();
427 prior_map
428 .insert(fixture_inbox_id(1), VLBytes::new(b"seq=5".to_vec()))
429 .unwrap();
430 prior_map
431 .insert(fixture_inbox_id(3), VLBytes::new(b"to-remove".to_vec()))
432 .unwrap();
433 let prior_bytes = GroupMembershipComponent::encode_value(&prior_map).unwrap();
434
435 let delta = TlsMapDelta::<InboxId, VLBytes>::new()
436 .update(fixture_inbox_id(1), VLBytes::new(b"seq=7".to_vec()))
437 .insert(fixture_inbox_id(2), VLBytes::new(b"seq=1".to_vec()))
438 .delete(fixture_inbox_id(3));
439 let payload = GroupMembershipComponent::encode_mutation(&delta).unwrap();
440 let new_bytes =
441 GroupMembershipComponent::apply_update_payload(&payload, Some(&prior_bytes)).unwrap();
442 let new = GroupMembershipComponent::decode_value(&new_bytes).unwrap();
443
444 assert_eq!(new.len(), 2);
445 assert_eq!(new.get(&fixture_inbox_id(1)).unwrap().as_slice(), b"seq=7");
446 assert_eq!(new.get(&fixture_inbox_id(2)).unwrap().as_slice(), b"seq=1");
447 assert!(new.get(&fixture_inbox_id(3)).is_none());
448 }
449
450 #[xmtp_common::test(unwrap_try = true)]
451 fn group_membership_expand_insert_carries_value_bytes() {
452 let delta = TlsMapDelta::<InboxId, VLBytes>::new()
453 .insert(fixture_inbox_id(4), VLBytes::new(b"new-member".to_vec()));
454 let payload = GroupMembershipComponent::encode_mutation(&delta).unwrap();
455 let op = AppDataUpdateOperation::Update(payload.into());
456 let changes = GroupMembershipComponent::expand_to_changes(&op, None).unwrap();
457 assert_eq!(changes.len(), 1);
458 assert_eq!(changes[0].op, ComponentOp::Insert);
459 assert_eq!(changes[0].value.as_deref(), Some(&b"new-member"[..]));
460 }
461
462 #[xmtp_common::test(unwrap_try = true)]
463 fn group_membership_expand_delete_carries_key_bytes() {
464 let delta = TlsMapDelta::<InboxId, VLBytes>::new().delete(fixture_inbox_id(5));
465 let payload = GroupMembershipComponent::encode_mutation(&delta).unwrap();
466 let op = AppDataUpdateOperation::Update(payload.into());
467 let changes = GroupMembershipComponent::expand_to_changes(&op, None).unwrap();
468 assert_eq!(changes.len(), 1);
469 assert_eq!(changes[0].op, ComponentOp::Delete);
470 assert_eq!(
471 changes[0].key,
472 Some(fixture_inbox_id(5).tls_serialize_detached()?)
473 );
474 assert_eq!(changes[0].value, None);
475 }
476
477 #[xmtp_common::test(unwrap_try = true)]
478 fn group_membership_expand_batched_yields_one_per_mutation() {
479 let delta = TlsMapDelta::<InboxId, VLBytes>::new()
480 .update(fixture_inbox_id(1), VLBytes::new(b"v1".to_vec()))
481 .insert(fixture_inbox_id(2), VLBytes::new(b"v2".to_vec()))
482 .delete(fixture_inbox_id(3));
483 let payload = GroupMembershipComponent::encode_mutation(&delta).unwrap();
484 let op = AppDataUpdateOperation::Update(payload.into());
485 let changes = GroupMembershipComponent::expand_to_changes(&op, None).unwrap();
486 assert_eq!(changes.len(), 3);
487 assert_eq!(changes[0].op, ComponentOp::Update);
488 assert_eq!(changes[1].op, ComponentOp::Insert);
489 assert_eq!(changes[2].op, ComponentOp::Delete);
490 }
491
492 #[xmtp_common::test(unwrap_try = true)]
493 fn component_registry_round_trip() {
494 let mut map: TlsMap<ComponentId, VLBytes> = TlsMap::new();
495 map.insert(
496 ComponentId::ADMIN_LIST,
497 VLBytes::new(b"meta-bytes".to_vec()),
498 )
499 .unwrap();
500 let bytes = ComponentRegistryComponent::encode_value(&map).unwrap();
501 let decoded = ComponentRegistryComponent::decode_value(&bytes).unwrap();
502 assert_eq!(decoded.len(), 1);
503 assert_eq!(
504 decoded.get(&ComponentId::ADMIN_LIST).unwrap().as_slice(),
505 b"meta-bytes"
506 );
507 }
508
509 #[xmtp_common::test(unwrap_try = true)]
510 fn component_registry_apply_update_replaces_metadata() {
511 let old_meta = valid_meta_bytes();
512 let mut new_meta = valid_meta_bytes();
513 new_meta = {
516 let mut decoded = xmtp_proto::xmtp::mls::message_contents::ComponentMetadata::decode(
517 new_meta.as_slice(),
518 )
519 .unwrap();
520 decoded.component_type = ComponentType::String as i32;
521 decoded.encode_to_vec()
522 };
523
524 let mut prior_map: TlsMap<ComponentId, VLBytes> = TlsMap::new();
525 prior_map
526 .insert(ComponentId::GROUP_NAME, VLBytes::new(old_meta))
527 .unwrap();
528 let prior_bytes = ComponentRegistryComponent::encode_value(&prior_map).unwrap();
529
530 let delta = TlsMapDelta::<ComponentId, VLBytes>::new()
531 .update(ComponentId::GROUP_NAME, VLBytes::new(new_meta.clone()));
532 let payload = ComponentRegistryComponent::encode_mutation(&delta).unwrap();
533 let new_bytes =
534 ComponentRegistryComponent::apply_update_payload(&payload, Some(&prior_bytes)).unwrap();
535 let new = ComponentRegistryComponent::decode_value(&new_bytes).unwrap();
536 assert_eq!(
537 new.get(&ComponentId::GROUP_NAME).unwrap().as_slice(),
538 new_meta.as_slice()
539 );
540 }
541
542 #[xmtp_common::test(unwrap_try = true)]
543 fn component_registry_apply_batched_delta_atomically() {
544 let delta = TlsMapDelta::<ComponentId, VLBytes>::new()
546 .insert(ComponentId::new(0xC100), VLBytes::new(valid_meta_bytes()))
547 .insert(ComponentId::new(0xC101), VLBytes::new(valid_meta_bytes()));
548 let payload = ComponentRegistryComponent::encode_mutation(&delta).unwrap();
549 let new_bytes = ComponentRegistryComponent::apply_update_payload(&payload, None).unwrap();
550 let new = ComponentRegistryComponent::decode_value(&new_bytes).unwrap();
551 assert_eq!(new.len(), 2);
552 assert!(new.get(&ComponentId::new(0xC100)).is_some());
553 assert!(new.get(&ComponentId::new(0xC101)).is_some());
554 }
555
556 fn assert_registry_delta_rejected_everywhere(delta: TlsMapDelta<ComponentId, VLBytes>) {
563 let payload = ComponentRegistryComponent::encode_mutation(&delta).unwrap();
564 let apply_err = ComponentRegistryComponent::apply_update_payload(&payload, None);
565 assert!(
566 matches!(apply_err, Err(ComponentTypedError::RegistryMutation(_))),
567 "apply must reject, got {apply_err:?}"
568 );
569 let op = AppDataUpdateOperation::Update(payload.into());
570 let expand_err = ComponentRegistryComponent::expand_to_changes(&op, None);
571 assert!(
572 matches!(expand_err, Err(ComponentTypedError::RegistryMutation(_))),
573 "expand must reject, got {expand_err:?}"
574 );
575 }
576
577 #[xmtp_common::test(unwrap_try = true)]
578 fn registry_delta_rejects_reserved_range_insert() {
579 assert_registry_delta_rejected_everywhere(
583 TlsMapDelta::<ComponentId, VLBytes>::new()
584 .insert(ComponentId::new(0xFF00), VLBytes::new(valid_meta_bytes())),
585 );
586 }
587
588 #[xmtp_common::test(unwrap_try = true)]
589 fn registry_delta_rejects_hardcoded_insert() {
590 assert_registry_delta_rejected_everywhere(
591 TlsMapDelta::<ComponentId, VLBytes>::new().insert(
592 ComponentId::SUPER_ADMIN_LIST,
593 VLBytes::new(valid_meta_bytes()),
594 ),
595 );
596 }
597
598 #[xmtp_common::test(unwrap_try = true)]
599 fn registry_delta_rejects_out_of_space_insert() {
600 assert_registry_delta_rejected_everywhere(
601 TlsMapDelta::<ComponentId, VLBytes>::new()
602 .insert(ComponentId::new(0x0001), VLBytes::new(valid_meta_bytes())),
603 );
604 }
605
606 #[xmtp_common::test(unwrap_try = true)]
607 fn registry_delta_rejects_undecodable_metadata_insert() {
608 assert_registry_delta_rejected_everywhere(
609 TlsMapDelta::<ComponentId, VLBytes>::new().insert(
610 ComponentId::new(0xC100),
611 VLBytes::new(vec![0xFF, 0xFF, 0xFF, 0xFF]),
612 ),
613 );
614 }
615
616 #[xmtp_common::test(unwrap_try = true)]
617 fn registry_delta_rejects_immutable_update_and_delete() {
618 assert_registry_delta_rejected_everywhere(
622 TlsMapDelta::<ComponentId, VLBytes>::new()
623 .update(ComponentId::new(0xBE00), VLBytes::new(valid_meta_bytes())),
624 );
625 assert_registry_delta_rejected_everywhere(
626 TlsMapDelta::<ComponentId, VLBytes>::new().delete(ComponentId::new(0xBE00)),
627 );
628 }
629
630 #[xmtp_common::test(unwrap_try = true)]
631 fn registry_delta_rejects_hardcoded_delete() {
632 assert_registry_delta_rejected_everywhere(
633 TlsMapDelta::<ComponentId, VLBytes>::new().delete(ComponentId::SUPER_ADMIN_LIST),
634 );
635 }
636
637 #[xmtp_common::test(unwrap_try = true)]
638 fn registry_component_rejects_whole_component_remove() {
639 let err =
643 ComponentRegistryComponent::expand_to_changes(&AppDataUpdateOperation::Remove, None);
644 assert!(
645 matches!(err, Err(ComponentTypedError::RegistryMutation(_))),
646 "Remove of the registry component must be rejected, got {err:?}"
647 );
648 }
649
650 #[xmtp_common::test(unwrap_try = true)]
651 fn registry_delta_valid_mutations_expand_and_apply() {
652 let delta = TlsMapDelta::<ComponentId, VLBytes>::new()
656 .insert(ComponentId::new(0xC200), VLBytes::new(valid_meta_bytes()));
657 let payload = ComponentRegistryComponent::encode_mutation(&delta).unwrap();
658
659 let op = AppDataUpdateOperation::Update(payload.clone().into());
660 let changes = ComponentRegistryComponent::expand_to_changes(&op, None).unwrap();
661 assert_eq!(changes.len(), 1);
662 assert_eq!(changes[0].op, ComponentOp::Insert);
663
664 let new_bytes = ComponentRegistryComponent::apply_update_payload(&payload, None).unwrap();
665 let new = ComponentRegistryComponent::decode_value(&new_bytes).unwrap();
666 assert!(new.get(&ComponentId::new(0xC200)).is_some());
667 }
668
669 #[xmtp_common::test(unwrap_try = true)]
670 fn apply_rejects_non_delta_payload() {
671 let mut map: TlsMap<InboxId, VLBytes> = TlsMap::new();
676 map.insert(fixture_inbox_id(1), VLBytes::new(b"v1".to_vec()))
677 .unwrap();
678 let raw_map_bytes = map.tls_serialize_detached().unwrap();
679 let err = GroupMembershipComponent::apply_update_payload(&raw_map_bytes, None).unwrap_err();
680 assert!(
681 matches!(err, ComponentTypedError::TlsCodec(_)),
682 "expected TlsCodec decode error for non-delta payload, got {err:?}"
683 );
684 }
685
686 #[xmtp_common::test(unwrap_try = true)]
687 fn component_types_are_correct() {
688 assert_eq!(
689 GroupMembershipComponent::COMPONENT_TYPE,
690 ComponentType::TlsMapInboxIdBytes
691 );
692 assert_eq!(
693 ComponentRegistryComponent::COMPONENT_TYPE,
694 ComponentType::TlsMapBytesBytes
695 );
696 }
697}