OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-proto-peer / test / swarm-device-forward-only-sign.test.mjs

82 lines · javascript ref:
// FORWARD-ONLY SIGN GATE — the anti-re-fork guard that makes re-admitting a healed peer safe.
//
// The stortron/tabertron split-brain: a stale/rejoined co-owner wins the lowest-key signer election and
// materializes the atproto chain from a LAGGING view — publishing either a REV regression (a rev <= one
// already served) or a CONTENT regression (a commit that drops records the network head already served).
// keet never has this because it projects nothing externally; our relay projection makes "temporarily
// behind" a published regression. The gate re-anchors to the network-latest signed commit and refuses to
// sign while the view is missing any record the net head served — so a lagging signer only ever CONTINUES
// the winning line, never rewinds it. Proves: caught-up re-anchors + signs forward (no false skip); a
// view behind the net head SKIPS (no content regression).

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

const DID = 'did:plc:forwardgate'
const KEY = (() => { const k = b4a.alloc(32); k[31] = 7; return k })()
const post = (text) => ({ $type: 'app.bsky.feed.post', text, 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('forward-only gate: caught-up device re-anchors to the network head + signs forward (no false skip)', async (t) => {
  const sb = new MemKV()
  // Device A authors 3 records + signs → #commit frames + record pointers land in the shared sb.
  const A = new SwarmDevice(sb, { did: DID, privateKey: KEY, self: 'A', firehose: true })
  await A.putRecord('app.bsky.feed.post', 'r1', post('one'))
  await A.putRecord('app.bsky.feed.post', 'r2', post('two'))
  await A.putRecord('app.bsky.feed.post', 'r3', post('three'))
  const hA = await A.sign({ force: true })
  t.ok(hA && hA.rev, 'A signed a network head')

  // Device B shares the sb (fully converged view) but starts with NO in-memory head (stale restart).
  const B = new SwarmDevice(sb, { did: DID, privateKey: KEY, self: 'B', firehose: true })
  const hB = await B.sign() // non-force: the gate runs
  t.is(hB, null, 'no new records beyond the net head → no-op (did NOT re-author or regress)')
  t.is(B.head && B.head.rev, hA.rev, 'B re-anchored its head to the network-latest rev (no rev regression)')

  // B adds a genuinely NEW record → its view is now a SUPERSET of the net head → signs FORWARD.
  await B.putRecord('app.bsky.feed.post', 'r4', post('four'))
  const hB2 = await B.sign()
  t.ok(hB2 && hB2.rev > hA.rev, `B signed forward past the net head (${hA.rev} -> ${hB2 && hB2.rev})`)
})

test('forward-only gate: a view BEHIND the network head SKIPS signing (no content regression)', async (t) => {
  const sb = new MemKV()
  const A = new SwarmDevice(sb, { did: DID, privateKey: KEY, self: 'A', firehose: true })
  await A.putRecord('app.bsky.feed.post', 'r1', post('one'))
  await A.putRecord('app.bsky.feed.post', 'r2', post('two'))
  const hA = await A.sign({ force: true })

  // Craft a GHOST network frame at a HIGHER rev that references r3 — a record whose POINTER no device has
  // replicated yet (so every device's view lists only {r1,r2}). Clone A's real frame for a valid,
  // decodable signed commit; only rev + ops change. This is the "net head is ahead of my view" state.
  const frames = Object.values(await sb.listNow('__commit__/'))
  const real = frames[frames.length - 1]
  const ghostRev = tidNow()
  await sb.set('__commit__/' + ghostRev, { ...real, rev: ghostRev, ops: [{ action: 'create', path: 'app.bsky.feed.post/r3', cid: null, prev: null }], seq: (real.seq || 1) + 1 })

  // A signer whose view has {r1,r2} but whose net head knows {r1,r2,r3} must NOT sign — signing would
  // materialize a {r1,r2} MST forward from the r3-bearing head = a commit that DELETES r3.
  const B = new SwarmDevice(sb, { did: DID, privateKey: KEY, self: 'B', firehose: true })
  const hB = await B.sign()
  t.is(hB, null, 'behind view (missing r3) → forward-only gate SKIPS the sign (no delete-r3 regression)')

  // force:true (bootstrap/reconcile paths that deliberately own head) bypasses the gate. Add a new
  // record so there's something to materialize, then force-sign despite the "behind" state — it must
  // NOT be blocked by the gate (a non-force sign would skip here; see the assertion above).
  await B.putRecord('app.bsky.feed.post', 'r4', post('four'))
  const hF = await B.sign({ force: true })
  t.ok(hF && hF.rev, 'force sign bypasses the gate (bootstrap/reconcile continuity is unaffected)')
})