// REBUILD-FROM-AUTHORITATIVE (the notary revival) — the clean alternative to in-place assume-genesis.
//
// When a co-owned DID's sole notary (genesis indexer) is lost/corrupted, a HEALTHY co-owner that holds
// the authoritative state can REVIVE the notary by re-creating the base FRESH from its own snapshot:
// fresh store (no stale view — sidesteps the autobase view-reindex wall)
// + forceCreator (become the genesis writer/indexer, quorum 1)
// + adopt(records, head) (AUTHOR the genesis from the snapshot — not backfill, so diagnostic-1 N/A;
// adopt continues the chain from the current head so the atproto/relay chain is unbroken).
// A live notary means NEW writes FINALIZE (indexedLength advances) instead of freezing optimistically.
//
// This proves: rebuild a base from an authoritative {records, head} into a fresh creator, and a NEW post
// gets FINALIZED.
import '../lib/bare-globals.js'
import test from 'brittle'
import fs from 'bare-fs'
import b4a from 'b4a'
import { join } from 'bare-path'
import createTestnet from '@hyperswarm/testnet'
import { bootMultiDevice, provisionDevice } from '../lib/device-node.js'
import { deviceKeetKeyPair } from 'atproto-room/lib/membership.js'
const M = {
root: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
dev: 'legal winner thank year wave sausage worth useful legal winner thank yellow'
}
const tmp = () => { const d = '/tmp/hpp-rebuild-' + Math.floor(Math.random() * 1e9); fs.mkdirSync(d, { recursive: true }); return d }
const sleep = (ms) => new Promise(r => setTimeout(r, ms))
const post = (rk, text) => ({ action: 'create', collection: 'app.bsky.feed.post', rkey: rk, record: { $type: 'app.bsky.feed.post', text, createdAt: new Date().toISOString() } })
const has = (st, rk) => st.records.some(r => r.rkey === rk)
async function bundle (rootMnem, devMnem, handle) {
const dk = await deviceKeetKeyPair(devMnem)
const b = await provisionDevice({ rootMnemonic: rootMnem, devicePublicKey: dk.publicKey, handle, pdsUrl: 'https://pds.local' })
return { entry: { deviceMnemonic: devMnem, did: b.did, signingKey: b.signingKey, deviceProof: b.deviceProof, rootIdentityKey: b.rootIdentityKey, handle }, keetPub: b4a.toString(dk.publicKey, 'hex'), did: b.did }
}
test('rebuild a base from an authoritative snapshot (fresh store + forceCreator + adopt) → a NEW post FINALIZES', { timeout: 240000 }, async (t) => {
const testnet = await createTestnet(3)
const swarmOpts = { bootstrap: testnet.bootstrap }
const s1 = tmp()
const D = await bundle(M.root, M.dev, 'wj.local')
D.entry.members = [{ key: D.keetPub, role: 1 }]
// ── Phase 1: an authoritative base (single creator/notary) with real records + a signed head ──
let n1 = await bootMultiDevice({ storage: s1, dids: [D.entry], swarmOpts, signIntervalMs: 1000 })
let node = n1.byDid.get(D.did)
let n1Closed = false
t.teardown(async () => { if (!n1Closed) { try { await n1.close() } catch {} } ; await testnet.destroy() })
await node.device.applyWrite(post('r-a', 'authoritative A'))
await node.device.applyWrite(post('r-b', 'authoritative B'))
for (let i = 0; i < 25; i++) { await node.sb.base.update().catch(() => {}); await sleep(300) }
const st1 = await node.device.state()
t.ok(has(st1, 'r-a') && has(st1, 'r-b'), 'phase1: authoritative base holds the records')
t.ok(node.sb.base.indexedLength > 0, 'phase1: the original notary finalized (indexedLength > 0)')
// export the authoritative snapshot the way a healthy co-owner would (records + last signed head)
await node.device.restore().catch(() => {})
const records = await node.device.repo.listRecords()
const h = node.device.head
t.ok(h && h.rev, 'phase1: exported a signed head to adopt from')
const authHead = { rev: h.rev, commitCid: h.commitCid, dataCid: h.dataCid, signedCommit: h.signedCommit }
// ── Phase 2: tear the base down; REBUILD fresh from the snapshot as the genesis creator/notary ──
await n1.close(); n1Closed = true
const s2 = tmp() // FRESH storage — quarantine-aside + rebuild
n1 = await bootMultiDevice({ storage: s2, dids: [{ ...D.entry, forceCreator: true, adopt: { records, head: authHead } }], swarmOpts, signIntervalMs: 1000 })
n1Closed = false
node = n1.byDid.get(D.did)
let rebuilt = null
for (let i = 0; i < 40 && !rebuilt; i++) { await node.sb.base.update().catch(() => {}); const st = await node.device.state(); if (has(st, 'r-a') && has(st, 'r-b')) rebuilt = st; else await sleep(500) }
t.ok(rebuilt, 'phase2: the REBUILT notary has the full authoritative record set (adopted, not lost)')
t.ok(node.sb.base.writable, 'phase2: the rebuilt node is the live creator/notary (writable)')
// ── Phase 2b: the adopted repo must be membrane-FRONTABLE with NO subsequent write. adopt() alone
// writes no __commit__ frame and the unchanged data root makes sign() a no-op, so without emitBaseline
// the firehose log is empty → serveMembrane projects nothing → the bridge can't serve getRepo/
// getLatestCommit until the first post. Assert the baseline frame exists and the head is exportable. ──
const fhlog = await node.device.firehoseLog()
t.is(fhlog.length, 1, 'phase2b: adopt emitted exactly ONE baseline firehose frame (frontable pre-write)')
const baseCar = await node.device.exportCar()
t.is(baseCar.rev, authHead.rev, 'phase2b: exportCar serves the canonical adopted head with no post')
// ── Phase 3: the whole point — a NEW post must FINALIZE (indexedLength advances past a snapshot) ──
const beforeIdx = node.sb.base.indexedLength
await node.device.applyWrite(post('r-new', 'post after the clean rebuild'))
let finalized = false
for (let i = 0; i < 60 && !finalized; i++) {
await node.sb.base.update().catch(() => {})
if (node.sb.base.indexedLength > beforeIdx && has(await node.device.state(), 'r-new')) finalized = true
else await sleep(750)
}
t.ok(finalized, 'phase3: a NEW post FINALIZED after the clean rebuild (indexedLength advanced past ' + beforeIdx + ') — the notary is LIVE')
})
// The CONFIG-DRIVEN rebuild path the `rebuild-did` tool sets up: a dids entry with adoptCar + forceCreator
// (bootMultiDevice reads adoptCar → readAdoptRepo → adopt). Proves the operational heal end-to-end.
test('config-driven rebuild: adoptCar + forceCreator dids entry → notary revived, new post FINALIZES', { timeout: 240000 }, async (t) => {
const testnet = await createTestnet(3)
const swarmOpts = { bootstrap: testnet.bootstrap }
const s1 = tmp()
const D = await bundle(M.root, M.dev, 'wj.local')
D.entry.members = [{ key: D.keetPub, role: 1 }]
let n1 = await bootMultiDevice({ storage: s1, dids: [D.entry], swarmOpts, signIntervalMs: 1000 })
let node = n1.byDid.get(D.did)
let n1Closed = false
t.teardown(async () => { if (!n1Closed) { try { await n1.close() } catch {} } ; await testnet.destroy() })
await node.device.applyWrite(post('r-a', 'authoritative A'))
await node.device.applyWrite(post('r-b', 'authoritative B'))
for (let i = 0; i < 25; i++) { await node.sb.base.update().catch(() => {}); await sleep(300) }
// export the authoritative CAR exactly as `rebuild-did` does (device.exportCar → written to a file)
const car = await node.device.exportCar()
const carPath = join(s1, 'rebuild.car')
fs.writeFileSync(carPath, car.carBytes)
t.ok(car.recordCount >= 2, 'exported an authoritative CAR (' + car.recordCount + ' records)')
await n1.close(); n1Closed = true
// REBUILD via config only: fresh storage + a dids entry carrying adoptCar + forceCreator
const s2 = tmp()
n1 = await bootMultiDevice({ storage: s2, dids: [{ ...D.entry, forceCreator: true, adoptCar: carPath }], swarmOpts, signIntervalMs: 1000 })
n1Closed = false
node = n1.byDid.get(D.did)
let rebuilt = null
for (let i = 0; i < 40 && !rebuilt; i++) { await node.sb.base.update().catch(() => {}); const st = await node.device.state(); if (has(st, 'r-a') && has(st, 'r-b')) rebuilt = st; else await sleep(500) }
t.ok(rebuilt, 'config adoptCar rebuild adopted the records from the CAR')
t.ok(node.sb.base.writable, 'config rebuild is the live creator/notary (writable)')
const beforeIdx = node.sb.base.indexedLength
await node.device.applyWrite(post('r-new', 'post after config-driven rebuild'))
let finalized = false
for (let i = 0; i < 60 && !finalized; i++) { await node.sb.base.update().catch(() => {}); if (node.sb.base.indexedLength > beforeIdx && has(await node.device.state(), 'r-new')) finalized = true; else await sleep(750) }
t.ok(finalized, 'config-driven adoptCar+forceCreator rebuild: a new post FINALIZED (indexedLength advanced past ' + beforeIdx + ')')
})