OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-proto-peer / test / viewreindex-repro.test.js

68 lines · javascript ref:
// REPRO: the in-place view-reindex crash. A MEMBER converges a base, then reopens the SAME storage
// forcing the genesis/creator role (assume the indexer in place, continuing the existing log). Autobase
// reindexes the view on the role change and something requests a stale checkout on the reset view core →
// hypercore "Invalid checkout N for batch, length is 0". Instrumented hypercore prints the culprit stack.
import '../lib/bare-globals.js'
import test from 'brittle'
import fs from 'bare-fs'
import b4a from 'b4a'
import createTestnet from '@hyperswarm/testnet'
import { bootMultiDevice, provisionDevice } from '../lib/device-node.js'
import { deviceKeetKeyPair } from 'atproto-room/lib/membership.js'

const M = {
  communityRoot: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art',
  device1: 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote',
  device2: 'legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will'
}
const tmp = () => { const d = '/tmp/hpp-vrx-' + 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('REPRO: member reopens SAME store as forceCreator (assume indexer in place) → view-reindex checkout crash', { timeout: 240000 }, async (t) => {
  try { process.on('unhandledRejection', (e) => { console.error('UNHANDLED ' + (e && (e.stack || e.message || e))) }) } catch {}
  const testnet = await createTestnet(3)
  const swarmOpts = { bootstrap: testnet.bootstrap }
  const sCre = tmp(); const sMem = tmp()
  const d1 = await bundle(M.communityRoot, M.device1, 'club.local')
  const d2 = await bundle(M.communityRoot, M.device2, 'club.local')
  const CRE = d1.keetPub < d2.keetPub ? d1 : d2
  const MEM = d1.keetPub < d2.keetPub ? d2 : d1
  const DID = CRE.did
  const members = [{ key: d1.keetPub, role: 1 }, { key: d2.keetPub, role: 1 }]
  CRE.entry.members = members; MEM.entry.members = members

  const nCre = await bootMultiDevice({ storage: sCre, dids: [CRE.entry], swarmOpts, signIntervalMs: 1000 })
  let nMem = await bootMultiDevice({ storage: sMem, dids: [MEM.entry], swarmOpts, signIntervalMs: 1000 })
  t.teardown(async () => { try { await nCre.close() } catch {} ; try { await nMem.close() } catch {} ; await testnet.destroy() })
  const cre = () => nCre.byDid.get(DID)
  let mem = nMem.byDid.get(DID)

  await cre().device.applyWrite(post('r-cre', 'creator'))
  let wrote = false
  for (let i = 0; i < 60 && !wrote; i++) { await cre().sb.base.update().catch(() => {}); await mem.sb.base.update().catch(() => {}); try { await mem.device.applyWrite(post('r-mem', 'member')); wrote = true } catch { await mem.sb.set('__ping__', { i }).catch(() => {}); await sleep(1000) } }
  t.ok(wrote, 'member admitted + wrote')
  for (let i = 0; i < 40; i++) { await mem.sb.base.update().catch(() => {}); await sleep(300) }
  t.ok(has(await mem.device.state(), 'r-cre'), 'member converged (has a working log)')

  // ── IN-PLACE role change: reopen the member's SAME storage forcing the genesis/creator role ──
  await nMem.close()
  nMem = await bootMultiDevice({ storage: sMem, dids: [{ ...MEM.entry, forceCreator: true }], swarmOpts, signIntervalMs: 1000 })
  mem = nMem.byDid.get(DID)
  // a write drives the tick/sign/membrane path that trips the reindex checkout
  let crashed = false
  try {
    for (let i = 0; i < 20; i++) { await mem.sb.base.update().catch(() => {}); await sleep(500) }
    await mem.device.applyWrite(post('r-post-switch', 'after in-place role change'))
    for (let i = 0; i < 20; i++) { await mem.sb.base.update().catch(() => {}); await sleep(500) }
  } catch (e) { crashed = true; console.error('CAUGHT ' + (e && e.stack)) }
  console.log('DIAG done crashed=' + crashed + ' memWritable=' + mem.sb.base.writable + ' len=' + mem.sb.base.length)
  t.pass('repro ran (see DIAG-CHECKOUT stack for the culprit)')
})