OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-proto-peer / test / swarm-device-reconcile.test.mjs

129 lines · javascript ref:
// PART D — adopt-from-CAR fork reconciliation (the RECOVERY for the wj62 fork).
//
// An autobase fork of a shared DID (two divergent authored lines, same base.key) can't be merged in
// place. The recovery: re-adopt the NETWORK-AUTHORITATIVE signed repo (its CAR) into a FRESH line, then
// UNION any record that only landed on the LOSING line so NOTHING is lost, then sign FORWARD (rev > CAR
// rev, prev = CAR commit) as a monotonic continuation the relay accepts. Proves: the winner's records
// are all preserved, a loser-only record is kept (no data loss), the head advances forward, the
// anti-re-author guard holds, and a loser that is a strict subset produces no spurious commit.

import '../lib/bare-globals.js'
import test from 'brittle'
import b4a from 'b4a'
import { SwarmDevice } from '../lib/swarm-device.js'
import { readAdoptRepo } from '../lib/device-node.js'
import { createRepo } from 'atproto-repo/lib/repo.js'
import { tidNow } from 'atproto-store/lib/tid.js'

const DID = 'did:plc:reconcile'
const KEY = (() => { const k = b4a.alloc(32); k[31] = 11; return k })()
const post = (text) => ({ $type: 'app.bsky.feed.post', text, createdAt: '2026-07-20T00:00:00.000Z' })
const follow = (subj) => ({ $type: 'app.bsky.graph.follow', subject: subj, createdAt: '2026-07-20T00:00:00.000Z' })

class MemKV {
  constructor () { this.m = new Map() }
  async set (k, v) { this.m.set(k, v) }
  async get (k) { return this.m.has(k) ? this.m.get(k) : null }
  async getNow (k) { return this.get(k) }
  async del (k) { this.m.delete(k) }
  async list (prefix = '') { const o = {}; for (const [k, v] of this.m) if (k.startsWith(prefix)) o[k] = v; return o }
  async listNow (prefix = '') { return this.list(prefix) }
}

test('reconcile: adopt winner CAR + union loser-only record ⇒ UNION, no loss, forward continuation', async (t) => {
  // WINNER (authoritative, e.g. tabertron's 3mr436): 3 records. Build its signed CAR.
  const carRev = tidNow()
  const winner = [
    { collection: 'app.bsky.feed.post', rkey: 'w1', record: post('winner-one') },
    { collection: 'app.bsky.feed.post', rkey: 'shared', record: post('shared-post') },
    { collection: 'app.bsky.feed.post', rkey: 'w2', record: post('winner-two') }
  ]
  const { carBytes } = await createRepo({ records: winner, did: DID, rev: carRev, privateKey: KEY })

  // LOSER (e.g. stortron's 85): shares one post, but has a follow that ONLY landed here (must NOT be lost).
  const loser = [
    { collection: 'app.bsky.feed.post', rkey: 'shared', record: post('shared-post') },
    { collection: 'app.bsky.graph.follow', rkey: 'onlyL', record: follow('did:plc:someone') }
  ]

  // FRESH reconcile device (own MemKV = clean line). Adopt the CAR + union the loser-only record.
  const dev = new SwarmDevice(new MemKV(), { did: DID, privateKey: KEY, self: 'reconciler', firehose: true })
  const parsed = await readAdoptRepo(carBytes)
  t.is(parsed.records.length, 3, 'CAR parsed to the 3 winner records')

  const r = await dev.reconcileFromCar(parsed, loser)
  t.alike({ adopted: r.adopted, unioned: r.unioned, total: r.total }, { adopted: 3, unioned: 1, total: 4 },
    'adopted 3 winner records + unioned 1 loser-only (shared post skipped) = 4')

  // UNION + NO LOSS: the reconciled line has all 3 winner records AND the loser-only follow.
  const finalKeys = (await dev.repo.listRecords()).map((x) => x.collection + '/' + x.rkey).sort()
  t.alike(finalKeys, ['app.bsky.feed.post/shared', 'app.bsky.feed.post/w1', 'app.bsky.feed.post/w2', 'app.bsky.graph.follow/onlyL'],
    'reconciled set = UNION of winner ∪ loser (nothing dropped)')
  t.alike(await dev.repo.getRecord('app.bsky.graph.follow', 'onlyL'), follow('did:plc:someone'), 'the loser-only record survived byte-exact')
  t.alike(await dev.repo.getRecord('app.bsky.feed.post', 'w2'), post('winner-two'), 'a winner-only record survived byte-exact')

  // FORWARD continuation: the reconciled head is a new commit AFTER the adopted CAR head.
  t.ok(r.head && r.head.rev > carRev, `head advanced forward (${carRev} -> ${r.head.rev}) — relay-monotonic`)
  t.is((await dev.firehoseLog())[0].ops.length, 1, 'exactly ONE forward op (the unioned follow) — no re-author of the winner set')
})

