OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-room / test / signer-presence-currency.test.mjs

65 lines · javascript ref:
// SignerPresence CURRENCY election — the anti-freeze fix for the stortron/tabertron split-brain aftermath.
//
// The forward-only sign gate stops a BEHIND signer from publishing a regression — but on its own it can
// FREEZE the chain: the lowest-key co-owner still wins the presence election and holds the lease, the gate
// blocks it (it's behind), and the caught-up peer defers to it → nobody signs. The election must therefore
// exclude a behind co-owner so the caught-up peer signs instead. Deterministic: every peer computes the
// same signer from (presence, announced heads, netHead), so there is still exactly one signer (no fork).
import test from 'brittle'
import { SignerPresence } from '../lib/signer-presence.js'

// Faithful to live: stortron key < tabertron key (lowest key = classic election winner).
const STOR = '71555d858367c85346d3e19752e82ec889613298e7da1066b9362130e5103583'
const TAB = '81e1cbbf9169d43e2232fca60e10ff937783790eba06c8cb0a74fff12e285715'
const BEHIND = '3mqzqosnt6c22' // 85-record head (stortron's stale line)
const AHEAD = '3mr4ocucu6c22' // 91-record head (tabertron's advanced line) — AHEAD > BEHIND lexicographically
const NOW = 1000
const mk = (self) => new SignerPresence({ self, clock: () => NOW })

test('currency election: a BEHIND lowest-key co-owner does NOT freeze the signer — the caught-up peer signs', (t) => {
  // From STORTRON's view (self behind at BEHIND; TAB present + caught up at AHEAD):
  const stor = mk(STOR)
  stor.setSelfHead(BEHIND)
  stor.markPresent(TAB, AHEAD)
  t.is(stor.currentSigner(NOW, AHEAD), TAB, 'behind STOR (lowest key) does NOT win — caught-up TAB does')
  t.absent(stor.holds(NOW, AHEAD), 'behind STOR does not hold the signer role (so the gate-skip is not a freeze)')

  // From TABERTRON's view (self caught up at AHEAD; STOR present but behind at BEHIND):
  const tab = mk(TAB)
  tab.setSelfHead(AHEAD)
  tab.markPresent(STOR, BEHIND)
  t.is(tab.currentSigner(NOW, AHEAD), TAB, 'TAB computes ITSELF the signer (behind STOR excluded)')
  t.ok(tab.holds(NOW, AHEAD), 'caught-up TAB holds the signer role → it signs → chain advances (unfreeze)')
  // Both peers elected TAB — deterministic, exactly one signer, no fork.
})

test('currency election: once the behind co-owner CATCHES UP, the lowest key reclaims the role', (t) => {
  const stor = mk(STOR)
  stor.markPresent(TAB, AHEAD)
  stor.setSelfHead(BEHIND)
  t.is(stor.currentSigner(NOW, AHEAD), TAB, 'while behind, STOR defers to TAB')
  stor.setSelfHead(AHEAD) // STOR replicated the missing records + caught up
  t.is(stor.currentSigner(NOW, AHEAD), STOR, 'caught-up STOR (lowest key) reclaims the signer role — normal steady state')
})

test('currency election: no netHead / no currency info → presence-only lowest-key (backward compatible)', (t) => {
  const sp = mk(STOR)
  sp.markPresent(TAB)
  t.is(sp.currentSigner(NOW), STOR, 'no netHead → classic lowest-key present election, unchanged')
  // netHead given but NOBODY is known-caught-up → fall back to lowest-key present; the forward-only sign
  // gate still prevents a behind sign, so the worst case is a transient no-op, never a regression/freeze-forever.
  t.is(sp.currentSigner(NOW, AHEAD), STOR, 'netHead but no currency announced → lowest-key fallback (gate still guards)')
})

test('currency election: noteHead (peer announcement) flips the elected signer + fires onChange', (t) => {
  let fired = 0
  const tab = new SignerPresence({ self: TAB, clock: () => NOW, onChange: () => { fired++ } })
  tab.setSelfHead(AHEAD)
  tab.markPresent(STOR) // STOR present, currency unknown yet
  t.is(tab.currentSigner(NOW, AHEAD), TAB, 'unknown STOR currency → TAB (self, caught up) signs')
  fired = 0
  tab.noteHead(STOR, AHEAD) // STOR announces it caught up
  t.ok(fired > 0, 'a currency change fires onChange (re-evaluate the signer)')
  t.is(tab.currentSigner(NOW, AHEAD), STOR, 'now STOR is known caught up + lower key → it becomes the signer')
})