update
This commit is contained in:
@@ -70,6 +70,13 @@ plus two smaller reasons the same shape comes back (§11):
|
||||
re-issues its standing REQ, because a subscription that died without saying so
|
||||
is indistinguishable from a quiet one.
|
||||
|
||||
**Revision 6.** Revision 5 gave the fold an authority to honor a Refounding
|
||||
snapshot on, and retained the roots that make one reachable. One report later the
|
||||
roster was still short (33 shown against Armada's 263), and the database said why:
|
||||
**a rotation that delivered this client no key was verified and then thrown away**,
|
||||
and the one place the reference keeps that authority — the member's own List — was
|
||||
round-tripped but never read (§13).
|
||||
|
||||
Read path today (phase 1 landed the walk, phase 2a moved each layer, phase 2b
|
||||
swapped the transport and put the pump in charge of settling pages, phase 3 added
|
||||
the rekey watch and held epochs, phase 4 made an empty or unreadable room say so,
|
||||
@@ -991,12 +998,14 @@ refounder.
|
||||
|
||||
What is honestly still missing, in the order it matters:
|
||||
|
||||
- **Recovery needs the old rotation chunks to still be on a relay.** A refounder
|
||||
is only learnable from the rotation that named it; if the relays have dropped
|
||||
the chunk for the epoch a client is on, the seeded members stay unknown until
|
||||
the next Refounding — the client cannot verify a snapshot it has no rotation
|
||||
for, and accepting one from an unverified npub would let any member inject
|
||||
arbitrary npubs into everyone's roster.
|
||||
- **Recovery needs the old rotation chunks to be readable at all.** A refounder
|
||||
is only learnable from the rotation that named it; if nothing holds the chunk
|
||||
for the epoch a client is on, the seeded members stay unknown until the next
|
||||
Refounding — the client cannot verify a snapshot it has no rotation for, and
|
||||
accepting one from an unverified npub would let any member inject arbitrary
|
||||
npubs into everyone's roster. §13 narrows what "readable" costs (the database
|
||||
alone is enough, and a rotation that never addressed us still names its
|
||||
minter) and adds the second source the reference uses.
|
||||
- **A Refounding this client publishes seeds nobody.** `build_snapshot_chunks`
|
||||
exists and the reference publishes one at every refounding ("present members
|
||||
only, chunked at 400"); `Community::rotate` does not yet. It has no caller, so
|
||||
@@ -1013,6 +1022,98 @@ What is honestly still missing, in the order it matters:
|
||||
already promises, and it is asserted here by reading it against the fold, not
|
||||
by a test.
|
||||
|
||||
### 13. The roster was still short (revision 6) — **landed**
|
||||
|
||||
Revision 5 gave `fold` an authority to honor a snapshot on, and retained the roots
|
||||
that make one reachable. Vector Community still showed 33 members against Armada's
|
||||
263, and the wraps on disk said why.
|
||||
|
||||
#### 13a. What the numbers said
|
||||
|
||||
Folded from the wraps already in the database, one roster each:
|
||||
|
||||
| honored authority | members |
|
||||
| ----------------- | ------- |
|
||||
| `state.refounders` (empty — what the app showed) | **39** |
|
||||
| the snapshot's author | **247** |
|
||||
| Armada's roster | 263 |
|
||||
|
||||
The epoch-9 Guestbook holds 13 wraps: 11 joins, 1 leave, and one snapshot naming
|
||||
**240 members**. Nothing was missing to read. The authority was.
|
||||
|
||||
#### 13b. A verified rotation names its minter whether or not it addressed us
|
||||
|
||||
§12c retained the root before the current one (7), and the rekey watch fetched the
|
||||
rotation that stepped off it: **7 → 8**, complete, 3 chunks, 209 blobs, rotator
|
||||
`d133ecb0…` — the same npub that authored the snapshot. `walk` verified every part
|
||||
of that and then discarded the name, because the rotator was only recorded, and
|
||||
only reached the state, inside the branch that *adopts* — and adoption needs a blob
|
||||
addressed to us. This client joined after that Refounding, so there was no blob for
|
||||
it, and the one npub the whole roster rests on was dropped by a `continue`.
|
||||
|
||||
**Fixed:** `walk` records the rotator of every base rotation whose continuity it
|
||||
verified, adopted or not — `Step.refounders` rather than `Adopted.refounders`, so
|
||||
the set survives a step that adopts nothing — and `Adoptions.refounders` now stands
|
||||
on its own: `Adoptions::is_empty()` is false with authority alone, `merge_adoptions`
|
||||
folds it into the state, and a newly learned refounder re-folds the roster, which is
|
||||
the only thing that depends on it.
|
||||
|
||||
What the widening is, precisely: a rotation is a candidate only when it is
|
||||
complete, continuity-valid against a key this client holds, and signed by an npub
|
||||
that is authorized for the scope and not banned. A blob proves *our* inclusion, not
|
||||
who minted the epoch — and the epoch is what a snapshot needs. The residual is the
|
||||
one CORD-02 §5 documents and §12b already accepted: the wire never says which
|
||||
epoch's stream a snapshot rode, so a verified refounder of one held epoch is
|
||||
honored on another's snapshot, all of them npubs that legitimately held that key.
|
||||
|
||||
#### 13c. The authority a peer's List carries
|
||||
|
||||
Armada does not depend on the rotation chunks surviving at all: it writes the
|
||||
refounder into the member's own Community List (`refounder` for the current epoch,
|
||||
`held_roots[*].refounder` for retained ones, both hex extensions beside the root
|
||||
keys that document already carries) and reads them back on a device that never held
|
||||
a rotation. The List is sealed to self and authored by self, which is the same trust
|
||||
class as the keys next to it — a different thing entirely from trusting the author
|
||||
of a snapshot.
|
||||
|
||||
`concord` already round-tripped those fields as unknown ones. **Now read:**
|
||||
`JoinMaterial::refounder()` and `JoinMaterial::held_roots()` decode them, and
|
||||
`sync::load` feeds both the authority set and the retained roots from the entry's
|
||||
`current` and `seed` material. Genesis names no authority, which is the reference's
|
||||
own `epoch > 0` skip — no rotation minted epoch 0.
|
||||
|
||||
#### 13d. Recovery does not need a relay to still hold the chunk
|
||||
|
||||
A rekey wrap the local database already holds is a rotation this client can read
|
||||
without asking anybody. `Community::tick` — the scheduler, once per
|
||||
`MIN_ROUND_INTERVAL` — asks the database again whenever history spans a rotation but
|
||||
no authority is known yet (`refounders` empty with more than one root held), and
|
||||
stops as soon as one is learned. That is the answer to "the relays dropped the
|
||||
chunk": the roster heals on the next tick rather than at the next Refounding.
|
||||
|
||||
Measured on that database after the fix: the same 3 wraps, no new fetch, roster
|
||||
**39 → 247**.
|
||||
|
||||
#### 13e. Where the numbers still differ
|
||||
|
||||
- **We do not write those fields ourselves.** Our List material is built from the
|
||||
state (`list_entry`), which carries no per-epoch attribution — `refounders` is a
|
||||
set, and an honest write needs a map — so a write we make omits
|
||||
`refounder`/`held_roots`, and whether a peer's copy survives one is then a
|
||||
canonical-bytes tie-break rather than a guarantee. Nothing regresses today: our
|
||||
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.
|
||||
- **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.
|
||||
|
||||
## Order of work
|
||||
|
||||
### Phase 2a — move the code (no behaviour change) — **landed**
|
||||
@@ -1240,6 +1341,18 @@ outstanding are the ones that need a GPUI harness or two live accounts.
|
||||
and `a_list_that_moved_the_root_on_retains_the_root_of_our_join`): material at a
|
||||
newer epoch replaces the current root and retires the one it moved past, and a
|
||||
List whose seed names our join keeps that root held.
|
||||
- A rotation that addressed somebody else — **landed in revision 6**
|
||||
(`rekey::tests::a_rotation_that_delivered_us_no_key_still_names_its_refounder`):
|
||||
a complete rotation off a root we hold, whose only blob is for a third party,
|
||||
adopts nothing and still yields its rotator as the epoch's refounder — which is
|
||||
the authority a snapshot is honored on.
|
||||
- The authority a List carries — **landed in revision 6**
|
||||
(`sync::tests::a_list_entrys_refounder_becomes_the_snapshot_authority`): an
|
||||
entry whose material names a refounder puts that npub in the state's authority
|
||||
set on load. Its wire half is
|
||||
`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.
|
||||
- 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.
|
||||
|
||||
+15
-1
@@ -312,13 +312,19 @@ as an inline row only when its author passes
|
||||
## Membership
|
||||
|
||||
```rust
|
||||
let states = cord02::guestbook::coalesce(&rumors, now_ms, Some(&refounder_pk), |actor, target, citation| {
|
||||
let states = cord02::guestbook::coalesce(&rumors, now_ms, &refounders, |actor, target, citation| {
|
||||
citation_ok(&owner, &community_id, actor, citation, &control.roles.floors)
|
||||
&& control.roles.can_act_on_member(actor, &owner, target, Permissions::KICK)
|
||||
});
|
||||
let members = cord02::guestbook::complete_memberlist(&states, &observed, &granted, &control.banned, &BTreeMap::new());
|
||||
```
|
||||
|
||||
- `refounders` is the set of npubs whose rotations minted an epoch this client
|
||||
verified (`CommunityState.refounders`) — a snapshot chunk is honored only from
|
||||
one of them, and an empty set honors none. A rotation that delivered this
|
||||
client no key still names its minter, so the set is filled by any base rotation
|
||||
whose continuity verifies against a root held (`rekey::walk`); a List entry's
|
||||
`refounder`, read by `list::JoinMaterial::refounder`, is the other source.
|
||||
- `observed` is npub → ms for every author this client has seen publish anything
|
||||
usable, which is what makes a member visible before their Join arrives. Only
|
||||
count it forward.
|
||||
@@ -553,6 +559,14 @@ fragment at every index below it. `merge` resolves a `frags` disagreement to the
|
||||
larger value. (`13302`, the single-event List, is retired by the spec — a
|
||||
replaceable kind cannot fragment.)
|
||||
|
||||
A join material may also carry two hex extensions this crate does not write but
|
||||
does read, because they are the only place a device that never held a rotation can
|
||||
learn who minted an epoch: `refounder` names the npub whose Refounding minted the
|
||||
entry's `root_epoch`, and `held_roots` is the retained prior epochs
|
||||
(`[{epoch, key, refounder?, control_pk?, retired_at?}]`). `JoinMaterial::refounder()`
|
||||
and `JoinMaterial::held_roots()` decode both; unknown fields are round-tripped
|
||||
regardless, so a document that carries them keeps them.
|
||||
|
||||
The payload's 32-byte values are **unpadded base64url at every depth**, which is
|
||||
section-scoped to §8: CORD-05 invites stay hex. The writer re-encodes them on
|
||||
every serialization, so its output is always the canonical 43-character spelling;
|
||||
|
||||
Reference in New Issue
Block a user