test('reconcile: loser is a strict SUBSET ⇒ no spurious commit (adopted CAR head is final)', async (t) => {
  const carRev = tidNow()
  const winner = [
    { collection: 'app.bsky.feed.post', rkey: 'w1', record: post('one') },
    { collection: 'app.bsky.feed.post', rkey: 'w2', record: post('two') }
  ]
  const { carBytes, dataCid } = await createRepo({ records: winner, did: DID, rev: carRev, privateKey: KEY })
  const loser = [{ collection: 'app.bsky.feed.post', rkey: 'w1', record: post('one') }] // ⊂ winner

  const dev = new SwarmDevice(new MemKV(), { did: DID, privateKey: KEY, self: 'reconciler', firehose: true })
  const r = await dev.reconcileFromCar(await readAdoptRepo(carBytes), loser)

  t.alike({ adopted: r.adopted, unioned: r.unioned }, { adopted: 2, unioned: 0 }, 'nothing to union (loser ⊆ winner)')
  t.is(r.head.dataCid.toString(), dataCid.toString(), 'head is the adopted CAR head unchanged (no forward commit)')
  t.is(r.head.rev, carRev, 'rev unchanged — no spurious re-sign')
  t.is((await dev.firehoseLog()).length, 0, 'no #commit emitted (adopt sets head without a firehose frame)')
})

test('reconcile: anti-re-author guard — a CAR whose records do NOT reproduce its data root is REJECTED', async (t) => {
  const carRev = tidNow()
  const { carBytes } = await createRepo({ records: [{ collection: 'app.bsky.feed.post', rkey: 'w1', record: post('one') }], did: DID, rev: carRev, privateKey: KEY })
  const parsed = await readAdoptRepo(carBytes)
  parsed.records = [] // tamper: claim zero records but keep the non-empty head → would re-author/wipe
  const dev = new SwarmDevice(new MemKV(), { did: DID, privateKey: KEY, self: 'reconciler', firehose: true })
  await t.exception(() => dev.reconcileFromCar(parsed, []), /do not reproduce the adopted data root/,
    'reconcile refuses an incomplete CAR (the anti-re-author guard fires before any write)')
})

// get-repo-car: a device exports its signed repo as a CAR; a PEER reconciles from it (the operational
// heal path — fetch the authoritative node's CAR → reconcileFromCar into a fresh line).
test('exportCar → readAdoptRepo → reconcileFromCar round-trips (the heal fetch path)', async (t) => {
  // Authoritative device A: author + sign a repo (its own line).
  const A = new SwarmDevice(new MemKV(), { did: DID, privateKey: KEY, self: 'A', firehose: true })
  await A.putRecord('app.bsky.feed.post', 'a1', post('one'))
  await A.putRecord('app.bsky.feed.post', 'a2', post('two'))
  await A.putRecord('app.bsky.actor.profile', 'self', { $type: 'app.bsky.actor.profile', displayName: 'T' })
  const headA = await A.sign({ force: true })

  const exp = await A.exportCar()
  t.is(exp.recordCount, 3, 'exportCar reports the record count')
  t.is(exp.dataCid, headA.dataCid.toString(), 'exported CAR data root == the signed head data root (integrity)')
  t.is(exp.commitCid, headA.commitCid.toString(), 'exported CAR commit == the signed head commit (uses existing signature)')

  // A PEER parses the CAR + reconciles from it into a FRESH line.
  const parsed = await readAdoptRepo(exp.carBytes)
  t.is(parsed.records.length, 3, 'peer parsed 3 records from the exported CAR')
  t.is(parsed.head.dataCid.toString(), headA.dataCid.toString(), 'peer parsed head matches A (round-trip)')

  const C = new SwarmDevice(new MemKV(), { did: DID, privateKey: KEY, self: 'C', firehose: true })
  const r = await C.reconcileFromCar(parsed, [])
  t.is(r.adopted, 3, 'peer adopted all 3 records from the exported CAR')
  const keys = (await C.repo.listRecords()).map((x) => x.collection + '/' + x.rkey).sort()
  t.alike(keys, ['app.bsky.actor.profile/self', 'app.bsky.feed.post/a1', 'app.bsky.feed.post/a2'], 'peer reproduced A\'s exact record set')
})

test('exportCar refuses when nothing is signed yet', async (t) => {
  const D = new SwarmDevice(new MemKV(), { did: DID, privateKey: KEY, self: 'D', firehose: true })
  await t.exception(() => D.exportCar(), /no signed head/, 'exportCar throws on an unsigned device (nothing to export)')
})