use super::derive::channel_pseudonym;
use super::{cipher, edition, roles, version, ChannelId, ChannelKey, CommunityId, Epoch, ServerRootKey};
use crate::stored_event::event_kind;
use nostr_sdk::prelude::*;
use std::collections::HashMap;
const VSK_COMMUNITY_ROOT: &str = "0";
const VSK_ROLE: &str = "1";
const VSK_CHANNEL: &str = "2";
const VSK_GRANT: &str = "3";
const VSK_BANLIST: &str = "4";
const VSK_INVITE_LINKS: &str = "8";
const VSK_DISSOLVED: &str = "10";
pub(crate) const MAX_CONTROL_EDITIONS: usize = 50_000;
fn hex32(s: &str) -> Option<[u8; 32]> {
(s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit()))
.then(|| crate::simd::hex::hex_to_bytes_32(s))
}
fn check_chain_shape(version: u64, prev_hash: Option<&[u8; 32]>) -> Result<(), String> {
match (version, prev_hash) {
(0, _) => Err("edition version starts at 1".to_string()),
(1, Some(_)) => Err("genesis edition (v1) must have no prev_hash".to_string()),
(v, None) if v > 1 => Err("continuation edition (v>1) requires a prev_hash".to_string()),
_ => Ok(()),
}
}
pub fn build_role_edition(
actor: &Keys,
role: &roles::Role,
version: u64,
prev_hash: Option<&[u8; 32]>,
created_at: u64,
authority: Option<&edition::AuthorityCitation>,
) -> Result<Event, String> {
build_role_edition_unsigned(actor.public_key(), role, version, prev_hash, created_at, authority)?
.sign_with_keys(actor)
.map_err(|e| format!("sign role edition: {e}"))
}
pub fn build_role_edition_unsigned(
author: PublicKey,
role: &roles::Role,
version: u64,
prev_hash: Option<&[u8; 32]>,
created_at: u64,
authority: Option<&edition::AuthorityCitation>,
) -> Result<UnsignedEvent, String> {
check_chain_shape(version, prev_hash)?;
let entity_id = hex32(&role.role_id).ok_or("role_id must be 64-char hex")?;
let content = serde_json::to_string(role).map_err(|e| e.to_string())?;
Ok(edition::build_edition_inner(author, VSK_ROLE, &entity_id, version, prev_hash, &content, created_at, authority))
}
pub fn build_grant_edition(
actor: &Keys,
community_id: &CommunityId,
grant: &roles::MemberGrant,
version: u64,
prev_hash: Option<&[u8; 32]>,
created_at: u64,
authority: Option<&edition::AuthorityCitation>,
) -> Result<Event, String> {
build_grant_edition_unsigned(actor.public_key(), community_id, grant, version, prev_hash, created_at, authority)?
.sign_with_keys(actor)
.map_err(|e| format!("sign grant edition: {e}"))
}
pub fn build_grant_edition_unsigned(
author: PublicKey,
community_id: &CommunityId,
grant: &roles::MemberGrant,
version: u64,
prev_hash: Option<&[u8; 32]>,
created_at: u64,
authority: Option<&edition::AuthorityCitation>,
) -> Result<UnsignedEvent, String> {
check_chain_shape(version, prev_hash)?;
let member_bytes = hex32(&grant.member).ok_or("member must be 64-char hex")?;
let entity_id = super::derive::grant_locator(community_id, &member_bytes);
let content = serde_json::to_string(grant).map_err(|e| e.to_string())?;
Ok(edition::build_edition_inner(author, VSK_GRANT, &entity_id, version, prev_hash, &content, created_at, authority))
}
pub fn build_banlist_edition(
actor: &Keys,
community_id: &CommunityId,
banned: &[String],
version: u64,
prev_hash: Option<&[u8; 32]>,
created_at: u64,
authority: Option<&edition::AuthorityCitation>,
) -> Result<Event, String> {
build_banlist_edition_unsigned(actor.public_key(), community_id, banned, version, prev_hash, created_at, authority)?
.sign_with_keys(actor)
.map_err(|e| format!("sign banlist edition: {e}"))
}
pub fn build_banlist_edition_unsigned(
author: PublicKey,
community_id: &CommunityId,
banned: &[String],
version: u64,
prev_hash: Option<&[u8; 32]>,
created_at: u64,
authority: Option<&edition::AuthorityCitation>,
) -> Result<UnsignedEvent, String> {
check_chain_shape(version, prev_hash)?;
let entity_id = super::derive::banlist_locator(community_id);
let content = serde_json::to_string(banned).map_err(|e| e.to_string())?;
Ok(edition::build_edition_inner(author, VSK_BANLIST, &entity_id, version, prev_hash, &content, created_at, authority))
}
pub fn build_group_dissolved_edition(
actor: &Keys,
community_id: &CommunityId,
created_at: u64,
) -> Result<Event, String> {
build_group_dissolved_edition_unsigned(actor.public_key(), community_id, created_at)
.sign_with_keys(actor)
.map_err(|e| format!("sign dissolved edition: {e}"))
}
pub fn build_group_dissolved_edition_unsigned(
author: PublicKey,
community_id: &CommunityId,
created_at: u64,
) -> UnsignedEvent {
let entity_id = super::derive::dissolved_locator(community_id);
edition::build_edition_inner(author, VSK_DISSOLVED, &entity_id, 1, None, "{}", created_at, None)
}
pub fn build_invite_links_edition(
actor: &Keys,
community_id: &CommunityId,
link_locators: &[String],
version: u64,
prev_hash: Option<&[u8; 32]>,
created_at: u64,
authority: Option<&edition::AuthorityCitation>,
) -> Result<Event, String> {
build_invite_links_edition_unsigned(actor.public_key(), community_id, link_locators, version, prev_hash, created_at, authority)?
.sign_with_keys(actor)
.map_err(|e| format!("sign invite-links edition: {e}"))
}
pub fn build_invite_links_edition_unsigned(
author: PublicKey,
community_id: &CommunityId,
link_locators: &[String],
version: u64,
prev_hash: Option<&[u8; 32]>,
created_at: u64,
authority: Option<&edition::AuthorityCitation>,
) -> Result<UnsignedEvent, String> {
check_chain_shape(version, prev_hash)?;
let entity_id = super::derive::invite_links_locator(community_id, &author.to_bytes());
let content = serde_json::to_string(link_locators).map_err(|e| e.to_string())?;
Ok(edition::build_edition_inner(author, VSK_INVITE_LINKS, &entity_id, version, prev_hash, &content, created_at, authority))
}
pub fn build_community_root_edition(
actor: &Keys,
community_id: &CommunityId,
meta: &super::metadata::CommunityMetadata,
version: u64,
prev_hash: Option<&[u8; 32]>,
created_at: u64,
authority: Option<&edition::AuthorityCitation>,
) -> Result<Event, String> {
build_community_root_edition_unsigned(actor.public_key(), community_id, meta, version, prev_hash, created_at, authority)?
.sign_with_keys(actor)
.map_err(|e| format!("sign community-root edition: {e}"))
}
pub fn build_community_root_edition_unsigned(
author: PublicKey,
community_id: &CommunityId,
meta: &super::metadata::CommunityMetadata,
version: u64,
prev_hash: Option<&[u8; 32]>,
created_at: u64,
authority: Option<&edition::AuthorityCitation>,
) -> Result<UnsignedEvent, String> {
check_chain_shape(version, prev_hash)?;
let content = serde_json::to_string(meta).map_err(|e| e.to_string())?;
Ok(edition::build_edition_inner(author, VSK_COMMUNITY_ROOT, &community_id.0, version, prev_hash, &content, created_at, authority))
}
pub fn build_channel_metadata_edition(
actor: &Keys,
channel_id: &ChannelId,
meta: &super::metadata::ChannelMetadata,
version: u64,
prev_hash: Option<&[u8; 32]>,
created_at: u64,
authority: Option<&edition::AuthorityCitation>,
) -> Result<Event, String> {
build_channel_metadata_edition_unsigned(actor.public_key(), channel_id, meta, version, prev_hash, created_at, authority)?
.sign_with_keys(actor)
.map_err(|e| format!("sign channel-metadata edition: {e}"))
}
pub fn build_channel_metadata_edition_unsigned(
author: PublicKey,
channel_id: &ChannelId,
meta: &super::metadata::ChannelMetadata,
version: u64,
prev_hash: Option<&[u8; 32]>,
created_at: u64,
authority: Option<&edition::AuthorityCitation>,
) -> Result<UnsignedEvent, String> {
check_chain_shape(version, prev_hash)?;
let content = serde_json::to_string(meta).map_err(|e| e.to_string())?;
Ok(edition::build_edition_inner(author, VSK_CHANNEL, &channel_id.0, version, prev_hash, &content, created_at, authority))
}
pub fn control_pseudonym(server_root: &ServerRootKey, community_id: &CommunityId, epoch: Epoch) -> String {
channel_pseudonym(&ChannelKey(*server_root.as_bytes()), &ChannelId(community_id.0), epoch).to_hex()
}
pub fn seal_control_edition(
ephemeral: &Keys,
inner: &Event,
server_root: &ServerRootKey,
community_id: &CommunityId,
epoch: Epoch,
) -> Result<Event, String> {
if inner.kind.as_u16() != event_kind::COMMUNITY_CONTROL {
return Err("a control edition must be kind 3308".to_string());
}
let content = cipher::seal(server_root.as_bytes(), inner.as_json().as_bytes())
.map_err(|e| format!("seal control edition: {e}"))?;
let pseudonym = control_pseudonym(server_root, community_id, epoch);
EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_CONTROL), content)
.tags([
Tag::custom(TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::Z)), [pseudonym]),
Tag::custom(TagKind::Custom("v".into()), ["1".to_string()]),
])
.sign_with_keys(ephemeral)
.map_err(|e| format!("sign control outer: {e}"))
}
pub fn open_control_edition(outer: &Event, server_root: &ServerRootKey) -> Result<Event, String> {
if outer.kind.as_u16() != event_kind::COMMUNITY_CONTROL {
return Err("not a control-plane outer (kind != 3308)".to_string());
}
match outer.tags.iter().find_map(|t| {
let s = t.as_slice();
(s.len() >= 2 && s[0] == "v").then(|| s[1].clone())
}) {
Some(v) if v == "1" => {}
other => return Err(format!("unsupported control edition version: {other:?}")),
}
let plaintext = cipher::open(server_root.as_bytes(), &outer.content)
.map_err(|e| format!("open control edition: {e}"))?;
let json = String::from_utf8(plaintext).map_err(|e| format!("control inner utf8: {e}"))?;
let inner = Event::from_json(&json).map_err(|e| format!("control inner parse: {e}"))?;
if inner.kind.as_u16() != event_kind::COMMUNITY_CONTROL {
return Err("control inner is not kind 3308".to_string());
}
Ok(inner)
}
pub fn seal_dissolved_edition(ephemeral: &Keys, inner: &Event, community_id: &CommunityId) -> Result<Event, String> {
if inner.kind.as_u16() != event_kind::COMMUNITY_CONTROL {
return Err("a dissolved tombstone must be kind 3308".to_string());
}
let key = super::derive::dissolved_envelope_key(community_id);
let content = cipher::seal(&key, inner.as_json().as_bytes())
.map_err(|e| format!("seal dissolved edition: {e}"))?;
let pseudonym = super::derive::dissolved_pseudonym(community_id);
EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_CONTROL), content)
.tags([
Tag::custom(TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::Z)), [pseudonym]),
Tag::custom(TagKind::Custom("v".into()), ["1".to_string()]),
])
.sign_with_keys(ephemeral)
.map_err(|e| format!("sign dissolved outer: {e}"))
}
pub fn dissolved_tombstone_signer(outer: &Event, community_id: &CommunityId) -> Option<PublicKey> {
if outer.kind.as_u16() != event_kind::COMMUNITY_CONTROL {
return None;
}
let key = super::derive::dissolved_envelope_key(community_id);
let plaintext = cipher::open(&key, &outer.content).ok()?;
let inner = Event::from_json(&String::from_utf8(plaintext).ok()?).ok()?;
let p = edition::parse_edition_inner(&inner).ok()?;
(p.vsk == VSK_DISSOLVED && p.entity_id == super::derive::dissolved_locator(community_id)).then_some(p.author)
}
#[derive(Clone)]
pub struct EntityHead {
pub entity_hex: String,
pub version: u64,
pub self_hash: [u8; 32],
pub inner_id: [u8; 32],
pub citation: Option<edition::AuthorityCitation>,
}
#[derive(Clone)]
pub struct FoldedRoster {
pub roles: roles::CommunityRoles,
pub role_authors: Vec<PublicKey>,
pub grant_authors: Vec<PublicKey>,
pub gapped_entities: Vec<[u8; 32]>,
pub skipped: usize,
pub fetched: usize,
pub heads: Vec<EntityHead>,
pub banned: Vec<String>,
pub banlist_author: Option<PublicKey>,
pub dissolved_by: Vec<PublicKey>,
pub banlist_head: Option<EntityHead>,
pub invite_link_sets: Vec<InviteLinkSet>,
pub root_meta: Option<super::metadata::CommunityMetadata>,
pub root_author: Option<PublicKey>,
pub root_head: Option<EntityHead>,
pub root_candidates: Vec<RootCandidate>,
pub channel_meta: Vec<ChannelMetaHead>,
pub channel_candidates: Vec<ChannelMetaHead>,
}
#[derive(Clone)]
pub struct InviteLinkSet {
pub creator: PublicKey,
pub locators: Vec<String>,
pub head: EntityHead,
}
#[derive(Clone)]
pub struct RootCandidate {
pub meta: super::metadata::CommunityMetadata,
pub author: PublicKey,
pub head: EntityHead,
}
#[derive(Clone)]
pub struct ChannelMetaHead {
pub channel_id: [u8; 32],
pub meta: super::metadata::ChannelMetadata,
pub author: PublicKey,
pub head: EntityHead,
}
pub fn fold_roster(
inner_editions: &[Event],
community_id: &CommunityId,
floors: &HashMap<String, (u64, [u8; 32])>,
) -> FoldedRoster {
let mut skipped = inner_editions.len().saturating_sub(MAX_CONTROL_EDITIONS);
let parsed: Vec<edition::ParsedEdition> = inner_editions
.iter()
.take(MAX_CONTROL_EDITIONS)
.filter_map(|e| match edition::parse_edition_inner(e) {
Ok(p) => Some(p),
Err(_) => {
skipped += 1;
None
}
})
.collect();
let dissolved_eid = super::derive::dissolved_locator(community_id);
let mut dissolved_by: Vec<PublicKey> = Vec::new();
for p in &parsed {
if p.vsk == VSK_DISSOLVED && p.entity_id == dissolved_eid && !dissolved_by.contains(&p.author) {
dissolved_by.push(p.author);
}
}
let mut by_entity: HashMap<[u8; 32], Vec<&edition::ParsedEdition>> = HashMap::new();
for p in &parsed {
if p.vsk == VSK_DISSOLVED {
continue;
}
by_entity.entry(p.entity_id).or_default().push(p);
}
let mut out = roles::CommunityRoles::default();
let mut role_authors: Vec<PublicKey> = Vec::new();
let mut grant_authors: Vec<PublicKey> = Vec::new();
let mut gapped_entities = Vec::new();
let mut heads: Vec<EntityHead> = Vec::new();
let mut banned: Vec<String> = Vec::new();
let mut banlist_author: Option<PublicKey> = None;
let mut banlist_head: Option<EntityHead> = None;
let banlist_eid = super::derive::banlist_locator(community_id);
let mut invite_link_sets: Vec<InviteLinkSet> = Vec::new();
let mut root_meta: Option<super::metadata::CommunityMetadata> = None;
let mut root_author: Option<PublicKey> = None;
let mut root_head: Option<EntityHead> = None;
let mut root_candidates: Vec<RootCandidate> = Vec::new();
let mut channel_meta: Vec<ChannelMetaHead> = Vec::new();
let mut channel_candidates: Vec<ChannelMetaHead> = Vec::new();
for (entity_id, editions) in by_entity {
let fold_eds: Vec<version::Edition> = editions.iter().map(|p| p.to_fold_edition()).collect();
let entity_hex = crate::simd::hex::bytes_to_hex_32(&entity_id);
let floor = floors.get(&entity_hex);
let floor_v = floor.map(|(v, _)| *v).unwrap_or(0);
let result = version::fold(&fold_eds, floor_v, floor.map(|(_, h)| h));
let head_idx = if result.gap {
gapped_entities.push(entity_id);
if floor_v == 0 {
match version::bootstrap_head(&fold_eds, 0) { Some(i) => i, None => continue }
} else {
match version::bootstrap_head(&fold_eds, floor_v) {
Some(i) if matches!(editions[i].vsk.as_str(), VSK_COMMUNITY_ROOT | VSK_CHANNEL) => i,
_ => continue, }
}
} else {
match result.head { Some(i) => i, None => continue }
};
let head = editions[head_idx];
let mut record_head = || {
heads.push(EntityHead {
entity_hex: crate::simd::hex::bytes_to_hex_32(&entity_id),
version: head.version,
self_hash: head.self_hash,
inner_id: head.inner_id,
citation: head.authority.clone(),
});
};
match head.vsk.as_str() {
VSK_ROLE => match serde_json::from_str::<roles::Role>(&head.content) {
Ok(role) if hex32(&role.role_id) == Some(entity_id) => {
record_head();
role_authors.push(head.author);
out.roles.push(role);
}
_ => skipped += 1,
},
VSK_GRANT => match serde_json::from_str::<roles::MemberGrant>(&head.content) {
Ok(grant)
if hex32(&grant.member)
.is_some_and(|m| super::derive::grant_locator(community_id, &m) == entity_id) =>
{
record_head();
if !grant.role_ids.is_empty() {
grant_authors.push(head.author);
out.grants.push(grant);
}
}
_ => skipped += 1,
},
VSK_BANLIST if entity_id == banlist_eid => match serde_json::from_str::<Vec<String>>(&head.content) {
Ok(list) => {
banned = list;
banlist_author = Some(head.author);
banlist_head = Some(EntityHead {
entity_hex: crate::simd::hex::bytes_to_hex_32(&entity_id),
version: head.version,
self_hash: head.self_hash,
inner_id: head.inner_id,
citation: head.authority.clone(),
});
}
_ => skipped += 1,
},
VSK_INVITE_LINKS if super::derive::invite_links_locator(community_id, &head.author.to_bytes()) == entity_id => {
match serde_json::from_str::<Vec<String>>(&head.content) {
Ok(locators) => invite_link_sets.push(InviteLinkSet {
creator: head.author,
locators,
head: EntityHead {
entity_hex: crate::simd::hex::bytes_to_hex_32(&entity_id),
version: head.version,
self_hash: head.self_hash,
inner_id: head.inner_id,
citation: head.authority.clone(),
},
}),
_ => skipped += 1,
}
}
VSK_COMMUNITY_ROOT if entity_id == community_id.0 => {
let entity_hex = crate::simd::hex::bytes_to_hex_32(&entity_id);
let mut cands: Vec<(u64, [u8; 32], RootCandidate)> = editions
.iter()
.filter(|e| e.version >= floor_v)
.filter_map(|e| {
serde_json::from_str::<super::metadata::CommunityMetadata>(&e.content)
.ok()
.map(|meta| (e.version, e.inner_id, RootCandidate {
meta,
author: e.author,
head: EntityHead {
entity_hex: entity_hex.clone(),
version: e.version,
self_hash: e.self_hash,
inner_id: e.inner_id,
citation: e.authority.clone(),
},
}))
})
.collect();
if cands.is_empty() {
skipped += 1;
} else {
cands.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)));
root_candidates = cands.into_iter().map(|(_, _, c)| c).collect();
let top = &root_candidates[0];
root_meta = Some(top.meta.clone());
root_author = Some(top.author);
root_head = Some(top.head.clone());
}
}
VSK_CHANNEL => {
let entity_hex = crate::simd::hex::bytes_to_hex_32(&entity_id);
let mut cands: Vec<(u64, [u8; 32], ChannelMetaHead)> = editions
.iter()
.filter(|e| e.version >= floor_v)
.filter_map(|e| {
serde_json::from_str::<super::metadata::ChannelMetadata>(&e.content)
.ok()
.map(|meta| (e.version, e.inner_id, ChannelMetaHead {
channel_id: entity_id,
meta,
author: e.author,
head: EntityHead {
entity_hex: entity_hex.clone(),
version: e.version,
self_hash: e.self_hash,
inner_id: e.inner_id,
citation: e.authority.clone(),
},
}))
})
.collect();
if cands.is_empty() {
skipped += 1;
} else {
cands.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)));
channel_meta.push(cands[0].2.clone());
channel_candidates.extend(cands.into_iter().map(|(_, _, c)| c));
}
}
_ => {} }
}
FoldedRoster {
roles: out, role_authors, grant_authors, gapped_entities, skipped, fetched: 0, heads,
banned, banlist_author, banlist_head, dissolved_by,
invite_link_sets,
root_meta, root_author, root_head, root_candidates, channel_meta, channel_candidates,
}
}
pub fn authorize_delegation(folded: &FoldedRoster, owner_hex: Option<&str>) -> roles::CommunityRoles {
use roles::Permissions;
let mut accepted = roles::CommunityRoles::default();
let mut role_done = vec![false; folded.roles.roles.len()];
let mut grant_done = vec![false; folded.roles.grants.len()];
loop {
let mut changed = false;
for (i, role) in folded.roles.roles.iter().enumerate() {
if role_done[i] {
continue;
}
if role.position == 0 {
role_done[i] = true;
continue;
}
let author = folded.role_authors[i].to_hex();
if accepted.can_act_on_position(&author, owner_hex, role.position, Permissions::MANAGE_ROLES) {
accepted.roles.push(role.clone());
role_done[i] = true;
changed = true;
}
}
for (i, grant) in folded.roles.grants.iter().enumerate() {
if grant_done[i] {
continue;
}
let positions: Option<Vec<u32>> =
grant.role_ids.iter().map(|rid| accepted.role(rid).map(|r| r.position)).collect();
let Some(positions) = positions else { continue }; let author = folded.grant_authors[i].to_hex();
let outranks_all_roles = positions
.iter()
.all(|p| accepted.can_act_on_position(&author, owner_hex, *p, Permissions::MANAGE_ROLES));
if outranks_all_roles
&& accepted.can_act_on_member(&author, owner_hex, &grant.member, Permissions::MANAGE_ROLES)
{
accepted.grants.push(grant.clone());
grant_done[i] = true;
changed = true;
}
}
if !changed {
break;
}
}
accepted
}
pub fn authority_citation_satisfied(
heads: &[EntityHead],
owner_hex: Option<&str>,
actor_hex: &str,
actor_grant_hex: &str,
citation: Option<&edition::AuthorityCitation>,
) -> bool {
if owner_hex == Some(actor_hex) {
return true;
}
let Some(c) = citation else { return false };
let entity_hex = crate::simd::hex::bytes_to_hex_32(&c.entity_id);
if entity_hex != actor_grant_hex {
return false;
}
match heads.iter().find(|h| h.entity_hex == entity_hex) {
Some(h) if h.version > c.version => true,
Some(h) if h.version == c.version => h.self_hash == c.edition_hash,
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::community::roles::{Permissions, Role, RoleScope};
fn sr() -> ServerRootKey {
ServerRootKey([0x07; 32])
}
#[test]
fn authority_citation_satisfied_pins_to_a_synced_complete_grant() {
let owner = "ow".repeat(32);
let actor = "ac".repeat(32);
let entity = [0x55u8; 32];
let hash = [0x66u8; 32];
let eh = crate::simd::hex::bytes_to_hex_32(&entity); let head = |v: u64, h: [u8; 32]| {
vec![EntityHead { entity_hex: eh.clone(), version: v, self_hash: h, inner_id: [0u8; 32], citation: None }]
};
let cite = edition::AuthorityCitation { entity_id: entity, version: 3, edition_hash: hash };
assert!(authority_citation_satisfied(&[], Some(&owner), &owner, &eh, None));
assert!(!authority_citation_satisfied(&head(3, hash), Some(&owner), &actor, &eh, None));
assert!(authority_citation_satisfied(&head(3, hash), Some(&owner), &actor, &eh, Some(&cite)));
assert!(!authority_citation_satisfied(&head(3, [0xEE; 32]), Some(&owner), &actor, &eh, Some(&cite)));
assert!(authority_citation_satisfied(&head(4, [0x77; 32]), Some(&owner), &actor, &eh, Some(&cite)));
assert!(!authority_citation_satisfied(&head(2, hash), Some(&owner), &actor, &eh, Some(&cite)));
assert!(!authority_citation_satisfied(&[], Some(&owner), &actor, &eh, Some(&cite)));
let foreign = "ff".repeat(32);
assert!(!authority_citation_satisfied(&head(3, hash), Some(&owner), &actor, &foreign, Some(&cite)));
}
fn cid() -> CommunityId {
CommunityId([0x09; 32])
}
fn role_event(owner: &Keys, role_id: &str, position: u32, version: u64, prev: Option<&[u8; 32]>, created_at: u64) -> Event {
let role = Role {
role_id: role_id.to_string(),
name: "Admin".into(),
position,
permissions: Permissions::admin(),
scope: RoleScope::Server,
color: 0,
};
let eid = crate::simd::hex::hex_to_bytes_32(role_id);
edition::build_edition_inner(owner.public_key(), VSK_ROLE, &eid, version, prev, &serde_json::to_string(&role).unwrap(), created_at, None)
.sign_with_keys(owner)
.unwrap()
}
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) {
let member_bytes = crate::simd::hex::hex_to_bytes_32(member_hex);
let eid = crate::community::derive::grant_locator(&cid(), &member_bytes);
let g = roles::MemberGrant { member: member_hex.to_string(), role_ids };
let content = serde_json::to_string(&g).unwrap();
let ev = edition::build_edition_inner(owner.public_key(), VSK_GRANT, &eid, version, prev, &content, created_at, None)
.sign_with_keys(owner)
.unwrap();
(ev, eid, content)
}
fn groot_event(signer: &Keys, name: &str, desc: &str, version: u64, prev: Option<&[u8; 32]>, created_at: u64) -> Event {
let meta = crate::community::metadata::CommunityMetadata {
name: name.to_string(),
relays: vec![],
description: Some(desc.to_string()),
icon: None,
banner: None,
owner_attestation: None,
};
let content = serde_json::to_string(&meta).unwrap();
edition::build_edition_inner(signer.public_key(), VSK_COMMUNITY_ROOT, &cid().0, version, prev, &content, created_at, None)
.sign_with_keys(signer)
.unwrap()
}
#[test]
fn folds_a_role_and_a_grant() {
let owner = Keys::generate();
let role_id = "a".repeat(64);
let member = "bb".repeat(32);
let role_ev = role_event(&owner, &role_id, 1, 1, None, 100);
let (grant_ev, _, _) = grant_event(&owner, &member, vec![role_id.clone()], 1, None, 101);
let folded = fold_roster(&[role_ev, grant_ev], &cid(), &Default::default());
assert_eq!(folded.roles.roles.len(), 1);
assert_eq!(folded.roles.roles[0].role_id, role_id);
assert!(folded.roles.is_admin(&member), "the member holds the granted Admin role");
assert_eq!(folded.skipped, 0);
assert!(folded.gapped_entities.is_empty());
}
#[test]
fn latest_grant_edition_wins() {
let owner = Keys::generate();
let role_a = "a".repeat(64);
let role_b = "b".repeat(64);
let member = "cc".repeat(32);
let (e1, eid, c1) = grant_event(&owner, &member, vec![role_a.clone()], 1, None, 100);
let v1_hash = version::edition_hash(&eid, 1, None, c1.as_bytes());
let (e2, _, _) = grant_event(&owner, &member, vec![role_b.clone()], 2, Some(&v1_hash), 101);
let folded = fold_roster(&[e1, e2], &cid(), &Default::default());
let held: Vec<&String> = folded.roles.grants.iter().flat_map(|g| &g.role_ids).collect();
assert_eq!(held, vec![&role_b], "v2 supersedes v1 — the member now holds role_b, not role_a");
assert_eq!(folded.skipped, 0);
}
#[test]
fn tracking_quarantines_a_gapped_head() {
let owner = Keys::generate();
let role_id = "e".repeat(64);
let eid = crate::simd::hex::hex_to_bytes_32(&role_id);
let ev = role_event(&owner, &role_id, 1, 5, Some(&[0x99u8; 32]), 100);
let mut floors = std::collections::HashMap::new();
floors.insert(role_id.clone(), (3u64, [0x11u8; 32]));
let folded = fold_roster(&[ev], &cid(), &floors);
assert!(folded.roles.roles.is_empty(), "tracking: a gapped tail is quarantined, not folded");
assert_eq!(folded.gapped_entities, vec![eid]);
}
#[test]
fn bootstrapping_surfaces_a_gapped_head_but_authority_gates() {
let owner = Keys::generate();
let owner_hex = owner.public_key().to_hex();
let role_id = "e".repeat(64);
let eid = crate::simd::hex::hex_to_bytes_32(&role_id);
let ev = role_event(&owner, &role_id, 1, 5, Some(&[0x99u8; 32]), 100);
let folded = fold_roster(&[ev], &cid(), &Default::default());
assert_eq!(folded.roles.roles.len(), 1, "bootstrapping surfaces the highest signed head despite the gap");
assert_eq!(folded.gapped_entities, vec![eid], "still flagged pending so the union can fill the gap");
assert_eq!(authorize_delegation(&folded, Some(&owner_hex)).roles.len(), 1, "owner-authored head is authorized");
let stranger = Keys::generate();
let ev2 = role_event(&stranger, &role_id, 1, 5, Some(&[0x99u8; 32]), 100);
let folded2 = fold_roster(&[ev2], &cid(), &Default::default());
assert_eq!(folded2.roles.roles.len(), 1, "surfaced into the raw fold (signature is valid)");
assert!(authorize_delegation(&folded2, Some(&owner_hex)).roles.is_empty(),
"Policy B does NOT bypass authority — an unauthorized author's surfaced head is rejected");
}
#[test]
fn fresh_joiner_recovers_full_plane_across_gaps() {
let owner = Keys::generate();
let owner_hex = owner.public_key().to_hex();
let admin = Keys::generate();
let admin_hex = admin.public_key().to_hex();
let role_id = "a".repeat(64);
let role = role_event(&owner, &role_id, 1, 1, None, 100);
let (grant, _, _) = grant_event(&owner, &admin_hex, vec![role_id.clone()], 2, Some(&[0x99u8; 32]), 110);
let groot_v1 = groot_event(&owner, "Genesis", "", 1, None, 90);
let groot_v11 = groot_event(&admin, "AggroTown v2", "King Claude was here", 11, Some(&[0xABu8; 32]), 200);
let folded = fold_roster(&[role, grant, groot_v1, groot_v11], &cid(), &Default::default());
assert_eq!(folded.root_author.map(|a| a.to_hex()), Some(admin_hex.clone()), "latest GroupRoot author surfaced");
assert_eq!(folded.root_meta.as_ref().unwrap().name, "AggroTown v2");
assert_eq!(folded.root_meta.as_ref().unwrap().description.as_deref(), Some("King Claude was here"));
let authed = authorize_delegation(&folded, Some(&owner_hex));
assert!(
authed.is_authorized(&admin_hex, Some(&owner_hex), roles::Permissions::MANAGE_METADATA),
"the bootstrapped grant authorizes the admin → the fresh joiner trusts the admin's metadata"
);
}
#[test]
fn unauthorized_high_version_metadata_is_surfaced_but_not_authorized() {
let owner = Keys::generate();
let owner_hex = owner.public_key().to_hex();
let stranger = Keys::generate();
let stranger_hex = stranger.public_key().to_hex();
let groot_v1 = groot_event(&owner, "Genesis", "", 1, None, 90);
let forged = groot_event(&stranger, "PWNED", "owned by a hostile relay", 99, Some(&[0xCDu8; 32]), 300);
let folded = fold_roster(&[groot_v1, forged], &cid(), &Default::default());
assert_eq!(folded.root_author.map(|a| a.to_hex()), Some(stranger_hex.clone()));
let authed = authorize_delegation(&folded, Some(&owner_hex));
assert!(
!authed.is_authorized(&stranger_hex, Some(&owner_hex), roles::Permissions::MANAGE_METADATA),
"an unauthorized author's surfaced metadata is NOT authorized — the caller drops it"
);
}
#[test]
fn fresh_fold_picks_owner_reassert_over_a_demoted_admins_forgery_and_edit() {
let owner = Keys::generate();
let owner_hex = owner.public_key().to_hex();
let alice = Keys::generate();
let alice_hex = alice.public_key().to_hex();
let role_id = "a".repeat(64);
let role = role_event(&owner, &role_id, 1, 1, None, 100);
let (grant, geid, gcontent) = grant_event(&owner, &alice_hex, vec![role_id.clone()], 1, None, 110);
let grant_hash = version::edition_hash(&geid, 1, None, gcontent.as_bytes());
let (revoke, _, _) = grant_event(&owner, &alice_hex, vec![], 2, Some(&grant_hash), 120);
let groot_v1 = groot_event(&owner, "Genesis", "", 1, None, 90);
let round1_v2 = groot_event(&alice, "Alice's HQ", "by admin alice", 2, Some(&[0x22u8; 32]), 200);
let forgery_v3 = groot_event(&alice, "FORGERY", "demoted alice", 3, Some(&[0x33u8; 32]), 300);
let reassert_v3 = groot_event(&owner, "Alice's HQ", "re-asserted by owner", 3, Some(&[0x33u8; 32]), 310);
let folded = fold_roster(
&[role, grant, revoke, groot_v1, round1_v2, forgery_v3, reassert_v3],
&cid(), &Default::default(),
);
let authed = authorize_delegation(&folded, Some(&owner_hex));
assert!(!authed.is_authorized(&alice_hex, Some(&owner_hex), Permissions::MANAGE_METADATA),
"Alice is revoked (grant→revoke folds to no roles)");
assert_eq!(folded.root_candidates.iter().filter(|c| c.head.version == 3).count(), 2,
"the candidate set includes both v3 fork members");
let chosen = folded.root_candidates.iter()
.find(|c| authed.is_authorized(&c.author.to_hex(), Some(&owner_hex), Permissions::MANAGE_METADATA))
.expect("an authorized candidate exists");
assert_eq!(chosen.author, owner.public_key(), "the owner's re-assert wins the fork, not Alice's forgery");
assert_eq!(chosen.meta.name, "Alice's HQ", "the demoted admin's content is preserved, not 'FORGERY'");
}
#[test]
fn bootstrapping_surfaces_a_stranger_grant_but_it_never_authorizes() {
let owner = Keys::generate();
let owner_hex = owner.public_key().to_hex();
let stranger = Keys::generate();
let stranger_hex = stranger.public_key().to_hex();
let role_id = "a".repeat(64);
let role = role_event(&owner, &role_id, 1, 1, None, 100);
let (grant, _, _) = grant_event(&stranger, &stranger_hex, vec![role_id.clone()], 99, Some(&[0x99u8; 32]), 200);
let folded = fold_roster(&[role, grant], &cid(), &Default::default());
assert_eq!(folded.roles.grants.len(), 1, "the stranger's grant is surfaced (valid signature)");
let authed = authorize_delegation(&folded, Some(&owner_hex));
assert!(authed.grants.is_empty(), "but it never chains to the owner → dropped");
assert!(!authed.is_authorized(&stranger_hex, Some(&owner_hex), roles::Permissions::BAN),
"the self-granting stranger holds no authority");
}
#[test]
fn mixed_tracking_and_bootstrapping_floors_in_one_fold() {
let owner = Keys::generate();
let role_id = "a".repeat(64);
let fresh_member = "cc".repeat(32);
let tracked_member = "dd".repeat(32);
let groot_v11 = groot_event(&owner, "Tracked", "held-at-v5", 11, Some(&[0xABu8; 32]), 200);
let (fresh_grant, _, _) = grant_event(&owner, &fresh_member, vec![role_id.clone()], 2, Some(&[0x99u8; 32]), 110);
let (tracked_grant, _, _) = grant_event(&owner, &tracked_member, vec![role_id.clone()], 11, Some(&[0x88u8; 32]), 120);
let role = role_event(&owner, &role_id, 1, 1, None, 100);
let cid_hex = crate::simd::hex::bytes_to_hex_32(&cid().0);
let tracked_grant_hex = crate::simd::hex::bytes_to_hex_32(
&crate::community::derive::grant_locator(&cid(), &crate::simd::hex::hex_to_bytes_32(&tracked_member)));
let mut floors = std::collections::HashMap::new();
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);
assert!(folded.root_meta.is_some(), "tracking GroupRoot with a gapped tail is SURFACED (display exemption)");
assert!(folded.gapped_entities.contains(&cid().0), "and still flagged pending for a refetch");
assert_eq!(folded.roles.grants.len(), 1, "only the bootstrapping grant surfaces; the tracked-gapped grant stays quarantined");
}
#[test]
fn empty_grant_revoke_advances_head_but_carries_no_entry() {
let owner = Keys::generate();
let role_id = "a".repeat(64);
let member = "cc".repeat(32);
let (g1, eid, c1) = grant_event(&owner, &member, vec![role_id], 1, None, 100);
let v1_hash = version::edition_hash(&eid, 1, None, c1.as_bytes());
let (g2, _, _) = grant_event(&owner, &member, vec![], 2, Some(&v1_hash), 101); let folded = fold_roster(&[g1, g2], &cid(), &Default::default());
assert!(folded.roles.grants.is_empty(), "a revoke leaves no roster entry");
assert!(
folded.heads.iter().any(|h| h.entity_hex == crate::simd::hex::bytes_to_hex_32(&eid) && h.version == 2),
"but the revoke still advances the head (so a replayed v1 can't re-add the role)"
);
}
#[test]
fn fold_roster_skips_junk_and_still_folds_the_valid() {
let owner = Keys::generate();
let owner_hex = owner.public_key().to_hex();
let role_id = "a".repeat(64);
let good = role_event(&owner, &role_id, 1, 1, None, 100);
let garbage = edition::build_edition_inner(
owner.public_key(), VSK_ROLE, &crate::simd::hex::hex_to_bytes_32(&"b".repeat(64)),
1, None, "not json at all", 100, None,
).sign_with_keys(&owner).unwrap();
let role_b = Role { role_id: "cc".repeat(64), name: "X".into(), position: 1, permissions: Permissions::admin(), scope: RoleScope::Server, color: 0 };
let mismatched = edition::build_edition_inner(
owner.public_key(), VSK_ROLE, &crate::simd::hex::hex_to_bytes_32(&"dd".repeat(64)),
1, None, &serde_json::to_string(&role_b).unwrap(), 100, None,
).sign_with_keys(&owner).unwrap();
let folded = fold_roster(&[good, garbage, mismatched], &cid(), &Default::default());
assert!(folded.roles.role(&role_id).is_some(), "the valid role still folds through the junk");
assert!(folded.skipped >= 2, "both junk editions are skipped, not folded (skipped={})", folded.skipped);
assert_eq!(authorize_delegation(&folded, Some(&owner_hex)).roles.len(), 1, "only the valid role authorizes");
}
#[test]
fn role_content_must_bind_to_its_entity_id() {
let owner = Keys::generate();
let role = Role {
role_id: "a".repeat(64), name: "X".into(),
position: 0,
permissions: Permissions::admin(),
scope: RoleScope::Server,
color: 0,
};
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)
.sign_with_keys(&owner)
.unwrap();
let folded = fold_roster(&[ev], &cid(), &Default::default());
assert!(folded.roles.roles.is_empty(), "entity_id != role_id → rejected");
assert_eq!(folded.skipped, 1);
}
#[test]
fn grant_at_wrong_locator_is_rejected() {
let owner = Keys::generate();
let member = "dd".repeat(32);
let g = roles::MemberGrant { member: member.clone(), role_ids: vec!["a".repeat(64)] };
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)
.sign_with_keys(&owner)
.unwrap();
let folded = fold_roster(&[ev], &cid(), &Default::default());
assert!(!folded.roles.is_admin(&member), "a grant at the wrong locator does not take effect");
assert_eq!(folded.skipped, 1);
}
#[test]
fn fold_is_order_independent() {
let owner = Keys::generate();
let role_id = "a".repeat(64);
let member = "bb".repeat(32);
let role_ev = role_event(&owner, &role_id, 1, 1, None, 100);
let (grant_ev, _, _) = grant_event(&owner, &member, vec![role_id.clone()], 1, None, 101);
let a = fold_roster(&[role_ev.clone(), grant_ev.clone()], &cid(), &Default::default());
let b = fold_roster(&[grant_ev, role_ev], &cid(), &Default::default());
assert_eq!(a.roles.roles.len(), b.roles.roles.len());
assert!(a.roles.is_admin(&member) && b.roles.is_admin(&member));
}
#[test]
fn public_builders_round_trip_through_fold() {
let owner = Keys::generate();
let role = Role {
role_id: "a".repeat(64),
name: "Admin".into(),
position: 1,
permissions: Permissions::admin(),
scope: RoleScope::Server,
color: 0,
};
let member = "bb".repeat(32);
let grant = roles::MemberGrant { member: member.clone(), role_ids: vec![role.role_id.clone()] };
let role_ev = build_role_edition(&owner, &role, 1, None, 100, None).unwrap();
let grant_ev = build_grant_edition(&owner, &cid(), &grant, 1, None, 101, None).unwrap();
let folded = fold_roster(&[role_ev, grant_ev], &cid(), &Default::default());
assert_eq!(folded.skipped, 0, "builders emit bound, anchored editions the fold accepts");
assert!(folded.gapped_entities.is_empty());
assert!(folded.roles.is_admin(&member));
assert_eq!(folded.roles.role(&role.role_id).unwrap().position, 1);
}
#[test]
fn builders_reject_malformed_chain_shape() {
let owner = Keys::generate();
let role = Role {
role_id: "a".repeat(64), name: "Admin".into(), position: 1,
permissions: Permissions::admin(), scope: RoleScope::Server, color: 0,
};
assert!(build_role_edition(&owner, &role, 1, Some(&[0u8; 32]), 100, None).is_err());
assert!(build_role_edition(&owner, &role, 5, None, 100, None).is_err());
let grant = roles::MemberGrant { member: "bb".repeat(32), role_ids: vec![role.role_id.clone()] };
assert!(build_grant_edition(&owner, &cid(), &grant, 1, Some(&[0u8; 32]), 100, None).is_err());
assert!(build_grant_edition(&owner, &cid(), &grant, 2, None, 100, None).is_err());
}
#[test]
fn builders_reject_bad_hex() {
let owner = Keys::generate();
let bad_role = Role {
role_id: "not-hex".into(), name: "X".into(), position: 1,
permissions: Permissions::admin(), scope: RoleScope::Server, color: 0,
};
assert!(build_role_edition(&owner, &bad_role, 1, None, 100, None).is_err());
let bad_grant = roles::MemberGrant { member: "zz".repeat(32), role_ids: vec!["a".repeat(64)] };
assert!(build_grant_edition(&owner, &cid(), &bad_grant, 1, None, 100, None).is_err());
}
#[test]
fn producer_built_chain_folds_to_latest() {
let owner = Keys::generate();
let member = "cc".repeat(32);
let role_a = "a".repeat(64);
let role_b = "b".repeat(64);
let g1 = roles::MemberGrant { member: member.clone(), role_ids: vec![role_a] };
let e1 = build_grant_edition(&owner, &cid(), &g1, 1, None, 100, None).unwrap();
let member_bytes = crate::simd::hex::hex_to_bytes_32(&member);
let eid = crate::community::derive::grant_locator(&cid(), &member_bytes);
let v1_hash = version::edition_hash(&eid, 1, None, serde_json::to_string(&g1).unwrap().as_bytes());
let g2 = roles::MemberGrant { member: member.clone(), role_ids: vec![role_b.clone()] };
let e2 = build_grant_edition(&owner, &cid(), &g2, 2, Some(&v1_hash), 101, None).unwrap();
let folded = fold_roster(&[e1, e2], &cid(), &Default::default());
assert_eq!(folded.skipped, 0);
assert!(folded.gapped_entities.is_empty(), "v2 links to v1 — no gap");
let held: Vec<&String> = folded.roles.grants.iter().flat_map(|g| &g.role_ids).collect();
assert_eq!(held, vec![&role_b]);
}
#[test]
fn control_edition_seals_opens_and_folds_end_to_end() {
let owner = Keys::generate();
let community_id = CommunityId([0x09; 32]);
let epoch = Epoch(0);
let role = Role {
role_id: "a".repeat(64), name: "Admin".into(), position: 1,
permissions: Permissions::admin(), scope: RoleScope::Server, color: 0,
};
let inner = build_role_edition(&owner, &role, 1, None, 100, None).unwrap();
let outer = seal_control_edition(&Keys::generate(), &inner, &sr(), &community_id, epoch).unwrap();
assert_ne!(outer.pubkey, owner.public_key(), "outer is ephemeral-signed");
assert!(!outer.content.contains(&role.role_id), "inner content is encrypted on the wire");
assert!(open_control_edition(&outer, &ServerRootKey([0xAA; 32])).is_err());
let reopened = open_control_edition(&outer, &sr()).unwrap();
assert_eq!(edition::parse_edition_inner(&reopened).unwrap().author, owner.public_key());
let folded = fold_roster(&[reopened], &cid(), &Default::default());
assert_eq!(folded.skipped, 0);
assert!(folded.roles.role(&role.role_id).is_some(), "build→seal→open→parse→fold round-trips");
}
#[test]
fn control_edition_round_trips_at_a_nonzero_epoch() {
let owner = Keys::generate();
let community_id = CommunityId([0x09; 32]);
let role = Role {
role_id: "a".repeat(64), name: "Admin".into(), position: 1,
permissions: Permissions::admin(), scope: RoleScope::Server, color: 0,
};
let inner = build_role_edition(&owner, &role, 1, None, 100, None).unwrap();
let outer = seal_control_edition(&Keys::generate(), &inner, &sr(), &community_id, Epoch(4)).unwrap();
let z4 = control_pseudonym(&sr(), &community_id, Epoch(4));
assert_ne!(z4, control_pseudonym(&sr(), &community_id, Epoch(0)), "epochs address distinct pseudonyms");
let z_tag = outer.tags.iter().find_map(|t| {
let s = t.as_slice();
(s.len() >= 2 && s[0] == "z").then(|| s[1].clone())
}).expect("control outer carries a z tag");
assert_eq!(z_tag, z4, "sealed at the epoch-4 pseudonym, NOT epoch 0 (regression #1 guard)");
let reopened = open_control_edition(&outer, &sr()).unwrap();
let folded = fold_roster(&[reopened], &cid(), &Default::default());
assert!(folded.roles.role(&role.role_id).is_some(), "seal→open→fold round-trips at a non-zero epoch");
}
#[test]
fn control_pseudonym_golden_vector() {
let server_root = ServerRootKey([0x07; 32]);
let community_id = CommunityId([0x09; 32]);
assert_eq!(
control_pseudonym(&server_root, &community_id, Epoch(0)),
"e719f2d29ca005dfe805b1f85f696948394661c748e1f98b2df7d396260f6378"
);
}
#[test]
fn open_rejects_non_control_outer() {
let bogus = EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_MESSAGE), "x")
.sign_with_keys(&Keys::generate())
.unwrap();
assert!(open_control_edition(&bogus, &sr()).is_err());
}
#[test]
fn delegation_rejects_a_self_signed_admin_grant() {
let owner = Keys::generate();
let mallory = Keys::generate();
let admin = "a".repeat(64);
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);
let folded = fold_roster(&[role_ev, self_grant], &cid(), &Default::default());
assert!(folded.roles.grants.iter().any(|g| g.member == mallory.public_key().to_hex()));
let authorized = authorize_delegation(&folded, Some(&owner.public_key().to_hex()));
assert!(authorized.roles.iter().any(|r| r.role_id == admin), "owner-signed role stays");
assert!(
!authorized.grants.iter().any(|g| g.member == mallory.public_key().to_hex()),
"self-signed Admin grant is REJECTED — no self-promotion"
);
}
#[test]
fn delegation_accepts_owner_signed_role_and_grant() {
let owner = Keys::generate();
let member = Keys::generate();
let admin = "a".repeat(64);
let role_ev = role_event(&owner, &admin, 1, 1, None, 100);
let (grant_ev, _, _) = grant_event(&owner, &member.public_key().to_hex(), vec![admin.clone()], 1, None, 101);
let authorized = authorize_delegation(&fold_roster(&[role_ev, grant_ev], &cid(), &Default::default()), Some(&owner.public_key().to_hex()));
assert!(authorized.roles.iter().any(|r| r.role_id == admin));
assert!(authorized.grants.iter().any(|g| g.member == member.public_key().to_hex()));
}
#[test]
fn delegation_chains_owner_to_admin_to_mod() {
let owner = Keys::generate();
let alice = Keys::generate();
let bob = Keys::generate();
let (admin, moderator) = ("a".repeat(64), "b".repeat(64));
let r_admin = role_event(&owner, &admin, 1, 1, None, 100);
let (g_alice, _, _) = grant_event(&owner, &alice.public_key().to_hex(), vec![admin.clone()], 1, None, 101);
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);
let authorized = authorize_delegation(
&fold_roster(&[r_admin, g_alice, r_mod, g_bob], &cid(), &Default::default()),
Some(&owner.public_key().to_hex()),
);
assert!(authorized.roles.iter().any(|r| r.role_id == moderator), "admin Alice could create Mod");
assert!(authorized.grants.iter().any(|g| g.member == alice.public_key().to_hex()), "owner→Alice Admin");
assert!(authorized.grants.iter().any(|g| g.member == bob.public_key().to_hex()), "Alice→Bob Mod (delegated)");
}
#[test]
fn delegation_demoted_admins_delegated_grant_is_dropped() {
let owner = Keys::generate();
let alice = Keys::generate();
let bob = Keys::generate();
let (admin, moderator) = ("a".repeat(64), "b".repeat(64));
let r_admin = role_event(&owner, &admin, 1, 1, None, 100);
let (g_alice1, a_eid, a_c1) = grant_event(&owner, &alice.public_key().to_hex(), vec![admin.clone()], 1, None, 101);
let a_v1 = version::edition_hash(&a_eid, 1, None, a_c1.as_bytes());
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);
let (g_bob, _, _) = grant_event(&alice, &bob.public_key().to_hex(), vec![moderator.clone()], 1, None, 104);
let folded = fold_roster(&[r_admin, g_alice1, g_alice2, r_mod, g_bob], &cid(), &Default::default());
let authorized = authorize_delegation(&folded, Some(&owner.public_key().to_hex()));
assert!(!authorized.grants.iter().any(|g| g.member == alice.public_key().to_hex()), "Alice's admin grant is revoked");
assert!(
!authorized.grants.iter().any(|g| g.member == bob.public_key().to_hex()),
"a since-demoted admin's delegated grant is dropped — version-pinning falls out, no citation needed"
);
}
#[test]
fn delegation_floor_blocks_a_rolled_back_admin_grant() {
let owner = Keys::generate();
let alice = Keys::generate();
let bob = Keys::generate();
let (admin, moderator) = ("a".repeat(64), "b".repeat(64));
let r_admin = role_event(&owner, &admin, 1, 1, None, 100);
let (g_alice1, a_eid, _) = grant_event(&owner, &alice.public_key().to_hex(), vec![admin.clone()], 1, None, 101);
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);
let mut floors = std::collections::HashMap::new();
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);
let authorized = authorize_delegation(&folded, Some(&owner.public_key().to_hex()));
assert!(!authorized.grants.iter().any(|g| g.member == alice.public_key().to_hex()), "rolled-back Alice grant refused");
assert!(
!authorized.grants.iter().any(|g| g.member == bob.public_key().to_hex()),
"Bob's delegated grant dropped — the floor blocks the rollback that would re-authorize Alice"
);
}
#[test]
fn delegation_admin_cannot_grant_a_peer_admin() {
let owner = Keys::generate();
let alice = Keys::generate();
let bob = Keys::generate();
let admin = "a".repeat(64);
let r_admin = role_event(&owner, &admin, 1, 1, None, 100);
let (g_alice, _, _) = grant_event(&owner, &alice.public_key().to_hex(), vec![admin.clone()], 1, None, 101);
let (g_bob, _, _) = grant_event(&alice, &bob.public_key().to_hex(), vec![admin.clone()], 1, None, 102);
let authorized = authorize_delegation(
&fold_roster(&[r_admin, g_alice, g_bob], &cid(), &Default::default()),
Some(&owner.public_key().to_hex()),
);
assert!(authorized.grants.iter().any(|g| g.member == alice.public_key().to_hex()), "owner→Alice ok");
assert!(
!authorized.grants.iter().any(|g| g.member == bob.public_key().to_hex()),
"an admin cannot grant a PEER-rank Admin"
);
}
#[test]
fn delegation_rejects_a_circular_grant_with_no_owner_root() {
let owner = Keys::generate(); let a = Keys::generate();
let b = Keys::generate();
let admin = "a".repeat(64);
let r_admin = role_event(&owner, &admin, 1, 1, None, 100);
let (g_ab, _, _) = grant_event(&a, &b.public_key().to_hex(), vec![admin.clone()], 1, None, 101);
let (g_ba, _, _) = grant_event(&b, &a.public_key().to_hex(), vec![admin.clone()], 1, None, 102);
let authorized = authorize_delegation(
&fold_roster(&[r_admin, g_ab, g_ba], &cid(), &Default::default()),
Some(&owner.public_key().to_hex()),
);
assert!(authorized.roles.iter().any(|r| r.role_id == admin), "owner's role stays");
assert!(authorized.grants.is_empty(), "a circular mutual-admin grant cannot bootstrap without the owner");
}
#[test]
fn delegation_rejects_a_position_0_role_even_owner_signed() {
let owner = Keys::generate();
let mallory = Keys::generate();
let by_mallory = authorize_delegation(
&fold_roster(&[role_event(&mallory, &"e".repeat(64), 0, 1, None, 100)], &cid(), &Default::default()),
Some(&owner.public_key().to_hex()),
);
assert!(by_mallory.roles.is_empty(), "a non-owner cannot mint a position-0 role");
let by_owner = authorize_delegation(
&fold_roster(&[role_event(&owner, &"f".repeat(64), 0, 1, None, 100)], &cid(), &Default::default()),
Some(&owner.public_key().to_hex()),
);
assert!(by_owner.roles.is_empty(), "position 0 is the attestation's — even the owner can't mint a pos-0 role");
}
#[test]
fn delegation_unproven_community_yields_empty_roster() {
let owner = Keys::generate();
let role_ev = role_event(&owner, &"a".repeat(64), 1, 1, None, 100);
let authorized = authorize_delegation(&fold_roster(&[role_ev], &cid(), &Default::default()), None);
assert!(authorized.roles.is_empty() && authorized.grants.is_empty(), "no root ⇒ empty authorized roster");
}
}