1use nostr_sdk::prelude::{Event, EventId, JsonUtil, Keys, Tag, ToBech32};
11
12use super::invite::CommunityInvite;
13use super::public_invite::{
14 self, build_public_invite_event, locator_hex, parse_public_invite_event, PublicInviteBundle,
15};
16use super::send::{delete_own_message, publish_signed_message};
17use super::transport::{Query, Transport};
18use super::{Channel, Community};
19use crate::state::SessionGuard;
20use crate::stored_event::event_kind;
21
22async fn active_signer() -> Result<std::sync::Arc<dyn nostr_sdk::prelude::NostrSigner>, String> {
29 if let Some(client) = crate::state::nostr_client() {
30 if let Ok(s) = client.signer().await {
31 return Ok(s);
32 }
33 }
34 let keys = crate::state::MY_SECRET_KEY.to_keys().ok_or("no signer available (no client and no local key)")?;
35 Ok(std::sync::Arc::new(keys))
36}
37
38pub const MAX_COMMUNITIES: usize = 50;
42
43fn enforce_community_cap() -> Result<(), String> {
47 let held = super::list::load_local_list().entries.len();
48 if held >= MAX_COMMUNITIES {
49 return Err(format!(
50 "You've reached the limit of {} communities. Leave one to join another.",
51 MAX_COMMUNITIES
52 ));
53 }
54 Ok(())
55}
56
57pub async fn create_community<T: Transport + ?Sized>(
62 transport: &T,
63 name: &str,
64 default_channel_name: &str,
65 relays: Vec<String>,
66) -> Result<Community, String> {
67 let session = SessionGuard::capture();
68 enforce_community_cap()?;
69 let mut community = Community::create(name, default_channel_name, relays);
70 let owner_pk = crate::state::my_public_key().ok_or("cannot create a community without an identity")?;
76 let unsigned = super::owner::build_owner_attestation_unsigned(owner_pk, &community.id.to_hex());
77 let attestation = if let Some(keys) = crate::state::MY_SECRET_KEY.to_keys().filter(|k| k.public_key() == owner_pk) {
81 unsigned.sign_with_keys(&keys).map_err(|e| format!("sign owner attestation: {e}"))?
82 } else if let Some(client) = crate::state::nostr_client() {
83 let signer = client.signer().await.map_err(|e| format!("no signer for owner attestation: {e}"))?;
84 unsigned.sign(&signer).await.map_err(|e| format!("sign owner attestation: {e}"))?
85 } else {
86 return Err("cannot create a community without an identity signer (the owner attestation is mandatory)".to_string());
87 };
88 community.owner_attestation = Some(attestation.as_json());
89 if !session.is_valid() {
91 return Err("account changed during community creation".to_string());
92 }
93 crate::db::community::save_community(&community)?;
99
100 let signer = active_signer().await?;
103 let cid = community.id.to_hex();
104 let created = std::time::SystemTime::now()
105 .duration_since(std::time::UNIX_EPOCH)
106 .map(|d| d.as_secs())
107 .unwrap_or(0);
108
109 let admin = super::roles::Role::admin(crate::simd::hex::bytes_to_hex_32(&super::random_32()));
117 let root_meta = super::metadata::CommunityMetadata::of(&community);
118 let root_inner = super::roster::build_community_root_edition_unsigned(owner_pk, &community.id, &root_meta, 1, None, created, None)?
119 .sign(&signer).await.map_err(|e| format!("sign genesis group-root: {e}"))?;
120 let role_inner = super::roster::build_role_edition_unsigned(owner_pk, &admin, 1, None, created, None)?
121 .sign(&signer).await.map_err(|e| format!("sign genesis admin-role: {e}"))?;
122 let mut heads: Vec<(String, [u8; 32], Option<[u8; 32]>)> = vec![
126 (cid.clone(), super::version::edition_hash(&community.id.0, 1, None, root_inner.content.as_bytes()), Some(root_inner.id.to_bytes())),
127 (admin.role_id.clone(), super::version::edition_hash(&crate::simd::hex::hex_to_bytes_32(&admin.role_id), 1, None, role_inner.content.as_bytes()), None),
128 ];
129 let mut to_publish: Vec<Event> = vec![
130 super::roster::seal_control_edition(&Keys::generate(), &root_inner, &community.server_root_key, &community.id, community.server_root_epoch)?,
131 super::roster::seal_control_edition(&Keys::generate(), &role_inner, &community.server_root_key, &community.id, community.server_root_epoch)?,
132 ];
133 for channel in &community.channels {
134 let meta = super::metadata::ChannelMetadata { name: channel.name.clone() };
135 let inner = super::roster::build_channel_metadata_edition_unsigned(owner_pk, &channel.id, &meta, 1, None, created, None)?
136 .sign(&signer).await.map_err(|e| format!("sign genesis channel-metadata: {e}"))?;
137 heads.push((channel.id.to_hex(), super::version::edition_hash(&channel.id.0, 1, None, inner.content.as_bytes()), Some(inner.id.to_bytes())));
138 to_publish.push(super::roster::seal_control_edition(&Keys::generate(), &inner, &community.server_root_key, &community.id, community.server_root_epoch)?);
139 }
140 for outer in &to_publish {
144 transport.publish_durable(outer, &community.relays).await?;
145 }
146 if session.is_valid() {
149 for (entity_hex, hash, inner_id) in &heads {
150 let _ = match inner_id {
151 Some(id) => crate::db::community::set_edition_head_with_id(&cid, entity_hex, 1, hash, id),
152 None => crate::db::community::set_edition_head(&cid, entity_hex, 1, hash),
153 };
154 }
155 let roster = super::roles::CommunityRoles { roles: vec![admin], grants: Vec::new() };
156 let _ = crate::db::community::set_community_roles(&cid, &roster, created as i64);
157 }
158 Ok(community)
159}
160
161pub async fn send_message<T: Transport + ?Sized>(
164 transport: &T,
165 community: &Community,
166 channel: &Channel,
167 author: &Keys,
168 content: &str,
169 ms: u64,
170) -> Result<Event, String> {
171 let session = SessionGuard::capture();
172 let inner = super::envelope::build_inner_event(author.public_key(), &channel.id, channel.epoch, content, ms, None)
176 .sign_with_keys(author)
177 .map_err(|e| e.to_string())?;
178 let (outer, ephemeral) = publish_signed_message(transport, community, channel, &inner, false).await?;
179 if !session.is_valid() {
182 return Err("account changed during send; not persisting message key".to_string());
183 }
184 crate::db::community::store_message_key(&inner.id.to_hex(), &outer.id.to_hex(), &ephemeral, &community.relays)?;
185 Ok(outer)
186}
187
188pub async fn send_signed_message<T: Transport + ?Sized>(
193 transport: &T,
194 community: &Community,
195 channel: &Channel,
196 inner: &Event,
197) -> Result<Event, String> {
198 let session = SessionGuard::capture();
199 let (outer, ephemeral) = publish_signed_message(transport, community, channel, inner, false).await?;
200 if !session.is_valid() {
201 return Err("account changed during send; not persisting message key".to_string());
202 }
203 crate::db::community::store_message_key(&inner.id.to_hex(), &outer.id.to_hex(), &ephemeral, &community.relays)?;
204 Ok(outer)
205}
206
207pub async fn build_presence(
217 channel: &Channel,
218 joined: bool,
219 attribution: Option<(String, Option<String>)>,
220) -> Result<nostr_sdk::Event, String> {
221 let author_pk = crate::state::my_public_key().ok_or("not logged in")?;
222 let ms = std::time::SystemTime::now()
223 .duration_since(std::time::UNIX_EPOCH)
224 .map(|d| d.as_millis() as u64)
225 .unwrap_or(0);
226 let content = match (joined, attribution) {
227 (false, _) => "leave".to_string(),
228 (true, Some((by, label))) => serde_json::json!({ "by": by, "l": label }).to_string(),
229 (true, None) => "join".to_string(),
230 };
231 let unsigned = super::envelope::build_inner_typed(
232 author_pk, &channel.id, channel.epoch, event_kind::COMMUNITY_PRESENCE, &content, ms, None, &[],
233 );
234 let signer = active_signer().await?;
235 unsigned.sign(&signer).await.map_err(|e| format!("Failed to sign presence: {e}"))
236}
237
238pub async fn publish_presence_event<T: Transport + ?Sized>(
240 transport: &T,
241 community: &Community,
242 channel: &Channel,
243 inner: &nostr_sdk::Event,
244) -> Result<(), String> {
245 let _ = publish_signed_message(transport, community, channel, inner, true).await?;
246 Ok(())
247}
248
249pub async fn publish_presence<T: Transport + ?Sized>(
250 transport: &T,
251 community: &Community,
252 channel: &Channel,
253 joined: bool,
254 attribution: Option<(String, Option<String>)>,
255) -> Result<(), String> {
256 let inner = build_presence(channel, joined, attribution).await?;
257 publish_presence_event(transport, community, channel, &inner).await
258}
259
260pub async fn publish_webxdc_signal<T: Transport + ?Sized>(
267 transport: &T,
268 community: &Community,
269 channel: &Channel,
270 topic_id: &str,
271 node_addr: Option<&str>,
272) -> Result<(), String> {
273 let author_pk = crate::state::my_public_key().ok_or("not logged in")?;
274 let ms = std::time::SystemTime::now()
275 .duration_since(std::time::UNIX_EPOCH)
276 .map(|d| d.as_millis() as u64)
277 .unwrap_or(0);
278 let content = crate::webxdc::peer_signal_content(topic_id, node_addr);
279 let unsigned = super::envelope::build_inner_typed(
280 author_pk, &channel.id, channel.epoch, event_kind::COMMUNITY_WEBXDC, &content, ms, None, &[],
281 );
282 let signer = active_signer().await?;
283 let inner = unsigned.sign(&signer).await.map_err(|e| format!("Failed to sign webxdc signal: {e}"))?;
284 let _ = publish_signed_message(transport, community, channel, &inner, true).await?;
285 Ok(())
286}
287
288pub async fn publish_typing_signal<T: Transport + ?Sized>(
294 transport: &T,
295 community: &Community,
296 channel: &Channel,
297) -> Result<(), String> {
298 let author_pk = crate::state::my_public_key().ok_or("not logged in")?;
299 let ms = std::time::SystemTime::now()
300 .duration_since(std::time::UNIX_EPOCH)
301 .map(|d| d.as_millis() as u64)
302 .unwrap_or(0);
303 let unsigned = super::envelope::build_inner_typed(
304 author_pk, &channel.id, channel.epoch, event_kind::COMMUNITY_TYPING, "typing", ms, None, &[],
305 );
306 let signer = active_signer().await?;
307 let inner = unsigned.sign(&signer).await.map_err(|e| format!("Failed to sign typing signal: {e}"))?;
308 let _ = publish_signed_message(transport, community, channel, &inner, false).await?;
309 Ok(())
310}
311
312pub async fn persist_webxdc_signal(
319 channel_hex: &str,
320 npub: &str,
321 topic_id: &str,
322 node_addr: Option<&str>,
323 event_id: &str,
324 created_at: u64,
325) {
326 if crate::db::events::event_exists(event_id).unwrap_or(true) {
327 return;
328 }
329 let now_secs = std::time::SystemTime::now()
332 .duration_since(std::time::UNIX_EPOCH)
333 .unwrap_or_default()
334 .as_secs();
335 let created_at = created_at.min(now_secs + 300);
336 let Ok(chat_id) = crate::db::id_cache::get_or_create_chat_id(channel_hex) else { return };
337 let mut tags = vec![
338 vec!["webxdc-topic".to_string(), topic_id.to_string()],
339 vec!["d".to_string(), "vector-webxdc-peer".to_string()],
340 ];
341 if let Some(addr) = node_addr {
342 tags.push(vec!["webxdc-node-addr".to_string(), addr.to_string()]);
343 }
344 let event = crate::stored_event::StoredEvent {
345 id: event_id.to_string(),
346 kind: crate::stored_event::event_kind::APPLICATION_SPECIFIC,
347 chat_id,
348 user_id: None,
349 content: if node_addr.is_some() { "peer-advertisement" } else { "peer-left" }.to_string(),
350 tags,
351 reference_id: Some(topic_id.to_string()),
352 created_at,
353 received_at: std::time::SystemTime::now()
354 .duration_since(std::time::UNIX_EPOCH)
355 .unwrap_or_default()
356 .as_millis() as u64,
357 mine: false,
358 pending: false,
359 failed: false,
360 wrapper_event_id: None,
361 npub: Some(npub.to_string()),
362 preview_metadata: None,
363 };
364 if let Err(e) = crate::db::events::save_event(&event).await {
365 crate::log_warn!("[community] failed to persist webxdc peer signal: {e}");
366 }
367}
368
369async fn strip_member_roles_on_removal<T: Transport + ?Sized>(
383 transport: &T,
384 community: &Community,
385 member_hex: &str,
386) {
387 let cid = community.id.to_hex();
388 let roster = match crate::db::community::get_community_roles(&cid) {
389 Ok(r) => r,
390 Err(_) => return,
391 };
392 let held: Vec<String> = roster
393 .grants
394 .iter()
395 .find(|g| g.member == member_hex)
396 .map(|g| g.role_ids.clone())
397 .unwrap_or_default();
398 if held.is_empty() {
399 return; }
401 for role_id in &held {
402 if caller_can_manage_role(community, &roster, role_id, member_hex).is_err() {
403 crate::log_warn!(
404 "removal: not authorized to revoke role {role_id} of {member_hex}; leaving the grant (kick/ban still neutralizes)"
405 );
406 return;
407 }
408 }
409 if let Err(e) = set_member_grant(transport, community, member_hex, Vec::new()).await {
410 crate::log_warn!("removal: role-strip publish failed for {member_hex}: {e}");
411 }
412}
413
414pub async fn publish_kick<T: Transport + ?Sized>(
415 transport: &T,
416 community: &Community,
417 channel: &Channel,
418 target_hex: &str,
419) -> Result<String, String> {
420 let author_pk = crate::state::my_public_key().ok_or("not logged in")?;
421 let me = author_pk.to_hex();
422 let cid = community.id.to_hex();
423 {
426 let owner = proven_owner_hex(community);
427 let roster = crate::db::community::get_community_roles(&cid).unwrap_or_default();
428 if !roster.can_act_on_member(&me, owner.as_deref(), target_hex, super::roles::Permissions::KICK) {
429 return Err("you can't kick a member who outranks you (or the owner)".to_string());
430 }
431 }
432 let ms = std::time::SystemTime::now()
433 .duration_since(std::time::UNIX_EPOCH)
434 .map(|d| d.as_millis() as u64)
435 .unwrap_or(0);
436 let citation = authority_citation(community, &me);
438 let extra: Vec<nostr_sdk::prelude::Tag> = citation.iter().map(|c| c.to_tag()).collect();
439 let unsigned = super::envelope::build_inner_full(
440 author_pk, &channel.id, channel.epoch, event_kind::COMMUNITY_KICK, target_hex, ms, None, &[], &extra,
441 );
442 let signer = active_signer().await?;
443 let inner = unsigned.sign(&signer).await.map_err(|e| format!("Failed to sign kick: {e}"))?;
444 publish_signed_message(transport, community, channel, &inner, true).await?;
445 strip_member_roles_on_removal(transport, community, target_hex).await;
448 Ok(inner.id.to_hex())
450}
451
452
453pub async fn publish_banlist<T: Transport + ?Sized>(
459 transport: &T,
460 community: &Community,
461 banned_hex: &[String],
462) -> Result<(), String> {
463 let session = SessionGuard::capture();
464 let cid = community.id.to_hex();
465 let signer = active_signer().await?;
468 let actor_pk = crate::state::my_public_key().ok_or("no local identity to sign the banlist edition")?;
469 {
474 let me = actor_pk.to_hex();
475 let owner = proven_owner_hex(community);
476 let roster = crate::db::community::get_community_roles(&cid).unwrap_or_default();
477 let current: std::collections::HashSet<String> =
478 crate::db::community::get_community_banlist(&cid).unwrap_or_default().into_iter().collect();
479 let next: std::collections::HashSet<&str> = banned_hex.iter().map(|s| s.as_str()).collect();
480 let added = banned_hex.iter().filter(|n| !current.contains(n.as_str()));
481 let removed = current.iter().filter(|n| !next.contains(n.as_str()));
482 for target in added.chain(removed) {
483 if !roster.can_act_on_member(&me, owner.as_deref(), target, super::roles::Permissions::BAN) {
484 return Err("you can't ban or unban a member who outranks you (or the owner)".to_string());
485 }
486 }
487 }
488 {
494 let prev: std::collections::HashSet<String> =
495 crate::db::community::get_community_banlist(&cid).unwrap_or_default().into_iter().collect();
496 let adds = banned_hex.iter().any(|n| !prev.contains(n.as_str()));
497 let cut_needed = (adds || crate::db::community::get_read_cut_pending(&cid)?) && !is_public(community)?;
498 if cut_needed && crate::state::MY_SECRET_KEY.to_keys().is_none() {
499 return Err("Banning someone from a private community cuts their read access, which needs a key rotation your account can't perform: it signs remotely (a NIP-46 bunker), and a rotation requires a local key. Ask a community admin who holds a local key to carry out the ban.".to_string());
500 }
501 }
502 let entity_id = super::derive::banlist_locator(&community.id);
504 let entity_hex = crate::simd::hex::bytes_to_hex_32(&entity_id);
505 let (version, prev_hash) = match crate::db::community::get_edition_head(&cid, &entity_hex)? {
506 Some((v, h)) => (v + 1, Some(h)),
507 None => (1, None),
508 };
509 let created_at = std::time::SystemTime::now()
510 .duration_since(std::time::UNIX_EPOCH)
511 .map(|d| d.as_secs())
512 .unwrap_or(0);
513 let citation = authority_citation(community, &actor_pk.to_hex());
516 let unsigned = super::roster::build_banlist_edition_unsigned(actor_pk, &community.id, banned_hex, version, prev_hash.as_ref(), created_at, citation.as_ref())?;
517 let inner = unsigned.sign(&signer).await.map_err(|e| format!("sign banlist edition: {e}"))?;
518 let outer = super::roster::seal_control_edition(&Keys::generate(), &inner, &community.server_root_key, &community.id, community.server_root_epoch)?;
519 let self_hash = super::version::edition_hash(&entity_id, version, prev_hash.as_ref(), inner.content.as_bytes());
520
521 let newly_added: Vec<String> = {
524 let prev: std::collections::HashSet<String> =
525 crate::db::community::get_community_banlist(&cid).unwrap_or_default().into_iter().collect();
526 banned_hex.iter().filter(|n| !prev.contains(n.as_str())).cloned().collect()
527 };
528 let newly_banned = !newly_added.is_empty();
529
530 transport.publish_durable(&outer, &community.relays).await?;
534 if session.is_valid() {
535 crate::db::community::set_community_banlist(&cid, banned_hex, created_at as i64)?;
536 crate::db::community::set_edition_head(&cid, &entity_hex, version, &self_hash)?;
537 }
538
539 if session.is_valid() {
544 for member_hex in &newly_added {
545 strip_member_roles_on_removal(transport, community, member_hex).await;
546 }
547 }
548
549 let need_cut = (newly_banned || crate::db::community::get_read_cut_pending(&cid)?)
564 && session.is_valid()
565 && !is_public(community)?;
566 if need_cut {
567 run_read_cut(transport, community, newly_banned).await?;
570 }
571 Ok(())
572}
573
574pub fn am_i_banned(community: &Community) -> bool {
580 let me = match crate::state::my_public_key() {
581 Some(p) => p.to_hex(),
582 None => return false,
583 };
584 crate::db::community::get_community_banlist(&community.id.to_hex())
585 .unwrap_or_default()
586 .iter()
587 .any(|b| b == &me)
588}
589
590pub async fn retry_pending_read_cut<T: Transport + ?Sized>(
596 transport: &T,
597 community: &Community,
598) -> Result<(), String> {
599 let cid = community.id.to_hex();
600 if !crate::db::community::get_read_cut_pending(&cid)? {
601 return Ok(());
602 }
603 if is_public(community)? {
604 crate::db::community::set_read_cut_pending(&cid, false)?; return Ok(());
606 }
607 let fresh = crate::db::community::load_community(&community.id)?.ok_or("community no longer present")?;
612 run_read_cut(transport, &fresh, false).await
613}
614
615async fn fetch_control_folded<T: Transport + ?Sized>(
625 transport: &T,
626 community: &Community,
627) -> Result<super::roster::FoldedRoster, String> {
628 let z_tags = vec![super::roster::control_pseudonym(&community.server_root_key, &community.id, community.server_root_epoch)];
633 let query = Query { kinds: vec![event_kind::COMMUNITY_CONTROL], z_tags, ..Default::default() };
637 let raw = transport.fetch(&query, &community.relays).await?;
638 let inner_editions: Vec<Event> = raw
641 .iter()
642 .take(super::roster::MAX_CONTROL_EDITIONS)
643 .filter_map(|ev| super::roster::open_control_edition(ev, &community.server_root_key).ok())
644 .collect();
645 let fetched = inner_editions.len();
650 let current_epoch = community.server_root_epoch.0;
656 let floors: std::collections::HashMap<String, (u64, [u8; 32])> =
657 crate::db::community::get_all_edition_heads_epoched(&community.id.to_hex())?
658 .into_iter()
659 .filter(|(_, (epoch, _, _))| *epoch == current_epoch)
660 .map(|(entity, (_epoch, version, hash))| (entity, (version, hash)))
661 .collect();
662 let mut folded = super::roster::fold_roster(&inner_editions, &community.id, &floors);
663 folded.fetched = fetched; Ok(folded)
665}
666
667pub async fn fetch_and_apply_control<T: Transport + ?Sized>(
673 transport: &T,
674 community: &Community,
675) -> Result<usize, String> {
676 let session = SessionGuard::capture();
677 let cid = community.id.to_hex();
678 if crate::db::community::get_community_dissolved(&cid)? {
681 return Ok(0);
682 }
683 let folded = fetch_control_folded(transport, community).await?;
684 if !session.is_valid() {
685 return Err("account changed during control fetch".to_string());
686 }
687 if let Some(owner) = proven_owner_hex(community) {
697 let by_fold = folded.dissolved_by.iter().any(|s| s.to_hex() == owner);
698 let by_probe = !by_fold && dissolved_tombstone_present(transport, community, &owner).await;
699 if by_fold || by_probe {
700 let _ = fetch_and_apply_banlist_inner(transport, community, Some(folded.clone())).await;
703 let _ = fetch_and_apply_roles_inner(transport, community, Some(folded.clone())).await;
704 let _ = fetch_and_apply_invite_links_inner(transport, community, Some(folded.clone())).await;
705 let _ = fetch_and_apply_metadata_inner(transport, community, Some(folded.clone())).await;
706 if session.is_valid() {
707 crate::db::community::set_community_dissolved(&cid)?;
708 crate::emit_event("community_refreshed", &serde_json::json!({ "community_id": cid }));
712 }
713 return Ok(folded.fetched);
714 }
715 }
716 let fetched = folded.fetched;
719 let _ = fetch_and_apply_banlist_inner(transport, community, Some(folded.clone())).await;
720 let _ = fetch_and_apply_roles_inner(transport, community, Some(folded.clone())).await;
721 let _ = fetch_and_apply_invite_links_inner(transport, community, Some(folded.clone())).await;
722 let _ = fetch_and_apply_metadata_inner(transport, community, Some(folded)).await;
723 Ok(fetched)
724}
725
726pub async fn fetch_and_apply_banlist<T: Transport + ?Sized>(
727 transport: &T,
728 community: &Community,
729) -> Result<Vec<String>, String> {
730 fetch_and_apply_banlist_inner(transport, community, None).await
731}
732
733async fn fetch_and_apply_banlist_inner<T: Transport + ?Sized>(
734 transport: &T,
735 community: &Community,
736 prefolded: Option<super::roster::FoldedRoster>,
737) -> Result<Vec<String>, String> {
738 let session = SessionGuard::capture();
739 let cid = community.id.to_hex();
740 let folded = match prefolded {
741 Some(f) => f,
742 None => fetch_control_folded(transport, community).await?,
743 };
744 let owner = proven_owner_hex(community);
747 let authorized = super::roster::authorize_delegation(&folded, owner.as_deref());
748 if !session.is_valid() {
749 return Err("account changed during banlist fetch".to_string());
750 }
751 if let (Some(author), Some(head)) = (folded.banlist_author, &folded.banlist_head) {
752 let author_hex = author.to_hex();
757 let held: std::collections::HashSet<String> =
758 crate::db::community::get_community_banlist(&cid)?.into_iter().collect();
759 let next: std::collections::HashSet<&str> = folded.banned.iter().map(|s| s.as_str()).collect();
760 let added = folded.banned.iter().filter(|n| !held.contains(n.as_str()));
761 let removed = held.iter().filter(|n| !next.contains(n.as_str()));
762 let citation = folded.banlist_head.as_ref().and_then(|h| h.citation.as_ref());
768 let banner_grant_hex = crate::simd::hex::bytes_to_hex_32(&super::derive::grant_locator(&community.id, &author.to_bytes()));
769 let pinned = super::roster::authority_citation_satisfied(&folded.heads, owner.as_deref(), &author_hex, &banner_grant_hex, citation);
770 let authed = pinned
771 && added.chain(removed).all(|target| {
772 authorized.can_act_on_member(&author_hex, owner.as_deref(), target, super::roles::Permissions::BAN)
773 });
774 let held_version = crate::db::community::get_edition_head(&cid, &head.entity_hex)?.map(|(v, _)| v).unwrap_or(0);
775 if authed && head.version > held_version {
776 crate::db::community::set_community_banlist(&cid, &folded.banned, head.version as i64)?;
777 crate::db::community::set_edition_head(&cid, &head.entity_hex, head.version, &head.self_hash)?;
778 return Ok(folded.banned);
779 }
780 }
781 crate::db::community::get_community_banlist(&cid)
783}
784
785pub async fn set_member_grant<T: Transport + ?Sized>(
790 transport: &T,
791 community: &Community,
792 member_hex: &str,
793 role_ids: Vec<String>,
794) -> Result<(), String> {
795 let session = SessionGuard::capture();
796 let signer = active_signer().await?;
799 let actor_pk = crate::state::my_public_key().ok_or("no local identity to sign the grant edition")?;
800 let cid = community.id.to_hex();
801 let grant = super::roles::MemberGrant { member: member_hex.to_string(), role_ids };
802
803 let member_bytes = crate::simd::hex::hex_to_bytes_32(member_hex);
806 let entity_id = super::derive::grant_locator(&community.id, &member_bytes);
807 let entity_hex = crate::simd::hex::bytes_to_hex_32(&entity_id);
808 let (version, prev_hash) = match crate::db::community::get_edition_head(&cid, &entity_hex)? {
809 Some((v, h)) => (v + 1, Some(h)),
810 None => (1, None),
811 };
812 let created_at = std::time::SystemTime::now()
813 .duration_since(std::time::UNIX_EPOCH)
814 .map(|d| d.as_secs())
815 .unwrap_or(0);
816
817 let citation = authority_citation(community, &actor_pk.to_hex());
824 let unsigned = super::roster::build_grant_edition_unsigned(actor_pk, &community.id, &grant, version, prev_hash.as_ref(), created_at, citation.as_ref())?;
825 let inner = unsigned.sign(&signer).await.map_err(|e| format!("sign grant edition: {e}"))?;
826 let outer = super::roster::seal_control_edition(&Keys::generate(), &inner, &community.server_root_key, &community.id, community.server_root_epoch)?;
827 let self_hash = super::version::edition_hash(&entity_id, version, prev_hash.as_ref(), inner.content.as_bytes());
831
832 let is_full_revoke = grant.role_ids.is_empty();
833 let mut roster = crate::db::community::get_community_roles(&cid)?;
835 roster.grants.retain(|g| g.member != member_hex);
836 if !grant.role_ids.is_empty() {
837 roster.grants.push(grant);
838 }
839
840 transport.publish_durable(&outer, &community.relays).await?;
846 if session.is_valid() {
847 crate::db::community::set_community_roles(&cid, &roster, created_at as i64)?;
848 crate::db::community::set_edition_head(&cid, &entity_hex, version, &self_hash)?;
849 }
850
851 if is_full_revoke && session.is_valid() {
859 if let Ok(folded) = fetch_control_folded(transport, community).await {
860 if session.is_valid() {
861 let current = crate::db::community::load_community(&community.id)?.unwrap_or_else(|| community.clone());
862 if folded.root_author.map(|a| a.to_hex()).as_deref() == Some(member_hex) {
863 if let Some(meta) = &folded.root_meta {
864 let mut c = current.clone();
865 c.name = meta.name.clone();
866 c.description = meta.description.clone();
867 c.icon = meta.icon.clone();
868 c.banner = meta.banner.clone();
869 let _ = republish_community_metadata(transport, &c).await;
870 }
871 }
872 for cm in &folded.channel_meta {
873 if cm.author.to_hex() == member_hex
874 && current.channels.iter().any(|ch| ch.id.0 == cm.channel_id)
875 {
876 let _ = republish_channel_metadata(
877 transport, ¤t, &crate::community::ChannelId(cm.channel_id), &cm.meta.name,
878 ).await;
879 }
880 }
881 }
882 }
883 }
884 Ok(())
885}
886
887pub fn is_proven_owner(community: &Community) -> bool {
892 match crate::state::my_public_key() {
893 Some(me) => proven_owner_hex(community).as_deref() == Some(me.to_hex().as_str()),
894 None => false,
895 }
896}
897
898pub fn caller_can_manage_roles(community: &Community) -> bool {
902 let me = match crate::state::my_public_key() {
903 Some(p) => p,
904 None => return false,
905 };
906 let cid = community.id.to_hex();
907 let is_owner = community
908 .owner_attestation
909 .as_ref()
910 .and_then(|a| super::owner::verify_owner_attestation(a, &cid))
911 .map(|pk| pk == me)
912 .unwrap_or(false);
913 if is_owner {
914 return true; }
916 crate::db::community::get_community_roles(&cid)
917 .unwrap_or_default()
918 .has_permission(&me.to_hex(), super::roles::Permissions::MANAGE_ROLES)
919}
920
921pub fn caller_has_permission(community: &Community, permission: u64) -> bool {
925 let me = match crate::state::my_public_key() {
926 Some(p) => p,
927 None => return false,
928 };
929 crate::db::community::get_community_roles(&community.id.to_hex())
930 .unwrap_or_default()
931 .is_authorized(&me.to_hex(), proven_owner_hex(community).as_deref(), permission)
932}
933
934pub fn caller_can_manage_role_id(community: &Community, role_id: &str) -> bool {
939 let me = match crate::state::my_public_key() {
940 Some(p) => p.to_hex(),
941 None => return false,
942 };
943 let roster = crate::db::community::get_community_roles(&community.id.to_hex()).unwrap_or_default();
944 let position = match roster.role(role_id) {
945 Some(r) => r.position,
946 None => return false,
947 };
948 roster.can_manage_position(&me, proven_owner_hex(community).as_deref(), position)
949}
950
951#[derive(Debug, Clone, Default, serde::Serialize)]
956pub struct CommunityCapabilities {
957 pub manage_metadata: bool,
958 pub manage_channels: bool,
959 pub create_invite: bool,
960 pub kick: bool,
961 pub ban: bool,
962 pub manage_messages: bool,
963 pub manage_roles: bool,
964}
965
966pub fn caller_capabilities(community: &Community) -> CommunityCapabilities {
967 use super::roles::Permissions as P;
968 let me_hex = match crate::state::my_public_key() {
969 Some(p) => p.to_hex(),
970 None => return CommunityCapabilities::default(),
971 };
972 let owner = proven_owner_hex(community);
973 let roster = crate::db::community::get_community_roles(&community.id.to_hex()).unwrap_or_default();
974 let has = |bit: u64| roster.is_authorized(&me_hex, owner.as_deref(), bit);
975 CommunityCapabilities {
976 manage_metadata: has(P::MANAGE_METADATA),
977 manage_channels: has(P::MANAGE_CHANNELS),
978 create_invite: has(P::CREATE_INVITE),
979 kick: has(P::KICK),
980 ban: has(P::BAN),
981 manage_messages: has(P::MANAGE_MESSAGES),
982 manage_roles: has(P::MANAGE_ROLES),
983 }
984}
985
986fn authority_citation(community: &Community, actor_hex: &str) -> Option<super::edition::AuthorityCitation> {
994 if proven_owner_hex(community).as_deref() == Some(actor_hex) {
995 return None;
996 }
997 let cid = community.id.to_hex();
998 let actor_bytes = crate::simd::hex::hex_to_bytes_32(actor_hex);
999 let entity_id = super::derive::grant_locator(&community.id, &actor_bytes);
1000 let entity_hex = crate::simd::hex::bytes_to_hex_32(&entity_id);
1001 crate::db::community::get_edition_head(&cid, &entity_hex)
1002 .ok()
1003 .flatten()
1004 .map(|(version, edition_hash)| super::edition::AuthorityCitation { entity_id, version, edition_hash })
1005}
1006
1007fn proven_owner_hex(community: &Community) -> Option<String> {
1010 let cid = community.id.to_hex();
1011 community
1012 .owner_attestation
1013 .as_ref()
1014 .and_then(|a| super::owner::verify_owner_attestation(a, &cid))
1015 .map(|pk| pk.to_hex())
1016}
1017
1018pub fn can_moderation_hide(community: &Community, actor_hex: &str, author_hex: &str) -> bool {
1024 let to_hex = |s: &str| nostr_sdk::PublicKey::parse(s).map(|pk| pk.to_hex()).unwrap_or_else(|_| s.to_string());
1031 let actor = to_hex(actor_hex);
1032 let author = to_hex(author_hex);
1033 let owner = proven_owner_hex(community);
1034 let roster = crate::db::community::get_community_roles(&community.id.to_hex()).unwrap_or_default();
1035 roster.can_act_on_member(&actor, owner.as_deref(), &author, super::roles::Permissions::MANAGE_MESSAGES)
1036}
1037
1038fn rotator_is_authorized(
1046 cid: &str,
1047 roster: &super::roles::CommunityRoles,
1048 owner_hex: Option<&str>,
1049 rotator_hex: &str,
1050 permission: u64,
1051) -> bool {
1052 if owner_hex != Some(rotator_hex)
1053 && crate::db::community::get_community_banlist(cid)
1054 .unwrap_or_default()
1055 .iter()
1056 .any(|b| b == rotator_hex)
1057 {
1058 return false;
1059 }
1060 roster.is_authorized(rotator_hex, owner_hex, permission)
1061}
1062
1063fn caller_can_manage_role(
1069 community: &Community,
1070 roster: &super::roles::CommunityRoles,
1071 role_id: &str,
1072 member_hex: &str,
1073) -> Result<(), String> {
1074 let me = crate::state::my_public_key().ok_or("no active identity")?.to_hex();
1075 let owner = proven_owner_hex(community);
1076 let owner_ref = owner.as_deref();
1077 let role = roster.role(role_id).ok_or("no such role")?;
1078 if !roster.can_manage_position(&me, owner_ref, role.position) {
1079 return Err("you can only manage roles below your own".to_string());
1080 }
1081 if !roster.can_manage_member(&me, owner_ref, member_hex) {
1082 return Err("you can't manage a member who outranks you".to_string());
1083 }
1084 Ok(())
1085}
1086
1087pub async fn grant_role<T: Transport + ?Sized>(
1091 transport: &T,
1092 community: &Community,
1093 member: nostr_sdk::prelude::PublicKey,
1094 role_id: &str,
1095) -> Result<(), String> {
1096 let cid = community.id.to_hex();
1097 let member_hex = member.to_hex();
1098 let roster = crate::db::community::get_community_roles(&cid)?;
1099 caller_can_manage_role(community, &roster, role_id, &member_hex)?;
1100 let mut role_ids: Vec<String> = roster
1102 .grants
1103 .iter()
1104 .find(|g| g.member == member_hex)
1105 .map(|g| g.role_ids.clone())
1106 .unwrap_or_default();
1107 if !role_ids.iter().any(|r| r == role_id) {
1108 role_ids.push(role_id.to_string());
1109 }
1110
1111 set_member_grant(transport, community, &member_hex, role_ids).await
1115}
1116
1117pub async fn revoke_role<T: Transport + ?Sized>(
1124 transport: &T,
1125 community: &Community,
1126 member: nostr_sdk::prelude::PublicKey,
1127 role_id: &str,
1128) -> Result<(), String> {
1129 let cid = community.id.to_hex();
1130 let member_hex = member.to_hex();
1131 let roster = crate::db::community::get_community_roles(&cid)?;
1132 caller_can_manage_role(community, &roster, role_id, &member_hex)?;
1133 let role_ids: Vec<String> = roster
1134 .grants
1135 .iter()
1136 .find(|g| g.member == member_hex)
1137 .map(|g| g.role_ids.iter().filter(|r| r.as_str() != role_id).cloned().collect())
1138 .unwrap_or_default();
1139 set_member_grant(transport, community, &member_hex, role_ids).await
1140}
1141
1142pub async fn fetch_and_apply_roles<T: Transport + ?Sized>(
1148 transport: &T,
1149 community: &Community,
1150) -> Result<super::roles::CommunityRoles, String> {
1151 fetch_and_apply_roles_inner(transport, community, None).await
1152}
1153
1154async fn fetch_and_apply_roles_inner<T: Transport + ?Sized>(
1155 transport: &T,
1156 community: &Community,
1157 prefolded: Option<super::roster::FoldedRoster>,
1158) -> Result<super::roles::CommunityRoles, String> {
1159 let session = SessionGuard::capture();
1160 let cid = community.id.to_hex();
1161 let folded = match prefolded {
1162 Some(f) => f,
1163 None => fetch_control_folded(transport, community).await?,
1164 };
1165
1166 if !session.is_valid() {
1167 return Err("account changed during roles fetch".to_string());
1168 }
1169 for head in &folded.heads {
1178 crate::db::community::set_edition_head(&cid, &head.entity_hex, head.version, &head.self_hash)?;
1179 }
1180 if folded.heads.is_empty() {
1185 return crate::db::community::get_community_roles(&cid);
1186 }
1187 let authorized = super::roster::authorize_delegation(&folded, proven_owner_hex(community).as_deref());
1191 crate::db::community::set_community_roles(&cid, &authorized, 0)?;
1192 Ok(authorized)
1193}
1194
1195pub async fn publish_owner_hide<T: Transport + ?Sized>(
1200 transport: &T,
1201 community: &Community,
1202 channel: &Channel,
1203 target_message_id: &str,
1204) -> Result<(), String> {
1205 let signer = active_signer().await?;
1210 let me_pk = crate::state::my_public_key().ok_or("no local identity to sign the hide")?;
1211 let me = me_pk.to_hex();
1212 {
1213 let target_author = {
1214 let st = crate::state::STATE.lock().await;
1215 st.find_message(target_message_id).and_then(|(_, m)| m.npub)
1216 };
1217 let author = target_author
1218 .ok_or("can't resolve the target message's author to authorize the hide")?;
1219 if !can_moderation_hide(community, &me, &author) {
1220 return Err("you can't hide a message from a member who outranks you (or the owner)".to_string());
1221 }
1222 }
1223 let ms = std::time::SystemTime::now()
1224 .duration_since(std::time::UNIX_EPOCH)
1225 .map(|d| d.as_millis() as u64)
1226 .unwrap_or(0);
1227 let citation = authority_citation(community, &me);
1233 let extra: Vec<Tag> = citation.iter().map(|c| c.to_tag()).collect();
1234 let inner = super::envelope::build_inner_full(
1235 me_pk, &channel.id, channel.epoch,
1236 event_kind::COMMUNITY_DELETE, "", ms, Some(target_message_id), &[], &extra,
1237 )
1238 .sign(&signer)
1239 .await
1240 .map_err(|e| format!("sign hide: {e}"))?;
1241 let _ = publish_signed_message(transport, community, channel, &inner, true).await?;
1242 Ok(())
1243}
1244
1245pub async fn delete_message<T: Transport + ?Sized>(
1250 transport: &T,
1251 message_id: &str,
1252) -> Result<(), String> {
1253 let session = SessionGuard::capture();
1254 if !session.is_valid() {
1255 return Err("account changed; aborting delete".to_string());
1256 }
1257 let (ephemeral, outer_event_id_hex, relays) = match crate::db::community::get_message_key(message_id)? {
1261 Some(v) => v,
1262 None => {
1263 return Err("no retained key for this message (not yours, or already deleted)".to_string())
1264 }
1265 };
1266 let id = EventId::from_hex(&outer_event_id_hex).map_err(|e| e.to_string())?;
1267 delete_own_message(transport, &relays, &ephemeral, id).await?;
1268 crate::db::community::delete_message_key(message_id)?;
1270 Ok(())
1271}
1272
1273pub fn accept_invite(invite: &CommunityInvite) -> Result<Community, String> {
1286 let session = SessionGuard::capture();
1287 let community = super::invite::accept_invite(invite)?; match crate::db::community::load_community(&community.id)? {
1290 Some(existing) => {
1292 if is_proven_owner(&existing) {
1293 return Err("you already own this Community".to_string());
1294 }
1295 if existing.server_root_key.as_bytes() != community.server_root_key.as_bytes() {
1299 return Err(
1300 "invite reuses a known Community id under a different authority — rejected"
1301 .to_string(),
1302 );
1303 }
1304 }
1305 None => enforce_community_cap()?,
1307 }
1308
1309 if !session.is_valid() {
1310 return Err("account changed during invite accept".to_string());
1311 }
1312 crate::db::community::save_community(&community)?;
1313 Ok(community)
1314}
1315
1316pub async fn preload_community(invite: &super::invite::CommunityInvite) {
1323 let Ok(community) = super::invite::accept_invite(invite) else { return };
1324 let Some(channel) = community.channels.first() else { return };
1325 let cid = community.id.to_hex();
1326 crate::community::cache::begin_preload(&cid);
1328 let transport = super::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1329 match super::send::fetch_channel_page(&transport, &community, channel, None, None, 50).await {
1331 Ok(page) if !page.is_empty() => crate::community::cache::finish_preload(&cid, page),
1332 _ => crate::community::cache::abort_preload(&cid),
1334 }
1335
1336 let prune_relays = community.relays.clone();
1340 let prune_id = community.id;
1341 let guard = crate::state::SessionGuard::capture();
1342 tokio::spawn(async move {
1343 tokio::time::sleep(crate::community::cache::PRELOAD_TTL).await;
1344 if !guard.is_valid() {
1345 return;
1346 }
1347 if matches!(crate::db::community::load_community(&prune_id), Ok(Some(_))) {
1350 return;
1351 }
1352 crate::community::cache::abort_preload(&prune_id.to_hex());
1354 super::transport::prune_unneeded_community_relays(&prune_relays).await;
1355 });
1356}
1357
1358pub async fn republish_community_metadata<T: Transport + ?Sized>(
1363 transport: &T,
1364 community: &Community,
1365) -> Result<(), String> {
1366 let session = SessionGuard::capture();
1367 let cid = community.id.to_hex();
1368 let signer = active_signer().await?;
1369 let actor_pk = crate::state::my_public_key().ok_or("no local identity to sign the metadata edition")?;
1370 let owner = proven_owner_hex(community);
1371 let roster = crate::db::community::get_community_roles(&cid).unwrap_or_default();
1372 if !roster.is_authorized(&actor_pk.to_hex(), owner.as_deref(), super::roles::Permissions::MANAGE_METADATA) {
1373 return Err("only a member with manage-metadata authority can edit the community".to_string());
1374 }
1375 let (version, prev_hash) = match crate::db::community::get_edition_head(&cid, &cid)? {
1380 Some((v, h)) => (v + 1, Some(h)),
1381 None => (1, None),
1382 };
1383 let created = std::time::SystemTime::now()
1384 .duration_since(std::time::UNIX_EPOCH)
1385 .map(|d| d.as_secs())
1386 .unwrap_or(0);
1387 let meta = super::metadata::CommunityMetadata::of(community);
1388 let citation = authority_citation(community, &actor_pk.to_hex());
1393 let unsigned = super::roster::build_community_root_edition_unsigned(actor_pk, &community.id, &meta, version, prev_hash.as_ref(), created, citation.as_ref())?;
1394 let inner = unsigned.sign(&signer).await.map_err(|e| format!("sign community-root edition: {e}"))?;
1395 let outer = super::roster::seal_control_edition(&Keys::generate(), &inner, &community.server_root_key, &community.id, community.server_root_epoch)?;
1396 transport.publish_durable(&outer, &community.relays).await?;
1397 if session.is_valid() {
1398 crate::db::community::save_community(community)?;
1399 let h = super::version::edition_hash(&community.id.0, version, prev_hash.as_ref(), inner.content.as_bytes());
1400 crate::db::community::set_edition_head_with_id(&cid, &cid, version, &h, &inner.id.to_bytes())?;
1403 }
1404 Ok(())
1405}
1406
1407pub async fn republish_channel_metadata<T: Transport + ?Sized>(
1413 transport: &T,
1414 community: &Community,
1415 channel_id: &crate::community::ChannelId,
1416 new_name: &str,
1417) -> Result<(), String> {
1418 let session = SessionGuard::capture();
1419 let cid = community.id.to_hex();
1420 let ch_hex = channel_id.to_hex();
1421 if !community.channels.iter().any(|c| &c.id == channel_id) {
1422 return Err("no such channel in this community".to_string());
1423 }
1424 let signer = active_signer().await?;
1425 let actor_pk = crate::state::my_public_key().ok_or("no local identity to sign the channel metadata edition")?;
1426 let owner = proven_owner_hex(community);
1427 let roster = crate::db::community::get_community_roles(&cid).unwrap_or_default();
1428 if !roster.is_authorized(&actor_pk.to_hex(), owner.as_deref(), super::roles::Permissions::MANAGE_CHANNELS) {
1429 return Err("only a member with manage-channels authority can rename a channel".to_string());
1430 }
1431 let (version, prev_hash) = match crate::db::community::get_edition_head(&cid, &ch_hex)? {
1432 Some((v, h)) => (v + 1, Some(h)),
1433 None => (1, None),
1434 };
1435 let created = std::time::SystemTime::now()
1436 .duration_since(std::time::UNIX_EPOCH)
1437 .map(|d| d.as_secs())
1438 .unwrap_or(0);
1439 let meta = super::metadata::ChannelMetadata { name: new_name.to_string() };
1440 let citation = authority_citation(community, &actor_pk.to_hex());
1443 let unsigned = super::roster::build_channel_metadata_edition_unsigned(actor_pk, channel_id, &meta, version, prev_hash.as_ref(), created, citation.as_ref())?;
1444 let inner = unsigned.sign(&signer).await.map_err(|e| format!("sign channel-metadata edition: {e}"))?;
1445 let outer = super::roster::seal_control_edition(&Keys::generate(), &inner, &community.server_root_key, &community.id, community.server_root_epoch)?;
1446 transport.publish_durable(&outer, &community.relays).await?;
1447 if session.is_valid() {
1448 let mut current = crate::db::community::load_community(&community.id)?.ok_or("community no longer present")?;
1449 if let Some(ch) = current.channels.iter_mut().find(|c| &c.id == channel_id) {
1450 ch.name = new_name.to_string();
1451 }
1452 crate::db::community::save_community(¤t)?;
1453 let h = super::version::edition_hash(&channel_id.0, version, prev_hash.as_ref(), inner.content.as_bytes());
1454 crate::db::community::set_edition_head_with_id(&cid, &ch_hex, version, &h, &inner.id.to_bytes())?;
1455 }
1456 Ok(())
1457}
1458
1459fn generate_invite_label() -> String {
1472 use rand::Rng;
1473 const ALPHABET: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
1474 let mut rng = rand::thread_rng();
1475 (0..6).map(|_| ALPHABET[rng.gen_range(0..ALPHABET.len())] as char).collect()
1476}
1477
1478pub async fn create_public_invite<T: Transport + ?Sized>(
1479 transport: &T,
1480 community: &Community,
1481 expires_at: Option<u64>,
1482 label: Option<String>,
1483) -> Result<(String, String), String> {
1484 if !caller_has_permission(community, super::roles::Permissions::CREATE_INVITE) {
1485 return Err("you need the create-invite permission to mint a public invite".to_string());
1486 }
1487 let session = SessionGuard::capture();
1488
1489 let existing = crate::db::community::list_public_invites(&community.id.to_hex()).unwrap_or_default();
1493 let label_taken = |cand: &str| {
1494 existing.iter().any(|r| r.label.as_deref().map(|e| e.eq_ignore_ascii_case(cand)).unwrap_or(false))
1495 };
1496 let label = match label {
1497 Some(l) if !l.trim().is_empty() => {
1498 let l = l.trim().to_string();
1499 if label_taken(&l) {
1500 return Err(format!("You already have an invite link labeled \u{201c}{l}\u{201d}. Pick a different label."));
1501 }
1502 Some(l)
1503 }
1504 _ => {
1506 let mut l = generate_invite_label();
1507 while label_taken(&l) {
1508 l = generate_invite_label();
1509 }
1510 Some(l)
1511 }
1512 };
1513
1514 let creator_npub = crate::state::my_public_key().and_then(|pk| pk.to_bech32().ok());
1517 let token = public_invite::new_token();
1518 let event = build_public_invite_event(community, &token, expires_at, creator_npub, label.clone()).map_err(|e| e.to_string())?;
1519 transport.publish_durable(&event, &community.relays).await?;
1520
1521 if !session.is_valid() {
1524 return Err("account changed during public invite creation".to_string());
1525 }
1526 let token_hex = crate::simd::hex::bytes_to_hex_32(&token);
1527 let url = public_invite::encode_invite_url(&community.relays, &token);
1528 crate::db::community::save_public_invite(
1529 &token_hex,
1530 &community.id.to_hex(),
1531 &url,
1532 expires_at.map(|e| e as i64),
1533 label.as_deref(),
1534 )?;
1535 super::invite_list::add_invite(super::invite_list::InviteEntry {
1538 token: token_hex.clone(),
1539 community_id: community.id.to_hex(),
1540 url: url.clone(),
1541 label: label.clone(),
1542 created_at: std::time::SystemTime::now()
1543 .duration_since(std::time::UNIX_EPOCH)
1544 .map(|d| d.as_secs())
1545 .unwrap_or(0),
1546 expires_at,
1547 });
1548 republish_my_invite_links(transport, community).await?;
1551 Ok((token_hex, url))
1552}
1553
1554pub async fn latest_invite_preview<T: Transport + ?Sized>(
1560 transport: &T,
1561 bundle: &public_invite::PublicInviteBundle,
1562) -> public_invite::PublicInvitePreview {
1563 let snapshot = bundle.preview.clone();
1564 let Ok(community) = super::invite::accept_invite(&bundle.join) else {
1565 return snapshot;
1566 };
1567 let Ok(folded) = fetch_control_folded(transport, &community).await else {
1568 return snapshot;
1569 };
1570 let owner = proven_owner_hex(&community);
1571 let authorized = super::roster::authorize_delegation(&folded, owner.as_deref());
1572 match folded.root_candidates.iter().find(|c| {
1573 authorized.is_authorized(&c.author.to_hex(), owner.as_deref(), super::roles::Permissions::MANAGE_METADATA)
1574 }) {
1575 Some(c) => public_invite::PublicInvitePreview {
1576 name: c.meta.name.clone(),
1577 description: c.meta.description.clone(),
1578 icon: c.meta.icon.clone(),
1579 },
1580 None => snapshot,
1581 }
1582}
1583
1584pub async fn fetch_public_invite<T: Transport + ?Sized>(
1588 transport: &T,
1589 relays: &[String],
1590 token: &[u8; 32],
1591) -> Result<PublicInviteBundle, String> {
1592 let query = Query {
1596 kinds: vec![event_kind::APPLICATION_SPECIFIC],
1597 d_tags: vec![locator_hex(token)],
1598 ..Default::default()
1599 };
1600 let events = transport.fetch(&query, relays).await?;
1601 let (mut bundle_at, mut bundle, mut revoked_at) = (0u64, None, None::<u64>);
1607 for ev in &events {
1608 match parse_public_invite_event(ev, token) {
1609 Ok(b) => if bundle.is_none() || ev.created_at.as_secs() > bundle_at {
1610 bundle_at = ev.created_at.as_secs();
1611 bundle = Some(b);
1612 },
1613 Err(super::public_invite::PublicInviteError::Revoked) => {
1614 let at = ev.created_at.as_secs();
1615 if revoked_at.map_or(true, |r| at > r) { revoked_at = Some(at); }
1616 }
1617 Err(_) => {} }
1619 }
1620 match (bundle, revoked_at) {
1621 (Some(b), Some(r)) if bundle_at > r => Ok(b), (_, Some(_)) => Err("this invite was revoked".to_string()),
1623 (Some(b), None) => Ok(b),
1624 (None, None) => Err("no public invite found at that link (revoked, never posted, or shadowed)".to_string()),
1625 }
1626}
1627
1628pub fn accept_public_invite(bundle: &PublicInviteBundle, now_secs: u64) -> Result<Community, String> {
1632 if bundle.is_expired(now_secs) {
1633 return Err("this invite link has expired".to_string());
1634 }
1635 let mut community = accept_invite(&bundle.join)?;
1636 if bundle.preview.description.is_some() || bundle.preview.icon.is_some() {
1639 community.description = bundle.preview.description.clone();
1640 community.icon = bundle.preview.icon.clone();
1641 crate::db::community::save_community(&community)?;
1642 }
1643 Ok(community)
1644}
1645
1646pub async fn revoke_public_invite<T: Transport + ?Sized>(
1653 transport: &T,
1654 community: &Community,
1655 token: &[u8; 32],
1656) -> Result<(), String> {
1657 let session = SessionGuard::capture();
1658 let cid = community.id.to_hex();
1659 let token_hex = crate::simd::hex::bytes_to_hex_32(token);
1660 let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
1661 if !crate::db::community::list_public_invites(&cid)?.iter().any(|r| r.token == token_hex) {
1664 return Ok(());
1665 }
1666 let my_locators_before: Vec<String> = crate::db::community::list_public_invites(&cid)?
1667 .iter()
1668 .filter(|r| r.expires_at.map_or(true, |e| (e as u64) > now))
1669 .map(|r| public_invite::locator_hex(&crate::simd::hex::hex_to_bytes_32(&r.token)))
1670 .collect();
1671 let _ = fetch_and_apply_invite_links(transport, community).await;
1675 if !session.is_valid() {
1676 return Err("account changed during invite revoke".to_string());
1677 }
1678 let this_locator = public_invite::locator_hex(token);
1684 let cached_aggregate: std::collections::BTreeSet<String> =
1685 crate::db::community::get_community_invite_registry(&cid)?.into_iter().collect();
1686 let my_before: std::collections::BTreeSet<String> = my_locators_before.iter().cloned().collect();
1687 let others: std::collections::BTreeSet<String> = cached_aggregate.difference(&my_before).cloned().collect();
1688 let my_after: std::collections::BTreeSet<String> =
1689 my_before.iter().filter(|l| **l != this_locator).cloned().collect();
1690 let would_empty_aggregate = others.is_empty() && my_after.is_empty();
1691 if would_empty_aggregate && crate::state::MY_SECRET_KEY.to_keys().is_none() {
1692 return Err("Revoking this last invite link makes the community private, which re-keys it so link-joined lurkers lose access. Your account signs remotely (a NIP-46 bunker) and can't perform that rotation. Ask a community admin who holds a local key to privatize the community.".to_string());
1693 }
1694 if let Ok(tombstone) = public_invite::build_public_invite_tombstone(token) {
1704 let _ = transport.publish_durable(&tombstone, &community.relays).await;
1705 }
1706 if !session.is_valid() {
1708 return Err("account changed during invite revoke".to_string());
1709 }
1710 crate::db::community::delete_public_invite(&token_hex)?;
1711 super::invite_list::revoke_invite(&token_hex, &cid);
1714 republish_my_invite_links(transport, community).await?;
1716 if session.is_valid() {
1717 let aggregate_after: Vec<String> = others.union(&my_after).cloned().collect();
1718 crate::db::community::set_community_invite_registry(&cid, &aggregate_after)?;
1719 }
1720 if would_empty_aggregate {
1721 run_read_cut(transport, community, true).await?;
1725 }
1726 Ok(())
1727}
1728
1729async fn dissolved_tombstone_present<T: Transport + ?Sized>(transport: &T, community: &Community, owner_hex: &str) -> bool {
1742 let z = super::derive::dissolved_pseudonym(&community.id);
1743 let q = Query { kinds: vec![event_kind::COMMUNITY_CONTROL], z_tags: vec![z], ..Default::default() };
1744 for ev in transport.fetch(&q, &community.relays).await.unwrap_or_default() {
1745 if super::roster::dissolved_tombstone_signer(&ev, &community.id).map(|s| s.to_hex()) == Some(owner_hex.to_string()) {
1746 return true;
1747 }
1748 }
1749 false
1750}
1751
1752pub async fn dissolve_community<T: Transport + ?Sized>(
1753 transport: &T,
1754 community: &Community,
1755) -> Result<(), String> {
1756 let session = SessionGuard::capture();
1757 let cid = community.id.to_hex();
1758
1759 if !is_proven_owner(community) {
1761 return Err("only the community owner can dissolve (delete) the community".to_string());
1762 }
1763 let signer = active_signer().await?;
1764 let actor_pk = crate::state::my_public_key().ok_or("no local identity to sign the dissolution")?;
1765
1766 let created_at = std::time::SystemTime::now()
1770 .duration_since(std::time::UNIX_EPOCH)
1771 .map(|d| d.as_secs())
1772 .unwrap_or(0);
1773 let unsigned = super::roster::build_group_dissolved_edition_unsigned(actor_pk, &community.id, created_at);
1774 let inner = unsigned.sign(&signer).await.map_err(|e| format!("sign dissolution tombstone: {e}"))?;
1775 let stable = super::roster::seal_dissolved_edition(&Keys::generate(), &inner, &community.id)?;
1779 transport.publish_durable(&stable, &community.relays).await?;
1780 if let Ok(outer) = super::roster::seal_control_edition(&Keys::generate(), &inner, &community.server_root_key, &community.id, community.server_root_epoch) {
1783 let _ = transport.publish_durable(&outer, &community.relays).await;
1784 }
1785 if !session.is_valid() {
1786 return Err("account changed during dissolution".to_string());
1787 }
1788
1789 if caller_has_permission(community, super::roles::Permissions::CREATE_INVITE) {
1795 let _ = publish_my_invite_links(transport, community, &[]).await;
1796 if let Ok(records) = crate::db::community::list_public_invites(&cid) {
1797 for r in records {
1798 let token = crate::simd::hex::hex_to_bytes_32(&r.token);
1799 if let Ok(tombstone) = public_invite::build_public_invite_tombstone(&token) {
1800 let _ = transport.publish_durable(&tombstone, &community.relays).await;
1801 }
1802 let _ = crate::db::community::delete_public_invite(&r.token);
1803 }
1804 }
1805 }
1806
1807 if !session.is_valid() {
1809 return Err("account changed during dissolution".to_string());
1810 }
1811 crate::db::community::set_community_dissolved(&cid)?;
1812 Ok(())
1813}
1814
1815pub async fn publish_my_invite_links<T: Transport + ?Sized>(
1822 transport: &T,
1823 community: &Community,
1824 my_locators: &[String],
1825) -> Result<(), String> {
1826 let session = SessionGuard::capture();
1827 if !caller_has_permission(community, super::roles::Permissions::CREATE_INVITE) {
1828 return Err("you need the create-invite permission to publish invite links".to_string());
1829 }
1830 let cid = community.id.to_hex();
1831 let signer = active_signer().await?;
1832 let actor_pk = crate::state::my_public_key().ok_or("no local identity to sign the invite links")?;
1833 let entity_id = super::derive::invite_links_locator(&community.id, &actor_pk.to_bytes());
1834 let entity_hex = crate::simd::hex::bytes_to_hex_32(&entity_id);
1835 let (version, prev_hash) = match crate::db::community::get_edition_head(&cid, &entity_hex)? {
1836 Some((v, h)) => (v + 1, Some(h)),
1837 None => (1, None),
1838 };
1839 let created_at = std::time::SystemTime::now()
1840 .duration_since(std::time::UNIX_EPOCH)
1841 .map(|d| d.as_secs())
1842 .unwrap_or(0);
1843 let citation = authority_citation(community, &actor_pk.to_hex());
1845 let unsigned = super::roster::build_invite_links_edition_unsigned(actor_pk, &community.id, my_locators, version, prev_hash.as_ref(), created_at, citation.as_ref())?;
1846 let inner = unsigned.sign(&signer).await.map_err(|e| format!("sign invite-links edition: {e}"))?;
1847 let outer = super::roster::seal_control_edition(&Keys::generate(), &inner, &community.server_root_key, &community.id, community.server_root_epoch)?;
1848 let self_hash = super::version::edition_hash(&entity_id, version, prev_hash.as_ref(), inner.content.as_bytes());
1849 transport.publish_durable(&outer, &community.relays).await?;
1850 if session.is_valid() {
1851 crate::db::community::set_edition_head(&cid, &entity_hex, version, &self_hash)?;
1852 let mut agg: std::collections::BTreeSet<String> =
1855 crate::db::community::get_community_invite_registry(&cid)?.into_iter().collect();
1856 agg.extend(my_locators.iter().cloned());
1857 crate::db::community::set_community_invite_registry(&cid, &agg.into_iter().collect::<Vec<_>>())?;
1858 crate::db::community::upsert_invite_link_set(&cid, &actor_pk.to_hex(), my_locators)?;
1859 }
1860 Ok(())
1861}
1862
1863pub async fn fetch_and_apply_invite_links<T: Transport + ?Sized>(
1869 transport: &T,
1870 community: &Community,
1871) -> Result<Vec<String>, String> {
1872 fetch_and_apply_invite_links_inner(transport, community, None).await
1873}
1874
1875async fn fetch_and_apply_invite_links_inner<T: Transport + ?Sized>(
1876 transport: &T,
1877 community: &Community,
1878 prefolded: Option<super::roster::FoldedRoster>,
1879) -> Result<Vec<String>, String> {
1880 let session = SessionGuard::capture();
1881 let cid = community.id.to_hex();
1882 let folded = match prefolded {
1883 Some(f) => f,
1884 None => fetch_control_folded(transport, community).await?,
1885 };
1886 if !session.is_valid() {
1887 return Err("account changed during invite-links fetch".to_string());
1888 }
1889 let owner = proven_owner_hex(community);
1890 let authorized = super::roster::authorize_delegation(&folded, owner.as_deref());
1891 let mut aggregate: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
1892 let mut per_creator: Vec<crate::db::community::InviteLinkSetRow> = Vec::new();
1894 for set in &folded.invite_link_sets {
1895 if !authorized.is_authorized(&set.creator.to_hex(), owner.as_deref(), super::roles::Permissions::CREATE_INVITE) {
1898 continue;
1899 }
1900 let held = crate::db::community::get_edition_head(&cid, &set.head.entity_hex)?.map(|(v, _)| v).unwrap_or(0);
1901 if set.head.version > held {
1902 crate::db::community::set_edition_head(&cid, &set.head.entity_hex, set.head.version, &set.head.self_hash)?;
1903 }
1904 aggregate.extend(set.locators.iter().cloned());
1905 per_creator.push(crate::db::community::InviteLinkSetRow {
1906 creator_hex: set.creator.to_hex(),
1907 locators: set.locators.clone(),
1908 });
1909 }
1910 let aggregate: Vec<String> = aggregate.into_iter().collect();
1911 crate::db::community::set_community_invite_registry(&cid, &aggregate)?;
1912 crate::db::community::replace_invite_link_sets(&cid, &per_creator)?;
1913 Ok(aggregate)
1914}
1915
1916pub async fn fetch_and_apply_metadata<T: Transport + ?Sized>(
1924 transport: &T,
1925 community: &Community,
1926) -> Result<(), String> {
1927 fetch_and_apply_metadata_inner(transport, community, None).await
1928}
1929
1930async fn fetch_and_apply_metadata_inner<T: Transport + ?Sized>(
1931 transport: &T,
1932 community: &Community,
1933 prefolded: Option<super::roster::FoldedRoster>,
1934) -> Result<(), String> {
1935 let session = SessionGuard::capture();
1936 let cid = community.id.to_hex();
1937 let folded = match prefolded {
1938 Some(f) => f,
1939 None => fetch_control_folded(transport, community).await?,
1940 };
1941 if !session.is_valid() {
1942 return Err("account changed during metadata fetch".to_string());
1943 }
1944 let owner = proven_owner_hex(community);
1945 let authorized = super::roster::authorize_delegation(&folded, owner.as_deref());
1946 let manage = super::roles::Permissions::MANAGE_METADATA;
1949 let manage_channels = super::roles::Permissions::MANAGE_CHANNELS;
1950
1951 let mut current = match crate::db::community::load_community(&community.id)? {
1954 Some(c) => c,
1955 None => return Ok(()),
1956 };
1957 let mut dirty = false;
1958 let mut head_updates: Vec<(String, u64, [u8; 32], [u8; 32], bool)> = Vec::new();
1962
1963 let decide = |entity_hex: &str, head: &super::roster::EntityHead| -> Result<Option<bool>, String> {
1970 let held = crate::db::community::get_edition_head(&cid, entity_hex)?;
1971 let held_v = held.map(|(v, _)| v).unwrap_or(0);
1972 if head.version > held_v {
1973 return Ok(Some(false)); }
1975 if head.version == held_v && held.map(|(_, h)| h) != Some(head.self_hash) {
1976 let held_id = crate::db::community::get_edition_head_inner_id(&cid, entity_hex)?;
1977 if held_id.is_none() || Some(head.inner_id) < held_id {
1978 return Ok(Some(true)); }
1980 }
1981 Ok(None)
1982 };
1983
1984 if let Some(c) = folded.root_candidates.iter()
1989 .find(|c| authorized.is_authorized(&c.author.to_hex(), owner.as_deref(), manage))
1990 {
1991 let head = &c.head;
1992 if let Some(is_converge) = decide(&head.entity_hex, head)? {
1993 let meta = &c.meta;
1994 current.name = meta.name.clone();
2003 current.description = meta.description.clone();
2004 current.icon = meta.icon.clone();
2005 current.banner = meta.banner.clone();
2006 dirty = true;
2007 head_updates.push((head.entity_hex.clone(), head.version, head.self_hash, head.inner_id, is_converge));
2008 }
2009 }
2010 let mut resolved_channels: std::collections::HashSet<[u8; 32]> = std::collections::HashSet::new();
2016 for cm in &folded.channel_candidates {
2017 if resolved_channels.contains(&cm.channel_id) {
2018 continue; }
2020 if !authorized.is_authorized(&cm.author.to_hex(), owner.as_deref(), manage_channels) {
2021 continue; }
2023 resolved_channels.insert(cm.channel_id);
2024 let Some(is_converge) = decide(&cm.head.entity_hex, &cm.head)? else { continue };
2025 if let Some(ch) = current.channels.iter_mut().find(|c| c.id.0 == cm.channel_id) {
2026 ch.name = cm.meta.name.clone();
2027 dirty = true;
2028 head_updates.push((cm.head.entity_hex.clone(), cm.head.version, cm.head.self_hash, cm.head.inner_id, is_converge));
2029 }
2030 }
2031
2032 if dirty && session.is_valid() {
2033 crate::db::community::save_community(¤t)?;
2034 for (entity_hex, version, self_hash, inner_id, is_converge) in &head_updates {
2037 if *is_converge {
2038 crate::db::community::converge_edition_head(&cid, entity_hex, *version, self_hash, inner_id)?;
2039 } else {
2040 crate::db::community::set_edition_head_with_id(&cid, entity_hex, *version, self_hash, inner_id)?;
2041 }
2042 }
2043 }
2044 Ok(())
2045}
2046
2047pub fn is_public(community: &Community) -> Result<bool, String> {
2055 Ok(!crate::db::community::get_community_invite_registry(&community.id.to_hex())?.is_empty())
2056}
2057
2058async fn republish_my_invite_links<T: Transport + ?Sized>(
2063 transport: &T,
2064 community: &Community,
2065) -> Result<Vec<String>, String> {
2066 let cid = community.id.to_hex();
2067 let now = std::time::SystemTime::now()
2068 .duration_since(std::time::UNIX_EPOCH)
2069 .map(|d| d.as_secs())
2070 .unwrap_or(0);
2071 let locators: Vec<String> = crate::db::community::list_public_invites(&cid)?
2072 .iter()
2073 .filter(|r| r.expires_at.map_or(true, |e| (e as u64) > now))
2074 .map(|r| public_invite::locator_hex(&crate::simd::hex::hex_to_bytes_32(&r.token)))
2075 .collect();
2076 publish_my_invite_links(transport, community, &locators).await?;
2077 Ok(locators)
2078}
2079
2080async fn observe_channel_activity<T: Transport + ?Sized>(
2086 transport: &T,
2087 community: &Community,
2088) -> Result<(), String> {
2089 let session = SessionGuard::capture();
2090 let my_pk = crate::state::my_public_key().ok_or("no local identity to observe channel activity")?;
2091 for channel in &community.channels {
2092 let events = super::send::fetch_channel_events(transport, community, channel)
2093 .await
2094 .unwrap_or_default();
2095 if !session.is_valid() {
2096 return Err("account changed during activity observation".to_string());
2097 }
2098 let outcomes = {
2099 let mut st = crate::state::STATE.lock().await;
2100 super::inbound::process_channel_batch(&mut st, &events, channel, &my_pk)
2101 };
2102 let ch_hex = channel.id.to_hex();
2103 for o in &outcomes {
2104 match o {
2105 super::inbound::IncomingEvent::NewMessage(m)
2106 | super::inbound::IncomingEvent::Updated { message: m, .. } => {
2107 let _ = crate::db::events::save_message(&ch_hex, m).await;
2108 }
2109 super::inbound::IncomingEvent::Presence { npub, joined, event_id, created_at, invited_by, invited_label } => {
2110 let et = if *joined {
2111 crate::stored_event::SystemEventType::MemberJoined
2112 } else {
2113 crate::stored_event::SystemEventType::MemberLeft
2114 };
2115 let note = invited_by.as_ref().map(|by| match invited_label {
2116 Some(l) if !l.is_empty() => format!("{by}|{l}"),
2117 _ => by.clone(),
2118 });
2119 let _ = crate::db::events::save_system_event_at(event_id, &ch_hex, et, npub, note.as_deref(), *created_at, invited_by.as_deref(), invited_label.as_deref()).await;
2120 }
2121 super::inbound::IncomingEvent::WebxdcPeer { npub, topic_id, node_addr, event_id, created_at } => {
2122 persist_webxdc_signal(&ch_hex, npub, topic_id, node_addr.as_deref(), event_id, *created_at).await;
2123 }
2124 _ => {}
2125 }
2126 }
2127 }
2128 Ok(())
2129}
2130
2131pub async fn sync_before_admin_write<T: Transport + ?Sized>(
2142 transport: &T,
2143 community: &Community,
2144 observe_activity: bool,
2145) -> Result<Community, String> {
2146 if catch_up_server_root(transport, community).await?.removed {
2148 return Err("you have been removed from this community".to_string());
2149 }
2150 let community = crate::db::community::load_community(&community.id)?
2151 .ok_or("community gone during admin sync")?;
2152 let cid = community.id.to_hex();
2153 let responded = fetch_and_apply_control(transport, &community).await.map(|n| n > 0).unwrap_or(false);
2160 let hold_local_heads = !crate::db::community::get_all_edition_heads_epoched(&cid)?.is_empty();
2161 if hold_local_heads && !responded {
2162 return Err("can't reach any relay to confirm this community's current state — administrative actions are blocked while offline (try again when connected)".to_string());
2163 }
2164 let community = crate::db::community::load_community(&community.id)?
2165 .ok_or("community gone during admin sync")?;
2166 if observe_activity {
2169 let _ = observe_channel_activity(transport, &community).await;
2170 }
2171 crate::db::community::load_community(&community.id)?.ok_or("community gone during admin sync".to_string())
2172}
2173
2174async fn run_read_cut<T: Transport + ?Sized>(
2184 transport: &T,
2185 community: &Community,
2186 fresh: bool,
2187) -> Result<(), String> {
2188 let cid = community.id.to_hex();
2189 let session = SessionGuard::capture();
2190 if fresh {
2191 let base = crate::db::community::load_community(&community.id)?
2194 .map(|c| c.server_root_epoch.0)
2195 .unwrap_or(community.server_root_epoch.0);
2196 crate::db::community::set_read_cut_target_epoch(&cid, base.saturating_add(1))?;
2197 }
2198 crate::db::community::set_read_cut_pending(&cid, true)?;
2199 reseal_base_to_observed(transport, community).await?;
2200 if session.is_valid() {
2201 crate::db::community::set_read_cut_pending(&cid, false)?;
2202 }
2203 Ok(())
2204}
2205
2206async fn reseal_base_to_observed<T: Transport + ?Sized>(
2216 transport: &T,
2217 community: &Community,
2218) -> Result<(), String> {
2219 let session = SessionGuard::capture();
2220 let cid = community.id.to_hex();
2221 let community = &sync_before_admin_write(transport, community, true).await?;
2226 let participants: Vec<nostr_sdk::PublicKey> = crate::db::community::community_member_activity(&cid)?
2230 .into_iter()
2231 .filter_map(|(npub, _)| nostr_sdk::PublicKey::parse(&npub).ok())
2232 .collect();
2233 let target = crate::db::community::get_read_cut_target_epoch(&cid)?;
2243 if community.server_root_epoch.0 < target {
2244 rotate_server_root(transport, community, &participants).await?;
2245 if !session.is_valid() {
2246 return Err("account changed during re-founding".to_string());
2247 }
2248 }
2249 let community = crate::db::community::load_community(&community.id)?
2255 .ok_or("community gone after base rotation")?;
2256 let cut_epoch = community.server_root_epoch.0;
2257 let prior_root = crate::db::community::held_epoch_key(&cid, crate::community::SERVER_ROOT_SCOPE_HEX, cut_epoch.saturating_sub(1))?
2263 .unwrap_or(*community.server_root_key.as_bytes()); for channel in &community.channels {
2265 let ch_hex = channel.id.to_hex();
2266 if crate::db::community::channel_rekeyed_at_server_epoch(&cid, &ch_hex)? >= cut_epoch {
2269 continue;
2270 }
2271 rotate_channel(transport, &community, &channel.id, &participants, &prior_root).await?;
2272 if !session.is_valid() {
2273 return Err("account changed during re-founding".to_string());
2274 }
2275 crate::db::community::mark_channel_rekeyed_at_server_epoch(&cid, &ch_hex, cut_epoch)?;
2276 }
2277 Ok(())
2278}
2279
2280#[derive(Debug, PartialEq, Eq)]
2282pub enum RekeyOutcome {
2283 Applied { head_advanced: bool },
2286 NotARecipient,
2289}
2290
2291pub fn apply_channel_rekey(
2302 community: &Community,
2303 parsed: &super::rekey::ParsedRekey,
2304) -> Result<RekeyOutcome, String> {
2305 let session = SessionGuard::capture();
2309
2310 let channel_id = match parsed.scope {
2312 super::derive::RekeyScope::Channel(c) => c,
2313 super::derive::RekeyScope::ServerRoot => {
2314 return Err("server-root rotation uses apply_server_root_rekey, not the channel path".to_string())
2315 }
2316 };
2317 if !community.channels.iter().any(|c| c.id == channel_id) {
2318 return Err("rekey targets a channel not in this community".to_string());
2319 }
2320 let cid = community.id.to_hex();
2321 let channel_hex = channel_id.to_hex();
2322
2323 let owner = proven_owner_hex(community);
2330 let roster = crate::db::community::get_community_roles(&cid).unwrap_or_else(|e| {
2331 crate::log_warn!("rekey apply: roster read failed ({e}); authorizing owner only");
2334 Default::default()
2335 });
2336 if !roster.is_authorized(
2337 &parsed.rotator.to_hex(),
2338 owner.as_deref(),
2339 super::roles::Permissions::MANAGE_CHANNELS,
2340 ) {
2341 return Err("rekey rotator lacks MANAGE_CHANNELS authority".to_string());
2342 }
2343
2344 if let Some(prev_key) = crate::db::community::held_epoch_key(&cid, &channel_hex, parsed.prev_epoch.0)? {
2354 if super::rekey::epoch_key_commitment(parsed.prev_epoch, &prev_key) != parsed.prev_key_commitment {
2355 crate::log_warn!(
2356 "channel rekey to epoch {} cites a prior-epoch key I don't hold (I'm on a losing fork of epoch {}) — converging forward onto the authorized chain",
2357 parsed.new_epoch.0, parsed.prev_epoch.0
2358 );
2359 }
2360 }
2361
2362 let my_keys = crate::state::MY_SECRET_KEY
2364 .to_keys()
2365 .ok_or("no local identity to open the rekey blob")?;
2366 let secret = super::rekey::rekey_pairwise_secret(my_keys.secret_key(), &parsed.rotator)?;
2367 let my_locator = super::derive::recipient_pseudonym(&secret, parsed.scope, parsed.new_epoch).to_hex();
2368 let mine = match parsed.blobs.iter().find(|b| b.locator == my_locator) {
2369 Some(b) => b,
2370 None => return Ok(RekeyOutcome::NotARecipient),
2371 };
2372 let new_key =
2373 super::rekey::open_rekey_blob(my_keys.secret_key(), &parsed.rotator, parsed.scope, parsed.new_epoch, mine)?;
2374
2375 if !session.is_valid() {
2377 return Err("session changed during rekey apply".to_string());
2378 }
2379 let head_advanced =
2380 crate::db::community::advance_channel_epoch(&cid, &channel_hex, parsed.new_epoch.0, &new_key)?;
2381 Ok(RekeyOutcome::Applied { head_advanced })
2382}
2383
2384fn mint_or_reuse_rotation_key(cid: &str, scope_id: &str, epoch: u64) -> Result<zeroize::Zeroizing<[u8; 32]>, String> {
2390 if let Some(k) = crate::db::community::held_epoch_key(cid, scope_id, epoch)? {
2391 return Ok(zeroize::Zeroizing::new(k));
2392 }
2393 let k = zeroize::Zeroizing::new(super::random_32());
2394 crate::db::community::store_epoch_key(cid, scope_id, epoch, &k)?;
2395 Ok(k)
2396}
2397
2398async fn publish_rekey_chunked<T, F>(
2406 transport: &T,
2407 relays: &[String],
2408 blobs: &[super::rekey::RekeyBlob],
2409 build: F,
2410) -> Result<(), String>
2411where
2412 T: Transport + ?Sized,
2413 F: Fn(&[super::rekey::RekeyBlob]) -> Result<Event, String>,
2414{
2415 if blobs.is_empty() {
2416 return Err("rekey has no recipients".to_string());
2417 }
2418 for chunk in blobs.chunks(super::rekey::MAX_REKEY_BLOBS) {
2419 let event = build(chunk)?;
2420 transport.publish_durable(&event, relays).await?;
2421 }
2422 Ok(())
2423}
2424
2425pub async fn rotate_channel<T: Transport + ?Sized>(
2437 transport: &T,
2438 community: &Community,
2439 channel_id: &super::ChannelId,
2440 recipients: &[nostr_sdk::PublicKey],
2441 envelope_root: &[u8; 32],
2447) -> Result<u64, String> {
2448 let session = SessionGuard::capture();
2449 let cid = community.id.to_hex();
2450
2451 let my_keys = crate::state::MY_SECRET_KEY.to_keys().ok_or("a key rotation requires a local key (bunker/NIP-46 accounts can't rekey)")?;
2455 let owner = proven_owner_hex(community);
2456 let roster = crate::db::community::get_community_roles(&cid).unwrap_or_default();
2457 if !roster.is_authorized(&my_keys.public_key().to_hex(), owner.as_deref(), super::roles::Permissions::MANAGE_CHANNELS) {
2458 return Err("not authorized to rotate this channel (no MANAGE_CHANNELS)".to_string());
2459 }
2460
2461 let channel = community
2465 .channels
2466 .iter()
2467 .find(|c| &c.id == channel_id)
2468 .ok_or("channel not found in community")?;
2469 let prev_epoch = channel.epoch;
2470 let new_epoch = super::Epoch(prev_epoch.0.checked_add(1).ok_or("channel epoch overflow")?);
2471 let prev_commit = super::rekey::epoch_key_commitment(prev_epoch, channel.key.as_bytes());
2472 let new_key = mint_or_reuse_rotation_key(&cid, &channel_id.to_hex(), new_epoch.0)?;
2475
2476 let mut seen = std::collections::HashSet::new();
2480 let mut blobs = Vec::new();
2481 for pk in recipients.iter().chain(std::iter::once(&my_keys.public_key())) {
2482 if !seen.insert(pk.to_hex()) {
2483 continue;
2484 }
2485 blobs.push(super::rekey::build_rekey_blob(
2486 my_keys.secret_key(), pk, super::derive::RekeyScope::Channel(*channel_id), new_epoch, &new_key,
2487 )?);
2488 }
2489
2490 publish_rekey_chunked(transport, &community.relays, &blobs, |chunk| {
2492 super::rekey::build_channel_rekey_event(
2493 &Keys::generate(), &my_keys, envelope_root, channel_id,
2494 new_epoch, prev_epoch, &prev_commit, chunk,
2495 )
2496 })
2497 .await?;
2498 if !session.is_valid() {
2499 return Err("session changed during channel rotation".to_string());
2500 }
2501 crate::db::community::advance_channel_epoch(&cid, &channel_id.to_hex(), new_epoch.0, &new_key)?;
2502 Ok(new_epoch.0)
2503}
2504
2505fn emit_rekey_progress(label: &str, pct: u8) {
2509 crate::emit_event("community_rekey_progress", &serde_json::json!({ "label": label, "pct": pct }));
2510}
2511
2512pub(crate) async fn rotate_server_root<T: Transport + ?Sized>(
2528 transport: &T,
2529 community: &Community,
2530 recipients: &[nostr_sdk::PublicKey],
2531) -> Result<u64, String> {
2532 let session = SessionGuard::capture();
2533 let cid = community.id.to_hex();
2534
2535 if crate::db::community::get_community_dissolved(&cid)? {
2537 return Err("community is dissolved; it cannot be re-founded".to_string());
2538 }
2539
2540 let my_keys = crate::state::MY_SECRET_KEY.to_keys().ok_or("a base rotation (privatize / private-ban read-cut) requires a local key (bunker/NIP-46 accounts can't rekey)")?;
2551 let owner = proven_owner_hex(community);
2552 let roster = crate::db::community::get_community_roles(&cid).unwrap_or_default();
2553 if !roster.is_authorized(&my_keys.public_key().to_hex(), owner.as_deref(), super::roles::Permissions::BAN) {
2554 return Err("not authorized to rotate the server root (no BAN)".to_string());
2555 }
2556
2557 let fresh = crate::db::community::load_community(&community.id)?
2563 .ok_or("community gone before base rotation")?;
2564 let community = &fresh;
2565 let prev_epoch = community.server_root_epoch;
2566 let new_epoch = super::Epoch(prev_epoch.0.checked_add(1).ok_or("server-root epoch overflow")?);
2567 let prev_commit = super::rekey::epoch_key_commitment(prev_epoch, community.server_root_key.as_bytes());
2569 let new_root = mint_or_reuse_rotation_key(&cid, crate::community::SERVER_ROOT_SCOPE_HEX, new_epoch.0)?;
2571 emit_rekey_progress("Rerolling community keys...", 5);
2572
2573 let sealed = prepare_reanchor_control_plane(transport, community, &new_root, new_epoch).await?;
2579 if !session.is_valid() {
2580 return Err("session changed during re-founding acquire".to_string());
2581 }
2582
2583 let total_recipients = (recipients.len() + 1).max(1); let mut seen = std::collections::HashSet::new();
2585 let mut blobs = Vec::new();
2586 for pk in recipients.iter().chain(std::iter::once(&my_keys.public_key())) {
2587 if !seen.insert(pk.to_hex()) {
2588 continue;
2589 }
2590 blobs.push(super::rekey::build_rekey_blob(
2591 my_keys.secret_key(), pk, super::derive::RekeyScope::ServerRoot, new_epoch, &new_root,
2592 )?);
2593 emit_rekey_progress(
2594 &format!("Preparing keys for members ({}/{})...", blobs.len(), total_recipients),
2595 (5 + 35 * blobs.len() / total_recipients) as u8,
2596 );
2597 }
2598
2599 emit_rekey_progress("Sending keys to members...", 42);
2603 publish_rekey_chunked(transport, &community.relays, &blobs, |chunk| {
2604 super::rekey::build_server_root_rekey_event(
2605 &Keys::generate(), &my_keys, community.server_root_key.as_bytes(), &community.id,
2606 new_epoch, prev_epoch, &prev_commit, chunk,
2607 )
2608 })
2609 .await?;
2610
2611 let snapshot = publish_reanchor_snapshot(transport, &community.relays, sealed).await?;
2619 if snapshot.iter().any(|e| !e.published) {
2620 return Err(
2621 "re-founding aborted: a snapshot edition did not land (rate-limited / unreachable relay?); base head NOT advanced".to_string()
2622 );
2623 }
2624 if !session.is_valid() {
2625 return Err("session changed during server-root rotation".to_string());
2626 }
2627 emit_rekey_progress("Finalizing...", 98);
2628 crate::db::community::advance_server_root_epoch(&cid, new_epoch.0, &new_root)?;
2630 for e in &snapshot {
2634 crate::db::community::set_edition_head_with_id(&cid, &e.entity_hex, e.version, &e.self_hash, &e.inner_id)?;
2635 }
2636 Ok(new_epoch.0)
2637}
2638
2639pub(crate) struct SnapshotEntry {
2674 pub entity_hex: String,
2675 pub version: u64,
2676 pub self_hash: [u8; 32],
2677 pub inner_id: [u8; 32],
2678 pub published: bool,
2679}
2680
2681pub(crate) async fn prepare_reanchor_control_plane<T: Transport + ?Sized>(
2687 transport: &T,
2688 community: &Community,
2689 new_root: &[u8; 32],
2690 new_epoch: super::Epoch,
2691) -> Result<Vec<(Event, SnapshotEntry)>, String> {
2692 let session = SessionGuard::capture();
2693 let cid = community.id.to_hex();
2694
2695 let z = super::roster::control_pseudonym(&community.server_root_key, &community.id, community.server_root_epoch);
2703 let query = Query { kinds: vec![event_kind::COMMUNITY_CONTROL], z_tags: vec![z], ..Default::default() };
2704 let outers = transport.fetch(&query, &community.relays).await?;
2705 if !session.is_valid() {
2706 return Err("session changed during re-founding fetch".to_string());
2707 }
2708 let mut by_hash: std::collections::HashMap<[u8; 32], (Event, [u8; 32])> = std::collections::HashMap::new();
2710 for outer in &outers {
2711 if let Ok(inner) = super::roster::open_control_edition(outer, &community.server_root_key) {
2712 if let Ok(parsed) = super::edition::parse_edition_inner(&inner) {
2713 by_hash.insert(parsed.self_hash, (inner, parsed.inner_id));
2714 }
2715 }
2716 }
2717
2718 let new_root_key = super::ServerRootKey(*new_root);
2722 let mut sealed: Vec<(Event, SnapshotEntry)> = Vec::new();
2723 for (entity_hex, (epoch, version, self_hash)) in crate::db::community::get_all_edition_heads_epoched(&cid)? {
2724 if epoch != community.server_root_epoch.0 {
2725 continue; }
2727 let (inner, inner_id) = by_hash.get(&self_hash).ok_or_else(|| {
2728 format!("re-founding aborted: head edition for entity {entity_hex} (v{version}) not fetchable — aborting so no member is stranded")
2729 })?;
2730 let outer = super::roster::seal_control_edition(&Keys::generate(), inner, &new_root_key, &community.id, new_epoch)?;
2731 sealed.push((outer, SnapshotEntry { entity_hex, version, self_hash, inner_id: *inner_id, published: false }));
2732 }
2733 Ok(sealed)
2734}
2735
2736pub(crate) async fn publish_reanchor_snapshot<T: Transport + ?Sized>(
2740 transport: &T,
2741 relays: &[String],
2742 sealed: Vec<(Event, SnapshotEntry)>,
2743) -> Result<Vec<SnapshotEntry>, String> {
2744 use futures_util::stream::StreamExt;
2747 let total = sealed.len().max(1);
2748 let done = std::sync::atomic::AtomicUsize::new(0);
2749 let done_ref = &done;
2750 emit_rekey_progress(&format!("Re-founding community (0/{total})..."), 50);
2751 let out: Vec<SnapshotEntry> = futures_util::stream::iter(sealed.into_iter().map(|(ev, mut entry)| async move {
2752 entry.published = transport.publish_durable(&ev, relays).await.is_ok();
2753 let n = done_ref.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
2754 emit_rekey_progress(&format!("Re-founding community ({n}/{total})..."), (50 + 45 * n / total) as u8);
2755 entry
2756 }))
2757 .buffer_unordered(4)
2758 .collect()
2759 .await;
2760 Ok(out)
2761}
2762
2763#[cfg(test)]
2766pub(crate) async fn reanchor_control_plane<T: Transport + ?Sized>(
2767 transport: &T,
2768 community: &Community,
2769 new_root: &[u8; 32],
2770 new_epoch: super::Epoch,
2771) -> Result<Vec<SnapshotEntry>, String> {
2772 let sealed = prepare_reanchor_control_plane(transport, community, new_root, new_epoch).await?;
2773 publish_reanchor_snapshot(transport, &community.relays, sealed).await
2774}
2775
2776pub fn apply_server_root_rekey(
2784 community: &Community,
2785 parsed: &super::rekey::ParsedRekey,
2786) -> Result<RekeyOutcome, String> {
2787 let session = SessionGuard::capture();
2788
2789 if !matches!(parsed.scope, super::derive::RekeyScope::ServerRoot) {
2791 return Err("not a server-root rekey (channel rekeys use apply_channel_rekey)".to_string());
2792 }
2793 let cid = community.id.to_hex();
2794
2795 if crate::db::community::get_community_dissolved(&cid)? {
2798 return Err("community is dissolved; base epoch cannot advance".to_string());
2799 }
2800
2801 let owner = proven_owner_hex(community);
2808 let roster = crate::db::community::get_community_roles(&cid).unwrap_or_else(|e| {
2809 crate::log_warn!("base rekey apply: roster read failed ({e}); authorizing owner only");
2810 Default::default()
2811 });
2812 if !rotator_is_authorized(&cid, &roster, owner.as_deref(), &parsed.rotator.to_hex(), super::roles::Permissions::BAN) {
2813 return Err("base rekey rotator lacks server-wide rotation authority (BAN)".to_string());
2814 }
2815
2816 if let Some(prev_root) =
2823 crate::db::community::held_epoch_key(&cid, crate::community::SERVER_ROOT_SCOPE_HEX, parsed.prev_epoch.0)?
2824 {
2825 if super::rekey::epoch_key_commitment(parsed.prev_epoch, &prev_root) != parsed.prev_key_commitment {
2826 crate::log_warn!(
2827 "base rekey to epoch {} cites a prior-root I don't hold (I'm on a losing fork of epoch {}) — converging forward onto the authorized chain",
2828 parsed.new_epoch.0, parsed.prev_epoch.0
2829 );
2830 }
2831 }
2832
2833 let my_keys = crate::state::MY_SECRET_KEY.to_keys().ok_or("no local identity to open the base rekey blob")?;
2835 let secret = super::rekey::rekey_pairwise_secret(my_keys.secret_key(), &parsed.rotator)?;
2836 let my_locator = super::derive::recipient_pseudonym(&secret, parsed.scope, parsed.new_epoch).to_hex();
2837 let mine = match parsed.blobs.iter().find(|b| b.locator == my_locator) {
2838 Some(b) => b,
2839 None => return Ok(RekeyOutcome::NotARecipient),
2840 };
2841 let new_root =
2842 super::rekey::open_rekey_blob(my_keys.secret_key(), &parsed.rotator, parsed.scope, parsed.new_epoch, mine)?;
2843
2844 if !session.is_valid() {
2845 return Err("session changed during base rekey apply".to_string());
2846 }
2847 let head_advanced = crate::db::community::advance_server_root_epoch(&cid, parsed.new_epoch.0, &new_root)?;
2848 Ok(RekeyOutcome::Applied { head_advanced })
2849}
2850
2851const REKEY_CATCHUP_WINDOW: u64 = 64;
2855const MAX_REKEY_CATCHUP_ROUNDS: usize = 64;
2858
2859async fn heal_channel_fork_epochs<T: Transport + ?Sized>(
2871 transport: &T,
2872 community: &Community,
2873 channel_id: &super::ChannelId,
2874 cid: &str,
2875 channel_hex: &str,
2876 epochs: &std::collections::BTreeSet<u64>,
2877 server_roots: &[[u8; 32]],
2878 session: &SessionGuard,
2879) -> Result<(), String> {
2880 if epochs.is_empty() {
2881 return Ok(());
2882 }
2883 let owner_hex = proven_owner_hex(community);
2884 let roster = crate::db::community::get_community_roles(cid).unwrap_or_default();
2885 let mut winner: std::collections::BTreeMap<u64, [u8; 32]> = std::collections::BTreeMap::new();
2888 for sr in server_roots {
2889 let z_tags: Vec<String> = epochs
2890 .iter()
2891 .map(|e| super::derive::rekey_pseudonym(&super::ServerRootKey(*sr), channel_id, super::Epoch(*e)).to_hex())
2892 .collect();
2893 let q = Query { kinds: vec![event_kind::COMMUNITY_REKEY], z_tags, ..Default::default() };
2894 for ev in transport.fetch(&q, &community.relays).await.unwrap_or_default() {
2895 let Ok(p) = super::rekey::open_rekey_event(&ev, sr) else { continue };
2896 if !matches!(p.scope, super::derive::RekeyScope::Channel(c) if &c == channel_id) || !epochs.contains(&p.new_epoch.0) {
2897 continue;
2898 }
2899 if !rotator_is_authorized(cid, &roster, owner_hex.as_deref(), &p.rotator.to_hex(), super::roles::Permissions::MANAGE_CHANNELS) {
2900 continue;
2901 }
2902 let Some(key) = peek_my_channel_key(&p) else { continue }; winner.entry(p.new_epoch.0).and_modify(|best| { if key < *best { *best = key; } }).or_insert(key);
2904 }
2905 }
2906 for (epoch, win_key) in winner {
2907 if !session.is_valid() {
2908 return Err("session changed during channel convergence".to_string());
2909 }
2910 if let Ok(Some(cur)) = crate::db::community::held_epoch_key(cid, channel_hex, epoch) {
2915 if win_key < cur {
2916 match crate::db::community::converge_channel_epoch(cid, channel_hex, epoch, &win_key) {
2919 Ok(false) => crate::log_trace!("channel heal: converge of epoch {epoch} did not apply (head moved)"),
2920 Err(e) => crate::log_trace!("channel heal: converge of epoch {epoch} errored: {e}"),
2921 Ok(true) => {}
2922 }
2923 }
2924 }
2925 }
2926 Ok(())
2927}
2928
2929pub async fn catch_up_channel_rekeys<T: Transport + ?Sized>(
2944 transport: &T,
2945 community: &Community,
2946 channel_id: &super::ChannelId,
2947) -> Result<u64, String> {
2948 let session = SessionGuard::capture();
2949 let server_root = community.server_root_key.as_bytes();
2950 let cid = community.id.to_hex();
2951 let channel_hex = channel_id.to_hex();
2952 let mut server_roots: Vec<[u8; 32]> = crate::db::community::held_epoch_keys(&cid, crate::community::SERVER_ROOT_SCOPE_HEX)
2958 .unwrap_or_default()
2959 .into_iter()
2960 .map(|(_, k)| k)
2961 .collect();
2962 if !server_roots.iter().any(|r| r == server_root) {
2963 server_roots.push(*server_root); }
2965 let mut head = community
2966 .channels
2967 .iter()
2968 .find(|c| &c.id == channel_id)
2969 .ok_or("channel not found in community")?
2970 .epoch
2971 .0;
2972
2973 let mut forked_epochs: std::collections::BTreeSet<u64> = std::collections::BTreeSet::new();
2977
2978 for _round in 0..MAX_REKEY_CATCHUP_ROUNDS {
2979 let window_top = head.saturating_add(REKEY_CATCHUP_WINDOW);
2980 let mut parsed: Vec<super::rekey::ParsedRekey> = Vec::new();
2984 for sr in &server_roots {
2985 let z_tags: Vec<String> = (head.saturating_add(1)..=window_top)
2986 .map(|e| super::derive::rekey_pseudonym(&super::ServerRootKey(*sr), channel_id, super::Epoch(e)).to_hex())
2987 .collect();
2988 let query = Query { kinds: vec![event_kind::COMMUNITY_REKEY], z_tags, ..Default::default() };
2989 for ev in transport.fetch(&query, &community.relays).await.unwrap_or_default() {
2994 if let Ok(p) = super::rekey::open_rekey_event(&ev, sr) {
2995 if matches!(p.scope, super::derive::RekeyScope::Channel(c) if &c == channel_id) {
2996 parsed.push(p);
2997 }
2998 }
2999 }
3000 }
3001 if parsed.is_empty() {
3002 break; }
3004 parsed.sort_by_key(|p| p.new_epoch.0);
3006 let max_found = parsed.last().map(|p| p.new_epoch.0).unwrap_or(head);
3007
3008 let head_before = head;
3009 let mut removed = false;
3010 let mut by_epoch: std::collections::BTreeMap<u64, Vec<&super::rekey::ParsedRekey>> = std::collections::BTreeMap::new();
3015 for p in &parsed {
3016 by_epoch.entry(p.new_epoch.0).or_default().push(p);
3017 }
3018 for (e, chunks) in by_epoch {
3019 if !session.is_valid() {
3020 return Err("session changed during rekey catch-up".to_string());
3021 }
3022 let mut applied = false;
3023 let mut saw_not_recipient = false;
3024 for p in &chunks {
3025 match apply_channel_rekey(community, p) {
3026 Ok(RekeyOutcome::Applied { .. }) => {
3027 applied = true;
3028 break;
3029 }
3030 Ok(RekeyOutcome::NotARecipient) => saw_not_recipient = true,
3031 Err(err) => crate::log_warn!("rekey catch-up: skipping epoch {e} chunk: {err}"),
3032 }
3033 }
3034 if applied {
3035 if let Some(p) = chunks.first() {
3041 let pe = p.prev_epoch.0;
3042 if let Ok(Some(prev_key)) = crate::db::community::held_epoch_key(&cid, &channel_hex, pe) {
3043 if super::rekey::epoch_key_commitment(p.prev_epoch, &prev_key) != p.prev_key_commitment {
3044 forked_epochs.insert(pe);
3045 }
3046 }
3047 }
3048 if e > head + 1 {
3051 crate::log_warn!(
3052 "rekey catch-up: channel epochs {}..={} not recovered (key gap; history unreadable until re-fetched)",
3053 head + 1, e - 1
3054 );
3055 }
3056 head = head.max(e);
3057 } else if saw_not_recipient {
3058 removed = true;
3061 break;
3062 }
3063 }
3065
3066 if removed || head == head_before || max_found < window_top {
3069 break;
3070 }
3071 }
3072
3073 let held: std::collections::HashSet<u64> = crate::db::community::held_epoch_keys(&cid, &channel_hex)
3080 .unwrap_or_default()
3081 .into_iter()
3082 .map(|(e, _)| e.0)
3083 .collect();
3084 let missing: Vec<u64> = (0..head).filter(|e| !held.contains(e)).collect();
3085 if !missing.is_empty() {
3086 for sr in &server_roots {
3087 if !session.is_valid() {
3088 return Err("session changed during rekey gap-fill".to_string());
3089 }
3090 let z_tags: Vec<String> = missing
3091 .iter()
3092 .map(|e| super::derive::rekey_pseudonym(&super::ServerRootKey(*sr), channel_id, super::Epoch(*e)).to_hex())
3093 .collect();
3094 let query = Query { kinds: vec![event_kind::COMMUNITY_REKEY], z_tags, ..Default::default() };
3095 for ev in transport.fetch(&query, &community.relays).await.unwrap_or_default() {
3098 if let Ok(p) = super::rekey::open_rekey_event(&ev, sr) {
3099 if matches!(p.scope, super::derive::RekeyScope::Channel(c) if &c == channel_id) {
3100 let _ = apply_channel_rekey(community, &p); }
3102 }
3103 }
3104 }
3105 }
3106
3107 if head > 0 && session.is_valid() {
3114 let lo = head.saturating_sub(REKEY_CATCHUP_WINDOW).max(1);
3115 let mut epochs: std::collections::BTreeSet<u64> = (lo..=head).collect();
3116 epochs.append(&mut forked_epochs);
3117 let _ = heal_channel_fork_epochs(transport, community, channel_id, &cid, &channel_hex, &epochs, &server_roots, &session).await;
3118 }
3119 Ok(head)
3120}
3121
3122const MAX_BASE_CATCHUP_STEPS: usize = 256;
3125
3126fn peek_my_server_root(parsed: &super::rekey::ParsedRekey) -> Result<Option<[u8; 32]>, String> {
3140 if !matches!(parsed.scope, super::derive::RekeyScope::ServerRoot) {
3141 return Ok(None);
3142 }
3143 let my_keys = crate::state::MY_SECRET_KEY.to_keys().ok_or("no local key to open a base rekey blob")?;
3144 let secret = super::rekey::rekey_pairwise_secret(my_keys.secret_key(), &parsed.rotator)?;
3145 let my_locator = super::derive::recipient_pseudonym(&secret, parsed.scope, parsed.new_epoch).to_hex();
3146 let mine = match parsed.blobs.iter().find(|b| b.locator == my_locator) {
3147 Some(b) => b,
3148 None => return Ok(None),
3149 };
3150 super::rekey::open_rekey_blob(my_keys.secret_key(), &parsed.rotator, parsed.scope, parsed.new_epoch, mine).map(Some)
3151}
3152
3153fn peek_my_channel_key(parsed: &super::rekey::ParsedRekey) -> Option<[u8; 32]> {
3157 let my_keys = crate::state::MY_SECRET_KEY.to_keys()?;
3158 let secret = super::rekey::rekey_pairwise_secret(my_keys.secret_key(), &parsed.rotator).ok()?;
3159 let my_locator = super::derive::recipient_pseudonym(&secret, parsed.scope, parsed.new_epoch).to_hex();
3160 let mine = parsed.blobs.iter().find(|b| b.locator == my_locator)?;
3161 super::rekey::open_rekey_blob(my_keys.secret_key(), &parsed.rotator, parsed.scope, parsed.new_epoch, mine).ok()
3162}
3163
3164pub async fn catch_up_server_root<T: Transport + ?Sized>(
3165 transport: &T,
3166 community: &Community,
3167) -> Result<BaseCatchup, String> {
3168 let session = SessionGuard::capture();
3169 let cid = community.id.to_hex();
3170 let mut head = community.server_root_epoch.0;
3171 let mut removed = false;
3176 let mut current_root: [u8; 32] = *community.server_root_key.as_bytes();
3178
3179 for _step in 0..MAX_BASE_CATCHUP_STEPS {
3180 let next = match head.checked_add(1) {
3181 Some(n) => n,
3182 None => break,
3183 };
3184 let addr = super::derive::base_rekey_pseudonym(&super::ServerRootKey(current_root), &community.id, super::Epoch(next)).to_hex();
3185 let query = Query { kinds: vec![event_kind::COMMUNITY_REKEY], z_tags: vec![addr], ..Default::default() };
3186 let events = transport.fetch(&query, &community.relays).await?;
3187 if events.is_empty() {
3188 break; }
3190
3191 let chunks: Vec<super::rekey::ParsedRekey> = events
3194 .iter()
3195 .filter_map(|ev| super::rekey::open_rekey_event(ev, ¤t_root).ok())
3196 .filter(|p| matches!(p.scope, super::derive::RekeyScope::ServerRoot) && p.new_epoch.0 == next)
3197 .collect();
3198 if chunks.is_empty() {
3199 break; }
3201
3202 if !session.is_valid() {
3203 return Err("session changed during base rekey catch-up".to_string());
3204 }
3205
3206 let owner_hex = proven_owner_hex(community);
3220 let roster = crate::db::community::get_community_roles(&cid).unwrap_or_default();
3221 let mut candidates: Vec<(&super::rekey::ParsedRekey, [u8; 32])> = Vec::new();
3222 for parsed in &chunks {
3223 if !rotator_is_authorized(&cid, &roster, owner_hex.as_deref(), &parsed.rotator.to_hex(), super::roles::Permissions::BAN) {
3224 continue;
3225 }
3226 match peek_my_server_root(parsed) {
3227 Ok(Some(root)) => candidates.push((parsed, root)),
3228 Ok(None) => {}
3229 Err(err) => crate::log_warn!("base rekey catch-up: epoch {next} peek: {err}"),
3230 }
3231 }
3232 let applied = match candidates.into_iter().min_by(|a, b| a.1.cmp(&b.1)) {
3233 Some((parsed, _)) => match apply_server_root_rekey(community, parsed) {
3234 Ok(RekeyOutcome::Applied { .. }) => true,
3235 Ok(RekeyOutcome::NotARecipient) => false, Err(err) => { crate::log_warn!("base rekey catch-up: epoch {next} apply: {err}"); false }
3237 },
3238 None => {
3239 let owner = proven_owner_hex(community);
3244 let roster = crate::db::community::get_community_roles(&cid).unwrap_or_default();
3245 if chunks.iter().any(|p| rotator_is_authorized(&cid, &roster, owner.as_deref(), &p.rotator.to_hex(), super::roles::Permissions::BAN)) {
3246 removed = true;
3247 }
3248 false }
3250 };
3251 if !applied {
3252 break;
3253 }
3254 match crate::db::community::held_epoch_key(&cid, crate::community::SERVER_ROOT_SCOPE_HEX, next)? {
3256 Some(root) => {
3257 current_root = root;
3258 head = next;
3259 }
3260 None => {
3261 crate::log_warn!("base rekey catch-up: epoch {next} applied but its root is not archived; halting walk");
3264 break;
3265 }
3266 }
3267 }
3268
3269 if head > 0 && !removed {
3276 if let Ok(Some(prior_root)) = crate::db::community::held_epoch_key(&cid, crate::community::SERVER_ROOT_SCOPE_HEX, head - 1) {
3277 let addr = super::derive::base_rekey_pseudonym(&super::ServerRootKey(prior_root), &community.id, super::Epoch(head)).to_hex();
3278 let query = Query { kinds: vec![event_kind::COMMUNITY_REKEY], z_tags: vec![addr], ..Default::default() };
3279 let events = transport.fetch(&query, &community.relays).await.unwrap_or_default();
3280 let chunks: Vec<super::rekey::ParsedRekey> = events
3281 .iter()
3282 .filter_map(|ev| super::rekey::open_rekey_event(ev, &prior_root).ok())
3283 .filter(|p| matches!(p.scope, super::derive::RekeyScope::ServerRoot) && p.new_epoch.0 == head)
3284 .collect();
3285 let owner_hex = proven_owner_hex(community);
3286 let roster = crate::db::community::get_community_roles(&cid).unwrap_or_default();
3287 let mut best: Option<(&super::rekey::ParsedRekey, [u8; 32])> = None;
3288 for p in &chunks {
3289 if !rotator_is_authorized(&cid, &roster, owner_hex.as_deref(), &p.rotator.to_hex(), super::roles::Permissions::BAN) {
3293 continue;
3294 }
3295 if let Ok(Some(root)) = peek_my_server_root(p) {
3296 if best.as_ref().map_or(true, |(_, br)| root < *br) {
3297 best = Some((p, root));
3298 }
3299 }
3300 }
3301 let current_deauthorized = chunks.iter().any(|p| {
3311 matches!(peek_my_server_root(p), Ok(Some(r)) if r == current_root)
3312 && !rotator_is_authorized(&cid, &roster, owner_hex.as_deref(), &p.rotator.to_hex(), super::roles::Permissions::BAN)
3313 });
3314 if let Some((winner, win_root)) = best {
3315 let adopt = if current_deauthorized {
3316 win_root != current_root
3317 } else {
3318 win_root < current_root
3319 };
3320 if adopt {
3321 if !session.is_valid() {
3322 return Err("session changed during base convergence".to_string());
3323 }
3324 if apply_server_root_rekey(community, winner).is_ok() {
3327 match crate::db::community::converge_server_root_epoch(&cid, head, &win_root) {
3328 Ok(false) => crate::log_trace!("base heal: converge of epoch {head} did not apply (head moved)"),
3329 Err(e) => crate::log_trace!("base heal: converge of epoch {head} errored: {e}"),
3330 Ok(true) => {}
3331 }
3332 current_root = win_root;
3333 if let Ok(Some(fresh)) = crate::db::community::load_community(&community.id) {
3334 let _ = fetch_and_apply_control(transport, &fresh).await;
3335 }
3336 }
3337 }
3338 }
3339 }
3340 }
3341 let _ = current_root; Ok(BaseCatchup { epoch: head, removed })
3343}
3344
3345#[derive(Debug, Clone, Copy)]
3349pub struct BaseCatchup {
3350 pub epoch: u64,
3351 pub removed: bool,
3352}
3353
3354#[cfg(test)]
3355mod tests {
3356 use super::*;
3357 use crate::community::send::fetch_channel_messages;
3358 use crate::community::transport::{memory::MemoryRelay, Query, Transport};
3359 use nostr_sdk::prelude::{EventBuilder, Kind};
3360
3361 struct FailingRelay;
3364 #[async_trait::async_trait]
3365 impl Transport for FailingRelay {
3366 async fn publish(&self, _event: &Event, _relays: &[String]) -> Result<(), String> {
3367 Err("relay unreachable".to_string())
3368 }
3369 async fn publish_durable(&self, _event: &Event, _relays: &[String]) -> Result<(), String> {
3370 Err("relay unreachable".to_string())
3371 }
3372 async fn fetch(&self, _query: &Query, _relays: &[String]) -> Result<Vec<Event>, String> {
3373 Ok(Vec::new())
3374 }
3375 }
3376
3377 struct RekeyFailingRelay {
3381 inner: MemoryRelay,
3382 fail_rekey: std::sync::atomic::AtomicBool,
3383 }
3384 impl RekeyFailingRelay {
3385 fn new() -> Self {
3386 Self { inner: MemoryRelay::new(), fail_rekey: std::sync::atomic::AtomicBool::new(true) }
3387 }
3388 fn allow_rekey(&self) {
3389 self.fail_rekey.store(false, std::sync::atomic::Ordering::Relaxed);
3390 }
3391 fn blocks(&self, event: &Event) -> bool {
3392 self.fail_rekey.load(std::sync::atomic::Ordering::Relaxed)
3393 && event.kind.as_u16() == crate::stored_event::event_kind::COMMUNITY_REKEY
3394 }
3395 }
3396 #[async_trait::async_trait]
3397 impl Transport for RekeyFailingRelay {
3398 async fn publish(&self, event: &Event, relays: &[String]) -> Result<(), String> {
3399 if self.blocks(event) { return Err("rekey relay down".to_string()); }
3400 self.inner.publish(event, relays).await
3401 }
3402 async fn publish_durable(&self, event: &Event, relays: &[String]) -> Result<(), String> {
3403 if self.blocks(event) { return Err("rekey relay down".to_string()); }
3404 self.inner.publish_durable(event, relays).await
3405 }
3406 async fn fetch(&self, query: &Query, relays: &[String]) -> Result<Vec<Event>, String> {
3407 self.inner.fetch(query, relays).await
3408 }
3409 }
3410
3411 static TEST_COUNTER: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(5000);
3412
3413 fn make_test_npub(n: u32) -> String {
3414 const BECH32: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
3415 let mut payload = vec![b'q'; 58];
3416 let mut x = n as u64;
3417 let mut i = 58;
3418 while x > 0 && i > 0 {
3419 i -= 1;
3420 payload[i] = BECH32[(x as usize) % 32];
3421 x /= 32;
3422 }
3423 format!("npub1{}", std::str::from_utf8(&payload).unwrap())
3424 }
3425
3426 fn init_test_db() -> (tempfile::TempDir, std::sync::MutexGuard<'static, ()>) {
3427 let guard = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
3428 crate::db::close_database();
3429 crate::db::clear_id_caches();
3432 let tmp = tempfile::tempdir().unwrap();
3433 let n = TEST_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3434 let account = make_test_npub(n);
3435 std::fs::create_dir_all(tmp.path().join(&account)).unwrap();
3436 crate::db::set_app_data_dir(tmp.path().to_path_buf());
3437 crate::db::set_current_account(account.clone()).unwrap();
3438 crate::db::init_database(&account).unwrap();
3439 let _ = crate::state::take_nostr_client();
3442 let owner = Keys::generate();
3444 crate::state::MY_SECRET_KEY.store_from_keys(&owner, &[]);
3445 crate::state::set_my_public_key(owner.public_key());
3446 (tmp, guard)
3447 }
3448
3449 #[test]
3450 fn community_cap_rejects_a_new_membership_at_the_limit() {
3451 let (_tmp, _guard) = init_test_db();
3452 let mk = |i: usize| {
3453 let id = format!("{:064x}", i);
3454 crate::community::list::CommunityListEntry {
3455 community_id: id.clone(),
3456 seed: crate::community::invite::CommunityInvite {
3457 community_id: id,
3458 name: String::new(),
3459 server_root_key: String::new(),
3460 server_root_epoch: 0,
3461 relays: vec![],
3462 channels: vec![],
3463 owner_attestation: None,
3464 icon: None,
3465 },
3466 current: None,
3467 added_at: 0,
3468 }
3469 };
3470 let mut list = crate::community::list::CommunityList::default();
3471 for i in 0..(MAX_COMMUNITIES - 1) {
3472 list.entries.push(mk(i));
3473 }
3474 crate::db::settings::set_sql_setting("community_list_json".to_string(), list.to_json()).unwrap();
3475 assert!(enforce_community_cap().is_ok(), "under the cap a new join is allowed");
3476
3477 list.entries.push(mk(MAX_COMMUNITIES - 1)); crate::db::settings::set_sql_setting("community_list_json".to_string(), list.to_json()).unwrap();
3479 assert!(enforce_community_cap().is_err(), "at the cap a new join is rejected");
3480 }
3481
3482 fn saved_community_owned_by(owner: &Keys) -> Community {
3487 use nostr_sdk::JsonUtil;
3488 let mut community = Community::create("HQ", "general", vec!["r".into()]);
3489 let cid = community.id.to_hex();
3490 community.owner_attestation = Some(
3491 crate::community::owner::build_owner_attestation_unsigned(owner.public_key(), &cid)
3492 .sign_with_keys(owner)
3493 .unwrap()
3494 .as_json(),
3495 );
3496 crate::db::community::save_community(&community).unwrap();
3497 community
3498 }
3499
3500 fn attested_community(name: &str, channel: &str, relays: Vec<String>) -> Community {
3504 use nostr_sdk::JsonUtil;
3505 let owner = crate::state::MY_SECRET_KEY.to_keys().unwrap();
3506 let mut community = Community::create(name, channel, relays);
3507 community.owner_attestation = Some(
3508 crate::community::owner::build_owner_attestation_unsigned(owner.public_key(), &community.id.to_hex())
3509 .sign_with_keys(&owner).unwrap().as_json(),
3510 );
3511 community
3512 }
3513
3514 fn become_local(me: &Keys) {
3516 crate::state::MY_SECRET_KEY.store_from_keys(me, &[]);
3517 crate::state::set_my_public_key(me.public_key());
3518 }
3519
3520 fn owner_channel_rekey(
3523 owner: &Keys,
3524 community: &Community,
3525 recipient_pk: &nostr_sdk::PublicKey,
3526 new_epoch: u64,
3527 new_key: &[u8; 32],
3528 ) -> super::super::rekey::ParsedRekey {
3529 let chan = &community.channels[0];
3530 let scope = super::super::derive::RekeyScope::Channel(chan.id);
3531 let blob = super::super::rekey::build_rekey_blob(
3532 owner.secret_key(), recipient_pk, scope, crate::community::Epoch(new_epoch), new_key,
3533 )
3534 .unwrap();
3535 let commit = super::super::rekey::epoch_key_commitment(crate::community::Epoch(0), chan.key.as_bytes());
3536 let outer = super::super::rekey::build_channel_rekey_event(
3537 &Keys::generate(), owner, community.server_root_key.as_bytes(), &chan.id,
3538 crate::community::Epoch(new_epoch), crate::community::Epoch(0), &commit, &[blob],
3539 )
3540 .unwrap();
3541 super::super::rekey::open_rekey_event(&outer, community.server_root_key.as_bytes()).unwrap()
3542 }
3543
3544 #[tokio::test]
3549 async fn outer_event_dedup_skips_an_already_persisted_wire_event() {
3550 let (_tmp, _guard) = init_test_db();
3551 let owner = Keys::generate();
3552 let me = Keys::generate();
3553 become_local(&me);
3554 let community = saved_community_owned_by(&owner);
3555 let channel = community.channels[0].clone();
3556 let chan_hex = channel.id.to_hex();
3557
3558 let author = Keys::generate();
3560 let outer = crate::community::envelope::seal_message(
3561 &author, &channel.key, &channel.id, channel.epoch, "gm", 1000,
3562 ).unwrap();
3563 let outer_hex = outer.id.to_hex();
3564
3565 let mut state = crate::state::ChatState::new();
3567 let msg = match crate::community::inbound::process_incoming(&mut state, &outer, &channel, &me.public_key()) {
3568 Some(crate::community::inbound::IncomingEvent::NewMessage(m)) => m,
3569 _ => panic!("expected NewMessage from a fresh wire event"),
3570 };
3571 assert_eq!(msg.wrapper_event_id.as_deref(), Some(outer_hex.as_str()),
3572 "the inner must carry its outer wire id as wrapper_event_id");
3573
3574 crate::db::events::save_message(&chan_hex, &msg).await.unwrap();
3576
3577 let mut state2 = crate::state::ChatState::new();
3579 let second = crate::community::inbound::process_incoming(&mut state2, &outer, &channel, &me.public_key());
3580 assert!(second.is_none(), "an already-processed wire event must dedup before decryption");
3581 }
3582
3583 #[tokio::test]
3586 async fn ledger_is_shared_but_negentropy_stays_nip17_only() {
3587 let (_tmp, _guard) = init_test_db();
3588 let dm = [0xA1u8; 32];
3589 let concord = [0xC0u8; 32];
3590 crate::db::wrappers::save_processed_wrapper(&dm, 100, crate::db::wrappers::TRANSPORT_NIP17).unwrap();
3591 crate::db::wrappers::save_processed_wrapper(&concord, 200, crate::db::wrappers::TRANSPORT_CONCORD).unwrap();
3592
3593 assert!(crate::db::wrappers::processed_wrapper_exists(&dm));
3595 assert!(crate::db::wrappers::processed_wrapper_exists(&concord));
3596
3597 let items = crate::db::wrappers::load_negentropy_items().unwrap();
3599 assert_eq!(items.len(), 1, "negentropy must exclude concord wrappers");
3600 assert_eq!(items[0].0.to_bytes(), dm);
3601 }
3602
3603 #[tokio::test]
3607 async fn non_message_subkind_dedups_via_the_shared_ledger() {
3608 let (_tmp, _guard) = init_test_db();
3609 let owner = Keys::generate();
3610 let me = Keys::generate();
3611 become_local(&me);
3612 let community = saved_community_owned_by(&owner);
3613 let channel = community.channels[0].clone();
3614
3615 let author = Keys::generate();
3617 let inner = super::super::envelope::build_inner_typed(
3618 author.public_key(), &channel.id, channel.epoch,
3619 crate::stored_event::event_kind::COMMUNITY_PRESENCE, "join", 5, None, &[],
3620 ).sign_with_keys(&author).unwrap();
3621 let outer = super::super::envelope::seal_with_signed_inner(
3622 &Keys::generate(), &inner, &channel.key, &channel.id, channel.epoch,
3623 ).unwrap();
3624
3625 let mut state = crate::state::ChatState::new();
3627 let first = crate::community::inbound::process_incoming(&mut state, &outer, &channel, &me.public_key());
3628 assert!(matches!(first, Some(crate::community::inbound::IncomingEvent::Presence { .. })),
3629 "expected a Presence outcome");
3630 assert!(crate::db::wrappers::processed_wrapper_exists(&outer.id.to_bytes()),
3631 "a non-message sub-kind must record its outer id in the shared ledger");
3632
3633 let second = crate::community::inbound::process_incoming(&mut crate::state::ChatState::new(), &outer, &channel, &me.public_key());
3635 assert!(second.is_none(), "a re-fetched presence must dedup via the shared ledger");
3636 }
3637
3638 #[test]
3639 fn apply_channel_rekey_recovers_and_advances_head() {
3640 let (_tmp, _guard) = init_test_db();
3641 let owner = Keys::generate(); let me = Keys::generate();
3643 become_local(&me);
3644 let community = saved_community_owned_by(&owner);
3645 let cid = community.id.to_hex();
3646 let chan_hex = community.channels[0].id.to_hex();
3647 let new_key = [0xCDu8; 32];
3648
3649 let parsed = owner_channel_rekey(&owner, &community, &me.public_key(), 1, &new_key);
3650 let outcome = apply_channel_rekey(&community, &parsed).unwrap();
3651 assert_eq!(outcome, RekeyOutcome::Applied { head_advanced: true });
3652
3653 assert_eq!(crate::db::community::held_epoch_key(&cid, &chan_hex, 1).unwrap(), Some(new_key));
3655 let reloaded = crate::db::community::load_community(&community.id).unwrap().unwrap();
3656 assert_eq!(reloaded.channels[0].epoch, crate::community::Epoch(1));
3657 assert_eq!(reloaded.channels[0].key.as_bytes(), &new_key);
3658 assert!(crate::db::community::held_epoch_key(&cid, &chan_hex, 0).unwrap().is_some());
3660 }
3661
3662 #[test]
3663 fn apply_channel_rekey_accepts_matching_continuity() {
3664 let (_tmp, _guard) = init_test_db();
3667 let owner = Keys::generate();
3668 let me = Keys::generate();
3669 become_local(&me);
3670 let community = saved_community_owned_by(&owner);
3671 let parsed = owner_channel_rekey(&owner, &community, &me.public_key(), 1, &[0x44u8; 32]);
3673 assert_eq!(
3674 apply_channel_rekey(&community, &parsed).unwrap(),
3675 RekeyOutcome::Applied { head_advanced: true },
3676 "a rekey whose prior-key commitment matches the held genesis key applies"
3677 );
3678 }
3679
3680 #[test]
3681 fn advance_channel_epoch_archives_when_no_head_row() {
3682 let (_tmp, _guard) = init_test_db();
3685 let cid = "f".repeat(64);
3686 let orphan_channel = "a".repeat(64);
3687 let advanced = crate::db::community::advance_channel_epoch(&cid, &orphan_channel, 2, &[0x77u8; 32]).unwrap();
3688 assert!(!advanced, "no head row → head not advanced");
3689 assert_eq!(crate::db::community::held_epoch_key(&cid, &orphan_channel, 2).unwrap(), Some([0x77u8; 32]), "key still archived");
3690 }
3691
3692 #[tokio::test]
3693 async fn rotate_channel_publishes_recoverable_rekey_and_advances_own_head() {
3694 use crate::community::derive::{recipient_pseudonym, rekey_pseudonym};
3695 use crate::community::rekey::{open_rekey_blob, open_rekey_event, rekey_pairwise_secret};
3696 let (_tmp, _guard) = init_test_db();
3697 let owner = Keys::generate();
3698 become_local(&owner); let community = saved_community_owned_by(&owner);
3700 let channel_id = community.channels[0].id;
3701 let member = Keys::generate(); let relay = MemoryRelay::new();
3703
3704 let new_epoch = rotate_channel(&relay, &community, &channel_id, &[member.public_key()], community.server_root_key.as_bytes())
3705 .await
3706 .expect("rotate");
3707 assert_eq!(new_epoch, 1);
3708
3709 let reloaded = crate::db::community::load_community(&community.id).unwrap().unwrap();
3711 assert_eq!(reloaded.channels[0].epoch, crate::community::Epoch(1));
3712
3713 let addr = rekey_pseudonym(
3716 &crate::community::ServerRootKey(*community.server_root_key.as_bytes()),
3717 &channel_id, crate::community::Epoch(1),
3718 )
3719 .to_hex();
3720 let found = relay
3721 .fetch(&Query { kinds: vec![event_kind::COMMUNITY_REKEY], z_tags: vec![addr], ..Default::default() }, &community.relays)
3722 .await
3723 .unwrap();
3724 assert_eq!(found.len(), 1, "rekey addressable by its server-root pseudonym");
3725 let parsed = open_rekey_event(&found[0], community.server_root_key.as_bytes()).unwrap();
3726 assert_eq!(parsed.rotator, owner.public_key());
3727 assert_eq!(parsed.new_epoch, crate::community::Epoch(1));
3728 assert_eq!(parsed.prev_epoch, crate::community::Epoch(0));
3729 assert_eq!(parsed.blobs.len(), 2, "the member + me (multi-device) each get a blob");
3730
3731 let secret = rekey_pairwise_secret(member.secret_key(), &parsed.rotator).unwrap();
3733 let loc = recipient_pseudonym(&secret, parsed.scope, parsed.new_epoch).to_hex();
3734 let mine = parsed.blobs.iter().find(|b| b.locator == loc).expect("member's blob present");
3735 let recovered = open_rekey_blob(member.secret_key(), &parsed.rotator, parsed.scope, parsed.new_epoch, mine).unwrap();
3736 assert_eq!(reloaded.channels[0].key.as_bytes(), &recovered, "member's recovered key == my advanced head key");
3737 }
3738
3739 #[tokio::test]
3740 async fn rotate_channel_failed_publish_leaves_head_unadvanced() {
3741 let (_tmp, _guard) = init_test_db();
3744 let owner = Keys::generate();
3745 become_local(&owner);
3746 let community = saved_community_owned_by(&owner);
3747 let member = Keys::generate();
3748 let err = rotate_channel(&FailingRelay, &community, &community.channels[0].id, &[member.public_key()], community.server_root_key.as_bytes()).await;
3749 assert!(err.is_err(), "a failed publish must propagate, not silently advance");
3750 let reloaded = crate::db::community::load_community(&community.id).unwrap().unwrap();
3751 assert_eq!(reloaded.channels[0].epoch, crate::community::Epoch(0), "head stays put on publish failure");
3752 }
3753
3754 fn build_rekey_chain(
3758 owner: &Keys, community: &Community, recipient_pk: &nostr_sdk::PublicKey, n: u64,
3759 ) -> (Vec<Event>, Vec<[u8; 32]>) {
3760 let chan = &community.channels[0];
3761 let scope = super::super::derive::RekeyScope::Channel(chan.id);
3762 let mut prev_key = *chan.key.as_bytes();
3763 let mut events = Vec::new();
3764 let mut keys = Vec::new();
3765 for e in 1..=n {
3766 let new_key = [e as u8; 32];
3767 let blob = super::super::rekey::build_rekey_blob(owner.secret_key(), recipient_pk, scope, crate::community::Epoch(e), &new_key).unwrap();
3768 let commit = super::super::rekey::epoch_key_commitment(crate::community::Epoch(e - 1), &prev_key);
3769 let ev = super::super::rekey::build_channel_rekey_event(
3770 &Keys::generate(), owner, community.server_root_key.as_bytes(), &chan.id,
3771 crate::community::Epoch(e), crate::community::Epoch(e - 1), &commit, &[blob],
3772 ).unwrap();
3773 events.push(ev);
3774 keys.push(new_key);
3775 prev_key = new_key;
3776 }
3777 (events, keys)
3778 }
3779
3780 #[tokio::test]
3781 async fn catch_up_steps_over_a_missing_epoch() {
3782 let (_tmp, _guard) = init_test_db();
3785 let owner = Keys::generate();
3786 let me = Keys::generate();
3787 become_local(&me);
3788 let community = saved_community_owned_by(&owner);
3789 let channel_id = community.channels[0].id;
3790 let cid = community.id.to_hex();
3791 let chan_hex = channel_id.to_hex();
3792
3793 let (events, keys) = build_rekey_chain(&owner, &community, &me.public_key(), 3);
3794 let relay = MemoryRelay::new();
3795 relay.inject(&events[0], &community.relays); relay.inject(&events[2], &community.relays); let reached = catch_up_channel_rekeys(&relay, &community, &channel_id).await.unwrap();
3798
3799 assert_eq!(reached, 3, "head reaches the latest present epoch, stepping over the gap");
3800 assert_eq!(crate::db::community::held_epoch_key(&cid, &chan_hex, 1).unwrap(), Some(keys[0]));
3801 assert_eq!(crate::db::community::held_epoch_key(&cid, &chan_hex, 2).unwrap(), None, "missing epoch is a hole");
3802 assert_eq!(crate::db::community::held_epoch_key(&cid, &chan_hex, 3).unwrap(), Some(keys[2]));
3803 }
3804
3805 #[tokio::test]
3806 async fn catch_up_recovers_a_rekey_under_a_prior_server_root() {
3807 let (_tmp, _guard) = init_test_db();
3812 let owner = Keys::generate();
3813 let me = Keys::generate();
3814 become_local(&me);
3815 let root0_community = saved_community_owned_by(&owner);
3816 let cid = root0_community.id.to_hex();
3817 let channel_id = root0_community.channels[0].id;
3818 let chan_hex = channel_id.to_hex();
3819 let scope = super::super::derive::RekeyScope::Channel(channel_id);
3820 let genesis_key = *root0_community.channels[0].key.as_bytes();
3821
3822 let root1 = [0x99u8; 32];
3824 crate::db::community::advance_server_root_epoch(&cid, 1, &root1).unwrap();
3825 let community = crate::db::community::load_community(&root0_community.id).unwrap().unwrap();
3826 assert_eq!(community.server_root_epoch, crate::community::Epoch(1));
3827
3828 let (k1, k2) = ([0x11u8; 32], [0x22u8; 32]);
3830 let blob1 = super::super::rekey::build_rekey_blob(owner.secret_key(), &me.public_key(), scope, crate::community::Epoch(1), &k1).unwrap();
3831 let commit0 = super::super::rekey::epoch_key_commitment(crate::community::Epoch(0), &genesis_key);
3832 let ev1 = super::super::rekey::build_channel_rekey_event(
3833 &Keys::generate(), &owner, root0_community.server_root_key.as_bytes(), &channel_id,
3834 crate::community::Epoch(1), crate::community::Epoch(0), &commit0, &[blob1]).unwrap();
3835 let blob2 = super::super::rekey::build_rekey_blob(owner.secret_key(), &me.public_key(), scope, crate::community::Epoch(2), &k2).unwrap();
3836 let commit1 = super::super::rekey::epoch_key_commitment(crate::community::Epoch(1), &k1);
3837 let ev2 = super::super::rekey::build_channel_rekey_event(
3838 &Keys::generate(), &owner, &root1, &channel_id,
3839 crate::community::Epoch(2), crate::community::Epoch(1), &commit1, &[blob2]).unwrap();
3840
3841 let relay = MemoryRelay::new();
3842 relay.inject(&ev1, &community.relays);
3843 relay.inject(&ev2, &community.relays);
3844
3845 let reached = catch_up_channel_rekeys(&relay, &community, &channel_id).await.unwrap();
3846 assert_eq!(reached, 2, "reached the latest channel epoch across the server-root rotation");
3847 assert_eq!(crate::db::community::held_epoch_key(&cid, &chan_hex, 1).unwrap(), Some(k1),
3848 "epoch-1 key recovered from a rekey under the PRIOR server root");
3849 assert_eq!(crate::db::community::held_epoch_key(&cid, &chan_hex, 2).unwrap(), Some(k2));
3850 }
3851
3852 #[tokio::test]
3853 async fn catch_up_backfills_a_sub_head_gap() {
3854 let (_tmp, _guard) = init_test_db();
3857 let owner = Keys::generate();
3858 let me = Keys::generate();
3859 become_local(&me);
3860 let community = saved_community_owned_by(&owner);
3861 let cid = community.id.to_hex();
3862 let channel_id = community.channels[0].id;
3863 let chan_hex = channel_id.to_hex();
3864 let scope = super::super::derive::RekeyScope::Channel(channel_id);
3865 let genesis_key = *community.channels[0].key.as_bytes();
3866
3867 let k2 = [0x22u8; 32];
3869 crate::db::community::advance_channel_epoch(&cid, &chan_hex, 2, &k2).unwrap();
3870 assert_eq!(crate::db::community::held_epoch_key(&cid, &chan_hex, 1).unwrap(), None, "epoch 1 starts as a hole");
3871
3872 let k1 = [0x11u8; 32];
3874 let blob1 = super::super::rekey::build_rekey_blob(owner.secret_key(), &me.public_key(), scope, crate::community::Epoch(1), &k1).unwrap();
3875 let commit0 = super::super::rekey::epoch_key_commitment(crate::community::Epoch(0), &genesis_key);
3876 let ev1 = super::super::rekey::build_channel_rekey_event(
3877 &Keys::generate(), &owner, community.server_root_key.as_bytes(), &channel_id,
3878 crate::community::Epoch(1), crate::community::Epoch(0), &commit0, &[blob1]).unwrap();
3879 let relay = MemoryRelay::new();
3880 relay.inject(&ev1, &community.relays);
3881
3882 let community = crate::db::community::load_community(&community.id).unwrap().unwrap();
3883 let reached = catch_up_channel_rekeys(&relay, &community, &channel_id).await.unwrap();
3884 assert_eq!(reached, 2, "head unchanged (gap-fill never regresses it)");
3885 assert_eq!(crate::db::community::held_epoch_key(&cid, &chan_hex, 1).unwrap(), Some(k1),
3886 "the sub-head hole was backfilled");
3887 }
3888
3889 #[tokio::test]
3890 async fn catch_up_walks_a_chain_of_rotations_to_the_latest() {
3891 let (_tmp, _guard) = init_test_db();
3892 let owner = Keys::generate();
3893 let me = Keys::generate();
3894 become_local(&me); let community = saved_community_owned_by(&owner);
3896 let channel_id = community.channels[0].id;
3897 let cid = community.id.to_hex();
3898 let chan_hex = channel_id.to_hex();
3899
3900 let (events, keys) = build_rekey_chain(&owner, &community, &me.public_key(), 3);
3902 let relay = MemoryRelay::new();
3903 for ev in events.iter().rev() {
3904 relay.inject(ev, &community.relays);
3905 }
3906
3907 let reached = catch_up_channel_rekeys(&relay, &community, &channel_id).await.unwrap();
3908 assert_eq!(reached, 3, "caught up to the latest epoch");
3909 let reloaded = crate::db::community::load_community(&community.id).unwrap().unwrap();
3911 assert_eq!(reloaded.channels[0].epoch, crate::community::Epoch(3));
3912 assert_eq!(reloaded.channels[0].key.as_bytes(), &keys[2]);
3913 for (i, k) in keys.iter().enumerate() {
3914 assert_eq!(crate::db::community::held_epoch_key(&cid, &chan_hex, (i + 1) as u64).unwrap(), Some(*k));
3915 }
3916 }
3917
3918 #[tokio::test]
3919 async fn catch_up_slides_across_the_window_boundary() {
3920 let (_tmp, _guard) = init_test_db();
3924 let owner = Keys::generate();
3925 let me = Keys::generate();
3926 become_local(&me);
3927 let community = saved_community_owned_by(&owner);
3928 let channel_id = community.channels[0].id;
3929 let cid = community.id.to_hex();
3930 let chan_hex = channel_id.to_hex();
3931
3932 let (events, keys) = build_rekey_chain(&owner, &community, &me.public_key(), 70);
3933 let relay = MemoryRelay::new();
3934 for ev in &events {
3935 relay.inject(ev, &community.relays);
3936 }
3937 let reached = catch_up_channel_rekeys(&relay, &community, &channel_id).await.unwrap();
3938 assert_eq!(reached, 70, "slid past the 64-epoch window boundary to the latest");
3939 assert_eq!(crate::db::community::held_epoch_key(&cid, &chan_hex, 70).unwrap(), Some(keys[69]));
3940 assert_eq!(crate::db::community::held_epoch_key(&cid, &chan_hex, 64).unwrap(), Some(keys[63]), "window-1 keys retained too");
3941 }
3942
3943 fn build_base_rekey_chain(
3949 owner: &Keys, community: &Community, recipient_pk: &nostr_sdk::PublicKey, n: u64,
3950 ) -> (Vec<Event>, Vec<[u8; 32]>) {
3951 let mut prior_root = *community.server_root_key.as_bytes();
3952 let mut events = Vec::new();
3953 let mut roots = Vec::new();
3954 for e in 1..=n {
3955 let new_root = [(e % 256) as u8; 32];
3956 let blob = super::super::rekey::build_rekey_blob(
3957 owner.secret_key(), recipient_pk, super::super::derive::RekeyScope::ServerRoot, crate::community::Epoch(e), &new_root,
3958 )
3959 .unwrap();
3960 let commit = super::super::rekey::epoch_key_commitment(crate::community::Epoch(e - 1), &prior_root);
3961 events.push(super::super::rekey::build_server_root_rekey_event(
3962 &Keys::generate(), owner, &prior_root, &community.id,
3963 crate::community::Epoch(e), crate::community::Epoch(e - 1), &commit, &[blob],
3964 ).unwrap());
3965 roots.push(new_root);
3966 prior_root = new_root;
3967 }
3968 (events, roots)
3969 }
3970
3971 #[tokio::test]
3972 async fn catch_up_server_root_walks_a_chain_of_base_rotations() {
3973 let (_tmp, _guard) = init_test_db();
3974 let owner = Keys::generate();
3975 let me = Keys::generate();
3976 become_local(&me);
3977 let community = saved_community_owned_by(&owner);
3978 let cid = community.id.to_hex();
3979
3980 let (events, roots) = build_base_rekey_chain(&owner, &community, &me.public_key(), 3);
3981 let relay = MemoryRelay::new();
3982 for ev in events.iter().rev() {
3983 relay.inject(ev, &community.relays);
3984 }
3985 let reached = catch_up_server_root(&relay, &community).await.unwrap();
3986 assert_eq!(reached.epoch, 3, "walked the base chain to the latest epoch");
3987 assert!(!reached.removed, "a normal catch-up is not a removal");
3988 let reloaded = crate::db::community::load_community(&community.id).unwrap().unwrap();
3989 assert_eq!(reloaded.server_root_epoch, crate::community::Epoch(3));
3990 assert_eq!(reloaded.server_root_key.as_bytes(), &roots[2], "base head is the latest root");
3991 for (i, r) in roots.iter().enumerate() {
3993 assert_eq!(crate::db::community::held_epoch_key(&cid, crate::community::SERVER_ROOT_SCOPE_HEX, (i + 1) as u64).unwrap(), Some(*r));
3994 }
3995 }
3996
3997 #[tokio::test]
3998 async fn catch_up_recovers_from_a_split_base_rotation_second_chunk() {
3999 let (_tmp, _guard) = init_test_db();
4003 let owner = Keys::generate();
4004 let me = Keys::generate();
4005 become_local(&me);
4006 let community = saved_community_owned_by(&owner);
4007 let genesis = *community.server_root_key.as_bytes();
4008 let new_root = [0x5Au8; 32];
4009 let scope = super::super::derive::RekeyScope::ServerRoot;
4010 let commit = super::super::rekey::epoch_key_commitment(crate::community::Epoch(0), &genesis);
4011 let mk = |recipient: &nostr_sdk::PublicKey| {
4012 let blob = super::super::rekey::build_rekey_blob(owner.secret_key(), recipient, scope, crate::community::Epoch(1), &new_root).unwrap();
4013 super::super::rekey::build_server_root_rekey_event(
4014 &Keys::generate(), &owner, &genesis, &community.id,
4015 crate::community::Epoch(1), crate::community::Epoch(0), &commit, &[blob],
4016 ).unwrap()
4017 };
4018 let relay = MemoryRelay::new();
4019 relay.inject(&mk(&Keys::generate().public_key()), &community.relays); relay.inject(&mk(&me.public_key()), &community.relays); let reached = catch_up_server_root(&relay, &community).await.unwrap();
4023 assert_eq!(reached.epoch, 1, "recovered the split rotation via the second chunk");
4024 let reloaded = crate::db::community::load_community(&community.id).unwrap().unwrap();
4025 assert_eq!(reloaded.server_root_key.as_bytes(), &new_root, "recovered the new root from chunk 2");
4026 }
4027
4028 #[tokio::test]
4029 async fn catch_up_converges_concurrent_refoundings_on_the_lowest_root() {
4030 let (_tmp, _guard) = init_test_db();
4034 let owner = Keys::generate();
4035 let me = Keys::generate();
4036 become_local(&me);
4037 let community = saved_community_owned_by(&owner);
4038 let genesis = *community.server_root_key.as_bytes();
4039 let scope = super::super::derive::RekeyScope::ServerRoot;
4040 let commit = super::super::rekey::epoch_key_commitment(crate::community::Epoch(0), &genesis);
4041 let root_lo = [0x10u8; 32];
4042 let root_hi = [0xF0u8; 32]; let mk = |root: &[u8; 32]| {
4044 let blob = super::super::rekey::build_rekey_blob(owner.secret_key(), &me.public_key(), scope, crate::community::Epoch(1), root).unwrap();
4045 super::super::rekey::build_server_root_rekey_event(
4046 &Keys::generate(), &owner, &genesis, &community.id,
4047 crate::community::Epoch(1), crate::community::Epoch(0), &commit, &[blob],
4048 ).unwrap()
4049 };
4050 let relay = MemoryRelay::new();
4051 relay.inject(&mk(&root_hi), &community.relays);
4053 relay.inject(&mk(&root_lo), &community.relays);
4054
4055 let reached = catch_up_server_root(&relay, &community).await.unwrap();
4056 assert_eq!(reached.epoch, 1, "advanced one epoch");
4057 let reloaded = crate::db::community::load_community(&community.id).unwrap().unwrap();
4058 assert_eq!(reloaded.server_root_key.as_bytes(), &root_lo, "converged on the LOWEST root, not the first-arrived");
4059 }
4060
4061 #[tokio::test]
4062 async fn rotate_retry_reuses_the_archived_root_no_same_epoch_fork() {
4063 let (_tmp, _guard) = init_test_db();
4068 let owner = Keys::generate();
4069 become_local(&owner);
4070 let community = saved_community_owned_by(&owner);
4071 let cid = community.id.to_hex();
4072 let relay = RekeyFailingRelay::new(); let member = Keys::generate();
4074
4075 assert!(rotate_server_root(&relay, &community, &[member.public_key()]).await.is_err(), "the rekey publish fails");
4076 let k1 = crate::db::community::held_epoch_key(&cid, crate::community::SERVER_ROOT_SCOPE_HEX, 1).unwrap()
4077 .expect("the new root is archived before publishing (fork-safety)");
4078 let reloaded = crate::db::community::load_community(&community.id).unwrap().unwrap();
4079 assert_eq!(reloaded.server_root_epoch, crate::community::Epoch(0), "head not advanced on a failed publish");
4080
4081 relay.allow_rekey();
4082 rotate_server_root(&relay, &reloaded, &[member.public_key()]).await.unwrap();
4083 let k2 = crate::db::community::held_epoch_key(&cid, crate::community::SERVER_ROOT_SCOPE_HEX, 1).unwrap().unwrap();
4084 assert_eq!(k1, k2, "the retry REUSES the archived root — no second root for epoch 1, no fork");
4085 let after = crate::db::community::load_community(&community.id).unwrap().unwrap();
4086 assert_eq!(after.server_root_epoch, crate::community::Epoch(1), "the retry completed the rotation");
4087 assert_eq!(after.server_root_key.as_bytes(), &k1, "the committed root is the one minted on attempt 1");
4088 }
4089
4090 #[tokio::test]
4091 async fn rotate_server_root_splits_a_large_recipient_set_into_multiple_events() {
4092 let (_tmp, _guard) = init_test_db();
4094 let owner = Keys::generate();
4095 become_local(&owner);
4096 let community = saved_community_owned_by(&owner);
4097 let genesis = *community.server_root_key.as_bytes();
4098 let relay = MemoryRelay::new();
4099 let recipients: Vec<_> = (0..super::super::rekey::MAX_REKEY_BLOBS).map(|_| Keys::generate().public_key()).collect();
4101 rotate_server_root(&relay, &community, &recipients).await.unwrap();
4102 let addr = super::super::derive::base_rekey_pseudonym(&super::super::ServerRootKey(genesis), &community.id, crate::community::Epoch(1)).to_hex();
4103 let evs = relay
4104 .fetch(&Query { kinds: vec![event_kind::COMMUNITY_REKEY], z_tags: vec![addr], ..Default::default() }, &community.relays)
4105 .await
4106 .unwrap();
4107 assert_eq!(evs.len(), 2, "a >MAX_REKEY_BLOBS rotation splits into 2 events at one address");
4108 }
4109
4110 #[tokio::test]
4111 async fn catch_up_server_root_is_a_noop_with_no_rotations() {
4112 let (_tmp, _guard) = init_test_db();
4113 let owner = Keys::generate();
4114 let me = Keys::generate();
4115 become_local(&me);
4116 let community = saved_community_owned_by(&owner);
4117 let relay = MemoryRelay::new();
4118 assert_eq!(catch_up_server_root(&relay, &community).await.unwrap().epoch, 0, "no base rotations → stays at 0");
4119 }
4120
4121 #[tokio::test]
4122 async fn concurrent_refounders_converge_to_the_lowest_root() {
4123 let (_tmp, _guard) = init_test_db();
4128 let owner = Keys::generate();
4129 let me = Keys::generate();
4130 become_local(&me); let community = saved_community_owned_by(&owner);
4132 let cid = community.id.to_hex();
4133 let genesis_root = *community.server_root_key.as_bytes();
4134 let scope = super::super::derive::RekeyScope::ServerRoot;
4135
4136 let root_lo = [0x10u8; 32];
4139 let root_hi = [0x99u8; 32]; let commit0 = super::super::rekey::epoch_key_commitment(crate::community::Epoch(0), &genesis_root);
4141 let blob_lo = super::super::rekey::build_rekey_blob(owner.secret_key(), &me.public_key(), scope, crate::community::Epoch(1), &root_lo).unwrap();
4142 let ev_lo = super::super::rekey::build_server_root_rekey_event(
4143 &Keys::generate(), &owner, &genesis_root, &community.id, crate::community::Epoch(1), crate::community::Epoch(0), &commit0, &[blob_lo]).unwrap();
4144
4145 let relay = MemoryRelay::new();
4146 relay.inject(&ev_lo, &community.relays);
4147
4148 crate::db::community::advance_server_root_epoch(&cid, 1, &root_hi).unwrap();
4150 let community = crate::db::community::load_community(&community.id).unwrap().unwrap();
4151 assert_eq!(community.server_root_key.as_bytes(), &root_hi, "start on the higher root");
4152
4153 let out = catch_up_server_root(&relay, &community).await.unwrap();
4154 assert_eq!(out.epoch, 1, "converged in place at the same epoch (not advanced)");
4155 assert!(!out.removed);
4156 let after = crate::db::community::load_community(&community.id).unwrap().unwrap();
4157 assert_eq!(after.server_root_key.as_bytes(), &root_lo, "originator converged to the lowest authorized root");
4158
4159 let _ = catch_up_server_root(&relay, &after).await.unwrap();
4161 assert_eq!(crate::db::community::load_community(&community.id).unwrap().unwrap().server_root_key.as_bytes(), &root_lo, "no flip-flop");
4162 }
4163
4164 #[tokio::test]
4165 async fn banned_rotators_rekey_is_not_a_convergence_candidate() {
4166 let (_tmp, _guard) = init_test_db();
4171 let owner = Keys::generate();
4172 let me = Keys::generate();
4173 let banned_admin = Keys::generate();
4174 become_local(&me);
4175 let community = saved_community_owned_by(&owner);
4176 let cid = community.id.to_hex();
4177 let genesis_root = *community.server_root_key.as_bytes();
4178 let scope = super::super::derive::RekeyScope::ServerRoot;
4179
4180 let role_id = "e".repeat(64);
4182 let roster = crate::community::roles::CommunityRoles {
4183 roles: vec![crate::community::roles::Role::admin(role_id.clone())],
4184 grants: vec![crate::community::roles::MemberGrant { member: banned_admin.public_key().to_hex(), role_ids: vec![role_id] }],
4185 };
4186 crate::db::community::set_community_roles(&cid, &roster, 1).unwrap();
4187 crate::db::community::set_community_banlist(&cid, &[banned_admin.public_key().to_hex()], 2).unwrap();
4189
4190 let root_evil = [0x01u8; 32];
4192 let commit0 = super::super::rekey::epoch_key_commitment(crate::community::Epoch(0), &genesis_root);
4193 let blob = super::super::rekey::build_rekey_blob(banned_admin.secret_key(), &me.public_key(), scope, crate::community::Epoch(1), &root_evil).unwrap();
4194 let ev = super::super::rekey::build_server_root_rekey_event(
4195 &Keys::generate(), &banned_admin, &genesis_root, &community.id, crate::community::Epoch(1), crate::community::Epoch(0), &commit0, &[blob]).unwrap();
4196 let relay = MemoryRelay::new();
4197 relay.inject(&ev, &community.relays);
4198
4199 let out = catch_up_server_root(&relay, &community).await.unwrap();
4202 assert_eq!(out.epoch, 0, "banned rotator's re-founding must not advance the base");
4203 assert!(!out.removed, "banned rotator's exclusion must not read as an authorized removal");
4204 let after = crate::db::community::load_community(&community.id).unwrap().unwrap();
4205 assert_eq!(after.server_root_key.as_bytes(), &genesis_root, "root unchanged");
4206
4207 let parsed = super::super::rekey::open_rekey_event(&ev, &genesis_root).unwrap();
4209 assert!(apply_server_root_rekey(&community, &parsed).is_err(), "apply must refuse a banned rotator");
4210 }
4211
4212 #[tokio::test]
4213 async fn heal_abandons_a_deauthorized_root_for_the_authorized_higher_sibling() {
4214 let (_tmp, _guard) = init_test_db();
4218 let owner = Keys::generate();
4219 let me = Keys::generate();
4220 let banned_admin = Keys::generate();
4221 become_local(&me);
4222 let community = saved_community_owned_by(&owner);
4223 let cid = community.id.to_hex();
4224 let genesis_root = *community.server_root_key.as_bytes();
4225 let scope = super::super::derive::RekeyScope::ServerRoot;
4226 let commit0 = super::super::rekey::epoch_key_commitment(crate::community::Epoch(0), &genesis_root);
4227
4228 let root_evil = [0x01u8; 32];
4231 let root_owner = [0x77u8; 32];
4232 let blob_evil = super::super::rekey::build_rekey_blob(banned_admin.secret_key(), &me.public_key(), scope, crate::community::Epoch(1), &root_evil).unwrap();
4233 let ev_evil = super::super::rekey::build_server_root_rekey_event(
4234 &Keys::generate(), &banned_admin, &genesis_root, &community.id, crate::community::Epoch(1), crate::community::Epoch(0), &commit0, &[blob_evil]).unwrap();
4235 let blob_owner = super::super::rekey::build_rekey_blob(owner.secret_key(), &me.public_key(), scope, crate::community::Epoch(1), &root_owner).unwrap();
4236 let ev_owner = super::super::rekey::build_server_root_rekey_event(
4237 &Keys::generate(), &owner, &genesis_root, &community.id, crate::community::Epoch(1), crate::community::Epoch(0), &commit0, &[blob_owner]).unwrap();
4238 let relay = MemoryRelay::new();
4239 relay.inject(&ev_evil, &community.relays);
4240 relay.inject(&ev_owner, &community.relays);
4241
4242 crate::db::community::advance_server_root_epoch(&cid, 1, &root_evil).unwrap();
4244 crate::db::community::set_community_banlist(&cid, &[banned_admin.public_key().to_hex()], 2).unwrap();
4245 let community = crate::db::community::load_community(&community.id).unwrap().unwrap();
4246 assert_eq!(community.server_root_key.as_bytes(), &root_evil, "start partitioned on the attacker's root");
4247
4248 let out = catch_up_server_root(&relay, &community).await.unwrap();
4249 assert_eq!(out.epoch, 1);
4250 assert!(!out.removed);
4251 let after = crate::db::community::load_community(&community.id).unwrap().unwrap();
4252 assert_eq!(after.server_root_key.as_bytes(), &root_owner,
4253 "heal must abandon the deauthorized root and adopt the owner's higher sibling");
4254
4255 let _ = catch_up_server_root(&relay, &after).await.unwrap();
4257 assert_eq!(crate::db::community::load_community(&community.id).unwrap().unwrap().server_root_key.as_bytes(), &root_owner, "no flap back to the banned root");
4258 }
4259
4260 #[tokio::test]
4261 async fn concurrent_channel_rekeyers_converge_to_the_lowest_key() {
4262 let (_tmp, _guard) = init_test_db();
4267 let owner = Keys::generate();
4268 let me = Keys::generate();
4269 become_local(&me); let community = saved_community_owned_by(&owner);
4271 let cid = community.id.to_hex();
4272 let channel_id = community.channels[0].id;
4273 let chan_hex = channel_id.to_hex();
4274 let scope = super::super::derive::RekeyScope::Channel(channel_id);
4275 let genesis_key = *community.channels[0].key.as_bytes();
4276 let root = *community.server_root_key.as_bytes();
4277 let commit0 = super::super::rekey::epoch_key_commitment(crate::community::Epoch(0), &genesis_key);
4278
4279 let key_lo = [0x10u8; 32];
4281 let key_hi = [0x99u8; 32];
4282 let blob_lo = super::super::rekey::build_rekey_blob(owner.secret_key(), &me.public_key(), scope, crate::community::Epoch(1), &key_lo).unwrap();
4283 let blob_hi = super::super::rekey::build_rekey_blob(owner.secret_key(), &me.public_key(), scope, crate::community::Epoch(1), &key_hi).unwrap();
4284 let ev_lo = super::super::rekey::build_channel_rekey_event(
4285 &Keys::generate(), &owner, &root, &channel_id, crate::community::Epoch(1), crate::community::Epoch(0), &commit0, &[blob_lo]).unwrap();
4286 let ev_hi = super::super::rekey::build_channel_rekey_event(
4287 &Keys::generate(), &owner, &root, &channel_id, crate::community::Epoch(1), crate::community::Epoch(0), &commit0, &[blob_hi]).unwrap();
4288
4289 let relay = MemoryRelay::new();
4290 relay.inject(&ev_hi, &community.relays); relay.inject(&ev_lo, &community.relays);
4292
4293 crate::db::community::advance_server_root_epoch(&cid, 1, &[0x42u8; 32]).unwrap();
4298 crate::db::community::advance_channel_epoch(&cid, &chan_hex, 1, &key_hi).unwrap();
4300 let community = crate::db::community::load_community(&community.id).unwrap().unwrap();
4301
4302 let reached = catch_up_channel_rekeys(&relay, &community, &channel_id).await.unwrap();
4303 assert_eq!(reached, 1, "converged in place at the same channel epoch");
4304 assert_eq!(crate::db::community::held_epoch_key(&cid, &chan_hex, 1).unwrap(), Some(key_lo),
4305 "adopted the lowest delivered key regardless of relay order");
4306
4307 let after = crate::db::community::load_community(&community.id).unwrap().unwrap();
4309 let _ = catch_up_channel_rekeys(&relay, &after, &channel_id).await.unwrap();
4310 assert_eq!(crate::db::community::held_epoch_key(&cid, &chan_hex, 1).unwrap(), Some(key_lo), "no flip-flop");
4311 }
4312
4313 #[tokio::test]
4314 async fn concurrent_channel_rekeyers_converge_when_i_authored_the_losing_fork() {
4315 let (_tmp, _guard) = init_test_db();
4320 let owner = Keys::generate();
4321 let me = Keys::generate(); become_local(&me);
4323 let community = saved_community_owned_by(&owner);
4324 let cid = community.id.to_hex();
4325 let channel_id = community.channels[0].id;
4326 let chan_hex = channel_id.to_hex();
4327 let scope = super::super::derive::RekeyScope::Channel(channel_id);
4328 let genesis_key = *community.channels[0].key.as_bytes();
4329 let root = *community.server_root_key.as_bytes();
4330 let commit0 = super::super::rekey::epoch_key_commitment(crate::community::Epoch(0), &genesis_key);
4331
4332 let role_id = "d".repeat(64);
4336 let roster = crate::community::roles::CommunityRoles {
4337 roles: vec![crate::community::roles::Role::admin(role_id.clone())],
4338 grants: vec![crate::community::roles::MemberGrant { member: me.public_key().to_hex(), role_ids: vec![role_id] }],
4339 };
4340 crate::db::community::set_community_roles(&cid, &roster, 1).unwrap();
4341
4342 let key_lo = [0x10u8; 32]; let key_hi = [0x99u8; 32]; let blob_lo = super::super::rekey::build_rekey_blob(owner.secret_key(), &me.public_key(), scope, crate::community::Epoch(1), &key_lo).unwrap();
4346 let ev_lo = super::super::rekey::build_channel_rekey_event(
4347 &Keys::generate(), &owner, &root, &channel_id, crate::community::Epoch(1), crate::community::Epoch(0), &commit0, &[blob_lo]).unwrap();
4348 let blob_hi = super::super::rekey::build_rekey_blob(me.secret_key(), &me.public_key(), scope, crate::community::Epoch(1), &key_hi).unwrap();
4350 let ev_hi = super::super::rekey::build_channel_rekey_event(
4351 &Keys::generate(), &me, &root, &channel_id, crate::community::Epoch(1), crate::community::Epoch(0), &commit0, &[blob_hi]).unwrap();
4352
4353 let relay = MemoryRelay::new();
4354 relay.inject(&ev_hi, &community.relays);
4355 relay.inject(&ev_lo, &community.relays);
4356
4357 crate::db::community::advance_server_root_epoch(&cid, 1, &[0x42u8; 32]).unwrap();
4359 crate::db::community::advance_channel_epoch(&cid, &chan_hex, 1, &key_hi).unwrap();
4361 let community = crate::db::community::load_community(&community.id).unwrap().unwrap();
4362
4363 let _ = catch_up_channel_rekeys(&relay, &community, &channel_id).await.unwrap();
4364 assert_eq!(crate::db::community::held_epoch_key(&cid, &chan_hex, 1).unwrap(), Some(key_lo),
4365 "I authored the losing fork but must converge DOWN to the owner's lower key");
4366 }
4367
4368 #[tokio::test]
4369 async fn reorg_through_a_fork_heals_the_forked_past_epoch() {
4370 let (_tmp, _guard) = init_test_db();
4375 let owner = Keys::generate();
4376 let me = Keys::generate();
4377 become_local(&me);
4378 let community = saved_community_owned_by(&owner);
4379 let cid = community.id.to_hex();
4380 let channel_id = community.channels[0].id;
4381 let chan_hex = channel_id.to_hex();
4382 let scope = super::super::derive::RekeyScope::Channel(channel_id);
4383 let genesis_key = *community.channels[0].key.as_bytes();
4384 let root = *community.server_root_key.as_bytes();
4385 let commit0 = super::super::rekey::epoch_key_commitment(crate::community::Epoch(0), &genesis_key);
4386
4387 let key_lo1 = [0x10u8; 32]; let key_hi1 = [0x99u8; 32]; let key_e2 = [0x20u8; 32]; let blob_lo1 = super::super::rekey::build_rekey_blob(owner.secret_key(), &me.public_key(), scope, crate::community::Epoch(1), &key_lo1).unwrap();
4392 let blob_hi1 = super::super::rekey::build_rekey_blob(owner.secret_key(), &me.public_key(), scope, crate::community::Epoch(1), &key_hi1).unwrap();
4393 let ev_lo1 = super::super::rekey::build_channel_rekey_event(
4394 &Keys::generate(), &owner, &root, &channel_id, crate::community::Epoch(1), crate::community::Epoch(0), &commit0, &[blob_lo1]).unwrap();
4395 let ev_hi1 = super::super::rekey::build_channel_rekey_event(
4396 &Keys::generate(), &owner, &root, &channel_id, crate::community::Epoch(1), crate::community::Epoch(0), &commit0, &[blob_hi1]).unwrap();
4397 let commit1_win = super::super::rekey::epoch_key_commitment(crate::community::Epoch(1), &key_lo1);
4399 let blob_e2 = super::super::rekey::build_rekey_blob(owner.secret_key(), &me.public_key(), scope, crate::community::Epoch(2), &key_e2).unwrap();
4400 let ev_e2 = super::super::rekey::build_channel_rekey_event(
4401 &Keys::generate(), &owner, &root, &channel_id, crate::community::Epoch(2), crate::community::Epoch(1), &commit1_win, &[blob_e2]).unwrap();
4402
4403 let relay = MemoryRelay::new();
4404 relay.inject(&ev_lo1, &community.relays);
4405 relay.inject(&ev_hi1, &community.relays);
4406 relay.inject(&ev_e2, &community.relays);
4407
4408 crate::db::community::advance_server_root_epoch(&cid, 1, &[0x42u8; 32]).unwrap();
4410 crate::db::community::advance_channel_epoch(&cid, &chan_hex, 1, &key_hi1).unwrap();
4412 let community = crate::db::community::load_community(&community.id).unwrap().unwrap();
4413
4414 let reached = catch_up_channel_rekeys(&relay, &community, &channel_id).await.unwrap();
4415 assert_eq!(reached, 2, "reorged forward to the head epoch");
4416 assert_eq!(crate::db::community::held_epoch_key(&cid, &chan_hex, 2).unwrap(), Some(key_e2), "head epoch adopted");
4417 assert_eq!(crate::db::community::held_epoch_key(&cid, &chan_hex, 1).unwrap(), Some(key_lo1),
4418 "the FORKED past epoch re-converged to the lowest sibling (its messages become readable)");
4419 }
4420
4421 #[tokio::test]
4422 async fn window_heal_converges_an_already_reorged_past_fork() {
4423 let (_tmp, _guard) = init_test_db();
4428 let owner = Keys::generate();
4429 let me = Keys::generate();
4430 become_local(&me);
4431 let community = saved_community_owned_by(&owner);
4432 let cid = community.id.to_hex();
4433 let channel_id = community.channels[0].id;
4434 let chan_hex = channel_id.to_hex();
4435 let scope = super::super::derive::RekeyScope::Channel(channel_id);
4436 let genesis_key = *community.channels[0].key.as_bytes();
4437 let root = *community.server_root_key.as_bytes();
4438 let commit0 = super::super::rekey::epoch_key_commitment(crate::community::Epoch(0), &genesis_key);
4439
4440 let key_lo1 = [0x10u8; 32]; let key_hi1 = [0x99u8; 32]; let key_e2 = [0x20u8; 32]; let blob_lo1 = super::super::rekey::build_rekey_blob(owner.secret_key(), &me.public_key(), scope, crate::community::Epoch(1), &key_lo1).unwrap();
4444 let blob_hi1 = super::super::rekey::build_rekey_blob(owner.secret_key(), &me.public_key(), scope, crate::community::Epoch(1), &key_hi1).unwrap();
4445 let ev_lo1 = super::super::rekey::build_channel_rekey_event(
4446 &Keys::generate(), &owner, &root, &channel_id, crate::community::Epoch(1), crate::community::Epoch(0), &commit0, &[blob_lo1]).unwrap();
4447 let ev_hi1 = super::super::rekey::build_channel_rekey_event(
4448 &Keys::generate(), &owner, &root, &channel_id, crate::community::Epoch(1), crate::community::Epoch(0), &commit0, &[blob_hi1]).unwrap();
4449
4450 let relay = MemoryRelay::new();
4451 relay.inject(&ev_lo1, &community.relays);
4452 relay.inject(&ev_hi1, &community.relays);
4453 crate::db::community::advance_server_root_epoch(&cid, 1, &[0x42u8; 32]).unwrap();
4457 crate::db::community::advance_channel_epoch(&cid, &chan_hex, 1, &key_hi1).unwrap();
4459 crate::db::community::advance_channel_epoch(&cid, &chan_hex, 2, &key_e2).unwrap();
4460 let community = crate::db::community::load_community(&community.id).unwrap().unwrap();
4461
4462 let reached = catch_up_channel_rekeys(&relay, &community, &channel_id).await.unwrap();
4463 assert_eq!(reached, 2, "head unchanged (no new rekey to apply)");
4464 assert_eq!(crate::db::community::held_epoch_key(&cid, &chan_hex, 2).unwrap(), Some(key_e2), "head epoch untouched");
4465 assert_eq!(crate::db::community::held_epoch_key(&cid, &chan_hex, 1).unwrap(), Some(key_lo1),
4466 "the already-forked past epoch re-converged to the lowest sibling via the window heal (no in-sync reorg)");
4467 }
4468
4469 #[tokio::test]
4470 async fn channel_heal_cannot_converge_to_a_key_i_was_not_given() {
4471 let (_tmp, _guard) = init_test_db();
4478 let owner = Keys::generate();
4479 let me = Keys::generate();
4480 become_local(&me);
4481 let community = saved_community_owned_by(&owner);
4482 let cid = community.id.to_hex();
4483 let channel_id = community.channels[0].id;
4484 let chan_hex = channel_id.to_hex();
4485 let scope = super::super::derive::RekeyScope::Channel(channel_id);
4486 let genesis_key = *community.channels[0].key.as_bytes();
4487 let root = *community.server_root_key.as_bytes();
4488 let commit0 = super::super::rekey::epoch_key_commitment(crate::community::Epoch(0), &genesis_key);
4489
4490 let key_lo = [0x10u8; 32]; let key_hi = [0x99u8; 32]; let other = Keys::generate();
4494 let blob_lo = super::super::rekey::build_rekey_blob(owner.secret_key(), &other.public_key(), scope, crate::community::Epoch(1), &key_lo).unwrap();
4495 let ev_lo = super::super::rekey::build_channel_rekey_event(
4496 &Keys::generate(), &owner, &root, &channel_id, crate::community::Epoch(1), crate::community::Epoch(0), &commit0, &[blob_lo]).unwrap();
4497 let blob_hi = super::super::rekey::build_rekey_blob(owner.secret_key(), &me.public_key(), scope, crate::community::Epoch(1), &key_hi).unwrap();
4499 let ev_hi = super::super::rekey::build_channel_rekey_event(
4500 &Keys::generate(), &owner, &root, &channel_id, crate::community::Epoch(1), crate::community::Epoch(0), &commit0, &[blob_hi]).unwrap();
4501
4502 let relay = MemoryRelay::new();
4503 relay.inject(&ev_lo, &community.relays);
4504 relay.inject(&ev_hi, &community.relays);
4505 crate::db::community::advance_server_root_epoch(&cid, 1, &[0x42u8; 32]).unwrap();
4506 crate::db::community::advance_channel_epoch(&cid, &chan_hex, 1, &key_hi).unwrap();
4507 let community = crate::db::community::load_community(&community.id).unwrap().unwrap();
4508
4509 let _ = catch_up_channel_rekeys(&relay, &community, &channel_id).await.unwrap();
4510 assert_eq!(crate::db::community::held_epoch_key(&cid, &chan_hex, 1).unwrap(), Some(key_hi),
4512 "excluded from the winning rekey ⇒ cannot converge");
4513 }
4514
4515 #[tokio::test]
4516 async fn refounding_channel_rekey_is_sealed_under_the_prior_root() {
4517 let (_tmp, _guard) = init_test_db();
4522 let owner = Keys::generate();
4523 become_local(&owner); let community = saved_community_owned_by(&owner);
4525 let channel_id = community.channels[0].id;
4526 let prior_root = [0x11u8; 32]; let relay = MemoryRelay::new();
4529 rotate_channel(&relay, &community, &channel_id, &[owner.public_key()], &prior_root).await.unwrap();
4530
4531 let z = super::super::derive::rekey_pseudonym(&crate::community::ServerRootKey(prior_root), &channel_id, crate::community::Epoch(1)).to_hex();
4533 let q = Query { kinds: vec![event_kind::COMMUNITY_REKEY], z_tags: vec![z], ..Default::default() };
4534 let evs = relay.fetch(&q, &community.relays).await.unwrap();
4535 assert_eq!(evs.len(), 1, "channel rekey is addressed at the PRIOR-root pseudonym");
4536 assert!(super::super::rekey::open_rekey_event(&evs[0], &prior_root).is_ok(),
4538 "opens under the prior (shared) root every retained member still holds");
4539 assert!(super::super::rekey::open_rekey_event(&evs[0], community.server_root_key.as_bytes()).is_err(),
4540 "does NOT open under the current/new root (which a base-fork loser would have dropped)");
4541 }
4542
4543 #[tokio::test]
4544 async fn apply_channel_rekey_converges_past_a_divergent_prior_epoch() {
4545 let (_tmp, _guard) = init_test_db();
4550 let owner = Keys::generate();
4551 let me = Keys::generate();
4552 become_local(&me);
4553 let community = saved_community_owned_by(&owner);
4554 let cid = community.id.to_hex();
4555 let channel_id = community.channels[0].id;
4556 let chan_hex = channel_id.to_hex();
4557 let scope = super::super::derive::RekeyScope::Channel(channel_id);
4558 let root = *community.server_root_key.as_bytes();
4559
4560 let my_fork_key = [0xAAu8; 32];
4562 crate::db::community::advance_channel_epoch(&cid, &chan_hex, 1, &my_fork_key).unwrap();
4563
4564 let winner_epoch1 = [0xBBu8; 32];
4566 let new_key = [0x22u8; 32];
4567 let commit = super::super::rekey::epoch_key_commitment(crate::community::Epoch(1), &winner_epoch1);
4568 let blob = super::super::rekey::build_rekey_blob(owner.secret_key(), &me.public_key(), scope, crate::community::Epoch(2), &new_key).unwrap();
4569 let ev = super::super::rekey::build_channel_rekey_event(
4570 &Keys::generate(), &owner, &root, &channel_id, crate::community::Epoch(2), crate::community::Epoch(1), &commit, &[blob]).unwrap();
4571 let parsed = super::super::rekey::open_rekey_event(&ev, &root).unwrap();
4572
4573 let outcome = apply_channel_rekey(&community, &parsed).unwrap();
4574 assert!(matches!(outcome, RekeyOutcome::Applied { head_advanced: true }),
4575 "must converge forward past the divergent prior epoch, got {outcome:?}");
4576 assert_eq!(crate::db::community::held_epoch_key(&cid, &chan_hex, 2).unwrap(), Some(new_key),
4577 "adopted the winner's epoch-2 key");
4578 }
4579
4580 #[tokio::test]
4581 async fn catch_up_server_root_stops_when_removed_from_base() {
4582 let (_tmp, _guard) = init_test_db();
4585 let owner = Keys::generate();
4586 let me = Keys::generate();
4587 become_local(&me);
4588 let community = saved_community_owned_by(&owner);
4589 let scope = super::super::derive::RekeyScope::ServerRoot;
4590 let relay = MemoryRelay::new();
4591
4592 let root1 = [0x11u8; 32];
4594 let b1 = super::super::rekey::build_rekey_blob(owner.secret_key(), &me.public_key(), scope, crate::community::Epoch(1), &root1).unwrap();
4595 let e1 = super::super::rekey::build_server_root_rekey_event(
4596 &Keys::generate(), &owner, community.server_root_key.as_bytes(), &community.id,
4597 crate::community::Epoch(1), crate::community::Epoch(0),
4598 &super::super::rekey::epoch_key_commitment(crate::community::Epoch(0), community.server_root_key.as_bytes()), &[b1],
4599 ).unwrap();
4600 let other = Keys::generate();
4602 let b2 = super::super::rekey::build_rekey_blob(owner.secret_key(), &other.public_key(), scope, crate::community::Epoch(2), &[0x22u8; 32]).unwrap();
4603 let e2 = super::super::rekey::build_server_root_rekey_event(
4604 &Keys::generate(), &owner, &root1, &community.id,
4605 crate::community::Epoch(2), crate::community::Epoch(1),
4606 &super::super::rekey::epoch_key_commitment(crate::community::Epoch(1), &root1), &[b2],
4607 ).unwrap();
4608 relay.inject(&e1, &community.relays);
4609 relay.inject(&e2, &community.relays);
4610
4611 let reached = catch_up_server_root(&relay, &community).await.unwrap();
4612 assert_eq!(reached.epoch, 1, "stops at the last base epoch I was a recipient of");
4613 assert!(reached.removed, "excluded by an AUTHORIZED (owner) base rotation → flagged removed so the caller erases");
4614 let reloaded = crate::db::community::load_community(&community.id).unwrap().unwrap();
4615 assert_eq!(reloaded.server_root_epoch, crate::community::Epoch(1));
4616 }
4617
4618 #[tokio::test]
4619 async fn catch_up_is_a_noop_with_no_rotations() {
4620 let (_tmp, _guard) = init_test_db();
4621 let owner = Keys::generate();
4622 let me = Keys::generate();
4623 become_local(&me);
4624 let community = saved_community_owned_by(&owner);
4625 let relay = MemoryRelay::new(); let reached = catch_up_channel_rekeys(&relay, &community, &community.channels[0].id).await.unwrap();
4627 assert_eq!(reached, 0, "no rotations → stays at the held epoch");
4628 }
4629
4630 #[tokio::test]
4631 async fn catch_up_stops_when_removed_midway() {
4632 let (_tmp, _guard) = init_test_db();
4635 let owner = Keys::generate();
4636 let me = Keys::generate();
4637 become_local(&me);
4638 let community = saved_community_owned_by(&owner);
4639 let channel_id = community.channels[0].id;
4640 let chan = &community.channels[0];
4641 let scope = super::super::derive::RekeyScope::Channel(channel_id);
4642 let relay = MemoryRelay::new();
4643
4644 let k1 = [0x11u8; 32];
4646 let b1 = super::super::rekey::build_rekey_blob(owner.secret_key(), &me.public_key(), scope, crate::community::Epoch(1), &k1).unwrap();
4647 let e1 = super::super::rekey::build_channel_rekey_event(
4648 &Keys::generate(), &owner, community.server_root_key.as_bytes(), &channel_id,
4649 crate::community::Epoch(1), crate::community::Epoch(0),
4650 &super::super::rekey::epoch_key_commitment(crate::community::Epoch(0), chan.key.as_bytes()), &[b1],
4651 ).unwrap();
4652 let other = Keys::generate();
4654 let b2 = super::super::rekey::build_rekey_blob(owner.secret_key(), &other.public_key(), scope, crate::community::Epoch(2), &[0x22u8; 32]).unwrap();
4655 let e2 = super::super::rekey::build_channel_rekey_event(
4656 &Keys::generate(), &owner, community.server_root_key.as_bytes(), &channel_id,
4657 crate::community::Epoch(2), crate::community::Epoch(1),
4658 &super::super::rekey::epoch_key_commitment(crate::community::Epoch(1), &k1), &[b2],
4659 ).unwrap();
4660 relay.inject(&e1, &community.relays);
4661 relay.inject(&e2, &community.relays);
4662
4663 let reached = catch_up_channel_rekeys(&relay, &community, &channel_id).await.unwrap();
4664 assert_eq!(reached, 1, "stops at the last epoch I was a recipient of");
4665 let reloaded = crate::db::community::load_community(&community.id).unwrap().unwrap();
4666 assert_eq!(reloaded.channels[0].epoch, crate::community::Epoch(1));
4667 }
4668
4669 #[tokio::test]
4670 async fn rotate_channel_rejects_unauthorized() {
4671 let (_tmp, _guard) = init_test_db();
4672 let owner = Keys::generate();
4673 let rogue = Keys::generate();
4674 become_local(&rogue); let community = saved_community_owned_by(&owner);
4676 let relay = MemoryRelay::new();
4677 assert!(
4678 rotate_channel(&relay, &community, &community.channels[0].id, &[], community.server_root_key.as_bytes()).await.is_err(),
4679 "a non-authorized member cannot rotate"
4680 );
4681 let reloaded = crate::db::community::load_community(&community.id).unwrap().unwrap();
4683 assert_eq!(reloaded.channels[0].epoch, crate::community::Epoch(0));
4684 }
4685
4686 #[tokio::test]
4689 async fn rotate_server_root_publishes_recoverable_rekey_and_advances_base() {
4690 use crate::community::derive::{base_rekey_pseudonym, recipient_pseudonym};
4691 use crate::community::rekey::{open_rekey_blob, open_rekey_event, rekey_pairwise_secret};
4692 let (_tmp, _guard) = init_test_db();
4693 let owner = Keys::generate();
4694 become_local(&owner); let community = saved_community_owned_by(&owner);
4696 let genesis_root = *community.server_root_key.as_bytes();
4697 let member = Keys::generate();
4698 let relay = MemoryRelay::new();
4699
4700 let new_epoch = rotate_server_root(&relay, &community, &[member.public_key()]).await.expect("rotate base");
4701 assert_eq!(new_epoch, 1);
4702
4703 let reloaded = crate::db::community::load_community(&community.id).unwrap().unwrap();
4705 assert_eq!(reloaded.server_root_epoch, crate::community::Epoch(1));
4706 assert_ne!(reloaded.server_root_key.as_bytes(), &genesis_root, "base root is fresh-random, not the genesis");
4707
4708 let addr = base_rekey_pseudonym(&crate::community::ServerRootKey(genesis_root), &community.id, crate::community::Epoch(1)).to_hex();
4710 let found = relay
4711 .fetch(&Query { kinds: vec![event_kind::COMMUNITY_REKEY], z_tags: vec![addr], ..Default::default() }, &community.relays)
4712 .await
4713 .unwrap();
4714 assert_eq!(found.len(), 1, "base rekey addressable by its prior-root pseudonym");
4715 let parsed = open_rekey_event(&found[0], &genesis_root).unwrap();
4716 assert!(matches!(parsed.scope, crate::community::derive::RekeyScope::ServerRoot));
4717 assert_eq!(parsed.rotator, owner.public_key());
4718 assert_eq!(parsed.blobs.len(), 2, "member + me (multi-device)");
4719
4720 let secret = rekey_pairwise_secret(member.secret_key(), &parsed.rotator).unwrap();
4722 let loc = recipient_pseudonym(&secret, parsed.scope, parsed.new_epoch).to_hex();
4723 let mine = parsed.blobs.iter().find(|b| b.locator == loc).expect("member's blob present");
4724 let recovered = open_rekey_blob(member.secret_key(), &parsed.rotator, parsed.scope, parsed.new_epoch, mine).unwrap();
4725 assert_eq!(reloaded.server_root_key.as_bytes(), &recovered, "member's recovered root == owner's advanced base head");
4726 }
4727
4728 #[tokio::test]
4729 async fn rotate_server_root_failed_publish_leaves_base_unadvanced() {
4730 let (_tmp, _guard) = init_test_db();
4731 let owner = Keys::generate();
4732 become_local(&owner);
4733 let community = saved_community_owned_by(&owner);
4734 let member = Keys::generate();
4735 assert!(rotate_server_root(&FailingRelay, &community, &[member.public_key()]).await.is_err());
4736 let reloaded = crate::db::community::load_community(&community.id).unwrap().unwrap();
4737 assert_eq!(reloaded.server_root_epoch, crate::community::Epoch(0), "base head stays put on publish failure");
4738 }
4739
4740 #[tokio::test]
4741 async fn rotate_server_root_dedups_self_in_recipients() {
4742 use crate::community::rekey::open_rekey_event;
4744 let (_tmp, _guard) = init_test_db();
4745 let owner = Keys::generate();
4746 become_local(&owner);
4747 let community = saved_community_owned_by(&owner);
4748 let relay = MemoryRelay::new();
4749 rotate_server_root(&relay, &community, &[owner.public_key()]).await.unwrap();
4750 let addr = crate::community::derive::base_rekey_pseudonym(
4751 &crate::community::ServerRootKey(*community.server_root_key.as_bytes()), &community.id, crate::community::Epoch(1),
4752 )
4753 .to_hex();
4754 let found = relay
4755 .fetch(&Query { kinds: vec![event_kind::COMMUNITY_REKEY], z_tags: vec![addr], ..Default::default() }, &community.relays)
4756 .await
4757 .unwrap();
4758 let parsed = open_rekey_event(&found[0], community.server_root_key.as_bytes()).unwrap();
4759 assert_eq!(parsed.blobs.len(), 1, "self listed in recipients yields exactly one blob, not two");
4760 }
4761
4762 #[tokio::test]
4763 async fn rotate_server_root_rejects_unauthorized() {
4764 let (_tmp, _guard) = init_test_db();
4765 let owner = Keys::generate();
4766 let rogue = Keys::generate();
4767 become_local(&rogue); let community = saved_community_owned_by(&owner);
4769 let relay = MemoryRelay::new();
4770 assert!(rotate_server_root(&relay, &community, &[]).await.is_err(), "a non-BAN member cannot rotate the base");
4771 let reloaded = crate::db::community::load_community(&community.id).unwrap().unwrap();
4772 assert_eq!(reloaded.server_root_epoch, crate::community::Epoch(0));
4773 }
4774
4775 #[tokio::test]
4776 async fn rotate_server_root_reanchors_the_control_plane_to_the_new_epoch() {
4777 let (_tmp, _guard) = init_test_db();
4780 let relay = MemoryRelay::new();
4781 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
4784 let cid = community.id.to_hex();
4785 assert_eq!(crate::db::community::edition_head_entity_ids(&cid).unwrap().len(), 3);
4786
4787 let member = Keys::generate();
4788 assert_eq!(rotate_server_root(&relay, &community, &[member.public_key()]).await.unwrap(), 1);
4789 let reloaded = crate::db::community::load_community(&community.id).unwrap().unwrap();
4790 assert_eq!(reloaded.server_root_epoch, crate::community::Epoch(1), "base head advanced");
4791
4792 let z = crate::community::roster::control_pseudonym(&reloaded.server_root_key, &community.id, crate::community::Epoch(1));
4794 let evs = relay
4795 .fetch(&Query { kinds: vec![event_kind::COMMUNITY_CONTROL], z_tags: vec![z], ..Default::default() }, &community.relays)
4796 .await
4797 .unwrap();
4798 let inners: Vec<_> = evs
4799 .iter()
4800 .filter_map(|o| crate::community::roster::open_control_edition(o, &reloaded.server_root_key).ok())
4801 .collect();
4802 let folded = crate::community::roster::fold_roster(&inners, &community.id, &Default::default());
4803 assert!(!folded.roles.roles.is_empty(), "control plane re-anchored at the new epoch as part of the rotation");
4804 }
4805
4806 #[tokio::test]
4807 async fn admin_refounding_carries_heads_verbatim_preserving_owner_and_peer_roles() {
4808 use crate::community::roles::Permissions;
4812 let (_tmp, _guard) = init_test_db();
4813 let owner = crate::state::MY_SECRET_KEY.to_keys().unwrap();
4814 let owner_hex = owner.public_key().to_hex();
4815 let relay = MemoryRelay::new();
4816 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
4817 let cid = community.id.to_hex();
4818 let admin_role = crate::db::community::get_community_roles(&cid).unwrap().roles[0].role_id.clone();
4819
4820 let alice = Keys::generate();
4822 let bob = Keys::generate();
4823 set_member_grant(&relay, &community, &alice.public_key().to_hex(), vec![admin_role.clone()]).await.unwrap();
4824 set_member_grant(&relay, &community, &bob.public_key().to_hex(), vec![admin_role.clone()]).await.unwrap();
4825 let _ = fetch_and_apply_control(&relay, &community).await;
4826 let community = crate::db::community::load_community(&community.id).unwrap().unwrap();
4827
4828 let mut edited = community.clone();
4831 edited.name = "HQ renamed".into();
4832 republish_community_metadata(&relay, &edited).await.unwrap();
4833 let _ = fetch_and_apply_control(&relay, &community).await;
4834 let community = crate::db::community::load_community(&community.id).unwrap().unwrap();
4835 assert!(crate::db::community::get_edition_head(&cid, &cid).unwrap().unwrap().0 >= 2, "GroupRoot now above v1");
4836
4837 become_local(&alice);
4839 let new_epoch = rotate_server_root(&relay, &community, &[owner.public_key(), bob.public_key()]).await.unwrap();
4840 assert_eq!(new_epoch, 1);
4841 let community = crate::db::community::load_community(&community.id).unwrap().unwrap();
4842 assert_eq!(community.server_root_epoch, crate::community::Epoch(1));
4843
4844 let z = crate::community::roster::control_pseudonym(&community.server_root_key, &community.id, crate::community::Epoch(1));
4846 let evs = relay.fetch(&Query { kinds: vec![event_kind::COMMUNITY_CONTROL], z_tags: vec![z], ..Default::default() }, &community.relays).await.unwrap();
4847 let inners: Vec<_> = evs.iter().filter_map(|o| crate::community::roster::open_control_edition(o, &community.server_root_key).ok()).collect();
4848 let folded = crate::community::roster::fold_roster(&inners, &community.id, &Default::default());
4849 let authed = crate::community::roster::authorize_delegation(&folded, Some(&owner_hex));
4850 assert!(authed.is_authorized(&alice.public_key().to_hex(), Some(&owner_hex), Permissions::BAN), "alice (re-founder) still admin");
4851 assert!(authed.is_authorized(&bob.public_key().to_hex(), Some(&owner_hex), Permissions::BAN), "bob (peer admin) NOT demoted by alice's re-founding");
4852 let new_owner = folded.root_meta.as_ref().and_then(|m| m.owner_attestation.as_ref())
4853 .and_then(|j| Event::from_json(j).ok()).map(|e| e.pubkey.to_hex());
4854 assert_eq!(new_owner.as_deref(), Some(owner_hex.as_str()), "owner deed carried verbatim — ownership intact after an admin re-founding");
4855 assert_eq!(folded.root_meta.as_ref().map(|m| m.name.as_str()), Some("HQ renamed"),
4856 "the >v1 GroupRoot head carried verbatim (content preserved across the re-founding)");
4857 let mut per_entity: std::collections::HashMap<[u8; 32], usize> = std::collections::HashMap::new();
4859 for i in &inners {
4860 if let Ok(p) = crate::community::edition::parse_edition_inner(i) { *per_entity.entry(p.entity_id).or_default() += 1; }
4861 }
4862 assert!(per_entity.values().all(|&c| c == 1), "one edition per entity at the new epoch (compacted)");
4863 }
4864
4865 #[tokio::test]
4869 async fn admin_write_blocked_when_isolated() {
4870 let (_tmp, _guard) = init_test_db();
4871 let me = Keys::generate();
4872 become_local(&me);
4873 let community = saved_community_owned_by(&me);
4874 let cid = community.id.to_hex();
4875 crate::db::community::set_edition_head_with_id(&cid, &cid, 1, &[1u8; 32], &[1u8; 32]).unwrap();
4877 crate::db::community::set_read_cut_target_epoch(&cid, 1).unwrap();
4878 let err = reseal_base_to_observed(&FailingRelay, &community).await.unwrap_err();
4880 assert!(err.contains("offline") || err.contains("can't reach any relay"),
4881 "isolated admin write must fail closed, got: {err}");
4882 assert_eq!(crate::db::community::load_community(&community.id).unwrap().unwrap().server_root_epoch,
4884 crate::community::Epoch(0), "no rotation while isolated");
4885 }
4886
4887 #[tokio::test]
4890 async fn refounding_rotates_channel_keys_too() {
4891 let (_tmp, _guard) = init_test_db();
4892 let relay = MemoryRelay::new();
4893 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
4894 let channel_id = community.channels[0].id;
4895 assert_eq!(community.channels[0].epoch, crate::community::Epoch(0));
4896 assert_eq!(community.server_root_epoch, crate::community::Epoch(0));
4897
4898 run_read_cut(&relay, &community, true).await.unwrap();
4899
4900 let after = crate::db::community::load_community(&community.id).unwrap().unwrap();
4901 assert_eq!(after.server_root_epoch, crate::community::Epoch(1), "base rotated");
4902 let ch = after.channels.iter().find(|c| c.id == channel_id).unwrap();
4903 assert_eq!(ch.epoch, crate::community::Epoch(1), "channel key rotated too (O2)");
4904 assert_eq!(crate::db::community::channel_rekeyed_at_server_epoch(&community.id.to_hex(), &channel_id.to_hex()).unwrap(),
4905 1, "channel marked rekeyed for the new base epoch");
4906 assert!(!crate::db::community::get_read_cut_pending(&community.id.to_hex()).unwrap(),
4907 "a complete read-cut clears the pending flag");
4908 }
4909
4910 #[tokio::test]
4915 async fn read_cut_resumes_without_double_base_rotation_after_channel_failure() {
4916 struct ChannelRekeyFails {
4920 inner: MemoryRelay,
4921 rekeys: std::sync::atomic::AtomicUsize,
4922 fail_channel: std::sync::atomic::AtomicBool,
4923 }
4924 #[async_trait::async_trait]
4925 impl Transport for ChannelRekeyFails {
4926 async fn publish(&self, e: &Event, r: &[String]) -> Result<(), String> { self.inner.publish(e, r).await }
4927 async fn publish_durable(&self, e: &Event, r: &[String]) -> Result<(), String> {
4928 if e.kind.as_u16() == event_kind::COMMUNITY_REKEY {
4929 let n = self.rekeys.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
4930 if n >= 1 && self.fail_channel.load(std::sync::atomic::Ordering::Relaxed) {
4931 return Err("channel rekey relay down".into());
4932 }
4933 }
4934 self.inner.publish_durable(e, r).await
4935 }
4936 async fn fetch(&self, q: &Query, r: &[String]) -> Result<Vec<Event>, String> { self.inner.fetch(q, r).await }
4937 }
4938 let (_tmp, _guard) = init_test_db();
4939 let relay = ChannelRekeyFails {
4940 inner: MemoryRelay::new(),
4941 rekeys: std::sync::atomic::AtomicUsize::new(0),
4942 fail_channel: std::sync::atomic::AtomicBool::new(true),
4943 };
4944 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
4945 let channel_id = community.channels[0].id;
4946 let cid = community.id.to_hex();
4947 let ch_hex = channel_id.to_hex();
4948
4949 assert!(run_read_cut(&relay, &community, true).await.is_err(), "the channel failure surfaces an error");
4951 let mid = crate::db::community::load_community(&community.id).unwrap().unwrap();
4952 assert_eq!(mid.server_root_epoch, crate::community::Epoch(1), "base advanced exactly once");
4953 assert_eq!(mid.channels.iter().find(|c| c.id == channel_id).unwrap().epoch, crate::community::Epoch(0),
4954 "channel NOT rotated (its rekey failed)");
4955 assert!(crate::db::community::get_read_cut_pending(&cid).unwrap(), "cut left pending after the failure");
4956 assert_eq!(crate::db::community::get_read_cut_target_epoch(&cid).unwrap(), 1, "target recorded durably");
4957 assert_eq!(crate::db::community::channel_rekeyed_at_server_epoch(&cid, &ch_hex).unwrap(), 0,
4958 "channel not yet marked for this cut");
4959
4960 relay.fail_channel.store(false, std::sync::atomic::Ordering::Relaxed);
4962 retry_pending_read_cut(&relay, &mid).await.unwrap();
4963 let done = crate::db::community::load_community(&community.id).unwrap().unwrap();
4964 assert_eq!(done.server_root_epoch, crate::community::Epoch(1),
4965 "base NOT rotated again — resumed at the same epoch (no double base rotation)");
4966 assert_eq!(done.channels.iter().find(|c| c.id == channel_id).unwrap().epoch, crate::community::Epoch(1),
4967 "the un-rotated channel finished on resume");
4968 assert_eq!(crate::db::community::channel_rekeyed_at_server_epoch(&cid, &ch_hex).unwrap(), 1,
4969 "channel marked rekeyed for the cut epoch");
4970 assert!(!crate::db::community::get_read_cut_pending(&cid).unwrap(), "pending cleared after the resume completes");
4971 }
4972
4973 #[tokio::test]
4974 async fn rotate_server_root_aborts_when_the_snapshot_does_not_land() {
4975 struct ControlPublishFails { inner: MemoryRelay, fail: std::sync::atomic::AtomicBool }
4980 #[async_trait::async_trait]
4981 impl Transport for ControlPublishFails {
4982 async fn publish(&self, e: &Event, r: &[String]) -> Result<(), String> { self.inner.publish(e, r).await }
4983 async fn publish_durable(&self, e: &Event, r: &[String]) -> Result<(), String> {
4984 if self.fail.load(std::sync::atomic::Ordering::Relaxed) && e.kind.as_u16() == event_kind::COMMUNITY_CONTROL {
4985 return Err("control relay down".into());
4986 }
4987 self.inner.publish_durable(e, r).await
4988 }
4989 async fn fetch(&self, q: &Query, r: &[String]) -> Result<Vec<Event>, String> { self.inner.fetch(q, r).await }
4990 }
4991 let (_tmp, _guard) = init_test_db();
4992 let relay = ControlPublishFails { inner: MemoryRelay::new(), fail: std::sync::atomic::AtomicBool::new(false) };
4993 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
4995 relay.fail.store(true, std::sync::atomic::Ordering::Relaxed);
4996
4997 assert!(
4998 rotate_server_root(&relay, &community, &[]).await.is_err(),
4999 "a snapshot whose editions can't be re-published must abort the rotation"
5000 );
5001 let reloaded = crate::db::community::load_community(&community.id).unwrap().unwrap();
5002 assert_eq!(reloaded.server_root_epoch, crate::community::Epoch(0), "base head NOT advanced when the snapshot doesn't land");
5003 }
5004
5005 #[tokio::test]
5006 async fn acquire_before_commit_a_reanchor_fetch_miss_publishes_no_base_rekey() {
5007 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
5012 struct ReanchorFetchEmpty { inner: MemoryRelay, drop_control: AtomicBool, base_rekeys: AtomicUsize }
5013 #[async_trait::async_trait]
5014 impl Transport for ReanchorFetchEmpty {
5015 async fn publish(&self, e: &Event, r: &[String]) -> Result<(), String> { self.inner.publish(e, r).await }
5016 async fn publish_durable(&self, e: &Event, r: &[String]) -> Result<(), String> {
5017 if e.kind.as_u16() == event_kind::COMMUNITY_REKEY {
5018 self.base_rekeys.fetch_add(1, Ordering::Relaxed);
5019 }
5020 self.inner.publish_durable(e, r).await
5021 }
5022 async fn fetch(&self, q: &Query, r: &[String]) -> Result<Vec<Event>, String> {
5023 if self.drop_control.load(Ordering::Relaxed) && q.kinds.iter().any(|k| *k == event_kind::COMMUNITY_CONTROL) {
5024 return Ok(vec![]); }
5026 self.inner.fetch(q, r).await
5027 }
5028 }
5029 let (_tmp, _guard) = init_test_db();
5030 let relay = ReanchorFetchEmpty { inner: MemoryRelay::new(), drop_control: AtomicBool::new(false), base_rekeys: AtomicUsize::new(0) };
5031 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
5032 relay.drop_control.store(true, Ordering::Relaxed);
5033
5034 assert!(rotate_server_root(&relay, &community, &[]).await.is_err(),
5035 "a re-anchor fetch miss must abort the rotation");
5036 assert_eq!(relay.base_rekeys.load(Ordering::Relaxed), 0,
5037 "the base rekey must NOT be published when the pre-publish fetch gate trips (acquire-before-commit)");
5038 let reloaded = crate::db::community::load_community(&community.id).unwrap().unwrap();
5039 assert_eq!(reloaded.server_root_epoch, crate::community::Epoch(0), "base head NOT advanced");
5040 }
5041
5042 #[tokio::test]
5045 async fn reanchor_carries_role_and_grant_to_the_new_epoch_under_the_new_root() {
5046 let (_tmp, _guard) = init_test_db();
5047 let relay = MemoryRelay::new();
5048 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
5050 let cid = community.id.to_hex();
5051 let admin_role_id = crate::db::community::get_community_roles(&cid).unwrap().roles[0].role_id.clone();
5052 let member = Keys::generate();
5053 set_member_grant(&relay, &community, &member.public_key().to_hex(), vec![admin_role_id]).await.unwrap();
5055 let _ = fetch_and_apply_control(&relay, &community).await;
5056 let community = crate::db::community::load_community(&community.id).unwrap().unwrap();
5057
5058 let new_root = [0x99u8; 32];
5060 let snap = reanchor_control_plane(&relay, &community, &new_root, crate::community::Epoch(1)).await.unwrap();
5061 assert!(snap.iter().all(|e| e.published), "every snapshot edition published");
5062 assert_eq!(snap.len(), 4, "GroupRoot + channel + Admin role + grant compacted to v1");
5063
5064 let new_z = crate::community::roster::control_pseudonym(
5066 &crate::community::ServerRootKey(new_root), &community.id, crate::community::Epoch(1),
5067 );
5068 let after = relay
5069 .fetch(&Query { kinds: vec![event_kind::COMMUNITY_CONTROL], z_tags: vec![new_z], ..Default::default() }, &community.relays)
5070 .await
5071 .unwrap();
5072 let inners: Vec<_> = after
5073 .iter()
5074 .filter_map(|o| crate::community::roster::open_control_edition(o, &crate::community::ServerRootKey(new_root)).ok())
5075 .collect();
5076 let folded = crate::community::roster::fold_roster(&inners, &community.id, &Default::default());
5077 assert!(!folded.roles.roles.is_empty(), "Admin role reachable at the new epoch");
5078 assert!(
5079 folded.roles.grants.iter().any(|g| g.member == member.public_key().to_hex()),
5080 "grant carried to the new epoch under the new root"
5081 );
5082 }
5083
5084 #[tokio::test]
5085 async fn grant_after_a_rekey_survives_the_fold_at_the_new_epoch() {
5086 use crate::community::roles::Permissions;
5092 let (_tmp, _guard) = init_test_db();
5093 let relay = MemoryRelay::new();
5094 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
5095 let cid = community.id.to_hex();
5096 let owner = crate::state::MY_SECRET_KEY.to_keys().unwrap();
5097 let admin_role_id = crate::db::community::get_community_roles(&cid).unwrap().roles[0].role_id.clone();
5098
5099 rotate_server_root(&relay, &community, &[owner.public_key()]).await.expect("rotate base");
5101 let community = crate::db::community::load_community(&community.id).unwrap().unwrap();
5102 assert_eq!(community.server_root_epoch, crate::community::Epoch(1), "advanced to the new epoch");
5103
5104 let alice = "aa".repeat(32);
5106 set_member_grant(&relay, &community, &alice, vec![admin_role_id]).await.unwrap();
5107
5108 let roster = fetch_and_apply_roles(&relay, &community).await.unwrap();
5110 assert!(
5111 roster.has_permission(&alice, Permissions::BAN),
5112 "post-rekey grant survives — Alice is Admin at the new epoch (pre-fix: dropped, role unreachable)"
5113 );
5114 assert_eq!(roster.highest_position(&alice), Some(1));
5115 }
5116
5117 #[tokio::test]
5121 async fn demote_re_asserts_the_demoted_members_metadata_head() {
5122 let (_tmp, _guard) = init_test_db();
5123 let relay = MemoryRelay::new();
5124 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
5125 let cid = community.id.to_hex();
5126 let owner = crate::state::MY_SECRET_KEY.to_keys().unwrap();
5127 let admin_role = crate::db::community::get_community_roles(&cid).unwrap().roles[0].role_id.clone();
5128 let alice = Keys::generate();
5129 let alice_hex = alice.public_key().to_hex();
5130
5131 set_member_grant(&relay, &community, &alice_hex, vec![admin_role]).await.unwrap();
5132 become_local(&alice);
5134 let mut as_alice = crate::db::community::load_community(&community.id).unwrap().unwrap();
5135 as_alice.name = "Alice's HQ".into();
5136 republish_community_metadata(&relay, &as_alice).await.unwrap();
5137 let community = crate::db::community::load_community(&community.id).unwrap().unwrap();
5138 assert_eq!(
5139 fetch_control_folded(&relay, &community).await.unwrap().root_author.map(|a| a.to_hex()),
5140 Some(alice_hex.clone()), "alice heads the GroupRoot after her edit",
5141 );
5142
5143 become_local(&owner);
5145 let community = crate::db::community::load_community(&community.id).unwrap().unwrap();
5146 set_member_grant(&relay, &community, &alice_hex, vec![]).await.unwrap();
5147
5148 let community = crate::db::community::load_community(&community.id).unwrap().unwrap();
5149 let folded = fetch_control_folded(&relay, &community).await.unwrap();
5150 assert_eq!(folded.root_author.map(|a| a.to_hex()), Some(owner.public_key().to_hex()),
5151 "the demote re-asserted the GroupRoot under the owner");
5152 assert_eq!(folded.root_meta.as_ref().unwrap().name, "Alice's HQ",
5153 "the re-assert preserves the demoted member's content");
5154 }
5155
5156 #[tokio::test]
5159 async fn demote_skips_reassert_when_member_does_not_head() {
5160 let (_tmp, _guard) = init_test_db();
5161 let relay = MemoryRelay::new();
5162 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
5163 let cid = community.id.to_hex();
5164 let admin_role = crate::db::community::get_community_roles(&cid).unwrap().roles[0].role_id.clone();
5165 let alice = Keys::generate();
5166 let alice_hex = alice.public_key().to_hex();
5167
5168 set_member_grant(&relay, &community, &alice_hex, vec![admin_role]).await.unwrap();
5169 let mut c = crate::db::community::load_community(&community.id).unwrap().unwrap();
5171 c.name = "Owner's HQ".into();
5172 republish_community_metadata(&relay, &c).await.unwrap();
5173 let community = crate::db::community::load_community(&community.id).unwrap().unwrap();
5174 let before = fetch_control_folded(&relay, &community).await.unwrap().root_head.unwrap().version;
5175
5176 set_member_grant(&relay, &community, &alice_hex, vec![]).await.unwrap();
5177 let community = crate::db::community::load_community(&community.id).unwrap().unwrap();
5178 let after = fetch_control_folded(&relay, &community).await.unwrap().root_head.unwrap().version;
5179 assert_eq!(after, before, "no re-assert published — the demoted member didn't head the GroupRoot");
5180 }
5181
5182 #[tokio::test]
5183 async fn reanchor_carries_the_banlist_edition_to_the_new_epoch() {
5184 let (_tmp, _guard) = init_test_db();
5187 let relay = MemoryRelay::new();
5188 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
5189 let carol = "cc".repeat(32);
5190 publish_banlist(&relay, &community, &[carol.clone()]).await.unwrap();
5192 let _ = fetch_and_apply_control(&relay, &community).await;
5193 let community = crate::db::community::load_community(&community.id).unwrap().unwrap();
5194
5195 let new_root = [0x99u8; 32];
5197 let n = reanchor_control_plane(&relay, &community, &new_root, crate::community::Epoch(1)).await.unwrap();
5198 assert!(n.iter().all(|e| e.published), "every snapshot edition published");
5199 assert_eq!(n.len(), 4, "GroupRoot + channel + Admin role + banlist compacted to v1");
5200
5201 let new_z = crate::community::roster::control_pseudonym(
5203 &crate::community::ServerRootKey(new_root), &community.id, crate::community::Epoch(1),
5204 );
5205 let after = relay
5206 .fetch(&Query { kinds: vec![event_kind::COMMUNITY_CONTROL], z_tags: vec![new_z], ..Default::default() }, &community.relays)
5207 .await
5208 .unwrap();
5209 let inners: Vec<_> = after
5210 .iter()
5211 .filter_map(|o| crate::community::roster::open_control_edition(o, &crate::community::ServerRootKey(new_root)).ok())
5212 .collect();
5213 let folded = crate::community::roster::fold_roster(&inners, &community.id, &Default::default());
5214 assert_eq!(folded.banned, vec![carol], "banlist reachable at the new epoch under the new root");
5215 }
5216
5217 fn owner_base_rekey(
5222 owner: &Keys, community: &Community, recipient_pk: &nostr_sdk::PublicKey, new_epoch: u64, new_root: &[u8; 32],
5223 ) -> super::super::rekey::ParsedRekey {
5224 let prev = community.server_root_epoch.0;
5225 let blob = super::super::rekey::build_rekey_blob(
5226 owner.secret_key(), recipient_pk, super::super::derive::RekeyScope::ServerRoot, crate::community::Epoch(new_epoch), new_root,
5227 )
5228 .unwrap();
5229 let commit = super::super::rekey::epoch_key_commitment(crate::community::Epoch(prev), community.server_root_key.as_bytes());
5230 let outer = super::super::rekey::build_server_root_rekey_event(
5231 &Keys::generate(), owner, community.server_root_key.as_bytes(), &community.id,
5232 crate::community::Epoch(new_epoch), crate::community::Epoch(prev), &commit, &[blob],
5233 )
5234 .unwrap();
5235 super::super::rekey::open_rekey_event(&outer, community.server_root_key.as_bytes()).unwrap()
5236 }
5237
5238 #[test]
5239 fn apply_server_root_rekey_recovers_new_root_and_advances_base() {
5240 let (_tmp, _guard) = init_test_db();
5241 let owner = Keys::generate();
5242 let me = Keys::generate();
5243 become_local(&me);
5244 let community = saved_community_owned_by(&owner);
5245 let cid = community.id.to_hex();
5246 let new_root = [0xCDu8; 32];
5247
5248 let parsed = owner_base_rekey(&owner, &community, &me.public_key(), 1, &new_root);
5249 assert_eq!(apply_server_root_rekey(&community, &parsed).unwrap(), RekeyOutcome::Applied { head_advanced: true });
5250
5251 let reloaded = crate::db::community::load_community(&community.id).unwrap().unwrap();
5252 assert_eq!(reloaded.server_root_epoch, crate::community::Epoch(1));
5253 assert_eq!(reloaded.server_root_key.as_bytes(), &new_root, "base head advanced to the new root");
5254 assert!(crate::db::community::held_epoch_key(&cid, crate::community::SERVER_ROOT_SCOPE_HEX, 0).unwrap().is_some());
5256 assert_eq!(crate::db::community::held_epoch_key(&cid, crate::community::SERVER_ROOT_SCOPE_HEX, 1).unwrap(), Some(new_root));
5257 }
5258
5259 #[test]
5260 fn apply_server_root_rekey_not_a_recipient_leaves_base_unchanged() {
5261 let (_tmp, _guard) = init_test_db();
5262 let owner = Keys::generate();
5263 let me = Keys::generate();
5264 become_local(&me);
5265 let community = saved_community_owned_by(&owner);
5266 let other = Keys::generate(); let parsed = owner_base_rekey(&owner, &community, &other.public_key(), 1, &[0x11u8; 32]);
5268 assert_eq!(apply_server_root_rekey(&community, &parsed).unwrap(), RekeyOutcome::NotARecipient);
5269 let reloaded = crate::db::community::load_community(&community.id).unwrap().unwrap();
5270 assert_eq!(reloaded.server_root_epoch, crate::community::Epoch(0), "removed-from-base member's head unchanged");
5271 }
5272
5273 #[test]
5274 fn apply_server_root_rekey_rejects_rotator_without_ban() {
5275 let (_tmp, _guard) = init_test_db();
5276 let owner = Keys::generate();
5277 let me = Keys::generate();
5278 become_local(&me);
5279 let community = saved_community_owned_by(&owner);
5280 let rogue = Keys::generate();
5282 let parsed = owner_base_rekey(&rogue, &community, &me.public_key(), 1, &[0x22u8; 32]);
5283 assert!(apply_server_root_rekey(&community, &parsed).is_err(), "unauthorized base rotation rejected");
5284 }
5285
5286 #[test]
5287 fn apply_server_root_rekey_reorgs_onto_authorized_chain_despite_prior_mismatch() {
5288 let (_tmp, _guard) = init_test_db();
5293 let owner = Keys::generate();
5294 let me = Keys::generate();
5295 become_local(&me);
5296 let community = saved_community_owned_by(&owner);
5297 let blob = super::super::rekey::build_rekey_blob(
5298 owner.secret_key(), &me.public_key(), super::super::derive::RekeyScope::ServerRoot, crate::community::Epoch(1), &[0x33u8; 32],
5299 )
5300 .unwrap();
5301 let bad = super::super::rekey::epoch_key_commitment(crate::community::Epoch(0), &[0xFFu8; 32]);
5303 let outer = super::super::rekey::build_server_root_rekey_event(
5304 &Keys::generate(), &owner, community.server_root_key.as_bytes(), &community.id,
5305 crate::community::Epoch(1), crate::community::Epoch(0), &bad, &[blob],
5306 )
5307 .unwrap();
5308 let parsed = super::super::rekey::open_rekey_event(&outer, community.server_root_key.as_bytes()).unwrap();
5309 let outcome = apply_server_root_rekey(&community, &parsed);
5310 assert!(
5311 matches!(outcome, Ok(RekeyOutcome::Applied { .. })),
5312 "an authorized base chain must be adopted (reorg), not rejected as foreign; got {outcome:?}"
5313 );
5314 }
5315
5316 #[test]
5317 fn apply_server_root_rekey_catchup_archives_without_regressing_base_head() {
5318 let (_tmp, _guard) = init_test_db();
5321 let owner = Keys::generate();
5322 let me = Keys::generate();
5323 become_local(&me);
5324 let community = saved_community_owned_by(&owner);
5325 let cid = community.id.to_hex();
5326
5327 let r5 = [0x55u8; 32];
5328 let p5 = owner_base_rekey(&owner, &community, &me.public_key(), 5, &r5);
5329 assert_eq!(apply_server_root_rekey(&community, &p5).unwrap(), RekeyOutcome::Applied { head_advanced: true });
5330 let r3 = [0x33u8; 32];
5331 let p3 = owner_base_rekey(&owner, &community, &me.public_key(), 3, &r3);
5332 assert_eq!(apply_server_root_rekey(&community, &p3).unwrap(), RekeyOutcome::Applied { head_advanced: false });
5333
5334 assert_eq!(crate::db::community::held_epoch_key(&cid, crate::community::SERVER_ROOT_SCOPE_HEX, 3).unwrap(), Some(r3));
5335 let reloaded = crate::db::community::load_community(&community.id).unwrap().unwrap();
5336 assert_eq!(reloaded.server_root_epoch, crate::community::Epoch(5), "base head stayed at newest");
5337 assert_eq!(reloaded.server_root_key.as_bytes(), &r5);
5338 }
5339
5340 #[test]
5341 fn apply_server_root_rekey_authorizes_a_granted_ban_admin() {
5342 let (_tmp, _guard) = init_test_db();
5346 let owner = Keys::generate();
5347 let me = Keys::generate();
5348 become_local(&me);
5349 let community = saved_community_owned_by(&owner);
5350 let cid = community.id.to_hex();
5351
5352 let admin = Keys::generate();
5353 let role_id = "d".repeat(64);
5354 let roster = crate::community::roles::CommunityRoles {
5355 roles: vec![crate::community::roles::Role::admin(role_id.clone())],
5356 grants: vec![crate::community::roles::MemberGrant { member: admin.public_key().to_hex(), role_ids: vec![role_id] }],
5357 };
5358 crate::db::community::set_community_roles(&cid, &roster, 1).unwrap();
5359
5360 let parsed = owner_base_rekey(&admin, &community, &me.public_key(), 1, &[0x77u8; 32]);
5361 assert_eq!(
5362 apply_server_root_rekey(&community, &parsed).unwrap(),
5363 RekeyOutcome::Applied { head_advanced: true },
5364 "a BAN-granted admin (not the owner) can rotate the base"
5365 );
5366 }
5367
5368 #[test]
5369 fn apply_server_root_rekey_accepts_when_prior_root_not_held() {
5370 let (_tmp, _guard) = init_test_db();
5373 let owner = Keys::generate();
5374 let me = Keys::generate();
5375 become_local(&me);
5376 let community = saved_community_owned_by(&owner);
5377
5378 let new_root = [0x99u8; 32];
5379 let blob = super::super::rekey::build_rekey_blob(
5380 owner.secret_key(), &me.public_key(), super::super::derive::RekeyScope::ServerRoot, crate::community::Epoch(5), &new_root,
5381 )
5382 .unwrap();
5383 let commit = super::super::rekey::epoch_key_commitment(crate::community::Epoch(4), &[0xEEu8; 32]);
5385 let outer = super::super::rekey::build_server_root_rekey_event(
5386 &Keys::generate(), &owner, community.server_root_key.as_bytes(), &community.id,
5387 crate::community::Epoch(5), crate::community::Epoch(4), &commit, &[blob],
5388 )
5389 .unwrap();
5390 let parsed = super::super::rekey::open_rekey_event(&outer, community.server_root_key.as_bytes()).unwrap();
5391 assert_eq!(apply_server_root_rekey(&community, &parsed).unwrap(), RekeyOutcome::Applied { head_advanced: true });
5392 let reloaded = crate::db::community::load_community(&community.id).unwrap().unwrap();
5393 assert_eq!(reloaded.server_root_epoch, crate::community::Epoch(5));
5394 }
5395
5396 #[test]
5397 fn apply_server_root_rekey_rejects_channel_scope() {
5398 let (_tmp, _guard) = init_test_db();
5400 let owner = Keys::generate();
5401 let me = Keys::generate();
5402 become_local(&me);
5403 let community = saved_community_owned_by(&owner);
5404 let channel_parsed = owner_channel_rekey(&owner, &community, &me.public_key(), 1, &[0x44u8; 32]);
5405 assert!(apply_server_root_rekey(&community, &channel_parsed).is_err(), "channel scope rejected by base apply");
5406 }
5407
5408 #[test]
5409 fn apply_channel_rekey_not_a_recipient() {
5410 let (_tmp, _guard) = init_test_db();
5411 let owner = Keys::generate();
5412 let me = Keys::generate();
5413 become_local(&me);
5414 let community = saved_community_owned_by(&owner);
5415 let other = Keys::generate();
5417 let parsed = owner_channel_rekey(&owner, &community, &other.public_key(), 1, &[0x11u8; 32]);
5418 assert_eq!(apply_channel_rekey(&community, &parsed).unwrap(), RekeyOutcome::NotARecipient);
5419 let reloaded = crate::db::community::load_community(&community.id).unwrap().unwrap();
5421 assert_eq!(reloaded.channels[0].epoch, crate::community::Epoch(0));
5422 }
5423
5424 #[test]
5425 fn apply_channel_rekey_rejects_unauthorized_rotator() {
5426 let (_tmp, _guard) = init_test_db();
5427 let owner = Keys::generate();
5428 let me = Keys::generate();
5429 become_local(&me);
5430 let community = saved_community_owned_by(&owner);
5431 let rogue = Keys::generate();
5433 let parsed = owner_channel_rekey(&rogue, &community, &me.public_key(), 1, &[0x22u8; 32]);
5434 assert!(apply_channel_rekey(&community, &parsed).is_err(), "unauthorized rotation must be rejected");
5435 }
5436
5437 #[test]
5438 fn apply_channel_rekey_reorgs_onto_authorized_chain_despite_prior_mismatch() {
5439 let (_tmp, _guard) = init_test_db();
5446 let owner = Keys::generate();
5447 let me = Keys::generate();
5448 become_local(&me);
5449 let community = saved_community_owned_by(&owner);
5450 let chan = &community.channels[0];
5451 let scope = super::super::derive::RekeyScope::Channel(chan.id);
5452 let new_key = [0x33u8; 32];
5453 let blob = super::super::rekey::build_rekey_blob(owner.secret_key(), &me.public_key(), scope, crate::community::Epoch(1), &new_key).unwrap();
5454 let other_commit = super::super::rekey::epoch_key_commitment(crate::community::Epoch(0), &[0xFFu8; 32]);
5456 let outer = super::super::rekey::build_channel_rekey_event(
5457 &Keys::generate(), &owner, community.server_root_key.as_bytes(), &chan.id,
5458 crate::community::Epoch(1), crate::community::Epoch(0), &other_commit, &[blob],
5459 )
5460 .unwrap();
5461 let parsed = super::super::rekey::open_rekey_event(&outer, community.server_root_key.as_bytes()).unwrap();
5462 let outcome = apply_channel_rekey(&community, &parsed).unwrap();
5463 assert!(matches!(outcome, RekeyOutcome::Applied { .. }),
5464 "an authorized chain must be adopted (reorg), not rejected as foreign; got {outcome:?}");
5465 assert_eq!(crate::db::community::held_epoch_key(&community.id.to_hex(), &chan.id.to_hex(), 1).unwrap(), Some(new_key));
5466 }
5467
5468 #[test]
5469 fn apply_channel_rekey_catchup_archives_without_regressing_head() {
5470 let (_tmp, _guard) = init_test_db();
5471 let owner = Keys::generate();
5472 let me = Keys::generate();
5473 become_local(&me);
5474 let community = saved_community_owned_by(&owner);
5475 let cid = community.id.to_hex();
5476 let chan_hex = community.channels[0].id.to_hex();
5477
5478 let k5 = [0x55u8; 32];
5480 let p5 = owner_channel_rekey(&owner, &community, &me.public_key(), 5, &k5);
5481 assert_eq!(apply_channel_rekey(&community, &p5).unwrap(), RekeyOutcome::Applied { head_advanced: true });
5482 let k3 = [0x33u8; 32];
5484 let p3 = owner_channel_rekey(&owner, &community, &me.public_key(), 3, &k3);
5485 assert_eq!(apply_channel_rekey(&community, &p3).unwrap(), RekeyOutcome::Applied { head_advanced: false });
5486
5487 assert_eq!(crate::db::community::held_epoch_key(&cid, &chan_hex, 3).unwrap(), Some(k3), "old epoch archived");
5488 assert_eq!(crate::db::community::held_epoch_key(&cid, &chan_hex, 5).unwrap(), Some(k5));
5489 let reloaded = crate::db::community::load_community(&community.id).unwrap().unwrap();
5490 assert_eq!(reloaded.channels[0].epoch, crate::community::Epoch(5), "head stayed at the newest epoch");
5491 assert_eq!(reloaded.channels[0].key.as_bytes(), &k5);
5492 }
5493
5494 #[tokio::test]
5495 async fn create_community_persists_and_publishes_metadata() {
5496 use crate::community::transport::Query;
5497 use crate::stored_event::event_kind;
5498
5499 let (_tmp, _guard) = init_test_db();
5500 let relay = MemoryRelay::new();
5501 let community = create_community(&relay, "Vector HQ", "general", vec!["r1".into()])
5502 .await
5503 .expect("create");
5504
5505 assert_eq!(community.name, "Vector HQ");
5507 assert_eq!(community.channels.len(), 1);
5508 assert_eq!(community.channels[0].name, "general");
5509
5510 let loaded = crate::db::community::load_community(&community.id).unwrap().expect("persisted");
5512 assert_eq!(loaded.channels[0].name, "general");
5513 assert_eq!(loaded.server_root_key.as_bytes(), community.server_root_key.as_bytes());
5514
5515 let meta_events = relay
5518 .fetch(
5519 &Query { kinds: vec![event_kind::APPLICATION_SPECIFIC], ..Default::default() },
5520 &community.relays,
5521 )
5522 .await
5523 .unwrap();
5524 assert!(meta_events.is_empty(), "no legacy 30078 metadata events");
5525
5526 let z = crate::community::roster::control_pseudonym(&community.server_root_key, &community.id, crate::community::Epoch(0));
5529 let control = relay
5530 .fetch(
5531 &Query { kinds: vec![event_kind::COMMUNITY_CONTROL], z_tags: vec![z], ..Default::default() },
5532 &community.relays,
5533 )
5534 .await
5535 .unwrap();
5536 assert_eq!(control.len(), 3, "GroupRoot + ChannelMetadata + Admin role editions");
5537 let owner_pk = crate::state::my_public_key().unwrap();
5538 let parsed: Vec<_> = control
5539 .iter()
5540 .filter_map(|o| crate::community::roster::open_control_edition(o, &community.server_root_key).ok())
5541 .filter_map(|i| crate::community::edition::parse_edition_inner(&i).ok())
5542 .collect();
5543 assert!(parsed.iter().all(|p| p.author == owner_pk), "every genesis edition authored by the owner");
5544 let root = parsed.iter().find(|p| p.entity_id == community.id.0).expect("GroupRoot edition");
5546 let root_meta: crate::community::metadata::CommunityMetadata = serde_json::from_str(&root.content).unwrap();
5547 assert_eq!(root_meta.name, "Vector HQ");
5548 assert!(root_meta.owner_attestation.is_some());
5549 let role: crate::community::roles::Role = parsed
5551 .iter()
5552 .find_map(|p| serde_json::from_str::<crate::community::roles::Role>(&p.content).ok().filter(|r| r.name == "Admin"))
5553 .expect("Admin role edition");
5554 assert_eq!(role.position, 1);
5555 assert!(role.permissions.contains(crate::community::roles::Permissions::ADMIN_ALL));
5556
5557 let cached = crate::db::community::get_community_roles(&community.id.to_hex()).unwrap();
5559 assert_eq!(cached.roles.len(), 1);
5560 assert!(cached.grants.is_empty(), "owner is implicit position 0, takes no grant");
5561 }
5562
5563 #[tokio::test]
5564 async fn role_grant_round_trips_through_relays_and_revokes() {
5565 use crate::community::roles::Permissions;
5566 let (_tmp, _guard) = init_test_db();
5567 let relay = MemoryRelay::new();
5568 let community = create_community(&relay, "HQ", "general", vec!["r1".into()])
5569 .await
5570 .expect("create");
5571 let cid = community.id.to_hex();
5572 let alice = "aa".repeat(32);
5573 let admin_role_id = crate::db::community::get_community_roles(&cid).unwrap().roles[0]
5574 .role_id
5575 .clone();
5576
5577 set_member_grant(&relay, &community, &alice, vec![admin_role_id.clone()])
5579 .await
5580 .unwrap();
5581 assert!(
5582 crate::db::community::get_community_roles(&cid).unwrap().is_privileged(&alice),
5583 "local cache reflects the grant immediately"
5584 );
5585
5586 let roster = fetch_and_apply_roles(&relay, &community).await.unwrap();
5589 assert!(roster.has_permission(&alice, Permissions::BAN));
5590 assert!(roster.has_permission(&alice, Permissions::MANAGE_ROLES));
5591 assert_eq!(roster.roles.len(), 1);
5592 assert_eq!(roster.highest_position(&alice), Some(1));
5593
5594 set_member_grant(&relay, &community, &alice, vec![]).await.unwrap();
5596 let after = crate::db::community::get_community_roles(&cid).unwrap();
5597 assert!(!after.is_privileged(&alice), "revoked member holds no role");
5598 assert!(after.grants.is_empty(), "empty grant pruned");
5599 }
5600
5601 #[tokio::test]
5602 async fn admin_cannot_grant_a_peer_rank_role() {
5603 let (_tmp, _guard) = init_test_db();
5607 let relay = MemoryRelay::new();
5608 let community = create_community(&relay, "HQ", "general", vec!["r1".into()])
5609 .await
5610 .expect("create");
5611 let cid = community.id.to_hex();
5612 let admin_role_id =
5613 crate::db::community::get_community_roles(&cid).unwrap().roles[0].role_id.clone();
5614 let alice = Keys::generate();
5615 set_member_grant(&relay, &community, &alice.public_key().to_hex(), vec![admin_role_id.clone()])
5617 .await
5618 .unwrap();
5619
5620 crate::state::set_my_public_key(alice.public_key());
5622 let bob = Keys::generate().public_key();
5623 let err = grant_role(&relay, &community, bob, &admin_role_id).await.unwrap_err();
5624 assert!(err.contains("below your own"), "peer-rank grant refused, got: {err}");
5625 }
5626
5627 #[tokio::test]
5628 async fn create_community_mints_a_verifiable_owner_attestation() {
5629 let (_tmp, _guard) = init_test_db();
5632 let me = crate::state::my_public_key().unwrap();
5633 let relay = MemoryRelay::new();
5634 let community = create_community(&relay, "HQ", "general", vec!["r1".into()])
5635 .await
5636 .expect("create");
5637 let att = community.owner_attestation.as_ref().expect("attestation is mandatory");
5638 let proven = super::super::owner::verify_owner_attestation(att, &community.id.to_hex());
5639 assert_eq!(proven, Some(me), "the creator is the proven owner");
5640 assert_eq!(
5642 super::super::owner::verify_owner_attestation(att, &"f".repeat(64)),
5643 None,
5644 );
5645 }
5646
5647 #[tokio::test]
5648 async fn admin_cannot_ban_a_peer_admin() {
5649 let (_tmp, _guard) = init_test_db();
5653 let relay = MemoryRelay::new();
5654 let community = create_community(&relay, "HQ", "general", vec!["r1".into()])
5655 .await
5656 .expect("create");
5657 let cid = community.id.to_hex();
5658 let admin_role_id =
5659 crate::db::community::get_community_roles(&cid).unwrap().roles[0].role_id.clone();
5660 let alice = Keys::generate();
5661 let bob = Keys::generate();
5662 set_member_grant(&relay, &community, &alice.public_key().to_hex(), vec![admin_role_id.clone()])
5664 .await
5665 .unwrap();
5666 set_member_grant(&relay, &community, &bob.public_key().to_hex(), vec![admin_role_id.clone()])
5667 .await
5668 .unwrap();
5669
5670 become_local(&alice);
5673 let err = publish_banlist(&relay, &community, &[bob.public_key().to_hex()])
5674 .await
5675 .unwrap_err();
5676 assert!(err.contains("outranks you"), "peer-admin ban refused, got: {err}");
5677 }
5678
5679 #[tokio::test]
5680 async fn roster_reconstructs_purely_from_relay() {
5681 let (_tmp, _guard) = init_test_db();
5685 let relay = MemoryRelay::new();
5686 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
5687 let cid = community.id.to_hex();
5688 let admin_role_id =
5689 crate::db::community::get_community_roles(&cid).unwrap().roles[0].role_id.clone();
5690 let alice = "aa".repeat(32);
5691 set_member_grant(&relay, &community, &alice, vec![admin_role_id.clone()]).await.unwrap();
5692
5693 crate::db::community::set_community_roles(&cid, &crate::community::roles::CommunityRoles::default(), 0).unwrap();
5695 assert!(crate::db::community::get_community_roles(&cid).unwrap().roles.is_empty(), "cache wiped");
5696
5697 let roster = fetch_and_apply_roles(&relay, &community).await.unwrap();
5698 assert!(roster.is_admin(&alice), "roster reconstructed from relay editions, not the cache");
5699 assert_eq!(roster.roles.len(), 1, "the Admin role edition folded back");
5700 }
5701
5702 #[tokio::test]
5703 async fn admin_cannot_unban_a_peer_admin() {
5704 let (_tmp, _guard) = init_test_db();
5707 let relay = MemoryRelay::new();
5708 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
5709 let cid = community.id.to_hex();
5710 let admin_role_id =
5711 crate::db::community::get_community_roles(&cid).unwrap().roles[0].role_id.clone();
5712 let alice = Keys::generate();
5713 let bob = Keys::generate();
5714 set_member_grant(&relay, &community, &alice.public_key().to_hex(), vec![admin_role_id.clone()])
5715 .await
5716 .unwrap();
5717 set_member_grant(&relay, &community, &bob.public_key().to_hex(), vec![admin_role_id.clone()])
5718 .await
5719 .unwrap();
5720 crate::db::community::set_community_banlist(&cid, &[bob.public_key().to_hex()], 1000).unwrap();
5722
5723 become_local(&alice);
5725 let err = publish_banlist(&relay, &community, &[]).await.unwrap_err();
5726 assert!(err.contains("unban"), "unbanning a peer admin refused, got: {err}");
5727 }
5728
5729 #[tokio::test]
5730 async fn create_community_rejects_signer_identity_mismatch() {
5731 let (_tmp, _guard) = init_test_db(); let other = Keys::generate();
5736 crate::state::set_my_public_key(other.public_key()); let relay = MemoryRelay::new();
5738 let err = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap_err();
5739 assert!(err.contains("identity signer"), "signer mismatch refused, got: {err}");
5740 }
5741
5742 #[tokio::test]
5743 async fn banlist_newer_edition_applies_older_is_refused() {
5744 let (_tmp, _guard) = init_test_db();
5745 let relay = MemoryRelay::new();
5746 let community = create_community(&relay, "HQ", "general", vec!["r1".into()])
5747 .await
5748 .expect("create");
5749 let id_hex = community.id.to_hex();
5750 let banlist_entity = crate::simd::hex::bytes_to_hex_32(&crate::community::derive::banlist_locator(&community.id));
5751 let mallory = "aa".repeat(32);
5752 let bob = "bb".repeat(32);
5753
5754 let owner = crate::state::MY_SECRET_KEY.to_keys().unwrap();
5757 let inner = crate::community::roster::build_banlist_edition(&owner, &community.id, &[mallory.clone()], 1, None, 1000, None).unwrap();
5758 let outer = crate::community::roster::seal_control_edition(&Keys::generate(), &inner, &community.server_root_key, &community.id, crate::community::Epoch(0)).unwrap();
5759 relay.inject(&outer, &community.relays);
5760
5761 let applied = fetch_and_apply_banlist(&relay, &community).await.unwrap();
5763 assert_eq!(applied, vec![mallory.clone()]);
5764 let (head_v, _) = crate::db::community::get_edition_head(&id_hex, &banlist_entity).unwrap().unwrap();
5765 assert_eq!(head_v, 1, "banlist edition head advanced to v1");
5766
5767 crate::db::community::set_community_banlist(&id_hex, &[mallory.clone(), bob.clone()], 2).unwrap();
5770 crate::db::community::set_edition_head(&id_hex, &banlist_entity, 2, &[0x22u8; 32]).unwrap();
5771 let after = fetch_and_apply_banlist(&relay, &community).await.unwrap();
5772 assert_eq!(after, vec![mallory, bob], "older relay edition refused, local banlist preserved");
5773 }
5774
5775 #[tokio::test]
5776 async fn unauthorized_banlist_edition_is_rejected() {
5777 let (_tmp, _guard) = init_test_db();
5781 let relay = MemoryRelay::new();
5782 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
5783 let bob = "bb".repeat(32);
5784
5785 let mallory = Keys::generate();
5787 let inner = crate::community::roster::build_banlist_edition(&mallory, &community.id, &[bob], 1, None, 1000, None).unwrap();
5788 let outer = crate::community::roster::seal_control_edition(&Keys::generate(), &inner, &community.server_root_key, &community.id, crate::community::Epoch(0)).unwrap();
5789 relay.inject(&outer, &community.relays);
5790
5791 let applied = fetch_and_apply_banlist(&relay, &community).await.unwrap();
5793 assert!(applied.is_empty(), "an unauthorized signer's banlist edition is rejected");
5794 }
5795
5796 #[tokio::test]
5797 async fn banlist_receiver_enforces_per_target_outrank() {
5798 let (_tmp, _guard) = init_test_db();
5801 let relay = MemoryRelay::new();
5802 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
5803 let cid = community.id.to_hex();
5804 let admin_role_id = crate::db::community::get_community_roles(&cid).unwrap().roles[0].role_id.clone();
5805 let alice = Keys::generate();
5806 let bob = Keys::generate();
5807 set_member_grant(&relay, &community, &alice.public_key().to_hex(), vec![admin_role_id.clone()]).await.unwrap();
5809 set_member_grant(&relay, &community, &bob.public_key().to_hex(), vec![admin_role_id]).await.unwrap();
5810
5811 let cite = authority_citation(&community, &alice.public_key().to_hex());
5814 let inner = crate::community::roster::build_banlist_edition(&alice, &community.id, &[bob.public_key().to_hex()], 1, None, 1000, cite.as_ref()).unwrap();
5815 let outer = crate::community::roster::seal_control_edition(&Keys::generate(), &inner, &community.server_root_key, &community.id, crate::community::Epoch(0)).unwrap();
5816 relay.inject(&outer, &community.relays);
5817
5818 let applied = fetch_and_apply_banlist(&relay, &community).await.unwrap();
5820 assert!(applied.is_empty(), "an admin can't ban a peer admin (receiver-side outrank)");
5821 }
5822
5823 #[tokio::test]
5824 async fn banlist_admin_bans_regular_member_applies() {
5825 let (_tmp, _guard) = init_test_db();
5828 let relay = MemoryRelay::new();
5829 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
5830 let cid = community.id.to_hex();
5831 let admin_role_id = crate::db::community::get_community_roles(&cid).unwrap().roles[0].role_id.clone();
5832 let alice = Keys::generate();
5833 set_member_grant(&relay, &community, &alice.public_key().to_hex(), vec![admin_role_id]).await.unwrap();
5834
5835 let carol = "cc".repeat(32);
5836 let cite = authority_citation(&community, &alice.public_key().to_hex());
5838 let inner = crate::community::roster::build_banlist_edition(&alice, &community.id, &[carol.clone()], 1, None, 1000, cite.as_ref()).unwrap();
5839 let outer = crate::community::roster::seal_control_edition(&Keys::generate(), &inner, &community.server_root_key, &community.id, crate::community::Epoch(0)).unwrap();
5840 relay.inject(&outer, &community.relays);
5841
5842 let applied = fetch_and_apply_banlist(&relay, &community).await.unwrap();
5843 assert_eq!(applied, vec![carol], "an admin's ban of a regular member applies");
5844 }
5845
5846 #[tokio::test]
5847 async fn owner_banlist_needs_no_citation() {
5848 let (_tmp, _guard) = init_test_db();
5851 let relay = MemoryRelay::new();
5852 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
5853 let victim = "cc".repeat(32);
5854
5855 let owner = crate::state::MY_SECRET_KEY.to_keys().unwrap();
5857 let inner = crate::community::roster::build_banlist_edition(&owner, &community.id, &[victim.clone()], 1, None, 1000, None).unwrap();
5858 let outer = crate::community::roster::seal_control_edition(&Keys::generate(), &inner, &community.server_root_key, &community.id, crate::community::Epoch(0)).unwrap();
5859 relay.inject(&outer, &community.relays);
5860
5861 let applied = fetch_and_apply_banlist(&relay, &community).await.unwrap();
5862 assert_eq!(applied, vec![victim], "an owner's uncited ban applies");
5863 }
5864
5865 #[tokio::test]
5866 async fn banlist_with_forged_citation_hash_is_rejected() {
5867 let (_tmp, _guard) = init_test_db();
5870 let relay = MemoryRelay::new();
5871 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
5872 let cid = community.id.to_hex();
5873 let admin_role_id = crate::db::community::get_community_roles(&cid).unwrap().roles[0].role_id.clone();
5874 let alice = Keys::generate();
5875 set_member_grant(&relay, &community, &alice.public_key().to_hex(), vec![admin_role_id]).await.unwrap();
5876
5877 let carol = "cc".repeat(32);
5878 let mut cite = authority_citation(&community, &alice.public_key().to_hex()).unwrap();
5880 cite.edition_hash = [0xEE; 32];
5881 let inner = crate::community::roster::build_banlist_edition(&alice, &community.id, &[carol], 1, None, 1000, Some(&cite)).unwrap();
5882 let outer = crate::community::roster::seal_control_edition(&Keys::generate(), &inner, &community.server_root_key, &community.id, crate::community::Epoch(0)).unwrap();
5883 relay.inject(&outer, &community.relays);
5884
5885 let applied = fetch_and_apply_banlist(&relay, &community).await.unwrap();
5886 assert!(applied.is_empty(), "a forged-hash citation is rejected");
5887 }
5888
5889 #[tokio::test]
5890 async fn banlist_citing_unsynced_future_version_is_rejected() {
5891 let (_tmp, _guard) = init_test_db();
5895 let relay = MemoryRelay::new();
5896 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
5897 let cid = community.id.to_hex();
5898 let admin_role_id = crate::db::community::get_community_roles(&cid).unwrap().roles[0].role_id.clone();
5899 let alice = Keys::generate();
5900 set_member_grant(&relay, &community, &alice.public_key().to_hex(), vec![admin_role_id]).await.unwrap();
5901
5902 let carol = "cc".repeat(32);
5903 let mut cite = authority_citation(&community, &alice.public_key().to_hex()).unwrap();
5904 cite.version += 5; let inner = crate::community::roster::build_banlist_edition(&alice, &community.id, &[carol], 1, None, 1000, Some(&cite)).unwrap();
5906 let outer = crate::community::roster::seal_control_edition(&Keys::generate(), &inner, &community.server_root_key, &community.id, crate::community::Epoch(0)).unwrap();
5907 relay.inject(&outer, &community.relays);
5908
5909 let applied = fetch_and_apply_banlist(&relay, &community).await.unwrap();
5910 assert!(applied.is_empty(), "citing an unsynced future grant version fails closed");
5911 }
5912
5913 #[tokio::test]
5914 async fn demoted_banner_superseded_ban_is_rejected() {
5915 let (_tmp, _guard) = init_test_db();
5919 let relay = MemoryRelay::new();
5920 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
5921 let cid = community.id.to_hex();
5922 let admin_role_id = crate::db::community::get_community_roles(&cid).unwrap().roles[0].role_id.clone();
5923 let alice = Keys::generate();
5924 set_member_grant(&relay, &community, &alice.public_key().to_hex(), vec![admin_role_id]).await.unwrap();
5925
5926 let carol = "cc".repeat(32);
5927 let cite = authority_citation(&community, &alice.public_key().to_hex());
5929 let inner = crate::community::roster::build_banlist_edition(&alice, &community.id, &[carol], 1, None, 1000, cite.as_ref()).unwrap();
5930 let outer = crate::community::roster::seal_control_edition(&Keys::generate(), &inner, &community.server_root_key, &community.id, crate::community::Epoch(0)).unwrap();
5931 relay.inject(&outer, &community.relays);
5932
5933 set_member_grant(&relay, &community, &alice.public_key().to_hex(), vec![]).await.unwrap();
5935
5936 let applied = fetch_and_apply_banlist(&relay, &community).await.unwrap();
5937 assert!(applied.is_empty(), "a since-demoted banner's stale ban is rejected (refuse-superseded)");
5938 }
5939
5940 #[tokio::test]
5941 async fn withheld_revocation_cannot_resurrect_a_demoted_banners_grant() {
5942 let (_tmp, _guard) = init_test_db();
5948 let relay = MemoryRelay::new();
5949 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
5950 let cid = community.id.to_hex();
5951 let admin_role_id = crate::db::community::get_community_roles(&cid).unwrap().roles[0].role_id.clone();
5952 let alice = Keys::generate();
5953 set_member_grant(&relay, &community, &alice.public_key().to_hex(), vec![admin_role_id]).await.unwrap();
5954
5955 let carol = "cc".repeat(32);
5957 let cite = authority_citation(&community, &alice.public_key().to_hex());
5958 let inner = crate::community::roster::build_banlist_edition(&alice, &community.id, &[carol], 1, None, 1000, cite.as_ref()).unwrap();
5959 let outer = crate::community::roster::seal_control_edition(&Keys::generate(), &inner, &community.server_root_key, &community.id, crate::community::Epoch(0)).unwrap();
5960 relay.inject(&outer, &community.relays);
5961
5962 let alice_bytes = alice.public_key().to_bytes();
5965 let grant_entity = crate::simd::hex::bytes_to_hex_32(&crate::community::derive::grant_locator(&community.id, &alice_bytes));
5966 crate::db::community::set_edition_head(&cid, &grant_entity, 2, &[0xAB; 32]).unwrap();
5967
5968 let applied = fetch_and_apply_banlist(&relay, &community).await.unwrap();
5969 assert!(applied.is_empty(), "a withheld revocation can't roll the banner's grant back to re-authorize them");
5970 }
5971
5972 #[tokio::test]
5973 async fn invite_registry_round_trips_and_drives_is_public() {
5974 let (_tmp, _guard) = init_test_db();
5978 let relay = MemoryRelay::new();
5979 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
5980 assert!(!is_public(&community).unwrap(), "a fresh community is Private");
5981
5982 let owner = crate::state::MY_SECRET_KEY.to_keys().unwrap();
5985 let loc = "1a".repeat(32);
5986 let inner = crate::community::roster::build_invite_links_edition(&owner, &community.id, &[loc.clone()], 1, None, 1000, None).unwrap();
5987 let outer = crate::community::roster::seal_control_edition(&Keys::generate(), &inner, &community.server_root_key, &community.id, crate::community::Epoch(0)).unwrap();
5988 relay.inject(&outer, &community.relays);
5989
5990 let applied = fetch_and_apply_invite_links(&relay, &community).await.unwrap();
5991 assert_eq!(applied, vec![loc], "the owner's link edition folds + unions from the relay");
5992 assert!(is_public(&community).unwrap(), "mode recomputed Public from the folded aggregate");
5993
5994 publish_my_invite_links(&relay, &community, &[]).await.unwrap();
5996 let applied = fetch_and_apply_invite_links(&relay, &community).await.unwrap();
5997 assert!(applied.is_empty() && !is_public(&community).unwrap(), "an empty aggregate is Private");
5998 }
5999
6000 #[tokio::test]
6001 async fn metadata_edit_round_trips_to_a_lagging_member() {
6002 let (_tmp, _guard) = init_test_db();
6006 let relay = MemoryRelay::new();
6007 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6008 let cid = community.id.to_hex();
6009 let owner = crate::state::MY_SECRET_KEY.to_keys().unwrap();
6010 let (genesis_v, genesis_hash) = crate::db::community::get_edition_head(&cid, &cid).unwrap().unwrap();
6011 assert_eq!(genesis_v, 1);
6012
6013 let mut edited = crate::community::metadata::CommunityMetadata::of(&community);
6014 edited.name = "Renamed HQ".into();
6015 edited.description = Some("now with a topic".into());
6016 let inner = crate::community::roster::build_community_root_edition(&owner, &community.id, &edited, 2, Some(&genesis_hash), 4000, None).unwrap();
6017 let outer = crate::community::roster::seal_control_edition(&Keys::generate(), &inner, &community.server_root_key, &community.id, crate::community::Epoch(0)).unwrap();
6018 relay.inject(&outer, &community.relays);
6019
6020 fetch_and_apply_metadata(&relay, &community).await.unwrap();
6021 let after = crate::db::community::load_community(&community.id).unwrap().unwrap();
6022 assert_eq!(after.name, "Renamed HQ", "the owner's GroupRoot edit folded from the relay");
6023 assert_eq!(after.description.as_deref(), Some("now with a topic"));
6024 assert_eq!(crate::db::community::get_edition_head(&cid, &cid).unwrap().unwrap().0, 2, "head advanced to v2");
6025 }
6026
6027 #[tokio::test]
6028 async fn unauthorized_metadata_edit_is_ignored() {
6029 let (_tmp, _guard) = init_test_db();
6032 let relay = MemoryRelay::new();
6033 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6034 let cid = community.id.to_hex();
6035 let (_, genesis_hash) = crate::db::community::get_edition_head(&cid, &cid).unwrap().unwrap();
6036
6037 let mallory = Keys::generate();
6038 let mut hacked = crate::community::metadata::CommunityMetadata::of(&community);
6039 hacked.name = "Pwned".into();
6040 let inner = crate::community::roster::build_community_root_edition(&mallory, &community.id, &hacked, 2, Some(&genesis_hash), 5000, None).unwrap();
6041 let outer = crate::community::roster::seal_control_edition(&Keys::generate(), &inner, &community.server_root_key, &community.id, crate::community::Epoch(0)).unwrap();
6042 relay.inject(&outer, &community.relays);
6043
6044 fetch_and_apply_metadata(&relay, &community).await.unwrap();
6045 let after = crate::db::community::load_community(&community.id).unwrap().unwrap();
6046 assert_eq!(after.name, "HQ", "a non-manage-metadata signer's GroupRoot edit is rejected");
6047 assert_eq!(crate::db::community::get_edition_head(&cid, &cid).unwrap().unwrap().0, 1, "an unauthorized edit never advances the head");
6048 }
6049
6050 #[tokio::test]
6051 async fn channel_rename_round_trips_from_owner_edition() {
6052 let (_tmp, _guard) = init_test_db();
6055 let relay = MemoryRelay::new();
6056 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6057 let cid = community.id.to_hex();
6058 let owner = crate::state::MY_SECRET_KEY.to_keys().unwrap();
6059 let channel = community.channels[0].clone();
6060 let ch_hex = channel.id.to_hex();
6061 let (_, genesis_ch_hash) = crate::db::community::get_edition_head(&cid, &ch_hex).unwrap().unwrap();
6062
6063 let meta = crate::community::metadata::ChannelMetadata { name: "announcements".into() };
6064 let inner = crate::community::roster::build_channel_metadata_edition(&owner, &channel.id, &meta, 2, Some(&genesis_ch_hash), 6000, None).unwrap();
6065 let outer = crate::community::roster::seal_control_edition(&Keys::generate(), &inner, &community.server_root_key, &community.id, crate::community::Epoch(0)).unwrap();
6066 relay.inject(&outer, &community.relays);
6067
6068 fetch_and_apply_metadata(&relay, &community).await.unwrap();
6069 let after = crate::db::community::load_community(&community.id).unwrap().unwrap();
6070 assert_eq!(after.channels[0].name, "announcements", "the owner's channel rename folded + applied");
6071 assert_eq!(crate::db::community::get_edition_head(&cid, &ch_hex).unwrap().unwrap().0, 2, "channel head advanced to v2");
6072 }
6073
6074 fn root_fork_v2(author: &Keys, community: &Community, name: &str, created: u64, genesis_hash: &[u8; 32]) -> (Event, [u8; 32], [u8; 32]) {
6077 let mut meta = crate::community::metadata::CommunityMetadata::of(community);
6078 meta.name = name.into();
6079 let inner = crate::community::roster::build_community_root_edition(author, &community.id, &meta, 2, Some(genesis_hash), created, None).unwrap();
6080 let self_hash = crate::community::version::edition_hash(&community.id.0, 2, Some(genesis_hash), inner.content.as_bytes());
6081 let inner_id = inner.id.to_bytes();
6082 let outer = crate::community::roster::seal_control_edition(&Keys::generate(), &inner, &community.server_root_key, &community.id, crate::community::Epoch(0)).unwrap();
6083 (outer, self_hash, inner_id)
6084 }
6085
6086 fn channel_fork_v2(author: &Keys, community: &Community, channel_id: &crate::community::ChannelId, name: &str, created: u64, genesis_hash: &[u8; 32]) -> (Event, [u8; 32], [u8; 32]) {
6089 let meta = crate::community::metadata::ChannelMetadata { name: name.into() };
6090 let inner = crate::community::roster::build_channel_metadata_edition(author, channel_id, &meta, 2, Some(genesis_hash), created, None).unwrap();
6091 let self_hash = crate::community::version::edition_hash(&channel_id.0, 2, Some(genesis_hash), inner.content.as_bytes());
6092 let inner_id = inner.id.to_bytes();
6093 let outer = crate::community::roster::seal_control_edition(&Keys::generate(), &inner, &community.server_root_key, &community.id, crate::community::Epoch(0)).unwrap();
6094 (outer, self_hash, inner_id)
6095 }
6096
6097 #[tokio::test]
6101 async fn channel_same_version_fork_converges_to_the_lower_inner_id() {
6102 let (_tmp, _guard) = init_test_db();
6103 let relay = MemoryRelay::new();
6104 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6105 let cid = community.id.to_hex();
6106 let owner = crate::state::MY_SECRET_KEY.to_keys().unwrap();
6107 let channel_id = community.channels[0].id;
6108 let ch_hex = channel_id.to_hex();
6109 let (_, genesis_hash) = crate::db::community::get_edition_head(&cid, &ch_hex).unwrap().unwrap();
6110
6111 let (out_a, ha, ida) = channel_fork_v2(&owner, &community, &channel_id, "alpha", 1000, &genesis_hash);
6112 let (out_b, hb, idb) = channel_fork_v2(&owner, &community, &channel_id, "bravo", 2000, &genesis_hash);
6113 let (win_name, win_h, win_id, lose_name, lose_h, lose_id) = if ida < idb {
6114 ("alpha", ha, ida, "bravo", hb, idb)
6115 } else {
6116 ("bravo", hb, idb, "alpha", ha, ida)
6117 };
6118 crate::db::community::set_edition_head_with_id(&cid, &ch_hex, 2, &lose_h, &lose_id).unwrap();
6120 {
6121 let mut c = crate::db::community::load_community(&community.id).unwrap().unwrap();
6122 c.channels.iter_mut().find(|ch| ch.id == channel_id).unwrap().name = lose_name.into();
6123 crate::db::community::save_community(&c).unwrap();
6124 }
6125 relay.inject(&out_a, &community.relays);
6126 relay.inject(&out_b, &community.relays);
6127
6128 fetch_and_apply_metadata(&relay, &community).await.unwrap();
6129 let after = crate::db::community::load_community(&community.id).unwrap().unwrap();
6130 let ch_name = &after.channels.iter().find(|c| c.id == channel_id).unwrap().name;
6131 assert_eq!(ch_name, win_name, "channel converged on the lower-inner-id winner, not our held fork");
6132 assert_eq!(crate::db::community::get_edition_head(&cid, &ch_hex).unwrap().unwrap(), (2, win_h), "channel head self_hash converged at the SAME version");
6133 assert_eq!(crate::db::community::get_edition_head_inner_id(&cid, &ch_hex).unwrap(), Some(win_id), "channel head inner_id moved to the winner");
6134
6135 fetch_and_apply_metadata(&relay, &community).await.unwrap();
6137 let after2 = crate::db::community::load_community(&community.id).unwrap().unwrap();
6138 assert_eq!(&after2.channels.iter().find(|c| c.id == channel_id).unwrap().name, win_name, "no flip back to the higher-id fork");
6139 }
6140
6141 #[tokio::test]
6144 async fn channel_same_version_fork_excludes_an_unauthorized_lower_id_edition() {
6145 let (_tmp, _guard) = init_test_db();
6146 let relay = MemoryRelay::new();
6147 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6148 let cid = community.id.to_hex();
6149 let owner = crate::state::MY_SECRET_KEY.to_keys().unwrap();
6150 let channel_id = community.channels[0].id;
6151 let ch_hex = channel_id.to_hex();
6152 let (_, genesis_hash) = crate::db::community::get_edition_head(&cid, &ch_hex).unwrap().unwrap();
6153
6154 let (owner_out, owner_h, owner_id) = channel_fork_v2(&owner, &community, &channel_id, "legit", 1000, &genesis_hash);
6155 let mallory = Keys::generate();
6157 let mal_out = {
6158 let mut chosen = None;
6159 for t in 1..=10_000u64 {
6160 let cand = channel_fork_v2(&mallory, &community, &channel_id, "forged", t, &genesis_hash);
6161 if cand.2 < owner_id { chosen = Some(cand.0); break; }
6162 }
6163 chosen.expect("a mallory channel edition with a lower inner id")
6164 };
6165 relay.inject(&owner_out, &community.relays);
6166 relay.inject(&mal_out, &community.relays);
6167
6168 fetch_and_apply_metadata(&relay, &community).await.unwrap();
6169 let after = crate::db::community::load_community(&community.id).unwrap().unwrap();
6170 assert_eq!(&after.channels.iter().find(|c| c.id == channel_id).unwrap().name, &"legit".to_string(), "the channel forgery never wins despite a lower inner id");
6171 assert_eq!(crate::db::community::get_edition_head(&cid, &ch_hex).unwrap().unwrap(), (2, owner_h), "the authorized channel edition is the head");
6172 }
6173
6174 #[tokio::test]
6178 async fn epoch_primary_floor_lets_a_refounding_v1_supersede_a_held_high_version() {
6179 let (_tmp, _guard) = init_test_db();
6180 let relay = MemoryRelay::new();
6181 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6182 let cid = community.id.to_hex();
6183 for v in 2..=5u64 {
6185 crate::db::community::set_edition_head_with_id(&cid, &cid, v, &[v as u8; 32], &[v as u8; 32]).unwrap();
6186 }
6187 assert_eq!(crate::db::community::get_edition_head(&cid, &cid).unwrap().unwrap().0, 5);
6188 crate::db::community::set_edition_head_with_id(&cid, &cid, 3, &[0x33; 32], &[0x33; 32]).unwrap();
6190 assert_eq!(crate::db::community::get_edition_head(&cid, &cid).unwrap().unwrap().0, 5, "in-epoch downgrade refused");
6191
6192 crate::db::community::advance_server_root_epoch(&cid, 1, &[0xEE; 32]).unwrap();
6194 crate::db::community::set_edition_head_with_id(&cid, &cid, 1, &[0x01; 32], &[0x01; 32]).unwrap();
6195 let (v, h) = crate::db::community::get_edition_head(&cid, &cid).unwrap().unwrap();
6196 assert_eq!((v, h), (1, [0x01; 32]), "epoch-1 v1 supersedes epoch-0 v5 (epoch-primary)");
6197 assert_eq!(
6198 crate::db::community::get_all_edition_heads_epoched(&cid).unwrap().get(&cid).map(|(e, v, _)| (*e, *v)),
6199 Some((1, 1)),
6200 "head now recorded at epoch 1",
6201 );
6202 crate::db::community::set_edition_head_with_id(&cid, &cid, 1, &[0xAA; 32], &[0x02; 32]).unwrap();
6204 assert_eq!(crate::db::community::get_edition_head(&cid, &cid).unwrap().unwrap().1, [0x01; 32], "same-epoch same-version is not an advance");
6205 }
6206
6207 #[tokio::test]
6211 async fn same_version_fork_converges_to_the_lower_inner_id() {
6212 let (_tmp, _guard) = init_test_db();
6213 let relay = MemoryRelay::new();
6214 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6215 let cid = community.id.to_hex();
6216 let owner = crate::state::MY_SECRET_KEY.to_keys().unwrap();
6217 let (_, genesis_hash) = crate::db::community::get_edition_head(&cid, &cid).unwrap().unwrap();
6218
6219 let (out_a, ha, ida) = root_fork_v2(&owner, &community, "Alpha", 1000, &genesis_hash);
6220 let (out_b, hb, idb) = root_fork_v2(&owner, &community, "Bravo", 2000, &genesis_hash);
6221 let (win_name, win_h, win_id, lose_name, lose_h, lose_id) = if ida < idb {
6223 ("Alpha", ha, ida, "Bravo", hb, idb)
6224 } else {
6225 ("Bravo", hb, idb, "Alpha", ha, ida)
6226 };
6227 crate::db::community::set_edition_head_with_id(&cid, &cid, 2, &lose_h, &lose_id).unwrap();
6228 {
6229 let mut c = crate::db::community::load_community(&community.id).unwrap().unwrap();
6230 c.name = lose_name.into();
6231 crate::db::community::save_community(&c).unwrap();
6232 }
6233 relay.inject(&out_a, &community.relays);
6234 relay.inject(&out_b, &community.relays);
6235
6236 fetch_and_apply_metadata(&relay, &community).await.unwrap();
6237 let after = crate::db::community::load_community(&community.id).unwrap().unwrap();
6238 assert_eq!(after.name, win_name, "converged on the lower-inner-id winner, not our own held fork");
6239 assert_eq!(crate::db::community::get_edition_head(&cid, &cid).unwrap().unwrap(), (2, win_h), "head self_hash converged at the SAME version");
6240 assert_eq!(crate::db::community::get_edition_head_inner_id(&cid, &cid).unwrap(), Some(win_id), "head inner_id moved to the winner");
6241
6242 fetch_and_apply_metadata(&relay, &community).await.unwrap();
6244 assert_eq!(crate::db::community::load_community(&community.id).unwrap().unwrap().name, win_name, "no flip back to the higher-id fork");
6245 }
6246
6247 #[tokio::test]
6250 async fn converged_head_chains_the_next_edit_without_reforking() {
6251 let (_tmp, _guard) = init_test_db();
6252 let relay = MemoryRelay::new();
6253 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6254 let cid = community.id.to_hex();
6255 let owner = crate::state::MY_SECRET_KEY.to_keys().unwrap();
6256 let (_, genesis_hash) = crate::db::community::get_edition_head(&cid, &cid).unwrap().unwrap();
6257
6258 let (out_a, ha, ida) = root_fork_v2(&owner, &community, "Alpha", 1000, &genesis_hash);
6259 let (out_b, hb, idb) = root_fork_v2(&owner, &community, "Bravo", 2000, &genesis_hash);
6260 let (lose_h, lose_id) = if ida < idb { (hb, idb) } else { (ha, ida) };
6261 crate::db::community::set_edition_head_with_id(&cid, &cid, 2, &lose_h, &lose_id).unwrap();
6262 relay.inject(&out_a, &community.relays);
6263 relay.inject(&out_b, &community.relays);
6264 fetch_and_apply_metadata(&relay, &community).await.unwrap();
6265 assert_eq!(crate::db::community::get_edition_head(&cid, &cid).unwrap().unwrap().0, 2, "converged at v2");
6266
6267 let mut c = crate::db::community::load_community(&community.id).unwrap().unwrap();
6268 c.name = "Third".into();
6269 republish_community_metadata(&relay, &c).await.unwrap();
6270 assert_eq!(crate::db::community::get_edition_head(&cid, &cid).unwrap().unwrap().0, 3, "advanced to v3 off the converged head");
6271
6272 let empty: std::collections::HashMap<String, (u64, [u8; 32])> = std::collections::HashMap::new();
6273 let folded = crate::community::roster::fold_roster(&fetch_control_inners(&relay, &community).await, &community.id, &empty);
6274 assert_eq!(folded.root_head.as_ref().map(|h| h.version), Some(3), "a fresh fold reaches v3");
6275 assert!(!folded.gapped_entities.contains(&community.id.0), "the chain is contiguous genesis -> winner -> v3");
6276 }
6277
6278 #[tokio::test]
6281 async fn same_version_fork_excludes_an_unauthorized_lower_id_edition() {
6282 let (_tmp, _guard) = init_test_db();
6283 let relay = MemoryRelay::new();
6284 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6285 let cid = community.id.to_hex();
6286 let owner = crate::state::MY_SECRET_KEY.to_keys().unwrap();
6287 let (_, genesis_hash) = crate::db::community::get_edition_head(&cid, &cid).unwrap().unwrap();
6288
6289 let (owner_out, owner_h, owner_id) = root_fork_v2(&owner, &community, "Legit", 1000, &genesis_hash);
6290 let mallory = Keys::generate();
6292 let (mal_out, mal_id) = {
6293 let mut chosen = None;
6294 for t in 1..=10_000u64 {
6295 let cand = root_fork_v2(&mallory, &community, "Forged", t, &genesis_hash);
6296 if cand.2 < owner_id { chosen = Some((cand.0, cand.2)); break; }
6297 }
6298 chosen.expect("a mallory edition with a lower inner id")
6299 };
6300 assert!(mal_id < owner_id, "premise: the forgery sorts first author-blind");
6301 relay.inject(&owner_out, &community.relays);
6302 relay.inject(&mal_out, &community.relays);
6303
6304 fetch_and_apply_metadata(&relay, &community).await.unwrap();
6306 let after = crate::db::community::load_community(&community.id).unwrap().unwrap();
6307 assert_eq!(after.name, "Legit", "the forgery never wins despite a lower inner id");
6308 assert_eq!(crate::db::community::get_edition_head(&cid, &cid).unwrap().unwrap(), (2, owner_h), "the authorized edition is the head");
6309 }
6310
6311 #[tokio::test]
6315 async fn same_version_fork_on_an_authority_record_fails_closed() {
6316 let (_tmp, _guard) = init_test_db();
6317 let relay = MemoryRelay::new();
6318 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6319 let cid = community.id.to_hex();
6320 let owner = crate::state::MY_SECRET_KEY.to_keys().unwrap();
6321 let bl_eid = crate::community::derive::banlist_locator(&community.id);
6322 let bl_hex = crate::simd::hex::bytes_to_hex_32(&bl_eid);
6323
6324 let prev = [0x99u8; 32]; let build_ban = |list: &[String], created: u64| {
6326 let inner = crate::community::roster::build_banlist_edition(&owner, &community.id, list, 2, Some(&prev), created, None).unwrap();
6327 let self_hash = crate::community::version::edition_hash(&bl_eid, 2, Some(&prev), inner.content.as_bytes());
6328 let outer = crate::community::roster::seal_control_edition(&Keys::generate(), &inner, &community.server_root_key, &community.id, crate::community::Epoch(0)).unwrap();
6329 (outer, self_hash, inner.id.to_bytes())
6330 };
6331 let (out_a, ha, ida) = build_ban(&["aa".repeat(32)], 1000);
6332 let (out_b, hb, idb) = build_ban(&["bb".repeat(32)], 2000);
6333 let (lose_h, lose_id) = if ida < idb { (hb, idb) } else { (ha, ida) };
6336 crate::db::community::set_edition_head_with_id(&cid, &bl_hex, 2, &lose_h, &lose_id).unwrap();
6337 relay.inject(&out_a, &community.relays);
6338 relay.inject(&out_b, &community.relays);
6339
6340 let floors = crate::db::community::get_all_edition_heads(&cid).unwrap();
6341 let folded = crate::community::roster::fold_roster(&fetch_control_inners(&relay, &community).await, &community.id, &floors);
6342 assert!(folded.gapped_entities.contains(&bl_eid), "the authority-record fork is quarantined");
6343 assert!(folded.banlist_head.is_none() && folded.banlist_author.is_none(), "no banlist folded off the withheld view");
6344 }
6345
6346 #[tokio::test]
6347 async fn editions_sign_through_the_active_client_signer() {
6348 let (_tmp, _guard) = init_test_db();
6353 let owner = crate::state::MY_SECRET_KEY.to_keys().unwrap();
6354 crate::state::set_nostr_client(nostr_sdk::Client::builder().signer(owner.clone()).build());
6355
6356 let relay = MemoryRelay::new();
6357 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6358 let cid = community.id.to_hex();
6359
6360 publish_banlist(&relay, &community, &["dd".repeat(32)]).await.unwrap();
6362 let floors = crate::db::community::get_all_edition_heads(&cid).unwrap();
6363 let folded = crate::community::roster::fold_roster(
6364 &fetch_control_inners(&relay, &community).await, &community.id, &floors);
6365 assert_eq!(folded.banlist_author, Some(owner.public_key()), "banlist signed by the client signer");
6366 assert!(folded.root_author.is_some(), "genesis GroupRoot folded");
6367 let _ = crate::state::take_nostr_client();
6368 }
6369
6370 async fn fetch_control_inners(relay: &MemoryRelay, community: &Community) -> Vec<Event> {
6372 let z = crate::community::roster::control_pseudonym(&community.server_root_key, &community.id, crate::community::Epoch(0));
6373 let query = Query { kinds: vec![event_kind::COMMUNITY_CONTROL], z_tags: vec![z], ..Default::default() };
6374 let mut out = Vec::new();
6375 for ev in relay.fetch(&query, &community.relays).await.unwrap() {
6376 if let Ok(inner) = crate::community::roster::open_control_edition(&ev, &community.server_root_key) {
6377 out.push(inner);
6378 }
6379 }
6380 out
6381 }
6382
6383 fn simulate_bunker(owner: &Keys) {
6386 crate::state::set_nostr_client(nostr_sdk::Client::builder().signer(owner.clone()).build());
6387 crate::state::MY_SECRET_KEY.clear(&[]);
6388 assert!(crate::state::MY_SECRET_KEY.to_keys().is_none(), "bunker sim: no local key");
6389 }
6390
6391 #[tokio::test]
6392 async fn am_i_banned_detects_own_npub_in_banlist() {
6393 let (_tmp, _guard) = init_test_db();
6395 let relay = MemoryRelay::new();
6396 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6397 let me = crate::state::MY_SECRET_KEY.to_keys().unwrap().public_key().to_hex();
6398 let cid = community.id.to_hex();
6399 assert!(!am_i_banned(&community), "not banned on a fresh community");
6400 crate::db::community::set_community_banlist(&cid, &[me], 1).unwrap();
6402 assert!(am_i_banned(&community), "own npub in the banlist → banned → self-remove");
6403 crate::db::community::set_community_banlist(&cid, &[], 2).unwrap();
6404 assert!(!am_i_banned(&community), "cleared banlist → not banned");
6405 }
6406
6407 #[tokio::test]
6408 async fn bunker_owner_cannot_ban_in_private_community() {
6409 let (_tmp, _guard) = init_test_db();
6412 let owner = crate::state::MY_SECRET_KEY.to_keys().unwrap();
6413 let relay = MemoryRelay::new();
6414 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6415 simulate_bunker(&owner);
6416
6417 let victim = "cc".repeat(32);
6418 let err = publish_banlist(&relay, &community, &[victim]).await.unwrap_err();
6419 assert!(err.contains("private community") && err.contains("bunker"), "clear bunker explanation: {err}");
6420 assert!(
6421 crate::db::community::get_community_banlist(&community.id.to_hex()).unwrap().is_empty(),
6422 "the ban must NOT half-apply (nothing published or persisted)"
6423 );
6424 let _ = crate::state::take_nostr_client();
6425 }
6426
6427 #[tokio::test]
6428 async fn bunker_owner_can_ban_in_public_community() {
6429 let (_tmp, _guard) = init_test_db();
6432 let owner = crate::state::MY_SECRET_KEY.to_keys().unwrap();
6433 let relay = MemoryRelay::new();
6434 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6435 create_public_invite(&relay, &community, None, None).await.unwrap();
6436 let community = crate::db::community::load_community(&community.id).unwrap().unwrap();
6437 assert!(is_public(&community).unwrap(), "minting a link made it Public");
6438 simulate_bunker(&owner);
6439
6440 let victim = "cc".repeat(32);
6441 publish_banlist(&relay, &community, &[victim.clone()]).await.unwrap();
6442 assert_eq!(
6443 crate::db::community::get_community_banlist(&community.id.to_hex()).unwrap(),
6444 vec![victim],
6445 "a public ban from a bunker account succeeds (no rekey needed)"
6446 );
6447 let _ = crate::state::take_nostr_client();
6448 }
6449
6450 #[tokio::test]
6451 async fn bunker_owner_cannot_privatize() {
6452 let (_tmp, _guard) = init_test_db();
6455 let owner = crate::state::MY_SECRET_KEY.to_keys().unwrap();
6456 let relay = MemoryRelay::new();
6457 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6458 let (token, _) = create_public_invite(&relay, &community, None, None).await.unwrap();
6459 let community = crate::db::community::load_community(&community.id).unwrap().unwrap();
6460 simulate_bunker(&owner);
6461
6462 let err = revoke_public_invite(&relay, &community, &crate::simd::hex::hex_to_bytes_32(&token)).await.unwrap_err();
6463 assert!(err.contains("private") && err.contains("bunker"), "clear bunker explanation: {err}");
6464 let after = crate::db::community::load_community(&community.id).unwrap().unwrap();
6465 assert!(is_public(&after).unwrap(), "the revoke must NOT half-apply — community stays Public");
6466 let _ = crate::state::take_nostr_client();
6467 }
6468
6469 #[tokio::test]
6470 async fn non_owner_admin_can_edit_community_metadata() {
6471 let (_tmp, _guard) = init_test_db();
6475 let relay = MemoryRelay::new();
6476 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6477 let cid = community.id.to_hex();
6478 let (_, genesis_hash) = crate::db::community::get_edition_head(&cid, &cid).unwrap().unwrap();
6479
6480 let admin = Keys::generate();
6482 let admin_role_id = crate::db::community::get_community_roles(&cid).unwrap().roles[0].role_id.clone();
6483 set_member_grant(&relay, &community, &admin.public_key().to_hex(), vec![admin_role_id]).await.unwrap();
6484
6485 let mut edited = crate::community::metadata::CommunityMetadata::of(&community);
6487 edited.name = "Admin Renamed".into();
6488 let inner = crate::community::roster::build_community_root_edition(&admin, &community.id, &edited, 2, Some(&genesis_hash), 7000, None).unwrap();
6489 let outer = crate::community::roster::seal_control_edition(&Keys::generate(), &inner, &community.server_root_key, &community.id, crate::community::Epoch(0)).unwrap();
6490 relay.inject(&outer, &community.relays);
6491
6492 fetch_and_apply_metadata(&relay, &community).await.unwrap();
6493 let after = crate::db::community::load_community(&community.id).unwrap().unwrap();
6494 assert_eq!(after.name, "Admin Renamed", "a MANAGE_METADATA admin (not the owner) can edit metadata");
6495 }
6496
6497 #[tokio::test]
6498 async fn banning_an_admin_revokes_their_role() {
6499 let (_tmp, _guard) = init_test_db();
6503 let relay = MemoryRelay::new();
6504 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6505 let cid = community.id.to_hex();
6506 create_public_invite(&relay, &community, None, None).await.unwrap();
6507 let community = crate::db::community::load_community(&community.id).unwrap().unwrap();
6508
6509 let alice = Keys::generate();
6510 let alice_hex = alice.public_key().to_hex();
6511 let admin_role_id = crate::db::community::get_community_roles(&cid).unwrap().roles[0].role_id.clone();
6512 set_member_grant(&relay, &community, &alice_hex, vec![admin_role_id]).await.unwrap();
6513 let holds_role = |hex: &str| crate::db::community::get_community_roles(&cid).unwrap()
6514 .grants.iter().any(|g| g.member == hex && !g.role_ids.is_empty());
6515 assert!(holds_role(&alice_hex), "alice is admin pre-ban");
6516
6517 publish_banlist(&relay, &community, &[alice_hex.clone()]).await.unwrap();
6518 assert!(!holds_role(&alice_hex), "banning an admin revokes their role — no dangling grant");
6519 }
6520
6521 #[tokio::test]
6522 async fn kicking_an_admin_revokes_their_role() {
6523 let (_tmp, _guard) = init_test_db();
6526 let relay = MemoryRelay::new();
6527 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6528 let cid = community.id.to_hex();
6529 let alice = Keys::generate();
6530 let alice_hex = alice.public_key().to_hex();
6531 let admin_role_id = crate::db::community::get_community_roles(&cid).unwrap().roles[0].role_id.clone();
6532 set_member_grant(&relay, &community, &alice_hex, vec![admin_role_id]).await.unwrap();
6533 let holds_role = |hex: &str| crate::db::community::get_community_roles(&cid).unwrap()
6534 .grants.iter().any(|g| g.member == hex && !g.role_ids.is_empty());
6535 assert!(holds_role(&alice_hex), "alice is admin pre-kick");
6536
6537 publish_kick(&relay, &community, &community.channels[0], &alice_hex).await.unwrap();
6538 assert!(!holds_role(&alice_hex), "kicking an admin revokes their role");
6539 }
6540
6541 #[tokio::test]
6542 async fn republish_channel_metadata_renames_and_publishes() {
6543 let (_tmp, _guard) = init_test_db();
6546 let relay = MemoryRelay::new();
6547 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6548 let cid = community.id.to_hex();
6549 let channel = community.channels[0].clone();
6550 let ch_hex = channel.id.to_hex();
6551
6552 republish_channel_metadata(&relay, &community, &channel.id, "lobby").await.unwrap();
6553 let after = crate::db::community::load_community(&community.id).unwrap().unwrap();
6554 assert_eq!(after.channels[0].name, "lobby", "the producer renamed the channel locally");
6555 assert_eq!(crate::db::community::get_edition_head(&cid, &ch_hex).unwrap().unwrap().0, 2, "channel head advanced");
6556 }
6557
6558 #[tokio::test]
6559 async fn revoking_the_last_link_privatizes_and_rotates_the_base() {
6560 let (_tmp, _guard) = init_test_db();
6565 let relay = MemoryRelay::new();
6566 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6567 assert!(!is_public(&community).unwrap(), "a fresh community is Private");
6568 assert_eq!(community.server_root_epoch, crate::community::Epoch(0));
6569
6570 let (t1, _) = create_public_invite(&relay, &community, None, None).await.unwrap();
6572 let (t2, _) = create_public_invite(&relay, &community, None, None).await.unwrap();
6573 let c = crate::db::community::load_community(&community.id).unwrap().unwrap();
6574 assert!(is_public(&c).unwrap(), "minting a link flips the mode to Public");
6575 assert_eq!(c.server_root_epoch, crate::community::Epoch(0), "minting links does NOT rotate the base");
6576
6577 revoke_public_invite(&relay, &c, &crate::simd::hex::hex_to_bytes_32(&t1)).await.unwrap();
6579 let c = crate::db::community::load_community(&community.id).unwrap().unwrap();
6580 assert!(is_public(&c).unwrap(), "one link remains → still Public");
6581 assert_eq!(c.server_root_epoch, crate::community::Epoch(0), "revoking a non-last link does NOT rotate");
6582
6583 revoke_public_invite(&relay, &c, &crate::simd::hex::hex_to_bytes_32(&t2)).await.unwrap();
6585 let c = crate::db::community::load_community(&community.id).unwrap().unwrap();
6586 assert!(!is_public(&c).unwrap(), "revoking the last link flips to Private");
6587 assert_eq!(c.server_root_epoch, crate::community::Epoch(1), "privatize re-founded: the base key rotated");
6588
6589 revoke_public_invite(&relay, &c, &crate::simd::hex::hex_to_bytes_32(&t2)).await.unwrap();
6592 let c = crate::db::community::load_community(&community.id).unwrap().unwrap();
6593 assert_eq!(c.server_root_epoch, crate::community::Epoch(1), "a no-op re-revoke does not double-rotate");
6594 }
6595
6596 #[tokio::test]
6597 async fn private_ban_reseals_base_public_ban_does_not() {
6598 let (_tmp, _guard) = init_test_db();
6601 let relay = MemoryRelay::new();
6602 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6603 let victim = "cc".repeat(32);
6604
6605 assert!(!is_public(&community).unwrap(), "fresh community is Private");
6607 publish_banlist(&relay, &community, &[victim.clone()]).await.unwrap();
6608 let c = crate::db::community::load_community(&community.id).unwrap().unwrap();
6609 assert_eq!(c.server_root_epoch, crate::community::Epoch(1), "a private-community ban re-seals the base");
6610
6611 create_public_invite(&relay, &c, None, None).await.unwrap();
6613 let c = crate::db::community::load_community(&community.id).unwrap().unwrap();
6614 assert!(is_public(&c).unwrap(), "minted a link → Public");
6615 publish_banlist(&relay, &c, &[victim.clone(), "dd".repeat(32)]).await.unwrap();
6616 let c2 = crate::db::community::load_community(&community.id).unwrap().unwrap();
6617 assert_eq!(c2.server_root_epoch, crate::community::Epoch(1), "a public-community ban does NOT rotate the base");
6618 }
6619
6620 #[tokio::test]
6621 async fn private_ban_seals_the_banned_member_out_of_the_new_root() {
6622 use crate::community::derive::{base_rekey_pseudonym, recipient_pseudonym};
6628 use crate::community::rekey::{open_rekey_event, rekey_pairwise_secret};
6629 use crate::types::Message;
6630 use nostr_sdk::ToBech32;
6631 let (_tmp, _guard) = init_test_db();
6632 let relay = MemoryRelay::new();
6633 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6634 let cid = community.id.to_hex();
6635 let genesis_root = *community.server_root_key.as_bytes();
6636 let channel_hex = community.channels[0].id.to_hex();
6637
6638 let victim = Keys::generate();
6640 let victim_b32 = victim.public_key().to_bech32().unwrap();
6641 let mut m = Message::default();
6642 m.id = "aa".repeat(32);
6643 m.npub = Some(victim_b32.clone());
6644 m.at = 1000;
6645 crate::db::events::save_message(&channel_hex, &m).await.unwrap();
6646 assert!(
6647 crate::db::community::community_member_activity(&cid).unwrap().iter().any(|(np, _)| np == &victim_b32),
6648 "victim is observed before the ban"
6649 );
6650
6651 publish_banlist(&relay, &community, &[victim.public_key().to_hex()]).await.unwrap();
6653 let reloaded = crate::db::community::load_community(&community.id).unwrap().unwrap();
6654 assert_eq!(reloaded.server_root_epoch, crate::community::Epoch(1), "private ban re-seals the base");
6655 assert!(
6656 !crate::db::community::community_member_activity(&cid).unwrap().iter().any(|(np, _)| np == &victim_b32),
6657 "the banned victim is no longer observed (banlist hex → bech32 reconciliation worked)"
6658 );
6659
6660 let addr = base_rekey_pseudonym(&crate::community::ServerRootKey(genesis_root), &community.id, crate::community::Epoch(1)).to_hex();
6662 let found = relay
6663 .fetch(&Query { kinds: vec![event_kind::COMMUNITY_REKEY], z_tags: vec![addr], ..Default::default() }, &community.relays)
6664 .await
6665 .unwrap();
6666 assert_eq!(found.len(), 1, "the base rekey is published");
6667 let parsed = open_rekey_event(&found[0], &genesis_root).unwrap();
6668 let secret = rekey_pairwise_secret(victim.secret_key(), &parsed.rotator).unwrap();
6669 let loc = recipient_pseudonym(&secret, parsed.scope, parsed.new_epoch).to_hex();
6670 assert!(
6671 parsed.blobs.iter().all(|b| b.locator != loc),
6672 "the BANNED victim has NO blob — sealed OUT of the new root (read access is actually cut)"
6673 );
6674 }
6675
6676 struct SwapDuringPublishRelay {
6680 inner: MemoryRelay,
6681 }
6682 #[async_trait::async_trait]
6683 impl Transport for SwapDuringPublishRelay {
6684 async fn publish(&self, event: &Event, relays: &[String]) -> Result<(), String> {
6685 crate::state::bump_session_generation();
6686 self.inner.publish(event, relays).await
6687 }
6688 async fn publish_durable(&self, event: &Event, relays: &[String]) -> Result<(), String> {
6689 crate::state::bump_session_generation();
6690 self.inner.publish_durable(event, relays).await
6691 }
6692 async fn fetch(&self, query: &Query, relays: &[String]) -> Result<Vec<Event>, String> {
6693 self.inner.fetch(query, relays).await
6694 }
6695 }
6696
6697 #[tokio::test]
6701 async fn account_swap_during_grant_publish_skips_the_local_persist() {
6702 let (_tmp, _guard) = init_test_db();
6703 let setup = MemoryRelay::new();
6704 let community = create_community(&setup, "HQ", "general", vec!["r1".into()]).await.unwrap();
6705 let cid = community.id.to_hex();
6706 let member = "cc".repeat(32);
6707 let entity_hex = crate::simd::hex::bytes_to_hex_32(
6708 &crate::community::derive::grant_locator(&community.id, &crate::simd::hex::hex_to_bytes_32(&member)));
6709 assert!(crate::db::community::get_edition_head(&cid, &entity_hex).unwrap().is_none(), "no grant head yet");
6710
6711 let swap = SwapDuringPublishRelay { inner: MemoryRelay::new() };
6712 set_member_grant(&swap, &community, &member, vec!["a".repeat(64)]).await.unwrap();
6713
6714 assert!(
6715 crate::db::community::get_edition_head(&cid, &entity_hex).unwrap().is_none(),
6716 "session straddled a swap → persist skipped → no local grant head (account B uncorrupted)"
6717 );
6718 }
6719
6720 #[tokio::test]
6724 async fn account_swap_during_ban_publish_applies_nothing_locally() {
6725 let (_tmp, _guard) = init_test_db();
6726 let setup = MemoryRelay::new();
6727 let community = create_community(&setup, "HQ", "general", vec!["r1".into()]).await.unwrap();
6728 let cid = community.id.to_hex();
6729 assert!(!is_public(&community).unwrap(), "fresh community is Private (a ban would normally re-seal)");
6730
6731 let swap = SwapDuringPublishRelay { inner: MemoryRelay::new() };
6732 publish_banlist(&swap, &community, &["cc".repeat(32)]).await.unwrap();
6733
6734 assert!(crate::db::community::get_community_banlist(&cid).unwrap().is_empty(),
6735 "banlist persist skipped on the stale session");
6736 assert_eq!(crate::db::community::load_community(&community.id).unwrap().unwrap().server_root_epoch,
6737 crate::community::Epoch(0), "no read-cut re-seal → base NOT rotated into the wrong account");
6738 assert!(!crate::db::community::get_read_cut_pending(&cid).unwrap(),
6739 "read_cut_pending untouched (need_cut requires is_valid())");
6740 }
6741
6742 #[tokio::test]
6745 async fn swap_session_clears_per_account_state_and_keys() {
6746 let (_tmp, _guard) = init_test_db();
6747 {
6748 let mut st = crate::state::STATE.lock().await;
6749 st.db_loaded = true;
6750 st.is_syncing = true;
6751 }
6752 assert!(crate::state::MY_SECRET_KEY.has_key(), "account A holds a live key");
6753
6754 crate::VectorCore.swap_session().await;
6755
6756 let st = crate::state::STATE.lock().await;
6757 assert!(st.chats.is_empty() && st.profiles.is_empty(), "STATE chats/profiles cleared on swap");
6758 assert!(!st.db_loaded && !st.is_syncing, "db_loaded / is_syncing reset");
6759 assert!(!crate::state::MY_SECRET_KEY.has_key(), "key vault cleared — no leak into account B");
6760 }
6761
6762 #[tokio::test]
6766 async fn join_finalization_persists_and_registers_the_channel() {
6767 let (_tmp, _guard) = init_test_db();
6768 crate::state::STATE.lock().await.chats.clear(); let relay = MemoryRelay::new();
6770 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6771 become_local(&Keys::generate());
6773
6774 crate::VectorCore.finalize_member_join(community.clone(), &relay, None).await.unwrap();
6775
6776 assert!(crate::db::community::load_community(&community.id).unwrap().is_some(), "community persisted on join");
6777 assert!(!crate::state::STATE.lock().await.chats.is_empty(), "the channel is registered as a chat");
6778 }
6779
6780 #[tokio::test]
6784 async fn join_finalization_tears_down_a_banned_joiner() {
6785 let (_tmp, _guard) = init_test_db();
6786 let relay = MemoryRelay::new();
6787 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6788 create_public_invite(&relay, &community, None, None).await.unwrap();
6790 let community = crate::db::community::load_community(&community.id).unwrap().unwrap();
6791
6792 let joiner = Keys::generate();
6794 publish_banlist(&relay, &community, &[joiner.public_key().to_hex()]).await.unwrap();
6795 become_local(&joiner);
6796 assert!(crate::db::community::load_community(&community.id).unwrap().is_some(), "community present pre-join");
6797
6798 let result = crate::VectorCore.finalize_member_join(community.clone(), &relay, None).await;
6799 assert!(result.is_err(), "a banned joiner's finalize must fail");
6800 assert!(result.unwrap_err().to_string().contains("banned"), "the error names the ban");
6801 assert!(
6802 crate::db::community::load_community(&community.id).unwrap().is_none(),
6803 "the just-saved community is torn back down — no orphaned row for a banned joiner"
6804 );
6805 }
6806
6807 #[tokio::test]
6813 async fn delete_community_wipes_every_community_scoped_table() {
6814 let (_tmp, _guard) = init_test_db();
6815 let relay = MemoryRelay::new();
6816 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6817 let cid = community.id.to_hex();
6818
6819 crate::db::community::store_epoch_key(&cid, crate::community::SERVER_ROOT_SCOPE_HEX, 1, &[0x11u8; 32]).unwrap();
6821 crate::db::community::save_public_invite("tok", &cid, "https://x/invite#y", None, None).unwrap();
6822 crate::db::community::save_pending_invite(&cid, "{}", "npub1inviter").unwrap();
6823 crate::db::community::set_edition_head(&cid, &cid, 1, &[0x22u8; 32]).unwrap();
6824 crate::db::community::set_community_banlist(&cid, &["cc".repeat(32)], 100).unwrap();
6825
6826 assert!(crate::db::community::community_exists(&community.id).unwrap());
6828 assert!(!crate::db::community::held_epoch_keys(&cid, crate::community::SERVER_ROOT_SCOPE_HEX).unwrap().is_empty());
6829 assert!(!crate::db::community::list_public_invites(&cid).unwrap().is_empty());
6830 assert!(crate::db::community::list_pending_invites().unwrap().iter().any(|p| p.community_id == cid));
6831 assert!(!crate::db::community::get_all_edition_heads(&cid).unwrap().is_empty());
6832 assert!(!crate::db::community::get_community_banlist(&cid).unwrap().is_empty());
6833
6834 crate::db::community::delete_community(&cid).unwrap();
6835
6836 assert!(!crate::db::community::community_exists(&community.id).unwrap(), "communities row gone");
6838 assert!(crate::db::community::load_community(&community.id).unwrap().is_none(), "community not loadable");
6839 assert!(crate::db::community::held_epoch_keys(&cid, crate::community::SERVER_ROOT_SCOPE_HEX).unwrap().is_empty(), "epoch keys wiped");
6840 assert!(crate::db::community::list_public_invites(&cid).unwrap().is_empty(), "public invites wiped");
6841 assert!(!crate::db::community::list_pending_invites().unwrap().iter().any(|p| p.community_id == cid), "pending invites wiped");
6842 assert!(crate::db::community::get_all_edition_heads(&cid).unwrap().is_empty(), "edition heads wiped");
6843 assert!(crate::db::community::get_community_banlist(&cid).unwrap().is_empty(), "banlist wiped with the channels");
6844 }
6845
6846 #[tokio::test]
6850 async fn fetch_control_folded_skips_junk_injected_at_the_coordinate() {
6851 let (_tmp, _guard) = init_test_db();
6852 let relay = MemoryRelay::new();
6853 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6854 let owner_hex = crate::state::MY_SECRET_KEY.to_keys().unwrap().public_key().to_hex();
6855
6856 let z = crate::community::roster::control_pseudonym(&community.server_root_key, &community.id, community.server_root_epoch);
6858 let junk = nostr_sdk::EventBuilder::new(nostr_sdk::Kind::Custom(event_kind::COMMUNITY_CONTROL), "not a sealed edition")
6859 .tags([nostr_sdk::Tag::custom(nostr_sdk::TagKind::Custom("z".into()), [z])])
6860 .sign_with_keys(&Keys::generate())
6861 .unwrap();
6862 relay.publish(&junk, &community.relays).await.unwrap();
6863
6864 let folded = fetch_control_folded(&relay, &community).await.unwrap();
6865 assert!(
6866 !crate::community::roster::authorize_delegation(&folded, Some(&owner_hex)).roles.is_empty(),
6867 "the genuine Admin role still folds; the un-openable junk is silently dropped"
6868 );
6869 }
6870
6871 #[tokio::test]
6874 async fn fetch_control_folded_on_dead_relays_is_empty_not_a_panic() {
6875 let (_tmp, _guard) = init_test_db();
6876 let community = saved_community_owned_by(&Keys::generate());
6877 let folded = fetch_control_folded(&FailingRelay, &community).await.unwrap();
6878 assert!(folded.roles.roles.is_empty() && folded.root_meta.is_none(), "dead relays → empty fold, no panic");
6879 }
6880
6881 #[tokio::test]
6882 async fn successful_private_ban_leaves_no_read_cut_pending() {
6883 let (_tmp, _guard) = init_test_db();
6885 let relay = MemoryRelay::new();
6886 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6887 let cid = community.id.to_hex();
6888 publish_banlist(&relay, &community, &["cc".repeat(32)]).await.unwrap();
6889 assert!(!crate::db::community::get_read_cut_pending(&cid).unwrap(), "a successful re-seal leaves no pending read-cut");
6890 let c = crate::db::community::load_community(&community.id).unwrap().unwrap();
6891 assert_eq!(c.server_root_epoch, crate::community::Epoch(1));
6892 }
6893
6894 #[tokio::test]
6895 async fn failed_reseal_sets_pending_then_sync_retry_recovers() {
6896 let (_tmp, _guard) = init_test_db();
6901 let relay = RekeyFailingRelay::new(); let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6903 let cid = community.id.to_hex();
6904 let victim = "cc".repeat(32);
6905
6906 assert!(publish_banlist(&relay, &community, &[victim.clone()]).await.is_err(), "the re-seal's base rekey fails");
6908 assert!(crate::db::community::get_read_cut_pending(&cid).unwrap(), "a failed re-seal leaves read_cut_pending set");
6909 assert_eq!(crate::db::community::get_community_banlist(&cid).unwrap(), vec![victim.clone()], "the ban itself still applied");
6910 let c = crate::db::community::load_community(&community.id).unwrap().unwrap();
6911 assert_eq!(c.server_root_epoch, crate::community::Epoch(0), "base NOT rotated while the re-seal is pending");
6912
6913 relay.allow_rekey();
6915 retry_pending_read_cut(&relay, &c).await.unwrap();
6916 assert!(!crate::db::community::get_read_cut_pending(&cid).unwrap(), "pending cleared after the retry succeeds");
6917 let c2 = crate::db::community::load_community(&community.id).unwrap().unwrap();
6918 assert_eq!(c2.server_root_epoch, crate::community::Epoch(1), "the read-cut finally rotated the base");
6919 }
6920
6921 #[tokio::test]
6922 async fn privatize_reseals_to_observed_participants_not_just_owner() {
6923 use crate::community::derive::{base_rekey_pseudonym, recipient_pseudonym};
6927 use crate::community::rekey::{open_rekey_blob, open_rekey_event, rekey_pairwise_secret};
6928 use crate::types::Message;
6929 use nostr_sdk::ToBech32;
6930 let (_tmp, _guard) = init_test_db();
6931 let relay = MemoryRelay::new();
6932 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6933 let cid = community.id.to_hex();
6934 let genesis_root = *community.server_root_key.as_bytes();
6935 let channel_hex = community.channels[0].id.to_hex();
6936
6937 let alice = Keys::generate();
6939 let alice_b32 = alice.public_key().to_bech32().unwrap();
6940 let mut m = Message::default();
6941 m.id = "aa".repeat(32);
6942 m.npub = Some(alice_b32.clone());
6943 m.at = 1000;
6944 crate::db::events::save_message(&channel_hex, &m).await.unwrap();
6945 assert!(
6946 crate::db::community::community_member_activity(&cid).unwrap().iter().any(|(np, _)| np == &alice_b32),
6947 "alice is an observed participant"
6948 );
6949
6950 let (token_hex, _) = create_public_invite(&relay, &community, None, None).await.unwrap();
6952 revoke_public_invite(&relay, &community, &crate::simd::hex::hex_to_bytes_32(&token_hex)).await.unwrap();
6953 let reloaded = crate::db::community::load_community(&community.id).unwrap().unwrap();
6954 assert_eq!(reloaded.server_root_epoch, crate::community::Epoch(1), "privatize rotated the base");
6955
6956 let addr = base_rekey_pseudonym(&crate::community::ServerRootKey(genesis_root), &community.id, crate::community::Epoch(1)).to_hex();
6959 let found = relay
6960 .fetch(&Query { kinds: vec![event_kind::COMMUNITY_REKEY], z_tags: vec![addr], ..Default::default() }, &community.relays)
6961 .await
6962 .unwrap();
6963 assert_eq!(found.len(), 1, "the base rekey is published");
6964 let parsed = open_rekey_event(&found[0], &genesis_root).unwrap();
6965 let secret = rekey_pairwise_secret(alice.secret_key(), &parsed.rotator).unwrap();
6966 let loc = recipient_pseudonym(&secret, parsed.scope, parsed.new_epoch).to_hex();
6967 let alice_blob = parsed.blobs.iter().find(|b| b.locator == loc).expect("alice's blob present (NOT sealed out)");
6968 let recovered = open_rekey_blob(alice.secret_key(), &parsed.rotator, parsed.scope, parsed.new_epoch, alice_blob).unwrap();
6969 assert_eq!(reloaded.server_root_key.as_bytes(), &recovered, "alice recovers the new root = owner's advanced base");
6970 }
6971
6972 #[tokio::test]
6973 async fn unpermissioned_invite_links_edition_is_rejected() {
6974 let (_tmp, _guard) = init_test_db();
6978 let relay = MemoryRelay::new();
6979 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
6980
6981 let mallory = Keys::generate();
6982 let loc = "2b".repeat(32);
6983 let inner = crate::community::roster::build_invite_links_edition(&mallory, &community.id, &[loc], 1, None, 1000, None).unwrap();
6984 let outer = crate::community::roster::seal_control_edition(&Keys::generate(), &inner, &community.server_root_key, &community.id, crate::community::Epoch(0)).unwrap();
6985 relay.inject(&outer, &community.relays);
6986
6987 let applied = fetch_and_apply_invite_links(&relay, &community).await.unwrap();
6988 assert!(applied.is_empty(), "an unpermissioned member's link edition is rejected");
6989 assert!(!is_public(&community).unwrap(), "mode stays Private despite the forged edition");
6990 }
6991
6992 #[tokio::test]
6993 async fn invite_links_union_across_authorized_creators() {
6994 let (_tmp, _guard) = init_test_db();
6998 let relay = MemoryRelay::new();
6999 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
7000 let cid = community.id.to_hex();
7001
7002 create_public_invite(&relay, &community, None, None).await.unwrap();
7004 let owner_loc = public_invite::locator_hex(&crate::simd::hex::hex_to_bytes_32(
7005 &crate::db::community::list_public_invites(&cid).unwrap()[0].token));
7006
7007 let admin = Keys::generate();
7009 let admin_role_id = crate::db::community::get_community_roles(&cid).unwrap().roles[0].role_id.clone();
7010 set_member_grant(&relay, &community, &admin.public_key().to_hex(), vec![admin_role_id]).await.unwrap();
7011 let admin_loc = "ab".repeat(32);
7012 let inner = crate::community::roster::build_invite_links_edition(&admin, &community.id, &[admin_loc.clone()], 1, None, 2000, None).unwrap();
7013 let outer = crate::community::roster::seal_control_edition(&Keys::generate(), &inner, &community.server_root_key, &community.id, crate::community::Epoch(0)).unwrap();
7014 relay.inject(&outer, &community.relays);
7015
7016 let agg = fetch_and_apply_invite_links(&relay, &community).await.unwrap();
7017 assert!(agg.contains(&owner_loc), "owner's link in the aggregate");
7018 assert!(agg.contains(&admin_loc), "the granted admin's link unions in too");
7019 assert!(is_public(&community).unwrap());
7020
7021 let c = crate::db::community::load_community(&community.id).unwrap().unwrap();
7025 let owner_token = crate::db::community::list_public_invites(&cid).unwrap()[0].token.clone();
7026 revoke_public_invite(&relay, &c, &crate::simd::hex::hex_to_bytes_32(&owner_token)).await.unwrap();
7027 let c = crate::db::community::load_community(&community.id).unwrap().unwrap();
7028 assert_eq!(c.server_root_epoch, crate::community::Epoch(0), "another creator's link remains → no privatize rekey");
7029 assert!(is_public(&c).unwrap(), "still Public (admin's link is live)");
7030 }
7031
7032 #[tokio::test]
7033 async fn failed_banlist_publish_does_not_persist_locally() {
7034 let (_tmp, _guard) = init_test_db();
7037 let relay = MemoryRelay::new();
7038 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
7039 let id_hex = community.id.to_hex();
7040 assert!(crate::db::community::get_community_banlist(&id_hex).unwrap().is_empty());
7041
7042 let victim = "cc".repeat(32);
7043 let err = publish_banlist(&FailingRelay, &community, &[victim]).await;
7044 assert!(err.is_err(), "a failed publish must propagate");
7045 assert!(
7046 crate::db::community::get_community_banlist(&id_hex).unwrap().is_empty(),
7047 "local banlist must be untouched when the publish failed"
7048 );
7049 }
7050
7051 #[tokio::test]
7052 async fn metadata_failed_publish_does_not_persist_locally() {
7053 let (_tmp, _guard) = init_test_db();
7057 let relay = MemoryRelay::new();
7058 let mut community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
7059 community.name = "Renamed HQ".to_string();
7060 assert!(republish_community_metadata(&FailingRelay, &community).await.is_err());
7061 let loaded = crate::db::community::load_community(&community.id).unwrap().unwrap();
7062 assert_eq!(loaded.name, "HQ", "a failed metadata publish leaves the local name unchanged");
7063 }
7064
7065 #[tokio::test]
7066 async fn send_persists_key_then_delete_round_trip() {
7067 let (_tmp, _guard) = init_test_db();
7068 let relay = MemoryRelay::new();
7069 let community = Community::create("HQ", "general", vec!["r1".into()]);
7070 let channel = community.channels[0].clone();
7071 let alice = Keys::generate();
7072
7073 let _outer = send_message(&relay, &community, &channel, &alice, "deletable", 1).await.unwrap();
7075 let before = fetch_channel_messages(&relay, &community, &channel).await.unwrap();
7076 assert_eq!(before.len(), 1);
7077 let message_id = before[0].message_id.to_hex();
7078
7079 delete_message(&relay, &message_id).await.unwrap();
7081 let after = fetch_channel_messages(&relay, &community, &channel).await.unwrap();
7082 assert!(after.is_empty(), "message should be deleted after delete_message");
7083
7084 assert!(delete_message(&relay, &message_id).await.is_err());
7086 }
7087
7088 #[tokio::test]
7089 async fn failed_delete_publish_preserves_key() {
7090 let (_tmp, _guard) = init_test_db();
7093 let relay = MemoryRelay::new();
7094 let community = Community::create("HQ", "general", vec!["r1".into()]);
7095 let channel = community.channels[0].clone();
7096 let alice = Keys::generate();
7097 send_message(&relay, &community, &channel, &alice, "delete me", 1).await.unwrap();
7098 let message_id = fetch_channel_messages(&relay, &community, &channel).await.unwrap()[0]
7099 .message_id
7100 .to_hex();
7101
7102 assert!(delete_message(&FailingRelay, &message_id).await.is_err());
7104
7105 delete_message(&relay, &message_id).await.unwrap();
7107 assert!(fetch_channel_messages(&relay, &community, &channel).await.unwrap().is_empty());
7108 }
7109
7110 #[tokio::test]
7111 async fn delete_unknown_message_errors() {
7112 let (_tmp, _guard) = init_test_db();
7113 let relay = MemoryRelay::new();
7114 let fake = Keys::generate();
7116 let bogus = EventBuilder::new(Kind::Custom(1), "x").sign_with_keys(&fake).unwrap().id;
7117 assert!(delete_message(&relay, &bogus.to_hex()).await.is_err());
7118 }
7119
7120 #[tokio::test]
7121 async fn accept_invite_persists_member_view() {
7122 let (_tmp, _guard) = init_test_db();
7123 let owner = Community::create("HQ", "general", vec!["r1".into()]);
7124 let invite = crate::community::invite::build_invite(&owner);
7125
7126 let joined = accept_invite(&invite).expect("accept");
7127 assert!(!is_proven_owner(&joined), "joined as member, not owner");
7128 let loaded = crate::db::community::load_community(&owner.id).unwrap().expect("saved");
7130 assert_eq!(loaded.channels[0].key.as_bytes(), owner.channels[0].key.as_bytes());
7131 }
7132
7133 #[tokio::test]
7134 async fn accept_invite_does_not_downgrade_owned_community() {
7135 let (_tmp, _guard) = init_test_db();
7138 let relay = MemoryRelay::new();
7139 let owner = create_community(&relay, "HQ", "general", vec![]).await.unwrap();
7140 assert!(is_proven_owner(&owner), "we are the proven owner");
7141
7142 let invite = crate::community::invite::build_invite(&owner);
7143 let err = accept_invite(&invite).unwrap_err();
7144 assert!(err.contains("already own"), "must refuse to downgrade an owned community, got: {err}");
7145
7146 let reloaded = crate::db::community::load_community(&owner.id).unwrap().unwrap();
7148 assert_eq!(reloaded.server_root_key.as_bytes(), owner.server_root_key.as_bytes());
7149 }
7150
7151 #[tokio::test]
7152 async fn accept_invite_rejects_id_collision_under_different_authority() {
7153 let (_tmp, _guard) = init_test_db();
7158 let legit = Community::create("X", "general", vec!["wss://legit".into()]);
7159 let member_x = accept_invite(&crate::community::invite::build_invite(&legit)).unwrap();
7160 let original_key = member_x.channels[0].key.as_bytes().to_vec();
7161
7162 let attacker = Community::create("evil", "general", vec!["wss://evil".into()]);
7164 let mut hostile = crate::community::invite::build_invite(&attacker);
7165 hostile.community_id = legit.id.to_hex();
7166 assert_ne!(hostile.server_root_key, crate::simd::hex::bytes_to_hex_32(member_x.server_root_key.as_bytes()));
7169
7170 assert!(accept_invite(&hostile).is_err(), "id-collision under new authority must be rejected");
7171
7172 let reloaded = crate::db::community::load_community(&legit.id).unwrap().unwrap();
7174 assert_eq!(reloaded.channels[0].key.as_bytes().to_vec(), original_key);
7175 assert_eq!(reloaded.relays, vec!["wss://legit".to_string()]);
7176 }
7177
7178 #[tokio::test]
7179 async fn rejected_accept_leaves_pending_invite_intact() {
7180 let (_tmp, _guard) = init_test_db();
7183
7184 let owner = attested_community("HQ", "general", vec![]);
7186 crate::db::community::save_community(&owner).unwrap();
7187 let bundle = crate::community::invite::build_invite(&owner).to_json().unwrap();
7188 let cid = owner.id.to_hex();
7189 crate::db::community::save_pending_invite(&cid, &bundle, "npub1inviter").unwrap();
7190
7191 let peeked = crate::db::community::get_pending_invite(&cid).unwrap().expect("parked");
7193 let invite = crate::community::invite::CommunityInvite::from_json(&peeked).unwrap();
7194 assert!(accept_invite(&invite).is_err(), "owning the id → reject");
7195 assert!(
7196 crate::db::community::pending_invite_exists(&cid).unwrap(),
7197 "rejected accept must leave the invite parked"
7198 );
7199
7200 let other = Community::create("Other", "general", vec![]);
7202 let ob = crate::community::invite::build_invite(&other).to_json().unwrap();
7203 let ocid = other.id.to_hex();
7204 crate::db::community::save_pending_invite(&ocid, &ob, "npub1inviter").unwrap();
7205 let op = crate::db::community::get_pending_invite(&ocid).unwrap().unwrap();
7206 let oinvite = crate::community::invite::CommunityInvite::from_json(&op).unwrap();
7207 accept_invite(&oinvite).expect("accept ok");
7208 crate::db::community::delete_pending_invite(&ocid).unwrap();
7209 assert!(!crate::db::community::pending_invite_exists(&ocid).unwrap(), "cleared on success");
7210 }
7211
7212 #[tokio::test]
7213 async fn public_invite_create_fetch_accept_revoke_round_trip() {
7214 let (_tmp, _guard) = init_test_db();
7215 let relay = MemoryRelay::new();
7216 let mut owner = Community::create("Public HQ", "general", vec!["r1".into(), "r2".into()]);
7217 owner.description = Some("everyone welcome".into());
7218 let owner_keys = crate::state::MY_SECRET_KEY.to_keys().unwrap();
7222 owner.owner_attestation = Some(
7223 crate::community::owner::build_owner_attestation_unsigned(owner_keys.public_key(), &owner.id.to_hex())
7224 .sign_with_keys(&owner_keys).unwrap().as_json(),
7225 );
7226 let (token_hex, url) = create_public_invite(&relay, &owner, None, None).await.expect("mint");
7228 assert!(url.contains('#'));
7229 assert_eq!(crate::db::community::list_public_invites(&owner.id.to_hex()).unwrap().len(), 1);
7230
7231 let (relays, token) = public_invite::parse_invite_url(&url).unwrap();
7233 assert_eq!(crate::simd::hex::bytes_to_hex_32(&token), token_hex);
7234 let bundle = fetch_public_invite(&relay, &relays, &token).await.expect("fetch");
7235 assert_eq!(bundle.preview.name, "Public HQ");
7236 assert_eq!(bundle.preview.description.as_deref(), Some("everyone welcome"));
7237
7238 let joined = accept_public_invite(&bundle, 0).expect("accept");
7239 assert_eq!(joined.id, owner.id);
7240 assert_eq!(joined.description.as_deref(), Some("everyone welcome"), "preview patched in");
7241
7242 revoke_public_invite(&relay, &owner, &token).await.expect("revoke");
7244 assert!(fetch_public_invite(&relay, &relays, &token).await.is_err(), "revoked link is dead");
7245 assert!(crate::db::community::list_public_invites(&owner.id.to_hex()).unwrap().is_empty());
7246 }
7247
7248 #[tokio::test]
7249 async fn revoked_invite_dies_even_if_one_relay_kept_the_bundle() {
7250 let (_tmp, _guard) = init_test_db();
7254 let relay = MemoryRelay::new();
7255 let owner = attested_community("HQ", "general", vec!["r1".into(), "r2".into()]);
7256 let (_token_hex, url) = create_public_invite(&relay, &owner, None, None).await.unwrap();
7257 let (relays, token) = public_invite::parse_invite_url(&url).unwrap();
7258 assert!(fetch_public_invite(&relay, &relays, &token).await.is_ok(), "live on both relays");
7259
7260 let tombstone = public_invite::build_public_invite_tombstone(&token).unwrap();
7262 relay.inject(&tombstone, &["r1".to_string()]);
7263
7264 assert!(
7265 fetch_public_invite(&relay, &relays, &token).await.is_err(),
7266 "a tombstone on any one relay kills the link, even with a stale live bundle elsewhere",
7267 );
7268 }
7269
7270 #[tokio::test]
7271 async fn fetch_skips_relay_shadow_junk_to_genuine_bundle() {
7272 use nostr_sdk::prelude::{EventBuilder, Keys, Kind, Tag, TagKind, Timestamp};
7276
7277 let (_tmp, _guard) = init_test_db();
7278 let relay = MemoryRelay::new();
7279 let owner = attested_community("HQ", "general", vec!["r1".into()]);
7280 let (_t, url) = create_public_invite(&relay, &owner, None, None).await.unwrap();
7281 let (relays, token) = public_invite::parse_invite_url(&url).unwrap();
7282
7283 let attacker = Keys::generate();
7286 let junk = EventBuilder::new(Kind::Custom(event_kind::APPLICATION_SPECIFIC), "garbage")
7287 .tags([
7288 Tag::identifier(public_invite::locator_hex(&token)),
7289 Tag::custom(TagKind::Custom("vsk".into()), ["6".to_string()]),
7290 Tag::custom(TagKind::Custom("v".into()), ["1".to_string()]),
7291 ])
7292 .custom_created_at(Timestamp::from_secs(9_000_000_000))
7293 .sign_with_keys(&attacker)
7294 .unwrap();
7295 relay.publish(&junk, &relays).await.unwrap();
7296
7297 let bundle = fetch_public_invite(&relay, &relays, &token).await.expect("genuine survives shadow");
7299 assert_eq!(bundle.preview.name, "HQ");
7300 }
7301
7302 #[tokio::test]
7303 async fn expired_public_invite_is_refused() {
7304 let (_tmp, _guard) = init_test_db();
7305 let relay = MemoryRelay::new();
7306 let owner = attested_community("HQ", "general", vec!["r1".into()]);
7307 let (_t, url) = create_public_invite(&relay, &owner, Some(1000), None).await.unwrap();
7308 let (relays, token) = public_invite::parse_invite_url(&url).unwrap();
7309 let bundle = fetch_public_invite(&relay, &relays, &token).await.unwrap();
7310 assert!(accept_public_invite(&bundle, 2000).is_err());
7312 assert!(crate::db::community::load_community(&owner.id).unwrap().is_none());
7313 }
7314
7315 #[tokio::test]
7316 async fn republish_metadata_saves_and_publishes() {
7317 use crate::community::CommunityImage;
7318 let (_tmp, _guard) = init_test_db();
7319 let relay = MemoryRelay::new();
7320 let mut owner = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
7323 let cid = owner.id.to_hex();
7324
7325 owner.name = "HQ Renamed".into();
7327 owner.description = Some("now with topic".into());
7328 owner.icon = Some(CommunityImage {
7329 url: "https://b/x".into(), key: "aa".repeat(32), nonce: "bb".repeat(12),
7330 hash: "cc".repeat(32), ext: "png".into(),
7331 });
7332 republish_community_metadata(&relay, &owner).await.expect("republish");
7333
7334 let loaded = crate::db::community::load_community(&owner.id).unwrap().unwrap();
7336 assert_eq!(loaded.name, "HQ Renamed");
7337 assert_eq!(loaded.description.as_deref(), Some("now with topic"));
7338 assert_eq!(loaded.icon.unwrap().url, "https://b/x");
7339
7340 let (head_v, _) = crate::db::community::get_edition_head(&cid, &cid).unwrap().unwrap();
7343 assert_eq!(head_v, 2, "GroupRoot edition advanced v1 (create) → v2 (republish)");
7344 let z = crate::community::roster::control_pseudonym(&owner.server_root_key, &owner.id, crate::community::Epoch(0));
7345 let control = relay
7346 .fetch(&Query { kinds: vec![event_kind::COMMUNITY_CONTROL], z_tags: vec![z], ..Default::default() }, &owner.relays)
7347 .await
7348 .unwrap();
7349 let newest = control
7350 .iter()
7351 .filter_map(|o| crate::community::roster::open_control_edition(o, &owner.server_root_key).ok())
7352 .filter_map(|i| crate::community::edition::parse_edition_inner(&i).ok())
7353 .filter(|p| p.entity_id == owner.id.0)
7354 .max_by_key(|p| p.version)
7355 .expect("GroupRoot edition on the relay");
7356 let meta: crate::community::metadata::CommunityMetadata = serde_json::from_str(&newest.content).unwrap();
7357 assert_eq!(meta.name, "HQ Renamed");
7358 assert_eq!(meta.icon.unwrap().ext, "png");
7359 }
7360
7361 #[tokio::test]
7362 async fn member_cannot_republish_metadata() {
7363 let (_tmp, _guard) = init_test_db();
7364 let relay = MemoryRelay::new();
7365 let owner = Community::create("HQ", "general", vec!["r1".into()]);
7366 let member = crate::community::invite::accept_invite(&crate::community::invite::build_invite(&owner)).unwrap();
7367 assert!(republish_community_metadata(&relay, &member).await.is_err());
7368 }
7369
7370 #[tokio::test]
7371 async fn member_cannot_mint_public_invite() {
7372 let (_tmp, _guard) = init_test_db();
7373 let relay = MemoryRelay::new();
7374 let owner = Community::create("HQ", "general", vec!["r1".into()]);
7375 let member = crate::community::invite::accept_invite(&crate::community::invite::build_invite(&owner)).unwrap();
7376 assert!(create_public_invite(&relay, &member, None, None).await.is_err(), "members can't mint links");
7377 }
7378
7379 #[tokio::test]
7380 async fn accept_oversized_bundle_rejected() {
7381 let (_tmp, _guard) = init_test_db();
7382 let owner = Community::create("HQ", "general", vec![]);
7383 let mut invite = crate::community::invite::build_invite(&owner);
7384 let template = invite.channels[0].clone();
7386 for _ in 0..300 {
7387 invite.channels.push(template.clone());
7388 }
7389 assert!(accept_invite(&invite).is_err(), "oversized bundle must be rejected");
7390 assert!(crate::db::community::load_community(&owner.id).unwrap().is_none(), "nothing persisted");
7391 }
7392
7393 async fn publish_tombstone<T: Transport + ?Sized>(transport: &T, community: &Community, author: &Keys, created_at: u64) {
7399 let inner = crate::community::roster::build_group_dissolved_edition_unsigned(author.public_key(), &community.id, created_at)
7400 .sign_with_keys(author).unwrap();
7401 let outer = crate::community::roster::seal_control_edition(&Keys::generate(), &inner, &community.server_root_key, &community.id, community.server_root_epoch).unwrap();
7402 transport.publish_durable(&outer, &community.relays).await.unwrap();
7403 }
7404
7405 struct RekeyCountingRelay {
7408 inner: MemoryRelay,
7409 rekeys: std::sync::atomic::AtomicUsize,
7410 }
7411 impl RekeyCountingRelay {
7412 fn new() -> Self { Self { inner: MemoryRelay::new(), rekeys: std::sync::atomic::AtomicUsize::new(0) } }
7413 fn count(&self, e: &Event) {
7414 if e.kind.as_u16() == event_kind::COMMUNITY_REKEY {
7415 self.rekeys.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
7416 }
7417 }
7418 }
7419 #[async_trait::async_trait]
7420 impl Transport for RekeyCountingRelay {
7421 async fn publish(&self, e: &Event, r: &[String]) -> Result<(), String> { self.count(e); self.inner.publish(e, r).await }
7422 async fn publish_durable(&self, e: &Event, r: &[String]) -> Result<(), String> { self.count(e); self.inner.publish_durable(e, r).await }
7423 async fn fetch(&self, q: &Query, r: &[String]) -> Result<Vec<Event>, String> { self.inner.fetch(q, r).await }
7424 }
7425
7426 #[tokio::test]
7427 async fn owner_tombstone_folds_to_dissolved() {
7428 let (_tmp, _guard) = init_test_db();
7429 let relay = MemoryRelay::new();
7430 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
7432 let cid = community.id.to_hex();
7433 let owner = crate::state::MY_SECRET_KEY.to_keys().unwrap();
7434 publish_tombstone(&relay, &community, &owner, 1000).await;
7435
7436 assert!(!crate::db::community::get_community_dissolved(&cid).unwrap(), "alive before the fold");
7437 fetch_and_apply_control(&relay, &community).await.unwrap();
7438 assert!(crate::db::community::get_community_dissolved(&cid).unwrap(), "owner tombstone seals the community");
7439 }
7440
7441 #[tokio::test]
7442 async fn non_owner_tombstone_is_ignored() {
7443 let (_tmp, _guard) = init_test_db();
7444 let relay = MemoryRelay::new();
7445 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
7446 let cid = community.id.to_hex();
7447 let mallory = Keys::generate();
7450 publish_tombstone(&relay, &community, &mallory, 1000).await;
7451
7452 fetch_and_apply_control(&relay, &community).await.unwrap();
7453 assert!(!crate::db::community::get_community_dissolved(&cid).unwrap(), "a non-owner tombstone is ignored");
7454 }
7455
7456 #[tokio::test]
7457 async fn unreadable_deed_rejects_the_tombstone() {
7458 let (_tmp, _guard) = init_test_db();
7459 let relay = MemoryRelay::new();
7460 let owner = crate::state::MY_SECRET_KEY.to_keys().unwrap();
7461 let mut community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
7462 let cid = community.id.to_hex();
7463 publish_tombstone(&relay, &community, &owner, 1000).await;
7464 community.owner_attestation = None;
7466 crate::db::community::save_community(&community).unwrap();
7467 let stripped = crate::db::community::load_community(&community.id).unwrap().unwrap();
7468
7469 fetch_and_apply_control(&relay, &stripped).await.unwrap();
7470 assert!(!crate::db::community::get_community_dissolved(&cid).unwrap(), "unverifiable tombstone is rejected, not death-by-default");
7471 }
7472
7473 #[tokio::test]
7474 async fn binary_seal_drops_every_subsequent_event_with_no_timestamp_test() {
7475 let (_tmp, _guard) = init_test_db();
7476 let relay = MemoryRelay::new();
7477 let owner = crate::state::MY_SECRET_KEY.to_keys().unwrap();
7478 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
7479 let cid = community.id.to_hex();
7480 publish_tombstone(&relay, &community, &owner, 1000).await;
7481 fetch_and_apply_control(&relay, &community).await.unwrap();
7482 assert!(crate::db::community::get_community_dissolved(&cid).unwrap());
7483
7484 let sealed = crate::db::community::load_community(&community.id).unwrap().unwrap();
7486 let channel = sealed.channels[0].clone();
7487 let me = owner.public_key();
7488
7489 let backdated = super::super::envelope::seal_message(
7491 &Keys::generate(), &channel.key, &channel.id, channel.epoch, "ghost", 1,
7492 ).unwrap();
7493 let mut state = crate::state::ChatState::new();
7494 assert!(super::super::inbound::process_incoming(&mut state, &backdated, &channel, &me).is_none(),
7495 "a backdated message after the seal is dropped (binary seal, no timestamp test)");
7496
7497 publish_tombstone(&relay, &sealed, &owner, 2000).await;
7499 assert_eq!(fetch_and_apply_control(&relay, &sealed).await.unwrap(), 0,
7500 "control fold stops advancing once sealed");
7501 }
7502
7503 #[tokio::test]
7504 async fn dissolve_community_emits_no_rekey_and_no_epoch_bump() {
7505 let (_tmp, _guard) = init_test_db();
7506 let relay = RekeyCountingRelay::new();
7507 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
7508 let cid = community.id.to_hex();
7509 create_public_invite(&relay, &community, None, None).await.unwrap();
7511 let before_epoch = crate::db::community::load_community(&community.id).unwrap().unwrap().server_root_epoch;
7512
7513 dissolve_community(&relay, &community).await.unwrap();
7514
7515 assert!(crate::db::community::get_community_dissolved(&cid).unwrap(), "sealed locally");
7516 assert_eq!(relay.rekeys.load(std::sync::atomic::Ordering::Relaxed), 0,
7517 "dissolution publishes NO 3303 rekey (no last-link privatize re-founding)");
7518 assert_eq!(crate::db::community::load_community(&community.id).unwrap().unwrap().server_root_epoch, before_epoch,
7519 "base epoch unchanged — dissolution rotates nothing");
7520 }
7521
7522 #[tokio::test]
7523 async fn duplicate_owner_tombstones_are_idempotent() {
7524 let (_tmp, _guard) = init_test_db();
7525 let relay = MemoryRelay::new();
7526 let owner = crate::state::MY_SECRET_KEY.to_keys().unwrap();
7527 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
7528 let cid = community.id.to_hex();
7529 publish_tombstone(&relay, &community, &owner, 1000).await;
7531 publish_tombstone(&relay, &community, &owner, 2000).await;
7532
7533 fetch_and_apply_control(&relay, &community).await.unwrap();
7534 assert!(crate::db::community::get_community_dissolved(&cid).unwrap(), "duplicates still just dissolve, no error");
7535 assert_eq!(fetch_and_apply_control(&relay, &community).await.unwrap(), 0);
7537 }
7538
7539 #[test]
7540 fn apply_server_root_rekey_refuses_once_dissolved() {
7541 let (_tmp, _guard) = init_test_db();
7542 let owner = Keys::generate();
7543 let me = Keys::generate();
7544 become_local(&me);
7545 let community = saved_community_owned_by(&owner);
7546 let cid = community.id.to_hex();
7547 crate::db::community::set_community_dissolved(&cid).unwrap();
7548
7549 let parsed = owner_base_rekey(&owner, &community, &me.public_key(), 1, &[0xCDu8; 32]);
7550 assert!(apply_server_root_rekey(&community, &parsed).is_err(),
7551 "a base rekey cannot cross a tombstone");
7552 assert_eq!(crate::db::community::load_community(&community.id).unwrap().unwrap().server_root_epoch,
7553 crate::community::Epoch(0), "base epoch did not advance");
7554 }
7555
7556 #[tokio::test]
7557 async fn tombstone_detected_after_a_base_rotation() {
7558 let (_tmp, _guard) = init_test_db();
7559 let relay = MemoryRelay::new();
7560 let owner = crate::state::MY_SECRET_KEY.to_keys().unwrap();
7561 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
7562 let cid = community.id.to_hex();
7563 rotate_server_root(&relay, &community, &[owner.public_key()]).await.unwrap();
7566 let rotated = crate::db::community::load_community(&community.id).unwrap().unwrap();
7567 assert_eq!(rotated.server_root_epoch, crate::community::Epoch(1));
7568 publish_tombstone(&relay, &rotated, &owner, 1000).await;
7569
7570 fetch_and_apply_control(&relay, &rotated).await.unwrap();
7571 assert!(crate::db::community::get_community_dissolved(&cid).unwrap(),
7572 "tombstone at the rotation-stable locator is detected post-rotation");
7573 }
7574
7575 #[tokio::test]
7576 async fn stable_coordinate_tombstone_survives_a_concurrent_rotation() {
7577 let (_tmp, _guard) = init_test_db();
7582 let relay = MemoryRelay::new();
7583 let owner = crate::state::MY_SECRET_KEY.to_keys().unwrap();
7584 let community = create_community(&relay, "HQ", "general", vec!["r1".into()]).await.unwrap();
7585 let cid = community.id.to_hex();
7586 let inner = crate::community::roster::build_group_dissolved_edition_unsigned(owner.public_key(), &community.id, 1000)
7588 .sign_with_keys(&owner).unwrap();
7589 let stable = crate::community::roster::seal_dissolved_edition(&Keys::generate(), &inner, &community.id).unwrap();
7590 relay.inject(&stable, &community.relays);
7591 rotate_server_root(&relay, &community, &[owner.public_key()]).await.unwrap();
7594 let rotated = crate::db::community::load_community(&community.id).unwrap().unwrap();
7595 assert_eq!(rotated.server_root_epoch, crate::community::Epoch(1));
7596 assert!(!crate::db::community::get_community_dissolved(&cid).unwrap(), "not folded yet");
7597 fetch_and_apply_control(&relay, &rotated).await.unwrap();
7600 assert!(crate::db::community::get_community_dissolved(&cid).unwrap(),
7601 "stable-coordinate probe discovers the tombstone cross-epoch (C3 closed)");
7602 }
7603}