// Multi-DID (multi-tenant) StateBaseV3 — the multi-DID-hpp foundation.
//
// Proves ONE daemon can host N DIDs over ONE Corestore + ONE Hyperswarm + ONE ProtomuxWakeup: each
// DID is a StateBaseV3 opened on a per-DID NAMESPACE of the shared store (opts.store) with the shared
// wakeup (opts.wakeup) and network:false (the daemon drives replication). We run 2 DIDs, each with a
// creator on "machine 1" and a member on "machine 2", and assert the FULL encrypted, attestation-gated
// admission doorbell works PER DID over the shared connection, data is isolated per DID, and each
// machine holds exactly ONE storage lock. This is the harder half the foundational corestore test
// (test/multi-did-mechanism) didn't cover: encrypted optimistic admission over a SHARED wakeup.
import '../lib/bare-globals.js'
import test from 'brittle'
import crypto from 'hypercore-crypto'
import fs from 'bare-fs'
import b4a from 'b4a'
import Corestore from 'corestore'
import Hyperswarm from 'hyperswarm'
import ProtomuxWakeup from 'protomux-wakeup'
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'
// 4 device identities: 2 per DID (a creator + a member).
const MNEMS = [
'legal winner thank year wave sausage worth useful legal winner thank yellow',
'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong',
'letter advice cage absurd amount doctor acoustic avoid letter advice cage above',
'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote'
]
const tmp = () => { const d = '/tmp/hpp-mtdid-' + Math.floor(Math.random() * 1e9); fs.mkdirSync(d, { recursive: true }); return d }
const sleep = (ms) => new Promise(r => setTimeout(r, ms))
// Build a DID spec: distinct room secrets + a creator (lowest key) + a member, both attested to ROOT.
async function mkDid (rootPub, m0, m1) {
let cd = await deviceKeetKeyPair(m0); let cp = await attestDeviceChain({ rootMnemonic: ROOT, devicePublicKey: cd.publicKey })
let md = await deviceKeetKeyPair(m1); let mp = await attestDeviceChain({ rootMnemonic: ROOT, devicePublicKey: md.publicKey })
let creator = { dev: cd, proof: cp }; let member = { dev: md, proof: mp }
if (b4a.compare(creator.dev.publicKey, member.dev.publicKey) > 0) { const t = creator; creator = member; member = t } // creator = lowest key
const members = [
{ key: b4a.toString(creator.dev.publicKey, 'hex'), role: 1 },
{ key: b4a.toString(member.dev.publicKey, 'hex'), role: 1 }
]
return { rootPub, creator, member, members, roomSeed: crypto.randomBytes(32), roomKey: crypto.randomBytes(32), encryptionKey: crypto.randomBytes(32) }
}
// A StateBaseV3 on a SHARED store + SHARED wakeup, network:false (daemon-driven replication).
function mkSb (store, wakeup, did, who) {
const dev = did[who]
return new StateBaseV3(null, did.roomKey, {
store, wakeup, network: false,
roomSeed: did.roomSeed, encryptionKey: did.encryptionKey,
selfKey: dev.dev.publicKey, attest: makeAttest(dev.dev, dev.proof), verifyAttest: makeVerifyAttest(did.rootPub),
getMembers: async () => did.members
})
}
// Wire the daemon side: one connection handler that muxes EVERY DID's cores + wakeup, then join each
// DID's base topic. This is exactly what a multi-DID hpp daemon does.
function wireDaemon (swarm, store, wakeup, sbs) {
swarm.on('connection', (conn) => {
conn.on('error', () => {})
try { wakeup.addStream(conn) } catch {}
try { store.replicate(conn) } catch {}
})
for (const sb of sbs) swarm.join(sb.base.discoveryKey, { server: true, client: true })
}
test('2 DIDs on ONE shared store/swarm/wakeup per machine: admission + isolation + one lock', { timeout: 120000 }, async (t) => {
const testnet = await createTestnet(3)
const rootPub = await rootIdentityPublicKey(ROOT)
const A = await mkDid(rootPub, MNEMS[0], MNEMS[1])
const B = await mkDid(rootPub, MNEMS[2], MNEMS[3])
// ── Machine 1 (creators): ONE store, ONE swarm, ONE wakeup ──
const dir1 = tmp()
const store1 = new Corestore(dir1); await store1.ready()
const swarm1 = new Hyperswarm({ bootstrap: testnet.bootstrap })
const wakeup1 = new ProtomuxWakeup()
const cA = mkSb(store1, wakeup1, A, 'creator'); await cA.ready()
const cB = mkSb(store1, wakeup1, B, 'creator'); await cB.ready()
await cA.projectMembers(A.members); await cB.projectMembers(B.members)
wireDaemon(swarm1, store1, wakeup1, [cA, cB])
await swarm1.flush().catch(() => {})
// ── Machine 2 (members): ONE store, ONE swarm, ONE wakeup ──
const dir2 = tmp()
const store2 = new Corestore(dir2); await store2.ready()
const swarm2 = new Hyperswarm({ bootstrap: testnet.bootstrap })
const wakeup2 = new ProtomuxWakeup()
const mA = mkSb(store2, wakeup2, A, 'member'); await mA.ready()
const mB = mkSb(store2, wakeup2, B, 'member'); await mB.ready()
wireDaemon(swarm2, store2, wakeup2, [mA, mB])
await swarm2.flush().catch(() => {})
t.teardown(async () => {
for (const sb of [cA, cB, mA, mB]) { try { await sb.close() } catch {} }
try { await store1.close() } catch {}; try { await store2.close() } catch {}
try { await swarm1.destroy() } catch {}; try { await swarm2.destroy() } catch {}
await testnet.destroy()
})
t.ok(cA.base.writable && cB.base.writable, 'both creators writable on ONE shared store (machine 1)')
t.absent(mA.base.writable, 'member A starts non-writable (awaiting admission)')
t.absent(mB.base.writable, 'member B starts non-writable (awaiting admission)')
// ── ADMISSION: each member writes → its own DID's creator admits it (encrypted optimistic doorbell
// over the SHARED wakeup). Distinct content per DID. ──
await mA.set('post/1', { did: 'A', text: 'hello-from-A' })
await mB.set('post/1', { did: 'B', text: 'hello-from-B' })
t.ok(mA.base.writable, 'member A got ADMITTED over the shared connection (DID A)')
t.ok(mB.base.writable, 'member B got ADMITTED over the shared connection (DID B)')
// converge
for (let i = 0; i < 16; i++) { for (const sb of [cA, cB, mA, mB]) await sb.base.update().catch(() => {}); await sleep(300) }
// ── ISOLATION: each DID's data is its own; no cross-tenant bleed ──
t.alike(await mA.get('post/1'), { did: 'A', text: 'hello-from-A' }, 'member A reads its own durable write back')
t.alike(await cA.get('post/1'), { did: 'A', text: 'hello-from-A' }, 'creator A converged with A\'s write')
t.alike(await cB.get('post/1'), { did: 'B', text: 'hello-from-B' }, 'creator B converged with B\'s write')
t.absent(await cA.get('post/1') && (await cA.get('post/1')).did === 'B', 'DID A base has NO DID B data (isolation)')
t.is((await cB.get('post/1')).did, 'B', 'DID B base holds only B\'s record (isolation)')
// ── ONE lock per machine (per-directory) ──
const locks1 = fs.readdirSync(dir1).filter(f => /CORESTORE/i.test(f))
const locks2 = fs.readdirSync(dir2).filter(f => /CORESTORE/i.test(f))
t.is(locks1.length, 1, 'machine 1: exactly ONE CORESTORE lock for both DIDs')
t.is(locks2.length, 1, 'machine 2: exactly ONE CORESTORE lock for both DIDs')
})