OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / device-invite-writer-mismatch.test.js

119 lines · javascript ref:
// Hypothesis test: DeviceLinker pre-computes a writer key that target
// daemons (post the keet-pattern revert 3b815e8) will never use.
//
// Concrete claim:
//   1. DeviceLinker.createInvite generates newDeviceKp + computes
//      autobaseLocalKey = Hypercore.key({ signers: [{ publicKey: newDeviceKp.publicKey }] })
//      Source addWriters that.
//   2. If target's Autobase is opened with `keyPair: newDeviceKp`,
//      base.local.key matches. ✓
//   3. If target's Autobase is opened WITHOUT `keyPair` (corestore
//      auto-derives `{name:'local'}` per the keet-pattern + post-revert
//      IdentityRegistry construction), base.local.key does NOT match. ✗
//
// If predictions 2 and 3 both pass, the device-invite ↔ post-revert
// IdentityRegistry contract is broken: addWriter goes to a key the
// target will never write under.

import test from 'brittle'
import Autobase from 'autobase'
import Corestore from 'corestore'
import Hypercore from 'hypercore'
import crypto from 'hypercore-crypto'
import b4a from 'b4a'
import os from '#node:os'
import path from '#node:path'
import fs from '#node:fs'

const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'dvi-mismatch-'))
let n = 0
const tmp = () => path.join(TMP, 'store-' + (n++))

async function apply (nodes, view, host) {
  for (const node of nodes) await view.append(node.value)
}

function predictedKey (publicKey) {
  // Exact formula DeviceLinker.createInvite uses.
  return Hypercore.key({
    version: 1,
    signers: [{ publicKey }]
  })
}

test('PREDICTION 1: Autobase opened with keyPair: K → base.local.key matches Hypercore.key({signers:[{publicKey: K.publicKey}]})', async (t) => {
  const newDeviceKp = crypto.keyPair()
  const predicted = predictedKey(newDeviceKp.publicKey)

  const store = new Corestore(tmp())
  await store.ready()
  const base = new Autobase(store, null, {
    apply,
    open: (s) => s.get('view', { valueEncoding: 'json' }),
    valueEncoding: 'json',
    keyPair: newDeviceKp,                  // ← target uses newDeviceKp as autobase keypair
    optimistic: true,
    ackInterval: 1000
  })
  await base.ready()

  console.log('  newDeviceKp.publicKey:', b4a.toString(newDeviceKp.publicKey, 'hex').slice(0, 16) + '…')
  console.log('  predicted:            ', b4a.toString(predicted, 'hex').slice(0, 16) + '…')
  console.log('  base.local.key:       ', b4a.toString(base.local.key, 'hex').slice(0, 16) + '…')

  t.ok(b4a.equals(base.local.key, predicted),
    'with keyPair: newDeviceKp, base.local.key MATCHES the DeviceLinker-predicted autobaseLocalKey')

  await base.close(); await store.close()
})

test('PREDICTION 2: Autobase opened WITHOUT keyPair → base.local.key does NOT match Hypercore.key({signers:[{publicKey: K.publicKey}]})', async (t) => {
  // Same newDeviceKp the source generated and addWritered.
  const newDeviceKp = crypto.keyPair()
  const predicted = predictedKey(newDeviceKp.publicKey)

  // Target opens Autobase the way IdentityRegistry does post-3b815e8 revert:
  // no keyPair passed.
  const store = new Corestore(tmp())
  await store.ready()
  const base = new Autobase(store, null, {
    apply,
    open: (s) => s.get('view', { valueEncoding: 'json' }),
    valueEncoding: 'json',
    // keyPair: undefined,   ← exactly what post-revert IdentityRegistry passes
    optimistic: true,
    ackInterval: 1000
  })
  await base.ready()

  console.log('  newDeviceKp.publicKey:', b4a.toString(newDeviceKp.publicKey, 'hex').slice(0, 16) + '…')
  console.log('  predicted:            ', b4a.toString(predicted, 'hex').slice(0, 16) + '…')
  console.log('  base.local.key:       ', b4a.toString(base.local.key, 'hex').slice(0, 16) + '…')

  t.absent(b4a.equals(base.local.key, predicted),
    'without keyPair, base.local.key does NOT match DeviceLinker-predicted autobaseLocalKey — ' +
    'so the addWriter goes to a key target never writes under (BUG CONFIRMED)')

  await base.close(); await store.close()
})

test('CROSS-CHECK: a SECOND fresh corestore (no keyPair) gets a DIFFERENT base.local.key — so the post-revert key is corestore-instance-specific, not derivable in advance', async (t) => {
  const storeA = new Corestore(tmp()); await storeA.ready()
  const baseA = new Autobase(storeA, null, { apply, open: (s) => s.get('view', { valueEncoding: 'json' }), valueEncoding: 'json', optimistic: true, ackInterval: 1000 })
  await baseA.ready()

  const storeB = new Corestore(tmp()); await storeB.ready()
  const baseB = new Autobase(storeB, null, { apply, open: (s) => s.get('view', { valueEncoding: 'json' }), valueEncoding: 'json', optimistic: true, ackInterval: 1000 })
  await baseB.ready()

  console.log('  baseA.local.key:', b4a.toString(baseA.local.key, 'hex').slice(0, 16) + '…')
  console.log('  baseB.local.key:', b4a.toString(baseB.local.key, 'hex').slice(0, 16) + '…')

  t.absent(b4a.equals(baseA.local.key, baseB.local.key),
    'two independent corestores without shared keyPair generate DIFFERENT local keys ' +
    '— confirms post-revert key is per-corestore, source cannot pre-compute it')

  await baseA.close(); await storeA.close()
  await baseB.close(); await storeB.close()
})