OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-proto-peer / test / adopt.test.js

89 lines · javascript ref:
// Adopt-into-swarm continuity (the wj62 path). An EXISTING single-tenant repo (records + head,
// signed by a raw #atproto key) is imported into a swarm that shares that key + has a SEPARATE keet
// membership root. The swarm must CONTINUE the chain — next commit.prev = the adopted commit,
// prevData = the adopted data root — NOT re-author (which would break federation). Deterministic
// (network:false). This is what makes converting wj62 to a swarm safe.

import '../lib/bare-globals.js'
import test from 'brittle'
import crypto from 'hypercore-crypto'
import fs from 'bare-fs'
import b4a from 'b4a'
import { StateBaseV3 } from 'atproto-room/lib/state-base.js'
import { SwarmDevice } from '../lib/swarm-device.js'
import { makeAttest, makeVerifyAttest, attestDeviceChain, deviceKeetKeyPair, rootIdentityPublicKey } from 'atproto-room/lib/membership.js'
import { createRepo, readCar, loadCommit, listRecords } from 'atproto-repo/lib/repo.js'
import { verifyCommit } from 'atproto-repo/lib/commit.js'
import { deriveAtprotoKey } from 'atproto-identity'
import { decodeDidKey } from 'atproto-identity/lib/did-key.js'

const SIGN_SRC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about' // stand-in for wj62's raw signing key
const KEET_ROOT = 'legal winner thank year wave sausage worth useful legal winner thank yellow'                // NEW keet membership root
const DEV = 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong'                                                // this device's identity
const DID = 'did:plc:adoptwj6222222222222222'
const POST = 'app.bsky.feed.post'
const signKey = deriveAtprotoKey(SIGN_SRC)
const { pubkey } = decodeDidKey(signKey.didKey)
const REV0 = '3mmwu7vak2000'; const RK1 = '3mmwu7vak2001'; const RK2 = '3mmwu7vak2002'; const RK3 = '3mmwu7vak2003'
const tmp = () => { const d = '/tmp/hpp-adopt-' + Math.floor(Math.random() * 1e9); fs.mkdirSync(d, { recursive: true }); return d }

test('the swarm continues an adopted repo chain (prev = adopted commit), not re-authors it', async (t) => {
  // 1. an EXISTING repo (stand-in for wj62's), signed by the shared key
  const existingRecords = [
    { collection: POST, rkey: RK1, record: { $type: POST, text: 'wj62 post 1', createdAt: '2026-07-01T00:00:00.000Z' } },
    { collection: POST, rkey: RK2, record: { $type: POST, text: 'wj62 post 2', createdAt: '2026-07-01T00:00:01.000Z' } }
  ]
  const existing = await createRepo({ records: existingRecords, did: DID, rev: REV0, privateKey: signKey.privateKey })
  const head = { rev: REV0, commitCid: existing.commitCid, dataCid: existing.dataCid, signedCommit: loadCommit(await readCar(existing.carBytes)).value }

  // 2. a swarm device for the SAME DID + signing key, with a NEW keet membership root
  const rootPub = await rootIdentityPublicKey(KEET_ROOT)
  const dev = await deviceKeetKeyPair(DEV); const proof = await attestDeviceChain({ rootMnemonic: KEET_ROOT, devicePublicKey: dev.publicKey })
  const members = [{ key: b4a.toString(dev.publicKey, 'hex'), role: 1 }]
  const A = new StateBaseV3(tmp(), crypto.randomBytes(32), {
    roomSeed: crypto.randomBytes(32), encryptionKey: crypto.randomBytes(32), network: false,
    selfKey: dev.publicKey, attest: makeAttest(dev, proof), verifyAttest: makeVerifyAttest(rootPub),
    getMembers: async () => members
  })
  await A.ready(); await A.projectMembers(members)
  const device = new SwarmDevice(A, { did: DID, privateKey: signKey.privateKey, self: b4a.toString(dev.publicKey, 'hex'), firehose: true })
  t.teardown(async () => { try { await A.close() } catch {} })

  // 3. adopt — the continuity check runs inside (imported records must reproduce the data root)
  const adopted = await device.adopt(existingRecords, head)
  t.ok(adopted.commitCid.equals(existing.commitCid), 'adopted head == the existing commit')
  t.alike((await device.repo.listRecords()).map((r) => r.rkey).sort(), [RK1, RK2].sort(), 'existing records imported into the swarm')

  // 4. a NEW swarm write continues the chain
  await device.putRecord(POST, RK3, { $type: POST, text: 'first swarm-signed post', createdAt: '2026-07-05T00:00:00.000Z' })
  await device.claimLease()
  const nh = await device.sign()
  t.ok(nh && nh.signedCommit, 'swarm signed a continuing commit')
  t.ok(nh.signedCommit.prev && nh.signedCommit.prev.equals(existing.commitCid), 'new commit.prev == the ADOPTED commit (continues, not re-authors)')
  t.ok(verifyCommit(nh.signedCommit, pubkey), 'continuing commit verifies against the DID signing key')
  const fh = await device.firehoseLog()
  t.is(fh[fh.length - 1].prevData, existing.dataCid.toString(), 'firehose #commit prevData == adopted data root (relay-invertible continuity)')
})

test('continuity guard: an INCOMPLETE import is rejected (would re-author)', async (t) => {
  const existingRecords = [
    { collection: POST, rkey: RK1, record: { $type: POST, text: 'wj62 post 1', createdAt: '2026-07-01T00:00:00.000Z' } },
    { collection: POST, rkey: RK2, record: { $type: POST, text: 'wj62 post 2', createdAt: '2026-07-01T00:00:01.000Z' } }
  ]
  const existing = await createRepo({ records: existingRecords, did: DID, rev: REV0, privateKey: signKey.privateKey })
  const head = { rev: REV0, commitCid: existing.commitCid, dataCid: existing.dataCid, signedCommit: loadCommit(await readCar(existing.carBytes)).value }
  const rootPub = await rootIdentityPublicKey(KEET_ROOT)
  const dev = await deviceKeetKeyPair(DEV); const proof = await attestDeviceChain({ rootMnemonic: KEET_ROOT, devicePublicKey: dev.publicKey })
  const members = [{ key: b4a.toString(dev.publicKey, 'hex'), role: 1 }]
  const A = new StateBaseV3(tmp(), crypto.randomBytes(32), {
    roomSeed: crypto.randomBytes(32), encryptionKey: crypto.randomBytes(32), network: false,
    selfKey: dev.publicKey, attest: makeAttest(dev, proof), verifyAttest: makeVerifyAttest(rootPub),
    getMembers: async () => members
  })
  await A.ready(); await A.projectMembers(members)
  const device = new SwarmDevice(A, { did: DID, privateKey: signKey.privateKey, self: b4a.toString(dev.publicKey, 'hex'), firehose: true })
  t.teardown(async () => { try { await A.close() } catch {} })
  // only ONE of the two records → data root won't match → MUST throw (prevents a re-authoring cutover)
  await t.exception(() => device.adopt([existingRecords[0]], head), /re-author|reproduce/, 'incomplete import rejected on a fresh device')
})