Skip to main content

vector_core/community/
roster.rs

1//! Folding fetched authority editions into the current roster.
2//!
3//! The control plane is a set of per-entity append editions (§edition), each real-npub-signed and
4//! version-chained (§version). This module is the consumer: given the (already-decrypted) inner
5//! edition events, it verifies each authorship signature, groups editions per entity, folds each
6//! entity's version chain to its current head, and deserializes the heads into the in-memory
7//! [`roles::CommunityRoles`] the authority gates query.
8//!
9//! Two layers: [`fold_roster`] produces the "validly signed, anchored, bound, current" roster (the
10//! inner signature proves WHO authored each edition), and [`authorize_delegation`] then filters it by
11//! the delegation chain — deciding WHETHER each signer was allowed (rank + the chain to the
12//! owner), so a self-signed or forged-delegation entry never becomes trusted authority. Entities that
13//! come back with a chain gap (unanchored / withheld prereqs, §version) are reported so the caller can
14//! fail closed (tracking) or accept-via-authority (bootstrapping) per.
15
16use 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
22/// Sub-kinds. The fold here interprets ROLE/GRANT/BANLIST (authority); COMMUNITY_ROOT/CHANNEL
23/// are display metadata, built as editions here but applied by the metadata consumer.
24const 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";
29// vsk allocations 0-7 are all spoken for (5=RoleOrder reserved-unbuilt, 6=PublicInvite is the
30// token-signed bundle, 7=OwnerAttestation). The invite-link REGISTRY (the member-readable Public/Private
31// source of truth) is a NEW control entity at the next free number, 8. Never reuse 0-7.
32const VSK_INVITE_LINKS: &str = "8";
33/// vsk=10: the owner-dissolution tombstone. 9 = public-invite-revoked. The tombstone lives at
34/// `dissolved_locator(community_id)` and has NO version chain / prev-hash — presence of ≥1 valid
35/// owner-signed edition at the locator IS the state (it is exempt from `check_chain_shape` + the fold's
36/// version discipline). Never reuse.
37const VSK_DISSOLVED: &str = "10";
38
39/// Hard cap on editions processed per fold — bounds the Schnorr-verify + fold work a hostile relay
40/// can force by piling junk at the control coordinate. Legit control history is far smaller.
41/// pub(crate): the fetch layer also bounds its AEAD open loop with this.
42pub(crate) const MAX_CONTROL_EDITIONS: usize = 50_000;
43
44/// Validate a 64-char lowercase/uppercase hex string and decode it, returning `None` on bad
45/// length/charset (so [`crate::simd::hex::hex_to_bytes_32`]'s silent zero-on-invalid never bites).
46fn 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
51/// Reject a malformed edition **chain shape** at mint time, so a mis-wired caller fails loud here
52/// rather than minting an edition the consumer fold silently quarantines (a baffling "my change never
53/// took effect" outage). NOTE: this validates only the *shape* — that the *value* of `prev_hash`
54/// actually equals the prior edition's `edition_hash` is the caller's responsibility (only the caller
55/// holds the prior head), and a wrong value likewise fails silently at fold, not here.
56fn 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
65/// Build a signed **RoleMetadata** edition (vsk=1) at its bound coordinate (`entity_id == role_id`),
66/// the next version in the role's chain. Signed by the ACTOR's real keys (the authorship proof); the
67/// caller supplies the next `version` + the prior edition's `prev_hash` (the held head's `self_hash`,
68/// or `None` with `version == 1` for a brand-new role — see [`check_chain_shape`]). The resulting
69/// inner event is what the envelope then seals under the server-root key for publication.
70pub 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
83/// The UNSIGNED RoleMetadata edition (the bunker path): build the inner, then sign it with the active
84/// `NostrSigner` (`unsigned.sign(&signer).await`) so a NIP-46 remote signer works. The sync
85/// [`build_role_edition`] is the local-keys convenience over this.
86pub 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
100/// Build a signed **Grant** edition (vsk=3) at its bound coordinate
101/// (`entity_id == grant_locator(community_id, member)` — community-scoped so it survives a base
102/// rotation, the keystone for re-anchoring), the next version in that member's grant chain.
103/// Signed by the actor's real keys. An empty `role_ids` is a revoke (folds to no entry).
104pub 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
118/// The UNSIGNED Grant edition (the bunker path); sign with the active `NostrSigner`.
119pub 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
135/// Build a signed **Banlist** edition (vsk=4) at the single community-wide coordinate
136/// (`entity_id == banlist_locator(community_id)` — community-scoped so it survives a base rotation and
137/// re-anchors). Content is the JSON array of
138/// banned pubkeys (lowercase hex). Signed by the actor's real keys; the consumer ([`fold_roster`] +
139/// the BAN-authority check) decides whether that signer was allowed to ban.
140pub 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
154/// The UNSIGNED Banlist edition (the bunker path); sign with the active `NostrSigner`.
155pub 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
170/// Build a signed **GroupDissolved** tombstone (vsk=10) at the community-scoped dissolved locator
171/// (`entity_id == dissolved_locator(community_id)` — rotation-stable so a post-rotation joiner still
172/// finds it). UNLIKE every other edition this has NO version chain and NO prev-hash: it is minted at
173/// a fixed `version == 1` with no `prev_hash` and is exempt from `check_chain_shape` — presence of ≥1 valid
174/// OWNER-signed edition here IS dissolution (duplicates are idempotent). Content is minimal (`{}`); the
175/// signer (the owner) is the whole payload. Authority (the signer == proven owner) is the CALLER's check.
176pub 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
186/// The UNSIGNED GroupDissolved tombstone (the bunker path — the owner may sign remotely); sign with the
187/// active `NostrSigner`. No chain discipline (see [`build_group_dissolved_edition`]).
188pub 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    // Chain-free terminal marker: fixed v1, no prev_hash, empty content.
195    edition::build_edition_inner(author, VSK_DISSOLVED, &entity_id, 1, None, "{}", created_at, None)
196}
197
198/// Build a signed **InviteLinks** edition (vsk=8) at the CREATOR's own coordinate
199/// (`entity_id == invite_links_locator(community_id, actor)`). Content is the JSON array of THAT
200/// creator's active public-invite-link LOCATORS (lowercase hex; the locator is public — the token in the
201/// URL is the secret, never listed). Per-creator: a creator publishes only their own list; members
202/// fold every creator's list (gated on the author holding `CREATE_INVITE`) into the aggregate active-set,
203/// the source of truth for the Public/Private mode + registry-authoritative joins. No shared registry.
204pub 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
218/// The UNSIGNED InviteLinks edition (the bunker path); sign with the active `NostrSigner`. The entity
219/// coordinate binds to `author`, so a creator can only publish links at their own coordinate.
220pub 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
235/// Build a signed **GroupRoot** edition (vsk=0) at the community-wide coordinate (`entity_id ==
236/// community_id`) — the Community's display descriptor (name/description/icon/banner + owner
237/// attestation). Real-npub signed; the consumer applies it only if the signer held `MANAGE_METADATA`.
238pub 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
252/// The UNSIGNED GroupRoot edition (the bunker path); sign with the active `NostrSigner`.
253pub 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
267/// Build a signed **ChannelMetadata** edition (vsk=2) at the channel's coordinate (`entity_id ==
268/// channel_id`) — the channel's display descriptor (name). Real-npub signed; the consumer applies it
269/// only if the signer held `MANAGE_CHANNELS`.
270pub 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
284/// The UNSIGNED ChannelMetadata edition (the bunker path); sign with the active `NostrSigner`.
285pub 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
299/// The relay-filterable pseudonym members use to fetch the control plane for `(community, epoch)`.
300/// Derived from the server-root key + community id, so members compute it but outsiders (no
301/// server-root) can't — the control plane has no stable on-wire identifier. This reuses the
302/// `channel-pseudonym` derivation; what keeps a control pseudonym from ever aliasing a channel's is
303/// the invariant that the server-root key is always distinct from every channel key (the HKDF
304/// label is shared, so domain separation rests on the distinct IKM + id32).
305pub 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
309/// Seal a signed control edition (kind 3308) for the wire: encrypt the inner under the **server-root
310/// key** (only members decrypt), with an **ephemeral outer signer** (the real author is the inner
311/// signature, hidden from relays), addressed by the control-plane pseudonym so members fetch by
312/// `#z` without exposing a stable group identifier.
313pub 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
335/// Open a control-edition outer → its inner edition event (decrypt under the server-root key). Does
336/// NOT verify the inner signature or parse fields — pass the result to [`edition::parse_edition_inner`]
337/// (which verifies authorship) and then [`fold_roster`] (which binds + folds). A wrong server-root
338/// key fails to decrypt → `Err`, which is also how **cross-community** replay is rejected.
339///
340/// CROSS-EPOCH replay (same community) is NOT an envelope concern and is deliberately not blocked
341/// here: the control plane is encrypted under the (epoch-agnostic) server-root key, so any edition
342/// can be re-wrapped under any epoch's pseudonym — the envelope cannot bind an epoch, and binding the
343/// inner to an epoch would break re-anchoring (a re-WRAP, not a re-sign). It is a COMPLETENESS-layer
344/// defense: a *tracking* client is protected by the version chain's refuse-downgrade; a
345/// *bootstrapping* joiner relies on quorum reconciliation + the re-anchoring guarantee (the current
346/// head is re-posted under the current epoch). Those MUST be wired into the fetch path (the rekey
347/// increment) before a fresh joiner is safe against a withheld-demotion replay. The single-epoch MVP
348/// (never rotates) is unaffected.
349pub 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
370/// Seal a GroupDissolved tombstone for the wire at the ROTATION-STABLE coordinate: encrypt the
371/// inner under the community-id-derived `dissolved_envelope_key` (NOT the per-epoch server root) and
372/// address it by `dissolved_pseudonym` (NOT `control_pseudonym`), so it is discoverable + openable by any
373/// member or joiner at any epoch. Ephemeral outer signer (the owner is the inner signature). The tombstone
374/// is also published at the current `control_pseudonym` (a current-epoch fast path); this is the cross-epoch
375/// path that survives a concurrent re-founding.
376pub 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
393/// Open a wire event at the dissolved coordinate and, IF it is a well-formed GroupDissolved tombstone
394/// (`vsk=10` at `dissolved_locator`), return its inner real-npub signer. The CALLER decides validity by
395/// checking that signer equals the proven owner (fail-closed authority). `None` for anything else
396/// (wrong key, malformed, wrong vsk, relabelled entity_id).
397pub 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/// The folded current head of one control entity — what the caller persists via `set_edition_head`
409/// (the monotonic refuse-downgrade floor) and the send side reads to emit the next version.
410#[derive(Clone)]
411pub struct EntityHead {
412    /// The entity coordinate (role_id for roles, grant_locator for grants), lowercase hex.
413    pub entity_hex: String,
414    pub version: u64,
415    pub self_hash: [u8; 32],
416    /// The head edition's deterministic tiebreak key (the inner edition id). Used to resolve a
417    /// same-version concurrent fork: every client converges on the lower `inner_id` among authorized
418    /// editions, and the persisted head ranks by it so a same-version adopt only moves toward the min.
419    pub inner_id: [u8; 32],
420    /// The authority citation the head edition carried, if any — the actor's pinned proof of
421    /// the authority they claimed for this edit. `None` for an owner-signed edition (supreme, cites
422    /// nothing) or an uncited one. Verifiers resolve the actor's standing at the cited grant version
423    /// via [`authority_citation_satisfied`].
424    pub citation: Option<edition::AuthorityCitation>,
425}
426
427/// The outcome of folding the control plane.
428#[derive(Clone)]
429pub struct FoldedRoster {
430    /// The anchored, bound, validly-signed heads. This is "validly signed, anchored, and current" — NOT
431    /// yet "authorized." [`authorize_delegation`] is the next layer: it filters these by the 
432    /// delegation chain (each signer must outrank what it defines, chaining to the owner). Use the
433    /// authorized result for any authority decision; this raw roster is the binding-layer output.
434    pub roles: roles::CommunityRoles,
435    /// The signer (inner real-npub author) of each entry in `roles.roles`, SAME ORDER — so
436    /// [`authorize_delegation`] can check whether that signer was allowed to define the role.
437    pub role_authors: Vec<PublicKey>,
438    /// The signer of each entry in `roles.grants`, SAME ORDER (the delegation chain needs the granter).
439    pub grant_authors: Vec<PublicKey>,
440    /// Entity ids whose head is NOT chain-anchored (a gap, §version). These are **quarantined** — NOT
441    /// folded into `roles` (fail closed). A bootstrapping joiner re-verifies them via authority
442    /// before trusting; a tracking client refetches the missing prereqs.
443    pub gapped_entities: Vec<[u8; 32]>,
444    /// Count of editions dropped: bad signature, missing/duplicate fields, an `entity_id`↔content
445    /// mismatch, unparseable content, or beyond the cap. **Nonzero ⇒ the roster may be degraded**
446    /// (a role/grant could be silently missing), so the caller should refetch rather than trust it.
447    pub skipped: usize,
448    /// Count of RAW control editions the source fetch returned (before opening/folding). Set by
449    /// `fetch_control_folded`; `0` for a roster folded from a hand-supplied edition set (tests, prefolds).
450    /// `fetch_and_apply_control` surfaces it so an admin-write guard can tell "≥1 relay responded" (any
451    /// raw event) from total network isolation — without a second, throwaway probe fetch.
452    pub fetched: usize,
453    /// The current head `(entity_hex, version, self_hash)` of every successfully-bound ROLE/GRANT
454    /// entity, for the caller to advance `set_edition_head` (monotonic). Excludes the banlist (its head
455    /// is `banlist_head`, advanced by the banlist path) and gapped/quarantined entities.
456    pub heads: Vec<EntityHead>,
457    /// The folded banlist content (banned pubkeys, lowercase hex) — only meaningful once the BAN
458    /// authority of `banlist_author` is checked against the authorized roster. Empty if no banlist
459    /// edition folded (distinct from "an authored empty banlist," which the caller learns via
460    /// `banlist_head`/`banlist_author` being `Some`).
461    pub banned: Vec<String>,
462    /// The signer (inner real-npub author) of the folded banlist head, so the caller can verify they
463    /// held `BAN`. `None` if no banlist edition folded.
464    pub banlist_author: Option<PublicKey>,
465    /// The DISTINCT signers of every well-formed GroupDissolved tombstone (vsk=10) at `dissolved_locator`
466    /// Detection is owner-SIGNATURE-filtered, NOT position/version dependent: the fold scans the
467    /// locator directly (NOT via the version-chain `version::fold` — the tombstone has no chain) and lists
468    /// each authoring npub, so a flood of forged NON-owner editions can never bury the real owner's signer
469    /// out of the `MAX_CONTROL_EDITIONS` cap and truncate the true tombstone away. The CALLER treats the
470    /// community as dissolved ONLY if the proven owner is in this set (mirrors `banlist_author`'s BAN
471    /// check). A malformed edition at the locator is dropped (`skipped`), never honored.
472    pub dissolved_by: Vec<PublicKey>,
473    /// The banlist entity's current head, for the banlist path to advance `set_edition_head`.
474    pub banlist_head: Option<EntityHead>,
475    /// Folded per-creator invite-link sets (vsk=8, one per creator at `invite_links_locator(cid,
476    /// creator)`). Each holds that creator's active link locators + head. The caller authorizes each
477    /// `creator` (held `CREATE_INVITE`) and UNIONS the locators into the aggregate active-set — the 
478    /// source of truth for the Public/Private mode + registry-authoritative joins. No shared registry.
479    pub invite_link_sets: Vec<InviteLinkSet>,
480    /// The folded GroupRoot (community metadata, vsk=0 at `entity_id == community_id`). `None` if no
481    /// GroupRoot edition folded. Applied only once the caller checks `root_author` held `MANAGE_METADATA`.
482    pub root_meta: Option<super::metadata::CommunityMetadata>,
483    /// The signer of the folded GroupRoot head, so the caller can verify they held `MANAGE_METADATA`.
484    pub root_author: Option<PublicKey>,
485    /// The GroupRoot entity's current head, for the metadata path to advance `set_edition_head`.
486    pub root_head: Option<EntityHead>,
487    /// Every gap-vetted GroupRoot candidate at-or-above the floor (fork members included), highest
488    /// version first (deterministic inner-id tiebreak within a version). The consumer applies the
489    /// highest whose author is CURRENTLY authorized (`MANAGE_METADATA`) — an author-aware descending
490    /// scan (B1b), so a demoted author's edition (incl. a same-version forgery) can't be the head,
491    /// and a fresh fold converges to the highest authorized edition (the owner's re-assert). `root_head`
492    /// is `root_candidates[0]` (the author-blind head) for back-compat.
493    pub root_candidates: Vec<RootCandidate>,
494    /// Folded per-channel metadata (vsk=2, one per channel `entity_id == channel_id`) — the author-blind
495    /// top head per channel (back-compat: used by the demoted-author re-assert path). For convergence the
496    /// consumer scans [`Self::channel_candidates`] instead.
497    pub channel_meta: Vec<ChannelMetaHead>,
498    /// Every gap-vetted channel-metadata candidate at-or-above each channel's floor (fork members
499    /// included), highest version first with the deterministic inner-id tiebreak — the per-channel mirror
500    /// of [`Self::root_candidates`]. Grouped contiguously per channel (sorted within each channel). The
501    /// consumer applies, per channel, the highest whose author CURRENTLY holds `MANAGE_CHANNELS` — an
502    /// author-aware scan, so a demoted author's same-version forgery can't orphan an authorized re-assert
503    /// and a concurrent same-version rename converges to one deterministic winner on every client.
504    pub channel_candidates: Vec<ChannelMetaHead>,
505}
506
507/// A folded per-creator InviteLinks edition (vsk=8): the creator who signed it, their active link
508/// locators, and the version head. Applied only if `creator` held `CREATE_INVITE`.
509#[derive(Clone)]
510pub struct InviteLinkSet {
511    pub creator: PublicKey,
512    pub locators: Vec<String>,
513    pub head: EntityHead,
514}
515
516/// One gap-vetted GroupRoot candidate (vsk=0): its content, signer (for the authority gate), and head.
517/// The consumer scans `FoldedRoster::root_candidates` (highest version first) for the highest whose
518/// author currently holds `MANAGE_METADATA`.
519#[derive(Clone)]
520pub struct RootCandidate {
521    pub meta: super::metadata::CommunityMetadata,
522    pub author: PublicKey,
523    pub head: EntityHead,
524}
525
526/// A folded ChannelMetadata edition (vsk=2): the channel it addresses, its content, its signer (for the
527/// `MANAGE_METADATA` authority gate), and its version head (to advance `set_edition_head`).
528#[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
536/// Fold a set of (already-decrypted) inner edition events into the current roster.
537///
538/// Each edition's inner Schnorr signature is verified (the authorship proof); editions are grouped
539/// per entity and version-folded to a head (from scratch, floor 0). A head is folded into the trusted
540/// roster ONLY if it is **chain-anchored** (not gapped — fail closed on withheld history) AND
541/// its `entity_id` **binds** to its content (a Role lives at `entity_id == role_id`, a Grant at
542/// `entity_id == grant_locator(community_id, member)`) — so a signed edition can't relabel
543/// itself to a different role/member. Anything else is dropped and counted in `skipped`. The grant
544/// binding is keyed by the **community id** (stable across a base rotation), so a re-anchored grant
545/// folds under any epoch's root — the keystone for re-anchoring.
546///
547/// `floors` is each entity's persisted head (`entity_hex → (version, self_hash)`, from
548/// [`crate::db::community::get_all_edition_heads`]) — the refuse-downgrade FLOOR. Each entity's
549/// chain is folded from ITS held floor, not from scratch, so a withholding relay serving editions below
550/// what we already hold can't roll an authority chain back (the attack: resurrecting a since-revoked
551/// admin's old grant by withholding the revocation). An entity absent from `floors` (a bootstrapping
552/// joiner) folds from genesis (floor 0); an empty map = a fresh joiner. A relay that serves only
553/// below-floor editions for an entity yields no head for it → that entity is simply absent from this
554/// fold (fail closed: it is not re-authorized off a rolled-back view; it self-heals when a relay serves
555/// ≥ floor).
556///
557/// This does NOT apply delegation-chain authorization — the signature proves WHO; deciding WHETHER
558/// they were allowed (rank + chain to the owner) is a separate, later layer.
559pub 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    // Verify + parse; drop (and count) anything that doesn't. The cap bounds verify work.
567    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    // tombstone detection — owner-signature-filtered, NOT position/version dependent. The dissolved
580    // tombstone has no version chain, so it does NOT route through the per-entity `version::fold` below;
581    // instead scan the locator directly and collect EVERY well-formed signer (the caller keeps only the
582    // proven owner). Listing all signers means a flood of forged non-owner editions can't bury the owner's.
583    // A vsk=10 edition at the WRONG entity_id (relabel attempt) is ignored here and dropped/skipped below.
584    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        // vsk=10 is chain-free and handled by the scan above; keep it out of the version-chain fold (a
595        // no-prev v1 would otherwise just no-op, but excluding it makes the exemption explicit).
596        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        // Seed the fold from this entity's PERSISTED head (refuse-downgrade floor), not from
622        // scratch — so a relay serving editions below what we hold can't roll the chain back.
623        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        // head selection by client mode. No gap → the chain-anchored head. Gap → the chain isn't
628        // anchored to genesis/floor; two modes:
629        //   - TRACKING (floor > 0): fail CLOSED — an unanchored tail can be a rollback/fork. Keep the
630        //     held floor; the entity is flagged pending for a re-fetch from the union.
631        // - BOOTSTRAPPING (floor == 0): a fresh joiner whose genesis was re-anchored away cannot
632        //     verify lineage, so accept the HIGHEST signed head (Policy B) and let AUTHORITY be the gate —
633        //     each VSK arm below re-checks the head's coordinate, and the caller runs is_authorized /
634        //     authorize_delegation. The head's signature is already verified (parse_edition_inner), so a
635        //     relay can't forge a higher version, only withhold/replay older valid ones — covered by the
636        //     relay union (Vector ships multiple trusted relays) + the refuse-downgrade floor.
637        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                // Tracking + gap. Authority records (roles/grants/banlist) FAIL CLOSED — an unanchored
643                // tail can be a rollback/fork, and converging authority off a withheld view is a
644                // relay-choosable censorship lever. DISPLAY metadata (GroupRoot, channel) is exempt:
645                // it carries no authority, the consumer's `is_authorized` filter is the real gate, and
646                // the consumer's refuse-downgrade floor still blocks any sub-floor rollback. So surface
647                // the ≥floor candidates (the per-version winner at the highest version) and let the
648                // consumer author-gate + converge a same-version fork. A withheld-history gap here at
649                // worst forward-jumps the displayed name to a validly-signed authorized edit.
650                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, // not display metadata → fail closed
653                }
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                // The edition coordinate IS the role id (d-tag = role_id), so the content can't
671                // claim to be a different (e.g. higher-powered) role than the entity it lives at.
672                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                // The grant's coordinate IS its member's opaque locator, so the content
681                // can't relabel the grant to a different member, and two entities can't claim one
682                // member (they'd share the locator → group + fold, not duplicate).
683                Ok(grant)
684                    if hex32(&grant.member)
685                        .is_some_and(|m| super::derive::grant_locator(community_id, &m) == entity_id) =>
686                {
687                    // Record the head even for an empty grant (a revoke is a real chain advance); the
688                    // empty grant just folds to "no roster entry" — don't carry a husk.
689                    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                // The banlist lives at the single community-wide locator; its content is the banned set.
699                // Only meaningful once the caller checks `banlist_author` held BAN (the authority gate).
700                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                // A creator's OWN link list, bound to its author's coordinate (so one creator can't
715                // publish links under another's identity). Content is that creator's active locators.
716                // Meaningful once the caller checks `creator` held CREATE_INVITE (the authority gate),
717                // then UNIONS authorized creators' locators into the aggregate active-set.
718                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                // GroupRoot lives at the community's own coordinate; its content is the community
735                // descriptor. Expose ALL gap-vetted candidates at-or-above the floor (fork members
736                // included — B1b), highest version first with the deterministic inner-id tiebreak,
737                // so the consumer can author-aware-scan: a same-version forgery can't orphan an
738                // authorized re-assert via the author-blind tiebreak. Contiguity stays author-blind
739                // here; the caller checks `MANAGE_METADATA` per candidate.
740                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                // ChannelMetadata lives at the channel's own coordinate (`entity_id == channel_id`), so its
773                // content can't relabel itself to a different channel. Mirror GroupRoot: expose ALL gap-
774                // vetted candidates at-or-above the floor (fork members included), highest version first with
775                // the inner-id tiebreak, so the consumer author-aware-scans + converges a same-version fork.
776                // Contiguity stays author-blind here; the caller checks MANAGE_CHANNELS per candidate.
777                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                    // Author-blind top stays in channel_meta (back-compat); the full list drives convergence.
803                    channel_meta.push(cands[0].2.clone());
804                    channel_candidates.extend(cands.into_iter().map(|(_, _, c)| c));
805                }
806            }
807            _ => {} // other entity types (role-order/...) handled elsewhere
808        }
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
819/// Filter a [`fold_roster`] result by the **delegation chain** → the AUTHORIZED roster. Binding
820/// + a valid signature prove an edition was *well-formed and authentic*, NOT that its signer was
821/// *allowed* to make it — without this layer a member could self-sign an Admin grant and have every
822/// peer fold it into their roster. Here an entry is trusted only if its signer was authorized:
823///
824/// - a **role** at position `P` is kept only if its signer can act on `P` with `MANAGE_ROLES` (strictly
825///   outranks `P`; the owner — position 0 via the attestation — is supreme);
826/// - a **grant** of roles `[R..]` to member `M` is kept only if its signer outranks every granted role's
827///   position AND outranks `M`, with `MANAGE_ROLES`.
828///
829/// Authority is resolved against the roster built SO FAR, so this is a fixpoint seeded by the owner:
830/// owner-signed entries are accepted first (they add admins), then admin-signed entries those admins are
831/// allowed to make, until stable. An entry whose signer never becomes authorized is dropped — that is
832/// the self-promotion / forged-delegation defense. `owner_hex == None` (unproven community, no root)
833/// yields an EMPTY roster (fail closed: no anchor, no authority).
834///
835/// Authority is evaluated against the CURRENT folded roster. With [`fold_roster`] now seeding each
836/// entity from its persisted refuse-downgrade FLOOR, that "current" roster is rollback-protected:
837/// a relay can't resurrect a since-revoked admin's grant to re-validate the chain. So the 
838/// version-pinned guarantee for the DELEGATION plane falls out here without a separate citation check —
839/// a demoted signer drops out of the accepted roster, and every edition they signed drops with them
840/// (refuse-superseded). The `vac` citation is the mechanism for the ACTION plane (ban/hide), where the
841/// actor is NOT the entity being folded; on the delegation plane the signer's authorizing grant IS a
842/// folded entity, so the floor-fold + this fixpoint already pin it. Grant editions still CARRY a `vac`
843/// (emitted by `set_member_grant`) for the audit log + forward-compat, but this consumer deliberately
844/// does not read it — the rank-chain over the floor-protected roster is the authority decision. The one behavior NOT provided is
845/// point-in-time PRESERVATION (keeping grants a since-demoted admin made while authorized) — that is the
846/// less-safe direction, needs a roster-wide snapshot version, and is deferred; the current fail-safe
847/// drop is correct and safer.
848pub 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        // A role is authorized if its signer can define a role at that position (outrank + MANAGE_ROLES).
858        for (i, role) in folded.roles.roles.iter().enumerate() {
859            if role_done[i] {
860                continue;
861            }
862            // Position 0 is reserved to the owner-attestation chain — no RoleMetadata may occupy
863            // it, even owner-signed (the owner's authority is the attestation, not a pos-0 role). Reject.
864            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        // A grant is authorized if its signer outranks every granted role's position AND the member,
877        // with MANAGE_ROLES. Granted-role positions are resolved from the ALREADY-accepted roles, so a
878        // grant referencing a not-yet-accepted role defers to a later round (or is dropped if its role
879        // never authorizes). This is the escalation defense: you can't grant a role you don't outrank.
880        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 }; // a granted role not yet accepted → defer
887            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
907/// version-pinned authority **completeness** check for an action that carries an
908/// [`edition::AuthorityCitation`] (a ban, a hide, a delegated grant). The action names the authorizing
909/// grant edition it claims authority under; this confirms we have folded a COMPLETE, un-forked view of
910/// that grant — synced to AT LEAST the cited version, with the cited hash matching ours at equality.
911///
912/// It does NOT decide the permission/outrank — that stays with the caller's `can_act_on_member` against
913/// the CURRENT authorized roster, so a since-demoted actor is refused there (refuse-superseded falls out:
914/// we hold a later head of their grant that dropped the role). This check is the orthogonal half: it
915/// guarantees the verdict is computed over the same authority view the actor cited, not a stale or
916/// partial one.
917///
918/// `heads` is the fold's per-entity head set ([`FoldedRoster::heads`]) — the haystack the cited grant
919/// must appear in. `actor_grant_hex` is the entity coordinate of the ACTOR's OWN authorizing grant
920/// (`grant_locator(community_id, actor)`, lowercase hex): the citation MUST name it, so an actor can't
921/// borrow completeness by citing some other synced edition. Returns true iff:
922///   - the actor is the proven owner (supreme — cites nothing), OR
923///   - the citation names the actor's own grant AND we surfaced it at version ≥ the cited version, and —
924///     when our head is EXACTLY the cited version — the cited hash equals ours (a cited fork is rejected).
925///
926/// A non-owner who cited nothing, cited a foreign entity, or whose cited grant we have NOT folded up to
927/// the cited version (a withholding relay, or we are simply behind), returns false: FAIL CLOSED — never
928/// act on an incomplete authority view (the §"never act on a partial view" tenet). The block-until-synced
929/// re-fetch escalation across a relay quorum is deferred (one signature suffices, per the MVP directive);
930/// MVP fails closed here and self-heals on the next sync once the cited grant arrives.
931pub 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    // The citation must name the ACTOR's OWN authorizing grant — citing a foreign synced edition can't
944    // borrow completeness (the permission check keys on the actor, but pinning to their grant keeps the
945    // sync-floor honest and is the coordinate the delegation verifier will resolve rank at).
946    if entity_hex != actor_grant_hex {
947        return false;
948    }
949    match heads.iter().find(|h| h.entity_hex == entity_hex) {
950        // We hold a LATER edition of the actor's grant than they cited — synced past it. Whether the
951        // actor is STILL authorized is the caller's roster check (which reflects this later head).
952        Some(h) if h.version > c.version => true,
953        // Synced to exactly the cited version: the cited hash must be the one that won our fold (else
954        // the actor cited a non-canonical fork of their own grant).
955        Some(h) if h.version == c.version => h.self_hash == c.edition_hash,
956        // Not surfaced, or our head is BEHIND the cited version → we cannot confirm the authority.
957        _ => 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); // the actor's own grant locator
977        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        // Owner is supreme and cites nothing.
983        assert!(authority_citation_satisfied(&[], Some(&owner), &owner, &eh, None));
984        // A non-owner with no citation fails closed.
985        assert!(!authority_citation_satisfied(&head(3, hash), Some(&owner), &actor, &eh, None));
986        // Synced to exactly the cited version with the matching hash → satisfied.
987        assert!(authority_citation_satisfied(&head(3, hash), Some(&owner), &actor, &eh, Some(&cite)));
988        // Same version, wrong hash (a cited fork at the tip) → rejected.
989        assert!(!authority_citation_satisfied(&head(3, [0xEE; 32]), Some(&owner), &actor, &eh, Some(&cite)));
990        // We hold a LATER head of the actor's grant → synced past it (the roster check handles supersession).
991        assert!(authority_citation_satisfied(&head(4, [0x77; 32]), Some(&owner), &actor, &eh, Some(&cite)));
992        // We are BEHIND the cited version → fail closed.
993        assert!(!authority_citation_satisfied(&head(2, hash), Some(&owner), &actor, &eh, Some(&cite)));
994        // The cited grant isn't surfaced at all → fail closed.
995        assert!(!authority_citation_satisfied(&[], Some(&owner), &actor, &eh, Some(&cite)));
996        // The citation names a FOREIGN entity (not the actor's grant) → rejected even if surfaced.
997        let foreign = "ff".repeat(32);
998        assert!(!authority_citation_satisfied(&head(3, hash), Some(&owner), &actor, &foreign, Some(&cite)));
999    }
1000    // The community id binds grant coordinates (stable across rotation); fold + grant builders take it.
1001    fn cid() -> CommunityId {
1002        CommunityId([0x09; 32])
1003    }
1004
1005    /// A role edition at its bound coordinate (entity_id == role_id).
1006    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    /// A grant edition at its bound coordinate (entity_id == grant_locator(community_id, member)).
1022    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    /// A GroupRoot (community metadata) edition at the community's own coordinate (entity_id == community_id).
1034    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    /// TRACKING (floor > 0): a lone forged high-version edition with withheld history is QUARANTINED —
1080    /// flagged gapped AND kept out of the trusted roster. This is the fail-closed defense, and it
1081    /// stays fully intact for any client that already holds a floor (rollback/fork protection).
1082    #[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        // We already hold this entity at v3, so a lone v5 with a non-linking prev is an unanchored tail.
1089        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    /// BOOTSTRAPPING (floor == 0, Policy B): a fresh joiner whose genesis was re-anchored away SURFACES
1097    /// the highest signed head despite the gap — but AUTHORITY still gates it. An owner-authored head is
1098    /// authorized; an unauthorized author's (equally surfaced) head is dropped by `authorize_delegation`.
1099    /// This is the narrow, deliberate relaxation of the fail-closed rule at first contact only.
1100    #[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        // Owner-authored position-1 role at v5 with no anchor (genesis re-anchored away) → gapped @ floor 0.
1107        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        // Same shape, authored by a STRANGER → still surfaced into the raw fold, but authority drops it.
1114        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    /// Fresh-joiner-after-rotation: a bootstrapping joiner (floor 0) recovers the FULL plane across version
1123    /// holes. It surfaces the latest admin-authored GroupRoot AND the owner's gapped (no-v1) grant, and
1124    /// authority folds the admin in so that admin's metadata is authorized.
1125    #[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        // Admin role def — owner-signed, position 1, admin perms (incl MANAGE_METADATA), contiguous v1.
1134        let role = role_event(&owner, &role_id, 1, 1, None, 100);
1135        // Owner grants the admin that role — but the grant chain is GAPPED (starts at v2, no v1 re-anchored).
1136        let (grant, _, _) = grant_event(&owner, &admin_hex, vec![role_id.clone()], 2, Some(&[0x99u8; 32]), 110);
1137        // GroupRoot: owner genesis v1, then the admin's edit at v11 with a hole below it (no v2..v10).
1138        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        // Bootstrapping surfaced the LATEST GroupRoot (v11, admin-authored) across the gap, not the genesis.
1144        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        // Authority folds the gapped grant → the admin holds MANAGE_METADATA, so the v11 edit IS authorized.
1149        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    /// Forgery-resistance under Policy B: a bootstrapping joiner SURFACES even a stranger's lone high-version
1157    /// GroupRoot (its signature is valid), but AUTHORITY refuses it — `is_authorized` is false for the
1158    /// unauthorized author, so the caller never applies it. Policy B trusts signature + authority, never a
1159    /// bare version number. (A relay can't forge a higher version; the worst it does is surface a non-author.)
1160    #[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        // Surfaced (highest signed head) — but authored by the stranger, who holds no authority.
1170        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    /// Publish-time authority (B1b): on a FRESH fold (no floor), a demoted admin's editions — incl. a
1179    /// same-version FORGERY sharing the owner's re-assert version — are skipped by the author-aware scan,
1180    /// and the owner's re-assert wins. The candidate set exposes BOTH v3 fork members so the forgery can't
1181    /// orphan the re-assert via the author-blind tiebreak. (This is the gauntlet's convergence demand.)
1182    #[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        // Owner: admin role (MANAGE_METADATA); grant Alice; then REVOKE Alice (chained grant→revoke).
1191        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        // GroupRoot: owner genesis; Alice's round-1 (while admin); then a v3 FORK — Alice's forgery vs the
1197        // owner's re-assert of Alice's content (arbitrary prevs → bootstrap path = the fresh-joiner case).
1198        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        // Both v3 fork members are exposed (so the forgery can't orphan the re-assert).
1212        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        // The author-aware descending scan (what the consumer runs) picks the owner's re-assert; Alice's
1216        // forgery (v3) and round-1 (v2) are skipped because she's revoked.
1217        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    /// A stranger publishes a high-version GRANT making themselves admin. Bootstrapping surfaces it
1225    /// into the raw fold (its signature is valid), but `authorize_delegation` drops it — it never chains to
1226    /// the owner. The grant analogue of `bootstrapping_surfaces_a_gapped_head_but_authority_gates`.
1227    #[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        // Owner-defined Admin role (so the role itself is legit) + a stranger-signed grant of it to themselves.
1235        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    /// ONE fold, the full per-entity floor x per-VSK matrix. A tracking GroupRoot on a gapped tail
1246    /// is now SURFACED (display metadata carries no authority — the refuse-downgrade floor still blocks
1247    /// any sub-floor rollback, so a gap at worst forward-jumps the name to a validly-signed authorized
1248    /// edit); a tracking AUTHORITY record (grant) on a gapped tail stays QUARANTINED (converging authority
1249    /// off a withheld view is a censorship lever); a bootstrapping (floor-0) grant is surfaced. The
1250    /// per-entity decision is the crux of mixed-mode correctness.
1251    #[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        // GroupRoot present only at v11 with a non-linking prev — we HOLD it at v5 (tracking, gapped).
1258        let groot_v11 = groot_event(&owner, "Tracked", "held-at-v5", 11, Some(&[0xABu8; 32]), 200);
1259        // A gapped grant for a member we've NEVER seen (floor 0 → bootstrapping → surfaced).
1260        let (fresh_grant, _, _) = grant_event(&owner, &fresh_member, vec![role_id.clone()], 2, Some(&[0x99u8; 32]), 110);
1261        // A gapped grant for a member we DO track at v5 (tracking authority → quarantined).
1262        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); // contiguous, folds normally
1264
1265        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]));           // GroupRoot held at v5 → tracking
1270        floors.insert(tracked_grant_hex, (5u64, [0x66u8; 32])); // tracked member's grant held at v5 → tracking
1271        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    /// An empty grant (a revoke) advances the entity's head (a real chain step) but carries NO roster
1279    /// entry — the husk must not linger as a phantom grant.
1280    #[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); // revoke = empty grant
1288        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    /// Junk-resilience: a hostile relay interleaves malformed editions with a valid one. `fold_roster`
1297    /// must SKIP the junk (counting it), still fold the valid role, and never panic.
1298    #[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        // Junk A: a role edition whose CONTENT is garbage JSON → parses, but the VSK_ROLE decode fails → skipped.
1305        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        // Junk B: a role whose content claims a DIFFERENT role_id than its entity coordinate → skipped (binding).
1310        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    /// A role edition whose content claims a role_id different from its entity coordinate is rejected
1323    /// — a signed edition can't relabel itself to a more powerful role.
1324    #[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), // content claims a..a
1329            name: "X".into(),
1330            position: 0,
1331            permissions: Permissions::admin(),
1332            scope: RoleScope::Server,
1333            color: 0,
1334        };
1335        let wrong_eid = [0x12u8; 32]; // but the edition lives at a different coordinate
1336        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    /// A grant for member M placed at an entity_id that isn't M's locator is rejected — closes the
1345    /// "forged second grant for M re-adds revoked roles" union vector (H3).
1346    #[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]; // not grant_locator(cid, member)
1352        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    /// The fold is independent of input order (deterministic convergence across clients).
1361    #[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    /// The PUBLIC send-side builders produce editions the consumer fold accepts cleanly — the
1375    /// producer↔consumer loop closes (bound coordinates, anchored genesis, valid signatures).
1376    #[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    /// Mis-shaped chains fail LOUD at mint (W1) — not silently quarantined at fold.
1401    #[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        // v1 with a prev_hash, and v>1 without one, are both rejected at build time.
1409        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    /// Bad role_id / member hex is an Err, never a panic.
1417    #[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    /// A genuine producer-built v1→v2 grant chain folds to v2 (mirrors the consumer test, but the
1430    /// editions come from the public builders — catches any drift between what they sign and what
1431    /// `edition_hash` expects as the next `prev_hash`).
1432    #[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        // The next edition cites v1's edition_hash over the SAME bytes the builder signed.
1442        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    /// The FULL control pipeline: build → seal under server-root → (wire) → open → parse → fold.
1457    #[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        // The wire event hides the real author (ephemeral outer) and the content (encrypted).
1471        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        // A wrong server-root can't open it — also how cross-community replay is rejected.
1475        assert!(open_control_edition(&outer, &ServerRootKey([0xAA; 32])).is_err());
1476
1477        // Members open → parse → fold: the role lands, real authorship preserved.
1478        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    /// Round-trip at a NON-ZERO epoch. The edition must be sealed at the epoch's pseudonym, NOT Epoch(0):
1486    /// sealing at the wrong epoch lands it at the wrong `#z` and a fresh joiner at epoch 4 would never find it.
1487    #[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        // Distinct epochs address distinct pseudonyms, and the edition sits at epoch 4's — not epoch 0's.
1499        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        // A fresh joiner (empty floors) at epoch 4 still opens + folds it.
1508        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    /// Golden vector — the control-plane pseudonym is a wire coordinate other clients must reproduce
1514    /// byte-for-byte, so pin it against fixed inputs.
1515    #[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    /// `open_control_edition` rejects a non-control outer (defensive kind check).
1526    #[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    // --- delegation-chain authorization (#2) ---
1535
1536    #[test]
1537    fn delegation_rejects_a_self_signed_admin_grant() {
1538        // THE fail-open being closed: a member self-signs a grant giving herself Admin. It is validly
1539        // signed + bound (so it FOLDS), but authorization must DROP it — she never chained to the owner.
1540        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); // owner creates Admin (pos 1)
1544        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        // Raw fold (binding layer) trusts it — proving authorization is the thing doing the work here.
1547        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        // owner → Alice (Admin, pos 1); Alice creates Mod (pos 3) and grants Bob — a legit 3-deep chain.
1572        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); // Alice (admin) creates a lower role
1579        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        // version-pinned delegation, proven SUBSUMED by the floor-fold + current-roster fixpoint (no
1592        // `vac` citation on the delegation plane): owner→Alice (admin v1), Alice→Bob (mod). The owner then
1593        // REVOKES Alice (her grant v2 = empty). After the fold her grant head is the empty revoke, so she
1594        // is not an authorized admin → her delegated grant of Bob is DROPPED (refuse-superseded).
1595        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); // revoke
1603        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        // The refuse-downgrade FLOOR protects the DELEGATION plane too: we already hold Alice's grant at
1618        // v2 (her revoke), but a withholding relay re-serves only her v1 admin grant. v1 is below the
1619        // floor → refused → Alice is absent from the fold → her delegated grant of Bob is dropped. This
1620        // is the delegation-plane analogue of the banlist `withheld_revocation` test.
1621        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])); // held floor = v2
1632        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        // escalation defense: a position-1 admin cannot grant another position-1 Admin (1 !< 1).
1644        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        // The headline adversarial case: A grants B Admin, B grants A Admin — neither chains to the
1665        // owner. The fixpoint cannot bootstrap a cycle (no owner-rooted seed), so NEITHER is accepted.
1666        let owner = Keys::generate(); // a real owner exists but is NOT party to these grants
1667        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        // Position 0 is reserved to the owner attestation. A role at pos 0 is rejected regardless
1684        // of signer — a non-owner can't outrank pos 0, and even the owner may not mint a pos-0 ROLE.
1685        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        // No owner attestation → no root of the chain → nothing is authorized (fail closed).
1702        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}