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
+1
View File
@@ -264,6 +264,7 @@ mod tests {
cursors: BTreeMap::new(),
held_roots: Vec::new(),
channel_cuts: BTreeMap::new(),
refounders: BTreeSet::new(),
removed_at: None,
stranded: false,
dissolved: false,
+73 -10
View File
@@ -298,8 +298,9 @@ impl Community {
return None;
}
held.current()
.or_else(|| (!held.private).then_some((held.epoch, self.state.community_root)))
held.current().or_else(|| {
(!held.private).then_some((self.state.root_epoch, self.state.community_root))
})
}
/// Every secret the client holds for a channel, newest epoch first.
@@ -354,12 +355,14 @@ impl Community {
let relays = self.state.relays.clone();
let pages = self.pages.clone();
let saved = self
.state
.cursors
.get(&channel)
.copied()
.unwrap_or_default();
let saved = clamped(
self.state
.cursors
.get(&channel)
.copied()
.unwrap_or_default(),
Timestamp::now(),
);
if let Some(round) = self.rounds.get_mut(&channel) {
round.running = true;
@@ -475,7 +478,8 @@ impl Community {
.get(&channel)
.copied()
.unwrap_or_default();
let merged = held.merge(cursor);
let merged = clamped(held.merge(cursor), Timestamp::now());
if merged == held {
return;
@@ -593,7 +597,7 @@ impl Community {
Some(cx.background_spawn(async move {
let group = channel_group_key(&secret, &channel, epoch)?;
let at_ms = Timestamp::now().as_secs().saturating_mul(1000);
let at_ms = now_ms()?;
let rumor = cord03::build_message(
author,
@@ -772,6 +776,10 @@ impl Community {
self.state.removed_at = None;
self.state.stranded = false;
// Who minted these epochs is only ever stated by the rotation itself,
// and a Guestbook snapshot is honored on that authority.
self.state.refounders.extend(base.refounders);
// Every channel's plane moved with the root.
touched.extend(self.state.channels.iter().map(|channel| channel.id));
}
@@ -968,6 +976,25 @@ impl Community {
/// One round's progress and the cursor material it earned.
type RoundOutcome = (Progress, ChannelCursor);
/// A cursor as a filter bound may use it.
fn clamped(cursor: ChannelCursor, now: Timestamp) -> ChannelCursor {
ChannelCursor {
newest: cursor.newest.map(|newest| newest.min(now)),
..cursor
}
}
/// The wall clock in epoch milliseconds: a rumor's `ms` tag carries the part of
/// the second the message was written in, and the fold orders rows by it.
fn now_ms() -> Result<u64> {
let elapsed = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|error| anyhow::anyhow!("the system clock is before the Unix epoch: {error}"))?;
u64::try_from(elapsed.as_millis())
.map_err(|error| anyhow::anyhow!("the system clock is out of range: {error}"))
}
/// Read a channel's history from the community's relays, in three passes.
async fn sync_round(
client: &Client,
@@ -1098,6 +1125,41 @@ fn absorb(progress: &mut Progress, page: &WrapPage) {
mod tests {
use super::*;
/// A stamp ahead of the local clock must never become a cursor.
///
/// A durable `newest` past `now` bounds every later REQ below a region that
/// has not happened yet, so the channel receives nothing at all — not live,
/// not by round — while its older history keeps working, which is exactly
/// what a peer with a fast clock (or a hostile stamp) would cause.
#[test]
fn a_future_stamp_cannot_push_a_cursor_past_now() {
let now = Timestamp::now();
let held = ChannelCursor {
newest: Some(now - Duration::from_secs(30)),
oldest: Some(now - Duration::from_secs(90)),
exhausted: false,
};
let round = ChannelCursor {
newest: Some(now + Duration::from_secs(86_400)),
oldest: None,
exhausted: true,
};
let merged = clamped(held.merge(round), now);
assert_eq!(merged.newest, Some(now), "the frontier stops at the clock");
assert_eq!(merged.oldest, Some(now - Duration::from_secs(90)));
assert!(merged.exhausted, "the round's other findings still land");
// A cursor already stored past `now` heals instead of staying deaf.
let poisoned = ChannelCursor {
newest: Some(now + Duration::from_secs(86_400)),
..ChannelCursor::default()
};
assert_eq!(clamped(poisoned, now).newest, Some(now));
}
/// One public channel under a held root, and nothing else.
fn state(channel: ChannelId) -> CommunityState {
CommunityState {
@@ -1123,6 +1185,7 @@ mod tests {
cursors: BTreeMap::new(),
held_roots: Vec::new(),
channel_cuts: BTreeMap::new(),
refounders: BTreeSet::new(),
removed_at: None,
stranded: false,
dissolved: false,
+11
View File
@@ -458,6 +458,11 @@ impl Walk {
}
}
// The walk's floor, which the round resumes the older pass from.
if let Some(oldest) = oldest {
self.oldest = Some(self.oldest.map_or(oldest, |held| held.min(oldest)));
}
match oldest {
Some(oldest) if !oldest.is_zero() => self.cursor = Some(oldest - 1u64),
Some(_) => self.bottom = true,
@@ -599,6 +604,12 @@ mod tests {
assert!(page.exhausted);
assert!(!page.failed);
assert_eq!(page.raw, 3);
assert_eq!(page.newest, Some(Timestamp::from_secs(1_700_000_002)));
assert_eq!(
page.oldest,
Some(Timestamp::from_secs(1_700_000_000)),
"the walk reports its floor, which the older pass resumes below"
);
}
/// A wrap that reaches us and still will not open is history we cannot read,
+47 -3
View File
@@ -26,6 +26,9 @@ pub use sync::*;
/// How long a burst of relay notifications is collected before it is folded.
const PUMP_WINDOW: Duration = Duration::from_millis(200);
/// How long a community's relays may deliver nothing before
/// its standing subscription is torn down and re-issued.
const LIVE_ROTATE: Duration = Duration::from_secs(90);
pub fn init(cx: &mut App) {
CommunityRegistry::set_global(cx.new(CommunityRegistry::new), cx);
@@ -139,6 +142,9 @@ pub struct CommunityRegistry {
index: HashMap<CommunityId, Entity<Community>>,
/// The plane set each community was last subscribed with
synced: HashMap<CommunityId, SubscriptionKey>,
/// When a relay last delivered something for a community,
/// which is the only evidence the standing subscription is alive.
last_event: HashMap<CommunityId, Instant>,
/// One observer per tracked community, dropped on reset
observers: HashMap<CommunityId, Subscription>,
signal_tx: flume::Sender<Signal>,
@@ -197,6 +203,7 @@ impl CommunityRegistry {
communities: Vec::new(),
index: HashMap::new(),
synced: HashMap::new(),
last_event: HashMap::new(),
observers: HashMap::new(),
signal_tx: tx,
signal_rx: rx,
@@ -300,6 +307,7 @@ impl CommunityRegistry {
self.communities.clear();
self.index.clear();
self.synced.clear();
self.last_event.clear();
cx.notify();
}
@@ -393,6 +401,7 @@ impl CommunityRegistry {
for id in dropped {
self.observers.remove(&id);
self.synced.remove(&id);
self.last_event.remove(&id);
}
self.communities = communities
@@ -428,11 +437,38 @@ impl CommunityRegistry {
community.update(cx, |community, cx| community.rekey(cx));
}
/// One scheduler pass: every community repairs itself if it has gone stale.
/// One scheduler pass: every community repairs itself if it has gone stale,
/// and any whose relays have gone quiet is re-subscribed.
fn tick(&mut self, cx: &mut Context<Self>) {
for community in self.communities.clone() {
community.update(cx, |community, cx| community.tick(cx));
}
self.rotate_quiet(cx);
}
/// Re-issue the standing subscription of every community that has been quiet for `LIVE_ROTATE`
fn rotate_quiet(&mut self, cx: &mut Context<Self>) {
let now = Instant::now();
let ids: Vec<CommunityId> = self.index.keys().copied().collect();
let mut rotated = false;
for id in ids {
let quiet = self
.last_event
.get(&id)
.is_none_or(|at| now.duration_since(*at) >= LIVE_ROTATE);
if !quiet {
continue;
}
self.synced.remove(&id);
rotated = true;
}
if rotated {
self.sync_subscriptions(cx);
}
}
/// Re-subscribe every community whose held planes moved.
@@ -464,9 +500,12 @@ impl CommunityRegistry {
};
let subscription = sync::subscription_id(&id);
let filter = sync::live_filter(&planes, sync::live_window(&state));
let filter = sync::live_filter(&planes, sync::live_window(&state, Timestamp::now()));
let relays = key.relays().to_vec();
// A fresh REQ counts as evidence of life for this community.
self.last_event.insert(id, Instant::now());
// The rekey watch is a second standing REQ over the same relays.
let watch = match rekey::watches(&state) {
Ok(watches) => rekey::watch_filter(&watches),
@@ -563,7 +602,12 @@ impl CommunityRegistry {
self.signal_consumer = Some(cx.spawn(async move |this, cx| {
while let Ok(signal) = rx.recv_async().await {
match signal {
Signal::Event(id) => this.update(cx, |this, cx| this.refresh(id, cx))?,
Signal::Event(id) => this.update(cx, |this, cx| {
// The only proof a community's subscription is still
// delivering anything.
this.last_event.insert(id, Instant::now());
this.refresh(id, cx);
})?,
Signal::Rekey(id) => this.update(cx, |this, cx| this.rekey(id, cx))?,
Signal::List => this.update(cx, |this, cx| this.load(cx))?,
}
+138 -13
View File
@@ -40,18 +40,25 @@ fn permissions(scope: RekeyScope) -> &'static [u64] {
}
/// Every address a community's rotations can arrive at.
///
/// The base scope watches the epoch after *every* held root, not only the
/// current one: a rotation published while this client was away is read from
/// the root it stepped off, and the npub that minted an epoch is only knowable
/// from the rotation that minted it.
pub fn watches(state: &CommunityState) -> Result<Vec<Watch>> {
let mut watches = Vec::new();
let roots = state.roots();
let next = Epoch(state.root_epoch.0 + 1);
let group = cord06::rekey_group(RekeyScope::Base, &state.community_root, &state.id, next)?;
watches.push(Watch {
address: group.pk(),
group,
scope: RekeyScope::Base,
epoch: next,
});
for root in &roots {
let next = Epoch(root.epoch.0 + 1);
let group = cord06::rekey_group(RekeyScope::Base, &root.key, &state.id, next)?;
watches.push(Watch {
address: group.pk(),
group,
scope: RekeyScope::Base,
epoch: next,
});
}
for channel in &state.channels {
if !channel.private {
@@ -153,6 +160,8 @@ pub struct BaseAdoption {
/// The new Control Plane signing root, delivered to staff only.
pub control_root: Option<[u8; 32]>,
pub stepped: Vec<HeldKey>,
/// The npubs whose rotations minted the epochs this walk passed through.
pub refounders: BTreeSet<PublicKey>,
}
#[derive(Debug, Clone)]
@@ -224,7 +233,7 @@ pub async fn adopt(
let rotations = cord06::collect_rotations(&chunks);
let mut adoptions = Adoptions::default();
let base = walk(
let mut base = walk(
RekeyScope::Base,
permissions(RekeyScope::Base),
state.root_epoch,
@@ -238,6 +247,42 @@ pub async fn adopt(
)
.await?;
let mut refounders: BTreeSet<PublicKey> = base
.adopted
.as_ref()
.map(|adopted| adopted.refounders.clone())
.unwrap_or_default();
for root in state.roots().into_iter().skip(1) {
let prior = walk(
RekeyScope::Base,
permissions(RekeyScope::Base),
root.epoch,
root.key,
state,
roles,
signer,
me,
&rotations,
&published,
)
.await?;
let Some(adopted) = prior.adopted else {
continue;
};
refounders.extend(adopted.refounders.iter().copied());
if base
.adopted
.as_ref()
.is_none_or(|held| adopted.epoch > held.epoch)
{
base.adopted = Some(adopted);
}
}
if let Some(step) = base.adopted {
adoptions.base = Some(BaseAdoption {
epoch: step.epoch,
@@ -245,6 +290,7 @@ pub async fn adopt(
control_pk: step.control_pk,
control_root: step.control_root,
stepped: step.stepped,
refounders,
});
}
@@ -552,6 +598,9 @@ struct Adopted {
control_pk: Option<PublicKey>,
control_root: Option<[u8; 32]>,
stepped: Vec<HeldKey>,
/// The rotator of every base epoch this walk passed through,
/// which is who may seed the Guestbook snapshot of the epoch it minted.
refounders: BTreeSet<PublicKey>,
}
/// What one rotation offered this client, and when it was published.
@@ -561,6 +610,7 @@ struct Delivery {
control_pk: Option<PublicKey>,
control_root: Option<[u8; 32]>,
at_ms: u64,
rotator: PublicKey,
}
/// Walk a scope's rotations forward, one epoch at a time, off the key held.
@@ -579,6 +629,7 @@ async fn walk(
) -> Result<Step> {
let mut step = Step::default();
let mut stepped: Vec<HeldKey> = Vec::new();
let mut refounders: BTreeSet<PublicKey> = BTreeSet::new();
let ceiling = held_epoch.0 + REKEY_LOOKAHEAD;
loop {
@@ -658,6 +709,7 @@ async fn walk(
control_pk,
control_root: delivered.control_root,
at_ms,
rotator: rotation.rotator,
});
} else if let Some(held) = delivery.as_mut() {
held.at_ms = held.at_ms.min(at_ms);
@@ -671,9 +723,6 @@ async fn walk(
HeldKey {
epoch: held_epoch,
key: held_key,
// The cutoff is a second-granular read boundary, so the
// rotation's millisecond publish time narrows to the second
// it landed in.
retired_at: Some(Timestamp::from_secs(delivered.at_ms / 1000)),
},
);
@@ -681,12 +730,17 @@ async fn walk(
held_epoch = target;
held_key = delivered.key;
if scope == RekeyScope::Base {
refounders.insert(delivered.rotator);
}
step.adopted = Some(Adopted {
epoch: target,
key: delivered.key,
control_pk: delivered.control_pk,
control_root: delivered.control_root,
stepped: stepped.clone(),
refounders: refounders.clone(),
});
continue;
@@ -739,7 +793,7 @@ mod tests {
base_rekey_group_key, channel_rekey_group_key, control_signer_group_key,
epoch_key_commitment,
};
use concord::state::ChannelKeyRef;
use concord::state::{ChannelKeyRef, HeldRoot};
use nostr_memory::MemoryDatabase;
use super::*;
@@ -747,6 +801,7 @@ mod tests {
const AT_MS: u64 = 1_700_000_000_000;
const ROOT: [u8; 32] = [0x55; 32];
const NEW_ROOT: [u8; 32] = [0x66; 32];
const NEWER_ROOT: [u8; 32] = [0x77; 32];
const CHANNEL_KEY: [u8; 32] = [0x07; 32];
const NEW_CHANNEL_KEY: [u8; 32] = [0x08; 32];
@@ -780,6 +835,7 @@ mod tests {
cursors: BTreeMap::new(),
held_roots: Vec::new(),
channel_cuts: BTreeMap::new(),
refounders: BTreeSet::new(),
removed_at: None,
stranded: false,
dissolved: false,
@@ -901,6 +957,75 @@ mod tests {
});
}
/// A complete base rotation off a retained root is adopted from it, which
/// is the only way its epoch's refounder is ever learned: a Guestbook
/// snapshot seeds members on that npub's authority alone (CORD-02 §5).
#[test]
fn a_rotation_past_a_retained_root_is_adopted_and_names_its_refounder() {
smol::block_on(async {
let client = client();
let owner = Keys::generate();
let me = Keys::generate();
let id = CommunityId::from_bytes([0x42; 32]);
let channel = ChannelId::from_bytes([0x9c; 32]);
// At epoch 1, with the root it stepped off retained.
let mut state = state(owner.public_key(), id, channel);
state.root_epoch = Epoch(1);
state.community_root = NEW_ROOT;
state.held_roots = vec![HeldRoot {
epoch: Epoch(0),
key: ROOT,
control_pk: None,
retired_at: None,
}];
let blob = smol::block_on(build_blob(
&owner,
&me.public_key(),
RekeyScope::Base,
Epoch(2),
&NEWER_ROOT,
None,
None,
))
.expect("builds");
let group = base_rekey_group_key(&NEW_ROOT, &id, Epoch(2)).expect("derives");
let wraps = smol::block_on(build_rekey_chunks(
&owner,
&group,
RekeyScope::Base,
Epoch(2),
Epoch(1),
&epoch_key_commitment(Epoch(1), &NEW_ROOT),
&[blob],
None,
false,
AT_MS / 1000,
))
.expect("builds");
store(&client, &wraps).await;
let signer = UniversalSigner::new(me.clone());
let adoptions = adopt(
&client,
&state,
&CommunityRoles::default(),
&signer,
me.public_key(),
)
.await
.expect("reads");
let base = adoptions.base.expect("adopted");
assert_eq!(base.epoch, Epoch(2));
assert_eq!(base.key, NEWER_ROOT);
assert_eq!(base.stepped.len(), 1, "the root it stepped off");
assert_eq!(base.stepped[0].epoch, Epoch(1));
assert!(base.refounders.contains(&owner.public_key()));
});
}
/// A rotation off a key we do not hold is a fork: adoptable by nobody here.
#[test]
fn a_rotation_that_does_not_extend_the_held_key_is_never_adopted() {
+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]