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 .sign_with_keys(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 .sign_with_keys(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 .sign_with_keys(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 .sign_with_keys(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 let entity_id = super::derive::dissolved_locator(community_id);
194 edition::build_edition_inner(author, VSK_DISSOLVED, &entity_id, 1, None, "{}", created_at, None)
196}
197
198pub fn build_invite_links_edition(
205 actor: &Keys,
206 community_id: &CommunityId,
207 link_locators: &[String],
208 version: u64,
209 prev_hash: Option<&[u8; 32]>,
210 created_at: u64,
211 authority: Option<&edition::AuthorityCitation>,
212) -> Result<Event, String> {
213 build_invite_links_edition_unsigned(actor.public_key(), community_id, link_locators, version, prev_hash, created_at, authority)?
214 .sign_with_keys(actor)
215 .map_err(|e| format!("sign invite-links edition: {e}"))
216}
217
218pub fn build_invite_links_edition_unsigned(
221 author: PublicKey,
222 community_id: &CommunityId,
223 link_locators: &[String],
224 version: u64,
225 prev_hash: Option<&[u8; 32]>,
226 created_at: u64,
227 authority: Option<&edition::AuthorityCitation>,
228) -> Result<UnsignedEvent, String> {
229 check_chain_shape(version, prev_hash)?;
230 let entity_id = super::derive::invite_links_locator(community_id, &author.to_bytes());
231 let content = serde_json::to_string(link_locators).map_err(|e| e.to_string())?;
232 Ok(edition::build_edition_inner(author, VSK_INVITE_LINKS, &entity_id, version, prev_hash, &content, created_at, authority))
233}
234
235pub fn build_community_root_edition(
239 actor: &Keys,
240 community_id: &CommunityId,
241 meta: &super::metadata::CommunityMetadata,
242 version: u64,
243 prev_hash: Option<&[u8; 32]>,
244 created_at: u64,
245 authority: Option<&edition::AuthorityCitation>,
246) -> Result<Event, String> {
247 build_community_root_edition_unsigned(actor.public_key(), community_id, meta, version, prev_hash, created_at, authority)?
248 .sign_with_keys(actor)
249 .map_err(|e| format!("sign community-root edition: {e}"))
250}
251
252pub fn build_community_root_edition_unsigned(
254 author: PublicKey,
255 community_id: &CommunityId,
256 meta: &super::metadata::CommunityMetadata,
257 version: u64,
258 prev_hash: Option<&[u8; 32]>,
259 created_at: u64,
260 authority: Option<&edition::AuthorityCitation>,
261) -> Result<UnsignedEvent, String> {
262 check_chain_shape(version, prev_hash)?;
263 let content = serde_json::to_string(meta).map_err(|e| e.to_string())?;
264 Ok(edition::build_edition_inner(author, VSK_COMMUNITY_ROOT, &community_id.0, version, prev_hash, &content, created_at, authority))
265}
266
267pub fn build_channel_metadata_edition(
271 actor: &Keys,
272 channel_id: &ChannelId,
273 meta: &super::metadata::ChannelMetadata,
274 version: u64,
275 prev_hash: Option<&[u8; 32]>,
276 created_at: u64,
277 authority: Option<&edition::AuthorityCitation>,
278) -> Result<Event, String> {
279 build_channel_metadata_edition_unsigned(actor.public_key(), channel_id, meta, version, prev_hash, created_at, authority)?
280 .sign_with_keys(actor)
281 .map_err(|e| format!("sign channel-metadata edition: {e}"))
282}
283
284pub fn build_channel_metadata_edition_unsigned(
286 author: PublicKey,
287 channel_id: &ChannelId,
288 meta: &super::metadata::ChannelMetadata,
289 version: u64,
290 prev_hash: Option<&[u8; 32]>,
291 created_at: u64,
292 authority: Option<&edition::AuthorityCitation>,
293) -> Result<UnsignedEvent, String> {
294 check_chain_shape(version, prev_hash)?;
295 let content = serde_json::to_string(meta).map_err(|e| e.to_string())?;
296 Ok(edition::build_edition_inner(author, VSK_CHANNEL, &channel_id.0, version, prev_hash, &content, created_at, authority))
297}
298
299pub fn control_pseudonym(server_root: &ServerRootKey, community_id: &CommunityId, epoch: Epoch) -> String {
306 channel_pseudonym(&ChannelKey(*server_root.as_bytes()), &ChannelId(community_id.0), epoch).to_hex()
307}
308
309pub fn seal_control_edition(
314 ephemeral: &Keys,
315 inner: &Event,
316 server_root: &ServerRootKey,
317 community_id: &CommunityId,
318 epoch: Epoch,
319) -> Result<Event, String> {
320 if inner.kind.as_u16() != event_kind::COMMUNITY_CONTROL {
321 return Err("a control edition must be kind 3308".to_string());
322 }
323 let content = cipher::seal(server_root.as_bytes(), inner.as_json().as_bytes())
324 .map_err(|e| format!("seal control edition: {e}"))?;
325 let pseudonym = control_pseudonym(server_root, community_id, epoch);
326 EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_CONTROL), content)
327 .tags([
328 Tag::custom(TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::Z)), [pseudonym]),
329 Tag::custom(TagKind::Custom("v".into()), ["1".to_string()]),
330 ])
331 .sign_with_keys(ephemeral)
332 .map_err(|e| format!("sign control outer: {e}"))
333}
334
335pub fn open_control_edition(outer: &Event, server_root: &ServerRootKey) -> Result<Event, String> {
350 if outer.kind.as_u16() != event_kind::COMMUNITY_CONTROL {
351 return Err("not a control-plane outer (kind != 3308)".to_string());
352 }
353 match outer.tags.iter().find_map(|t| {
354 let s = t.as_slice();
355 (s.len() >= 2 && s[0] == "v").then(|| s[1].clone())
356 }) {
357 Some(v) if v == "1" => {}
358 other => return Err(format!("unsupported control edition version: {other:?}")),
359 }
360 let plaintext = cipher::open(server_root.as_bytes(), &outer.content)
361 .map_err(|e| format!("open control edition: {e}"))?;
362 let json = String::from_utf8(plaintext).map_err(|e| format!("control inner utf8: {e}"))?;
363 let inner = Event::from_json(&json).map_err(|e| format!("control inner parse: {e}"))?;
364 if inner.kind.as_u16() != event_kind::COMMUNITY_CONTROL {
365 return Err("control inner is not kind 3308".to_string());
366 }
367 Ok(inner)
368}
369
370pub fn seal_dissolved_edition(ephemeral: &Keys, inner: &Event, community_id: &CommunityId) -> Result<Event, String> {
377 if inner.kind.as_u16() != event_kind::COMMUNITY_CONTROL {
378 return Err("a dissolved tombstone must be kind 3308".to_string());
379 }
380 let key = super::derive::dissolved_envelope_key(community_id);
381 let content = cipher::seal(&key, inner.as_json().as_bytes())
382 .map_err(|e| format!("seal dissolved edition: {e}"))?;
383 let pseudonym = super::derive::dissolved_pseudonym(community_id);
384 EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_CONTROL), content)
385 .tags([
386 Tag::custom(TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::Z)), [pseudonym]),
387 Tag::custom(TagKind::Custom("v".into()), ["1".to_string()]),
388 ])
389 .sign_with_keys(ephemeral)
390 .map_err(|e| format!("sign dissolved outer: {e}"))
391}
392
393pub fn dissolved_tombstone_signer(outer: &Event, community_id: &CommunityId) -> Option<PublicKey> {
398 if outer.kind.as_u16() != event_kind::COMMUNITY_CONTROL {
399 return None;
400 }
401 let key = super::derive::dissolved_envelope_key(community_id);
402 let plaintext = cipher::open(&key, &outer.content).ok()?;
403 let inner = Event::from_json(&String::from_utf8(plaintext).ok()?).ok()?;
404 let p = edition::parse_edition_inner(&inner).ok()?;
405 (p.vsk == VSK_DISSOLVED && p.entity_id == super::derive::dissolved_locator(community_id)).then_some(p.author)
406}
407
408#[derive(Clone)]
411pub struct EntityHead {
412 pub entity_hex: String,
414 pub version: u64,
415 pub self_hash: [u8; 32],
416 pub inner_id: [u8; 32],
420 pub citation: Option<edition::AuthorityCitation>,
425}
426
427#[derive(Clone)]
429pub struct FoldedRoster {
430 pub roles: roles::CommunityRoles,
435 pub role_authors: Vec<PublicKey>,
438 pub grant_authors: Vec<PublicKey>,
440 pub gapped_entities: Vec<[u8; 32]>,
444 pub skipped: usize,
448 pub fetched: usize,
453 pub heads: Vec<EntityHead>,
457 pub banned: Vec<String>,
462 pub banlist_author: Option<PublicKey>,
465 pub dissolved_by: Vec<PublicKey>,
473 pub banlist_head: Option<EntityHead>,
475 pub invite_link_sets: Vec<InviteLinkSet>,
480 pub root_meta: Option<super::metadata::CommunityMetadata>,
483 pub root_author: Option<PublicKey>,
485 pub root_head: Option<EntityHead>,
487 pub root_candidates: Vec<RootCandidate>,
494 pub channel_meta: Vec<ChannelMetaHead>,
498 pub channel_candidates: Vec<ChannelMetaHead>,
505}
506
507#[derive(Clone)]
510pub struct InviteLinkSet {
511 pub creator: PublicKey,
512 pub locators: Vec<String>,
513 pub head: EntityHead,
514}
515
516#[derive(Clone)]
520pub struct RootCandidate {
521 pub meta: super::metadata::CommunityMetadata,
522 pub author: PublicKey,
523 pub head: EntityHead,
524}
525
526#[derive(Clone)]
529pub struct ChannelMetaHead {
530 pub channel_id: [u8; 32],
531 pub meta: super::metadata::ChannelMetadata,
532 pub author: PublicKey,
533 pub head: EntityHead,
534}
535
536pub fn fold_roster(
560 inner_editions: &[Event],
561 community_id: &CommunityId,
562 floors: &HashMap<String, (u64, [u8; 32])>,
563) -> FoldedRoster {
564 let mut skipped = inner_editions.len().saturating_sub(MAX_CONTROL_EDITIONS);
565
566 let parsed: Vec<edition::ParsedEdition> = inner_editions
568 .iter()
569 .take(MAX_CONTROL_EDITIONS)
570 .filter_map(|e| match edition::parse_edition_inner(e) {
571 Ok(p) => Some(p),
572 Err(_) => {
573 skipped += 1;
574 None
575 }
576 })
577 .collect();
578
579 let dissolved_eid = super::derive::dissolved_locator(community_id);
585 let mut dissolved_by: Vec<PublicKey> = Vec::new();
586 for p in &parsed {
587 if p.vsk == VSK_DISSOLVED && p.entity_id == dissolved_eid && !dissolved_by.contains(&p.author) {
588 dissolved_by.push(p.author);
589 }
590 }
591
592 let mut by_entity: HashMap<[u8; 32], Vec<&edition::ParsedEdition>> = HashMap::new();
593 for p in &parsed {
594 if p.vsk == VSK_DISSOLVED {
597 continue;
598 }
599 by_entity.entry(p.entity_id).or_default().push(p);
600 }
601
602 let mut out = roles::CommunityRoles::default();
603 let mut role_authors: Vec<PublicKey> = Vec::new();
604 let mut grant_authors: Vec<PublicKey> = Vec::new();
605 let mut gapped_entities = Vec::new();
606 let mut heads: Vec<EntityHead> = Vec::new();
607 let mut banned: Vec<String> = Vec::new();
608 let mut banlist_author: Option<PublicKey> = None;
609 let mut banlist_head: Option<EntityHead> = None;
610 let banlist_eid = super::derive::banlist_locator(community_id);
611 let mut invite_link_sets: Vec<InviteLinkSet> = Vec::new();
612 let mut root_meta: Option<super::metadata::CommunityMetadata> = None;
613 let mut root_author: Option<PublicKey> = None;
614 let mut root_head: Option<EntityHead> = None;
615 let mut root_candidates: Vec<RootCandidate> = Vec::new();
616 let mut channel_meta: Vec<ChannelMetaHead> = Vec::new();
617 let mut channel_candidates: Vec<ChannelMetaHead> = Vec::new();
618
619 for (entity_id, editions) in by_entity {
620 let fold_eds: Vec<version::Edition> = editions.iter().map(|p| p.to_fold_edition()).collect();
621 let entity_hex = crate::simd::hex::bytes_to_hex_32(&entity_id);
624 let floor = floors.get(&entity_hex);
625 let floor_v = floor.map(|(v, _)| *v).unwrap_or(0);
626 let result = version::fold(&fold_eds, floor_v, floor.map(|(_, h)| h));
627 let head_idx = if result.gap {
638 gapped_entities.push(entity_id);
639 if floor_v == 0 {
640 match version::bootstrap_head(&fold_eds, 0) { Some(i) => i, None => continue }
641 } else {
642 match version::bootstrap_head(&fold_eds, floor_v) {
651 Some(i) if matches!(editions[i].vsk.as_str(), VSK_COMMUNITY_ROOT | VSK_CHANNEL) => i,
652 _ => continue, }
654 }
655 } else {
656 match result.head { Some(i) => i, None => continue }
657 };
658 let head = editions[head_idx];
659 let mut record_head = || {
660 heads.push(EntityHead {
661 entity_hex: crate::simd::hex::bytes_to_hex_32(&entity_id),
662 version: head.version,
663 self_hash: head.self_hash,
664 inner_id: head.inner_id,
665 citation: head.authority.clone(),
666 });
667 };
668 match head.vsk.as_str() {
669 VSK_ROLE => match serde_json::from_str::<roles::Role>(&head.content) {
670 Ok(role) if hex32(&role.role_id) == Some(entity_id) => {
673 record_head();
674 role_authors.push(head.author);
675 out.roles.push(role);
676 }
677 _ => skipped += 1,
678 },
679 VSK_GRANT => match serde_json::from_str::<roles::MemberGrant>(&head.content) {
680 Ok(grant)
684 if hex32(&grant.member)
685 .is_some_and(|m| super::derive::grant_locator(community_id, &m) == entity_id) =>
686 {
687 record_head();
690 if !grant.role_ids.is_empty() {
691 grant_authors.push(head.author);
692 out.grants.push(grant);
693 }
694 }
695 _ => skipped += 1,
696 },
697 VSK_BANLIST if entity_id == banlist_eid => match serde_json::from_str::<Vec<String>>(&head.content) {
698 Ok(list) => {
701 banned = list;
702 banlist_author = Some(head.author);
703 banlist_head = Some(EntityHead {
704 entity_hex: crate::simd::hex::bytes_to_hex_32(&entity_id),
705 version: head.version,
706 self_hash: head.self_hash,
707 inner_id: head.inner_id,
708 citation: head.authority.clone(),
709 });
710 }
711 _ => skipped += 1,
712 },
713 VSK_INVITE_LINKS if super::derive::invite_links_locator(community_id, &head.author.to_bytes()) == entity_id => {
714 match serde_json::from_str::<Vec<String>>(&head.content) {
719 Ok(locators) => invite_link_sets.push(InviteLinkSet {
720 creator: head.author,
721 locators,
722 head: EntityHead {
723 entity_hex: crate::simd::hex::bytes_to_hex_32(&entity_id),
724 version: head.version,
725 self_hash: head.self_hash,
726 inner_id: head.inner_id,
727 citation: head.authority.clone(),
728 },
729 }),
730 _ => skipped += 1,
731 }
732 }
733 VSK_COMMUNITY_ROOT if entity_id == community_id.0 => {
734 let entity_hex = crate::simd::hex::bytes_to_hex_32(&entity_id);
741 let mut cands: Vec<(u64, [u8; 32], RootCandidate)> = editions
742 .iter()
743 .filter(|e| e.version >= floor_v)
744 .filter_map(|e| {
745 serde_json::from_str::<super::metadata::CommunityMetadata>(&e.content)
746 .ok()
747 .map(|meta| (e.version, e.inner_id, RootCandidate {
748 meta,
749 author: e.author,
750 head: EntityHead {
751 entity_hex: entity_hex.clone(),
752 version: e.version,
753 self_hash: e.self_hash,
754 inner_id: e.inner_id,
755 citation: e.authority.clone(),
756 },
757 }))
758 })
759 .collect();
760 if cands.is_empty() {
761 skipped += 1;
762 } else {
763 cands.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)));
764 root_candidates = cands.into_iter().map(|(_, _, c)| c).collect();
765 let top = &root_candidates[0];
766 root_meta = Some(top.meta.clone());
767 root_author = Some(top.author);
768 root_head = Some(top.head.clone());
769 }
770 }
771 VSK_CHANNEL => {
772 let entity_hex = crate::simd::hex::bytes_to_hex_32(&entity_id);
778 let mut cands: Vec<(u64, [u8; 32], ChannelMetaHead)> = editions
779 .iter()
780 .filter(|e| e.version >= floor_v)
781 .filter_map(|e| {
782 serde_json::from_str::<super::metadata::ChannelMetadata>(&e.content)
783 .ok()
784 .map(|meta| (e.version, e.inner_id, ChannelMetaHead {
785 channel_id: entity_id,
786 meta,
787 author: e.author,
788 head: EntityHead {
789 entity_hex: entity_hex.clone(),
790 version: e.version,
791 self_hash: e.self_hash,
792 inner_id: e.inner_id,
793 citation: e.authority.clone(),
794 },
795 }))
796 })
797 .collect();
798 if cands.is_empty() {
799 skipped += 1;
800 } else {
801 cands.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)));
802 channel_meta.push(cands[0].2.clone());
804 channel_candidates.extend(cands.into_iter().map(|(_, _, c)| c));
805 }
806 }
807 _ => {} }
809 }
810
811 FoldedRoster {
812 roles: out, role_authors, grant_authors, gapped_entities, skipped, fetched: 0, heads,
813 banned, banlist_author, banlist_head, dissolved_by,
814 invite_link_sets,
815 root_meta, root_author, root_head, root_candidates, channel_meta, channel_candidates,
816 }
817}
818
819pub fn authorize_delegation(folded: &FoldedRoster, owner_hex: Option<&str>) -> roles::CommunityRoles {
849 use roles::Permissions;
850 let mut accepted = roles::CommunityRoles::default();
851 let mut role_done = vec![false; folded.roles.roles.len()];
852 let mut grant_done = vec![false; folded.roles.grants.len()];
853
854 loop {
855 let mut changed = false;
856
857 for (i, role) in folded.roles.roles.iter().enumerate() {
859 if role_done[i] {
860 continue;
861 }
862 if role.position == 0 {
865 role_done[i] = true;
866 continue;
867 }
868 let author = folded.role_authors[i].to_hex();
869 if accepted.can_act_on_position(&author, owner_hex, role.position, Permissions::MANAGE_ROLES) {
870 accepted.roles.push(role.clone());
871 role_done[i] = true;
872 changed = true;
873 }
874 }
875
876 for (i, grant) in folded.roles.grants.iter().enumerate() {
881 if grant_done[i] {
882 continue;
883 }
884 let positions: Option<Vec<u32>> =
885 grant.role_ids.iter().map(|rid| accepted.role(rid).map(|r| r.position)).collect();
886 let Some(positions) = positions else { continue }; let author = folded.grant_authors[i].to_hex();
888 let outranks_all_roles = positions
889 .iter()
890 .all(|p| accepted.can_act_on_position(&author, owner_hex, *p, Permissions::MANAGE_ROLES));
891 if outranks_all_roles
892 && accepted.can_act_on_member(&author, owner_hex, &grant.member, Permissions::MANAGE_ROLES)
893 {
894 accepted.grants.push(grant.clone());
895 grant_done[i] = true;
896 changed = true;
897 }
898 }
899
900 if !changed {
901 break;
902 }
903 }
904 accepted
905}
906
907pub fn authority_citation_satisfied(
932 heads: &[EntityHead],
933 owner_hex: Option<&str>,
934 actor_hex: &str,
935 actor_grant_hex: &str,
936 citation: Option<&edition::AuthorityCitation>,
937) -> bool {
938 if owner_hex == Some(actor_hex) {
939 return true;
940 }
941 let Some(c) = citation else { return false };
942 let entity_hex = crate::simd::hex::bytes_to_hex_32(&c.entity_id);
943 if entity_hex != actor_grant_hex {
947 return false;
948 }
949 match heads.iter().find(|h| h.entity_hex == entity_hex) {
950 Some(h) if h.version > c.version => true,
953 Some(h) if h.version == c.version => h.self_hash == c.edition_hash,
956 _ => false,
958 }
959}
960
961#[cfg(test)]
962mod tests {
963 use super::*;
964 use crate::community::roles::{Permissions, Role, RoleScope};
965
966 fn sr() -> ServerRootKey {
967 ServerRootKey([0x07; 32])
968 }
969
970 #[test]
971 fn authority_citation_satisfied_pins_to_a_synced_complete_grant() {
972 let owner = "ow".repeat(32);
973 let actor = "ac".repeat(32);
974 let entity = [0x55u8; 32];
975 let hash = [0x66u8; 32];
976 let eh = crate::simd::hex::bytes_to_hex_32(&entity); let head = |v: u64, h: [u8; 32]| {
978 vec![EntityHead { entity_hex: eh.clone(), version: v, self_hash: h, inner_id: [0u8; 32], citation: None }]
979 };
980 let cite = edition::AuthorityCitation { entity_id: entity, version: 3, edition_hash: hash };
981
982 assert!(authority_citation_satisfied(&[], Some(&owner), &owner, &eh, None));
984 assert!(!authority_citation_satisfied(&head(3, hash), Some(&owner), &actor, &eh, None));
986 assert!(authority_citation_satisfied(&head(3, hash), Some(&owner), &actor, &eh, Some(&cite)));
988 assert!(!authority_citation_satisfied(&head(3, [0xEE; 32]), Some(&owner), &actor, &eh, Some(&cite)));
990 assert!(authority_citation_satisfied(&head(4, [0x77; 32]), Some(&owner), &actor, &eh, Some(&cite)));
992 assert!(!authority_citation_satisfied(&head(2, hash), Some(&owner), &actor, &eh, Some(&cite)));
994 assert!(!authority_citation_satisfied(&[], Some(&owner), &actor, &eh, Some(&cite)));
996 let foreign = "ff".repeat(32);
998 assert!(!authority_citation_satisfied(&head(3, hash), Some(&owner), &actor, &foreign, Some(&cite)));
999 }
1000 fn cid() -> CommunityId {
1002 CommunityId([0x09; 32])
1003 }
1004
1005 fn role_event(owner: &Keys, role_id: &str, position: u32, version: u64, prev: Option<&[u8; 32]>, created_at: u64) -> Event {
1007 let role = Role {
1008 role_id: role_id.to_string(),
1009 name: "Admin".into(),
1010 position,
1011 permissions: Permissions::admin(),
1012 scope: RoleScope::Server,
1013 color: 0,
1014 };
1015 let eid = crate::simd::hex::hex_to_bytes_32(role_id);
1016 edition::build_edition_inner(owner.public_key(), VSK_ROLE, &eid, version, prev, &serde_json::to_string(&role).unwrap(), created_at, None)
1017 .sign_with_keys(owner)
1018 .unwrap()
1019 }
1020
1021 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) {
1023 let member_bytes = crate::simd::hex::hex_to_bytes_32(member_hex);
1024 let eid = crate::community::derive::grant_locator(&cid(), &member_bytes);
1025 let g = roles::MemberGrant { member: member_hex.to_string(), role_ids };
1026 let content = serde_json::to_string(&g).unwrap();
1027 let ev = edition::build_edition_inner(owner.public_key(), VSK_GRANT, &eid, version, prev, &content, created_at, None)
1028 .sign_with_keys(owner)
1029 .unwrap();
1030 (ev, eid, content)
1031 }
1032
1033 fn groot_event(signer: &Keys, name: &str, desc: &str, version: u64, prev: Option<&[u8; 32]>, created_at: u64) -> Event {
1035 let meta = crate::community::metadata::CommunityMetadata {
1036 name: name.to_string(),
1037 relays: vec![],
1038 description: Some(desc.to_string()),
1039 icon: None,
1040 banner: None,
1041 owner_attestation: None,
1042 };
1043 let content = serde_json::to_string(&meta).unwrap();
1044 edition::build_edition_inner(signer.public_key(), VSK_COMMUNITY_ROOT, &cid().0, version, prev, &content, created_at, None)
1045 .sign_with_keys(signer)
1046 .unwrap()
1047 }
1048
1049 #[test]
1050 fn folds_a_role_and_a_grant() {
1051 let owner = Keys::generate();
1052 let role_id = "a".repeat(64);
1053 let member = "bb".repeat(32);
1054 let role_ev = role_event(&owner, &role_id, 1, 1, None, 100);
1055 let (grant_ev, _, _) = grant_event(&owner, &member, vec![role_id.clone()], 1, None, 101);
1056 let folded = fold_roster(&[role_ev, grant_ev], &cid(), &Default::default());
1057 assert_eq!(folded.roles.roles.len(), 1);
1058 assert_eq!(folded.roles.roles[0].role_id, role_id);
1059 assert!(folded.roles.is_admin(&member), "the member holds the granted Admin role");
1060 assert_eq!(folded.skipped, 0);
1061 assert!(folded.gapped_entities.is_empty());
1062 }
1063
1064 #[test]
1065 fn latest_grant_edition_wins() {
1066 let owner = Keys::generate();
1067 let role_a = "a".repeat(64);
1068 let role_b = "b".repeat(64);
1069 let member = "cc".repeat(32);
1070 let (e1, eid, c1) = grant_event(&owner, &member, vec![role_a.clone()], 1, None, 100);
1071 let v1_hash = version::edition_hash(&eid, 1, None, c1.as_bytes());
1072 let (e2, _, _) = grant_event(&owner, &member, vec![role_b.clone()], 2, Some(&v1_hash), 101);
1073 let folded = fold_roster(&[e1, e2], &cid(), &Default::default());
1074 let held: Vec<&String> = folded.roles.grants.iter().flat_map(|g| &g.role_ids).collect();
1075 assert_eq!(held, vec![&role_b], "v2 supersedes v1 — the member now holds role_b, not role_a");
1076 assert_eq!(folded.skipped, 0);
1077 }
1078
1079 #[test]
1083 fn tracking_quarantines_a_gapped_head() {
1084 let owner = Keys::generate();
1085 let role_id = "e".repeat(64);
1086 let eid = crate::simd::hex::hex_to_bytes_32(&role_id);
1087 let ev = role_event(&owner, &role_id, 1, 5, Some(&[0x99u8; 32]), 100);
1088 let mut floors = std::collections::HashMap::new();
1090 floors.insert(role_id.clone(), (3u64, [0x11u8; 32]));
1091 let folded = fold_roster(&[ev], &cid(), &floors);
1092 assert!(folded.roles.roles.is_empty(), "tracking: a gapped tail is quarantined, not folded");
1093 assert_eq!(folded.gapped_entities, vec![eid]);
1094 }
1095
1096 #[test]
1101 fn bootstrapping_surfaces_a_gapped_head_but_authority_gates() {
1102 let owner = Keys::generate();
1103 let owner_hex = owner.public_key().to_hex();
1104 let role_id = "e".repeat(64);
1105 let eid = crate::simd::hex::hex_to_bytes_32(&role_id);
1106 let ev = role_event(&owner, &role_id, 1, 5, Some(&[0x99u8; 32]), 100);
1108 let folded = fold_roster(&[ev], &cid(), &Default::default());
1109 assert_eq!(folded.roles.roles.len(), 1, "bootstrapping surfaces the highest signed head despite the gap");
1110 assert_eq!(folded.gapped_entities, vec![eid], "still flagged pending so the union can fill the gap");
1111 assert_eq!(authorize_delegation(&folded, Some(&owner_hex)).roles.len(), 1, "owner-authored head is authorized");
1112
1113 let stranger = Keys::generate();
1115 let ev2 = role_event(&stranger, &role_id, 1, 5, Some(&[0x99u8; 32]), 100);
1116 let folded2 = fold_roster(&[ev2], &cid(), &Default::default());
1117 assert_eq!(folded2.roles.roles.len(), 1, "surfaced into the raw fold (signature is valid)");
1118 assert!(authorize_delegation(&folded2, Some(&owner_hex)).roles.is_empty(),
1119 "Policy B does NOT bypass authority — an unauthorized author's surfaced head is rejected");
1120 }
1121
1122 #[test]
1126 fn fresh_joiner_recovers_full_plane_across_gaps() {
1127 let owner = Keys::generate();
1128 let owner_hex = owner.public_key().to_hex();
1129 let admin = Keys::generate();
1130 let admin_hex = admin.public_key().to_hex();
1131 let role_id = "a".repeat(64);
1132
1133 let role = role_event(&owner, &role_id, 1, 1, None, 100);
1135 let (grant, _, _) = grant_event(&owner, &admin_hex, vec![role_id.clone()], 2, Some(&[0x99u8; 32]), 110);
1137 let groot_v1 = groot_event(&owner, "Genesis", "", 1, None, 90);
1139 let groot_v11 = groot_event(&admin, "AggroTown v2", "King Claude was here", 11, Some(&[0xABu8; 32]), 200);
1140
1141 let folded = fold_roster(&[role, grant, groot_v1, groot_v11], &cid(), &Default::default());
1142
1143 assert_eq!(folded.root_author.map(|a| a.to_hex()), Some(admin_hex.clone()), "latest GroupRoot author surfaced");
1145 assert_eq!(folded.root_meta.as_ref().unwrap().name, "AggroTown v2");
1146 assert_eq!(folded.root_meta.as_ref().unwrap().description.as_deref(), Some("King Claude was here"));
1147
1148 let authed = authorize_delegation(&folded, Some(&owner_hex));
1150 assert!(
1151 authed.is_authorized(&admin_hex, Some(&owner_hex), roles::Permissions::MANAGE_METADATA),
1152 "the bootstrapped grant authorizes the admin → the fresh joiner trusts the admin's metadata"
1153 );
1154 }
1155
1156 #[test]
1161 fn unauthorized_high_version_metadata_is_surfaced_but_not_authorized() {
1162 let owner = Keys::generate();
1163 let owner_hex = owner.public_key().to_hex();
1164 let stranger = Keys::generate();
1165 let stranger_hex = stranger.public_key().to_hex();
1166 let groot_v1 = groot_event(&owner, "Genesis", "", 1, None, 90);
1167 let forged = groot_event(&stranger, "PWNED", "owned by a hostile relay", 99, Some(&[0xCDu8; 32]), 300);
1168 let folded = fold_roster(&[groot_v1, forged], &cid(), &Default::default());
1169 assert_eq!(folded.root_author.map(|a| a.to_hex()), Some(stranger_hex.clone()));
1171 let authed = authorize_delegation(&folded, Some(&owner_hex));
1172 assert!(
1173 !authed.is_authorized(&stranger_hex, Some(&owner_hex), roles::Permissions::MANAGE_METADATA),
1174 "an unauthorized author's surfaced metadata is NOT authorized — the caller drops it"
1175 );
1176 }
1177
1178 #[test]
1183 fn fresh_fold_picks_owner_reassert_over_a_demoted_admins_forgery_and_edit() {
1184 let owner = Keys::generate();
1185 let owner_hex = owner.public_key().to_hex();
1186 let alice = Keys::generate();
1187 let alice_hex = alice.public_key().to_hex();
1188 let role_id = "a".repeat(64);
1189
1190 let role = role_event(&owner, &role_id, 1, 1, None, 100);
1192 let (grant, geid, gcontent) = grant_event(&owner, &alice_hex, vec![role_id.clone()], 1, None, 110);
1193 let grant_hash = version::edition_hash(&geid, 1, None, gcontent.as_bytes());
1194 let (revoke, _, _) = grant_event(&owner, &alice_hex, vec![], 2, Some(&grant_hash), 120);
1195
1196 let groot_v1 = groot_event(&owner, "Genesis", "", 1, None, 90);
1199 let round1_v2 = groot_event(&alice, "Alice's HQ", "by admin alice", 2, Some(&[0x22u8; 32]), 200);
1200 let forgery_v3 = groot_event(&alice, "FORGERY", "demoted alice", 3, Some(&[0x33u8; 32]), 300);
1201 let reassert_v3 = groot_event(&owner, "Alice's HQ", "re-asserted by owner", 3, Some(&[0x33u8; 32]), 310);
1202
1203 let folded = fold_roster(
1204 &[role, grant, revoke, groot_v1, round1_v2, forgery_v3, reassert_v3],
1205 &cid(), &Default::default(),
1206 );
1207 let authed = authorize_delegation(&folded, Some(&owner_hex));
1208 assert!(!authed.is_authorized(&alice_hex, Some(&owner_hex), Permissions::MANAGE_METADATA),
1209 "Alice is revoked (grant→revoke folds to no roles)");
1210
1211 assert_eq!(folded.root_candidates.iter().filter(|c| c.head.version == 3).count(), 2,
1213 "the candidate set includes both v3 fork members");
1214
1215 let chosen = folded.root_candidates.iter()
1218 .find(|c| authed.is_authorized(&c.author.to_hex(), Some(&owner_hex), Permissions::MANAGE_METADATA))
1219 .expect("an authorized candidate exists");
1220 assert_eq!(chosen.author, owner.public_key(), "the owner's re-assert wins the fork, not Alice's forgery");
1221 assert_eq!(chosen.meta.name, "Alice's HQ", "the demoted admin's content is preserved, not 'FORGERY'");
1222 }
1223
1224 #[test]
1228 fn bootstrapping_surfaces_a_stranger_grant_but_it_never_authorizes() {
1229 let owner = Keys::generate();
1230 let owner_hex = owner.public_key().to_hex();
1231 let stranger = Keys::generate();
1232 let stranger_hex = stranger.public_key().to_hex();
1233 let role_id = "a".repeat(64);
1234 let role = role_event(&owner, &role_id, 1, 1, None, 100);
1236 let (grant, _, _) = grant_event(&stranger, &stranger_hex, vec![role_id.clone()], 99, Some(&[0x99u8; 32]), 200);
1237 let folded = fold_roster(&[role, grant], &cid(), &Default::default());
1238 assert_eq!(folded.roles.grants.len(), 1, "the stranger's grant is surfaced (valid signature)");
1239 let authed = authorize_delegation(&folded, Some(&owner_hex));
1240 assert!(authed.grants.is_empty(), "but it never chains to the owner → dropped");
1241 assert!(!authed.is_authorized(&stranger_hex, Some(&owner_hex), roles::Permissions::BAN),
1242 "the self-granting stranger holds no authority");
1243 }
1244
1245 #[test]
1252 fn mixed_tracking_and_bootstrapping_floors_in_one_fold() {
1253 let owner = Keys::generate();
1254 let role_id = "a".repeat(64);
1255 let fresh_member = "cc".repeat(32);
1256 let tracked_member = "dd".repeat(32);
1257 let groot_v11 = groot_event(&owner, "Tracked", "held-at-v5", 11, Some(&[0xABu8; 32]), 200);
1259 let (fresh_grant, _, _) = grant_event(&owner, &fresh_member, vec![role_id.clone()], 2, Some(&[0x99u8; 32]), 110);
1261 let (tracked_grant, _, _) = grant_event(&owner, &tracked_member, vec![role_id.clone()], 11, Some(&[0x88u8; 32]), 120);
1263 let role = role_event(&owner, &role_id, 1, 1, None, 100); let cid_hex = crate::simd::hex::bytes_to_hex_32(&cid().0);
1266 let tracked_grant_hex = crate::simd::hex::bytes_to_hex_32(
1267 &crate::community::derive::grant_locator(&cid(), &crate::simd::hex::hex_to_bytes_32(&tracked_member)));
1268 let mut floors = std::collections::HashMap::new();
1269 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);
1272
1273 assert!(folded.root_meta.is_some(), "tracking GroupRoot with a gapped tail is SURFACED (display exemption)");
1274 assert!(folded.gapped_entities.contains(&cid().0), "and still flagged pending for a refetch");
1275 assert_eq!(folded.roles.grants.len(), 1, "only the bootstrapping grant surfaces; the tracked-gapped grant stays quarantined");
1276 }
1277
1278 #[test]
1281 fn empty_grant_revoke_advances_head_but_carries_no_entry() {
1282 let owner = Keys::generate();
1283 let role_id = "a".repeat(64);
1284 let member = "cc".repeat(32);
1285 let (g1, eid, c1) = grant_event(&owner, &member, vec![role_id], 1, None, 100);
1286 let v1_hash = version::edition_hash(&eid, 1, None, c1.as_bytes());
1287 let (g2, _, _) = grant_event(&owner, &member, vec![], 2, Some(&v1_hash), 101); let folded = fold_roster(&[g1, g2], &cid(), &Default::default());
1289 assert!(folded.roles.grants.is_empty(), "a revoke leaves no roster entry");
1290 assert!(
1291 folded.heads.iter().any(|h| h.entity_hex == crate::simd::hex::bytes_to_hex_32(&eid) && h.version == 2),
1292 "but the revoke still advances the head (so a replayed v1 can't re-add the role)"
1293 );
1294 }
1295
1296 #[test]
1299 fn fold_roster_skips_junk_and_still_folds_the_valid() {
1300 let owner = Keys::generate();
1301 let owner_hex = owner.public_key().to_hex();
1302 let role_id = "a".repeat(64);
1303 let good = role_event(&owner, &role_id, 1, 1, None, 100);
1304 let garbage = edition::build_edition_inner(
1306 owner.public_key(), VSK_ROLE, &crate::simd::hex::hex_to_bytes_32(&"b".repeat(64)),
1307 1, None, "not json at all", 100, None,
1308 ).sign_with_keys(&owner).unwrap();
1309 let role_b = Role { role_id: "cc".repeat(64), name: "X".into(), position: 1, permissions: Permissions::admin(), scope: RoleScope::Server, color: 0 };
1311 let mismatched = edition::build_edition_inner(
1312 owner.public_key(), VSK_ROLE, &crate::simd::hex::hex_to_bytes_32(&"dd".repeat(64)),
1313 1, None, &serde_json::to_string(&role_b).unwrap(), 100, None,
1314 ).sign_with_keys(&owner).unwrap();
1315
1316 let folded = fold_roster(&[good, garbage, mismatched], &cid(), &Default::default());
1317 assert!(folded.roles.role(&role_id).is_some(), "the valid role still folds through the junk");
1318 assert!(folded.skipped >= 2, "both junk editions are skipped, not folded (skipped={})", folded.skipped);
1319 assert_eq!(authorize_delegation(&folded, Some(&owner_hex)).roles.len(), 1, "only the valid role authorizes");
1320 }
1321
1322 #[test]
1325 fn role_content_must_bind_to_its_entity_id() {
1326 let owner = Keys::generate();
1327 let role = Role {
1328 role_id: "a".repeat(64), name: "X".into(),
1330 position: 0,
1331 permissions: Permissions::admin(),
1332 scope: RoleScope::Server,
1333 color: 0,
1334 };
1335 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)
1337 .sign_with_keys(&owner)
1338 .unwrap();
1339 let folded = fold_roster(&[ev], &cid(), &Default::default());
1340 assert!(folded.roles.roles.is_empty(), "entity_id != role_id → rejected");
1341 assert_eq!(folded.skipped, 1);
1342 }
1343
1344 #[test]
1347 fn grant_at_wrong_locator_is_rejected() {
1348 let owner = Keys::generate();
1349 let member = "dd".repeat(32);
1350 let g = roles::MemberGrant { member: member.clone(), role_ids: vec!["a".repeat(64)] };
1351 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)
1353 .sign_with_keys(&owner)
1354 .unwrap();
1355 let folded = fold_roster(&[ev], &cid(), &Default::default());
1356 assert!(!folded.roles.is_admin(&member), "a grant at the wrong locator does not take effect");
1357 assert_eq!(folded.skipped, 1);
1358 }
1359
1360 #[test]
1362 fn fold_is_order_independent() {
1363 let owner = Keys::generate();
1364 let role_id = "a".repeat(64);
1365 let member = "bb".repeat(32);
1366 let role_ev = role_event(&owner, &role_id, 1, 1, None, 100);
1367 let (grant_ev, _, _) = grant_event(&owner, &member, vec![role_id.clone()], 1, None, 101);
1368 let a = fold_roster(&[role_ev.clone(), grant_ev.clone()], &cid(), &Default::default());
1369 let b = fold_roster(&[grant_ev, role_ev], &cid(), &Default::default());
1370 assert_eq!(a.roles.roles.len(), b.roles.roles.len());
1371 assert!(a.roles.is_admin(&member) && b.roles.is_admin(&member));
1372 }
1373
1374 #[test]
1377 fn public_builders_round_trip_through_fold() {
1378 let owner = Keys::generate();
1379 let role = Role {
1380 role_id: "a".repeat(64),
1381 name: "Admin".into(),
1382 position: 1,
1383 permissions: Permissions::admin(),
1384 scope: RoleScope::Server,
1385 color: 0,
1386 };
1387 let member = "bb".repeat(32);
1388 let grant = roles::MemberGrant { member: member.clone(), role_ids: vec![role.role_id.clone()] };
1389
1390 let role_ev = build_role_edition(&owner, &role, 1, None, 100, None).unwrap();
1391 let grant_ev = build_grant_edition(&owner, &cid(), &grant, 1, None, 101, None).unwrap();
1392
1393 let folded = fold_roster(&[role_ev, grant_ev], &cid(), &Default::default());
1394 assert_eq!(folded.skipped, 0, "builders emit bound, anchored editions the fold accepts");
1395 assert!(folded.gapped_entities.is_empty());
1396 assert!(folded.roles.is_admin(&member));
1397 assert_eq!(folded.roles.role(&role.role_id).unwrap().position, 1);
1398 }
1399
1400 #[test]
1402 fn builders_reject_malformed_chain_shape() {
1403 let owner = Keys::generate();
1404 let role = Role {
1405 role_id: "a".repeat(64), name: "Admin".into(), position: 1,
1406 permissions: Permissions::admin(), scope: RoleScope::Server, color: 0,
1407 };
1408 assert!(build_role_edition(&owner, &role, 1, Some(&[0u8; 32]), 100, None).is_err());
1410 assert!(build_role_edition(&owner, &role, 5, None, 100, None).is_err());
1411 let grant = roles::MemberGrant { member: "bb".repeat(32), role_ids: vec![role.role_id.clone()] };
1412 assert!(build_grant_edition(&owner, &cid(), &grant, 1, Some(&[0u8; 32]), 100, None).is_err());
1413 assert!(build_grant_edition(&owner, &cid(), &grant, 2, None, 100, None).is_err());
1414 }
1415
1416 #[test]
1418 fn builders_reject_bad_hex() {
1419 let owner = Keys::generate();
1420 let bad_role = Role {
1421 role_id: "not-hex".into(), name: "X".into(), position: 1,
1422 permissions: Permissions::admin(), scope: RoleScope::Server, color: 0,
1423 };
1424 assert!(build_role_edition(&owner, &bad_role, 1, None, 100, None).is_err());
1425 let bad_grant = roles::MemberGrant { member: "zz".repeat(32), role_ids: vec!["a".repeat(64)] };
1426 assert!(build_grant_edition(&owner, &cid(), &bad_grant, 1, None, 100, None).is_err());
1427 }
1428
1429 #[test]
1433 fn producer_built_chain_folds_to_latest() {
1434 let owner = Keys::generate();
1435 let member = "cc".repeat(32);
1436 let role_a = "a".repeat(64);
1437 let role_b = "b".repeat(64);
1438
1439 let g1 = roles::MemberGrant { member: member.clone(), role_ids: vec![role_a] };
1440 let e1 = build_grant_edition(&owner, &cid(), &g1, 1, None, 100, None).unwrap();
1441 let member_bytes = crate::simd::hex::hex_to_bytes_32(&member);
1443 let eid = crate::community::derive::grant_locator(&cid(), &member_bytes);
1444 let v1_hash = version::edition_hash(&eid, 1, None, serde_json::to_string(&g1).unwrap().as_bytes());
1445
1446 let g2 = roles::MemberGrant { member: member.clone(), role_ids: vec![role_b.clone()] };
1447 let e2 = build_grant_edition(&owner, &cid(), &g2, 2, Some(&v1_hash), 101, None).unwrap();
1448
1449 let folded = fold_roster(&[e1, e2], &cid(), &Default::default());
1450 assert_eq!(folded.skipped, 0);
1451 assert!(folded.gapped_entities.is_empty(), "v2 links to v1 — no gap");
1452 let held: Vec<&String> = folded.roles.grants.iter().flat_map(|g| &g.role_ids).collect();
1453 assert_eq!(held, vec![&role_b]);
1454 }
1455
1456 #[test]
1458 fn control_edition_seals_opens_and_folds_end_to_end() {
1459 let owner = Keys::generate();
1460 let community_id = CommunityId([0x09; 32]);
1461 let epoch = Epoch(0);
1462 let role = Role {
1463 role_id: "a".repeat(64), name: "Admin".into(), position: 1,
1464 permissions: Permissions::admin(), scope: RoleScope::Server, color: 0,
1465 };
1466
1467 let inner = build_role_edition(&owner, &role, 1, None, 100, None).unwrap();
1468 let outer = seal_control_edition(&Keys::generate(), &inner, &sr(), &community_id, epoch).unwrap();
1469
1470 assert_ne!(outer.pubkey, owner.public_key(), "outer is ephemeral-signed");
1472 assert!(!outer.content.contains(&role.role_id), "inner content is encrypted on the wire");
1473
1474 assert!(open_control_edition(&outer, &ServerRootKey([0xAA; 32])).is_err());
1476
1477 let reopened = open_control_edition(&outer, &sr()).unwrap();
1479 assert_eq!(edition::parse_edition_inner(&reopened).unwrap().author, owner.public_key());
1480 let folded = fold_roster(&[reopened], &cid(), &Default::default());
1481 assert_eq!(folded.skipped, 0);
1482 assert!(folded.roles.role(&role.role_id).is_some(), "build→seal→open→parse→fold round-trips");
1483 }
1484
1485 #[test]
1488 fn control_edition_round_trips_at_a_nonzero_epoch() {
1489 let owner = Keys::generate();
1490 let community_id = CommunityId([0x09; 32]);
1491 let role = Role {
1492 role_id: "a".repeat(64), name: "Admin".into(), position: 1,
1493 permissions: Permissions::admin(), scope: RoleScope::Server, color: 0,
1494 };
1495 let inner = build_role_edition(&owner, &role, 1, None, 100, None).unwrap();
1496 let outer = seal_control_edition(&Keys::generate(), &inner, &sr(), &community_id, Epoch(4)).unwrap();
1497
1498 let z4 = control_pseudonym(&sr(), &community_id, Epoch(4));
1500 assert_ne!(z4, control_pseudonym(&sr(), &community_id, Epoch(0)), "epochs address distinct pseudonyms");
1501 let z_tag = outer.tags.iter().find_map(|t| {
1502 let s = t.as_slice();
1503 (s.len() >= 2 && s[0] == "z").then(|| s[1].clone())
1504 }).expect("control outer carries a z tag");
1505 assert_eq!(z_tag, z4, "sealed at the epoch-4 pseudonym, NOT epoch 0 (regression #1 guard)");
1506
1507 let reopened = open_control_edition(&outer, &sr()).unwrap();
1509 let folded = fold_roster(&[reopened], &cid(), &Default::default());
1510 assert!(folded.roles.role(&role.role_id).is_some(), "seal→open→fold round-trips at a non-zero epoch");
1511 }
1512
1513 #[test]
1516 fn control_pseudonym_golden_vector() {
1517 let server_root = ServerRootKey([0x07; 32]);
1518 let community_id = CommunityId([0x09; 32]);
1519 assert_eq!(
1520 control_pseudonym(&server_root, &community_id, Epoch(0)),
1521 "e719f2d29ca005dfe805b1f85f696948394661c748e1f98b2df7d396260f6378"
1522 );
1523 }
1524
1525 #[test]
1527 fn open_rejects_non_control_outer() {
1528 let bogus = EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_MESSAGE), "x")
1529 .sign_with_keys(&Keys::generate())
1530 .unwrap();
1531 assert!(open_control_edition(&bogus, &sr()).is_err());
1532 }
1533
1534 #[test]
1537 fn delegation_rejects_a_self_signed_admin_grant() {
1538 let owner = Keys::generate();
1541 let mallory = Keys::generate();
1542 let admin = "a".repeat(64);
1543 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);
1545 let folded = fold_roster(&[role_ev, self_grant], &cid(), &Default::default());
1546 assert!(folded.roles.grants.iter().any(|g| g.member == mallory.public_key().to_hex()));
1548
1549 let authorized = authorize_delegation(&folded, Some(&owner.public_key().to_hex()));
1550 assert!(authorized.roles.iter().any(|r| r.role_id == admin), "owner-signed role stays");
1551 assert!(
1552 !authorized.grants.iter().any(|g| g.member == mallory.public_key().to_hex()),
1553 "self-signed Admin grant is REJECTED — no self-promotion"
1554 );
1555 }
1556
1557 #[test]
1558 fn delegation_accepts_owner_signed_role_and_grant() {
1559 let owner = Keys::generate();
1560 let member = Keys::generate();
1561 let admin = "a".repeat(64);
1562 let role_ev = role_event(&owner, &admin, 1, 1, None, 100);
1563 let (grant_ev, _, _) = grant_event(&owner, &member.public_key().to_hex(), vec![admin.clone()], 1, None, 101);
1564 let authorized = authorize_delegation(&fold_roster(&[role_ev, grant_ev], &cid(), &Default::default()), Some(&owner.public_key().to_hex()));
1565 assert!(authorized.roles.iter().any(|r| r.role_id == admin));
1566 assert!(authorized.grants.iter().any(|g| g.member == member.public_key().to_hex()));
1567 }
1568
1569 #[test]
1570 fn delegation_chains_owner_to_admin_to_mod() {
1571 let owner = Keys::generate();
1573 let alice = Keys::generate();
1574 let bob = Keys::generate();
1575 let (admin, moderator) = ("a".repeat(64), "b".repeat(64));
1576 let r_admin = role_event(&owner, &admin, 1, 1, None, 100);
1577 let (g_alice, _, _) = grant_event(&owner, &alice.public_key().to_hex(), vec![admin.clone()], 1, None, 101);
1578 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);
1580 let authorized = authorize_delegation(
1581 &fold_roster(&[r_admin, g_alice, r_mod, g_bob], &cid(), &Default::default()),
1582 Some(&owner.public_key().to_hex()),
1583 );
1584 assert!(authorized.roles.iter().any(|r| r.role_id == moderator), "admin Alice could create Mod");
1585 assert!(authorized.grants.iter().any(|g| g.member == alice.public_key().to_hex()), "owner→Alice Admin");
1586 assert!(authorized.grants.iter().any(|g| g.member == bob.public_key().to_hex()), "Alice→Bob Mod (delegated)");
1587 }
1588
1589 #[test]
1590 fn delegation_demoted_admins_delegated_grant_is_dropped() {
1591 let owner = Keys::generate();
1596 let alice = Keys::generate();
1597 let bob = Keys::generate();
1598 let (admin, moderator) = ("a".repeat(64), "b".repeat(64));
1599 let r_admin = role_event(&owner, &admin, 1, 1, None, 100);
1600 let (g_alice1, a_eid, a_c1) = grant_event(&owner, &alice.public_key().to_hex(), vec![admin.clone()], 1, None, 101);
1601 let a_v1 = version::edition_hash(&a_eid, 1, None, a_c1.as_bytes());
1602 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);
1604 let (g_bob, _, _) = grant_event(&alice, &bob.public_key().to_hex(), vec![moderator.clone()], 1, None, 104);
1605
1606 let folded = fold_roster(&[r_admin, g_alice1, g_alice2, r_mod, g_bob], &cid(), &Default::default());
1607 let authorized = authorize_delegation(&folded, Some(&owner.public_key().to_hex()));
1608 assert!(!authorized.grants.iter().any(|g| g.member == alice.public_key().to_hex()), "Alice's admin grant is revoked");
1609 assert!(
1610 !authorized.grants.iter().any(|g| g.member == bob.public_key().to_hex()),
1611 "a since-demoted admin's delegated grant is dropped — version-pinning falls out, no citation needed"
1612 );
1613 }
1614
1615 #[test]
1616 fn delegation_floor_blocks_a_rolled_back_admin_grant() {
1617 let owner = Keys::generate();
1622 let alice = Keys::generate();
1623 let bob = Keys::generate();
1624 let (admin, moderator) = ("a".repeat(64), "b".repeat(64));
1625 let r_admin = role_event(&owner, &admin, 1, 1, None, 100);
1626 let (g_alice1, a_eid, _) = grant_event(&owner, &alice.public_key().to_hex(), vec![admin.clone()], 1, None, 101);
1627 let r_mod = role_event(&alice, &moderator, 3, 1, None, 102);
1628 let (g_bob, _, _) = grant_event(&alice, &bob.public_key().to_hex(), vec![moderator.clone()], 1, None, 103);
1629
1630 let mut floors = std::collections::HashMap::new();
1631 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);
1633 let authorized = authorize_delegation(&folded, Some(&owner.public_key().to_hex()));
1634 assert!(!authorized.grants.iter().any(|g| g.member == alice.public_key().to_hex()), "rolled-back Alice grant refused");
1635 assert!(
1636 !authorized.grants.iter().any(|g| g.member == bob.public_key().to_hex()),
1637 "Bob's delegated grant dropped — the floor blocks the rollback that would re-authorize Alice"
1638 );
1639 }
1640
1641 #[test]
1642 fn delegation_admin_cannot_grant_a_peer_admin() {
1643 let owner = Keys::generate();
1645 let alice = Keys::generate();
1646 let bob = Keys::generate();
1647 let admin = "a".repeat(64);
1648 let r_admin = role_event(&owner, &admin, 1, 1, None, 100);
1649 let (g_alice, _, _) = grant_event(&owner, &alice.public_key().to_hex(), vec![admin.clone()], 1, None, 101);
1650 let (g_bob, _, _) = grant_event(&alice, &bob.public_key().to_hex(), vec![admin.clone()], 1, None, 102);
1651 let authorized = authorize_delegation(
1652 &fold_roster(&[r_admin, g_alice, g_bob], &cid(), &Default::default()),
1653 Some(&owner.public_key().to_hex()),
1654 );
1655 assert!(authorized.grants.iter().any(|g| g.member == alice.public_key().to_hex()), "owner→Alice ok");
1656 assert!(
1657 !authorized.grants.iter().any(|g| g.member == bob.public_key().to_hex()),
1658 "an admin cannot grant a PEER-rank Admin"
1659 );
1660 }
1661
1662 #[test]
1663 fn delegation_rejects_a_circular_grant_with_no_owner_root() {
1664 let owner = Keys::generate(); let a = Keys::generate();
1668 let b = Keys::generate();
1669 let admin = "a".repeat(64);
1670 let r_admin = role_event(&owner, &admin, 1, 1, None, 100);
1671 let (g_ab, _, _) = grant_event(&a, &b.public_key().to_hex(), vec![admin.clone()], 1, None, 101);
1672 let (g_ba, _, _) = grant_event(&b, &a.public_key().to_hex(), vec![admin.clone()], 1, None, 102);
1673 let authorized = authorize_delegation(
1674 &fold_roster(&[r_admin, g_ab, g_ba], &cid(), &Default::default()),
1675 Some(&owner.public_key().to_hex()),
1676 );
1677 assert!(authorized.roles.iter().any(|r| r.role_id == admin), "owner's role stays");
1678 assert!(authorized.grants.is_empty(), "a circular mutual-admin grant cannot bootstrap without the owner");
1679 }
1680
1681 #[test]
1682 fn delegation_rejects_a_position_0_role_even_owner_signed() {
1683 let owner = Keys::generate();
1686 let mallory = Keys::generate();
1687 let by_mallory = authorize_delegation(
1688 &fold_roster(&[role_event(&mallory, &"e".repeat(64), 0, 1, None, 100)], &cid(), &Default::default()),
1689 Some(&owner.public_key().to_hex()),
1690 );
1691 assert!(by_mallory.roles.is_empty(), "a non-owner cannot mint a position-0 role");
1692 let by_owner = authorize_delegation(
1693 &fold_roster(&[role_event(&owner, &"f".repeat(64), 0, 1, None, 100)], &cid(), &Default::default()),
1694 Some(&owner.public_key().to_hex()),
1695 );
1696 assert!(by_owner.roles.is_empty(), "position 0 is the attestation's — even the owner can't mint a pos-0 role");
1697 }
1698
1699 #[test]
1700 fn delegation_unproven_community_yields_empty_roster() {
1701 let owner = Keys::generate();
1703 let role_ev = role_event(&owner, &"a".repeat(64), 1, 1, None, 100);
1704 let authorized = authorize_delegation(&fold_roster(&[role_ev], &cid(), &Default::default()), None);
1705 assert!(authorized.roles.is_empty() && authorized.grants.is_empty(), "no root ⇒ empty authorized roster");
1706 }
1707}