feat: add community ui #52
@@ -762,6 +762,17 @@ impl Community {
|
||||
let mut held = Vec::with_capacity(base.stepped.len() + self.state.held_roots.len());
|
||||
|
||||
for key in base.stepped {
|
||||
let known = self
|
||||
.state
|
||||
.held_roots
|
||||
.iter()
|
||||
.chain(held.iter())
|
||||
.any(|root| root.epoch == key.epoch && root.key == key.key);
|
||||
|
||||
if known {
|
||||
continue;
|
||||
}
|
||||
|
||||
held.push(HeldRoot {
|
||||
epoch: key.epoch,
|
||||
key: key.key,
|
||||
@@ -770,7 +781,17 @@ impl Community {
|
||||
});
|
||||
}
|
||||
|
||||
held.extend(self.state.held_roots.iter().copied());
|
||||
for root in &self.state.held_roots {
|
||||
if held
|
||||
.iter()
|
||||
.any(|kept| kept.epoch == root.epoch && kept.key == root.key)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
held.push(*root);
|
||||
}
|
||||
|
||||
self.state.held_roots = held;
|
||||
|
||||
if let Some(control_pk) = base.control_pk {
|
||||
|
||||
@@ -628,6 +628,11 @@ pub async fn fold(client: &Client, state: &CommunityState) -> Result<Option<Snap
|
||||
|
||||
if let Ok(edition) = cord02::open_edition(wrap, &plane.group, &plane.address, true)
|
||||
{
|
||||
observe(
|
||||
&mut observed,
|
||||
edition.author,
|
||||
wrap.created_at.as_secs().saturating_mul(1000),
|
||||
);
|
||||
editions.push(edition);
|
||||
}
|
||||
}
|
||||
@@ -1199,6 +1204,75 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
/// The reference counts anyone seen publishing anywhere, and a control edit
|
||||
/// is the one publication that leaves no other trace: an npub that has
|
||||
/// never joined a channel and never touched the Guestbook exists in no
|
||||
/// other plane, so a fold that reads only those reads them as a stranger.
|
||||
#[test]
|
||||
fn a_control_editions_author_is_observed_as_a_member() {
|
||||
smol::block_on(async {
|
||||
let client = client();
|
||||
let keys = Keys::generate();
|
||||
let signer = UniversalSigner::new(keys.clone());
|
||||
let editor = Keys::generate();
|
||||
|
||||
let created = create(&client, &signer, &metadata("coop"))
|
||||
.await
|
||||
.expect("creates");
|
||||
|
||||
let community_head = created
|
||||
.heads
|
||||
.iter()
|
||||
.find(|head| head.entity == *created.id.as_bytes())
|
||||
.expect("a community head");
|
||||
let writer = cord02::ControlWriter {
|
||||
author: editor.public_key(),
|
||||
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"),
|
||||
};
|
||||
|
||||
// The editor holds no grant, so the edit is inert: it supersedes
|
||||
// nothing. It is still a publication by that npub.
|
||||
let (wrap, _) = writer
|
||||
.set_community_metadata(
|
||||
&editor,
|
||||
&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 snapshot = fold(&client, &created)
|
||||
.await
|
||||
.expect("folds")
|
||||
.expect("a control plane");
|
||||
|
||||
assert!(
|
||||
snapshot.members.contains(&editor.public_key()),
|
||||
"an edition's author is a member the fold has seen publishing"
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.control
|
||||
.community
|
||||
.as_ref()
|
||||
.map(|metadata| metadata.name.as_str()),
|
||||
Some("coop"),
|
||||
"an unauthorized edit still changes nothing"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// 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).
|
||||
|
||||
@@ -1104,16 +1104,56 @@ Measured on that database after the fix: the same 3 wraps, no new fetch, roster
|
||||
own devices re-derive the authority from the rotation chunks §13b reads. Closing
|
||||
it is a state-schema change (epoch → minter) plus the write, and is left as its
|
||||
own step.
|
||||
- **247 against Armada's 263 is `observed`.** We merge authors seen publishing in
|
||||
the Guestbook and the channel planes; the reference merges anyone seen publishing
|
||||
anywhere, control-plane editions included. A control wrap proves only that its
|
||||
author held the plane's read key, so that stays out for now. The failure mode is a
|
||||
member the other client lists and we do not — never the reverse.
|
||||
- **`observed` — closed, and what it was worth.** We merged authors seen publishing
|
||||
in the Guestbook and the channel planes; the reference merges anyone seen
|
||||
publishing anywhere, and a control edition is the one publication that leaves no
|
||||
trace in either: a member who has only ever edited metadata exists in no other
|
||||
plane. The fold now observes an opened edition's author, dated by the wrap's own
|
||||
non-backdated stamp, because an opened edition carries no time of its own
|
||||
(CORD-01). What that proves is what the other planes prove — its publisher held
|
||||
the epoch's key, which only a member can derive.
|
||||
|
||||
Measured on the store that reported the short roster: **254 either way.** Every
|
||||
readable edition's author there had already been seen in another plane, so the
|
||||
parity is real and the number does not move with it. That fold accounts for every
|
||||
npub it can see: of the 79 that published anywhere readable, 3 are not in the
|
||||
roster and all 3 are gone — two left and one was kicked, each at or after its own
|
||||
last observation, which is the reference's forward-only rule. The community's two
|
||||
banned npubs are absent for a different reason this pass did not touch: the fold
|
||||
hands `complete_memberlist` no ban times, so a ban excludes unconditionally where
|
||||
the reference passes `bannedAt` and re-admits an npub whose activity is newer.
|
||||
- **What is left of 254 against the reference's 263 is evidence this client cannot
|
||||
reach, not evidence it drops.** The guestbook planes of epochs it no longer holds,
|
||||
a Control Plane whose signer address the document never named, and the reference's
|
||||
own Members tab — which builds a row for every npub its watchdog has flagged as
|
||||
well as for every member (`memberDirectory.ts`), so a number read off that tab is
|
||||
a row count rather than a roster size. Which of the three it is needs the other
|
||||
client's npub list; both snapshots this client does hold (240 and 209 members,
|
||||
both from `d133ecb0…`) are honored, and one single wrap on the whole store is
|
||||
left unreadable.
|
||||
- **An authority is still only as good as the rotations on disk.** A device that
|
||||
installed after every relevant chunk was dropped, whose List carries no refounder,
|
||||
reads an inferred roster until the next Refounding — §12c's honest miss, now with
|
||||
two more ways out of it.
|
||||
|
||||
#### 13f. The same root, held twice
|
||||
|
||||
A walk that starts from a *retained* root steps off it: `Adopted.stepped` carries
|
||||
one entry for the key it started from, and `merge_adoptions` prepended that to
|
||||
`held_roots` without asking whether the document already held it. So every
|
||||
adoption that began at a retained root added a second copy of an epoch's key — the
|
||||
store that reported the short roster held epoch 8 twice, once retired by the
|
||||
8 → 9 rotation and once with no cutoff at all.
|
||||
|
||||
A duplicate is not a deeper hold on history. `planes` dedupes by address, so the
|
||||
same plane is subscribed twice and read once, and `rekey::adopt` walks every
|
||||
retained root, so the doubled entry doubles that walk's work — its filters, its
|
||||
blob openings — on every adoption. The rebuild now skips a step whose epoch *and*
|
||||
key it already holds, and drops the copies a document already carried the next time
|
||||
that document adopts (until then a stored duplicate is harmless: `planes` dedupes
|
||||
its address, so it is subscribed once and read once). The channel `priors` loop
|
||||
beside it had always guarded its own.
|
||||
|
||||
## Order of work
|
||||
|
||||
### Phase 2a — move the code (no behaviour change) — **landed**
|
||||
@@ -1353,6 +1393,12 @@ outstanding are the ones that need a GPUI harness or two live accounts.
|
||||
`cord02::list::tests::the_wire_round_trips_a_refounder_and_the_retained_roots`:
|
||||
both extensions survive the round trip, the accessors read them, and a retained
|
||||
root without a key is skipped rather than adopted.
|
||||
- A control edition's author — **landed in revision 6**
|
||||
(`sync::tests::a_control_editions_author_is_observed_as_a_member`): an npub
|
||||
holding no grant, whose edit supersedes nothing and changes no metadata, is in the
|
||||
roster all the same, because an opened edition is a publication and the reference
|
||||
counts anyone seen publishing. The same test pins the other half — an edition
|
||||
nobody authorized moves no head.
|
||||
- The page a rotated community's history sits on — the same retention is what
|
||||
`a_walk_pages_back_across_a_rekey` pages across, and revision 5 is what makes a
|
||||
*List*-learned refounding leave those keys held, not only a blob-learned one.
|
||||
|
||||
Reference in New Issue
Block a user