// 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')
})