OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-did-peer / test / coowner-set.test.mjs

114 lines · javascript ref:
// Co-owner SET integrity — the invariant co-owned.test.mjs could NOT see.
//
// co-owned.test.mjs asserted `diagnose().cachedFronts === 2`, which counts MAP ENTRIES. It stayed green
// while both entries aliased ONE DidPeer: _ensureFront reused the primary for a second co-owner because
// `peer.holds` (=> !!seq) is false until the primary's first provide() COMPLETES, so the second front's
// provide() returned the FIRST co-owner's in-flight open. The co-owner SET collapsed to a single front,
// freshest-by-rev had nothing to choose between, and which co-owner got served was a coin flip — which is
// why co-owned.test.mjs flaked (~3 in 4 runs served the BEHIND co-owner forever).
//
// This test asserts the real invariant: N announced co-owners => N DISTINCT DidPeers, each bound to the
// firehose key it is filed under. Run: node test/coowner-set.test.mjs
import { mkdtempSync, rmSync } from 'node:fs'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import createTestnet from '@hyperswarm/testnet'
import Hyperswarm from 'hyperswarm'
import b4a from 'b4a'
import { RepoStore } from 'atproto-store/lib/repo-store.js'
import { RepoManager } from 'atproto-store/lib/repo-manager.js'
import { FirehoseSequencer } from 'atproto-store/lib/sequencer.js'
import { openCorestore } from 'atproto-store/lib/corestore.js'
import { createAtprotoIdentityFromKey } from 'atproto-identity/lib/atproto-identity.js'
import { discoveryTopic } from 'atproto-identity/lib/discovery.js'
import { DidPeer, DidPeerRouter } from '../index.js'

const assert = (c, m) => { if (!c) { console.error('FAIL:', m); cleanupSync(); process.exit(1) } console.log('  ok -', m) }
const iso = '2026-07-21T00:00:00.000Z'
const dirs = []
const toClose = []
function newDir (p) { const d = mkdtempSync(join(tmpdir(), p)); dirs.push(d); return d }
function cleanupSync () { for (const d of dirs) { try { rmSync(d, { recursive: true, force: true }) } catch {} } }
const post = (rk, text) => ({ action: 'create', collection: 'app.bsky.feed.post', rkey: rk, record: { $type: 'app.bsky.feed.post', text, createdAt: iso, langs: ['en'] } })

const id = createAtprotoIdentityFromKey(new Uint8Array(32).fill(7), { handle: 'coowned.test', pdsUrl: 'https://pds.local' })
const DID = id.did

const testnet = await createTestnet(3)
const bootstrap = testnet.bootstrap

// A co-owner: its own cores, nCommits signed over the SHARED DID key. All co-owners join the DID topic
// AT ONCE, which is what puts their announces inside the primary's first in-flight provide().
async function coOwner (nCommits) {
  const store = openCorestore(newDir('coowner-'))
  const repo = await new RepoStore(store, { name: `atproto-repo:${DID}` }).ready()
  let micros = 1_700_000_000_000_000n
  const mgr = await new RepoManager(repo, { did: DID, privateKey: id.privateKey, clock: () => (micros += 1_000_000n) }).init()
  const seq = await new FirehoseSequencer(store.get({ name: 'firehose' })).ready()
  for (let i = 0; i < nCommits; i++) { const w = await mgr.applyWrite(post('3post' + i + 'aaaaaa2', 'c' + i)); await seq.sequenceCommit(w, { time: iso }) }
  const peer = new DidPeer(store, { did: DID, verifyKey: id.publicKey })
  await peer.provide({ repoKey: repo.core.key, firehoseKey: seq.core.key })
  const swarm = new Hyperswarm({ bootstrap })
  swarm.on('connection', (c) => peer.attach(c))
  await swarm.join(discoveryTopic(DID), { server: true, client: false }).flushed()
  toClose.push(async () => { try { await swarm.destroy() } catch {}; try { await peer.close() } catch {}; try { await store.close() } catch {} })
  return { peer, fhKey: seq.core.key.toString('hex'), nCommits, rev: peer.head.rev }
}

// THREE co-owners: with two, an aliasing bug still had a 50% chance of aliasing onto the freshest and
// looking correct. The record counts also disambiguate WHICH co-owner is being served.
const owners = [await coOwner(1), await coOwner(3), await coOwner(2)]
const expectedKeys = new Set(owners.map((o) => o.fhKey))
// Derive the expected winner from the holders' ACTUAL head revs rather than assuming "most commits wins".
// The injected clock is shared-base, so two co-owners can land the same TID timestamp and be ordered by
// the trailing clock-id — a fixture artifact, not a router property. Record counts differ per co-owner,
// so the served count still identifies WHICH front was picked.
const winner = owners.reduce((b, o) => (!b || o.rev > b.rev ? o : b), null)
console.log('co-owners:', owners.map((o) => `${o.fhKey.slice(0, 8)}(n=${o.nCommits} rev=${o.rev})`).join(' '))
console.log('highest-rev co-owner:', winner.fhKey.slice(0, 8), `(n=${winner.nCommits})`)

const store = openCorestore(newDir('reader-'))
const swarm = new Hyperswarm({ bootstrap })
const router = new DidPeerRouter(store)
swarm.on('connection', (c) => router.onconnection(c))
router.add(DID, { verifyKey: id.publicKey })
await swarm.join(discoveryTopic(DID), { server: false, client: true }).flushed()

// Let every co-owner announce and open, polling front() the way a real getRepo request does. A front
// EXISTING is not the same as its head having replicated + verified, so poll for convergence rather than
// breaking on front count (an early break reads a set that is complete but not yet reconciled).
const counts = []
for (let i = 0; i < 40; i++) {
  const f = await router.front(DID).catch(() => null)
  // listRecords returns { records, cursor } per the lexicon (0.2.10) — NOT a bare array.
  const recs = f ? ((await f.listRecords('app.bsky.feed.post').catch(() => null)) || {}).records || [] : []
  counts.push(recs.length)
  await new Promise((r) => setTimeout(r, 300))
}
console.log('  served record counts:', JSON.stringify(counts))

const set = router._fronts.get(DID) || new Map()
assert(set.size === owners.length, `every announced co-owner has a front entry (${set.size}/${owners.length})`)

// THE INVARIANT: distinct objects, each bound to the key it is filed under.
const objects = new Set()
let mismatched = null
for (const [mapKey, f] of set) {
  objects.add(f)
  const boundFh = f._firehoseKey ? b4a.toString(f._firehoseKey, 'hex') : null
  if (boundFh !== mapKey) mismatched = `${mapKey.slice(0, 8)} is bound to ${boundFh ? boundFh.slice(0, 8) : 'null'}`
}
assert(!mismatched, `each front is bound to its OWN firehose key (${mismatched || 'all match'})`)
assert(objects.size === owners.length, `fronts are DISTINCT objects, not aliases (${objects.size}/${owners.length})`)
assert([...set.keys()].every((k) => expectedKeys.has(k)), 'front keys are exactly the announced co-owners')

// With a real SET to choose from, freshest-by-rev serves the highest-rev co-owner.
assert(counts[counts.length - 1] === winner.nCommits,
  `freshest-by-rev serves the highest-rev co-owner (${winner.nCommits} records), got ${counts[counts.length - 1]}`)

console.log('\nCO-OWNER SET OK: N announced co-owners => N distinct fronts, each correctly bound; freshest served')
await swarm.destroy(); await router.close(); await store.close()
for (const close of toClose) await close()
await testnet.destroy()
cleanupSync()