| … | … | @@ -156,6 +156,10 @@ export class StateBaseV3 { |
| 156 | 156 | // its store re-joins as a NEW random writer via the doorbell (keet-core pairing/keys.js), never the |
| 157 | 157 | // old writer. Set on a resync boot only (device-node.js passes forceNonCreator: !!resyncStore). |
| 158 | 158 | 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 |
| 159 | 163 | // Injectable crypto for membership-gated admission (keet attestation primitives). |
| 160 | 164 | // attest: sign localKey‖base.key as our keet identity; verifyAttest: IdentityKey.verify. |
| 161 | 165 | this._attest = typeof opts.attest === 'function' ? opts.attest : null |
| … | … | @@ -355,9 +359,27 @@ export class StateBaseV3 { |
| 355 | 359 | // forceNonCreator (resync heal): never elect as creator → open with keyPair=null → a FRESH local |
| 356 | 360 | // writer; the deterministic genesis (_gsKeyPair) becomes a REMOTE writer that replicates + linearizes |
| 357 | 361 | // 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)' : '') |
| 359 | 381 | 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)) |
| 361 | 383 | await this._openBase(this._detBaseKey, amCreator ? this._gsKeyPair : null) |
| 362 | 384 | } |
| 363 | 385 | |