// MINIMAL REPRO: does a writer's PRE-ADMISSION optimistic block ever land in its OWN view?
// A = creator/sole-indexer. B = member. B writes ONE optimistic block 'k1' before admission,
// gets admitted, then we inspect: can B read k1? can A read k1? B.local.length? Then B (now a
// real writer) writes 'k2' and we check k2 visibility on both. Isolates optimistic-orphan from
// every other layer (no RepoBase, no lease, no atproto).
import '../lib/bare-globals.js'
import crypto from 'hypercore-crypto'
import fs from 'bare-fs'
import b4a from 'b4a'
import createTestnet from '@hyperswarm/testnet'
import { StateBaseV3 } from '../lib/state-base.js'
import { makeAttest, makeVerifyAttest, attestDeviceChain, deviceKeetKeyPair, rootIdentityPublicKey } from '../lib/membership.js'
const ROOT = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
const DEVA = 'legal winner thank year wave sausage worth useful legal winner thank yellow'
const DEVB = 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong'
const tmp = () => { const d = '/tmp/hpp-orphan-' + Math.floor(Math.random() * 1e9); fs.mkdirSync(d, { recursive: true }); return d }
const sleep = (ms) => new Promise(r => setTimeout(r, ms))
const log = (...a) => console.log('[REPRO]', ...a)
async function main () {
const testnet = await createTestnet(3)
const rootPub = await rootIdentityPublicKey(ROOT)
let da = await deviceKeetKeyPair(DEVA); let pa = await attestDeviceChain({ rootMnemonic: ROOT, devicePublicKey: da.publicKey })
let db = await deviceKeetKeyPair(DEVB); let pb = await attestDeviceChain({ rootMnemonic: ROOT, devicePublicKey: db.publicKey })
// creator = LOWEST key (deterministic election), like stortron<tabertron
let A = { dev: da, proof: pa }, B = { dev: db, proof: pb }
if (b4a.compare(A.dev.publicKey, B.dev.publicKey) > 0) { const t = A; A = B; B = t }
const members = [
{ key: b4a.toString(A.dev.publicKey, 'hex'), role: 1 },
{ key: b4a.toString(B.dev.publicKey, 'hex'), role: 1 }
]
const roomSeed = crypto.randomBytes(32); const roomKey = crypto.randomBytes(32); const encryptionKey = crypto.randomBytes(32)
const mk = (dev, proof, dir) => new StateBaseV3(dir, roomKey, {
roomSeed, encryptionKey, swarmOpts: { bootstrap: testnet.bootstrap },
selfKey: dev.publicKey, attest: makeAttest(dev, proof), verifyAttest: makeVerifyAttest(rootPub),
getMembers: async () => members
})
const sbA = mk(A.dev, A.proof, tmp()); await sbA.ready(); await sbA.projectMembers(members)
const sbB = mk(B.dev, B.proof, tmp()); await sbB.ready()
log('A base.key', sbA.base.key.toString('hex').slice(0, 12), 'B base.key', sbB.base.key.toString('hex').slice(0, 12))
log('A.local', sbA.base.local.key.toString('hex').slice(0, 12), 'B.local', sbB.base.local.key.toString('hex').slice(0, 12))
// --- B writes EXACTLY ONE optimistic block k1 (pre-admission), then keeps pinging to get admitted ---
log('B writing k1 OPTIMISTICALLY (pre-admission)...')
await sbB.set('k1', { v: 'k1-from-B-preadmit' })
log('after k1: B.local.length', sbB.base.local.length, 'B.writable', sbB.base.writable)
let admitted = false
for (let i = 0; i < 40 && !admitted; i++) {
await sbB.set('__ping__', { i }).catch(() => {})
await sleep(1000)
admitted = await sbB.effectiveWritable().catch(() => false)
if (i % 5 === 0) log(' waiting admit... i', i, 'B.writable', sbB.base.writable, 'effWritable', admitted, 'B.local.length', sbB.base.local.length)
}
log('B ADMITTED:', admitted, 'B.writable', sbB.base.writable, 'B.local.length', sbB.base.local.length)
// give replication a chance to settle both views.
// A_IDLE=1 → simulate the rejoin-test condition: ONLY B updates (indexer A not actively acking).
const A_IDLE = process.env.A_IDLE === '1'
log('settle mode:', A_IDLE ? 'A IDLE (only B.update)' : 'both active')
for (let i = 0; i < 10; i++) { if (!A_IDLE) await sbA.base.update().catch(() => {}); await sbB.base.update().catch(() => {}); await sleep(500) }
// --- INSPECT k1 visibility ---
const aK1 = await sbA.get('k1'); const bK1 = await sbB.get('k1')
log('=== k1 (pre-admission optimistic) ===')
log(' A.get(k1):', JSON.stringify(aK1))
log(' B.get(k1):', JSON.stringify(bK1), bK1 ? 'B SEES ITS OWN k1' : 'B CANNOT SEE ITS OWN k1 <-- ORPHAN')
// --- B (now a real writer) writes k2 ---
log('B writing k2 (POST-admission, should be a normal writer append)...')
await sbB.set('k2', { v: 'k2-from-B-postadmit' })
for (let i = 0; i < 10; i++) { await sbA.base.update().catch(() => {}); await sbB.base.update().catch(() => {}); await sleep(500) }
const aK2 = await sbA.get('k2'); const bK2 = await sbB.get('k2')
log('=== k2 (post-admission) ===')
log(' A.get(k2):', JSON.stringify(aK2))
log(' B.get(k2):', JSON.stringify(bK2), bK2 ? 'B sees k2' : 'B CANNOT see k2')
// --- direct DAG/linearizer inspection ---
try {
log('=== linearizer / writer state ===')
log(' A indexers', sbA.base.linearizer.indexers.length, 'B indexers', sbB.base.linearizer.indexers.length)
log(' A.system.has(B.local):', await sbA.base.system.has(sbB.base.local.key).catch(e => 'ERR ' + e.message))
log(' B.system.has(B.local):', await sbB.base.system.has(sbB.base.local.key).catch(e => 'ERR ' + e.message))
log(' B.local.length (final):', sbB.base.local.length)
// is k1 in B's RAW view (bypassing our get wrapper)?
const rawB = await sbB.base.view.get('@state/entries', { key: 'k1' }).catch(e => 'ERR ' + e.message)
log(' B raw view.get(k1):', JSON.stringify(rawB))
} catch (e) { log('inspect threw', e.message) }
await sbA.close(); await sbB.close(); await testnet.destroy()
log('DONE')
process.exit(0)
}
main().catch(e => { console.error('REPRO FAILED', e); process.exit(1) })