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());
|
let mut held = Vec::with_capacity(base.stepped.len() + self.state.held_roots.len());
|
||||||
|
|
||||||
for key in base.stepped {
|
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 {
|
held.push(HeldRoot {
|
||||||
epoch: key.epoch,
|
epoch: key.epoch,
|
||||||
key: key.key,
|
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;
|
self.state.held_roots = held;
|
||||||
|
|
||||||
if let Some(control_pk) = base.control_pk {
|
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)
|
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);
|
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
|
/// 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
|
/// Community List, and a later create must union into it rather than replace
|
||||||
/// it (CORD-02 §8 read-modify-write).
|
/// 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
|
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
|
it is a state-schema change (epoch → minter) plus the write, and is left as its
|
||||||
own step.
|
own step.
|
||||||
- **247 against Armada's 263 is `observed`.** We merge authors seen publishing in
|
- **`observed` — closed, and what it was worth.** We merged authors seen publishing
|
||||||
the Guestbook and the channel planes; the reference merges anyone seen publishing
|
in the Guestbook and the channel planes; the reference merges anyone seen
|
||||||
anywhere, control-plane editions included. A control wrap proves only that its
|
publishing anywhere, and a control edition is the one publication that leaves no
|
||||||
author held the plane's read key, so that stays out for now. The failure mode is a
|
trace in either: a member who has only ever edited metadata exists in no other
|
||||||
member the other client lists and we do not — never the reverse.
|
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
|
- **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,
|
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
|
reads an inferred roster until the next Refounding — §12c's honest miss, now with
|
||||||
two more ways out of it.
|
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
|
## Order of work
|
||||||
|
|
||||||
### Phase 2a — move the code (no behaviour change) — **landed**
|
### 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`:
|
`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
|
both extensions survive the round trip, the accessors read them, and a retained
|
||||||
root without a key is skipped rather than adopted.
|
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
|
- 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
|
`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.
|
*List*-learned refounding leave those keys held, not only a blob-learned one.
|
||||||
|
|||||||
Reference in New Issue
Block a user