OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-store / test / ensure-local-no-loss.test.js

75 lines · javascript ref:
// ensureLocal must never DESTROY bytes the converged pointer may still name.
//
// THE INCIDENT (loser-club, stortron, 2026-10-14). An operator `update` of app.bsky.labeler.service/self:
// put() wrote the new bytes (B) and appended the pointer — but BEFORE the linearized view showed it, a
// reader (the sign gate) called ensureLocal with the OLD converged pointer (A). The local file (B) did not
// match A, so it was treated as a superseded copy and atomically REPLACED with A, fetched from the
// co-owner. Then B's pointer converged — and no node held B anymore. Both co-owners' sign gates stalled
// ("pointer present, bytes unreplicated") until a further write superseded it.
//
// A local mismatch can be "behind" (a stale copy — replace it, ensure-local-verify.test.js) OR "ahead"
// (our own write, the view not yet caught up). The store cannot tell which. So it must not choose by
// destroying: the bytes it replaces are kept, content-addressed, and a later pointer to them resolves
// locally instead of stalling.
//
// Run: brittle-node test/ensure-local-no-loss.test.js
import test from 'brittle'
import fs from 'fs'
import os from 'os'
import path from 'path'
import crypto from 'hypercore-crypto'
import { OverlayRepoStore } from '../lib/overlay-repo-store.js'

const tmp = () => fs.mkdtempSync(path.join(os.tmpdir(), 'elnl-'))
const LABELER = 'app.bsky.labeler.service'
const A = { $type: LABELER, createdAt: '2026-10-14T01:19:49.000Z', policies: { labelValues: [] } }
const B = { $type: LABELER, createdAt: '2026-10-14T01:23:30.000Z', policies: { labelValues: ['spam'] } }

// A REALISTIC overlay: it serves an oid only while some registered path STILL holds bytes hashing to it
// (a real overlay indexes paths; overwrite the file and those bytes are gone from the network too).
function realisticOverlay (peers = []) {
  const ov = {
    paths: new Map(), // diskPath -> overlayPath
    async registerFile (overlayPath, diskPath) { ov.paths.set(diskPath, overlayPath); return { contentHash: hash(fs.readFileSync(diskPath)) } },
    async hashFile (p) { try { const b = fs.readFileSync(p); return { contentHash: hash(b), size: b.length } } catch { return null } },
    // What this node can serve right now.
    servable (oid) { for (const p of ov.paths.keys()) { try { const b = fs.readFileSync(p); if (hash(b) === oid) return b } catch {} } return null },
    async fetchFile (oid, o = {}) {
      for (const peer of peers) { const b = peer.servable(oid); if (b) { fs.writeFileSync(o.destPath, b); return { destPath: o.destPath, size: b.length } } }
      return null
    }
  }
  return ov
}
const hash = (b) => crypto.data(b).toString('hex')

test('⛔ a newer local write clobbered by a lagging pointer is RESTORED when its own pointer converges', async (t) => {
  // The co-owner holds A (replicated earlier); we hold A, then write B.
  const coOwner = realisticOverlay()
  const theirs = await new OverlayRepoStore(tmp(), { overlay: coOwner }).ready()
  const { oid: oidA } = await theirs.put(LABELER, 'self', A)

  const ours = await new OverlayRepoStore(tmp(), { overlay: realisticOverlay([coOwner]) }).ready()
  await ours.put(LABELER, 'self', A)
  const { oid: oidB } = await ours.put(LABELER, 'self', B) // the operator update
  t.not(oidA, oidB)

  // The view still shows A's pointer: the stale-copy rule replaces our file with A (unchanged behaviour).
  t.alike(await ours.ensureLocal(LABELER, 'self', oidA), A, 'the lagging pointer resolves to A')

  // Now B's pointer converges. Nobody on the network holds B any more — only what WE kept.
  const got = await ours.ensureLocal(LABELER, 'self', oidB)
  t.alike(got, B, 'B is restored from the bytes this node kept, instead of stalling the chain')
  t.is((await ours.localOid(LABELER, 'self')).oid, oidB, 'and the record file holds B again')
})

test('the kept bytes are verified: a cache entry that does not hash to the oid is never installed', async (t) => {
  const store = await new OverlayRepoStore(tmp(), { overlay: realisticOverlay() }).ready()
  const { oid: oidA } = await store.put(LABELER, 'self', A)
  const keptDir = path.join(store._root, 'superseded')
  fs.mkdirSync(keptDir, { recursive: true })
  fs.writeFileSync(path.join(keptDir, 'f'.repeat(64) + '.dagcbor'), Buffer.from('not the bytes'))
  t.is(await store.ensureLocal(LABELER, 'self', 'f'.repeat(64)), null, 'unverifiable → unresolved (the sign gate fails safe)')
  t.is((await store.localOid(LABELER, 'self')).oid, oidA, 'and the file we hold is untouched')
})