OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / identity-apply-determinism.test.js

115 lines · javascript ref:
// Determinism test for the IdentityRegistry apply function.
//
// Autobase requires apply to be DETERMINISTIC — it is replayed on every boot
// and every reorg, and each peer runs it independently to build its own view.
// If apply writes non-deterministic values, two peers replaying the SAME ops
// produce DIFFERENT view bytes, so their view cores and checkpoints can never
// match.
//
// identity-apply.js writes Date.now() into the view in three places:
//   :437  ADD_WRITER              -> { addedAt: Date.now() }
//   :474  REGISTER_MIRROR_WRITERS -> { recordedAt: Date.now() }
//   :428  SET_USERNAME            -> user.updatedAt = Date.now()
//
// Claim under test: replaying an identical op batch twice yields different
// stored bytes. The CONTROL uses REGISTER_IDENTITY, which stores only
// op-provided fields — it must come out byte-identical, proving any
// difference in the other cases is attributable to apply itself and not to
// the harness.

import test from 'brittle'
import Corestore from 'corestore'
import Hyperbee from 'hyperbee'
import b4a from 'b4a'
import os from '#node:os'
import path from '#node:path'
import fs from '#node:fs'
import { createApplyFunction } from '../node_modules/hyper-identity/lib/auth/identity-apply.js'
import { encodeOp, OpTypes } from '../node_modules/hyper-identity/lib/auth/identity-operations.js'

const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'apply-determinism-'))
let n = 0

async function freshView () {
  const store = new Corestore(path.join(TMP, 'store-' + (n++)))
  await store.ready()
  // Raw Corestore takes { name }; the string form in identity-registry.js:77
  // is autobase's view-store API, not this one.
  const bee = new Hyperbee(store.get({ name: 'identity-view' }), {
    extension: false, keyEncoding: 'utf-8', valueEncoding: 'json'
  })
  await bee.ready()
  return { store, bee }
}

// A writer whose addWriter never fails, so the ADD_WRITER path completes.
const stubHost = { addWriter: async () => {}, removeWriter: async () => {} }

const node = (op) => ({ value: encodeOp(op), from: { key: b4a.alloc(32) } })

// Run the REAL apply over a batch, return the stored value at `key`.
async function applyAndRead (batch, key) {
  const { store, bee } = await freshView()
  const apply = createApplyFunction({})
  await apply(batch, bee, stubHost)
  const entry = await bee.get(key)
  const value = entry ? entry.value : null
  await bee.close(); await store.close()
  return value
}

const sleep = (ms) => new Promise(r => setTimeout(r, ms))

test('CONTROL: an op storing only op-provided fields replays byte-identically', async (t) => {
  const identity = {
    identityId: 'ident-control',
    method: 'web-identity',
    publicKey: 'ff'.repeat(32),
    userId: 'user-control',
    createdAt: 1700000000000   // fixed by the op, not by apply
  }
  const batch = [node({ type: OpTypes.REGISTER_IDENTITY, identity })]

  const a = await applyAndRead(batch, 'identity:ident-control')
  await sleep(25)
  const b = await applyAndRead(batch, 'identity:ident-control')

  t.ok(a && b, 'both replays stored the record')
  t.alike(a, b, 'CONTROL replays byte-identical — the harness itself is deterministic')
})

test('ADD_WRITER: apply stamps Date.now() -> replays DIVERGE', async (t) => {
  const writerKey = 'ab'.repeat(32)
  const batch = [node({
    type: OpTypes.ADD_WRITER, writerKey, role: 'writer', addedBy: 'test'
  })]

  const a = await applyAndRead(batch, 'writer:' + writerKey)
  await sleep(25)
  const b = await applyAndRead(batch, 'writer:' + writerKey)

  t.ok(a && b, 'both replays stored the writer record')
  t.comment('replay A addedAt=' + (a && a.addedAt) + '  replay B addedAt=' + (b && b.addedAt))
  t.unlike(a, b,
    'identical ADD_WRITER op produces DIFFERENT view bytes on replay — ' +
    'apply is NON-DETERMINISTIC, so peers cannot converge on a shared view')
})

test('REGISTER_MIRROR_WRITERS: apply stamps Date.now() -> replays DIVERGE', async (t) => {
  const devicePubkey = 'cd'.repeat(32)
  const batch = [node({
    type: OpTypes.REGISTER_MIRROR_WRITERS,
    devicePubkey,
    keys: { orgs: 'ef'.repeat(32) }
  })]

  const a = await applyAndRead(batch, 'mirror-writers:' + devicePubkey)
  await sleep(25)
  const b = await applyAndRead(batch, 'mirror-writers:' + devicePubkey)

  t.ok(a && b, 'both replays stored the announcement')
  t.comment('replay A recordedAt=' + (a && a.recordedAt) + '  replay B recordedAt=' + (b && b.recordedAt))
  t.unlike(a, b,
    'identical REGISTER_MIRROR_WRITERS op produces DIFFERENT view bytes on replay')
})