Files
coop/crates/community/src/sync.rs
T
2026-09-22 20:15:15 +07:00

1556 lines
52 KiB
Rust

use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use anyhow::{Context, Result};
use concord::cord01::KIND_WRAP_EPHEMERAL;
use concord::cord02::list::{CommunityList, JoinMaterial, KIND_COMMUNITY_LIST};
use concord::cord02::{self, ControlFold, ImageRef};
use concord::cord04::AuthorityCitation;
use concord::cord04::roles::{Permissions, citation_ok};
use concord::derive::{
channel_group_key, control_group_key, control_signer_group_key, guestbook_group_key,
};
use concord::state::{CommunityState, HeldKey, HeldRoot, list_entry};
use concord::{ChannelId, CommunityId, Epoch, GroupKey, decode_hex_32};
use gpui::AsyncApp;
use nostr_sdk::prelude::*;
use state::UniversalSigner;
use crate::cache::{self, Observed};
use crate::history::{CURSOR_OVERLAP, Window};
/// How much of what a relay stores a cold subscription replays per relay.
const LIVE_REPLAY: usize = 500;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PlaneKind {
Control(Epoch),
Guestbook,
Channel(ChannelId, Epoch),
}
#[derive(Debug, Clone)]
pub struct Plane {
pub kind: PlaneKind,
/// The wrap's author: the control signer for Control, the group's own key otherwise.
pub address: PublicKey,
pub group: GroupKey,
/// When the rotation that retired this plane's key published.
pub retired_at: Option<Timestamp>,
}
impl Plane {
/// Whether a wrap sealed under this plane is still inside its key's life.
pub fn accepts(&self, wrap: &Event) -> bool {
self.retired_at
.is_none_or(|retired| wrap.created_at <= retired)
}
}
pub fn planes(state: &CommunityState) -> Result<Vec<Plane>> {
let mut planes = Vec::new();
let roots = state.roots();
for (epoch, address) in &state.control_pks {
let epoch = Epoch(*epoch);
// An epoch's Control Plane reads under the root that was current then,
// so a rotation that kept a floor for the prior root republishes here.
let root = roots
.iter()
.find(|root| root.epoch == epoch)
.copied()
.unwrap_or(HeldRoot {
epoch,
key: state.community_root,
control_pk: None,
retired_at: None,
});
let group = control_group_key(&root.key, &state.id, epoch)?;
planes.push(Plane {
kind: PlaneKind::Control(epoch),
address: *address,
group,
retired_at: root.retired_at,
});
}
if state.control_pks.is_empty() {
let group = control_group_key(&state.community_root, &state.id, state.root_epoch)?;
planes.push(Plane {
kind: PlaneKind::Control(state.root_epoch),
address: group.pk(),
group,
retired_at: None,
});
}
for root in &roots {
let group = guestbook_group_key(&root.key, &state.id, root.epoch)?;
planes.push(Plane {
kind: PlaneKind::Guestbook,
address: group.pk(),
group,
retired_at: root.retired_at,
});
}
for channel in &state.channels {
for held in state.held_keys(&channel.id) {
let group = channel_group_key(&held.key, &channel.id, held.epoch)?;
planes.push(Plane {
kind: PlaneKind::Channel(channel.id, held.epoch),
address: group.pk(),
group,
retired_at: held.retired_at,
});
}
}
// A rotation can re-derive an address the current root already produced.
//
// A duplicate author would only repeat a filter, so keep the set unique.
let mut seen = BTreeSet::new();
planes.retain(|plane| seen.insert(plane.address));
Ok(planes)
}
pub fn plane_filter(planes: &[Plane]) -> Filter {
Filter::new()
.kinds([Kind::GiftWrap, Kind::Custom(KIND_WRAP_EPHEMERAL)])
.authors(planes.iter().map(|plane| plane.address))
}
pub fn live_filter(planes: &[Plane], window: Window) -> Filter {
let mut filter = plane_filter(planes);
if let Some(until) = window.until {
filter = filter.until(until);
}
if let Some(since) = window.since {
filter = filter.since(since);
}
filter.limit(LIVE_REPLAY)
}
/// The window a community's standing subscription opens with.
pub fn live_window(state: &CommunityState, now: Timestamp) -> Window {
let floor = state
.channels
.iter()
.filter_map(|channel| state.cursors.get(&channel.id)?.newest)
.min();
match floor {
Some(floor) => Window {
since: Some(floor.min(now) - CURSOR_OVERLAP),
until: None,
},
None => Window::default(),
}
}
/// The subscription id carrying a community's planes.
pub fn subscription_id(id: &CommunityId) -> SubscriptionId {
SubscriptionId::new(id.to_hex())
}
pub fn community_of(subscription_id: &SubscriptionId) -> Option<CommunityId> {
subscription_id.as_str().parse().ok()
}
/// Download and decrypt a community icon into a content-addressed cache file.
pub async fn resolve_image(image: &ImageRef, cx: &AsyncApp) -> Result<PathBuf> {
let url = Url::parse(&image.url).context("community image url")?;
state::download_and_decrypt_to_cache(&url, &image.key, &image.nonce, &image.hash, cx).await
}
#[derive(Debug, Clone)]
pub struct Snapshot {
pub state: CommunityState,
pub control: ControlFold,
pub members: BTreeSet<PublicKey>,
/// Wraps the store holds per channel that no held key can open.
pub unreadable: BTreeMap<ChannelId, usize>,
}
pub async fn create<S>(
client: &Client,
signer: &S,
metadata: &cord02::CommunityMetadata,
) -> Result<CommunityState>
where
S: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44 + ?Sized,
{
let at_secs = Timestamp::now().as_secs();
let genesis = cord02::genesis(signer, metadata, at_secs).await?;
let id = genesis.identity.community_id;
let read = control_group_key(&genesis.community_root, &id, cord02::ROOT_EPOCH)?;
let address = control_signer_group_key(&genesis.control_root, &id, cord02::ROOT_EPOCH)?.pk();
let mut editions = Vec::with_capacity(genesis.wraps.len());
for wrap in &genesis.wraps {
editions.push(cord02::open_edition(wrap, &read, &address, true)?);
}
let state = CommunityState::from_genesis(&genesis, &editions, at_secs.saturating_mul(1000))?;
cache::save_state(client, &state).await?;
publish_wraps(client, &genesis.wraps, &state.relays).await;
if let Err(error) = record_membership(client, signer, &state, &metadata.name).await {
log::warn!(
"community {}: recording the membership failed: {error}",
state.id.to_hex()
);
}
Ok(state)
}
/// Best-effort publication of the genesis wraps to the community's relays.
pub(crate) async fn publish_wraps(client: &Client, wraps: &[Event], relays: &[RelayUrl]) {
connect_relays(client, relays).await;
for wrap in wraps {
publish_wrap(client, wrap, relays).await;
}
}
/// Bring the community's relays into the pool before anything is sent through them.
pub(crate) async fn connect_relays(client: &Client, relays: &[RelayUrl]) {
for url in relays {
if let Err(error) = client.add_relay(url).and_connect().await {
log::warn!("community: failed to add relay {url}: {error}");
}
}
}
/// Best-effort publication of a single wrap to the community's relays.
pub(crate) async fn publish_wrap(client: &Client, wrap: &Event, relays: &[RelayUrl]) {
let sent = if relays.is_empty() {
client.send_event(wrap).broadcast().await
} else {
client.send_event(wrap).to(relays.iter().cloned()).await
};
match sent {
Ok(output) if output.failed.is_empty() => {}
Ok(output) => log::warn!(
"community: {} relay(s) rejected {}",
output.failed.len(),
wrap.id
),
Err(error) => log::warn!("community: publishing {} failed: {error}", wrap.id),
}
}
async fn record_membership<S>(
client: &Client,
signer: &S,
state: &CommunityState,
name: &str,
) -> Result<()>
where
S: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44 + ?Sized,
{
let self_pk = signer.get_public_key_async().await?;
let held = load_list(client, signer, self_pk).await?;
let frags = held.as_ref().map_or(1, |list| list.frags);
if frags > 1 {
log::warn!(
"community {}: the list spans {frags} fragments; deferring the membership write",
state.id.to_hex()
);
return Ok(());
}
let entry = list_entry(state, name);
let list = match held {
Some(held) => held.joined(entry),
None => CommunityList::default().joined(entry),
};
let previous = newest_fragment_at(client, self_pk).await?;
let now = Timestamp::now().as_secs();
let at_secs = previous.map_or(now, |previous| now.max(previous.as_secs() + 1));
let event = cord02::list::build_list_event(signer, &list, 0, at_secs).await?;
publish_list(client, &event).await;
Ok(())
}
async fn publish_list(client: &Client, event: &Event) {
match client.send_event(event).to_nip65().await {
Ok(output) if output.failed.is_empty() => {}
Ok(output) => log::warn!(
"community list: {} relay(s) rejected the publish",
output.failed.len()
),
Err(error) => log::warn!("community list: publish failed: {error}"),
}
}
/// The newest `created_at` the account holds across its list fragments.
async fn newest_fragment_at(client: &Client, self_pk: PublicKey) -> Result<Option<Timestamp>> {
Ok(newest_fragments(client, self_pk)
.await?
.into_values()
.map(|event| event.created_at)
.max())
}
/// The subscription id carrying the account's own Community List.
pub const LIST_SUBSCRIPTION: &str = "concord/list";
pub fn list_subscription_id() -> SubscriptionId {
SubscriptionId::new(LIST_SUBSCRIPTION)
}
pub fn is_list_subscription(id: &SubscriptionId) -> bool {
id.as_str() == LIST_SUBSCRIPTION
}
/// Subscribes to the account's community list.
pub async fn subscribe_list(client: &Client, self_pk: PublicKey) -> Result<()> {
let id = list_subscription_id();
client.unsubscribe(&id).await?;
let filter = Filter::new()
.kind(Kind::Custom(KIND_COMMUNITY_LIST))
.author(self_pk);
let output = client
.subscribe(ReqTarget::auto(vec![filter]))
.with_id(id)
.await?;
if !output.failed.is_empty() {
log::warn!(
"community list: {} relay(s) rejected the subscription",
output.failed.len()
);
}
Ok(())
}
/// Discovers the current account's communities: every live membership the List
/// carries, plus any locally-held membership the List does not mention.
///
/// A held membership is dropped only when the List carries a tombstone at least
/// as new as it, because absence from the List is never a fact (§8).
pub async fn load(
client: &Client,
signer: &UniversalSigner,
self_pk: PublicKey,
) -> Result<Vec<CommunityState>> {
let list = match load_list(client, signer, self_pk).await? {
Some(list) => list,
None => return cache::load_states(client).await,
};
let mut held: BTreeMap<CommunityId, CommunityState> = cache::load_states(client)
.await?
.into_iter()
.map(|state| (state.id, state))
.collect();
held.retain(|id, state| !retired(&list, id, state.added_at_ms));
for entry in &list.entries {
if !list.is_live(&entry.community_id) {
continue;
}
let fresh = match CommunityState::from_join_material(&entry.current, entry.added_at) {
Ok(fresh) => fresh,
Err(error) => {
log::warn!(
"ignoring unreadable community {} from the list: {error}",
entry.community_id.to_hex()
);
continue;
}
};
let mut state = match held.remove(&entry.community_id) {
Some(materialized) => refresh(materialized, fresh),
None => fresh,
};
retain_join_root(&mut state, &entry.seed);
cache::save_state(client, &state).await?;
held.insert(entry.community_id, state);
}
Ok(held.into_values().collect())
}
fn retain_join_root(state: &mut CommunityState, seed: &JoinMaterial) {
if seed.root_epoch >= state.root_epoch {
return;
}
let Ok(key) = decode_hex_32(&seed.community_root) else {
return;
};
retain_root(state, seed.root_epoch, key, seed.control_pk);
}
/// Retain a root the community has rotated past, newest first.
fn retain_root(
state: &mut CommunityState,
epoch: Epoch,
key: [u8; 32],
control_pk: Option<PublicKey>,
) {
if state.held_roots.iter().any(|root| root.epoch == epoch) {
return;
}
let at = state
.held_roots
.iter()
.position(|root| root.epoch < epoch)
.unwrap_or(state.held_roots.len());
state.held_roots.insert(
at,
HeldRoot {
epoch,
key,
control_pk,
retired_at: None,
},
);
}
fn retired(list: &CommunityList, id: &CommunityId, added_at_ms: u64) -> bool {
list.tombstones
.iter()
.find(|tombstone| tombstone.community_id == *id)
.is_some_and(|tombstone| tombstone.removed_at >= added_at_ms)
}
fn refresh(mut held: CommunityState, fresh: CommunityState) -> CommunityState {
if fresh.root_epoch > held.root_epoch {
let (epoch, key) = (held.root_epoch, held.community_root);
let control_pk = held.control_pks.get(&epoch.0).copied();
retain_root(&mut held, epoch, key, control_pk);
}
held.owner = fresh.owner;
held.owner_salt = fresh.owner_salt;
held.community_root = fresh.community_root;
held.root_epoch = fresh.root_epoch;
held.added_at_ms = fresh.added_at_ms;
if fresh.control_root.is_some() {
held.control_root = fresh.control_root;
}
if let Some(name) = fresh.name {
held.name = Some(name);
}
for (epoch, address) in fresh.control_pks {
held.control_pks.insert(epoch, address);
}
held.relays = fresh.relays;
for channel in fresh.channels {
let cut = held.channel_cuts.get(&channel.id).copied();
match held.channels.iter_mut().find(|held| held.id == channel.id) {
Some(held) => {
held.name = channel.name;
if cut.is_some_and(|cut| channel.epoch <= cut) {
continue;
}
if channel.private {
held.private = true;
if let Some(key) = channel.key
&& (held.key != Some(key) || held.epoch != channel.epoch)
{
// The key being superseded still reads everything
// written under it, so it is retained, never overwritten.
if let Some((epoch, previous)) = held.current()
&& !held.priors.iter().any(|prior| prior.epoch == epoch)
{
held.priors.push(HeldKey {
epoch,
key: previous,
retired_at: None,
});
}
held.key = Some(key);
held.epoch = channel.epoch;
}
}
}
None => {
if cut.is_none() {
held.channels.push(channel);
}
}
}
}
held
}
/// The newest held copy of each fragment, keyed by its `d` index.
async fn newest_fragments(client: &Client, self_pk: PublicKey) -> Result<BTreeMap<u64, Event>> {
let filter = Filter::new()
.kind(Kind::Custom(KIND_COMMUNITY_LIST))
.author(self_pk);
let mut newest: BTreeMap<u64, Event> = BTreeMap::new();
for event in client.database().query(filter).await? {
let Ok(index) = cord02::list::fragment_index(&event) else {
continue;
};
match newest.get(&index) {
Some(existing) if existing.created_at >= event.created_at => {}
_ => {
newest.insert(index, event);
}
}
}
Ok(newest)
}
/// Every fragment of the account's list in the local database, merged.
async fn load_list<S>(
client: &Client,
signer: &S,
self_pk: PublicKey,
) -> Result<Option<CommunityList>>
where
S: AsyncGetPublicKey + AsyncNip44 + ?Sized,
{
let mut merged: Option<CommunityList> = None;
for event in newest_fragments(client, self_pk).await?.into_values() {
match cord02::list::parse_list_event(signer, &event).await {
Ok(list) => {
merged = Some(match merged {
Some(held) => cord02::list::merge(held, list),
None => list,
});
}
Err(error) => {
log::warn!("ignoring unreadable community list {}: {error}", event.id);
}
}
}
Ok(merged)
}
/// Rebuilds a community from the wraps already in the local database.
pub async fn fold(client: &Client, state: &CommunityState) -> Result<Option<Snapshot>> {
let planes = planes(state)?;
if planes.is_empty() {
return Ok(None);
}
let wraps = client.database().query(plane_filter(&planes)).await?;
let mut editions = Vec::new();
let mut observed: BTreeMap<PublicKey, u64> = BTreeMap::new();
let mut guestbook_rumors = Vec::new();
let mut unreadable: BTreeMap<ChannelId, usize> = BTreeMap::new();
let mut cached: BTreeMap<ChannelId, BTreeMap<EventId, Observed>> = BTreeMap::new();
for plane in &planes {
if let PlaneKind::Channel(channel, _) = plane.kind
&& !cached.contains_key(&channel)
{
cached.insert(channel, cache::wrapper_index(client, &channel).await?);
}
}
for wrap in &wraps {
let Some(plane) = planes.iter().find(|plane| plane.address == wrap.pubkey) else {
continue;
};
match plane.kind {
PlaneKind::Control(_) => {
// A retired root reads only what was sealed before its rotation.
if !plane.accepts(wrap) {
continue;
}
if let Ok(edition) = cord02::open_edition(wrap, &plane.group, &plane.address, true)
{
editions.push(edition);
}
}
PlaneKind::Guestbook => {
if !plane.accepts(wrap) {
continue;
}
if let Ok((_, rumor)) = cord02::guestbook::open(wrap, &plane.group) {
observe(&mut observed, rumor.author, rumor.at_ms);
guestbook_rumors.push(rumor);
}
}
PlaneKind::Channel(channel, epoch) => {
if let Some(row) = cached.get(&channel).and_then(|index| index.get(&wrap.id)) {
observe(&mut observed, row.author, row.at_ms);
continue;
}
let opened = if plane.accepts(wrap) {
concord::cord03::open(wrap, &plane.group, &channel, epoch).ok()
} else {
None
};
match opened {
Some((opened, rumor)) => {
cache::cache_rumor(client, &channel, &opened).await?;
observe(&mut observed, rumor.author, rumor.at_ms);
}
None => *unreadable.entry(channel).or_default() += 1,
}
}
}
}
if editions.is_empty() {
return Ok(None);
}
let control = cord02::fold_control(
&state.owner,
&state.id,
&editions,
&state.floors(),
&state.banned,
);
let granted: BTreeSet<PublicKey> = control
.roles
.grants()
.filter(|grant| !grant.role_ids.is_empty())
.map(|grant| grant.member)
.collect();
let floors = state.floors();
let can_kick = |actor: &PublicKey, target: &PublicKey, citation: Option<&AuthorityCitation>| {
citation_ok(&state.owner, &state.id, actor, citation, &floors)
&& control
.roles
.can_act_on_member(actor, &state.owner, target, Permissions::KICK)
};
let now_ms = Timestamp::now().as_secs().saturating_mul(1000);
let coalesced =
cord02::guestbook::coalesce(&guestbook_rumors, now_ms, &state.refounders, can_kick);
let mut members = cord02::guestbook::complete_memberlist(
&coalesced,
&observed,
&granted,
&control.banned,
&BTreeMap::new(),
);
// The roster has no grant for the owner, so membership is stated here.
members.insert(state.owner);
let mut state = state.clone();
state.apply_fold(&control);
cache::save_state(client, &state).await?;
Ok(Some(Snapshot {
state,
control,
members,
unreadable,
}))
}
fn observe(observed: &mut BTreeMap<PublicKey, u64>, author: PublicKey, at_ms: u64) {
observed
.entry(author)
.and_modify(|seen| *seen = (*seen).max(at_ms))
.or_insert(at_ms);
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use nostr_memory::MemoryDatabase;
use super::*;
fn client() -> Client {
ClientBuilder::default()
.database(MemoryDatabase::unbounded())
.build()
}
#[test]
fn planes_address_the_control_guestbook_and_every_readable_channel() {
let owner = Keys::generate().public_key();
let control_pk = Keys::generate().public_key();
let general = ChannelId::from_bytes([0x9c; 32]);
let staff = ChannelId::from_bytes([0x9d; 32]);
let state = CommunityState {
id: CommunityId::from_bytes([0x42; 32]),
name: Some("Anime and Manga".to_owned()),
owner,
owner_salt: [0x01; 32],
community_root: [0x02; 32],
root_epoch: Epoch(0),
control_root: None,
control_pks: BTreeMap::from([(0, control_pk)]),
channels: vec![
concord::state::ChannelKeyRef {
id: general,
name: "general".to_owned(),
private: false,
epoch: Epoch(0),
key: None,
priors: Vec::new(),
},
concord::state::ChannelKeyRef {
id: staff,
name: "staff".to_owned(),
private: true,
epoch: Epoch(0),
key: Some([0x04; 32]),
priors: Vec::new(),
},
concord::state::ChannelKeyRef {
id: ChannelId::from_bytes([0x9e; 32]),
name: "locked".to_owned(),
private: true,
epoch: Epoch(0),
key: None,
priors: Vec::new(),
},
],
relays: vec![RelayUrl::parse("wss://relay.example").expect("a url")],
heads: Vec::new(),
banned: BTreeSet::new(),
cursors: BTreeMap::new(),
held_roots: Vec::new(),
channel_cuts: BTreeMap::new(),
refounders: BTreeSet::new(),
removed_at: None,
stranded: false,
dissolved: false,
added_at_ms: 0,
};
let planes = planes(&state).expect("planes");
assert_eq!(planes.len(), 4);
assert!(planes.iter().any(|plane| plane.address == control_pk));
assert!(
planes
.iter()
.any(|plane| matches!(plane.kind, PlaneKind::Guestbook))
);
assert!(
planes
.iter()
.any(|plane| matches!(plane.kind, PlaneKind::Channel(id, _) if id == general))
);
assert!(
planes
.iter()
.any(|plane| matches!(plane.kind, PlaneKind::Channel(id, _) if id == staff)),
"a private channel whose key is held is subscribed"
);
let filter = plane_filter(&planes);
let addresses: BTreeSet<PublicKey> = planes.iter().map(|plane| plane.address).collect();
assert_eq!(filter.authors, Some(addresses));
assert_eq!(
filter.kinds,
Some(BTreeSet::from([
Kind::GiftWrap,
Kind::Custom(KIND_WRAP_EPHEMERAL)
])),
"the standing subscription asks for both wrap kinds"
);
}
/// A cold subscription asks wide; a warm one resumes at the oldest held
/// cursor, minus the overlap, so no channel's new region is skipped. A floor
/// the local clock has not reached is a peer's stamp and is clamped, or the
/// subscription would ask from a region that is still in the future.
#[test]
fn the_live_window_is_wide_cold_and_resumes_at_the_oldest_cursor_warm() {
let mut state = held(
CommunityId::from_bytes([0x42; 32]),
Keys::generate().public_key(),
);
let channel = state.channels[0].id;
let now = Timestamp::now();
assert_eq!(live_window(&state, now), Window::default());
state.cursors.insert(
channel,
concord::state::ChannelCursor {
newest: Some(Timestamp::from_secs(2_000_000)),
oldest: Some(Timestamp::from_secs(1_000)),
exhausted: false,
},
);
assert_eq!(
live_window(&state, now),
Window {
since: Some(Timestamp::from_secs(2_000_000) - CURSOR_OVERLAP),
until: None,
}
);
let ahead = now + Duration::from_secs(3_600);
state.cursors.insert(
channel,
concord::state::ChannelCursor {
newest: Some(ahead),
oldest: Some(Timestamp::from_secs(1_000)),
exhausted: false,
},
);
assert_eq!(
live_window(&state, now),
Window {
since: Some(now - CURSOR_OVERLAP),
until: None,
},
"a cursor stamped in the future must not open the REQ ahead of now"
);
}
fn metadata(name: &str) -> cord02::CommunityMetadata {
cord02::CommunityMetadata {
name: name.to_owned(),
..cord02::CommunityMetadata::default()
}
}
fn held(id: CommunityId, control_pk: PublicKey) -> CommunityState {
CommunityState {
id,
name: Some("Anime and Manga".to_owned()),
owner: Keys::generate().public_key(),
owner_salt: [0x01; 32],
community_root: [0x02; 32],
root_epoch: Epoch(0),
control_root: Some([0x03; 32]),
control_pks: BTreeMap::from([(0, control_pk)]),
channels: vec![concord::state::ChannelKeyRef {
id: ChannelId::from_bytes([0x9c; 32]),
name: "general".to_owned(),
private: false,
epoch: Epoch(0),
key: None,
priors: Vec::new(),
}],
relays: Vec::new(),
heads: Vec::new(),
banned: BTreeSet::new(),
cursors: BTreeMap::new(),
held_roots: Vec::new(),
channel_cuts: BTreeMap::new(),
refounders: BTreeSet::new(),
removed_at: None,
stranded: false,
dissolved: false,
added_at_ms: 1_700_000_000_000,
}
}
/// Puts a List in the database as the account's own fragment, exactly as a
/// relay would have delivered it.
async fn store_fragment<S>(client: &Client, signer: &S, list: &CommunityList)
where
S: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44 + ?Sized,
{
let event = cord02::list::build_list_event(signer, list, 0, 1_700_000_000)
.await
.expect("builds");
client.database().save_event(&event).await.expect("saves");
}
/// A Refounding snapshot seeds members this client has never seen publish —
/// but only on the authority of the npub whose rotation minted the epoch it
/// seeds (CORD-02 §5), which is why the refounder is recorded at all.
#[test]
fn a_refounders_snapshot_seeds_the_members_it_names() {
smol::block_on(async {
let client = client();
let keys = Keys::generate();
let signer = UniversalSigner::new(keys.clone());
let refounder = Keys::generate();
let quiet = Keys::generate().public_key();
let created = create(&client, &signer, &metadata("coop"))
.await
.expect("creates");
// Past genesis, and past the rotation whose refounder this client
// verified: genesis mints no epoch by rotation, so it has no
// snapshot authority at all.
let mut state = created;
state.root_epoch = Epoch(1);
state.refounders.insert(refounder.public_key());
let group = guestbook_group_key(&state.community_root, &state.id, state.root_epoch)
.expect("a guestbook plane");
let chunks = cord02::guestbook::build_snapshot_chunks(
refounder.public_key(),
&[quiet],
[0x77u8; 32],
state.added_at_ms,
);
for chunk in &chunks {
let (wrap, _) = cord02::guestbook::seal_rumor(chunk, &group, &refounder)
.await
.expect("seals");
client.database().save_event(&wrap).await.expect("saves");
}
let seeded = fold(&client, &state)
.await
.expect("folds")
.expect("a control plane");
assert!(
seeded.members.contains(&quiet),
"the snapshot's members are the roster"
);
// Without the rotation that minted the epoch, the same chunks seed
// nobody: an unverifiable snapshot is not an authority.
let mut unverified = state.clone();
unverified.refounders.clear();
let folded = fold(&client, &unverified)
.await
.expect("folds")
.expect("a control plane");
assert!(!folded.members.contains(&quiet));
});
}
/// A List that has rotated on keeps the root of our join: the material is
/// the community as we were given it, and the planes that root addressed
/// stay readable only while it is held.
#[test]
fn a_list_that_moved_the_root_on_retains_the_root_of_our_join() {
smol::block_on(async {
let client = client();
let keys = Keys::generate();
let signer = UniversalSigner::new(keys.clone());
let joined = held(
CommunityId::from_bytes([0x42; 32]),
Keys::generate().public_key(),
);
let mut rotated = joined.clone();
rotated.root_epoch = Epoch(2);
rotated.community_root = [0x44; 32];
// The entry as a community that rotated twice since would carry it:
// the join's material as the seed, the current material as current.
let mut entry = list_entry(&rotated, "coop");
entry.seed = list_entry(&joined, "coop").seed;
entry.added_at = joined.added_at_ms;
let list = CommunityList::default().joined(entry);
store_fragment(&client, &signer, &list).await;
let loaded = load(&client, &signer, keys.public_key())
.await
.expect("loads");
let state = loaded
.iter()
.find(|state| state.id == joined.id)
.expect("loaded");
assert_eq!(state.root_epoch, Epoch(2));
assert_eq!(state.community_root, [0x44; 32]);
assert_eq!(
state
.held_roots
.iter()
.map(|root| (root.epoch, root.key))
.collect::<Vec<_>>(),
vec![(joined.root_epoch, joined.community_root)]
);
});
}
/// The List discovering a root moves the community on without dropping the
/// one held: only the current root is exchanged for the material's.
#[test]
fn a_list_material_at_a_newer_epoch_keeps_the_root_it_superseded() {
let held_state = held(
CommunityId::from_bytes([0x42; 32]),
Keys::generate().public_key(),
);
let mut fresh = held_state.clone();
fresh.root_epoch = Epoch(3);
fresh.community_root = [0x55; 32];
let merged = refresh(held_state.clone(), fresh);
assert_eq!(merged.root_epoch, Epoch(3));
assert_eq!(merged.community_root, [0x55; 32]);
assert_eq!(
merged
.held_roots
.iter()
.map(|root| (root.epoch, root.key))
.collect::<Vec<_>>(),
vec![(held_state.root_epoch, held_state.community_root)]
);
}
/// What `CommunityRegistry` needs from a created community: a state document
/// `load` finds, a control plane the subscription filter actually addresses,
/// and a fold that survives an inbound control edit.
#[test]
fn creating_a_community_persists_a_state_that_subscribes_and_folds() {
smol::block_on(async {
let client = client();
let keys = Keys::generate();
let signer = UniversalSigner::new(keys.clone());
let created = create(&client, &signer, &metadata("coop"))
.await
.expect("creates");
let loaded = load(&client, &signer, keys.public_key())
.await
.expect("loads");
assert_eq!(loaded, vec![created.clone()]);
// The plane filter must address the genesis wraps, or a fold would
// read a plane nothing is ever published on.
let planes = planes(&created).expect("planes");
let wraps = client
.database()
.query(plane_filter(&planes))
.await
.expect("queries");
assert_eq!(wraps.len(), created.heads.len());
assert!(wraps.iter().all(|wrap| wrap.kind == Kind::GiftWrap));
let snapshot = fold(&client, &created)
.await
.expect("folds")
.expect("a control plane");
assert_eq!(snapshot.state.channels.len(), 1);
assert_eq!(snapshot.members, BTreeSet::from([keys.public_key()]));
assert_eq!(
snapshot
.control
.community
.as_ref()
.map(|metadata| metadata.name.as_str()),
Some("coop")
);
// An inbound control edit made by the owner folds over the created state.
let community_head = created
.heads
.iter()
.find(|head| head.entity == *created.id.as_bytes())
.expect("a community head");
let writer = cord02::ControlWriter {
author: created.owner,
read: control_group_key(&created.community_root, &created.id, cord02::ROOT_EPOCH)
.expect("a reading key"),
signer: control_signer_group_key(
&created.control_root.expect("a control root"),
&created.id,
cord02::ROOT_EPOCH,
)
.expect("a signing key"),
};
let (wrap, _) = writer
.set_community_metadata(
&keys,
&created.id,
&metadata("coop two"),
Some(community_head),
None,
Timestamp::now().as_secs() + 1,
)
.await
.expect("publishes");
client.database().save_event(&wrap).await.expect("saves");
let updated = fold(&client, &created)
.await
.expect("folds")
.expect("a control plane");
assert_eq!(
updated
.control
.community
.as_ref()
.map(|metadata| metadata.name.as_str()),
Some("coop two")
);
});
}
/// The other half of `create`: the membership must reach the account's
/// Community List, and a later create must union into it rather than replace
/// it (CORD-02 §8 read-modify-write).
#[test]
fn creating_a_community_records_the_membership_in_the_list() {
smol::block_on(async {
let client = client();
let keys = Keys::generate();
let signer = UniversalSigner::new(keys.clone());
let self_pk = keys.public_key();
let created = create(&client, &signer, &metadata("coop"))
.await
.expect("creates");
let list = load_list(&client, &signer, self_pk)
.await
.expect("reads")
.expect("a list");
assert_eq!(list.frags, 1);
assert!(list.is_complete([0]), "the whole list is one fragment");
assert!(list.is_live(&created.id));
let entry = list
.entries
.iter()
.find(|entry| entry.community_id == created.id)
.expect("the membership");
assert_eq!(
entry.seed, entry.current,
"a fresh membership has one anchor"
);
assert_eq!(entry.current.name, "coop");
assert_eq!(entry.current.owner, created.owner);
assert_eq!(entry.current.root_epoch, created.root_epoch);
assert_eq!(
entry.current.control_pk,
created.control_pks.get(&0).copied()
);
assert!(
entry.current.control_root.is_some(),
"the owner holds the control root"
);
assert_eq!(entry.current.channels.len(), created.channels.len());
assert_eq!(entry.added_at, created.added_at_ms);
// A second create unions into the same document: the first
// membership survives and both are live.
let second = create(&client, &signer, &metadata("second"))
.await
.expect("creates");
let grown = load_list(&client, &signer, self_pk)
.await
.expect("reads")
.expect("a list");
assert!(grown.is_live(&created.id));
assert!(grown.is_live(&second.id));
assert_eq!(grown.entries.len(), 2);
});
}
/// The discovery fix: a membership the List carries is materialized even when
/// no state document has ever been written for it.
#[test]
fn load_materializes_a_membership_the_list_carries_with_no_state_document() {
smol::block_on(async {
let client = client();
let keys = Keys::generate();
let signer = UniversalSigner::new(keys.clone());
let listed = held(
CommunityId::from_bytes([0x42; 32]),
Keys::generate().public_key(),
);
let list = CommunityList::default().joined(list_entry(&listed, "coop"));
store_fragment(&client, &signer, &list).await;
assert!(
cache::load_state(&client, &listed.id)
.await
.expect("reads")
.is_none()
);
let loaded = load(&client, &signer, keys.public_key())
.await
.expect("loads");
let materialized = loaded
.iter()
.find(|state| state.id == listed.id)
.expect("the list materializes the community");
assert_eq!(materialized.owner, listed.owner);
assert_eq!(materialized.community_root, listed.community_root);
assert_eq!(materialized.control_root, listed.control_root);
assert_eq!(materialized.control_pks, listed.control_pks);
assert_eq!(materialized.channels, listed.channels);
assert_eq!(materialized.added_at_ms, listed.added_at_ms);
// Discovery writes the document, so the next load is warm.
assert_eq!(
cache::load_state(&client, &listed.id)
.await
.expect("reads")
.map(|state| state.id),
Some(listed.id)
);
});
}
/// Absence from the List is never a fact (§8): a held membership the List
/// does not mention survives alongside the one it does.
#[test]
fn load_keeps_a_local_membership_the_list_does_not_mention() {
smol::block_on(async {
let client = client();
let keys = Keys::generate();
let signer = UniversalSigner::new(keys.clone());
let listed = held(
CommunityId::from_bytes([0x42; 32]),
Keys::generate().public_key(),
);
let local = held(
CommunityId::from_bytes([0x43; 32]),
Keys::generate().public_key(),
);
let list = CommunityList::default().joined(list_entry(&listed, "listed"));
store_fragment(&client, &signer, &list).await;
cache::save_state(&client, &local).await.expect("saves");
let loaded = load(&client, &signer, keys.public_key())
.await
.expect("loads");
let ids: BTreeSet<CommunityId> = loaded.iter().map(|state| state.id).collect();
assert!(ids.contains(&listed.id));
assert!(ids.contains(&local.id));
});
}
/// Only a tombstone subtracts a membership (§8).
#[test]
fn a_tombstone_drops_a_held_membership() {
smol::block_on(async {
let client = client();
let keys = Keys::generate();
let signer = UniversalSigner::new(keys.clone());
let local = held(
CommunityId::from_bytes([0x42; 32]),
Keys::generate().public_key(),
);
cache::save_state(&client, &local).await.expect("saves");
let list = CommunityList::default().tombstoned(local.id, u64::MAX);
store_fragment(&client, &signer, &list).await;
let loaded = load(&client, &signer, keys.public_key())
.await
.expect("loads");
assert!(loaded.iter().all(|state| state.id != local.id));
});
}
/// The `concord/list` id must not be read as a community's subscription, or
/// every list event would refresh a community instead of triggering `load`.
#[test]
fn the_list_subscription_is_not_read_as_a_community_subscription() {
let id = CommunityId::from_bytes([0x42; 32]);
assert!(is_list_subscription(&list_subscription_id()));
assert!(community_of(&list_subscription_id()).is_none());
assert_eq!(community_of(&subscription_id(&id)), Some(id));
}
/// A relay answers a longer REQ with `invalid subscription id length` but
/// `subscribe` still reports success, so an over-long id fails silently.
#[test]
fn a_community_subscription_id_fits_the_nip01_cap() {
let id = CommunityId::from_bytes([0x42; 32]);
assert!(subscription_id(&id).as_str().len() <= 64);
}
fn event_at(at_ms: u64) -> Event {
EventBuilder::new(Kind::TextNote, "wrap")
.custom_created_at(Timestamp::from_secs(at_ms / 1000))
.finalize(&Keys::generate())
.expect("signs")
}
/// A key a rotation stepped off still reads its own history, but nothing
/// sealed after the rotation published it.
#[test]
fn a_superseded_key_reads_only_up_to_the_rotation_that_retired_it() {
let channel = ChannelId::from_bytes([0x9c; 32]);
let mut state = held(
CommunityId::from_bytes([0x42; 32]),
Keys::generate().public_key(),
);
state.channels = vec![concord::state::ChannelKeyRef {
id: channel,
name: "staff".to_owned(),
private: true,
epoch: Epoch(1),
key: Some([0x08; 32]),
priors: vec![HeldKey {
epoch: Epoch(0),
key: [0x07; 32],
retired_at: Some(Timestamp::from_secs(1_000)),
}],
}];
let planes = planes(&state).expect("planes");
let retired = planes
.iter()
.find(|plane| plane.kind == PlaneKind::Channel(channel, Epoch(0)))
.expect("the retired epoch keeps a plane");
assert_eq!(retired.retired_at, Some(Timestamp::from_secs(1_000)));
assert!(retired.accepts(&event_at(1_000_000)));
assert!(!retired.accepts(&event_at(1_001_000)));
}
/// A public channel reads one plane per held root epoch, and the plane for
/// the CURRENT root is the one the community writes to now.
///
/// A Refounding moves the community root to a new epoch and every public
/// channel's plane with it (CORD-03 §1: a public channel's secret is the
/// community root, at the root epoch). Material the channel carries from
/// before the rotation names the old epoch, so a reader that trusts it asks
/// a retired address forever: the channel shows everything written before
/// the rotation and nothing after, while the control and guestbook planes —
/// which derive from the roots directly — stay current.
#[test]
fn a_refounding_moves_a_public_channels_plane_to_the_new_root_epoch() {
smol::block_on(async {
let client = client();
let keys = Keys::generate();
let signer = UniversalSigner::new(keys.clone());
let mut state = create(&client, &signer, &metadata("coop"))
.await
.expect("creates");
let channel = state.channels[0].id;
let rotated_at = Timestamp::now();
let prior_root = state.community_root;
// A Refounding: the root moves to epoch 1, the prior one is held for
// history, and the channel's own material still names epoch 0.
state.community_root = [0x5a; 32];
state.root_epoch = Epoch(1);
state.held_roots = vec![HeldRoot {
epoch: Epoch(0),
key: prior_root,
control_pk: None,
retired_at: Some(rotated_at),
}];
assert_eq!(state.channels[0].epoch, Epoch(0));
let current = channel_group_key(&state.community_root, &channel, state.root_epoch)
.expect("derives");
let plane = planes(&state)
.expect("planes")
.into_iter()
.find(|plane| plane.address == current.pk())
.expect("the current root's channel plane is subscribed and read");
assert_eq!(plane.kind, PlaneKind::Channel(channel, Epoch(1)));
assert!(
plane.accepts(
&EventBuilder::new(Kind::GiftWrap, "")
.finalize(plane.group.keys())
.expect("signs")
)
);
// And a message sealed at the current plane folds into the channel,
// which is what the timeline reads.
let rumor = concord::cord03::build_message(
keys.public_key(),
&channel,
state.root_epoch,
"after the refounding",
None,
rotated_at.as_secs().saturating_mul(1000),
None,
);
let (wrap, _) = concord::cord03::seal_rumor(&rumor, &plane.group, &signer, false)
.await
.expect("seals");
client.database().save_event(&wrap).await.expect("saves");
let snapshot = fold(&client, &state)
.await
.expect("folds")
.expect("a control plane");
assert_eq!(snapshot.unreadable.get(&channel).copied(), None);
let cached = cache::query_rumors(
&client,
&channel,
None,
10,
Some(&concord::cord03::ROW_KINDS),
)
.await
.expect("reads");
assert_eq!(cached.len(), 1, "a message at the new epoch reads back");
});
}
/// A wrap a subscription delivered lands in the database and nowhere else.
/// The fold is what turns it into a cached rumor, which is the only thing
/// the timeline reads, so a live message depends on this step.
#[test]
fn a_fold_caches_a_channel_wrap_a_relay_delivered() {
smol::block_on(async {
let client = client();
let keys = Keys::generate();
let signer = UniversalSigner::new(keys.clone());
let created = create(&client, &signer, &metadata("coop"))
.await
.expect("creates");
let (channel, epoch, plane) = planes(&created)
.expect("planes")
.into_iter()
.find_map(|plane| match plane.kind {
PlaneKind::Channel(channel, epoch) => Some((channel, epoch, plane)),
_ => None,
})
.expect("a channel plane");
let rumor = concord::cord03::build_message(
keys.public_key(),
&channel,
epoch,
"a live message",
None,
1_700_000_000_000,
None,
);
let (wrap, _) = concord::cord03::seal_rumor(&rumor, &plane.group, &signer, false)
.await
.expect("seals");
// What the SDK does with a wrap a standing subscription delivered.
client.database().save_event(&wrap).await.expect("saves");
let snapshot = fold(&client, &created)
.await
.expect("folds")
.expect("a control plane");
assert_eq!(snapshot.unreadable.get(&channel).copied(), None);
let cached = cache::query_rumors(
&client,
&channel,
None,
10,
Some(&concord::cord03::ROW_KINDS),
)
.await
.expect("reads");
assert_eq!(cached.len(), 1, "the delivered wrap is cached as a rumor");
assert_eq!(cached[0].pubkey, keys.public_key());
});
}
/// A wrap addressed to a held channel plane that will not open is counted, so
/// the panel can tell a quiet room from one whose history it cannot read.
#[test]
fn a_fold_counts_the_channel_wraps_it_cannot_open() {
smol::block_on(async {
let client = client();
let keys = Keys::generate();
let signer = UniversalSigner::new(keys.clone());
let created = create(&client, &signer, &metadata("coop"))
.await
.expect("creates");
let (channel, plane) = planes(&created)
.expect("planes")
.into_iter()
.find_map(|plane| match plane.kind {
PlaneKind::Channel(channel, _) => Some((channel, plane)),
_ => None,
})
.expect("a channel plane");
// Sealed to the channel's own address, but not a seal at all, so no
// held key opens it.
let junk = EventBuilder::new(Kind::GiftWrap, "not a seal")
.custom_created_at(Timestamp::from_secs(1_700_000_000))
.finalize(plane.group.keys())
.expect("signs");
client.database().save_event(&junk).await.expect("saves");
let snapshot = fold(&client, &created)
.await
.expect("folds")
.expect("a control plane");
assert_eq!(snapshot.unreadable.get(&channel).copied(), Some(1));
});
}
}