// Probe (room-state spec P1): deterministic-creator bootstrap — NO split-brain.
//
// Two daemons open the SAME room's StateBase with injected keet membership:
// - M = a MOD (role 2) → the elected creator (lex-smallest admin/mod)
// - P = a PEER (role 3) → NOT the creator; must NOT mint its own base
// Expected: M mints genesis once and broadcasts; P never founds a competing
// base, adopts M's announced base.key, and both converge to ONE base.key.
// This is the exact scenario that produced the bd2b0d/4d8550 split-brain
// under the old random-founder + lex-election code.
//
// Run: bare test/probe-state-creator-election.js
// PASS = both base.keys are non-null AND identical, and P reads M's write.
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
import crypto from 'hypercore-crypto'
import createTestnet from 'hyperdht/testnet.js'
import { StateBase } from '../lib/state-base.js'
const ROOM_ID = 'probe-elect-' + Math.random().toString(36).slice(2, 10)
const roomTag = crypto.hash(Buffer.from(ROOM_ID, 'utf-8'))
const DEADLINE_MS = 25000
// Two member identities (keet identity pubkeys — NOT autobase keys).
const modKp = crypto.keyPair()
const peerKp = crypto.keyPair()
const modKey = modKp.publicKey.toString('hex')
const peerKey = peerKp.publicKey.toString('hex')
// Same membership view on both daemons → deterministic, identical election.
const MEMBERS = [
{ key: modKey, role: 2 }, // mod → eligible creator
{ key: peerKey, role: 3 } // peer → not eligible
]
const getMembers = async () => MEMBERS
const dirM = path.join(os.tmpdir(), 'probe-elect-M-' + Date.now())
const dirP = path.join(os.tmpdir(), 'probe-elect-P-' + Date.now())
fs.mkdirSync(dirM, { recursive: true })
fs.mkdirSync(dirP, { recursive: true })
console.log('roomId:', ROOM_ID)
console.log('mod key:', modKey.slice(0, 12) + '… (expected creator)')
console.log('peer key:', peerKey.slice(0, 12) + '…')
console.log('\nspinning up local DHT testnet...')
const testnet = await createTestnet(3)
const swarmOpts = { bootstrap: testnet.bootstrap }
const sbMod = new StateBase(dirM, roomTag, { swarmOpts, selfKey: modKp.publicKey, getMembers })
const sbPeer = new StateBase(dirP, roomTag, { swarmOpts, selfKey: peerKp.publicKey, getMembers })
let exitCode = 1
function fail (msg) { console.error('\n✗ FAIL:', msg); }
try {
console.log('\n[mod] ready() — should be elected creator and MINT')
await sbMod.ready()
console.log('[peer] ready() — should NOT mint, awaits adoption')
await sbPeer.ready()
const modKeyHex = sbMod.stateBaseKey ? sbMod.stateBaseKey.toString('hex') : null
console.log('\n[mod] base.key:', modKeyHex ? modKeyHex.slice(0, 12) + '…' : 'NULL')
console.log('[peer] base.key:', sbPeer.stateBaseKey ? sbPeer.stateBaseKey.toString('hex').slice(0, 12) + '…' : 'NULL (awaiting)')
if (!modKeyHex) throw new Error('mod (creator) did not mint a base')
if (sbPeer.stateBaseKey) {
// If the peer already has a key here it MUST be the mod's (adopted), never
// a self-minted one. A different key = split-brain regression.
if (sbPeer.stateBaseKey.toString('hex') !== modKeyHex) {
throw new Error('peer minted/holds a DIFFERENT key — split-brain regression')
}
}
console.log('\n[mod] set world:time = 12.0')
await sbMod.set('world:time', 12.0)
// Sanity: the creator must read back its own write locally.
const modLocal = await sbMod.get('world:time').catch(() => null)
console.log('[mod] local get world:time =', modLocal)
console.log('\npolling until peer adopts the creator key and replicates the write...')
const start = Date.now()
let lastLog = 0
while (Date.now() - start < DEADLINE_MS) {
const pk = sbPeer.stateBaseKey ? sbPeer.stateBaseKey.toString('hex') : null
if (pk === modKeyHex) {
// StateBase.get() returns the {value, ts, _by} wrapper (daemon contract).
const v = await sbPeer.get('world:time').catch(() => null)
if (v && v.value === 12.0) {
console.log('\n✓ converged: peer adopted creator key', pk.slice(0, 12) + '… and replicated world:time =', v.value)
exitCode = 0
break
}
const now = Date.now()
if (now - lastLog > 3000) {
lastLog = now
let vlen = '?'; let writers = '?'; let conns = '?'
try { vlen = sbPeer.base.view.version } catch {}
try { writers = sbPeer.base.activeWriters ? sbPeer.base.activeWriters.length : (sbPeer.base.writers ? sbPeer.base.writers.length : '?') } catch {}
try { conns = sbPeer.swarm ? sbPeer.swarm.connections.size : '?' } catch {}
console.log(' [peer] adopted, get=', v, ' view.version=', vlen, ' writers=', writers, ' baseSwarmConns=', conns)
}
}
await new Promise(r => setTimeout(r, 500))
}
if (exitCode !== 0) {
const pk = sbPeer.stateBaseKey ? sbPeer.stateBaseKey.toString('hex') : 'NULL'
fail('peer did not converge to the creator base.key within deadline (peer key=' + (pk === 'NULL' ? pk : pk.slice(0, 12) + '…') + ')')
}
} catch (err) {
fail(err.message)
if (err.stack) console.error(err.stack)
} finally {
// close()/destroy() teardown is known to hang (pre-existing, separate issue);
// bound it so the result prints and the process exits.
await Promise.race([
(async () => {
await sbMod.close().catch(() => {})
await sbPeer.close().catch(() => {})
await testnet.destroy().catch(() => {})
})(),
new Promise(r => setTimeout(r, 3000))
])
try { fs.rmSync(dirM, { recursive: true, force: true }) } catch {}
try { fs.rmSync(dirP, { recursive: true, force: true }) } catch {}
}
console.log(exitCode === 0 ? '\nPASS' : '\nFAIL')
Bare.exit(exitCode)