OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-room commits / a9b76ca

state-base: force-non-creator open for the resync heal
A poisoned/forked StateBaseV3 CREATOR cannot heal via a fresh-corestore resync: its genesis writer is the deterministic _gsKeyPair (roomSeed-derived), so a fresh store re-opens the SAME writer, and autobase will NOT linearize an externally- backfilled LOCAL writer core (base.length stays 0, system core empty, peers never discovered). Add opts.forceNonCreator so a resync opens as a NON-creator (fresh primaryKey local writer). The deterministic genesis then becomes a REMOTE writer that replicates + linearizes normally (the proven non-creator path). Mirrors keet: every writer is a fresh random keypair, and a store-loss device re-joins as a NEW writer via the doorbell (keet-core pairing/keys.js), never the old writer. Base identity (bootstrapKey = _detBaseKey) is unchanged; the opt defaults off, so existing flows are unaffected (resync-fresh-corestore 16/16, multi-did-2node 12/12). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
a9b76ca39ada0bc4e6c4ead7fdfbee51b2f66e22
by plankatron <fleeky@gmail.com> · 2026-07-21T15:04:26.000Z

Changed files (1) +13 -3

lib/state-base.js +13 -3
……@@ -149,6 +149,13 @@ export class StateBaseV3 {
149149 this._selfHead = null // our current signed head rev — announced over presence so a BEHIND co-owner is
150150 this._presenceSenders = new Set() // excluded from the election (anti-freeze); re-announced on head change
151151 this._getMembers = typeof opts.getMembers === 'function' ? opts.getMembers : null
152 // RESYNC heal (force-non-creator): open as a NON-creator even when we're the lowest-key member, so a
153 // fresh-corestore re-join gets a FRESH (primaryKey-derived) local writer instead of re-opening the
154 // deterministic _gsKeyPair genesis writer (which a fresh store cannot shed and autobase will not
155 // linearize when backfilled — see _open + hyper-proto-peer resync). Mirrors keet: a device that lost
156 // its store re-joins as a NEW random writer via the doorbell (keet-core pairing/keys.js), never the
157 // old writer. Set on a resync boot only (device-node.js passes forceNonCreator: !!resyncStore).
158 this._forceNonCreator = opts.forceNonCreator === true
152159 // Injectable crypto for membership-gated admission (keet attestation primitives).
153160 // attest: sign localKey‖base.key as our keet identity; verifyAttest: IdentityKey.verify.
154161 this._attest = typeof opts.attest === 'function' ? opts.attest : null
……@@ -345,9 +352,12 @@ export class StateBaseV3 {
345352
346353 async _open () {
347354 if (!this._detBaseKey) throw new Error('StateBaseV3 requires roomSeed (deterministic key)')
348 const amCreator = (!this._getMembers || !this._selfKey) ? true : await this._amCreator()
349 console.log('[state-base-v3] room=%s %s base=%s', this._roomKeyHex.slice(0, 12),
350 amCreator ? 'creator' : 'member', this._detBaseKey.toString('hex').slice(0, 12))
355 // forceNonCreator (resync heal): never elect as creator → open with keyPair=null → a FRESH local
356 // writer; the deterministic genesis (_gsKeyPair) becomes a REMOTE writer that replicates + linearizes
357 // 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())
359 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))
351361 await this._openBase(this._detBaseKey, amCreator ? this._gsKeyPair : null)
352362 }
353363