This commit is contained in:
2026-09-22 20:15:15 +07:00
parent 1c5ef58049
commit 706cc72c1f
14 changed files with 1021 additions and 104 deletions
+369 -9
View File
@@ -3,7 +3,7 @@ use std::path::PathBuf;
use anyhow::{Context, Result};
use concord::cord01::KIND_WRAP_EPHEMERAL;
use concord::cord02::list::{CommunityList, KIND_COMMUNITY_LIST};
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};
@@ -11,7 +11,7 @@ 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};
use concord::{ChannelId, CommunityId, Epoch, GroupKey, decode_hex_32};
use gpui::AsyncApp;
use nostr_sdk::prelude::*;
use state::UniversalSigner;
@@ -137,7 +137,7 @@ pub fn live_filter(planes: &[Plane], window: Window) -> Filter {
}
/// The window a community's standing subscription opens with.
pub fn live_window(state: &CommunityState) -> Window {
pub fn live_window(state: &CommunityState, now: Timestamp) -> Window {
let floor = state
.channels
.iter()
@@ -146,7 +146,7 @@ pub fn live_window(state: &CommunityState) -> Window {
match floor {
Some(floor) => Window {
since: Some(floor - CURSOR_OVERLAP),
since: Some(floor.min(now) - CURSOR_OVERLAP),
until: None,
},
None => Window::default(),
@@ -382,11 +382,13 @@ pub async fn load(
}
};
let state = match held.remove(&entry.community_id) {
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);
}
@@ -394,6 +396,46 @@ pub async fn load(
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()
@@ -402,6 +444,12 @@ fn retired(list: &CommunityList, id: &CommunityId, added_at_ms: u64) -> bool {
}
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;
@@ -621,7 +669,8 @@ pub async fn fold(client: &Client, state: &CommunityState) -> Result<Option<Snap
};
let now_ms = Timestamp::now().as_secs().saturating_mul(1000);
let coalesced = cord02::guestbook::coalesce(&guestbook_rumors, now_ms, None, can_kick);
let coalesced =
cord02::guestbook::coalesce(&guestbook_rumors, now_ms, &state.refounders, can_kick);
let mut members = cord02::guestbook::complete_memberlist(
&coalesced,
@@ -655,6 +704,8 @@ fn observe(observed: &mut BTreeMap<PublicKey, u64>, author: PublicKey, at_ms: u6
#[cfg(test)]
mod tests {
use std::time::Duration;
use nostr_memory::MemoryDatabase;
use super::*;
@@ -713,6 +764,7 @@ mod tests {
cursors: BTreeMap::new(),
held_roots: Vec::new(),
channel_cuts: BTreeMap::new(),
refounders: BTreeSet::new(),
removed_at: None,
stranded: false,
dissolved: false,
@@ -754,7 +806,9 @@ mod tests {
}
/// 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.
/// 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(
@@ -762,8 +816,9 @@ mod tests {
Keys::generate().public_key(),
);
let channel = state.channels[0].id;
let now = Timestamp::now();
assert_eq!(live_window(&state), Window::default());
assert_eq!(live_window(&state, now), Window::default());
state.cursors.insert(
channel,
@@ -774,12 +829,30 @@ mod tests {
},
);
assert_eq!(
live_window(&state),
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 {
@@ -813,6 +886,7 @@ mod tests {
cursors: BTreeMap::new(),
held_roots: Vec::new(),
channel_cuts: BTreeMap::new(),
refounders: BTreeSet::new(),
removed_at: None,
stranded: false,
dissolved: false,
@@ -832,6 +906,141 @@ mod tests {
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.
@@ -1154,6 +1363,157 @@ mod tests {
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]