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        .finalize(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/// `VectorSigner` (`unsigned.finalize_async(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        .finalize(actor)
115        .map_err(|e| format!("sign grant edition: {e}"))
116}
117
118/// The UNSIGNED Grant edition (the bunker path); sign with the active `VectorSigner`.
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        .finalize(actor)
151        .map_err(|e| format!("sign banlist edition: {e}"))
152}
153
154/// The UNSIGNED Banlist edition (the bunker path); sign with the active `VectorSigner`.
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        .finalize(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 `VectorSigner`. 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    build_group_dissolved_edition_unsigned_with_content(author, community_id, created_at, "{}")
194}
195
196/// The tombstone with explicit content — the v1→v2 migration carrier (§migration). The
197/// content is covered by the inner owner signature; shipped clients never parse it, so a
198/// payload-bearing tombstone folds as a plain dissolution everywhere pre-migration.
199pub fn build_group_dissolved_edition_unsigned_with_content(
200    author: PublicKey,
201    community_id: &CommunityId,
202    created_at: u64,
203    content: &str,
204) -> UnsignedEvent {
205    let entity_id = super::derive::dissolved_locator(community_id);
206    // Chain-free terminal marker: fixed v1, no prev_hash.
207    edition::build_edition_inner(author, VSK_DISSOLVED, &entity_id, 1, None, content, created_at, None)
208}
209
210/// Signed convenience twin of [`build_group_dissolved_edition_unsigned_with_content`] (local-keys path).
211pub fn build_group_dissolved_edition_with_content(
212    actor: &Keys,
213    community_id: &CommunityId,
214    created_at: u64,
215    content: &str,
216) -> Result<Event, String> {
217    build_group_dissolved_edition_unsigned_with_content(actor.public_key(), community_id, created_at, content)
218        .finalize(actor)
219        .map_err(|e| format!("sign dissolved edition: {e}"))
220}
221
222/// Build a signed **InviteLinks** edition (vsk=8) at the CREATOR's own coordinate
223/// (`entity_id == invite_links_locator(community_id, actor)`). Content is the JSON array of THAT
224/// creator's active public-invite-link LOCATORS (lowercase hex; the locator is public — the token in the
225/// URL is the secret, never listed). Per-creator: a creator publishes only their own list; members
226/// fold every creator's list (gated on the author holding `CREATE_INVITE`) into the aggregate active-set,
227/// the source of truth for the Public/Private mode + registry-authoritative joins. No shared registry.
228pub fn build_invite_links_edition(
229    actor: &Keys,
230    community_id: &CommunityId,
231    link_locators: &[String],
232    version: u64,
233    prev_hash: Option<&[u8; 32]>,
234    created_at: u64,
235    authority: Option<&edition::AuthorityCitation>,
236) -> Result<Event, String> {
237    build_invite_links_edition_unsigned(actor.public_key(), community_id, link_locators, version, prev_hash, created_at, authority)?
238        .finalize(actor)
239        .map_err(|e| format!("sign invite-links edition: {e}"))
240}
241
242/// The UNSIGNED InviteLinks edition (the bunker path); sign with the active `VectorSigner`. The entity
243/// coordinate binds to `author`, so a creator can only publish links at their own coordinate.
244pub fn build_invite_links_edition_unsigned(
245    author: PublicKey,
246    community_id: &CommunityId,
247    link_locators: &[String],
248    version: u64,
249    prev_hash: Option<&[u8; 32]>,
250    created_at: u64,
251    authority: Option<&edition::AuthorityCitation>,
252) -> Result<UnsignedEvent, String> {
253    check_chain_shape(version, prev_hash)?;
254    let entity_id = super::derive::invite_links_locator(community_id, &author.to_bytes());
255    let content = serde_json::to_string(link_locators).map_err(|e| e.to_string())?;
256    Ok(edition::build_edition_inner(author, VSK_INVITE_LINKS, &entity_id, version, prev_hash, &content, created_at, authority))
257}
258
259/// Build a signed **GroupRoot** edition (vsk=0) at the community-wide coordinate (`entity_id ==
260/// community_id`) — the Community's display descriptor (name/description/icon/banner + owner
261/// attestation). Real-npub signed; the consumer applies it only if the signer held `MANAGE_METADATA`.
262pub fn build_community_root_edition(
263    actor: &Keys,
264    community_id: &CommunityId,
265    meta: &super::metadata::CommunityMetadata,
266    version: u64,
267    prev_hash: Option<&[u8; 32]>,
268    created_at: u64,
269    authority: Option<&edition::AuthorityCitation>,
270) -> Result<Event, String> {
271    build_community_root_edition_unsigned(actor.public_key(), community_id, meta, version, prev_hash, created_at, authority)?
272        .finalize(actor)
273        .map_err(|e| format!("sign community-root edition: {e}"))
274}
275
276/// The UNSIGNED GroupRoot edition (the bunker path); sign with the active `VectorSigner`.
277pub fn build_community_root_edition_unsigned(
278    author: PublicKey,
279    community_id: &CommunityId,
280    meta: &super::metadata::CommunityMetadata,
281    version: u64,
282    prev_hash: Option<&[u8; 32]>,
283    created_at: u64,
284    authority: Option<&edition::AuthorityCitation>,
285) -> Result<UnsignedEvent, String> {
286    check_chain_shape(version, prev_hash)?;
287    let content = serde_json::to_string(meta).map_err(|e| e.to_string())?;
288    Ok(edition::build_edition_inner(author, VSK_COMMUNITY_ROOT, &community_id.0, version, prev_hash, &content, created_at, authority))
289}
290
291/// Build a signed **ChannelMetadata** edition (vsk=2) at the channel's coordinate (`entity_id ==
292/// channel_id`) — the channel's display descriptor (name). Real-npub signed; the consumer applies it
293/// only if the signer held `MANAGE_CHANNELS`.
294pub fn build_channel_metadata_edition(
295    actor: &Keys,
296    channel_id: &ChannelId,
297    meta: &super::metadata::ChannelMetadata,
298    version: u64,
299    prev_hash: Option<&[u8; 32]>,
300    created_at: u64,
301    authority: Option<&edition::AuthorityCitation>,
302) -> Result<Event, String> {
303    build_channel_metadata_edition_unsigned(actor.public_key(), channel_id, meta, version, prev_hash, created_at, authority)?
304        .finalize(actor)
305        .map_err(|e| format!("sign channel-metadata edition: {e}"))
306}
307
308/// The UNSIGNED ChannelMetadata edition (the bunker path); sign with the active `VectorSigner`.
309pub fn build_channel_metadata_edition_unsigned(
310    author: PublicKey,
311    channel_id: &ChannelId,
312    meta: &super::metadata::ChannelMetadata,
313    version: u64,
314    prev_hash: Option<&[u8; 32]>,
315    created_at: u64,
316    authority: Option<&edition::AuthorityCitation>,
317) -> Result<UnsignedEvent, String> {
318    check_chain_shape(version, prev_hash)?;
319    let content = serde_json::to_string(meta).map_err(|e| e.to_string())?;
320    Ok(edition::build_edition_inner(author, VSK_CHANNEL, &channel_id.0, version, prev_hash, &content, created_at, authority))
321}
322
323/// The relay-filterable pseudonym members use to fetch the control plane for `(community, epoch)`.
324/// Derived from the server-root key + community id, so members compute it but outsiders (no
325/// server-root) can't — the control plane has no stable on-wire identifier. This reuses the
326/// `channel-pseudonym` derivation; what keeps a control pseudonym from ever aliasing a channel's is
327/// the invariant that the server-root key is always distinct from every channel key (the HKDF
328/// label is shared, so domain separation rests on the distinct IKM + id32).
329pub fn control_pseudonym(server_root: &ServerRootKey, community_id: &CommunityId, epoch: Epoch) -> String {
330    channel_pseudonym(&ChannelKey(*server_root.as_bytes()), &ChannelId(community_id.0), epoch).to_hex()
331}
332
333/// Seal a signed control edition (kind 3308) for the wire: encrypt the inner under the **server-root
334/// key** (only members decrypt), with an **ephemeral outer signer** (the real author is the inner
335/// signature, hidden from relays), addressed by the control-plane pseudonym so members fetch by
336/// `#z` without exposing a stable group identifier.
337pub fn seal_control_edition(
338    ephemeral: &Keys,
339    inner: &Event,
340    server_root: &ServerRootKey,
341    community_id: &CommunityId,
342    epoch: Epoch,
343) -> Result<Event, String> {
344    if inner.kind.as_u16() != event_kind::COMMUNITY_CONTROL {
345        return Err("a control edition must be kind 3308".to_string());
346    }
347    let content = cipher::seal(server_root.as_bytes(), inner.as_json().as_bytes())
348        .map_err(|e| format!("seal control edition: {e}"))?;
349    let pseudonym = control_pseudonym(server_root, community_id, epoch);
350    EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_CONTROL), content)
351        .tags([
352            Tag::custom("z", [pseudonym]),
353            Tag::custom("v", ["1".to_string()]),
354        ])
355        .finalize(ephemeral)
356        .map_err(|e| format!("sign control outer: {e}"))
357}
358
359/// Open a control-edition outer → its inner edition event (decrypt under the server-root key). Does
360/// NOT verify the inner signature or parse fields — pass the result to [`edition::parse_edition_inner`]
361/// (which verifies authorship) and then [`fold_roster`] (which binds + folds). A wrong server-root
362/// key fails to decrypt → `Err`, which is also how **cross-community** replay is rejected.
363///
364/// CROSS-EPOCH replay (same community) is NOT an envelope concern and is deliberately not blocked
365/// here: the control plane is encrypted under the (epoch-agnostic) server-root key, so any edition
366/// can be re-wrapped under any epoch's pseudonym — the envelope cannot bind an epoch, and binding the
367/// inner to an epoch would break re-anchoring (a re-WRAP, not a re-sign). It is a COMPLETENESS-layer
368/// defense: a *tracking* client is protected by the version chain's refuse-downgrade; a
369/// *bootstrapping* joiner relies on quorum reconciliation + the re-anchoring guarantee (the current
370/// head is re-posted under the current epoch). Those MUST be wired into the fetch path (the rekey
371/// increment) before a fresh joiner is safe against a withheld-demotion replay. The single-epoch MVP
372/// (never rotates) is unaffected.
373pub fn open_control_edition(outer: &Event, server_root: &ServerRootKey) -> Result<Event, String> {
374    if outer.kind.as_u16() != event_kind::COMMUNITY_CONTROL {
375        return Err("not a control-plane outer (kind != 3308)".to_string());
376    }
377    match outer.tags.iter().find_map(|t| {
378        let s = t.as_slice();
379        (s.len() >= 2 && s[0] == "v").then(|| s[1].clone())
380    }) {
381        Some(v) if v == "1" => {}
382        other => return Err(format!("unsupported control edition version: {other:?}")),
383    }
384    let plaintext = cipher::open(server_root.as_bytes(), &outer.content)
385        .map_err(|e| format!("open control edition: {e}"))?;
386    let json = String::from_utf8(plaintext).map_err(|e| format!("control inner utf8: {e}"))?;
387    let inner = Event::from_json(&json).map_err(|e| format!("control inner parse: {e}"))?;
388    if inner.kind.as_u16() != event_kind::COMMUNITY_CONTROL {
389        return Err("control inner is not kind 3308".to_string());
390    }
391    Ok(inner)
392}
393
394/// Seal a GroupDissolved tombstone for the wire at the ROTATION-STABLE coordinate: encrypt the
395/// inner under the community-id-derived `dissolved_envelope_key` (NOT the per-epoch server root) and
396/// address it by `dissolved_pseudonym` (NOT `control_pseudonym`), so it is discoverable + openable by any
397/// member or joiner at any epoch. Ephemeral outer signer (the owner is the inner signature). The tombstone
398/// is also published at the current `control_pseudonym` (a current-epoch fast path); this is the cross-epoch
399/// path that survives a concurrent re-founding.
400pub fn seal_dissolved_edition(ephemeral: &Keys, inner: &Event, community_id: &CommunityId) -> Result<Event, String> {
401    if inner.kind.as_u16() != event_kind::COMMUNITY_CONTROL {
402        return Err("a dissolved tombstone must be kind 3308".to_string());
403    }
404    let key = super::derive::dissolved_envelope_key(community_id);
405    let content = cipher::seal(&key, inner.as_json().as_bytes())
406        .map_err(|e| format!("seal dissolved edition: {e}"))?;
407    let pseudonym = super::derive::dissolved_pseudonym(community_id);
408    EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_CONTROL), content)
409        .tags([
410            Tag::custom("z", [pseudonym]),
411            Tag::custom("v", ["1".to_string()]),
412        ])
413        .finalize(ephemeral)
414        .map_err(|e| format!("sign dissolved outer: {e}"))
415}
416
417/// Open a wire event at the dissolved coordinate and, IF it is a well-formed GroupDissolved tombstone
418/// (`vsk=10` at `dissolved_locator`), return its inner real-npub signer. The CALLER decides validity by
419/// checking that signer equals the proven owner (fail-closed authority). `None` for anything else
420/// (wrong key, malformed, wrong vsk, relabelled entity_id).
421pub fn dissolved_tombstone_signer(outer: &Event, community_id: &CommunityId) -> Option<PublicKey> {
422    dissolved_tombstone_open(outer, community_id).map(|d| d.author)
423}
424
425/// One well-formed GroupDissolved tombstone at the community's dissolved locator: the
426/// signer (validity is the CALLER's owner check) plus the fields the migration payload
427/// selection needs. Content is opaque here — `migration::parse_migration_payload` decides
428/// whether it carries a pointer; a plain `{}` is still a full-fledged dissolution.
429#[derive(Clone)]
430pub struct DissolvedEdition {
431    pub author: PublicKey,
432    pub created_at: u64,
433    pub inner_id: [u8; 32],
434    pub content: String,
435}
436
437/// Payload-aware twin of [`dissolved_tombstone_signer`]: same validity gate, full record out.
438pub fn dissolved_tombstone_open(outer: &Event, community_id: &CommunityId) -> Option<DissolvedEdition> {
439    if outer.kind.as_u16() != event_kind::COMMUNITY_CONTROL {
440        return None;
441    }
442    let key = super::derive::dissolved_envelope_key(community_id);
443    let plaintext = cipher::open(&key, &outer.content).ok()?;
444    let inner = Event::from_json(&String::from_utf8(plaintext).ok()?).ok()?;
445    let p = edition::parse_edition_inner(&inner).ok()?;
446    (p.vsk == VSK_DISSOLVED && p.entity_id == super::derive::dissolved_locator(community_id)).then_some(
447        DissolvedEdition { author: p.author, created_at: p.created_at, inner_id: p.inner_id, content: p.content },
448    )
449}
450
451/// The folded current head of one control entity — what the caller persists via `set_edition_head`
452/// (the monotonic refuse-downgrade floor) and the send side reads to emit the next version.
453#[derive(Clone)]
454pub struct EntityHead {
455    /// The entity coordinate (role_id for roles, grant_locator for grants), lowercase hex.
456    pub entity_hex: String,
457    pub version: u64,
458    pub self_hash: [u8; 32],
459    /// The head edition's deterministic tiebreak key (the inner edition id). Used to resolve a
460    /// same-version concurrent fork: every client converges on the lower `inner_id` among authorized
461    /// editions, and the persisted head ranks by it so a same-version adopt only moves toward the min.
462    pub inner_id: [u8; 32],
463    /// The authority citation the head edition carried, if any — the actor's pinned proof of
464    /// the authority they claimed for this edit. `None` for an owner-signed edition (supreme, cites
465    /// nothing) or an uncited one. Verifiers resolve the actor's standing at the cited grant version
466    /// via [`authority_citation_satisfied`].
467    pub citation: Option<edition::AuthorityCitation>,
468}
469
470/// The outcome of folding the control plane.
471#[derive(Clone)]
472pub struct FoldedRoster {
473    /// The anchored, bound, validly-signed heads. This is "validly signed, anchored, and current" — NOT
474    /// yet "authorized." [`authorize_delegation`] is the next layer: it filters these by the 
475    /// delegation chain (each signer must outrank what it defines, chaining to the owner). Use the
476    /// authorized result for any authority decision; this raw roster is the binding-layer output.
477    pub roles: roles::CommunityRoles,
478    /// The signer (inner real-npub author) of each entry in `roles.roles`, SAME ORDER — so
479    /// [`authorize_delegation`] can check whether that signer was allowed to define the role.
480    pub role_authors: Vec<PublicKey>,
481    /// The signer of each entry in `roles.grants`, SAME ORDER (the delegation chain needs the granter).
482    pub grant_authors: Vec<PublicKey>,
483    /// Entity ids whose head is NOT chain-anchored (a gap, §version). These are **quarantined** — NOT
484    /// folded into `roles` (fail closed). A bootstrapping joiner re-verifies them via authority
485    /// before trusting; a tracking client refetches the missing prereqs.
486    pub gapped_entities: Vec<[u8; 32]>,
487    /// Count of editions dropped: bad signature, missing/duplicate fields, an `entity_id`↔content
488    /// mismatch, unparseable content, or beyond the cap. **Nonzero ⇒ the roster may be degraded**
489    /// (a role/grant could be silently missing), so the caller should refetch rather than trust it.
490    pub skipped: usize,
491    /// Count of RAW control editions the source fetch returned (before opening/folding). Set by
492    /// `fetch_control_folded`; `0` for a roster folded from a hand-supplied edition set (tests, prefolds).
493    /// `fetch_and_apply_control` surfaces it so an admin-write guard can tell "≥1 relay responded" (any
494    /// raw event) from total network isolation — without a second, throwaway probe fetch.
495    pub fetched: usize,
496    /// The current head `(entity_hex, version, self_hash)` of every successfully-bound ROLE/GRANT
497    /// entity, for the caller to advance `set_edition_head` (monotonic). Excludes the banlist (its head
498    /// is `banlist_head`, advanced by the banlist path) and gapped/quarantined entities.
499    pub heads: Vec<EntityHead>,
500    /// The folded banlist content (banned pubkeys, lowercase hex) — only meaningful once the BAN
501    /// authority of `banlist_author` is checked against the authorized roster. Empty if no banlist
502    /// edition folded (distinct from "an authored empty banlist," which the caller learns via
503    /// `banlist_head`/`banlist_author` being `Some`).
504    pub banned: Vec<String>,
505    /// The signer (inner real-npub author) of the folded banlist head, so the caller can verify they
506    /// held `BAN`. `None` if no banlist edition folded.
507    pub banlist_author: Option<PublicKey>,
508    /// The DISTINCT signers of every well-formed GroupDissolved tombstone (vsk=10) at `dissolved_locator`
509    /// Detection is owner-SIGNATURE-filtered, NOT position/version dependent: the fold scans the
510    /// locator directly (NOT via the version-chain `version::fold` — the tombstone has no chain) and lists
511    /// each authoring npub, so a flood of forged NON-owner editions can never bury the real owner's signer
512    /// out of the `MAX_CONTROL_EDITIONS` cap and truncate the true tombstone away. The CALLER treats the
513    /// community as dissolved ONLY if the proven owner is in this set (mirrors `banlist_author`'s BAN
514    /// check). A malformed edition at the locator is dropped (`skipped`), never honored.
515    pub dissolved_by: Vec<PublicKey>,
516    /// EVERY well-formed tombstone record at the locator (not signer-deduped) — the v1→v2
517    /// migration's pointer source. `migration::select_pointer` filters to the proven owner
518    /// and picks the newest PAYLOAD-CARRYING one; `dissolved_by` stays the seal signal.
519    pub dissolved_editions: Vec<DissolvedEdition>,
520    /// The banlist entity's current head, for the banlist path to advance `set_edition_head`.
521    pub banlist_head: Option<EntityHead>,
522    /// Folded per-creator invite-link sets (vsk=8, one per creator at `invite_links_locator(cid,
523    /// creator)`). Each holds that creator's active link locators + head. The caller authorizes each
524    /// `creator` (held `CREATE_INVITE`) and UNIONS the locators into the aggregate active-set — the 
525    /// source of truth for the Public/Private mode + registry-authoritative joins. No shared registry.
526    pub invite_link_sets: Vec<InviteLinkSet>,
527    /// The folded GroupRoot (community metadata, vsk=0 at `entity_id == community_id`). `None` if no
528    /// GroupRoot edition folded. Applied only once the caller checks `root_author` held `MANAGE_METADATA`.
529    pub root_meta: Option<super::metadata::CommunityMetadata>,
530    /// The signer of the folded GroupRoot head, so the caller can verify they held `MANAGE_METADATA`.
531    pub root_author: Option<PublicKey>,
532    /// The GroupRoot entity's current head, for the metadata path to advance `set_edition_head`.
533    pub root_head: Option<EntityHead>,
534    /// Every gap-vetted GroupRoot candidate at-or-above the floor (fork members included), highest
535    /// version first (deterministic inner-id tiebreak within a version). The consumer applies the
536    /// highest whose author is CURRENTLY authorized (`MANAGE_METADATA`) — an author-aware descending
537    /// scan (B1b), so a demoted author's edition (incl. a same-version forgery) can't be the head,
538    /// and a fresh fold converges to the highest authorized edition (the owner's re-assert). `root_head`
539    /// is `root_candidates[0]` (the author-blind head) for back-compat.
540    pub root_candidates: Vec<RootCandidate>,
541    /// Folded per-channel metadata (vsk=2, one per channel `entity_id == channel_id`) — the author-blind
542    /// top head per channel (back-compat: used by the demoted-author re-assert path). For convergence the
543    /// consumer scans [`Self::channel_candidates`] instead.
544    pub channel_meta: Vec<ChannelMetaHead>,
545    /// Every gap-vetted channel-metadata candidate at-or-above each channel's floor (fork members
546    /// included), highest version first with the deterministic inner-id tiebreak — the per-channel mirror
547    /// of [`Self::root_candidates`]. Grouped contiguously per channel (sorted within each channel). The
548    /// consumer applies, per channel, the highest whose author CURRENTLY holds `MANAGE_CHANNELS` — an
549    /// author-aware scan, so a demoted author's same-version forgery can't orphan an authorized re-assert
550    /// and a concurrent same-version rename converges to one deterministic winner on every client.
551    pub channel_candidates: Vec<ChannelMetaHead>,
552}
553
554/// A folded per-creator InviteLinks edition (vsk=8): the creator who signed it, their active link
555/// locators, and the version head. Applied only if `creator` held `CREATE_INVITE`.
556#[derive(Clone)]
557pub struct InviteLinkSet {
558    pub creator: PublicKey,
559    pub locators: Vec<String>,
560    pub head: EntityHead,
561}
562
563/// One gap-vetted GroupRoot candidate (vsk=0): its content, signer (for the authority gate), and head.
564/// The consumer scans `FoldedRoster::root_candidates` (highest version first) for the highest whose
565/// author currently holds `MANAGE_METADATA`.
566#[derive(Clone)]
567pub struct RootCandidate {
568    pub meta: super::metadata::CommunityMetadata,
569    pub author: PublicKey,
570    pub head: EntityHead,
571}
572
573/// A folded ChannelMetadata edition (vsk=2): the channel it addresses, its content, its signer (for the
574/// `MANAGE_METADATA` authority gate), and its version head (to advance `set_edition_head`).
575#[derive(Clone)]
576pub struct ChannelMetaHead {
577    pub channel_id: [u8; 32],
578    pub meta: super::metadata::ChannelMetadata,
579    pub author: PublicKey,
580    pub head: EntityHead,
581}
582
583/// Fold a set of (already-decrypted) inner edition events into the current roster.
584///
585/// Each edition's inner Schnorr signature is verified (the authorship proof); editions are grouped
586/// per entity and version-folded to a head (from scratch, floor 0). A head is folded into the trusted
587/// roster ONLY if it is **chain-anchored** (not gapped — fail closed on withheld history) AND
588/// its `entity_id` **binds** to its content (a Role lives at `entity_id == role_id`, a Grant at
589/// `entity_id == grant_locator(community_id, member)`) — so a signed edition can't relabel
590/// itself to a different role/member. Anything else is dropped and counted in `skipped`. The grant
591/// binding is keyed by the **community id** (stable across a base rotation), so a re-anchored grant
592/// folds under any epoch's root — the keystone for re-anchoring.
593///
594/// `floors` is each entity's persisted head (`entity_hex → (version, self_hash)`, from
595/// [`crate::db::community::get_all_edition_heads`]) — the refuse-downgrade FLOOR. Each entity's
596/// chain is folded from ITS held floor, not from scratch, so a withholding relay serving editions below
597/// what we already hold can't roll an authority chain back (the attack: resurrecting a since-revoked
598/// admin's old grant by withholding the revocation). An entity absent from `floors` (a bootstrapping
599/// joiner) folds from genesis (floor 0); an empty map = a fresh joiner. A relay that serves only
600/// below-floor editions for an entity yields no head for it → that entity is simply absent from this
601/// fold (fail closed: it is not re-authorized off a rolled-back view; it self-heals when a relay serves
602/// ≥ floor).
603///
604/// This does NOT apply delegation-chain authorization — the signature proves WHO; deciding WHETHER
605/// they were allowed (rank + chain to the owner) is a separate, later layer.
606pub fn fold_roster(
607    inner_editions: &[Event],
608    community_id: &CommunityId,
609    floors: &HashMap<String, (u64, [u8; 32])>,
610) -> FoldedRoster {
611    let mut skipped = inner_editions.len().saturating_sub(MAX_CONTROL_EDITIONS);
612
613    // Verify + parse; drop (and count) anything that doesn't. The cap bounds verify work.
614    let parsed: Vec<edition::ParsedEdition> = inner_editions
615        .iter()
616        .take(MAX_CONTROL_EDITIONS)
617        .filter_map(|e| match edition::parse_edition_inner(e) {
618            Ok(p) => Some(p),
619            Err(_) => {
620                skipped += 1;
621                None
622            }
623        })
624        .collect();
625
626    // tombstone detection — owner-signature-filtered, NOT position/version dependent. The dissolved
627    // tombstone has no version chain, so it does NOT route through the per-entity `version::fold` below;
628    // instead scan the locator directly and collect EVERY well-formed signer (the caller keeps only the
629    // proven owner). Listing all signers means a flood of forged non-owner editions can't bury the owner's.
630    // A vsk=10 edition at the WRONG entity_id (relabel attempt) is ignored here and dropped/skipped below.
631    let dissolved_eid = super::derive::dissolved_locator(community_id);
632    let mut dissolved_by: Vec<PublicKey> = Vec::new();
633    let mut dissolved_editions: Vec<DissolvedEdition> = Vec::new();
634    for p in &parsed {
635        if p.vsk == VSK_DISSOLVED && p.entity_id == dissolved_eid {
636            if !dissolved_by.contains(&p.author) {
637                dissolved_by.push(p.author);
638            }
639            // Full records (every well-formed one, not signer-deduped): the migration
640            // pointer selection needs created_at/id/content across ALL of an owner's
641            // tombstones, and a non-owner's are filtered by the caller like dissolved_by.
642            dissolved_editions.push(DissolvedEdition {
643                author: p.author,
644                created_at: p.created_at,
645                inner_id: p.inner_id,
646                content: p.content.clone(),
647            });
648        }
649    }
650
651    let mut by_entity: HashMap<[u8; 32], Vec<&edition::ParsedEdition>> = HashMap::new();
652    for p in &parsed {
653        // vsk=10 is chain-free and handled by the scan above; keep it out of the version-chain fold (a
654        // no-prev v1 would otherwise just no-op, but excluding it makes the exemption explicit).
655        if p.vsk == VSK_DISSOLVED {
656            continue;
657        }
658        by_entity.entry(p.entity_id).or_default().push(p);
659    }
660
661    let mut out = roles::CommunityRoles::default();
662    let mut role_authors: Vec<PublicKey> = Vec::new();
663    let mut grant_authors: Vec<PublicKey> = Vec::new();
664    let mut gapped_entities = Vec::new();
665    let mut heads: Vec<EntityHead> = Vec::new();
666    let mut banned: Vec<String> = Vec::new();
667    let mut banlist_author: Option<PublicKey> = None;
668    let mut banlist_head: Option<EntityHead> = None;
669    let banlist_eid = super::derive::banlist_locator(community_id);
670    let mut invite_link_sets: Vec<InviteLinkSet> = Vec::new();
671    let mut root_meta: Option<super::metadata::CommunityMetadata> = None;
672    let mut root_author: Option<PublicKey> = None;
673    let mut root_head: Option<EntityHead> = None;
674    let mut root_candidates: Vec<RootCandidate> = Vec::new();
675    let mut channel_meta: Vec<ChannelMetaHead> = Vec::new();
676    let mut channel_candidates: Vec<ChannelMetaHead> = Vec::new();
677
678    for (entity_id, editions) in by_entity {
679        let fold_eds: Vec<version::Edition> = editions.iter().map(|p| p.to_fold_edition()).collect();
680        // Seed the fold from this entity's PERSISTED head (refuse-downgrade floor), not from
681        // scratch — so a relay serving editions below what we hold can't roll the chain back.
682        let entity_hex = crate::simd::hex::bytes_to_hex_32(&entity_id);
683        let floor = floors.get(&entity_hex);
684        let floor_v = floor.map(|(v, _)| *v).unwrap_or(0);
685        let result = version::fold(&fold_eds, floor_v, floor.map(|(_, h)| h));
686        // head selection by client mode. No gap → the chain-anchored head. Gap → the chain isn't
687        // anchored to genesis/floor; two modes:
688        //   - TRACKING (floor > 0): fail CLOSED — an unanchored tail can be a rollback/fork. Keep the
689        //     held floor; the entity is flagged pending for a re-fetch from the union.
690        // - BOOTSTRAPPING (floor == 0): a fresh joiner whose genesis was re-anchored away cannot
691        //     verify lineage, so accept the HIGHEST signed head (Policy B) and let AUTHORITY be the gate —
692        //     each VSK arm below re-checks the head's coordinate, and the caller runs is_authorized /
693        //     authorize_delegation. The head's signature is already verified (parse_edition_inner), so a
694        //     relay can't forge a higher version, only withhold/replay older valid ones — covered by the
695        //     relay union (Vector ships multiple trusted relays) + the refuse-downgrade floor.
696        let head_idx = if result.gap {
697            gapped_entities.push(entity_id);
698            if floor_v == 0 {
699                match version::bootstrap_head(&fold_eds, 0) { Some(i) => i, None => continue }
700            } else {
701                // Tracking + gap. Authority records (roles/grants/banlist) FAIL CLOSED — an unanchored
702                // tail can be a rollback/fork, and converging authority off a withheld view is a
703                // relay-choosable censorship lever. DISPLAY metadata (GroupRoot, channel) is exempt:
704                // it carries no authority, the consumer's `is_authorized` filter is the real gate, and
705                // the consumer's refuse-downgrade floor still blocks any sub-floor rollback. So surface
706                // the ≥floor candidates (the per-version winner at the highest version) and let the
707                // consumer author-gate + converge a same-version fork. A withheld-history gap here at
708                // worst forward-jumps the displayed name to a validly-signed authorized edit.
709                match version::bootstrap_head(&fold_eds, floor_v) {
710                    Some(i) if matches!(editions[i].vsk.as_str(), VSK_COMMUNITY_ROOT | VSK_CHANNEL) => i,
711                    _ => continue, // not display metadata → fail closed
712                }
713            }
714        } else {
715            match result.head { Some(i) => i, None => continue }
716        };
717        let head = editions[head_idx];
718        let mut record_head = || {
719            heads.push(EntityHead {
720                entity_hex: crate::simd::hex::bytes_to_hex_32(&entity_id),
721                version: head.version,
722                self_hash: head.self_hash,
723                inner_id: head.inner_id,
724                citation: head.authority.clone(),
725            });
726        };
727        match head.vsk.as_str() {
728            VSK_ROLE => match serde_json::from_str::<roles::Role>(&head.content) {
729                // The edition coordinate IS the role id (d-tag = role_id), so the content can't
730                // claim to be a different (e.g. higher-powered) role than the entity it lives at.
731                Ok(role) if hex32(&role.role_id) == Some(entity_id) => {
732                    record_head();
733                    role_authors.push(head.author);
734                    out.roles.push(role);
735                }
736                _ => skipped += 1,
737            },
738            VSK_GRANT => match serde_json::from_str::<roles::MemberGrant>(&head.content) {
739                // The grant's coordinate IS its member's opaque locator, so the content
740                // can't relabel the grant to a different member, and two entities can't claim one
741                // member (they'd share the locator → group + fold, not duplicate).
742                Ok(grant)
743                    if hex32(&grant.member)
744                        .is_some_and(|m| super::derive::grant_locator(community_id, &m) == entity_id) =>
745                {
746                    // Record the head even for an empty grant (a revoke is a real chain advance); the
747                    // empty grant just folds to "no roster entry" — don't carry a husk.
748                    record_head();
749                    if !grant.role_ids.is_empty() {
750                        grant_authors.push(head.author);
751                        out.grants.push(grant);
752                    }
753                }
754                _ => skipped += 1,
755            },
756            VSK_BANLIST if entity_id == banlist_eid => match serde_json::from_str::<Vec<String>>(&head.content) {
757                // The banlist lives at the single community-wide locator; its content is the banned set.
758                // Only meaningful once the caller checks `banlist_author` held BAN (the authority gate).
759                Ok(list) => {
760                    banned = list;
761                    banlist_author = Some(head.author);
762                    banlist_head = Some(EntityHead {
763                        entity_hex: crate::simd::hex::bytes_to_hex_32(&entity_id),
764                        version: head.version,
765                        self_hash: head.self_hash,
766                        inner_id: head.inner_id,
767                        citation: head.authority.clone(),
768                    });
769                }
770                _ => skipped += 1,
771            },
772            VSK_INVITE_LINKS if super::derive::invite_links_locator(community_id, &head.author.to_bytes()) == entity_id => {
773                // A creator's OWN link list, bound to its author's coordinate (so one creator can't
774                // publish links under another's identity). Content is that creator's active locators.
775                // Meaningful once the caller checks `creator` held CREATE_INVITE (the authority gate),
776                // then UNIONS authorized creators' locators into the aggregate active-set.
777                match serde_json::from_str::<Vec<String>>(&head.content) {
778                    Ok(locators) => invite_link_sets.push(InviteLinkSet {
779                        creator: head.author,
780                        locators,
781                        head: EntityHead {
782                            entity_hex: crate::simd::hex::bytes_to_hex_32(&entity_id),
783                            version: head.version,
784                            self_hash: head.self_hash,
785                            inner_id: head.inner_id,
786                            citation: head.authority.clone(),
787                        },
788                    }),
789                    _ => skipped += 1,
790                }
791            }
792            VSK_COMMUNITY_ROOT if entity_id == community_id.0 => {
793                // GroupRoot lives at the community's own coordinate; its content is the community
794                // descriptor. Expose ALL gap-vetted candidates at-or-above the floor (fork members
795                // included — B1b), highest version first with the deterministic inner-id tiebreak,
796                // so the consumer can author-aware-scan: a same-version forgery can't orphan an
797                // authorized re-assert via the author-blind tiebreak. Contiguity stays author-blind
798                // here; the caller checks `MANAGE_METADATA` per candidate.
799                let entity_hex = crate::simd::hex::bytes_to_hex_32(&entity_id);
800                let mut cands: Vec<(u64, [u8; 32], RootCandidate)> = editions
801                    .iter()
802                    .filter(|e| e.version >= floor_v)
803                    .filter_map(|e| {
804                        serde_json::from_str::<super::metadata::CommunityMetadata>(&e.content)
805                            .ok()
806                            .map(|meta| (e.version, e.inner_id, RootCandidate {
807                                meta,
808                                author: e.author,
809                                head: EntityHead {
810                                    entity_hex: entity_hex.clone(),
811                                    version: e.version,
812                                    self_hash: e.self_hash,
813                                    inner_id: e.inner_id,
814                                    citation: e.authority.clone(),
815                                },
816                            }))
817                    })
818                    .collect();
819                if cands.is_empty() {
820                    skipped += 1;
821                } else {
822                    cands.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)));
823                    root_candidates = cands.into_iter().map(|(_, _, c)| c).collect();
824                    let top = &root_candidates[0];
825                    root_meta = Some(top.meta.clone());
826                    root_author = Some(top.author);
827                    root_head = Some(top.head.clone());
828                }
829            }
830            VSK_CHANNEL => {
831                // ChannelMetadata lives at the channel's own coordinate (`entity_id == channel_id`), so its
832                // content can't relabel itself to a different channel. Mirror GroupRoot: expose ALL gap-
833                // vetted candidates at-or-above the floor (fork members included), highest version first with
834                // the inner-id tiebreak, so the consumer author-aware-scans + converges a same-version fork.
835                // Contiguity stays author-blind here; the caller checks MANAGE_CHANNELS per candidate.
836                let entity_hex = crate::simd::hex::bytes_to_hex_32(&entity_id);
837                let mut cands: Vec<(u64, [u8; 32], ChannelMetaHead)> = editions
838                    .iter()
839                    .filter(|e| e.version >= floor_v)
840                    .filter_map(|e| {
841                        serde_json::from_str::<super::metadata::ChannelMetadata>(&e.content)
842                            .ok()
843                            .map(|meta| (e.version, e.inner_id, ChannelMetaHead {
844                                channel_id: entity_id,
845                                meta,
846                                author: e.author,
847                                head: EntityHead {
848                                    entity_hex: entity_hex.clone(),
849                                    version: e.version,
850                                    self_hash: e.self_hash,
851                                    inner_id: e.inner_id,
852                                    citation: e.authority.clone(),
853                                },
854                            }))
855                    })
856                    .collect();
857                if cands.is_empty() {
858                    skipped += 1;
859                } else {
860                    cands.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)));
861                    // Author-blind top stays in channel_meta (back-compat); the full list drives convergence.
862                    channel_meta.push(cands[0].2.clone());
863                    channel_candidates.extend(cands.into_iter().map(|(_, _, c)| c));
864                }
865            }
866            _ => {} // other entity types (role-order/...) handled elsewhere
867        }
868    }
869
870    FoldedRoster {
871        roles: out, role_authors, grant_authors, gapped_entities, skipped, fetched: 0, heads,
872        banned, banlist_author, banlist_head, dissolved_by, dissolved_editions,
873        invite_link_sets,
874        root_meta, root_author, root_head, root_candidates, channel_meta, channel_candidates,
875    }
876}
877
878/// Filter a [`fold_roster`] result by the **delegation chain** → the AUTHORIZED roster. Binding
879/// + a valid signature prove an edition was *well-formed and authentic*, NOT that its signer was
880/// *allowed* to make it — without this layer a member could self-sign an Admin grant and have every
881/// peer fold it into their roster. Here an entry is trusted only if its signer was authorized:
882///
883/// - a **role** at position `P` is kept only if its signer can act on `P` with `MANAGE_ROLES` (strictly
884///   outranks `P`; the owner — position 0 via the attestation — is supreme);
885/// - a **grant** of roles `[R..]` to member `M` is kept only if its signer outranks every granted role's
886///   position AND outranks `M`, with `MANAGE_ROLES`.
887///
888/// Authority is resolved against the roster built SO FAR, so this is a fixpoint seeded by the owner:
889/// owner-signed entries are accepted first (they add admins), then admin-signed entries those admins are
890/// allowed to make, until stable. An entry whose signer never becomes authorized is dropped — that is
891/// the self-promotion / forged-delegation defense. `owner_hex == None` (unproven community, no root)
892/// yields an EMPTY roster (fail closed: no anchor, no authority).
893///
894/// Authority is evaluated against the CURRENT folded roster. With [`fold_roster`] now seeding each
895/// entity from its persisted refuse-downgrade FLOOR, that "current" roster is rollback-protected:
896/// a relay can't resurrect a since-revoked admin's grant to re-validate the chain. So the 
897/// version-pinned guarantee for the DELEGATION plane falls out here without a separate citation check —
898/// a demoted signer drops out of the accepted roster, and every edition they signed drops with them
899/// (refuse-superseded). The `vac` citation is the mechanism for the ACTION plane (ban/hide), where the
900/// actor is NOT the entity being folded; on the delegation plane the signer's authorizing grant IS a
901/// folded entity, so the floor-fold + this fixpoint already pin it. Grant editions still CARRY a `vac`
902/// (emitted by `set_member_grant`) for the audit log + forward-compat, but this consumer deliberately
903/// does not read it — the rank-chain over the floor-protected roster is the authority decision. The one behavior NOT provided is
904/// point-in-time PRESERVATION (keeping grants a since-demoted admin made while authorized) — that is the
905/// less-safe direction, needs a roster-wide snapshot version, and is deferred; the current fail-safe
906/// drop is correct and safer.
907pub fn authorize_delegation(folded: &FoldedRoster, owner_hex: Option<&str>) -> roles::CommunityRoles {
908    use roles::Permissions;
909    let mut accepted = roles::CommunityRoles::default();
910    let mut role_done = vec![false; folded.roles.roles.len()];
911    let mut grant_done = vec![false; folded.roles.grants.len()];
912
913    loop {
914        let mut changed = false;
915
916        // A role is authorized if its signer can define a role at that position (outrank + MANAGE_ROLES).
917        for (i, role) in folded.roles.roles.iter().enumerate() {
918            if role_done[i] {
919                continue;
920            }
921            // Position 0 is reserved to the owner-attestation chain — no RoleMetadata may occupy
922            // it, even owner-signed (the owner's authority is the attestation, not a pos-0 role). Reject.
923            if role.position == 0 {
924                role_done[i] = true;
925                continue;
926            }
927            let author = folded.role_authors[i].to_hex();
928            if accepted.can_act_on_position(&author, owner_hex, role.position, Permissions::MANAGE_ROLES) {
929                accepted.roles.push(role.clone());
930                role_done[i] = true;
931                changed = true;
932            }
933        }
934
935        // A grant is authorized if its signer outranks every granted role's position AND the member,
936        // with MANAGE_ROLES. Granted-role positions are resolved from the ALREADY-accepted roles, so a
937        // grant referencing a not-yet-accepted role defers to a later round (or is dropped if its role
938        // never authorizes). This is the escalation defense: you can't grant a role you don't outrank.
939        for (i, grant) in folded.roles.grants.iter().enumerate() {
940            if grant_done[i] {
941                continue;
942            }
943            let positions: Option<Vec<u32>> =
944                grant.role_ids.iter().map(|rid| accepted.role(rid).map(|r| r.position)).collect();
945            let Some(positions) = positions else { continue }; // a granted role not yet accepted → defer
946            let author = folded.grant_authors[i].to_hex();
947            let outranks_all_roles = positions
948                .iter()
949                .all(|p| accepted.can_act_on_position(&author, owner_hex, *p, Permissions::MANAGE_ROLES));
950            if outranks_all_roles
951                && accepted.can_act_on_member(&author, owner_hex, &grant.member, Permissions::MANAGE_ROLES)
952            {
953                accepted.grants.push(grant.clone());
954                grant_done[i] = true;
955                changed = true;
956            }
957        }
958
959        if !changed {
960            break;
961        }
962    }
963    accepted
964}
965
966/// version-pinned authority **completeness** check for an action that carries an
967/// [`edition::AuthorityCitation`] (a ban, a hide, a delegated grant). The action names the authorizing
968/// grant edition it claims authority under; this confirms we have folded a COMPLETE, un-forked view of
969/// that grant — synced to AT LEAST the cited version, with the cited hash matching ours at equality.
970///
971/// It does NOT decide the permission/outrank — that stays with the caller's `can_act_on_member` against
972/// the CURRENT authorized roster, so a since-demoted actor is refused there (refuse-superseded falls out:
973/// we hold a later head of their grant that dropped the role). This check is the orthogonal half: it
974/// guarantees the verdict is computed over the same authority view the actor cited, not a stale or
975/// partial one.
976///
977/// `heads` is the fold's per-entity head set ([`FoldedRoster::heads`]) — the haystack the cited grant
978/// must appear in. `actor_grant_hex` is the entity coordinate of the ACTOR's OWN authorizing grant
979/// (`grant_locator(community_id, actor)`, lowercase hex): the citation MUST name it, so an actor can't
980/// borrow completeness by citing some other synced edition. Returns true iff:
981///   - the actor is the proven owner (supreme — cites nothing), OR
982///   - the citation names the actor's own grant AND we surfaced it at version ≥ the cited version, and —
983///     when our head is EXACTLY the cited version — the cited hash equals ours (a cited fork is rejected).
984///
985/// A non-owner who cited nothing, cited a foreign entity, or whose cited grant we have NOT folded up to
986/// the cited version (a withholding relay, or we are simply behind), returns false: FAIL CLOSED — never
987/// act on an incomplete authority view (the §"never act on a partial view" tenet). The block-until-synced
988/// re-fetch escalation across a relay quorum is deferred (one signature suffices, per the MVP directive);
989/// MVP fails closed here and self-heals on the next sync once the cited grant arrives.
990pub fn authority_citation_satisfied(
991    heads: &[EntityHead],
992    owner_hex: Option<&str>,
993    actor_hex: &str,
994    actor_grant_hex: &str,
995    citation: Option<&edition::AuthorityCitation>,
996) -> bool {
997    if owner_hex == Some(actor_hex) {
998        return true;
999    }
1000    let Some(c) = citation else { return false };
1001    let entity_hex = crate::simd::hex::bytes_to_hex_32(&c.entity_id);
1002    // The citation must name the ACTOR's OWN authorizing grant — citing a foreign synced edition can't
1003    // borrow completeness (the permission check keys on the actor, but pinning to their grant keeps the
1004    // sync-floor honest and is the coordinate the delegation verifier will resolve rank at).
1005    if entity_hex != actor_grant_hex {
1006        return false;
1007    }
1008    match heads.iter().find(|h| h.entity_hex == entity_hex) {
1009        // We hold a LATER edition of the actor's grant than they cited — synced past it. Whether the
1010        // actor is STILL authorized is the caller's roster check (which reflects this later head).
1011        Some(h) if h.version > c.version => true,
1012        // Synced to exactly the cited version: the cited hash must be the one that won our fold (else
1013        // the actor cited a non-canonical fork of their own grant).
1014        Some(h) if h.version == c.version => h.self_hash == c.edition_hash,
1015        // Not surfaced, or our head is BEHIND the cited version → we cannot confirm the authority.
1016        _ => false,
1017    }
1018}
1019
1020#[cfg(test)]
1021mod tests {
1022    use super::*;
1023    use crate::community::roles::{Permissions, Role, RoleScope};
1024
1025    fn sr() -> ServerRootKey {
1026        ServerRootKey([0x07; 32])
1027    }
1028
1029    #[test]
1030    fn authority_citation_satisfied_pins_to_a_synced_complete_grant() {
1031        let owner = "ow".repeat(32);
1032        let actor = "ac".repeat(32);
1033        let entity = [0x55u8; 32];
1034        let hash = [0x66u8; 32];
1035        let eh = crate::simd::hex::bytes_to_hex_32(&entity); // the actor's own grant locator
1036        let head = |v: u64, h: [u8; 32]| {
1037            vec![EntityHead { entity_hex: eh.clone(), version: v, self_hash: h, inner_id: [0u8; 32], citation: None }]
1038        };
1039        let cite = edition::AuthorityCitation { entity_id: entity, version: 3, edition_hash: hash };
1040
1041        // Owner is supreme and cites nothing.
1042        assert!(authority_citation_satisfied(&[], Some(&owner), &owner, &eh, None));
1043        // A non-owner with no citation fails closed.
1044        assert!(!authority_citation_satisfied(&head(3, hash), Some(&owner), &actor, &eh, None));
1045        // Synced to exactly the cited version with the matching hash → satisfied.
1046        assert!(authority_citation_satisfied(&head(3, hash), Some(&owner), &actor, &eh, Some(&cite)));
1047        // Same version, wrong hash (a cited fork at the tip) → rejected.
1048        assert!(!authority_citation_satisfied(&head(3, [0xEE; 32]), Some(&owner), &actor, &eh, Some(&cite)));
1049        // We hold a LATER head of the actor's grant → synced past it (the roster check handles supersession).
1050        assert!(authority_citation_satisfied(&head(4, [0x77; 32]), Some(&owner), &actor, &eh, Some(&cite)));
1051        // We are BEHIND the cited version → fail closed.
1052        assert!(!authority_citation_satisfied(&head(2, hash), Some(&owner), &actor, &eh, Some(&cite)));
1053        // The cited grant isn't surfaced at all → fail closed.
1054        assert!(!authority_citation_satisfied(&[], Some(&owner), &actor, &eh, Some(&cite)));
1055        // The citation names a FOREIGN entity (not the actor's grant) → rejected even if surfaced.
1056        let foreign = "ff".repeat(32);
1057        assert!(!authority_citation_satisfied(&head(3, hash), Some(&owner), &actor, &foreign, Some(&cite)));
1058    }
1059    // The community id binds grant coordinates (stable across rotation); fold + grant builders take it.
1060    fn cid() -> CommunityId {
1061        CommunityId([0x09; 32])
1062    }
1063
1064    /// A role edition at its bound coordinate (entity_id == role_id).
1065    fn role_event(owner: &Keys, role_id: &str, position: u32, version: u64, prev: Option<&[u8; 32]>, created_at: u64) -> Event {
1066        let role = Role {
1067            role_id: role_id.to_string(),
1068            name: "Admin".into(),
1069            position,
1070            permissions: Permissions::admin(),
1071            scope: RoleScope::Server,
1072            color: 0,
1073        };
1074        let eid = crate::simd::hex::hex_to_bytes_32(role_id);
1075        edition::build_edition_inner(owner.public_key(), VSK_ROLE, &eid, version, prev, &serde_json::to_string(&role).unwrap(), created_at, None)
1076            .finalize(owner)
1077            .unwrap()
1078    }
1079
1080    /// A grant edition at its bound coordinate (entity_id == grant_locator(community_id, member)).
1081    fn grant_event(owner: &Keys, member_hex: &str, role_ids: Vec<String>, version: u64, prev: Option<&[u8; 32]>, created_at: u64) -> (Event, [u8; 32], String) {
1082        let member_bytes = crate::simd::hex::hex_to_bytes_32(member_hex);
1083        let eid = crate::community::derive::grant_locator(&cid(), &member_bytes);
1084        let g = roles::MemberGrant { member: member_hex.to_string(), role_ids };
1085        let content = serde_json::to_string(&g).unwrap();
1086        let ev = edition::build_edition_inner(owner.public_key(), VSK_GRANT, &eid, version, prev, &content, created_at, None)
1087            .finalize(owner)
1088            .unwrap();
1089        (ev, eid, content)
1090    }
1091
1092    /// A GroupRoot (community metadata) edition at the community's own coordinate (entity_id == community_id).
1093    fn groot_event(signer: &Keys, name: &str, desc: &str, version: u64, prev: Option<&[u8; 32]>, created_at: u64) -> Event {
1094        let meta = crate::community::metadata::CommunityMetadata {
1095            name: name.to_string(),
1096            relays: vec![],
1097            description: Some(desc.to_string()),
1098            icon: None,
1099            banner: None,
1100            owner_attestation: None,
1101        };
1102        let content = serde_json::to_string(&meta).unwrap();
1103        edition::build_edition_inner(signer.public_key(), VSK_COMMUNITY_ROOT, &cid().0, version, prev, &content, created_at, None)
1104            .finalize(signer)
1105            .unwrap()
1106    }
1107
1108    #[test]
1109    fn folds_a_role_and_a_grant() {
1110        let owner = Keys::generate();
1111        let role_id = "a".repeat(64);
1112        let member = "bb".repeat(32);
1113        let role_ev = role_event(&owner, &role_id, 1, 1, None, 100);
1114        let (grant_ev, _, _) = grant_event(&owner, &member, vec![role_id.clone()], 1, None, 101);
1115        let folded = fold_roster(&[role_ev, grant_ev], &cid(), &Default::default());
1116        assert_eq!(folded.roles.roles.len(), 1);
1117        assert_eq!(folded.roles.roles[0].role_id, role_id);
1118        assert!(folded.roles.is_admin(&member), "the member holds the granted Admin role");
1119        assert_eq!(folded.skipped, 0);
1120        assert!(folded.gapped_entities.is_empty());
1121    }
1122
1123    #[test]
1124    fn latest_grant_edition_wins() {
1125        let owner = Keys::generate();
1126        let role_a = "a".repeat(64);
1127        let role_b = "b".repeat(64);
1128        let member = "cc".repeat(32);
1129        let (e1, eid, c1) = grant_event(&owner, &member, vec![role_a.clone()], 1, None, 100);
1130        let v1_hash = version::edition_hash(&eid, 1, None, c1.as_bytes());
1131        let (e2, _, _) = grant_event(&owner, &member, vec![role_b.clone()], 2, Some(&v1_hash), 101);
1132        let folded = fold_roster(&[e1, e2], &cid(), &Default::default());
1133        let held: Vec<&String> = folded.roles.grants.iter().flat_map(|g| &g.role_ids).collect();
1134        assert_eq!(held, vec![&role_b], "v2 supersedes v1 — the member now holds role_b, not role_a");
1135        assert_eq!(folded.skipped, 0);
1136    }
1137
1138    /// TRACKING (floor > 0): a lone forged high-version edition with withheld history is QUARANTINED —
1139    /// flagged gapped AND kept out of the trusted roster. This is the fail-closed defense, and it
1140    /// stays fully intact for any client that already holds a floor (rollback/fork protection).
1141    #[test]
1142    fn tracking_quarantines_a_gapped_head() {
1143        let owner = Keys::generate();
1144        let role_id = "e".repeat(64);
1145        let eid = crate::simd::hex::hex_to_bytes_32(&role_id);
1146        let ev = role_event(&owner, &role_id, 1, 5, Some(&[0x99u8; 32]), 100);
1147        // We already hold this entity at v3, so a lone v5 with a non-linking prev is an unanchored tail.
1148        let mut floors = std::collections::HashMap::new();
1149        floors.insert(role_id.clone(), (3u64, [0x11u8; 32]));
1150        let folded = fold_roster(&[ev], &cid(), &floors);
1151        assert!(folded.roles.roles.is_empty(), "tracking: a gapped tail is quarantined, not folded");
1152        assert_eq!(folded.gapped_entities, vec![eid]);
1153    }
1154
1155    /// BOOTSTRAPPING (floor == 0, Policy B): a fresh joiner whose genesis was re-anchored away SURFACES
1156    /// the highest signed head despite the gap — but AUTHORITY still gates it. An owner-authored head is
1157    /// authorized; an unauthorized author's (equally surfaced) head is dropped by `authorize_delegation`.
1158    /// This is the narrow, deliberate relaxation of the fail-closed rule at first contact only.
1159    #[test]
1160    fn bootstrapping_surfaces_a_gapped_head_but_authority_gates() {
1161        let owner = Keys::generate();
1162        let owner_hex = owner.public_key().to_hex();
1163        let role_id = "e".repeat(64);
1164        let eid = crate::simd::hex::hex_to_bytes_32(&role_id);
1165        // Owner-authored position-1 role at v5 with no anchor (genesis re-anchored away) → gapped @ floor 0.
1166        let ev = role_event(&owner, &role_id, 1, 5, Some(&[0x99u8; 32]), 100);
1167        let folded = fold_roster(&[ev], &cid(), &Default::default());
1168        assert_eq!(folded.roles.roles.len(), 1, "bootstrapping surfaces the highest signed head despite the gap");
1169        assert_eq!(folded.gapped_entities, vec![eid], "still flagged pending so the union can fill the gap");
1170        assert_eq!(authorize_delegation(&folded, Some(&owner_hex)).roles.len(), 1, "owner-authored head is authorized");
1171
1172        // Same shape, authored by a STRANGER → still surfaced into the raw fold, but authority drops it.
1173        let stranger = Keys::generate();
1174        let ev2 = role_event(&stranger, &role_id, 1, 5, Some(&[0x99u8; 32]), 100);
1175        let folded2 = fold_roster(&[ev2], &cid(), &Default::default());
1176        assert_eq!(folded2.roles.roles.len(), 1, "surfaced into the raw fold (signature is valid)");
1177        assert!(authorize_delegation(&folded2, Some(&owner_hex)).roles.is_empty(),
1178            "Policy B does NOT bypass authority — an unauthorized author's surfaced head is rejected");
1179    }
1180
1181    /// Fresh-joiner-after-rotation: a bootstrapping joiner (floor 0) recovers the FULL plane across version
1182    /// holes. It surfaces the latest admin-authored GroupRoot AND the owner's gapped (no-v1) grant, and
1183    /// authority folds the admin in so that admin's metadata is authorized.
1184    #[test]
1185    fn fresh_joiner_recovers_full_plane_across_gaps() {
1186        let owner = Keys::generate();
1187        let owner_hex = owner.public_key().to_hex();
1188        let admin = Keys::generate();
1189        let admin_hex = admin.public_key().to_hex();
1190        let role_id = "a".repeat(64);
1191
1192        // Admin role def — owner-signed, position 1, admin perms (incl MANAGE_METADATA), contiguous v1.
1193        let role = role_event(&owner, &role_id, 1, 1, None, 100);
1194        // Owner grants the admin that role — but the grant chain is GAPPED (starts at v2, no v1 re-anchored).
1195        let (grant, _, _) = grant_event(&owner, &admin_hex, vec![role_id.clone()], 2, Some(&[0x99u8; 32]), 110);
1196        // GroupRoot: owner genesis v1, then the admin's edit at v11 with a hole below it (no v2..v10).
1197        let groot_v1 = groot_event(&owner, "Genesis", "", 1, None, 90);
1198        let groot_v11 = groot_event(&admin, "AggroTown v2", "King Claude was here", 11, Some(&[0xABu8; 32]), 200);
1199
1200        let folded = fold_roster(&[role, grant, groot_v1, groot_v11], &cid(), &Default::default());
1201
1202        // Bootstrapping surfaced the LATEST GroupRoot (v11, admin-authored) across the gap, not the genesis.
1203        assert_eq!(folded.root_author.map(|a| a.to_hex()), Some(admin_hex.clone()), "latest GroupRoot author surfaced");
1204        assert_eq!(folded.root_meta.as_ref().unwrap().name, "AggroTown v2");
1205        assert_eq!(folded.root_meta.as_ref().unwrap().description.as_deref(), Some("King Claude was here"));
1206
1207        // Authority folds the gapped grant → the admin holds MANAGE_METADATA, so the v11 edit IS authorized.
1208        let authed = authorize_delegation(&folded, Some(&owner_hex));
1209        assert!(
1210            authed.is_authorized(&admin_hex, Some(&owner_hex), roles::Permissions::MANAGE_METADATA),
1211            "the bootstrapped grant authorizes the admin → the fresh joiner trusts the admin's metadata"
1212        );
1213    }
1214
1215    /// Forgery-resistance under Policy B: a bootstrapping joiner SURFACES even a stranger's lone high-version
1216    /// GroupRoot (its signature is valid), but AUTHORITY refuses it — `is_authorized` is false for the
1217    /// unauthorized author, so the caller never applies it. Policy B trusts signature + authority, never a
1218    /// bare version number. (A relay can't forge a higher version; the worst it does is surface a non-author.)
1219    #[test]
1220    fn unauthorized_high_version_metadata_is_surfaced_but_not_authorized() {
1221        let owner = Keys::generate();
1222        let owner_hex = owner.public_key().to_hex();
1223        let stranger = Keys::generate();
1224        let stranger_hex = stranger.public_key().to_hex();
1225        let groot_v1 = groot_event(&owner, "Genesis", "", 1, None, 90);
1226        let forged = groot_event(&stranger, "PWNED", "owned by a hostile relay", 99, Some(&[0xCDu8; 32]), 300);
1227        let folded = fold_roster(&[groot_v1, forged], &cid(), &Default::default());
1228        // Surfaced (highest signed head) — but authored by the stranger, who holds no authority.
1229        assert_eq!(folded.root_author.map(|a| a.to_hex()), Some(stranger_hex.clone()));
1230        let authed = authorize_delegation(&folded, Some(&owner_hex));
1231        assert!(
1232            !authed.is_authorized(&stranger_hex, Some(&owner_hex), roles::Permissions::MANAGE_METADATA),
1233            "an unauthorized author's surfaced metadata is NOT authorized — the caller drops it"
1234        );
1235    }
1236
1237    /// Publish-time authority (B1b): on a FRESH fold (no floor), a demoted admin's editions — incl. a
1238    /// same-version FORGERY sharing the owner's re-assert version — are skipped by the author-aware scan,
1239    /// and the owner's re-assert wins. The candidate set exposes BOTH v3 fork members so the forgery can't
1240    /// orphan the re-assert via the author-blind tiebreak. (This is the gauntlet's convergence demand.)
1241    #[test]
1242    fn fresh_fold_picks_owner_reassert_over_a_demoted_admins_forgery_and_edit() {
1243        let owner = Keys::generate();
1244        let owner_hex = owner.public_key().to_hex();
1245        let alice = Keys::generate();
1246        let alice_hex = alice.public_key().to_hex();
1247        let role_id = "a".repeat(64);
1248
1249        // Owner: admin role (MANAGE_METADATA); grant Alice; then REVOKE Alice (chained grant→revoke).
1250        let role = role_event(&owner, &role_id, 1, 1, None, 100);
1251        let (grant, geid, gcontent) = grant_event(&owner, &alice_hex, vec![role_id.clone()], 1, None, 110);
1252        let grant_hash = version::edition_hash(&geid, 1, None, gcontent.as_bytes());
1253        let (revoke, _, _) = grant_event(&owner, &alice_hex, vec![], 2, Some(&grant_hash), 120);
1254
1255        // GroupRoot: owner genesis; Alice's round-1 (while admin); then a v3 FORK — Alice's forgery vs the
1256        // owner's re-assert of Alice's content (arbitrary prevs → bootstrap path = the fresh-joiner case).
1257        let groot_v1 = groot_event(&owner, "Genesis", "", 1, None, 90);
1258        let round1_v2 = groot_event(&alice, "Alice's HQ", "by admin alice", 2, Some(&[0x22u8; 32]), 200);
1259        let forgery_v3 = groot_event(&alice, "FORGERY", "demoted alice", 3, Some(&[0x33u8; 32]), 300);
1260        let reassert_v3 = groot_event(&owner, "Alice's HQ", "re-asserted by owner", 3, Some(&[0x33u8; 32]), 310);
1261
1262        let folded = fold_roster(
1263            &[role, grant, revoke, groot_v1, round1_v2, forgery_v3, reassert_v3],
1264            &cid(), &Default::default(),
1265        );
1266        let authed = authorize_delegation(&folded, Some(&owner_hex));
1267        assert!(!authed.is_authorized(&alice_hex, Some(&owner_hex), Permissions::MANAGE_METADATA),
1268            "Alice is revoked (grant→revoke folds to no roles)");
1269
1270        // Both v3 fork members are exposed (so the forgery can't orphan the re-assert).
1271        assert_eq!(folded.root_candidates.iter().filter(|c| c.head.version == 3).count(), 2,
1272            "the candidate set includes both v3 fork members");
1273
1274        // The author-aware descending scan (what the consumer runs) picks the owner's re-assert; Alice's
1275        // forgery (v3) and round-1 (v2) are skipped because she's revoked.
1276        let chosen = folded.root_candidates.iter()
1277            .find(|c| authed.is_authorized(&c.author.to_hex(), Some(&owner_hex), Permissions::MANAGE_METADATA))
1278            .expect("an authorized candidate exists");
1279        assert_eq!(chosen.author, owner.public_key(), "the owner's re-assert wins the fork, not Alice's forgery");
1280        assert_eq!(chosen.meta.name, "Alice's HQ", "the demoted admin's content is preserved, not 'FORGERY'");
1281    }
1282
1283    /// A stranger publishes a high-version GRANT making themselves admin. Bootstrapping surfaces it
1284    /// into the raw fold (its signature is valid), but `authorize_delegation` drops it — it never chains to
1285    /// the owner. The grant analogue of `bootstrapping_surfaces_a_gapped_head_but_authority_gates`.
1286    #[test]
1287    fn bootstrapping_surfaces_a_stranger_grant_but_it_never_authorizes() {
1288        let owner = Keys::generate();
1289        let owner_hex = owner.public_key().to_hex();
1290        let stranger = Keys::generate();
1291        let stranger_hex = stranger.public_key().to_hex();
1292        let role_id = "a".repeat(64);
1293        // Owner-defined Admin role (so the role itself is legit) + a stranger-signed grant of it to themselves.
1294        let role = role_event(&owner, &role_id, 1, 1, None, 100);
1295        let (grant, _, _) = grant_event(&stranger, &stranger_hex, vec![role_id.clone()], 99, Some(&[0x99u8; 32]), 200);
1296        let folded = fold_roster(&[role, grant], &cid(), &Default::default());
1297        assert_eq!(folded.roles.grants.len(), 1, "the stranger's grant is surfaced (valid signature)");
1298        let authed = authorize_delegation(&folded, Some(&owner_hex));
1299        assert!(authed.grants.is_empty(), "but it never chains to the owner → dropped");
1300        assert!(!authed.is_authorized(&stranger_hex, Some(&owner_hex), roles::Permissions::BAN),
1301            "the self-granting stranger holds no authority");
1302    }
1303
1304    /// ONE fold, the full per-entity floor x per-VSK matrix. A tracking GroupRoot on a gapped tail
1305    /// is now SURFACED (display metadata carries no authority — the refuse-downgrade floor still blocks
1306    /// any sub-floor rollback, so a gap at worst forward-jumps the name to a validly-signed authorized
1307    /// edit); a tracking AUTHORITY record (grant) on a gapped tail stays QUARANTINED (converging authority
1308    /// off a withheld view is a censorship lever); a bootstrapping (floor-0) grant is surfaced. The
1309    /// per-entity decision is the crux of mixed-mode correctness.
1310    #[test]
1311    fn mixed_tracking_and_bootstrapping_floors_in_one_fold() {
1312        let owner = Keys::generate();
1313        let role_id = "a".repeat(64);
1314        let fresh_member = "cc".repeat(32);
1315        let tracked_member = "dd".repeat(32);
1316        // GroupRoot present only at v11 with a non-linking prev — we HOLD it at v5 (tracking, gapped).
1317        let groot_v11 = groot_event(&owner, "Tracked", "held-at-v5", 11, Some(&[0xABu8; 32]), 200);
1318        // A gapped grant for a member we've NEVER seen (floor 0 → bootstrapping → surfaced).
1319        let (fresh_grant, _, _) = grant_event(&owner, &fresh_member, vec![role_id.clone()], 2, Some(&[0x99u8; 32]), 110);
1320        // A gapped grant for a member we DO track at v5 (tracking authority → quarantined).
1321        let (tracked_grant, _, _) = grant_event(&owner, &tracked_member, vec![role_id.clone()], 11, Some(&[0x88u8; 32]), 120);
1322        let role = role_event(&owner, &role_id, 1, 1, None, 100); // contiguous, folds normally
1323
1324        let cid_hex = crate::simd::hex::bytes_to_hex_32(&cid().0);
1325        let tracked_grant_hex = crate::simd::hex::bytes_to_hex_32(
1326            &crate::community::derive::grant_locator(&cid(), &crate::simd::hex::hex_to_bytes_32(&tracked_member)));
1327        let mut floors = std::collections::HashMap::new();
1328        floors.insert(cid_hex, (5u64, [0x77u8; 32]));           // GroupRoot held at v5 → tracking
1329        floors.insert(tracked_grant_hex, (5u64, [0x66u8; 32])); // tracked member's grant held at v5 → tracking
1330        let folded = fold_roster(&[groot_v11, fresh_grant, tracked_grant, role], &cid(), &floors);
1331
1332        assert!(folded.root_meta.is_some(), "tracking GroupRoot with a gapped tail is SURFACED (display exemption)");
1333        assert!(folded.gapped_entities.contains(&cid().0), "and still flagged pending for a refetch");
1334        assert_eq!(folded.roles.grants.len(), 1, "only the bootstrapping grant surfaces; the tracked-gapped grant stays quarantined");
1335    }
1336
1337    /// An empty grant (a revoke) advances the entity's head (a real chain step) but carries NO roster
1338    /// entry — the husk must not linger as a phantom grant.
1339    #[test]
1340    fn empty_grant_revoke_advances_head_but_carries_no_entry() {
1341        let owner = Keys::generate();
1342        let role_id = "a".repeat(64);
1343        let member = "cc".repeat(32);
1344        let (g1, eid, c1) = grant_event(&owner, &member, vec![role_id], 1, None, 100);
1345        let v1_hash = version::edition_hash(&eid, 1, None, c1.as_bytes());
1346        let (g2, _, _) = grant_event(&owner, &member, vec![], 2, Some(&v1_hash), 101); // revoke = empty grant
1347        let folded = fold_roster(&[g1, g2], &cid(), &Default::default());
1348        assert!(folded.roles.grants.is_empty(), "a revoke leaves no roster entry");
1349        assert!(
1350            folded.heads.iter().any(|h| h.entity_hex == crate::simd::hex::bytes_to_hex_32(&eid) && h.version == 2),
1351            "but the revoke still advances the head (so a replayed v1 can't re-add the role)"
1352        );
1353    }
1354
1355    /// Junk-resilience: a hostile relay interleaves malformed editions with a valid one. `fold_roster`
1356    /// must SKIP the junk (counting it), still fold the valid role, and never panic.
1357    #[test]
1358    fn fold_roster_skips_junk_and_still_folds_the_valid() {
1359        let owner = Keys::generate();
1360        let owner_hex = owner.public_key().to_hex();
1361        let role_id = "a".repeat(64);
1362        let good = role_event(&owner, &role_id, 1, 1, None, 100);
1363        // Junk A: a role edition whose CONTENT is garbage JSON → parses, but the VSK_ROLE decode fails → skipped.
1364        let garbage = edition::build_edition_inner(
1365            owner.public_key(), VSK_ROLE, &crate::simd::hex::hex_to_bytes_32(&"b".repeat(64)),
1366            1, None, "not json at all", 100, None,
1367        ).finalize(&owner).unwrap();
1368        // Junk B: a role whose content claims a DIFFERENT role_id than its entity coordinate → skipped (binding).
1369        let role_b = Role { role_id: "cc".repeat(64), name: "X".into(), position: 1, permissions: Permissions::admin(), scope: RoleScope::Server, color: 0 };
1370        let mismatched = edition::build_edition_inner(
1371            owner.public_key(), VSK_ROLE, &crate::simd::hex::hex_to_bytes_32(&"dd".repeat(64)),
1372            1, None, &serde_json::to_string(&role_b).unwrap(), 100, None,
1373        ).finalize(&owner).unwrap();
1374
1375        let folded = fold_roster(&[good, garbage, mismatched], &cid(), &Default::default());
1376        assert!(folded.roles.role(&role_id).is_some(), "the valid role still folds through the junk");
1377        assert!(folded.skipped >= 2, "both junk editions are skipped, not folded (skipped={})", folded.skipped);
1378        assert_eq!(authorize_delegation(&folded, Some(&owner_hex)).roles.len(), 1, "only the valid role authorizes");
1379    }
1380
1381    /// A role edition whose content claims a role_id different from its entity coordinate is rejected
1382    /// — a signed edition can't relabel itself to a more powerful role.
1383    #[test]
1384    fn role_content_must_bind_to_its_entity_id() {
1385        let owner = Keys::generate();
1386        let role = Role {
1387            role_id: "a".repeat(64), // content claims a..a
1388            name: "X".into(),
1389            position: 0,
1390            permissions: Permissions::admin(),
1391            scope: RoleScope::Server,
1392            color: 0,
1393        };
1394        let wrong_eid = [0x12u8; 32]; // but the edition lives at a different coordinate
1395        let ev = edition::build_edition_inner(owner.public_key(), VSK_ROLE, &wrong_eid, 1, None, &serde_json::to_string(&role).unwrap(), 100, None)
1396            .finalize(&owner)
1397            .unwrap();
1398        let folded = fold_roster(&[ev], &cid(), &Default::default());
1399        assert!(folded.roles.roles.is_empty(), "entity_id != role_id → rejected");
1400        assert_eq!(folded.skipped, 1);
1401    }
1402
1403    /// A grant for member M placed at an entity_id that isn't M's locator is rejected — closes the
1404    /// "forged second grant for M re-adds revoked roles" union vector (H3).
1405    #[test]
1406    fn grant_at_wrong_locator_is_rejected() {
1407        let owner = Keys::generate();
1408        let member = "dd".repeat(32);
1409        let g = roles::MemberGrant { member: member.clone(), role_ids: vec!["a".repeat(64)] };
1410        let wrong_eid = [0x34u8; 32]; // not grant_locator(cid, member)
1411        let ev = edition::build_edition_inner(owner.public_key(), VSK_GRANT, &wrong_eid, 1, None, &serde_json::to_string(&g).unwrap(), 100, None)
1412            .finalize(&owner)
1413            .unwrap();
1414        let folded = fold_roster(&[ev], &cid(), &Default::default());
1415        assert!(!folded.roles.is_admin(&member), "a grant at the wrong locator does not take effect");
1416        assert_eq!(folded.skipped, 1);
1417    }
1418
1419    /// The fold is independent of input order (deterministic convergence across clients).
1420    #[test]
1421    fn fold_is_order_independent() {
1422        let owner = Keys::generate();
1423        let role_id = "a".repeat(64);
1424        let member = "bb".repeat(32);
1425        let role_ev = role_event(&owner, &role_id, 1, 1, None, 100);
1426        let (grant_ev, _, _) = grant_event(&owner, &member, vec![role_id.clone()], 1, None, 101);
1427        let a = fold_roster(&[role_ev.clone(), grant_ev.clone()], &cid(), &Default::default());
1428        let b = fold_roster(&[grant_ev, role_ev], &cid(), &Default::default());
1429        assert_eq!(a.roles.roles.len(), b.roles.roles.len());
1430        assert!(a.roles.is_admin(&member) && b.roles.is_admin(&member));
1431    }
1432
1433    /// The PUBLIC send-side builders produce editions the consumer fold accepts cleanly — the
1434    /// producer↔consumer loop closes (bound coordinates, anchored genesis, valid signatures).
1435    #[test]
1436    fn public_builders_round_trip_through_fold() {
1437        let owner = Keys::generate();
1438        let role = Role {
1439            role_id: "a".repeat(64),
1440            name: "Admin".into(),
1441            position: 1,
1442            permissions: Permissions::admin(),
1443            scope: RoleScope::Server,
1444            color: 0,
1445        };
1446        let member = "bb".repeat(32);
1447        let grant = roles::MemberGrant { member: member.clone(), role_ids: vec![role.role_id.clone()] };
1448
1449        let role_ev = build_role_edition(&owner, &role, 1, None, 100, None).unwrap();
1450        let grant_ev = build_grant_edition(&owner, &cid(), &grant, 1, None, 101, None).unwrap();
1451
1452        let folded = fold_roster(&[role_ev, grant_ev], &cid(), &Default::default());
1453        assert_eq!(folded.skipped, 0, "builders emit bound, anchored editions the fold accepts");
1454        assert!(folded.gapped_entities.is_empty());
1455        assert!(folded.roles.is_admin(&member));
1456        assert_eq!(folded.roles.role(&role.role_id).unwrap().position, 1);
1457    }
1458
1459    /// Mis-shaped chains fail LOUD at mint (W1) — not silently quarantined at fold.
1460    #[test]
1461    fn builders_reject_malformed_chain_shape() {
1462        let owner = Keys::generate();
1463        let role = Role {
1464            role_id: "a".repeat(64), name: "Admin".into(), position: 1,
1465            permissions: Permissions::admin(), scope: RoleScope::Server, color: 0,
1466        };
1467        // v1 with a prev_hash, and v>1 without one, are both rejected at build time.
1468        assert!(build_role_edition(&owner, &role, 1, Some(&[0u8; 32]), 100, None).is_err());
1469        assert!(build_role_edition(&owner, &role, 5, None, 100, None).is_err());
1470        let grant = roles::MemberGrant { member: "bb".repeat(32), role_ids: vec![role.role_id.clone()] };
1471        assert!(build_grant_edition(&owner, &cid(), &grant, 1, Some(&[0u8; 32]), 100, None).is_err());
1472        assert!(build_grant_edition(&owner, &cid(), &grant, 2, None, 100, None).is_err());
1473    }
1474
1475    /// Bad role_id / member hex is an Err, never a panic.
1476    #[test]
1477    fn builders_reject_bad_hex() {
1478        let owner = Keys::generate();
1479        let bad_role = Role {
1480            role_id: "not-hex".into(), name: "X".into(), position: 1,
1481            permissions: Permissions::admin(), scope: RoleScope::Server, color: 0,
1482        };
1483        assert!(build_role_edition(&owner, &bad_role, 1, None, 100, None).is_err());
1484        let bad_grant = roles::MemberGrant { member: "zz".repeat(32), role_ids: vec!["a".repeat(64)] };
1485        assert!(build_grant_edition(&owner, &cid(), &bad_grant, 1, None, 100, None).is_err());
1486    }
1487
1488    /// A genuine producer-built v1→v2 grant chain folds to v2 (mirrors the consumer test, but the
1489    /// editions come from the public builders — catches any drift between what they sign and what
1490    /// `edition_hash` expects as the next `prev_hash`).
1491    #[test]
1492    fn producer_built_chain_folds_to_latest() {
1493        let owner = Keys::generate();
1494        let member = "cc".repeat(32);
1495        let role_a = "a".repeat(64);
1496        let role_b = "b".repeat(64);
1497
1498        let g1 = roles::MemberGrant { member: member.clone(), role_ids: vec![role_a] };
1499        let e1 = build_grant_edition(&owner, &cid(), &g1, 1, None, 100, None).unwrap();
1500        // The next edition cites v1's edition_hash over the SAME bytes the builder signed.
1501        let member_bytes = crate::simd::hex::hex_to_bytes_32(&member);
1502        let eid = crate::community::derive::grant_locator(&cid(), &member_bytes);
1503        let v1_hash = version::edition_hash(&eid, 1, None, serde_json::to_string(&g1).unwrap().as_bytes());
1504
1505        let g2 = roles::MemberGrant { member: member.clone(), role_ids: vec![role_b.clone()] };
1506        let e2 = build_grant_edition(&owner, &cid(), &g2, 2, Some(&v1_hash), 101, None).unwrap();
1507
1508        let folded = fold_roster(&[e1, e2], &cid(), &Default::default());
1509        assert_eq!(folded.skipped, 0);
1510        assert!(folded.gapped_entities.is_empty(), "v2 links to v1 — no gap");
1511        let held: Vec<&String> = folded.roles.grants.iter().flat_map(|g| &g.role_ids).collect();
1512        assert_eq!(held, vec![&role_b]);
1513    }
1514
1515    /// The FULL control pipeline: build → seal under server-root → (wire) → open → parse → fold.
1516    #[test]
1517    fn control_edition_seals_opens_and_folds_end_to_end() {
1518        let owner = Keys::generate();
1519        let community_id = CommunityId([0x09; 32]);
1520        let epoch = Epoch(0);
1521        let role = Role {
1522            role_id: "a".repeat(64), name: "Admin".into(), position: 1,
1523            permissions: Permissions::admin(), scope: RoleScope::Server, color: 0,
1524        };
1525
1526        let inner = build_role_edition(&owner, &role, 1, None, 100, None).unwrap();
1527        let outer = seal_control_edition(&Keys::generate(), &inner, &sr(), &community_id, epoch).unwrap();
1528
1529        // The wire event hides the real author (ephemeral outer) and the content (encrypted).
1530        assert_ne!(outer.pubkey, owner.public_key(), "outer is ephemeral-signed");
1531        assert!(!outer.content.contains(&role.role_id), "inner content is encrypted on the wire");
1532
1533        // A wrong server-root can't open it — also how cross-community replay is rejected.
1534        assert!(open_control_edition(&outer, &ServerRootKey([0xAA; 32])).is_err());
1535
1536        // Members open → parse → fold: the role lands, real authorship preserved.
1537        let reopened = open_control_edition(&outer, &sr()).unwrap();
1538        assert_eq!(edition::parse_edition_inner(&reopened).unwrap().author, owner.public_key());
1539        let folded = fold_roster(&[reopened], &cid(), &Default::default());
1540        assert_eq!(folded.skipped, 0);
1541        assert!(folded.roles.role(&role.role_id).is_some(), "build→seal→open→parse→fold round-trips");
1542    }
1543
1544    /// Round-trip at a NON-ZERO epoch. The edition must be sealed at the epoch's pseudonym, NOT Epoch(0):
1545    /// sealing at the wrong epoch lands it at the wrong `#z` and a fresh joiner at epoch 4 would never find it.
1546    #[test]
1547    fn control_edition_round_trips_at_a_nonzero_epoch() {
1548        let owner = Keys::generate();
1549        let community_id = CommunityId([0x09; 32]);
1550        let role = Role {
1551            role_id: "a".repeat(64), name: "Admin".into(), position: 1,
1552            permissions: Permissions::admin(), scope: RoleScope::Server, color: 0,
1553        };
1554        let inner = build_role_edition(&owner, &role, 1, None, 100, None).unwrap();
1555        let outer = seal_control_edition(&Keys::generate(), &inner, &sr(), &community_id, Epoch(4)).unwrap();
1556
1557        // Distinct epochs address distinct pseudonyms, and the edition sits at epoch 4's — not epoch 0's.
1558        let z4 = control_pseudonym(&sr(), &community_id, Epoch(4));
1559        assert_ne!(z4, control_pseudonym(&sr(), &community_id, Epoch(0)), "epochs address distinct pseudonyms");
1560        let z_tag = outer.tags.iter().find_map(|t| {
1561            let s = t.as_slice();
1562            (s.len() >= 2 && s[0] == "z").then(|| s[1].clone())
1563        }).expect("control outer carries a z tag");
1564        assert_eq!(z_tag, z4, "sealed at the epoch-4 pseudonym, NOT epoch 0 (regression #1 guard)");
1565
1566        // A fresh joiner (empty floors) at epoch 4 still opens + folds it.
1567        let reopened = open_control_edition(&outer, &sr()).unwrap();
1568        let folded = fold_roster(&[reopened], &cid(), &Default::default());
1569        assert!(folded.roles.role(&role.role_id).is_some(), "seal→open→fold round-trips at a non-zero epoch");
1570    }
1571
1572    /// Golden vector — the control-plane pseudonym is a wire coordinate other clients must reproduce
1573    /// byte-for-byte, so pin it against fixed inputs.
1574    #[test]
1575    fn control_pseudonym_golden_vector() {
1576        let server_root = ServerRootKey([0x07; 32]);
1577        let community_id = CommunityId([0x09; 32]);
1578        assert_eq!(
1579            control_pseudonym(&server_root, &community_id, Epoch(0)),
1580            "e719f2d29ca005dfe805b1f85f696948394661c748e1f98b2df7d396260f6378"
1581        );
1582    }
1583
1584    /// `open_control_edition` rejects a non-control outer (defensive kind check).
1585    #[test]
1586    fn open_rejects_non_control_outer() {
1587        let bogus = EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_MESSAGE), "x")
1588            .finalize(&Keys::generate())
1589            .unwrap();
1590        assert!(open_control_edition(&bogus, &sr()).is_err());
1591    }
1592
1593    // --- delegation-chain authorization (#2) ---
1594
1595    #[test]
1596    fn delegation_rejects_a_self_signed_admin_grant() {
1597        // THE fail-open being closed: a member self-signs a grant giving herself Admin. It is validly
1598        // signed + bound (so it FOLDS), but authorization must DROP it — she never chained to the owner.
1599        let owner = Keys::generate();
1600        let mallory = Keys::generate();
1601        let admin = "a".repeat(64);
1602        let role_ev = role_event(&owner, &admin, 1, 1, None, 100); // owner creates Admin (pos 1)
1603        let (self_grant, _, _) = grant_event(&mallory, &mallory.public_key().to_hex(), vec![admin.clone()], 1, None, 101);
1604        let folded = fold_roster(&[role_ev, self_grant], &cid(), &Default::default());
1605        // Raw fold (binding layer) trusts it — proving authorization is the thing doing the work here.
1606        assert!(folded.roles.grants.iter().any(|g| g.member == mallory.public_key().to_hex()));
1607
1608        let authorized = authorize_delegation(&folded, Some(&owner.public_key().to_hex()));
1609        assert!(authorized.roles.iter().any(|r| r.role_id == admin), "owner-signed role stays");
1610        assert!(
1611            !authorized.grants.iter().any(|g| g.member == mallory.public_key().to_hex()),
1612            "self-signed Admin grant is REJECTED — no self-promotion"
1613        );
1614    }
1615
1616    #[test]
1617    fn delegation_accepts_owner_signed_role_and_grant() {
1618        let owner = Keys::generate();
1619        let member = Keys::generate();
1620        let admin = "a".repeat(64);
1621        let role_ev = role_event(&owner, &admin, 1, 1, None, 100);
1622        let (grant_ev, _, _) = grant_event(&owner, &member.public_key().to_hex(), vec![admin.clone()], 1, None, 101);
1623        let authorized = authorize_delegation(&fold_roster(&[role_ev, grant_ev], &cid(), &Default::default()), Some(&owner.public_key().to_hex()));
1624        assert!(authorized.roles.iter().any(|r| r.role_id == admin));
1625        assert!(authorized.grants.iter().any(|g| g.member == member.public_key().to_hex()));
1626    }
1627
1628    #[test]
1629    fn delegation_chains_owner_to_admin_to_mod() {
1630        // owner → Alice (Admin, pos 1); Alice creates Mod (pos 3) and grants Bob — a legit 3-deep chain.
1631        let owner = Keys::generate();
1632        let alice = Keys::generate();
1633        let bob = Keys::generate();
1634        let (admin, moderator) = ("a".repeat(64), "b".repeat(64));
1635        let r_admin = role_event(&owner, &admin, 1, 1, None, 100);
1636        let (g_alice, _, _) = grant_event(&owner, &alice.public_key().to_hex(), vec![admin.clone()], 1, None, 101);
1637        let r_mod = role_event(&alice, &moderator, 3, 1, None, 102); // Alice (admin) creates a lower role
1638        let (g_bob, _, _) = grant_event(&alice, &bob.public_key().to_hex(), vec![moderator.clone()], 1, None, 103);
1639        let authorized = authorize_delegation(
1640            &fold_roster(&[r_admin, g_alice, r_mod, g_bob], &cid(), &Default::default()),
1641            Some(&owner.public_key().to_hex()),
1642        );
1643        assert!(authorized.roles.iter().any(|r| r.role_id == moderator), "admin Alice could create Mod");
1644        assert!(authorized.grants.iter().any(|g| g.member == alice.public_key().to_hex()), "owner→Alice Admin");
1645        assert!(authorized.grants.iter().any(|g| g.member == bob.public_key().to_hex()), "Alice→Bob Mod (delegated)");
1646    }
1647
1648    #[test]
1649    fn delegation_demoted_admins_delegated_grant_is_dropped() {
1650        // version-pinned delegation, proven SUBSUMED by the floor-fold + current-roster fixpoint (no
1651        // `vac` citation on the delegation plane): owner→Alice (admin v1), Alice→Bob (mod). The owner then
1652        // REVOKES Alice (her grant v2 = empty). After the fold her grant head is the empty revoke, so she
1653        // is not an authorized admin → her delegated grant of Bob is DROPPED (refuse-superseded).
1654        let owner = Keys::generate();
1655        let alice = Keys::generate();
1656        let bob = Keys::generate();
1657        let (admin, moderator) = ("a".repeat(64), "b".repeat(64));
1658        let r_admin = role_event(&owner, &admin, 1, 1, None, 100);
1659        let (g_alice1, a_eid, a_c1) = grant_event(&owner, &alice.public_key().to_hex(), vec![admin.clone()], 1, None, 101);
1660        let a_v1 = version::edition_hash(&a_eid, 1, None, a_c1.as_bytes());
1661        let (g_alice2, _, _) = grant_event(&owner, &alice.public_key().to_hex(), vec![], 2, Some(&a_v1), 102); // revoke
1662        let r_mod = role_event(&alice, &moderator, 3, 1, None, 103);
1663        let (g_bob, _, _) = grant_event(&alice, &bob.public_key().to_hex(), vec![moderator.clone()], 1, None, 104);
1664
1665        let folded = fold_roster(&[r_admin, g_alice1, g_alice2, r_mod, g_bob], &cid(), &Default::default());
1666        let authorized = authorize_delegation(&folded, Some(&owner.public_key().to_hex()));
1667        assert!(!authorized.grants.iter().any(|g| g.member == alice.public_key().to_hex()), "Alice's admin grant is revoked");
1668        assert!(
1669            !authorized.grants.iter().any(|g| g.member == bob.public_key().to_hex()),
1670            "a since-demoted admin's delegated grant is dropped — version-pinning falls out, no citation needed"
1671        );
1672    }
1673
1674    #[test]
1675    fn delegation_floor_blocks_a_rolled_back_admin_grant() {
1676        // The refuse-downgrade FLOOR protects the DELEGATION plane too: we already hold Alice's grant at
1677        // v2 (her revoke), but a withholding relay re-serves only her v1 admin grant. v1 is below the
1678        // floor → refused → Alice is absent from the fold → her delegated grant of Bob is dropped. This
1679        // is the delegation-plane analogue of the banlist `withheld_revocation` test.
1680        let owner = Keys::generate();
1681        let alice = Keys::generate();
1682        let bob = Keys::generate();
1683        let (admin, moderator) = ("a".repeat(64), "b".repeat(64));
1684        let r_admin = role_event(&owner, &admin, 1, 1, None, 100);
1685        let (g_alice1, a_eid, _) = grant_event(&owner, &alice.public_key().to_hex(), vec![admin.clone()], 1, None, 101);
1686        let r_mod = role_event(&alice, &moderator, 3, 1, None, 102);
1687        let (g_bob, _, _) = grant_event(&alice, &bob.public_key().to_hex(), vec![moderator.clone()], 1, None, 103);
1688
1689        let mut floors = std::collections::HashMap::new();
1690        floors.insert(crate::simd::hex::bytes_to_hex_32(&a_eid), (2u64, [0x9Au8; 32])); // held floor = v2
1691        let folded = fold_roster(&[r_admin, g_alice1, r_mod, g_bob], &cid(), &floors);
1692        let authorized = authorize_delegation(&folded, Some(&owner.public_key().to_hex()));
1693        assert!(!authorized.grants.iter().any(|g| g.member == alice.public_key().to_hex()), "rolled-back Alice grant refused");
1694        assert!(
1695            !authorized.grants.iter().any(|g| g.member == bob.public_key().to_hex()),
1696            "Bob's delegated grant dropped — the floor blocks the rollback that would re-authorize Alice"
1697        );
1698    }
1699
1700    #[test]
1701    fn delegation_admin_cannot_grant_a_peer_admin() {
1702        // escalation defense: a position-1 admin cannot grant another position-1 Admin (1 !< 1).
1703        let owner = Keys::generate();
1704        let alice = Keys::generate();
1705        let bob = Keys::generate();
1706        let admin = "a".repeat(64);
1707        let r_admin = role_event(&owner, &admin, 1, 1, None, 100);
1708        let (g_alice, _, _) = grant_event(&owner, &alice.public_key().to_hex(), vec![admin.clone()], 1, None, 101);
1709        let (g_bob, _, _) = grant_event(&alice, &bob.public_key().to_hex(), vec![admin.clone()], 1, None, 102);
1710        let authorized = authorize_delegation(
1711            &fold_roster(&[r_admin, g_alice, g_bob], &cid(), &Default::default()),
1712            Some(&owner.public_key().to_hex()),
1713        );
1714        assert!(authorized.grants.iter().any(|g| g.member == alice.public_key().to_hex()), "owner→Alice ok");
1715        assert!(
1716            !authorized.grants.iter().any(|g| g.member == bob.public_key().to_hex()),
1717            "an admin cannot grant a PEER-rank Admin"
1718        );
1719    }
1720
1721    #[test]
1722    fn delegation_rejects_a_circular_grant_with_no_owner_root() {
1723        // The headline adversarial case: A grants B Admin, B grants A Admin — neither chains to the
1724        // owner. The fixpoint cannot bootstrap a cycle (no owner-rooted seed), so NEITHER is accepted.
1725        let owner = Keys::generate(); // a real owner exists but is NOT party to these grants
1726        let a = Keys::generate();
1727        let b = Keys::generate();
1728        let admin = "a".repeat(64);
1729        let r_admin = role_event(&owner, &admin, 1, 1, None, 100);
1730        let (g_ab, _, _) = grant_event(&a, &b.public_key().to_hex(), vec![admin.clone()], 1, None, 101);
1731        let (g_ba, _, _) = grant_event(&b, &a.public_key().to_hex(), vec![admin.clone()], 1, None, 102);
1732        let authorized = authorize_delegation(
1733            &fold_roster(&[r_admin, g_ab, g_ba], &cid(), &Default::default()),
1734            Some(&owner.public_key().to_hex()),
1735        );
1736        assert!(authorized.roles.iter().any(|r| r.role_id == admin), "owner's role stays");
1737        assert!(authorized.grants.is_empty(), "a circular mutual-admin grant cannot bootstrap without the owner");
1738    }
1739
1740    #[test]
1741    fn delegation_rejects_a_position_0_role_even_owner_signed() {
1742        // Position 0 is reserved to the owner attestation. A role at pos 0 is rejected regardless
1743        // of signer — a non-owner can't outrank pos 0, and even the owner may not mint a pos-0 ROLE.
1744        let owner = Keys::generate();
1745        let mallory = Keys::generate();
1746        let by_mallory = authorize_delegation(
1747            &fold_roster(&[role_event(&mallory, &"e".repeat(64), 0, 1, None, 100)], &cid(), &Default::default()),
1748            Some(&owner.public_key().to_hex()),
1749        );
1750        assert!(by_mallory.roles.is_empty(), "a non-owner cannot mint a position-0 role");
1751        let by_owner = authorize_delegation(
1752            &fold_roster(&[role_event(&owner, &"f".repeat(64), 0, 1, None, 100)], &cid(), &Default::default()),
1753            Some(&owner.public_key().to_hex()),
1754        );
1755        assert!(by_owner.roles.is_empty(), "position 0 is the attestation's — even the owner can't mint a pos-0 role");
1756    }
1757
1758    #[test]
1759    fn delegation_unproven_community_yields_empty_roster() {
1760        // No owner attestation → no root of the chain → nothing is authorized (fail closed).
1761        let owner = Keys::generate();
1762        let role_ev = role_event(&owner, &"a".repeat(64), 1, 1, None, 100);
1763        let authorized = authorize_delegation(&fold_roster(&[role_ev], &cid(), &Default::default()), None);
1764        assert!(authorized.roles.is_empty() && authorized.grants.is_empty(), "no root ⇒ empty authorized roster");
1765    }
1766}