1use crate::room_state::member::MemberId;
2use crate::room_state::privacy::SealedBytes;
3use crate::room_state::ChatRoomParametersV1;
4use crate::room_state::ChatRoomStateV1;
5use crate::util::{sign_struct, verify_struct};
6use ed25519_dalek::{Signature, SigningKey, VerifyingKey};
7use freenet_scaffold::ComposableState;
8use serde::{Deserialize, Serialize};
9use std::collections::{BTreeMap, HashMap};
10
11pub const MAX_DEPUTIES: usize = 64;
15
16#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
17pub struct MemberInfoV1 {
18 pub member_info: Vec<AuthorizedMemberInfo>,
19}
20
21impl MemberInfoV1 {
22 pub fn canonical(&self, member_id: MemberId) -> Option<&AuthorizedMemberInfo> {
40 let mut best: Option<(&AuthorizedMemberInfo, (u32, SigDigest))> = None;
53 for info in self
54 .member_info
55 .iter()
56 .filter(|info| info.member_info.member_id == member_id)
57 {
58 let rank = member_info_rank(info.member_info.version, &info.signature);
59 let better = match &best {
60 Some((_, best_rank)) => outranks(rank, *best_rank),
61 None => true,
62 };
63 if better {
64 best = Some((info, rank));
65 }
66 }
67 best.map(|(info, _)| info)
68 }
69
70 pub fn deputies_of(&self, member_id: MemberId) -> &[MemberId] {
75 self.canonical(member_id)
76 .map(|info| info.member_info.deputies.as_slice())
77 .unwrap_or(&[])
78 }
79
80 pub fn dedup_to_canonical(&mut self) {
93 if self.member_info.len() < 2 {
94 return;
95 }
96 let mut best: HashMap<MemberId, ((u32, SigDigest), AuthorizedMemberInfo)> = HashMap::new();
102 for info in self.member_info.drain(..) {
103 let id = info.member_info.member_id;
104 let rank = member_info_rank(info.member_info.version, &info.signature);
105 match best.entry(id) {
106 std::collections::hash_map::Entry::Occupied(mut e) => {
107 if outranks(rank, e.get().0) {
108 e.insert((rank, info));
109 }
110 }
111 std::collections::hash_map::Entry::Vacant(e) => {
112 e.insert((rank, info));
113 }
114 }
115 }
116 self.member_info = best.into_values().map(|(_, info)| info).collect();
117 self.member_info
119 .sort_by_key(|info| info.member_info.member_id);
120 }
121}
122
123fn member_info_rank(version: u32, signature: &Signature) -> (u32, SigDigest) {
154 (version, sig_digest(signature))
155}
156
157fn outranks(candidate: (u32, SigDigest), incumbent: (u32, SigDigest)) -> bool {
174 candidate > incumbent
175}
176
177#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
264pub struct SigDigest(pub [u8; 16]);
265
266impl Serialize for SigDigest {
267 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
268 serializer.serialize_bytes(&self.0)
269 }
270}
271
272impl<'de> Deserialize<'de> for SigDigest {
273 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
274 let bytes = <Vec<u8>>::deserialize(deserializer)?;
275 let arr: [u8; 16] = bytes.as_slice().try_into().map_err(|_| {
276 serde::de::Error::custom(format!(
277 "expected 16-byte SigDigest, got {} bytes",
278 bytes.len()
279 ))
280 })?;
281 Ok(SigDigest(arr))
282 }
283}
284
285fn sig_digest(signature: &Signature) -> SigDigest {
288 let digest = blake3::hash(signature.to_bytes().as_ref());
289 let mut out = [0u8; 16];
290 out.copy_from_slice(&digest.as_bytes()[..16]);
291 SigDigest(out)
292}
293
294impl ComposableState for MemberInfoV1 {
295 type ParentState = ChatRoomStateV1;
296 type Summary = BTreeMap<MemberId, (u32, SigDigest)>;
314 type Delta = Vec<AuthorizedMemberInfo>;
315 type Parameters = ChatRoomParametersV1;
316
317 fn verify(
318 &self,
319 parent_state: &Self::ParentState,
320 parameters: &Self::Parameters,
321 ) -> Result<(), String> {
322 let members_by_id = parent_state.members.members_by_member_id();
323 let owner_id = parameters.owner_id();
324
325 for member_info in &self.member_info {
339 let member_id = member_info.member_info.member_id;
340
341 if member_info.member_info.deputies.len() > MAX_DEPUTIES {
343 return Err(format!(
344 "Member {:?} lists {} deputies, exceeding the maximum of {}",
345 member_id,
346 member_info.member_info.deputies.len(),
347 MAX_DEPUTIES
348 ));
349 }
350
351 if member_id == owner_id {
352 member_info.verify_signature(parameters)?;
354 } else {
355 let member = members_by_id.get(&member_id).ok_or_else(|| {
357 format!("MemberInfo exists for non-existent member: {:?}", member_id)
358 })?;
359
360 member_info.verify_signature_with_key(&member.member.member_vk)?;
362 }
363 }
364 Ok(())
365 }
366
367 fn summarize(
368 &self,
369 _parent_state: &Self::ParentState,
370 _parameters: &Self::Parameters,
371 ) -> Self::Summary {
372 let mut summary: Self::Summary = BTreeMap::new();
384 for info in &self.member_info {
385 let candidate = member_info_rank(info.member_info.version, &info.signature);
388 summary
389 .entry(info.member_info.member_id)
390 .and_modify(|existing| {
391 if outranks(candidate, *existing) {
392 *existing = candidate;
393 }
394 })
395 .or_insert(candidate);
396 }
397 summary
398 }
399
400 fn delta(
401 &self,
402 _parent_state: &Self::ParentState,
403 _parameters: &Self::Parameters,
404 old_state_summary: &Self::Summary,
405 ) -> Option<Self::Delta> {
406 let delta: Vec<AuthorizedMemberInfo> = self
407 .member_info
408 .iter()
409 .filter(|info| {
410 match old_state_summary.get(&info.member_info.member_id) {
417 None => true,
418 Some(old_rank) => outranks(
421 member_info_rank(info.member_info.version, &info.signature),
422 *old_rank,
423 ),
424 }
425 })
426 .cloned()
427 .collect();
428
429 if delta.is_empty() {
430 None
431 } else {
432 Some(delta)
433 }
434 }
435
436 fn apply_delta(
437 &mut self,
438 parent_state: &Self::ParentState,
439 parameters: &Self::Parameters,
440 delta: &Option<Self::Delta>,
441 ) -> Result<(), String> {
442 let max_nickname_size = parent_state.configuration.configuration.max_nickname_size;
443
444 if let Some(delta) = delta {
445 for member_info in delta {
446 let member_id = &member_info.member_info.member_id;
447
448 if member_info.member_info.preferred_nickname.declared_len() > max_nickname_size {
450 return Err(format!(
451 "Nickname declared length {} exceeds max_nickname_size {}",
452 member_info.member_info.preferred_nickname.declared_len(),
453 max_nickname_size
454 ));
455 }
456
457 if member_info.member_info.deputies.len() > MAX_DEPUTIES {
468 continue;
469 }
470
471 if *member_id == parameters.owner_id() {
473 member_info.verify_signature(parameters)?;
475 } else {
476 let members = parent_state.members.members_by_member_id();
480 let member = match members.get(member_id) {
481 Some(m) => m,
482 None => continue,
483 };
484 member_info.verify_signature_with_key(&member.member.member_vk)?;
485 }
486
487 if let Some(existing_info) = self
496 .member_info
497 .iter_mut()
498 .find(|info| info.member_info.member_id == *member_id)
499 {
500 if outranks(
501 member_info_rank(member_info.member_info.version, &member_info.signature),
502 member_info_rank(
503 existing_info.member_info.version,
504 &existing_info.signature,
505 ),
506 ) {
507 *existing_info = member_info.clone();
508 }
509 } else {
510 self.member_info.push(member_info.clone());
511 }
512 }
513 }
514 let member_map = parent_state.members.members_by_member_id();
516 self.member_info.retain(|info| {
517 parameters.owner_id() == info.member_info.member_id
518 || member_map.contains_key(&info.member_info.member_id)
519 });
520
521 self.dedup_to_canonical();
534
535 Ok(())
536 }
537}
538
539#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
540pub struct AuthorizedMemberInfo {
541 pub member_info: MemberInfo,
542 pub signature: Signature,
543}
544
545impl AuthorizedMemberInfo {
546 pub fn new(member_info: MemberInfo, owner_signing_key: &SigningKey) -> Self {
547 let signature = sign_struct(&member_info, owner_signing_key);
548 Self {
549 member_info,
550 signature,
551 }
552 }
553
554 pub fn new_with_member_key(member_info: MemberInfo, member_signing_key: &SigningKey) -> Self {
555 let signature = sign_struct(&member_info, member_signing_key);
556 Self {
557 member_info,
558 signature,
559 }
560 }
561
562 pub fn with_signature(member_info: MemberInfo, signature: Signature) -> Self {
565 Self {
566 member_info,
567 signature,
568 }
569 }
570
571 pub fn verify_signature(&self, parameters: &ChatRoomParametersV1) -> Result<(), String> {
572 self.verify_signature_with_key(¶meters.owner)
573 }
574
575 pub fn verify_signature_with_key(&self, verifying_key: &VerifyingKey) -> Result<(), String> {
576 verify_struct(&self.member_info, &self.signature, verifying_key)
577 .map_err(|e| format!("Invalid signature: {}", e))
578 }
579
580 #[cfg(test)]
582 pub fn with_invalid_signature(mut self) -> Self {
583 self.signature = Signature::from_bytes(&[0; 64]);
584 self
585 }
586}
587
588#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
589pub struct MemberInfo {
590 pub member_id: MemberId,
591 pub version: u32,
592 pub preferred_nickname: SealedBytes,
593 #[serde(default, skip_serializing_if = "Vec::is_empty")]
608 pub deputies: Vec<MemberId>,
609}
610
611impl MemberInfo {
612 pub fn new_public(member_id: MemberId, version: u32, nickname: String) -> Self {
614 Self {
615 member_id,
616 version,
617 preferred_nickname: SealedBytes::public(nickname.into_bytes()),
618 deputies: Vec::new(),
619 }
620 }
621
622 pub fn new_private(
624 member_id: MemberId,
625 version: u32,
626 ciphertext: Vec<u8>,
627 nonce: [u8; 12],
628 secret_version: u32,
629 declared_len: u32,
630 ) -> Self {
631 Self {
632 member_id,
633 version,
634 preferred_nickname: SealedBytes::private(
635 ciphertext,
636 nonce,
637 secret_version,
638 declared_len,
639 ),
640 deputies: Vec::new(),
641 }
642 }
643}
644
645#[cfg(test)]
646mod tests {
647 use super::*;
648 use crate::room_state::member::{AuthorizedMember, Member};
649 use ed25519_dalek::{Signer, SigningKey};
650 use rand::rngs::OsRng;
651
652 fn create_test_member_info(member_id: MemberId) -> MemberInfo {
653 MemberInfo::new_public(member_id, 1, "TestUser".to_string())
654 }
655
656 #[test]
690 fn sig_digest_golden_vector() {
691 let mut raw = [0u8; 64];
695 for (i, b) in raw.iter_mut().enumerate() {
696 *b = i as u8;
697 }
698 let signature = Signature::from_bytes(&raw);
699 assert_eq!(
700 signature.to_bytes(),
701 raw,
702 "precondition: the fixture signature must round-trip to the bytes hashed"
703 );
704
705 const EXPECTED: [u8; 16] = [
706 0x4e, 0xed, 0x71, 0x41, 0xea, 0x4a, 0x5c, 0xd4, 0xb7, 0x88, 0x60, 0x6b, 0xd2, 0x3f,
707 0x46, 0xe2,
708 ];
709
710 let digest = sig_digest(&signature);
711 assert_eq!(
712 digest.0, EXPECTED,
713 "sig_digest changed. This is a WIRE-FORMAT and ORDERING change: every \
714 peer derives the tiebreak discriminator with this exact function, and \
715 freenet-core byte-compares summarize_state output for staleness. If \
716 the change is intended it re-keys the room contract — follow the \
717 migration ritual and update this vector deliberately."
718 );
719
720 let mut reversed = EXPECTED;
723 reversed.reverse();
724 assert_ne!(
725 digest.0, reversed,
726 "the golden vector must distinguish byte order"
727 );
728
729 let mut encoded = Vec::new();
734 ciborium::ser::into_writer(&digest, &mut encoded).expect("serialize SigDigest");
735 let mut want = vec![0x50u8];
736 want.extend_from_slice(&EXPECTED);
737 assert_eq!(
738 encoded, want,
739 "SigDigest must serialize as a 17-byte CBOR byte string"
740 );
741
742 let decoded: SigDigest =
745 ciborium::de::from_reader(encoded.as_slice()).expect("deserialize SigDigest");
746 assert_eq!(
747 decoded, digest,
748 "SigDigest must round-trip through ciborium"
749 );
750 }
751
752 #[test]
770 fn outranks_keeps_the_incumbent_on_a_tie() {
771 let lo = SigDigest([0x11; 16]);
772 let hi = SigDigest([0x22; 16]);
773
774 assert!(
776 !outranks((1, lo), (1, lo)),
777 "a tie must keep the INCUMBENT — all five consumers depend on this"
778 );
779
780 assert!(outranks((2, lo), (1, hi)), "higher version wins");
782 assert!(!outranks((1, hi), (2, lo)), "lower version loses");
783
784 assert!(
786 outranks((1, hi), (1, lo)),
787 "greater digest wins at equal version"
788 );
789 assert!(
790 !outranks((1, lo), (1, hi)),
791 "lesser digest loses at equal version"
792 );
793 }
794
795 #[test]
803 fn canonical_selects_by_digest_not_just_version() {
804 let mut csprng = OsRng;
805 let member_sk = SigningKey::generate(&mut csprng);
806 let member_id = MemberId::from(&member_sk.verifying_key());
807
808 let low_sig = Signature::from_bytes(&[1u8; 64]);
812 let high_sig = Signature::from_bytes(&[2u8; 64]);
813 assert!(
814 sig_digest(&high_sig) > sig_digest(&low_sig),
815 "fixture precondition: [2; 64] must digest greater than [1; 64]"
816 );
817
818 let mut winner_info = MemberInfo::new_public(member_id, 1, "Same".to_string());
819 winner_info.deputies = vec![member_id];
820 let winner = AuthorizedMemberInfo::with_signature(winner_info, high_sig);
821 let loser = AuthorizedMemberInfo::with_signature(
822 MemberInfo::new_public(member_id, 1, "Same".to_string()),
823 low_sig,
824 );
825
826 let state = MemberInfoV1 {
829 member_info: vec![loser, winner.clone()],
830 };
831
832 assert_eq!(
833 state.canonical(member_id),
834 Some(&winner),
835 "canonical must break the equal-version tie on the digest, not fall \
836 back to vector position"
837 );
838 assert_eq!(
839 state.deputies_of(member_id),
840 &[member_id],
841 "deputies_of must follow canonical's choice — this is the ban-authority path"
842 );
843 }
844
845 #[test]
854 fn canonical_and_dedup_break_rank_ties_identically() {
855 let mut csprng = OsRng;
856 let member_sk = SigningKey::generate(&mut csprng);
857 let member_id = MemberId::from(&member_sk.verifying_key());
858 let other_id = MemberId::from(&SigningKey::generate(&mut csprng).verifying_key());
859
860 let signed = AuthorizedMemberInfo::new_with_member_key(
861 MemberInfo::new_public(member_id, 1, "Tie".to_string()),
862 &member_sk,
863 );
864 let shared_signature = signed.signature;
865
866 let mut with_deputy = MemberInfo::new_public(member_id, 1, "Tie".to_string());
867 with_deputy.deputies = vec![other_id];
868
869 let first = AuthorizedMemberInfo::with_signature(
870 MemberInfo::new_public(member_id, 1, "Tie".to_string()),
871 shared_signature,
872 );
873 let second = AuthorizedMemberInfo::with_signature(with_deputy, shared_signature);
874
875 assert_ne!(first, second, "precondition: the records must differ");
876 assert_eq!(
877 member_info_rank(first.member_info.version, &first.signature),
878 member_info_rank(second.member_info.version, &second.signature),
879 "precondition: the records must nevertheless tie on rank"
880 );
881
882 let mut state = MemberInfoV1 {
883 member_info: vec![first.clone(), second],
884 };
885 let picked = state.canonical(member_id).cloned();
886 state.dedup_to_canonical();
887
888 assert_eq!(
889 state.member_info.len(),
890 1,
891 "dedup must collapse the tied duplicate"
892 );
893 assert_eq!(
894 picked.as_ref(),
895 state.member_info.first(),
896 "canonical() and dedup_to_canonical() must keep the SAME record on a tie \
897 (`max_by_key` keeps the LAST maximum, dedup keeps the FIRST — using both \
898 makes deputies_of flip after an unrelated apply_delta)"
899 );
900 assert_eq!(
902 picked.as_ref(),
903 Some(&first),
904 "the tie must keep the first record"
905 );
906 }
907
908 #[test]
926 fn empty_deputies_serializes_identically_to_legacy_member_info() {
927 use crate::util::{sign_struct, verify_struct};
928
929 #[derive(Serialize)]
931 struct OldMemberInfo {
932 member_id: MemberId,
933 version: u32,
934 preferred_nickname: SealedBytes,
935 }
936
937 let signing_key = SigningKey::generate(&mut OsRng);
938 let member_id: MemberId = signing_key.verifying_key().into();
939 let nickname = SealedBytes::public("LegacyNick".to_string().into_bytes());
940
941 let old = OldMemberInfo {
942 member_id,
943 version: 7,
944 preferred_nickname: nickname.clone(),
945 };
946 let new_empty = MemberInfo {
948 member_id,
949 version: 7,
950 preferred_nickname: nickname.clone(),
951 deputies: Vec::new(),
952 };
953
954 let mut old_bytes = Vec::new();
956 ciborium::ser::into_writer(&old, &mut old_bytes).unwrap();
957 let mut new_bytes = Vec::new();
958 ciborium::ser::into_writer(&new_empty, &mut new_bytes).unwrap();
959 assert_eq!(
960 old_bytes, new_bytes,
961 "MemberInfo with empty deputies MUST serialize byte-identically to \
962 the legacy 3-field record; dropping skip_serializing_if breaks this \
963 and strands every existing room (issue #410)"
964 );
965
966 let signature = sign_struct(&old, &signing_key);
968 assert!(
969 verify_struct(&new_empty, &signature, &signing_key.verifying_key()).is_ok(),
970 "signature over legacy MemberInfo bytes must still verify against the \
971 new struct with empty deputies (proves byte-identical serialization)"
972 );
973
974 let with_deputy = MemberInfo {
977 member_id,
978 version: 7,
979 preferred_nickname: nickname,
980 deputies: vec![member_id],
981 };
982 let mut with_deputy_bytes = Vec::new();
983 ciborium::ser::into_writer(&with_deputy, &mut with_deputy_bytes).unwrap();
984 assert_ne!(
985 old_bytes, with_deputy_bytes,
986 "a populated deputies list must change the serialized bytes"
987 );
988 }
989
990 #[test]
991 fn test_member_info_v1_default() {
992 let default_member_info = MemberInfoV1::default();
993 assert!(default_member_info.member_info.is_empty());
994 }
995
996 #[test]
997 fn test_member_info_v1_verify() {
998 let owner_signing_key = SigningKey::generate(&mut OsRng);
999 let owner_verifying_key = owner_signing_key.verifying_key();
1000 let owner_id = owner_verifying_key.into();
1001
1002 let member_signing_key = SigningKey::generate(&mut OsRng);
1003 let member_verifying_key = member_signing_key.verifying_key();
1004 let member_id = member_verifying_key.into();
1005
1006 let member_info = create_test_member_info(member_id);
1007 let authorized_member_info = AuthorizedMemberInfo::new(member_info, &member_signing_key);
1008
1009 let mut member_info_v1 = MemberInfoV1::default();
1010 member_info_v1
1011 .member_info
1012 .push(authorized_member_info.clone());
1013
1014 let mut parent_state = ChatRoomStateV1::default();
1015 let member = Member {
1016 owner_member_id: owner_id,
1017 invited_by: owner_id,
1018 member_vk: member_verifying_key,
1019 };
1020 let authorized_member = AuthorizedMember::new(member, &owner_signing_key);
1021 parent_state.members.members.push(authorized_member);
1022
1023 let parameters = ChatRoomParametersV1 {
1024 owner: owner_verifying_key,
1025 };
1026
1027 let result = member_info_v1.verify(&parent_state, ¶meters);
1028 assert!(
1029 result.is_ok(),
1030 "Verification failed: {}",
1031 result.unwrap_err()
1032 );
1033
1034 let non_existent_member_id = SigningKey::generate(&mut OsRng).verifying_key().into();
1036 let non_existent_member_info = create_test_member_info(non_existent_member_id);
1037 let non_existent_authorized_member_info =
1038 AuthorizedMemberInfo::new(non_existent_member_info, &owner_signing_key);
1039 member_info_v1
1040 .member_info
1041 .push(non_existent_authorized_member_info);
1042
1043 let verify_result = member_info_v1.verify(&parent_state, ¶meters);
1044 assert!(
1045 verify_result.is_err(),
1046 "Expected verification to fail, but it succeeded"
1047 );
1048 if let Err(err) = verify_result {
1049 assert!(
1050 err.contains("MemberInfo exists for non-existent member"),
1051 "Unexpected error message: {}",
1052 err
1053 );
1054 }
1055
1056 let invalid_authorized_member_info = authorized_member_info.with_invalid_signature();
1058 member_info_v1.member_info.clear();
1059 member_info_v1
1060 .member_info
1061 .push(invalid_authorized_member_info);
1062
1063 let verify_result = member_info_v1.verify(&parent_state, ¶meters);
1064 assert!(
1065 verify_result.is_err(),
1066 "Expected verification to fail, but it succeeded"
1067 );
1068 if let Err(err) = verify_result {
1069 assert!(
1070 err.contains("Invalid signature"),
1071 "Unexpected error message: {}",
1072 err
1073 );
1074 }
1075 }
1076
1077 #[test]
1078 fn test_member_info_v1_summarize() {
1079 let owner_signing_key = SigningKey::generate(&mut OsRng);
1080 let member_id = SigningKey::generate(&mut OsRng).verifying_key().into();
1081 let member_info = create_test_member_info(member_id);
1082 let authorized_member_info = AuthorizedMemberInfo::new(member_info, &owner_signing_key);
1083
1084 let mut member_info_v1 = MemberInfoV1::default();
1085 member_info_v1.member_info.push(authorized_member_info);
1086
1087 let parent_state = ChatRoomStateV1::default();
1088 let parameters = ChatRoomParametersV1 {
1089 owner: owner_signing_key.verifying_key(),
1090 };
1091
1092 let summary = member_info_v1.summarize(&parent_state, ¶meters);
1093 assert_eq!(summary.len(), 1);
1094 assert!(summary.contains_key(&member_id));
1095 assert_eq!(summary.get(&member_id).unwrap().0, 1); }
1097
1098 #[test]
1099 fn test_member_info_v1_delta() {
1100 let owner_signing_key = SigningKey::generate(&mut OsRng);
1101 let member_id1 = SigningKey::generate(&mut OsRng).verifying_key().into();
1102 let member_id2 = SigningKey::generate(&mut OsRng).verifying_key().into();
1103
1104 let member_info1 = create_test_member_info(member_id1);
1105 let member_info2 = create_test_member_info(member_id2);
1106
1107 let authorized_member_info1 = AuthorizedMemberInfo::new(member_info1, &owner_signing_key);
1108 let authorized_member_info2 = AuthorizedMemberInfo::new(member_info2, &owner_signing_key);
1109 let sig1 = authorized_member_info1.signature;
1111
1112 let mut member_info_v1 = MemberInfoV1::default();
1113 member_info_v1.member_info.push(authorized_member_info1);
1114 member_info_v1.member_info.push(authorized_member_info2);
1115
1116 let parent_state = ChatRoomStateV1::default();
1117 let parameters = ChatRoomParametersV1 {
1118 owner: owner_signing_key.verifying_key(),
1119 };
1120
1121 let mut old_summary = BTreeMap::new();
1124 old_summary.insert(member_id1, member_info_rank(1, &sig1));
1125
1126 let delta = member_info_v1.delta(&parent_state, ¶meters, &old_summary);
1127
1128 assert!(delta.is_some());
1129 let delta = delta.unwrap();
1130 assert_eq!(delta.len(), 1);
1131 assert_eq!(delta[0].member_info.member_id, member_id2);
1132 }
1133
1134 #[test]
1135 fn test_member_info_v1_apply_delta() {
1136 let owner_signing_key = SigningKey::generate(&mut OsRng);
1137 let owner_verifying_key = owner_signing_key.verifying_key();
1138 let owner_id = owner_verifying_key.into();
1139
1140 let member_signing_key = SigningKey::generate(&mut OsRng);
1141 let member_verifying_key = member_signing_key.verifying_key();
1142 let member_id = member_verifying_key.into();
1143
1144 let member_info = create_test_member_info(member_id);
1145 let authorized_member_info =
1146 AuthorizedMemberInfo::new_with_member_key(member_info, &member_signing_key);
1147
1148 let mut member_info_v1 = MemberInfoV1::default();
1149 let delta = vec![authorized_member_info.clone()];
1150
1151 let mut parent_state = ChatRoomStateV1::default();
1152 parent_state.members.members.push(AuthorizedMember {
1153 member: Member {
1154 owner_member_id: owner_id,
1155 invited_by: owner_id,
1156 member_vk: member_verifying_key,
1157 },
1158 signature: owner_signing_key
1159 .sign("TestUser".as_bytes())
1160 .to_bytes()
1161 .into(),
1162 });
1163
1164 let parameters = ChatRoomParametersV1 {
1165 owner: owner_verifying_key,
1166 };
1167
1168 println!("Applying delta with a new member");
1170 let result = member_info_v1.apply_delta(&parent_state, ¶meters, &Some(delta));
1171 println!("Result: {:?}", result);
1172 assert!(result.is_ok(), "Failed to apply delta: {:?}", result.err());
1173 assert_eq!(member_info_v1.member_info.len(), 1);
1174 assert_eq!(member_info_v1.member_info[0], authorized_member_info);
1175
1176 println!("Applying delta with an existing member (update)");
1178 let updated_member_info =
1179 MemberInfo::new_public(member_id, 2, "UpdatedNickname".to_string());
1180 let updated_authorized_member_info =
1181 AuthorizedMemberInfo::new_with_member_key(updated_member_info, &member_signing_key);
1182 let update_delta = vec![updated_authorized_member_info.clone()];
1183
1184 let result = member_info_v1.apply_delta(&parent_state, ¶meters, &Some(update_delta));
1185 println!("Result: {:?}", result);
1186 assert!(
1187 result.is_ok(),
1188 "Failed to apply update delta: {:?}",
1189 result.err()
1190 );
1191 assert_eq!(member_info_v1.member_info.len(), 1);
1192 assert_eq!(
1193 member_info_v1.member_info[0],
1194 updated_authorized_member_info
1195 );
1196
1197 println!("Applying delta with a non-existent member");
1199 let non_existent_member_id = SigningKey::generate(&mut OsRng).verifying_key().into();
1200 let non_existent_member_info = create_test_member_info(non_existent_member_id);
1201 let non_existent_authorized_member_info = AuthorizedMemberInfo::new_with_member_key(
1202 non_existent_member_info,
1203 &SigningKey::generate(&mut OsRng),
1204 );
1205 let non_existent_delta = vec![non_existent_authorized_member_info];
1206
1207 let prev_len = member_info_v1.member_info.len();
1208 let result =
1209 member_info_v1.apply_delta(&parent_state, ¶meters, &Some(non_existent_delta));
1210 println!("Result: {:?}", result);
1211 assert!(
1212 result.is_ok(),
1213 "Non-existent member should be silently skipped"
1214 );
1215 assert_eq!(
1216 member_info_v1.member_info.len(),
1217 prev_len,
1218 "Entry should not be added"
1219 );
1220
1221 println!("Applying delta with an older version");
1223 let older_member_info = MemberInfo::new_public(member_id, 1, "TestUser".to_string());
1224 let older_authorized_member_info =
1225 AuthorizedMemberInfo::new_with_member_key(older_member_info, &member_signing_key);
1226 let older_delta = vec![older_authorized_member_info];
1227
1228 let result = member_info_v1.apply_delta(&parent_state, ¶meters, &Some(older_delta));
1229 println!("Result: {:?}", result);
1230 assert!(
1231 result.is_ok(),
1232 "Failed to apply older version delta: {:?}",
1233 result.err()
1234 );
1235 assert_eq!(member_info_v1.member_info.len(), 1);
1236 assert_eq!(member_info_v1.member_info[0].member_info.version, 2);
1237
1238 println!("Applying delta with multiple members");
1240 let new_member_signing_key = SigningKey::generate(&mut OsRng);
1241 let new_member_verifying_key = new_member_signing_key.verifying_key();
1242 let new_member_id = new_member_verifying_key.into();
1243 let new_member_info = create_test_member_info(new_member_id);
1244 let new_authorized_member_info =
1245 AuthorizedMemberInfo::new_with_member_key(new_member_info, &new_member_signing_key);
1246
1247 parent_state.members.members.push(AuthorizedMember {
1248 member: Member {
1249 owner_member_id: owner_id,
1250 invited_by: owner_id,
1251 member_vk: new_member_verifying_key,
1252 },
1253 signature: owner_signing_key
1254 .sign("NewTestUser".as_bytes())
1255 .to_bytes()
1256 .into(),
1257 });
1258
1259 let multi_delta = vec![
1260 updated_authorized_member_info.clone(),
1261 new_authorized_member_info.clone(),
1262 ];
1263
1264 let result = member_info_v1.apply_delta(&parent_state, ¶meters, &Some(multi_delta));
1265 println!("Result: {:?}", result);
1266 assert!(
1267 result.is_ok(),
1268 "Failed to apply multi-member delta: {:?}",
1269 result.err()
1270 );
1271 assert_eq!(member_info_v1.member_info.len(), 2);
1272 assert!(member_info_v1
1273 .member_info
1274 .contains(&updated_authorized_member_info));
1275 assert!(member_info_v1
1276 .member_info
1277 .contains(&new_authorized_member_info));
1278 }
1279
1280 #[test]
1281 fn test_authorized_member_info_new_and_verify() {
1282 let owner_signing_key = SigningKey::generate(&mut OsRng);
1283 let member_id = SigningKey::generate(&mut OsRng).verifying_key().into();
1284 let member_info = create_test_member_info(member_id);
1285
1286 let authorized_member_info =
1287 AuthorizedMemberInfo::new(member_info.clone(), &owner_signing_key);
1288
1289 let parameters = ChatRoomParametersV1 {
1290 owner: owner_signing_key.verifying_key(),
1291 };
1292
1293 assert!(authorized_member_info.verify_signature(¶meters).is_ok());
1294
1295 let wrong_key = SigningKey::generate(&mut OsRng).verifying_key();
1297 let wrong_parameters = ChatRoomParametersV1 { owner: wrong_key };
1298 assert!(authorized_member_info
1299 .verify_signature(&wrong_parameters)
1300 .is_err());
1301 }
1302
1303 #[test]
1304 fn test_member_info_v1_delta_scenarios() {
1305 let owner_signing_key = SigningKey::generate(&mut OsRng);
1306 let owner_verifying_key = owner_signing_key.verifying_key();
1307
1308 let mut member_info_v1 = MemberInfoV1::default();
1309 let parent_state = ChatRoomStateV1::default();
1310 let parameters = ChatRoomParametersV1 {
1311 owner: owner_verifying_key,
1312 };
1313
1314 let member_infos: Vec<AuthorizedMemberInfo> = (0..5)
1316 .map(|_| {
1317 let member_id = SigningKey::generate(&mut OsRng).verifying_key().into();
1318 let member_info = create_test_member_info(member_id);
1319 AuthorizedMemberInfo::new(member_info, &owner_signing_key)
1320 })
1321 .collect();
1322
1323 member_info_v1.member_info = member_infos.clone();
1325 let delta = member_info_v1.delta(&parent_state, ¶meters, &BTreeMap::new());
1326 assert_eq!(delta.unwrap().len(), 5);
1327
1328 let old_summary: BTreeMap<MemberId, (u32, SigDigest)> = member_infos
1331 .iter()
1332 .map(|info| {
1333 (
1334 info.member_info.member_id,
1335 member_info_rank(info.member_info.version, &info.signature),
1336 )
1337 })
1338 .collect();
1339 let delta = member_info_v1.delta(&parent_state, ¶meters, &old_summary);
1340 assert!(delta.is_none());
1341
1342 let mut old_summary = BTreeMap::new();
1344 old_summary.insert(
1345 member_infos[0].member_info.member_id,
1346 member_info_rank(1, &member_infos[0].signature),
1347 );
1348 old_summary.insert(
1349 member_infos[1].member_info.member_id,
1350 member_info_rank(1, &member_infos[1].signature),
1351 );
1352 let delta = member_info_v1.delta(&parent_state, ¶meters, &old_summary);
1353 assert_eq!(delta.unwrap().len(), 3);
1354
1355 let mut updated_member_info = member_infos[0].clone();
1357 updated_member_info.member_info.version = 2;
1358 member_info_v1.member_info[0] = updated_member_info;
1359
1360 let delta = member_info_v1.delta(&parent_state, ¶meters, &old_summary);
1361 assert_eq!(delta.unwrap().len(), 4); }
1363
1364 #[test]
1365 fn test_member_info_version_handling() {
1366 let owner_signing_key = SigningKey::generate(&mut OsRng);
1367 let owner_verifying_key = owner_signing_key.verifying_key();
1368 let owner_id = owner_verifying_key.into();
1369
1370 let member_signing_key = SigningKey::generate(&mut OsRng);
1372 let member_verifying_key = member_signing_key.verifying_key();
1373 let member_id = member_verifying_key.into();
1374
1375 let member_info_v1 = create_test_member_info(member_id);
1377 let authorized_member_info_v1 =
1378 AuthorizedMemberInfo::new_with_member_key(member_info_v1, &member_signing_key);
1379
1380 let member_info_v2 = MemberInfo::new_public(member_id, 2, "UpdatedNickname".to_string());
1382 let authorized_member_info_v2 =
1383 AuthorizedMemberInfo::new_with_member_key(member_info_v2, &member_signing_key);
1384
1385 let mut member_info_state = MemberInfoV1::default();
1387 member_info_state
1388 .member_info
1389 .push(authorized_member_info_v1.clone());
1390
1391 let mut parent_state = ChatRoomStateV1::default();
1393 let member = Member {
1394 owner_member_id: owner_id,
1395 invited_by: owner_id,
1396 member_vk: member_verifying_key,
1397 };
1398 let authorized_member = AuthorizedMember::new(member, &owner_signing_key);
1399 parent_state.members.members.push(authorized_member);
1400
1401 let parameters = ChatRoomParametersV1 {
1402 owner: owner_verifying_key,
1403 };
1404
1405 let summary = member_info_state.summarize(&parent_state, ¶meters);
1407 assert_eq!(summary.get(&member_id).unwrap().0, 1);
1408
1409 let mut updated_state = MemberInfoV1::default();
1411 updated_state
1412 .member_info
1413 .push(authorized_member_info_v2.clone());
1414
1415 let delta = updated_state.delta(&parent_state, ¶meters, &summary);
1416 assert!(delta.is_some());
1417 assert_eq!(delta.as_ref().unwrap().len(), 1);
1418 assert_eq!(delta.as_ref().unwrap()[0].member_info.version, 2);
1419
1420 member_info_state
1422 .apply_delta(&parent_state, ¶meters, &delta)
1423 .unwrap();
1424 assert_eq!(member_info_state.member_info.len(), 1);
1425 assert_eq!(member_info_state.member_info[0].member_info.version, 2);
1426 assert_eq!(
1427 member_info_state.member_info[0]
1428 .member_info
1429 .preferred_nickname,
1430 SealedBytes::public("UpdatedNickname".to_string().into_bytes())
1431 );
1432 }
1433
1434 #[test]
1435 fn test_room_owner_member_info() {
1436 let owner_signing_key = SigningKey::generate(&mut OsRng);
1437 let owner_verifying_key = owner_signing_key.verifying_key();
1438 let owner_id = owner_verifying_key.into();
1439
1440 let owner_member_info = create_test_member_info(owner_id);
1441 let authorized_owner_info =
1442 AuthorizedMemberInfo::new(owner_member_info, &owner_signing_key);
1443
1444 let mut member_info_v1 = MemberInfoV1::default();
1445 member_info_v1.member_info.push(authorized_owner_info);
1446
1447 let mut parent_state = ChatRoomStateV1::default();
1448 parent_state.members.members.push(AuthorizedMember {
1449 member: Member {
1450 owner_member_id: owner_id,
1451 invited_by: owner_id,
1452 member_vk: owner_verifying_key,
1453 },
1454 signature: owner_signing_key
1455 .sign("TestOwner".as_bytes())
1456 .to_bytes()
1457 .into(),
1458 });
1459
1460 let parameters = ChatRoomParametersV1 {
1461 owner: owner_verifying_key,
1462 };
1463
1464 let result = member_info_v1.verify(&parent_state, ¶meters);
1465 assert!(
1466 result.is_ok(),
1467 "Room owner should be allowed to have member info: {:?}",
1468 result
1469 );
1470 }
1471
1472 #[test]
1473 fn test_member_info_retention() {
1474 let owner_signing_key = SigningKey::generate(&mut OsRng);
1475 let owner_verifying_key = owner_signing_key.verifying_key();
1476 let owner_id = owner_verifying_key.into();
1477
1478 let owner_member_info = create_test_member_info(owner_id);
1480 let authorized_owner_info =
1481 AuthorizedMemberInfo::new(owner_member_info, &owner_signing_key);
1482
1483 let member_signing_key = SigningKey::generate(&mut OsRng);
1485 let member_verifying_key = member_signing_key.verifying_key();
1486 let member_id = member_verifying_key.into();
1487 let member_info = create_test_member_info(member_id);
1488 let authorized_member_info =
1489 AuthorizedMemberInfo::new_with_member_key(member_info, &member_signing_key);
1490
1491 let mut member_info_v1 = MemberInfoV1::default();
1493 member_info_v1
1494 .member_info
1495 .push(authorized_owner_info.clone());
1496 member_info_v1
1497 .member_info
1498 .push(authorized_member_info.clone());
1499
1500 let mut parent_state = ChatRoomStateV1::default();
1502 parent_state.members.members.push(AuthorizedMember {
1503 member: Member {
1504 owner_member_id: owner_id,
1505 invited_by: owner_id,
1506 member_vk: member_verifying_key,
1507 },
1508 signature: owner_signing_key
1509 .sign("TestMember".as_bytes())
1510 .to_bytes()
1511 .into(),
1512 });
1513
1514 let parameters = ChatRoomParametersV1 {
1515 owner: owner_verifying_key,
1516 };
1517
1518 let result = member_info_v1.apply_delta(&parent_state, ¶meters, &Some(vec![]));
1520 assert!(result.is_ok(), "Failed to apply delta: {:?}", result.err());
1521
1522 assert!(
1524 member_info_v1
1525 .member_info
1526 .iter()
1527 .any(|info| info.member_info.member_id == owner_id),
1528 "Owner's member info should be retained"
1529 );
1530
1531 parent_state.members.members.clear();
1533
1534 let result = member_info_v1.apply_delta(&parent_state, ¶meters, &Some(vec![]));
1536 assert!(
1537 result.is_ok(),
1538 "Failed to apply second delta: {:?}",
1539 result.err()
1540 );
1541
1542 assert_eq!(
1544 member_info_v1.member_info.len(),
1545 1,
1546 "Should only contain owner's info"
1547 );
1548 assert_eq!(
1549 member_info_v1.member_info[0].member_info.member_id, owner_id,
1550 "Remaining info should be owner's"
1551 );
1552 }
1553
1554 #[test]
1558 fn test_apply_delta_with_removed_member_info() {
1559 let owner_signing_key = SigningKey::generate(&mut OsRng);
1560 let owner_verifying_key = owner_signing_key.verifying_key();
1561 let owner_id = owner_verifying_key.into();
1562
1563 let member_signing_key = SigningKey::generate(&mut OsRng);
1564 let member_verifying_key = member_signing_key.verifying_key();
1565 let member_id = member_verifying_key.into();
1566
1567 let member_info = create_test_member_info(member_id);
1569 let authorized_member_info =
1570 AuthorizedMemberInfo::new_with_member_key(member_info, &member_signing_key);
1571
1572 let mut member_info_v1 = MemberInfoV1 {
1573 member_info: vec![authorized_member_info.clone()],
1574 };
1575
1576 let parent_state = ChatRoomStateV1::default();
1578 let parameters = ChatRoomParametersV1 {
1579 owner: owner_verifying_key,
1580 };
1581
1582 let updated_info = MemberInfo::new_public(member_id, 2, "NewNick".to_string());
1584 let updated_authorized =
1585 AuthorizedMemberInfo::new_with_member_key(updated_info, &member_signing_key);
1586 let delta = vec![updated_authorized];
1587
1588 let result = member_info_v1.apply_delta(&parent_state, ¶meters, &Some(delta));
1590 assert!(
1591 result.is_ok(),
1592 "apply_delta should skip removed member's info, got: {:?}",
1593 result.err()
1594 );
1595
1596 assert!(
1598 !member_info_v1
1599 .member_info
1600 .iter()
1601 .any(|info| info.member_info.member_id == member_id),
1602 "Removed member's info should be pruned"
1603 );
1604
1605 let owner_info = create_test_member_info(owner_id);
1607 let authorized_owner = AuthorizedMemberInfo::new(owner_info, &owner_signing_key);
1608 member_info_v1.member_info.push(authorized_owner.clone());
1609
1610 let result = member_info_v1.apply_delta(&parent_state, ¶meters, &None);
1611 assert!(result.is_ok());
1612 assert_eq!(member_info_v1.member_info.len(), 1);
1613 assert_eq!(
1614 member_info_v1.member_info[0].member_info.member_id,
1615 owner_id
1616 );
1617 }
1618
1619 #[test]
1627 fn duplicate_member_info_reads_canonicalize_to_highest_rank() {
1628 let owner_signing_key = SigningKey::generate(&mut OsRng);
1629 let owner_verifying_key = owner_signing_key.verifying_key();
1630 let owner_id = owner_verifying_key.into();
1631
1632 let member_signing_key = SigningKey::generate(&mut OsRng);
1633 let member_verifying_key = member_signing_key.verifying_key();
1634 let member_id = member_verifying_key.into();
1635
1636 let deputy = SigningKey::generate(&mut OsRng).verifying_key().into();
1637
1638 let mut grant_mi = MemberInfo::new_public(member_id, 1, "nick".to_string());
1640 grant_mi.deputies = vec![deputy];
1641 let grant = AuthorizedMemberInfo::new_with_member_key(grant_mi, &member_signing_key);
1642
1643 let mut revoke_mi = MemberInfo::new_public(member_id, 2, "nick".to_string());
1645 revoke_mi.deputies = vec![];
1646 let revoke = AuthorizedMemberInfo::new_with_member_key(revoke_mi, &member_signing_key);
1647 let revoke_summary_value = member_info_rank(2u32, &revoke.signature);
1648
1649 let mut parent_state = ChatRoomStateV1::default();
1650 parent_state.members.members.push(AuthorizedMember::new(
1651 Member {
1652 owner_member_id: owner_id,
1653 invited_by: owner_id,
1654 member_vk: member_verifying_key,
1655 },
1656 &owner_signing_key,
1657 ));
1658 let parameters = ChatRoomParametersV1 {
1659 owner: owner_verifying_key,
1660 };
1661
1662 for (label, entries) in [
1663 ("grant-then-revoke", vec![grant.clone(), revoke.clone()]),
1664 ("revoke-then-grant", vec![revoke.clone(), grant.clone()]),
1665 ] {
1666 let state = MemberInfoV1 {
1667 member_info: entries,
1668 };
1669
1670 assert!(
1672 state.verify(&parent_state, ¶meters).is_ok(),
1673 "verify must ACCEPT a duplicate-member_info state ({label})"
1674 );
1675
1676 assert_eq!(
1678 state.deputies_of(member_id),
1679 &[] as &[MemberId],
1680 "deputies_of must return the v2 (revoke) result ({label})"
1681 );
1682
1683 let summary = state.summarize(&parent_state, ¶meters);
1685 assert_eq!(
1686 summary.get(&member_id).copied(),
1687 Some(revoke_summary_value),
1688 "summarize must advertise the v2 (revoke) (version, digest) ({label})"
1689 );
1690 }
1691 }
1692
1693 #[test]
1699 fn deputies_of_picks_highest_rank_record() {
1700 let member_signing_key = SigningKey::generate(&mut OsRng);
1701 let member_id = member_signing_key.verifying_key().into();
1702
1703 let deputy_x = SigningKey::generate(&mut OsRng).verifying_key().into();
1704 let deputy_y = SigningKey::generate(&mut OsRng).verifying_key().into();
1705
1706 let mut mi_v1 = MemberInfo::new_public(member_id, 1, "nick".to_string());
1708 mi_v1.deputies = vec![deputy_x];
1709 let low = AuthorizedMemberInfo::new_with_member_key(mi_v1, &member_signing_key);
1710
1711 let mut mi_v2 = MemberInfo::new_public(member_id, 2, "nick".to_string());
1712 mi_v2.deputies = vec![deputy_y];
1713 let high = AuthorizedMemberInfo::new_with_member_key(mi_v2, &member_signing_key);
1714
1715 let member_info_v1 = MemberInfoV1 {
1717 member_info: vec![low, high],
1718 };
1719
1720 assert_eq!(
1721 member_info_v1.deputies_of(member_id),
1722 &[deputy_y],
1723 "deputies_of must return the highest-version record's deputies"
1724 );
1725
1726 let mut mi_v1b = MemberInfo::new_public(member_id, 1, "nick".to_string());
1728 mi_v1b.deputies = vec![deputy_x];
1729 let low_b = AuthorizedMemberInfo::new_with_member_key(mi_v1b, &member_signing_key);
1730 let mut mi_v2b = MemberInfo::new_public(member_id, 2, "nick".to_string());
1731 mi_v2b.deputies = vec![deputy_y];
1732 let high_b = AuthorizedMemberInfo::new_with_member_key(mi_v2b, &member_signing_key);
1733 let reversed = MemberInfoV1 {
1734 member_info: vec![high_b, low_b],
1735 };
1736 assert_eq!(
1737 reversed.deputies_of(member_id),
1738 &[deputy_y],
1739 "deputies_of result must be independent of vector order"
1740 );
1741 }
1742
1743 #[test]
1748 fn canonical_and_dedup_to_canonical_pick_highest_rank() {
1749 let owner_sk = SigningKey::generate(&mut OsRng);
1750 let owner_id: MemberId = owner_sk.verifying_key().into();
1751
1752 let m_sk = SigningKey::generate(&mut OsRng);
1753 let m_id: MemberId = m_sk.verifying_key().into();
1754 let n_sk = SigningKey::generate(&mut OsRng);
1755 let n_id: MemberId = n_sk.verifying_key().into();
1756
1757 let dep = SigningKey::generate(&mut OsRng).verifying_key().into();
1758 let mut g = MemberInfo::new_public(m_id, 1, "nick".to_string());
1760 g.deputies = vec![dep];
1761 let grant = AuthorizedMemberInfo::new_with_member_key(g, &m_sk);
1762 let mut r = MemberInfo::new_public(m_id, 2, "nick".to_string());
1763 r.deputies = vec![];
1764 let revoke = AuthorizedMemberInfo::new_with_member_key(r, &m_sk);
1765 let n_info = AuthorizedMemberInfo::new_with_member_key(
1767 MemberInfo::new_public(n_id, 3, "n".to_string()),
1768 &n_sk,
1769 );
1770 let owner_info = AuthorizedMemberInfo::new(
1771 MemberInfo::new_public(owner_id, 1, "o".to_string()),
1772 &owner_sk,
1773 );
1774
1775 for order in [
1776 vec![
1777 grant.clone(),
1778 revoke.clone(),
1779 n_info.clone(),
1780 owner_info.clone(),
1781 ],
1782 vec![
1783 owner_info.clone(),
1784 n_info.clone(),
1785 revoke.clone(),
1786 grant.clone(),
1787 ],
1788 ] {
1789 let mut mi = MemberInfoV1 { member_info: order };
1790
1791 assert_eq!(
1793 mi.canonical(m_id).map(|c| c.member_info.version),
1794 Some(2),
1795 "canonical(M) must be the v2 revoke"
1796 );
1797 assert_eq!(mi.canonical(n_id).map(|c| c.member_info.version), Some(3));
1798 assert_eq!(
1799 mi.canonical(owner_id).map(|c| c.member_info.version),
1800 Some(1)
1801 );
1802 assert!(mi
1803 .canonical(SigningKey::generate(&mut OsRng).verifying_key().into())
1804 .is_none());
1805
1806 mi.dedup_to_canonical();
1808 assert_eq!(mi.member_info.len(), 3, "exactly one record per member_id");
1809 let m_rec = mi
1810 .member_info
1811 .iter()
1812 .find(|i| i.member_info.member_id == m_id)
1813 .unwrap();
1814 assert_eq!(
1815 m_rec.member_info.version, 2,
1816 "M's kept record is the revoke"
1817 );
1818 assert!(
1819 m_rec.member_info.deputies.is_empty(),
1820 "revoke has no deputies"
1821 );
1822 assert!(mi
1823 .member_info
1824 .iter()
1825 .any(|i| i.member_info.member_id == owner_id));
1826 let before = mi.clone();
1828 mi.dedup_to_canonical();
1829 assert_eq!(before, mi, "dedup_to_canonical is idempotent");
1830 }
1831 }
1832
1833 #[test]
1838 fn apply_delta_dedups_preexisting_duplicates_to_canonical() {
1839 let owner_sk = SigningKey::generate(&mut OsRng);
1840 let owner_vk = owner_sk.verifying_key();
1841 let owner_id = owner_vk.into();
1842
1843 let m_sk = SigningKey::generate(&mut OsRng);
1844 let m_vk = m_sk.verifying_key();
1845 let m_id: MemberId = m_vk.into();
1846
1847 let dep = SigningKey::generate(&mut OsRng).verifying_key().into();
1848 let mut g = MemberInfo::new_public(m_id, 1, "n".to_string());
1849 g.deputies = vec![dep];
1850 let grant = AuthorizedMemberInfo::new_with_member_key(g, &m_sk);
1851 let mut r = MemberInfo::new_public(m_id, 2, "n".to_string());
1852 r.deputies = vec![];
1853 let revoke = AuthorizedMemberInfo::new_with_member_key(r, &m_sk);
1854
1855 let mut mi = MemberInfoV1 {
1857 member_info: vec![grant, revoke],
1858 };
1859
1860 let mut parent_state = ChatRoomStateV1::default();
1861 parent_state.members.members.push(AuthorizedMember::new(
1862 Member {
1863 owner_member_id: owner_id,
1864 invited_by: owner_id,
1865 member_vk: m_vk,
1866 },
1867 &owner_sk,
1868 ));
1869 let params = ChatRoomParametersV1 { owner: owner_vk };
1870
1871 mi.apply_delta(&parent_state, ¶ms, &None).unwrap();
1873
1874 let recs: Vec<_> = mi
1875 .member_info
1876 .iter()
1877 .filter(|i| i.member_info.member_id == m_id)
1878 .collect();
1879 assert_eq!(
1880 recs.len(),
1881 1,
1882 "apply_delta must collapse the pre-existing duplicate on the write path"
1883 );
1884 assert_eq!(recs[0].member_info.version, 2, "canonical (revoke) kept");
1885 assert!(recs[0].member_info.deputies.is_empty());
1886 }
1887}