OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / anchor-proof.test.js

132 lines · javascript ref:
import test from 'brittle'
import b4a from 'b4a'
import sodium from 'sodium-universal'
import { auditAnchors, fetchAnchorProof } from 'p2p-hiverelay-verifier'

// ─── Helpers ───────────────────────────────────────────────────

function genKeypair () {
  const pk = b4a.alloc(sodium.crypto_sign_PUBLICKEYBYTES)
  const sk = b4a.alloc(sodium.crypto_sign_SECRETKEYBYTES)
  sodium.crypto_sign_keypair(pk, sk)
  return { pk, sk }
}

function buildProof ({ appKey, anchored, version, attestedAt, sk, pk }) {
  const tag = b4a.from('hiverelay-anchor-proof-v1')
  const keyBuf = b4a.from(appKey, 'hex')
  const versionBuf = b4a.alloc(8)
  new DataView(versionBuf.buffer, versionBuf.byteOffset).setBigUint64(0, BigInt(version), false)
  const tsBuf = b4a.alloc(8)
  new DataView(tsBuf.buffer, tsBuf.byteOffset).setBigUint64(0, BigInt(attestedAt), false)
  const flagBuf = b4a.from([anchored ? 1 : 0])
  const payload = b4a.concat([tag, keyBuf, versionBuf, tsBuf, flagBuf])
  const sig = b4a.alloc(sodium.crypto_sign_BYTES)
  sodium.crypto_sign_detached(sig, payload, sk)
  return {
    schemaVersion: 1,
    appKey,
    anchored,
    version,
    attestedAt,
    relayPubkey: b4a.toString(pk, 'hex'),
    signature: b4a.toString(sig, 'hex')
  }
}

function mockFetch (responses) {
  // url → response object
  return async (url) => {
    const r = responses[url]
    if (!r) return { ok: false, status: 404, text: async () => '' }
    if (r.networkError) throw new Error(r.networkError)
    return {
      ok: r.ok !== false,
      status: r.ok === false ? (r.status || 500) : 200,
      text: async () => JSON.stringify(r.body || {})
    }
  }
}

// ─── Tests ─────────────────────────────────────────────────────

const APP_KEY = b4a.toString(b4a.alloc(32, 0xab), 'hex')

test('fetchAnchorProof: returns verified=true for a properly-signed proof', async (t) => {
  const { pk, sk } = genKeypair()
  const proof = buildProof({ appKey: APP_KEY, anchored: true, version: 5, attestedAt: Date.now(), pk, sk })
  const fetch = mockFetch({
    ['https://relay.example/api/anchors/' + APP_KEY + '/proof']: { body: proof }
  })
  const v = await fetchAnchorProof('https://relay.example', APP_KEY, { fetch })
  t.is(v.ok, true, 'fetched ok')
  t.is(v.verified, true, 'signature verified')
  t.is(v.proof.anchored, true)
  t.is(v.proof.version, 5)
})

test('fetchAnchorProof: returns verified=false for a tampered signature', async (t) => {
  const { pk, sk } = genKeypair()
  const proof = buildProof({ appKey: APP_KEY, anchored: true, version: 5, attestedAt: Date.now(), pk, sk })
  // Tamper with the version after signing
  const tampered = { ...proof, version: 999 }
  const fetch = mockFetch({
    ['https://relay.example/api/anchors/' + APP_KEY + '/proof']: { body: tampered }
  })
  const v = await fetchAnchorProof('https://relay.example', APP_KEY, { fetch })
  t.is(v.ok, true)
  t.is(v.verified, false, 'tampered version → signature invalid')
})

test('fetchAnchorProof: returns ok=false for unreachable relay', async (t) => {
  const fetch = mockFetch({}) // empty responses → 404
  const v = await fetchAnchorProof('https://nope.example', APP_KEY, { fetch })
  t.is(v.ok, false)
})

test('auditAnchors: aggregates anchored / unanchored / unreachable / unverified', async (t) => {
  const { pk: pk1, sk: sk1 } = genKeypair()
  const { pk: pk2, sk: sk2 } = genKeypair()
  const { pk: pk3, sk: sk3 } = genKeypair()
  const now = Date.now()

  // Relay 1: signs anchored=true, valid signature
  const proof1 = buildProof({ appKey: APP_KEY, anchored: true, version: 10, attestedAt: now, pk: pk1, sk: sk1 })
  // Relay 2: signs anchored=false, valid
  const proof2 = buildProof({ appKey: APP_KEY, anchored: false, version: 0, attestedAt: now, pk: pk2, sk: sk2 })
  // Relay 3: signs anchored=true but signature is invalid (we'll tamper)
  const proof3 = { ...buildProof({ appKey: APP_KEY, anchored: true, version: 7, attestedAt: now, pk: pk3, sk: sk3 }), version: 999 }
  // Relay 4: unreachable

  const fetch = mockFetch({
    ['https://r1.example/api/anchors/' + APP_KEY + '/proof']: { body: proof1 },
    ['https://r2.example/api/anchors/' + APP_KEY + '/proof']: { body: proof2 },
    ['https://r3.example/api/anchors/' + APP_KEY + '/proof']: { body: proof3 }
    // r4 absent → 404
  })

  const report = await auditAnchors(APP_KEY, [
    'https://r1.example',
    'https://r2.example',
    'https://r3.example',
    'https://r4.example'
  ], { fetch })

  t.is(report.relayCount, 4)
  t.is(report.anchored.count, 1, 'r1 anchored & verified')
  t.is(report.unanchored.count, 1, 'r2 unanchored & verified')
  t.is(report.unverifiedSignatures.length, 1, 'r3 has bad signature')
  t.is(report.unreachable.length, 1, 'r4 unreachable')
})

test('auditAnchors: rejects bad inputs', async (t) => {
  try {
    await auditAnchors('not-hex', ['https://x.example'])
    t.fail('should throw')
  } catch (err) { t.ok(err.message.includes('64 hex chars')) }
  try {
    await auditAnchors(APP_KEY, [])
    t.fail('should throw')
  } catch (err) { t.ok(err.message.includes('at least one')) }
})