1use super::derive::channel_pseudonym;
17use super::{cipher, edition, roles, version, ChannelId, ChannelKey, CommunityId, Epoch, ServerRootKey};
18use crate::stored_event::event_kind;
19use nostr_sdk::prelude::*;
20use std::collections::HashMap;
21
22const VSK_COMMUNITY_ROOT: &str = "0";
25const VSK_ROLE: &str = "1";
26const VSK_CHANNEL: &str = "2";
27const VSK_GRANT: &str = "3";
28const VSK_BANLIST: &str = "4";
29const VSK_INVITE_LINKS: &str = "8";
33const VSK_DISSOLVED: &str = "10";
38
39pub(crate) const MAX_CONTROL_EDITIONS: usize = 50_000;
43
44fn hex32(s: &str) -> Option<[u8; 32]> {
47 (s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit()))
48 .then(|| crate::simd::hex::hex_to_bytes_32(s))
49}
50
51fn check_chain_shape(version: u64, prev_hash: Option<&[u8; 32]>) -> Result<(), String> {
57 match (version, prev_hash) {
58 (0, _) => Err("edition version starts at 1".to_string()),
59 (1, Some(_)) => Err("genesis edition (v1) must have no prev_hash".to_string()),
60 (v, None) if v > 1 => Err("continuation edition (v>1) requires a prev_hash".to_string()),
61 _ => Ok(()),
62 }
63}
64
65pub fn build_role_edition(
71 actor: &Keys,
72 role: &roles::Role,
73 version: u64,
74 prev_hash: Option<&[u8; 32]>,
75 created_at: u64,
76 authority: Option<&edition::AuthorityCitation>,
77) -> Result<Event, String> {
78 build_role_edition_unsigned(actor.public_key(), role, version, prev_hash, created_at, authority)?
79 .finalize(actor)
80 .map_err(|e| format!("sign role edition: {e}"))
81}
82
83pub fn build_role_edition_unsigned(
87 author: PublicKey,
88 role: &roles::Role,
89 version: u64,
90 prev_hash: Option<&[u8; 32]>,
91 created_at: u64,
92 authority: Option<&edition::AuthorityCitation>,
93) -> Result<UnsignedEvent, String> {
94 check_chain_shape(version, prev_hash)?;
95 let entity_id = hex32(&role.role_id).ok_or("role_id must be 64-char hex")?;
96 let content = serde_json::to_string(role).map_err(|e| e.to_string())?;
97 Ok(edition::build_edition_inner(author, VSK_ROLE, &entity_id, version, prev_hash, &content, created_at, authority))
98}
99
100pub fn build_grant_edition(
105 actor: &Keys,
106 community_id: &CommunityId,
107 grant: &roles::MemberGrant,
108 version: u64,
109 prev_hash: Option<&[u8; 32]>,
110 created_at: u64,
111 authority: Option<&edition::AuthorityCitation>,
112) -> Result<Event, String> {
113 build_grant_edition_unsigned(actor.public_key(), community_id, grant, version, prev_hash, created_at, authority)?
114 .finalize(actor)
115 .map_err(|e| format!("sign grant edition: {e}"))
116}
117
118pub fn build_grant_edition_unsigned(
120 author: PublicKey,
121 community_id: &CommunityId,
122 grant: &roles::MemberGrant,
123 version: u64,
124 prev_hash: Option<&[u8; 32]>,
125 created_at: u64,
126 authority: Option<&edition::AuthorityCitation>,
127) -> Result<UnsignedEvent, String> {
128 check_chain_shape(version, prev_hash)?;
129 let member_bytes = hex32(&grant.member).ok_or("member must be 64-char hex")?;
130 let entity_id = super::derive::grant_locator(community_id, &member_bytes);
131 let content = serde_json::to_string(grant).map_err(|e| e.to_string())?;
132 Ok(edition::build_edition_inner(author, VSK_GRANT, &entity_id, version, prev_hash, &content, created_at, authority))
133}
134
135pub fn build_banlist_edition(
141 actor: &Keys,
142 community_id: &CommunityId,
143 banned: &[String],
144 version: u64,
145 prev_hash: Option<&[u8; 32]>,
146 created_at: u64,
147 authority: Option<&edition::AuthorityCitation>,
148) -> Result<Event, String> {
149 build_banlist_edition_unsigned(actor.public_key(), community_id, banned, version, prev_hash, created_at, authority)?
150 .finalize(actor)
151 .map_err(|e| format!("sign banlist edition: {e}"))
152}
153
154pub fn build_banlist_edition_unsigned(
156 author: PublicKey,
157 community_id: &CommunityId,
158 banned: &[String],
159 version: u64,
160 prev_hash: Option<&[u8; 32]>,
161 created_at: u64,
162 authority: Option<&edition::AuthorityCitation>,
163) -> Result<UnsignedEvent, String> {
164 check_chain_shape(version, prev_hash)?;
165 let entity_id = super::derive::banlist_locator(community_id);
166 let content = serde_json::to_string(banned).map_err(|e| e.to_string())?;
167 Ok(edition::build_edition_inner(author, VSK_BANLIST, &entity_id, version, prev_hash, &content, created_at, authority))
168}
169
170pub fn build_group_dissolved_edition(
177 actor: &Keys,
178 community_id: &CommunityId,
179 created_at: u64,
180) -> Result<Event, String> {
181 build_group_dissolved_edition_unsigned(actor.public_key(), community_id, created_at)
182 .finalize(actor)
183 .map_err(|e| format!("sign dissolved edition: {e}"))
184}
185
186pub fn build_group_dissolved_edition_unsigned(
189 author: PublicKey,
190 community_id: &CommunityId,
191 created_at: u64,
192) -> UnsignedEvent {
193 build_group_dissolved_edition_unsigned_with_content(author, community_id, created_at, "{}")
194}
195
196pub fn build_group_dissolved_edition_unsigned_with_content(
200 author: PublicKey,
201 community_id: &CommunityId,
202 created_at: u64,
203 content: &str,
204) -> UnsignedEvent {
205 let entity_id = super::derive::dissolved_locator(community_id);
206 edition::build_edition_inner(author, VSK_DISSOLVED, &entity_id, 1, None, content, created_at, None)
208}
209
210pub fn build_group_dissolved_edition_with_content(
212 actor: &Keys,
213 community_id: &CommunityId,
214 created_at: u64,
215 content: &str,
216) -> Result<Event, String> {
217 build_group_dissolved_edition_unsigned_with_content(actor.public_key(), community_id, created_at, content)
218 .finalize(actor)
219 .map_err(|e| format!("sign dissolved edition: {e}"))
220}
221
222pub fn build_invite_links_edition(
229 actor: &Keys,
230 community_id: &CommunityId,
231 link_locators: &[String],
232 version: u64,
233 prev_hash: Option<&[u8; 32]>,
234 created_at: u64,
235 authority: Option<&edition::AuthorityCitation>,
236) -> Result<Event, String> {
237 build_invite_links_edition_unsigned(actor.public_key(), community_id, link_locators, version, prev_hash, created_at, authority)?
238 .finalize(actor)
239 .map_err(|e| format!("sign invite-links edition: {e}"))
240}
241
242pub fn build_invite_links_edition_unsigned(
245 author: PublicKey,
246 community_id: &CommunityId,
247 link_locators: &[String],
248 version: u64,
249 prev_hash: Option<&[u8; 32]>,
250 created_at: u64,
251 authority: Option<&edition::AuthorityCitation>,
252) -> Result<UnsignedEvent, String> {
253 check_chain_shape(version, prev_hash)?;
254 let entity_id = super::derive::invite_links_locator(community_id, &author.to_bytes());
255 let content = serde_json::to_string(link_locators).map_err(|e| e.to_string())?;
256 Ok(edition::build_edition_inner(author, VSK_INVITE_LINKS, &entity_id, version, prev_hash, &content, created_at, authority))
257}
258
259pub fn build_community_root_edition(
263 actor: &Keys,
264 community_id: &CommunityId,
265 meta: &super::metadata::CommunityMetadata,
266 version: u64,
267 prev_hash: Option<&[u8; 32]>,
268 created_at: u64,
269 authority: Option<&edition::AuthorityCitation>,
270) -> Result<Event, String> {
271 build_community_root_edition_unsigned(actor.public_key(), community_id, meta, version, prev_hash, created_at, authority)?
272 .finalize(actor)
273 .map_err(|e| format!("sign community-root edition: {e}"))
274}
275
276pub fn build_community_root_edition_unsigned(
278 author: PublicKey,
279 community_id: &CommunityId,
280 meta: &super::metadata::CommunityMetadata,
281 version: u64,
282 prev_hash: Option<&[u8; 32]>,
283 created_at: u64,
284 authority: Option<&edition::AuthorityCitation>,
285) -> Result<UnsignedEvent, String> {
286 check_chain_shape(version, prev_hash)?;
287 let content = serde_json::to_string(meta).map_err(|e| e.to_string())?;
288 Ok(edition::build_edition_inner(author, VSK_COMMUNITY_ROOT, &community_id.0, version, prev_hash, &content, created_at, authority))
289}
290
291pub fn build_channel_metadata_edition(
295 actor: &Keys,
296 channel_id: &ChannelId,
297 meta: &super::metadata::ChannelMetadata,
298 version: u64,
299 prev_hash: Option<&[u8; 32]>,
300 created_at: u64,
301 authority: Option<&edition::AuthorityCitation>,
302) -> Result<Event, String> {
303 build_channel_metadata_edition_unsigned(actor.public_key(), channel_id, meta, version, prev_hash, created_at, authority)?
304 .finalize(actor)
305 .map_err(|e| format!("sign channel-metadata edition: {e}"))
306}
307
308pub fn build_channel_metadata_edition_unsigned(
310 author: PublicKey,
311 channel_id: &ChannelId,
312 meta: &super::metadata::ChannelMetadata,
313 version: u64,
314 prev_hash: Option<&[u8; 32]>,
315 created_at: u64,
316 authority: Option<&edition::AuthorityCitation>,
317) -> Result<UnsignedEvent, String> {
318 check_chain_shape(version, prev_hash)?;
319 let content = serde_json::to_string(meta).map_err(|e| e.to_string())?;
320 Ok(edition::build_edition_inner(author, VSK_CHANNEL, &channel_id.0, version, prev_hash, &content, created_at, authority))
321}
322
323pub fn control_pseudonym(server_root: &ServerRootKey, community_id: &CommunityId, epoch: Epoch) -> String {
330 channel_pseudonym(&ChannelKey(*server_root.as_bytes()), &ChannelId(community_id.0), epoch).to_hex()
331}
332
333pub fn seal_control_edition(
338 ephemeral: &Keys,
339 inner: &Event,
340 server_root: &ServerRootKey,
341 community_id: &CommunityId,
342 epoch: Epoch,
343) -> Result<Event, String> {
344 if inner.kind.as_u16() != event_kind::COMMUNITY_CONTROL {
345 return Err("a control edition must be kind 3308".to_string());
346 }
347 let content = cipher::seal(server_root.as_bytes(), inner.as_json().as_bytes())
348 .map_err(|e| format!("seal control edition: {e}"))?;
349 let pseudonym = control_pseudonym(server_root, community_id, epoch);
350 EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_CONTROL), content)
351 .tags([
352 Tag::custom("z", [pseudonym]),
353 Tag::custom("v", ["1".to_string()]),
354 ])
355 .finalize(ephemeral)
356 .map_err(|e| format!("sign control outer: {e}"))
357}
358
359pub fn open_control_edition(outer: &Event, server_root: &ServerRootKey) -> Result<Event, String> {
374 if outer.kind.as_u16() != event_kind::COMMUNITY_CONTROL {
375 return Err("not a control-plane outer (kind != 3308)".to_string());
376 }
377 match outer.tags.iter().find_map(|t| {
378 let s = t.as_slice();
379 (s.len() >= 2 && s[0] == "v").then(|| s[1].clone())
380 }) {
381 Some(v) if v == "1" => {}
382 other => return Err(format!("unsupported control edition version: {other:?}")),
383 }
384 let plaintext = cipher::open(server_root.as_bytes(), &outer.content)
385 .map_err(|e| format!("open control edition: {e}"))?;
386 let json = String::from_utf8(plaintext).map_err(|e| format!("control inner utf8: {e}"))?;
387 let inner = Event::from_json(&json).map_err(|e| format!("control inner parse: {e}"))?;
388 if inner.kind.as_u16() != event_kind::COMMUNITY_CONTROL {
389 return Err("control inner is not kind 3308".to_string());
390 }
391 Ok(inner)
392}
393
394pub fn seal_dissolved_edition(ephemeral: &Keys, inner: &Event, community_id: &CommunityId) -> Result<Event, String> {
401 if inner.kind.as_u16() != event_kind::COMMUNITY_CONTROL {
402 return Err("a dissolved tombstone must be kind 3308".to_string());
403 }
404 let key = super::derive::dissolved_envelope_key(community_id);
405 let content = cipher::seal(&key, inner.as_json().as_bytes())
406 .map_err(|e| format!("seal dissolved edition: {e}"))?;
407 let pseudonym = super::derive::dissolved_pseudonym(community_id);
408 EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_CONTROL), content)
409 .tags([
410 Tag::custom("z", [pseudonym]),
411 Tag::custom("v", ["1".to_string()]),
412 ])
413 .finalize(ephemeral)
414 .map_err(|e| format!("sign dissolved outer: {e}"))
415}
416
417pub fn dissolved_tombstone_signer(outer: &Event, community_id: &CommunityId) -> Option<PublicKey> {
422 dissolved_tombstone_open(outer, community_id).map(|d| d.author)
423}
424
425#[derive(Clone)]
430pub struct DissolvedEdition {
431 pub author: PublicKey,
432 pub created_at: u64,
433 pub inner_id: [u8; 32],
434 pub content: String,
435}
436
437pub fn dissolved_tombstone_open(outer: &Event, community_id: &CommunityId) -> Option<DissolvedEdition> {
439 if outer.kind.as_u16() != event_kind::COMMUNITY_CONTROL {
440 return None;
441 }
442 let key = super::derive::dissolved_envelope_key(community_id);
443 let plaintext = cipher::open(&key, &outer.content).ok()?;
444 let inner = Event::from_json(&String::from_utf8(plaintext).ok()?).ok()?;
445 let p = edition::parse_edition_inner(&inner).ok()?;
446 (p.vsk == VSK_DISSOLVED && p.entity_id == super::derive::dissolved_locator(community_id)).then_some(
447 DissolvedEdition { author: p.author, created_at: p.created_at, inner_id: p.inner_id, content: p.content },
448 )
449}
450
451#[derive(Clone)]
454pub struct EntityHead {
455 pub entity_hex: String,
457 pub version: u64,
458 pub self_hash: [u8; 32],
459 pub inner_id: [u8; 32],
463 pub citation: Option<edition::AuthorityCitation>,
468}
469
470#[derive(Clone)]
472pub struct FoldedRoster {
473 pub roles: roles::CommunityRoles,
478 pub role_authors: Vec<PublicKey>,
481 pub grant_authors: Vec<PublicKey>,
483 pub gapped_entities: Vec<[u8; 32]>,
487 pub skipped: usize,
491 pub fetched: usize,
496 pub heads: Vec<EntityHead>,
500 pub banned: Vec<String>,
505 pub banlist_author: Option<PublicKey>,
508 pub dissolved_by: Vec<PublicKey>,
516 pub dissolved_editions: Vec<DissolvedEdition>,
520 pub banlist_head: Option<EntityHead>,
522 pub invite_link_sets: Vec<InviteLinkSet>,
527 pub root_meta: Option<super::metadata::CommunityMetadata>,
530 pub root_author: Option<PublicKey>,
532 pub root_head: Option<EntityHead>,
534 pub root_candidates: Vec<RootCandidate>,
541 pub channel_meta: Vec<ChannelMetaHead>,
545 pub channel_candidates: Vec<ChannelMetaHead>,
552}
553
554#[derive(Clone)]
557pub struct InviteLinkSet {
558 pub creator: PublicKey,
559 pub locators: Vec<String>,
560 pub head: EntityHead,
561}
562
563#[derive(Clone)]
567pub struct RootCandidate {
568 pub meta: super::metadata::CommunityMetadata,
569 pub author: PublicKey,
570 pub head: EntityHead,
571}
572
573#[derive(Clone)]
576pub struct ChannelMetaHead {
577 pub channel_id: [u8; 32],
578 pub meta: super::metadata::ChannelMetadata,
579 pub author: PublicKey,
580 pub head: EntityHead,
581}
582
583pub fn fold_roster(
607 inner_editions: &[Event],
608 community_id: &CommunityId,
609 floors: &HashMap<String, (u64, [u8; 32])>,
610) -> FoldedRoster {
611 let mut skipped = inner_editions.len().saturating_sub(MAX_CONTROL_EDITIONS);
612
613 let parsed: Vec<edition::ParsedEdition> = inner_editions
615 .iter()
616 .take(MAX_CONTROL_EDITIONS)
617 .filter_map(|e| match edition::parse_edition_inner(e) {
618 Ok(p) => Some(p),
619 Err(_) => {
620 skipped += 1;
621 None
622 }
623 })
624 .collect();
625
626 let dissolved_eid = super::derive::dissolved_locator(community_id);
632 let mut dissolved_by: Vec<PublicKey> = Vec::new();
633 let mut dissolved_editions: Vec<DissolvedEdition> = Vec::new();
634 for p in &parsed {
635 if p.vsk == VSK_DISSOLVED && p.entity_id == dissolved_eid {
636 if !dissolved_by.contains(&p.author) {
637 dissolved_by.push(p.author);
638 }
639 dissolved_editions.push(DissolvedEdition {
643 author: p.author,
644 created_at: p.created_at,
645 inner_id: p.inner_id,
646 content: p.content.clone(),
647 });
648 }
649 }
650
651 let mut by_entity: HashMap<[u8; 32], Vec<&edition::ParsedEdition>> = HashMap::new();
652 for p in &parsed {
653 if p.vsk == VSK_DISSOLVED {
656 continue;
657 }
658 by_entity.entry(p.entity_id).or_default().push(p);
659 }
660
661 let mut out = roles::CommunityRoles::default();
662 let mut role_authors: Vec<PublicKey> = Vec::new();
663 let mut grant_authors: Vec<PublicKey> = Vec::new();
664 let mut gapped_entities = Vec::new();
665 let mut heads: Vec<EntityHead> = Vec::new();
666 let mut banned: Vec<String> = Vec::new();
667 let mut banlist_author: Option<PublicKey> = None;
668 let mut banlist_head: Option<EntityHead> = None;
669 let banlist_eid = super::derive::banlist_locator(community_id);
670 let mut invite_link_sets: Vec<InviteLinkSet> = Vec::new();
671 let mut root_meta: Option<super::metadata::CommunityMetadata> = None;
672 let mut root_author: Option<PublicKey> = None;
673 let mut root_head: Option<EntityHead> = None;
674 let mut root_candidates: Vec<RootCandidate> = Vec::new();
675 let mut channel_meta: Vec<ChannelMetaHead> = Vec::new();
676 let mut channel_candidates: Vec<ChannelMetaHead> = Vec::new();
677
678 for (entity_id, editions) in by_entity {
679 let fold_eds: Vec<version::Edition> = editions.iter().map(|p| p.to_fold_edition()).collect();
680 let entity_hex = crate::simd::hex::bytes_to_hex_32(&entity_id);
683 let floor = floors.get(&entity_hex);
684 let floor_v = floor.map(|(v, _)| *v).unwrap_or(0);
685 let result = version::fold(&fold_eds, floor_v, floor.map(|(_, h)| h));
686 let head_idx = if result.gap {
697 gapped_entities.push(entity_id);
698 if floor_v == 0 {
699 match version::bootstrap_head(&fold_eds, 0) { Some(i) => i, None => continue }
700 } else {
701 match version::bootstrap_head(&fold_eds, floor_v) {
710 Some(i) if matches!(editions[i].vsk.as_str(), VSK_COMMUNITY_ROOT | VSK_CHANNEL) => i,
711 _ => continue, }
713 }
714 } else {
715 match result.head { Some(i) => i, None => continue }
716 };
717 let head = editions[head_idx];
718 let mut record_head = || {
719 heads.push(EntityHead {
720 entity_hex: crate::simd::hex::bytes_to_hex_32(&entity_id),
721 version: head.version,
722 self_hash: head.self_hash,
723 inner_id: head.inner_id,
724 citation: head.authority.clone(),
725 });
726 };
727 match head.vsk.as_str() {
728 VSK_ROLE => match serde_json::from_str::<roles::Role>(&head.content) {
729 Ok(role) if hex32(&role.role_id) == Some(entity_id) => {
732 record_head();
733 role_authors.push(head.author);
734 out.roles.push(role);
735 }
736 _ => skipped += 1,
737 },
738 VSK_GRANT => match serde_json::from_str::<roles::MemberGrant>(&head.content) {
739 Ok(grant)
743 if hex32(&grant.member)
744 .is_some_and(|m| super::derive::grant_locator(community_id, &m) == entity_id) =>
745 {
746 record_head();
749 if !grant.role_ids.is_empty() {
750 grant_authors.push(head.author);
751 out.grants.push(grant);
752 }
753 }
754 _ => skipped += 1,
755 },
756 VSK_BANLIST if entity_id == banlist_eid => match serde_json::from_str::<Vec<String>>(&head.content) {
757 Ok(list) => {
760 banned = list;
761 banlist_author = Some(head.author);
762 banlist_head = Some(EntityHead {
763 entity_hex: crate::simd::hex::bytes_to_hex_32(&entity_id),
764 version: head.version,
765 self_hash: head.self_hash,
766 inner_id: head.inner_id,
767 citation: head.authority.clone(),
768 });
769 }
770 _ => skipped += 1,
771 },
772 VSK_INVITE_LINKS if super::derive::invite_links_locator(community_id, &head.author.to_bytes()) == entity_id => {
773 match serde_json::from_str::<Vec<String>>(&head.content) {
778 Ok(locators) => invite_link_sets.push(InviteLinkSet {
779 creator: head.author,
780 locators,
781 head: EntityHead {
782 entity_hex: crate::simd::hex::bytes_to_hex_32(&entity_id),
783 version: head.version,
784 self_hash: head.self_hash,
785 inner_id: head.inner_id,
786 citation: head.authority.clone(),
787 },
788 }),
789 _ => skipped += 1,
790 }
791 }
792 VSK_COMMUNITY_ROOT if entity_id == community_id.0 => {
793 let entity_hex = crate::simd::hex::bytes_to_hex_32(&entity_id);
800 let mut cands: Vec<(u64, [u8; 32], RootCandidate)> = editions
801 .iter()
802 .filter(|e| e.version >= floor_v)
803 .filter_map(|e| {
804 serde_json::from_str::<super::metadata::CommunityMetadata>(&e.content)
805 .ok()
806 .map(|meta| (e.version, e.inner_id, RootCandidate {
807 meta,
808 author: e.author,
809 head: EntityHead {
810 entity_hex: entity_hex.clone(),
811 version: e.version,
812 self_hash: e.self_hash,
813 inner_id: e.inner_id,
814 citation: e.authority.clone(),
815 },
816 }))
817 })
818 .collect();
819 if cands.is_empty() {
820 skipped += 1;
821 } else {
822 cands.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)));
823 root_candidates = cands.into_iter().map(|(_, _, c)| c).collect();
824 let top = &root_candidates[0];
825 root_meta = Some(top.meta.clone());
826 root_author = Some(top.author);
827 root_head = Some(top.head.clone());
828 }
829 }
830 VSK_CHANNEL => {
831 let entity_hex = crate::simd::hex::bytes_to_hex_32(&entity_id);
837 let mut cands: Vec<(u64, [u8; 32], ChannelMetaHead)> = editions
838 .iter()
839 .filter(|e| e.version >= floor_v)
840 .filter_map(|e| {
841 serde_json::from_str::<super::metadata::ChannelMetadata>(&e.content)
842 .ok()
843 .map(|meta| (e.version, e.inner_id, ChannelMetaHead {
844 channel_id: entity_id,
845 meta,
846 author: e.author,
847 head: EntityHead {
848 entity_hex: entity_hex.clone(),
849 version: e.version,
850 self_hash: e.self_hash,
851 inner_id: e.inner_id,
852 citation: e.authority.clone(),
853 },
854 }))
855 })
856 .collect();
857 if cands.is_empty() {
858 skipped += 1;
859 } else {
860 cands.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)));
861 channel_meta.push(cands[0].2.clone());
863 channel_candidates.extend(cands.into_iter().map(|(_, _, c)| c));
864 }
865 }
866 _ => {} }
868 }
869
870 FoldedRoster {
871 roles: out, role_authors, grant_authors, gapped_entities, skipped, fetched: 0, heads,
872 banned, banlist_author, banlist_head, dissolved_by, dissolved_editions,
873 invite_link_sets,
874 root_meta, root_author, root_head, root_candidates, channel_meta, channel_candidates,
875 }
876}
877
878pub fn authorize_delegation(folded: &FoldedRoster, owner_hex: Option<&str>) -> roles::CommunityRoles {
908 use roles::Permissions;
909 let mut accepted = roles::CommunityRoles::default();
910 let mut role_done = vec![false; folded.roles.roles.len()];
911 let mut grant_done = vec![false; folded.roles.grants.len()];
912
913 loop {
914 let mut changed = false;
915
916 for (i, role) in folded.roles.roles.iter().enumerate() {
918 if role_done[i] {
919 continue;
920 }
921 if role.position == 0 {
924 role_done[i] = true;
925 continue;
926 }
927 let author = folded.role_authors[i].to_hex();
928 if accepted.can_act_on_position(&author, owner_hex, role.position, Permissions::MANAGE_ROLES) {
929 accepted.roles.push(role.clone());
930 role_done[i] = true;
931 changed = true;
932 }
933 }
934
935 for (i, grant) in folded.roles.grants.iter().enumerate() {
940 if grant_done[i] {
941 continue;
942 }
943 let positions: Option<Vec<u32>> =
944 grant.role_ids.iter().map(|rid| accepted.role(rid).map(|r| r.position)).collect();
945 let Some(positions) = positions else { continue }; let author = folded.grant_authors[i].to_hex();
947 let outranks_all_roles = positions
948 .iter()
949 .all(|p| accepted.can_act_on_position(&author, owner_hex, *p, Permissions::MANAGE_ROLES));
950 if outranks_all_roles
951 && accepted.can_act_on_member(&author, owner_hex, &grant.member, Permissions::MANAGE_ROLES)
952 {
953 accepted.grants.push(grant.clone());
954 grant_done[i] = true;
955 changed = true;
956 }
957 }
958
959 if !changed {
960 break;
961 }
962 }
963 accepted
964}
965
966pub fn authority_citation_satisfied(
991 heads: &[EntityHead],
992 owner_hex: Option<&str>,
993 actor_hex: &str,
994 actor_grant_hex: &str,
995 citation: Option<&edition::AuthorityCitation>,
996) -> bool {
997 if owner_hex == Some(actor_hex) {
998 return true;
999 }
1000 let Some(c) = citation else { return false };
1001 let entity_hex = crate::simd::hex::bytes_to_hex_32(&c.entity_id);
1002 if entity_hex != actor_grant_hex {
1006 return false;
1007 }
1008 match heads.iter().find(|h| h.entity_hex == entity_hex) {
1009 Some(h) if h.version > c.version => true,
1012 Some(h) if h.version == c.version => h.self_hash == c.edition_hash,
1015 _ => false,
1017 }
1018}
1019
1020#[cfg(test)]
1021mod tests {
1022 use super::*;
1023 use crate::community::roles::{Permissions, Role, RoleScope};
1024
1025 fn sr() -> ServerRootKey {
1026 ServerRootKey([0x07; 32])
1027 }
1028
1029 #[test]
1030 fn authority_citation_satisfied_pins_to_a_synced_complete_grant() {
1031 let owner = "ow".repeat(32);
1032 let actor = "ac".repeat(32);
1033 let entity = [0x55u8; 32];
1034 let hash = [0x66u8; 32];
1035 let eh = crate::simd::hex::bytes_to_hex_32(&entity); let head = |v: u64, h: [u8; 32]| {
1037 vec![EntityHead { entity_hex: eh.clone(), version: v, self_hash: h, inner_id: [0u8; 32], citation: None }]
1038 };
1039 let cite = edition::AuthorityCitation { entity_id: entity, version: 3, edition_hash: hash };
1040
1041 assert!(authority_citation_satisfied(&[], Some(&owner), &owner, &eh, None));
1043 assert!(!authority_citation_satisfied(&head(3, hash), Some(&owner), &actor, &eh, None));
1045 assert!(authority_citation_satisfied(&head(3, hash), Some(&owner), &actor, &eh, Some(&cite)));
1047 assert!(!authority_citation_satisfied(&head(3, [0xEE; 32]), Some(&owner), &actor, &eh, Some(&cite)));
1049 assert!(authority_citation_satisfied(&head(4, [0x77; 32]), Some(&owner), &actor, &eh, Some(&cite)));
1051 assert!(!authority_citation_satisfied(&head(2, hash), Some(&owner), &actor, &eh, Some(&cite)));
1053 assert!(!authority_citation_satisfied(&[], Some(&owner), &actor, &eh, Some(&cite)));
1055 let foreign = "ff".repeat(32);
1057 assert!(!authority_citation_satisfied(&head(3, hash), Some(&owner), &actor, &foreign, Some(&cite)));
1058 }
1059 fn cid() -> CommunityId {
1061 CommunityId([0x09; 32])
1062 }
1063
1064 fn role_event(owner: &Keys, role_id: &str, position: u32, version: u64, prev: Option<&[u8; 32]>, created_at: u64) -> Event {
1066 let role = Role {
1067 role_id: role_id.to_string(),
1068 name: "Admin".into(),
1069 position,
1070 permissions: Permissions::admin(),
1071 scope: RoleScope::Server,
1072 color: 0,
1073 };
1074 let eid = crate::simd::hex::hex_to_bytes_32(role_id);
1075 edition::build_edition_inner(owner.public_key(), VSK_ROLE, &eid, version, prev, &serde_json::to_string(&role).unwrap(), created_at, None)
1076 .finalize(owner)
1077 .unwrap()
1078 }
1079
1080 fn grant_event(owner: &Keys, member_hex: &str, role_ids: Vec<String>, version: u64, prev: Option<&[u8; 32]>, created_at: u64) -> (Event, [u8; 32], String) {
1082 let member_bytes = crate::simd::hex::hex_to_bytes_32(member_hex);
1083 let eid = crate::community::derive::grant_locator(&cid(), &member_bytes);
1084 let g = roles::MemberGrant { member: member_hex.to_string(), role_ids };
1085 let content = serde_json::to_string(&g).unwrap();
1086 let ev = edition::build_edition_inner(owner.public_key(), VSK_GRANT, &eid, version, prev, &content, created_at, None)
1087 .finalize(owner)
1088 .unwrap();
1089 (ev, eid, content)
1090 }
1091
1092 fn groot_event(signer: &Keys, name: &str, desc: &str, version: u64, prev: Option<&[u8; 32]>, created_at: u64) -> Event {
1094 let meta = crate::community::metadata::CommunityMetadata {
1095 name: name.to_string(),
1096 relays: vec![],
1097 description: Some(desc.to_string()),
1098 icon: None,
1099 banner: None,
1100 owner_attestation: None,
1101 };
1102 let content = serde_json::to_string(&meta).unwrap();
1103 edition::build_edition_inner(signer.public_key(), VSK_COMMUNITY_ROOT, &cid().0, version, prev, &content, created_at, None)
1104 .finalize(signer)
1105 .unwrap()
1106 }
1107
1108 #[test]
1109 fn folds_a_role_and_a_grant() {
1110 let owner = Keys::generate();
1111 let role_id = "a".repeat(64);
1112 let member = "bb".repeat(32);
1113 let role_ev = role_event(&owner, &role_id, 1, 1, None, 100);
1114 let (grant_ev, _, _) = grant_event(&owner, &member, vec![role_id.clone()], 1, None, 101);
1115 let folded = fold_roster(&[role_ev, grant_ev], &cid(), &Default::default());
1116 assert_eq!(folded.roles.roles.len(), 1);
1117 assert_eq!(folded.roles.roles[0].role_id, role_id);
1118 assert!(folded.roles.is_admin(&member), "the member holds the granted Admin role");
1119 assert_eq!(folded.skipped, 0);
1120 assert!(folded.gapped_entities.is_empty());
1121 }
1122
1123 #[test]
1124 fn latest_grant_edition_wins() {
1125 let owner = Keys::generate();
1126 let role_a = "a".repeat(64);
1127 let role_b = "b".repeat(64);
1128 let member = "cc".repeat(32);
1129 let (e1, eid, c1) = grant_event(&owner, &member, vec![role_a.clone()], 1, None, 100);
1130 let v1_hash = version::edition_hash(&eid, 1, None, c1.as_bytes());
1131 let (e2, _, _) = grant_event(&owner, &member, vec![role_b.clone()], 2, Some(&v1_hash), 101);
1132 let folded = fold_roster(&[e1, e2], &cid(), &Default::default());
1133 let held: Vec<&String> = folded.roles.grants.iter().flat_map(|g| &g.role_ids).collect();
1134 assert_eq!(held, vec![&role_b], "v2 supersedes v1 — the member now holds role_b, not role_a");
1135 assert_eq!(folded.skipped, 0);
1136 }
1137
1138 #[test]
1142 fn tracking_quarantines_a_gapped_head() {
1143 let owner = Keys::generate();
1144 let role_id = "e".repeat(64);
1145 let eid = crate::simd::hex::hex_to_bytes_32(&role_id);
1146 let ev = role_event(&owner, &role_id, 1, 5, Some(&[0x99u8; 32]), 100);
1147 let mut floors = std::collections::HashMap::new();
1149 floors.insert(role_id.clone(), (3u64, [0x11u8; 32]));
1150 let folded = fold_roster(&[ev], &cid(), &floors);
1151 assert!(folded.roles.roles.is_empty(), "tracking: a gapped tail is quarantined, not folded");
1152 assert_eq!(folded.gapped_entities, vec![eid]);
1153 }
1154
1155 #[test]
1160 fn bootstrapping_surfaces_a_gapped_head_but_authority_gates() {
1161 let owner = Keys::generate();
1162 let owner_hex = owner.public_key().to_hex();
1163 let role_id = "e".repeat(64);
1164 let eid = crate::simd::hex::hex_to_bytes_32(&role_id);
1165 let ev = role_event(&owner, &role_id, 1, 5, Some(&[0x99u8; 32]), 100);
1167 let folded = fold_roster(&[ev], &cid(), &Default::default());
1168 assert_eq!(folded.roles.roles.len(), 1, "bootstrapping surfaces the highest signed head despite the gap");
1169 assert_eq!(folded.gapped_entities, vec![eid], "still flagged pending so the union can fill the gap");
1170 assert_eq!(authorize_delegation(&folded, Some(&owner_hex)).roles.len(), 1, "owner-authored head is authorized");
1171
1172 let stranger = Keys::generate();
1174 let ev2 = role_event(&stranger, &role_id, 1, 5, Some(&[0x99u8; 32]), 100);
1175 let folded2 = fold_roster(&[ev2], &cid(), &Default::default());
1176 assert_eq!(folded2.roles.roles.len(), 1, "surfaced into the raw fold (signature is valid)");
1177 assert!(authorize_delegation(&folded2, Some(&owner_hex)).roles.is_empty(),
1178 "Policy B does NOT bypass authority — an unauthorized author's surfaced head is rejected");
1179 }
1180
1181 #[test]
1185 fn fresh_joiner_recovers_full_plane_across_gaps() {
1186 let owner = Keys::generate();
1187 let owner_hex = owner.public_key().to_hex();
1188 let admin = Keys::generate();
1189 let admin_hex = admin.public_key().to_hex();
1190 let role_id = "a".repeat(64);
1191
1192 let role = role_event(&owner, &role_id, 1, 1, None, 100);
1194 let (grant, _, _) = grant_event(&owner, &admin_hex, vec![role_id.clone()], 2, Some(&[0x99u8; 32]), 110);
1196 let groot_v1 = groot_event(&owner, "Genesis", "", 1, None, 90);
1198 let groot_v11 = groot_event(&admin, "AggroTown v2", "King Claude was here", 11, Some(&[0xABu8; 32]), 200);
1199
1200 let folded = fold_roster(&[role, grant, groot_v1, groot_v11], &cid(), &Default::default());
1201
1202 assert_eq!(folded.root_author.map(|a| a.to_hex()), Some(admin_hex.clone()), "latest GroupRoot author surfaced");
1204 assert_eq!(folded.root_meta.as_ref().unwrap().name, "AggroTown v2");
1205 assert_eq!(folded.root_meta.as_ref().unwrap().description.as_deref(), Some("King Claude was here"));
1206
1207 let authed = authorize_delegation(&folded, Some(&owner_hex));
1209 assert!(
1210 authed.is_authorized(&admin_hex, Some(&owner_hex), roles::Permissions::MANAGE_METADATA),
1211 "the bootstrapped grant authorizes the admin → the fresh joiner trusts the admin's metadata"
1212 );
1213 }
1214
1215 #[test]
1220 fn unauthorized_high_version_metadata_is_surfaced_but_not_authorized() {
1221 let owner = Keys::generate();
1222 let owner_hex = owner.public_key().to_hex();
1223 let stranger = Keys::generate();
1224 let stranger_hex = stranger.public_key().to_hex();
1225 let groot_v1 = groot_event(&owner, "Genesis", "", 1, None, 90);
1226 let forged = groot_event(&stranger, "PWNED", "owned by a hostile relay", 99, Some(&[0xCDu8; 32]), 300);
1227 let folded = fold_roster(&[groot_v1, forged], &cid(), &Default::default());
1228 assert_eq!(folded.root_author.map(|a| a.to_hex()), Some(stranger_hex.clone()));
1230 let authed = authorize_delegation(&folded, Some(&owner_hex));
1231 assert!(
1232 !authed.is_authorized(&stranger_hex, Some(&owner_hex), roles::Permissions::MANAGE_METADATA),
1233 "an unauthorized author's surfaced metadata is NOT authorized — the caller drops it"
1234 );
1235 }
1236
1237 #[test]
1242 fn fresh_fold_picks_owner_reassert_over_a_demoted_admins_forgery_and_edit() {
1243 let owner = Keys::generate();
1244 let owner_hex = owner.public_key().to_hex();
1245 let alice = Keys::generate();
1246 let alice_hex = alice.public_key().to_hex();
1247 let role_id = "a".repeat(64);
1248
1249 let role = role_event(&owner, &role_id, 1, 1, None, 100);
1251 let (grant, geid, gcontent) = grant_event(&owner, &alice_hex, vec![role_id.clone()], 1, None, 110);
1252 let grant_hash = version::edition_hash(&geid, 1, None, gcontent.as_bytes());
1253 let (revoke, _, _) = grant_event(&owner, &alice_hex, vec![], 2, Some(&grant_hash), 120);
1254
1255 let groot_v1 = groot_event(&owner, "Genesis", "", 1, None, 90);
1258 let round1_v2 = groot_event(&alice, "Alice's HQ", "by admin alice", 2, Some(&[0x22u8; 32]), 200);
1259 let forgery_v3 = groot_event(&alice, "FORGERY", "demoted alice", 3, Some(&[0x33u8; 32]), 300);
1260 let reassert_v3 = groot_event(&owner, "Alice's HQ", "re-asserted by owner", 3, Some(&[0x33u8; 32]), 310);
1261
1262 let folded = fold_roster(
1263 &[role, grant, revoke, groot_v1, round1_v2, forgery_v3, reassert_v3],
1264 &cid(), &Default::default(),
1265 );
1266 let authed = authorize_delegation(&folded, Some(&owner_hex));
1267 assert!(!authed.is_authorized(&alice_hex, Some(&owner_hex), Permissions::MANAGE_METADATA),
1268 "Alice is revoked (grant→revoke folds to no roles)");
1269
1270 assert_eq!(folded.root_candidates.iter().filter(|c| c.head.version == 3).count(), 2,
1272 "the candidate set includes both v3 fork members");
1273
1274 let chosen = folded.root_candidates.iter()
1277 .find(|c| authed.is_authorized(&c.author.to_hex(), Some(&owner_hex), Permissions::MANAGE_METADATA))
1278 .expect("an authorized candidate exists");
1279 assert_eq!(chosen.author, owner.public_key(), "the owner's re-assert wins the fork, not Alice's forgery");
1280 assert_eq!(chosen.meta.name, "Alice's HQ", "the demoted admin's content is preserved, not 'FORGERY'");
1281 }
1282
1283 #[test]
1287 fn bootstrapping_surfaces_a_stranger_grant_but_it_never_authorizes() {
1288 let owner = Keys::generate();
1289 let owner_hex = owner.public_key().to_hex();
1290 let stranger = Keys::generate();
1291 let stranger_hex = stranger.public_key().to_hex();
1292 let role_id = "a".repeat(64);
1293 let role = role_event(&owner, &role_id, 1, 1, None, 100);
1295 let (grant, _, _) = grant_event(&stranger, &stranger_hex, vec![role_id.clone()], 99, Some(&[0x99u8; 32]), 200);
1296 let folded = fold_roster(&[role, grant], &cid(), &Default::default());
1297 assert_eq!(folded.roles.grants.len(), 1, "the stranger's grant is surfaced (valid signature)");
1298 let authed = authorize_delegation(&folded, Some(&owner_hex));
1299 assert!(authed.grants.is_empty(), "but it never chains to the owner → dropped");
1300 assert!(!authed.is_authorized(&stranger_hex, Some(&owner_hex), roles::Permissions::BAN),
1301 "the self-granting stranger holds no authority");
1302 }
1303
1304 #[test]
1311 fn mixed_tracking_and_bootstrapping_floors_in_one_fold() {
1312 let owner = Keys::generate();
1313 let role_id = "a".repeat(64);
1314 let fresh_member = "cc".repeat(32);
1315 let tracked_member = "dd".repeat(32);
1316 let groot_v11 = groot_event(&owner, "Tracked", "held-at-v5", 11, Some(&[0xABu8; 32]), 200);
1318 let (fresh_grant, _, _) = grant_event(&owner, &fresh_member, vec![role_id.clone()], 2, Some(&[0x99u8; 32]), 110);
1320 let (tracked_grant, _, _) = grant_event(&owner, &tracked_member, vec![role_id.clone()], 11, Some(&[0x88u8; 32]), 120);
1322 let role = role_event(&owner, &role_id, 1, 1, None, 100); let cid_hex = crate::simd::hex::bytes_to_hex_32(&cid().0);
1325 let tracked_grant_hex = crate::simd::hex::bytes_to_hex_32(
1326 &crate::community::derive::grant_locator(&cid(), &crate::simd::hex::hex_to_bytes_32(&tracked_member)));
1327 let mut floors = std::collections::HashMap::new();
1328 floors.insert(cid_hex, (5u64, [0x77u8; 32])); floors.insert(tracked_grant_hex, (5u64, [0x66u8; 32])); let folded = fold_roster(&[groot_v11, fresh_grant, tracked_grant, role], &cid(), &floors);
1331
1332 assert!(folded.root_meta.is_some(), "tracking GroupRoot with a gapped tail is SURFACED (display exemption)");
1333 assert!(folded.gapped_entities.contains(&cid().0), "and still flagged pending for a refetch");
1334 assert_eq!(folded.roles.grants.len(), 1, "only the bootstrapping grant surfaces; the tracked-gapped grant stays quarantined");
1335 }
1336
1337 #[test]
1340 fn empty_grant_revoke_advances_head_but_carries_no_entry() {
1341 let owner = Keys::generate();
1342 let role_id = "a".repeat(64);
1343 let member = "cc".repeat(32);
1344 let (g1, eid, c1) = grant_event(&owner, &member, vec![role_id], 1, None, 100);
1345 let v1_hash = version::edition_hash(&eid, 1, None, c1.as_bytes());
1346 let (g2, _, _) = grant_event(&owner, &member, vec![], 2, Some(&v1_hash), 101); let folded = fold_roster(&[g1, g2], &cid(), &Default::default());
1348 assert!(folded.roles.grants.is_empty(), "a revoke leaves no roster entry");
1349 assert!(
1350 folded.heads.iter().any(|h| h.entity_hex == crate::simd::hex::bytes_to_hex_32(&eid) && h.version == 2),
1351 "but the revoke still advances the head (so a replayed v1 can't re-add the role)"
1352 );
1353 }
1354
1355 #[test]
1358 fn fold_roster_skips_junk_and_still_folds_the_valid() {
1359 let owner = Keys::generate();
1360 let owner_hex = owner.public_key().to_hex();
1361 let role_id = "a".repeat(64);
1362 let good = role_event(&owner, &role_id, 1, 1, None, 100);
1363 let garbage = edition::build_edition_inner(
1365 owner.public_key(), VSK_ROLE, &crate::simd::hex::hex_to_bytes_32(&"b".repeat(64)),
1366 1, None, "not json at all", 100, None,
1367 ).finalize(&owner).unwrap();
1368 let role_b = Role { role_id: "cc".repeat(64), name: "X".into(), position: 1, permissions: Permissions::admin(), scope: RoleScope::Server, color: 0 };
1370 let mismatched = edition::build_edition_inner(
1371 owner.public_key(), VSK_ROLE, &crate::simd::hex::hex_to_bytes_32(&"dd".repeat(64)),
1372 1, None, &serde_json::to_string(&role_b).unwrap(), 100, None,
1373 ).finalize(&owner).unwrap();
1374
1375 let folded = fold_roster(&[good, garbage, mismatched], &cid(), &Default::default());
1376 assert!(folded.roles.role(&role_id).is_some(), "the valid role still folds through the junk");
1377 assert!(folded.skipped >= 2, "both junk editions are skipped, not folded (skipped={})", folded.skipped);
1378 assert_eq!(authorize_delegation(&folded, Some(&owner_hex)).roles.len(), 1, "only the valid role authorizes");
1379 }
1380
1381 #[test]
1384 fn role_content_must_bind_to_its_entity_id() {
1385 let owner = Keys::generate();
1386 let role = Role {
1387 role_id: "a".repeat(64), name: "X".into(),
1389 position: 0,
1390 permissions: Permissions::admin(),
1391 scope: RoleScope::Server,
1392 color: 0,
1393 };
1394 let wrong_eid = [0x12u8; 32]; let ev = edition::build_edition_inner(owner.public_key(), VSK_ROLE, &wrong_eid, 1, None, &serde_json::to_string(&role).unwrap(), 100, None)
1396 .finalize(&owner)
1397 .unwrap();
1398 let folded = fold_roster(&[ev], &cid(), &Default::default());
1399 assert!(folded.roles.roles.is_empty(), "entity_id != role_id → rejected");
1400 assert_eq!(folded.skipped, 1);
1401 }
1402
1403 #[test]
1406 fn grant_at_wrong_locator_is_rejected() {
1407 let owner = Keys::generate();
1408 let member = "dd".repeat(32);
1409 let g = roles::MemberGrant { member: member.clone(), role_ids: vec!["a".repeat(64)] };
1410 let wrong_eid = [0x34u8; 32]; let ev = edition::build_edition_inner(owner.public_key(), VSK_GRANT, &wrong_eid, 1, None, &serde_json::to_string(&g).unwrap(), 100, None)
1412 .finalize(&owner)
1413 .unwrap();
1414 let folded = fold_roster(&[ev], &cid(), &Default::default());
1415 assert!(!folded.roles.is_admin(&member), "a grant at the wrong locator does not take effect");
1416 assert_eq!(folded.skipped, 1);
1417 }
1418
1419 #[test]
1421 fn fold_is_order_independent() {
1422 let owner = Keys::generate();
1423 let role_id = "a".repeat(64);
1424 let member = "bb".repeat(32);
1425 let role_ev = role_event(&owner, &role_id, 1, 1, None, 100);
1426 let (grant_ev, _, _) = grant_event(&owner, &member, vec![role_id.clone()], 1, None, 101);
1427 let a = fold_roster(&[role_ev.clone(), grant_ev.clone()], &cid(), &Default::default());
1428 let b = fold_roster(&[grant_ev, role_ev], &cid(), &Default::default());
1429 assert_eq!(a.roles.roles.len(), b.roles.roles.len());
1430 assert!(a.roles.is_admin(&member) && b.roles.is_admin(&member));
1431 }
1432
1433 #[test]
1436 fn public_builders_round_trip_through_fold() {
1437 let owner = Keys::generate();
1438 let role = Role {
1439 role_id: "a".repeat(64),
1440 name: "Admin".into(),
1441 position: 1,
1442 permissions: Permissions::admin(),
1443 scope: RoleScope::Server,
1444 color: 0,
1445 };
1446 let member = "bb".repeat(32);
1447 let grant = roles::MemberGrant { member: member.clone(), role_ids: vec![role.role_id.clone()] };
1448
1449 let role_ev = build_role_edition(&owner, &role, 1, None, 100, None).unwrap();
1450 let grant_ev = build_grant_edition(&owner, &cid(), &grant, 1, None, 101, None).unwrap();
1451
1452 let folded = fold_roster(&[role_ev, grant_ev], &cid(), &Default::default());
1453 assert_eq!(folded.skipped, 0, "builders emit bound, anchored editions the fold accepts");
1454 assert!(folded.gapped_entities.is_empty());
1455 assert!(folded.roles.is_admin(&member));
1456 assert_eq!(folded.roles.role(&role.role_id).unwrap().position, 1);
1457 }
1458
1459 #[test]
1461 fn builders_reject_malformed_chain_shape() {
1462 let owner = Keys::generate();
1463 let role = Role {
1464 role_id: "a".repeat(64), name: "Admin".into(), position: 1,
1465 permissions: Permissions::admin(), scope: RoleScope::Server, color: 0,
1466 };
1467 assert!(build_role_edition(&owner, &role, 1, Some(&[0u8; 32]), 100, None).is_err());
1469 assert!(build_role_edition(&owner, &role, 5, None, 100, None).is_err());
1470 let grant = roles::MemberGrant { member: "bb".repeat(32), role_ids: vec![role.role_id.clone()] };
1471 assert!(build_grant_edition(&owner, &cid(), &grant, 1, Some(&[0u8; 32]), 100, None).is_err());
1472 assert!(build_grant_edition(&owner, &cid(), &grant, 2, None, 100, None).is_err());
1473 }
1474
1475 #[test]
1477 fn builders_reject_bad_hex() {
1478 let owner = Keys::generate();
1479 let bad_role = Role {
1480 role_id: "not-hex".into(), name: "X".into(), position: 1,
1481 permissions: Permissions::admin(), scope: RoleScope::Server, color: 0,
1482 };
1483 assert!(build_role_edition(&owner, &bad_role, 1, None, 100, None).is_err());
1484 let bad_grant = roles::MemberGrant { member: "zz".repeat(32), role_ids: vec!["a".repeat(64)] };
1485 assert!(build_grant_edition(&owner, &cid(), &bad_grant, 1, None, 100, None).is_err());
1486 }
1487
1488 #[test]
1492 fn producer_built_chain_folds_to_latest() {
1493 let owner = Keys::generate();
1494 let member = "cc".repeat(32);
1495 let role_a = "a".repeat(64);
1496 let role_b = "b".repeat(64);
1497
1498 let g1 = roles::MemberGrant { member: member.clone(), role_ids: vec![role_a] };
1499 let e1 = build_grant_edition(&owner, &cid(), &g1, 1, None, 100, None).unwrap();
1500 let member_bytes = crate::simd::hex::hex_to_bytes_32(&member);
1502 let eid = crate::community::derive::grant_locator(&cid(), &member_bytes);
1503 let v1_hash = version::edition_hash(&eid, 1, None, serde_json::to_string(&g1).unwrap().as_bytes());
1504
1505 let g2 = roles::MemberGrant { member: member.clone(), role_ids: vec![role_b.clone()] };
1506 let e2 = build_grant_edition(&owner, &cid(), &g2, 2, Some(&v1_hash), 101, None).unwrap();
1507
1508 let folded = fold_roster(&[e1, e2], &cid(), &Default::default());
1509 assert_eq!(folded.skipped, 0);
1510 assert!(folded.gapped_entities.is_empty(), "v2 links to v1 — no gap");
1511 let held: Vec<&String> = folded.roles.grants.iter().flat_map(|g| &g.role_ids).collect();
1512 assert_eq!(held, vec![&role_b]);
1513 }
1514
1515 #[test]
1517 fn control_edition_seals_opens_and_folds_end_to_end() {
1518 let owner = Keys::generate();
1519 let community_id = CommunityId([0x09; 32]);
1520 let epoch = Epoch(0);
1521 let role = Role {
1522 role_id: "a".repeat(64), name: "Admin".into(), position: 1,
1523 permissions: Permissions::admin(), scope: RoleScope::Server, color: 0,
1524 };
1525
1526 let inner = build_role_edition(&owner, &role, 1, None, 100, None).unwrap();
1527 let outer = seal_control_edition(&Keys::generate(), &inner, &sr(), &community_id, epoch).unwrap();
1528
1529 assert_ne!(outer.pubkey, owner.public_key(), "outer is ephemeral-signed");
1531 assert!(!outer.content.contains(&role.role_id), "inner content is encrypted on the wire");
1532
1533 assert!(open_control_edition(&outer, &ServerRootKey([0xAA; 32])).is_err());
1535
1536 let reopened = open_control_edition(&outer, &sr()).unwrap();
1538 assert_eq!(edition::parse_edition_inner(&reopened).unwrap().author, owner.public_key());
1539 let folded = fold_roster(&[reopened], &cid(), &Default::default());
1540 assert_eq!(folded.skipped, 0);
1541 assert!(folded.roles.role(&role.role_id).is_some(), "build→seal→open→parse→fold round-trips");
1542 }
1543
1544 #[test]
1547 fn control_edition_round_trips_at_a_nonzero_epoch() {
1548 let owner = Keys::generate();
1549 let community_id = CommunityId([0x09; 32]);
1550 let role = Role {
1551 role_id: "a".repeat(64), name: "Admin".into(), position: 1,
1552 permissions: Permissions::admin(), scope: RoleScope::Server, color: 0,
1553 };
1554 let inner = build_role_edition(&owner, &role, 1, None, 100, None).unwrap();
1555 let outer = seal_control_edition(&Keys::generate(), &inner, &sr(), &community_id, Epoch(4)).unwrap();
1556
1557 let z4 = control_pseudonym(&sr(), &community_id, Epoch(4));
1559 assert_ne!(z4, control_pseudonym(&sr(), &community_id, Epoch(0)), "epochs address distinct pseudonyms");
1560 let z_tag = outer.tags.iter().find_map(|t| {
1561 let s = t.as_slice();
1562 (s.len() >= 2 && s[0] == "z").then(|| s[1].clone())
1563 }).expect("control outer carries a z tag");
1564 assert_eq!(z_tag, z4, "sealed at the epoch-4 pseudonym, NOT epoch 0 (regression #1 guard)");
1565
1566 let reopened = open_control_edition(&outer, &sr()).unwrap();
1568 let folded = fold_roster(&[reopened], &cid(), &Default::default());
1569 assert!(folded.roles.role(&role.role_id).is_some(), "seal→open→fold round-trips at a non-zero epoch");
1570 }
1571
1572 #[test]
1575 fn control_pseudonym_golden_vector() {
1576 let server_root = ServerRootKey([0x07; 32]);
1577 let community_id = CommunityId([0x09; 32]);
1578 assert_eq!(
1579 control_pseudonym(&server_root, &community_id, Epoch(0)),
1580 "e719f2d29ca005dfe805b1f85f696948394661c748e1f98b2df7d396260f6378"
1581 );
1582 }
1583
1584 #[test]
1586 fn open_rejects_non_control_outer() {
1587 let bogus = EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_MESSAGE), "x")
1588 .finalize(&Keys::generate())
1589 .unwrap();
1590 assert!(open_control_edition(&bogus, &sr()).is_err());
1591 }
1592
1593 #[test]
1596 fn delegation_rejects_a_self_signed_admin_grant() {
1597 let owner = Keys::generate();
1600 let mallory = Keys::generate();
1601 let admin = "a".repeat(64);
1602 let role_ev = role_event(&owner, &admin, 1, 1, None, 100); let (self_grant, _, _) = grant_event(&mallory, &mallory.public_key().to_hex(), vec![admin.clone()], 1, None, 101);
1604 let folded = fold_roster(&[role_ev, self_grant], &cid(), &Default::default());
1605 assert!(folded.roles.grants.iter().any(|g| g.member == mallory.public_key().to_hex()));
1607
1608 let authorized = authorize_delegation(&folded, Some(&owner.public_key().to_hex()));
1609 assert!(authorized.roles.iter().any(|r| r.role_id == admin), "owner-signed role stays");
1610 assert!(
1611 !authorized.grants.iter().any(|g| g.member == mallory.public_key().to_hex()),
1612 "self-signed Admin grant is REJECTED — no self-promotion"
1613 );
1614 }
1615
1616 #[test]
1617 fn delegation_accepts_owner_signed_role_and_grant() {
1618 let owner = Keys::generate();
1619 let member = Keys::generate();
1620 let admin = "a".repeat(64);
1621 let role_ev = role_event(&owner, &admin, 1, 1, None, 100);
1622 let (grant_ev, _, _) = grant_event(&owner, &member.public_key().to_hex(), vec![admin.clone()], 1, None, 101);
1623 let authorized = authorize_delegation(&fold_roster(&[role_ev, grant_ev], &cid(), &Default::default()), Some(&owner.public_key().to_hex()));
1624 assert!(authorized.roles.iter().any(|r| r.role_id == admin));
1625 assert!(authorized.grants.iter().any(|g| g.member == member.public_key().to_hex()));
1626 }
1627
1628 #[test]
1629 fn delegation_chains_owner_to_admin_to_mod() {
1630 let owner = Keys::generate();
1632 let alice = Keys::generate();
1633 let bob = Keys::generate();
1634 let (admin, moderator) = ("a".repeat(64), "b".repeat(64));
1635 let r_admin = role_event(&owner, &admin, 1, 1, None, 100);
1636 let (g_alice, _, _) = grant_event(&owner, &alice.public_key().to_hex(), vec![admin.clone()], 1, None, 101);
1637 let r_mod = role_event(&alice, &moderator, 3, 1, None, 102); let (g_bob, _, _) = grant_event(&alice, &bob.public_key().to_hex(), vec![moderator.clone()], 1, None, 103);
1639 let authorized = authorize_delegation(
1640 &fold_roster(&[r_admin, g_alice, r_mod, g_bob], &cid(), &Default::default()),
1641 Some(&owner.public_key().to_hex()),
1642 );
1643 assert!(authorized.roles.iter().any(|r| r.role_id == moderator), "admin Alice could create Mod");
1644 assert!(authorized.grants.iter().any(|g| g.member == alice.public_key().to_hex()), "owner→Alice Admin");
1645 assert!(authorized.grants.iter().any(|g| g.member == bob.public_key().to_hex()), "Alice→Bob Mod (delegated)");
1646 }
1647
1648 #[test]
1649 fn delegation_demoted_admins_delegated_grant_is_dropped() {
1650 let owner = Keys::generate();
1655 let alice = Keys::generate();
1656 let bob = Keys::generate();
1657 let (admin, moderator) = ("a".repeat(64), "b".repeat(64));
1658 let r_admin = role_event(&owner, &admin, 1, 1, None, 100);
1659 let (g_alice1, a_eid, a_c1) = grant_event(&owner, &alice.public_key().to_hex(), vec![admin.clone()], 1, None, 101);
1660 let a_v1 = version::edition_hash(&a_eid, 1, None, a_c1.as_bytes());
1661 let (g_alice2, _, _) = grant_event(&owner, &alice.public_key().to_hex(), vec![], 2, Some(&a_v1), 102); let r_mod = role_event(&alice, &moderator, 3, 1, None, 103);
1663 let (g_bob, _, _) = grant_event(&alice, &bob.public_key().to_hex(), vec![moderator.clone()], 1, None, 104);
1664
1665 let folded = fold_roster(&[r_admin, g_alice1, g_alice2, r_mod, g_bob], &cid(), &Default::default());
1666 let authorized = authorize_delegation(&folded, Some(&owner.public_key().to_hex()));
1667 assert!(!authorized.grants.iter().any(|g| g.member == alice.public_key().to_hex()), "Alice's admin grant is revoked");
1668 assert!(
1669 !authorized.grants.iter().any(|g| g.member == bob.public_key().to_hex()),
1670 "a since-demoted admin's delegated grant is dropped — version-pinning falls out, no citation needed"
1671 );
1672 }
1673
1674 #[test]
1675 fn delegation_floor_blocks_a_rolled_back_admin_grant() {
1676 let owner = Keys::generate();
1681 let alice = Keys::generate();
1682 let bob = Keys::generate();
1683 let (admin, moderator) = ("a".repeat(64), "b".repeat(64));
1684 let r_admin = role_event(&owner, &admin, 1, 1, None, 100);
1685 let (g_alice1, a_eid, _) = grant_event(&owner, &alice.public_key().to_hex(), vec![admin.clone()], 1, None, 101);
1686 let r_mod = role_event(&alice, &moderator, 3, 1, None, 102);
1687 let (g_bob, _, _) = grant_event(&alice, &bob.public_key().to_hex(), vec![moderator.clone()], 1, None, 103);
1688
1689 let mut floors = std::collections::HashMap::new();
1690 floors.insert(crate::simd::hex::bytes_to_hex_32(&a_eid), (2u64, [0x9Au8; 32])); let folded = fold_roster(&[r_admin, g_alice1, r_mod, g_bob], &cid(), &floors);
1692 let authorized = authorize_delegation(&folded, Some(&owner.public_key().to_hex()));
1693 assert!(!authorized.grants.iter().any(|g| g.member == alice.public_key().to_hex()), "rolled-back Alice grant refused");
1694 assert!(
1695 !authorized.grants.iter().any(|g| g.member == bob.public_key().to_hex()),
1696 "Bob's delegated grant dropped — the floor blocks the rollback that would re-authorize Alice"
1697 );
1698 }
1699
1700 #[test]
1701 fn delegation_admin_cannot_grant_a_peer_admin() {
1702 let owner = Keys::generate();
1704 let alice = Keys::generate();
1705 let bob = Keys::generate();
1706 let admin = "a".repeat(64);
1707 let r_admin = role_event(&owner, &admin, 1, 1, None, 100);
1708 let (g_alice, _, _) = grant_event(&owner, &alice.public_key().to_hex(), vec![admin.clone()], 1, None, 101);
1709 let (g_bob, _, _) = grant_event(&alice, &bob.public_key().to_hex(), vec![admin.clone()], 1, None, 102);
1710 let authorized = authorize_delegation(
1711 &fold_roster(&[r_admin, g_alice, g_bob], &cid(), &Default::default()),
1712 Some(&owner.public_key().to_hex()),
1713 );
1714 assert!(authorized.grants.iter().any(|g| g.member == alice.public_key().to_hex()), "owner→Alice ok");
1715 assert!(
1716 !authorized.grants.iter().any(|g| g.member == bob.public_key().to_hex()),
1717 "an admin cannot grant a PEER-rank Admin"
1718 );
1719 }
1720
1721 #[test]
1722 fn delegation_rejects_a_circular_grant_with_no_owner_root() {
1723 let owner = Keys::generate(); let a = Keys::generate();
1727 let b = Keys::generate();
1728 let admin = "a".repeat(64);
1729 let r_admin = role_event(&owner, &admin, 1, 1, None, 100);
1730 let (g_ab, _, _) = grant_event(&a, &b.public_key().to_hex(), vec![admin.clone()], 1, None, 101);
1731 let (g_ba, _, _) = grant_event(&b, &a.public_key().to_hex(), vec![admin.clone()], 1, None, 102);
1732 let authorized = authorize_delegation(
1733 &fold_roster(&[r_admin, g_ab, g_ba], &cid(), &Default::default()),
1734 Some(&owner.public_key().to_hex()),
1735 );
1736 assert!(authorized.roles.iter().any(|r| r.role_id == admin), "owner's role stays");
1737 assert!(authorized.grants.is_empty(), "a circular mutual-admin grant cannot bootstrap without the owner");
1738 }
1739
1740 #[test]
1741 fn delegation_rejects_a_position_0_role_even_owner_signed() {
1742 let owner = Keys::generate();
1745 let mallory = Keys::generate();
1746 let by_mallory = authorize_delegation(
1747 &fold_roster(&[role_event(&mallory, &"e".repeat(64), 0, 1, None, 100)], &cid(), &Default::default()),
1748 Some(&owner.public_key().to_hex()),
1749 );
1750 assert!(by_mallory.roles.is_empty(), "a non-owner cannot mint a position-0 role");
1751 let by_owner = authorize_delegation(
1752 &fold_roster(&[role_event(&owner, &"f".repeat(64), 0, 1, None, 100)], &cid(), &Default::default()),
1753 Some(&owner.public_key().to_hex()),
1754 );
1755 assert!(by_owner.roles.is_empty(), "position 0 is the attestation's — even the owner can't mint a pos-0 role");
1756 }
1757
1758 #[test]
1759 fn delegation_unproven_community_yields_empty_roster() {
1760 let owner = Keys::generate();
1762 let role_ev = role_event(&owner, &"a".repeat(64), 1, 1, None, 100);
1763 let authorized = authorize_delegation(&fold_roster(&[role_ev], &cid(), &Default::default()), None);
1764 assert!(authorized.roles.is_empty() && authorized.grants.is_empty(), "no root ⇒ empty authorized roster");
1765 }
1766}