OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-room commits / 5014169

state-base: forceCreator for notary revival (rebuild-from-authoritative)
After the original creator abandons the deterministic genesis writer via a resync (forceNonCreator), a co-owned base has NO live indexer — indexedLength freezes and new writes stay optimistic (never finalize). Because _gsKeyPair is roomSeed-derived (derivable by any co-owner), a HEALTHY co-owner that holds the authoritative state can re-assume it as the sole live indexer, breaking the "need-an-indexer-to-make-an- indexer" deadlock keet cannot escape (keet writers are random). Once one notary is live, keet's dynamic start/move/stop-indexing take over. Add opts.forceCreator (force amCreator=true regardless of key ordering). CANONICAL usage is rebuild-from-authoritative: forceCreator + a FRESH store + adopt(records, head) — AUTHOR the genesis from the snapshot (adopt continues the atproto chain from the current head → relay chain unbroken). This sidesteps the autobase view-reindex crash the in-place existing-store variant hit (stale view checkout) AND the backfill-can't-linearize wall (authoring != backfilling). Mutually exclusive with forceNonCreator; exactly one co-owner holds gsKeyPair at a time. Default off. Proven: hyper-proto-peer/test/rebuild-from-authoritative.test.js (new post FINALIZES after a clean rebuild). No regression (resync-fresh-corestore 16/16, 2node 12/12, resync-creator-convergence 13/13). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
50141694c645fa8709b98792c61a3686c2d03333
by plankatron <fleeky@gmail.com> · 2026-07-21T16:27:30.000Z

Changed files (1) +24 -2

lib/state-base.js +24 -2
……@@ -156,6 +156,10 @@ export class StateBaseV3 {
156156 // its store re-joins as a NEW random writer via the doorbell (keet-core pairing/keys.js), never the
157157 // old writer. Set on a resync boot only (device-node.js passes forceNonCreator: !!resyncStore).
158158 this._forceNonCreator = opts.forceNonCreator === true
159 // forceCreator (assume-genesis heal): the healthy co-owner re-assumes the deterministic genesis
160 // writer/indexer after the original creator abandoned it via a resync — see _open. Mutually exclusive
161 // with forceNonCreator; exactly one co-owner holds gsKeyPair at a time.
162 this._forceCreator = opts.forceCreator === true
159163 // Injectable crypto for membership-gated admission (keet attestation primitives).
160164 // attest: sign localKey‖base.key as our keet identity; verifyAttest: IdentityKey.verify.
161165 this._attest = typeof opts.attest === 'function' ? opts.attest : null
……@@ -355,9 +359,27 @@ export class StateBaseV3 {
355359 // forceNonCreator (resync heal): never elect as creator → open with keyPair=null → a FRESH local
356360 // writer; the deterministic genesis (_gsKeyPair) becomes a REMOTE writer that replicates + linearizes
357361 // normally (the proven non-creator path), instead of an un-sheddable, un-linearizable local writer.
358 const amCreator = this._forceNonCreator ? false : ((!this._getMembers || !this._selfKey) ? true : await this._amCreator())
362 //
363 // forceCreator (notary revival): re-assume the deterministic genesis writer/indexer on a HEALTHY
364 // co-owner even when it is NOT the lowest key. Used when the original creator (lowest-key) abandoned
365 // the genesis via a resync (forceNonCreator) — leaving the base with NO live indexer (indexedLength
366 // frozen, new writes optimistic-only). Because _gsKeyPair is roomSeed-derived (derivable by any
367 // co-owner), a healthy co-owner that holds the authoritative state can become the sole live indexer
368 // (quorum 1) — breaking the "need-an-indexer-to-make-an-indexer" deadlock keet cannot escape (keet
369 // writers are random). Once ONE notary is live again, keet's dynamic start/move/stop-indexing take over.
370 //
371 // CANONICAL usage = REBUILD-FROM-AUTHORITATIVE: pair forceCreator with a FRESH store + adopt(records,
372 // head) — the notary AUTHORS the genesis from the snapshot (adopt continues the atproto chain from the
373 // current head, so the relay chain is unbroken). This sidesteps two walls the in-place variant hit:
374 // a fresh store has NO stale view core (no autobase view-reindex checkout crash), and AUTHORING (not
375 // backfilling) an empty genesis is the normal creation path (diagnostic-1 N/A). Proven in
376 // hyper-proto-peer/test/rebuild-from-authoritative.test.js (new post FINALIZES). Do NOT forceCreator an
377 // EXISTING store to "continue" the genesis in place — that reindexes the view and crashes on write.
378 // EXACTLY ONE co-owner may hold gsKeyPair at a time — the resynced peer MUST be forceNonCreator.
379 const amCreator = this._forceCreator ? true : (this._forceNonCreator ? false : ((!this._getMembers || !this._selfKey) ? true : await this._amCreator()))
380 const roleTag = this._forceCreator ? ' (assume-genesis: forced creator)' : (this._forceNonCreator ? ' (resync: forced non-creator)' : '')
359381 console.log('[state-base-v3] room=%s %s%s base=%s', this._roomKeyHex.slice(0, 12),
360 amCreator ? 'creator' : 'member', this._forceNonCreator ? ' (resync: forced non-creator)' : '', this._detBaseKey.toString('hex').slice(0, 12))
382 amCreator ? 'creator' : 'member', roleTag, this._detBaseKey.toString('hex').slice(0, 12))
361383 await this._openBase(this._detBaseKey, amCreator ? this._gsKeyPair : null)
362384 }
363385