OverviewFilesCommitsIssuesDocsReleasesPeers

keet-daemon / test / probe-state-creator-election.js

134 lines · javascript ref:
// 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)