// SMOKING GUN: our set() fuses content into a pre-admission optimistic block and does NOT block
// until writable. If no indexer is online to admit+ack, set() returns SUCCESS but autobase rolls
// the block back every drain → the write is SILENTLY LOST. KEET never does this (optimistic =
// content-free admission doorbell; content only via a confirmed-writer append).
//
// Three scenarios:
// HAPPY : A(indexer) online, B writes k1 once (no retry loop) → does a SINGLE set commit?
// OFFLINE : A never online, B writes k1 → set() reports success; is k1 durable? (expect NO)
// RECOVER : A offline when B writes k1, THEN A comes online → does k1 recover, or is it lost
// because B never re-appended?
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-loss-' + 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('[LOSS]', ...a)
async function main () {
const MODE = process.env.MODE || 'HAPPY'
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 })
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 dirA = tmp()
log('MODE', MODE)
let sbA = null
if (MODE === 'HAPPY') { sbA = mk(A.dev, A.proof, dirA); await sbA.ready(); await sbA.projectMembers(members); log('A (indexer) ONLINE') }
else { log('A (indexer) OFFLINE at write time') }
const sbB = mk(B.dev, B.proof, tmp()); await sbB.ready()
// THE REAL API USAGE: one set(), no ping/retry loop.
log('B: set(k1) ONCE (no retry loop)...')
let setThrew = null
try { await sbB.set('k1', { v: 'important-post' }) } catch (e) { setThrew = e.message }
log('B set() returned:', setThrew ? ('THREW: ' + setThrew) : 'SUCCESS (no throw)')
log('B.local.length', sbB.base.local.length, 'B.writable', sbB.base.writable)
// settle
for (let i = 0; i < 12; i++) { if (sbA) await sbA.base.update().catch(() => {}); await sbB.base.update().catch(() => {}); await sleep(500) }
const bK1 = await sbB.get('k1')
log('after settle — B.get(k1):', JSON.stringify(bK1), bK1 ? 'durable on B' : 'GONE on B (rolled back / never committed)')
log('B.writable now:', sbB.base.writable, 'B.local.length', sbB.base.local.length)
if (sbA) { const aK1 = await sbA.get('k1'); log('A.get(k1):', JSON.stringify(aK1), aK1 ? 'committed on A' : 'NOT on A') }
if (MODE === 'RECOVER') {
log('--- now bringing A (indexer) ONLINE (B already stopped writing) ---')
sbA = mk(A.dev, A.proof, dirA); await sbA.ready(); await sbA.projectMembers(members)
for (let i = 0; i < 30; i++) { await sbA.base.update().catch(() => {}); await sbB.base.update().catch(() => {}); await sleep(1000) }
const bK1r = await sbB.get('k1'); const aK1r = await sbA.get('k1')
log('after A online — B.get(k1):', JSON.stringify(bK1r), 'A.get(k1):', JSON.stringify(aK1r))
log(bK1r ? 'RECOVERED (block re-applied on admission)' : 'STILL LOST — the single optimistic write never recovered')
}
if (sbA) await sbA.close(); await sbB.close(); await testnet.destroy()
log('DONE')
process.exit(0)
}
main().catch(e => { console.error('LOSS REPRO FAILED', e); process.exit(1) })