OverviewFilesCommitsIssuesDocsReleasesPeers

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

62 lines · javascript ref:
// Slice 4 (epoch enforcement) — the last revocation layer (atproto-base-REVOCATION §3, keet
// verifyAttestation(…, latestReceipt)). A minimum-epoch receipt gates admission: any attestation
// whose epoch is below it is rejected. Bump it (+ re-attest survivors at the new epoch) to
// invalidate ALL stale/revoked proofs at once — mass invalidation, complementing the per-device ban.

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, encodeReceipt, receiptEpoch, attestDeviceChain, deviceKeetKeyPair, rootIdentityPublicKey } from 'atproto-room/lib/membership.js'
import { deriveAtprotoKey } from 'atproto-identity'

const ROOT = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
const DEVA = 'legal winner thank year wave sausage worth useful legal winner thank yellow'
const DID = 'did:plc:swarmepoch222222222222222'
const signKey = deriveAtprotoKey(ROOT)
const tmp = () => { const d = '/tmp/hpp-ep-' + Math.floor(Math.random() * 1e9); fs.mkdirSync(d, { recursive: true }); return d }
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))

test('enforcement: an attestation below the minimum epoch is rejected, at/above it is accepted', async (t) => {
  const rootPub = await rootIdentityPublicKey(ROOT)
  const dev = await deviceKeetKeyPair(DEVA)
  const proof = await attestDeviceChain({ rootMnemonic: ROOT, devicePublicKey: dev.publicKey })
  const verify = makeVerifyAttest(rootPub)
  const attest = makeAttest(dev, proof)
  const signData = b4a.from('epoch-enforcement-test')
  const attestation = await attest(signData)

  const r0 = await verify(attestation, signData, dev.publicKey)
  t.ok(r0 && r0.receipt, 'accepted with no minimum epoch')
  const e = receiptEpoch(r0.receipt)
  t.ok(e > 0, 'the proof carries an epoch (' + e + ')')

  t.ok(await verify(attestation, signData, dev.publicKey, encodeReceipt(e)), 'accepted when epoch == minimum')
  t.ok(await verify(attestation, signData, dev.publicKey, encodeReceipt(e - 1)), 'accepted when epoch > minimum')
  t.absent(await verify(attestation, signData, dev.publicKey, encodeReceipt(e + 1000)), 'REJECTED when epoch < minimum (stale proof after a bump)')
})

test('bumpEpoch: a role-1 member sets the minimum epoch (authorized + durable)', async (t) => {
  const rootPub = await rootIdentityPublicKey(ROOT)
  const dev = await deviceKeetKeyPair(DEVA); const proof = await attestDeviceChain({ rootMnemonic: ROOT, devicePublicKey: dev.publicKey })
  const idA = b4a.toString(dev.publicKey, 'hex')
  const members = [{ key: idA, 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)
  t.teardown(async () => { try { await A.close() } catch {} })
  const deviceA = new SwarmDevice(A, { did: DID, privateKey: signKey.privateKey, self: idA })

  t.is(await deviceA.minEpoch(), 0, 'no minimum epoch by default (unrestricted)')
  const EPOCH = 1780000000000
  await deviceA.bumpEpoch(EPOCH)
  for (let i = 0; i < 12 && await deviceA.minEpoch() !== EPOCH; i++) { await A.base.update().catch(() => {}); await sleep(200) }
  t.is(await deviceA.minEpoch(), EPOCH, 'minimum epoch bumped + durable in the shared state')
})