OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / accounting-receipt.test.js

85 lines · javascript ref:
import test from 'brittle'
import b4a from 'b4a'
import sodium from 'sodium-universal'
import { readFile } from 'fs/promises'
import {
  ACCOUNTING_RECEIPT_DOMAIN,
  accountingFieldsFromStorageSummary,
  accountingReceiptSignable,
  createAccountingReceipt,
  verifyAccountingReceipt
} from 'p2p-hiverelay/core/protocol/accounting-receipt.js'

const VECTOR_URL = new URL('../fixtures/relaykernel-profile/accounting-receipt-v1.json', import.meta.url)

async function loadVector () {
  return JSON.parse(await readFile(VECTOR_URL, 'utf8'))
}

function keypairFromSeed (seedHex) {
  const publicKey = b4a.alloc(sodium.crypto_sign_PUBLICKEYBYTES)
  const secretKey = b4a.alloc(sodium.crypto_sign_SECRETKEYBYTES)
  sodium.crypto_sign_seed_keypair(publicKey, secretKey, b4a.from(seedHex, 'hex'))
  return { publicKey, secretKey }
}

test('accounting receipt vector verifies and pins canonical signable bytes', async (t) => {
  const vector = await loadVector()
  const verdict = verifyAccountingReceipt(vector.receipt)

  t.ok(verdict.valid, 'fixture receipt verifies')
  t.is(vector.receipt.relayPubkey, vector.relayPubkey, 'fixture names relay identity')
  t.is(b4a.toString(accountingReceiptSignable(vector.receipt), 'hex'), vector.signableHex, 'canonical bytes are stable')

  const decoded = JSON.parse(b4a.toString(accountingReceiptSignable(vector.receipt), 'utf8'))
  t.is(decoded[0], ACCOUNTING_RECEIPT_DOMAIN, 'signature is domain separated')
})

test('accounting receipt can be built from StorageAccounting summary fields', async (t) => {
  const vector = await loadVector()
  const keyPair = keypairFromSeed(vector.seedHex)
  const fields = accountingFieldsFromStorageSummary({
    totalBytes: vector.receipt.storageBytes,
    diskBytes: vector.receipt.diskBytes,
    perEntryBytes: vector.receipt.perEntryBytes,
    measuredEntries: vector.receipt.seededCount
  }, {
    periodStart: vector.receipt.periodStart,
    periodEnd: vector.receipt.periodEnd,
    measuredAt: vector.receipt.measuredAt,
    bytesServed: vector.receipt.bytesServed,
    bytesReceived: vector.receipt.bytesReceived,
    leaseCount: vector.receipt.leaseCount
  })

  const receipt = createAccountingReceipt(keyPair, { ...fields, nonce: vector.receipt.nonce })
  t.alike(receipt, vector.receipt, 'deterministic receipt matches vector')
})

test('accounting receipt rejects tampering, unknown fields, and shape drift', async (t) => {
  const { receipt } = await loadVector()

  t.absent(verifyAccountingReceipt({ ...receipt, storageBytes: receipt.storageBytes + 1 }).valid, 'tampered storageBytes')
  t.absent(verifyAccountingReceipt({ ...receipt, bytesServed: receipt.bytesServed + 1 }).valid, 'tampered bytesServed')
  t.absent(verifyAccountingReceipt({ ...receipt, source: 'hypercore-counter-v1' }).valid, 'wrong source')
  t.absent(verifyAccountingReceipt({ ...receipt, caption: 'looks harmless' }).valid, 'unknown caption field')
  t.absent(verifyAccountingReceipt({ ...receipt, path: '/private/store' }).valid, 'unknown path field')
  t.absent(verifyAccountingReceipt({ ...receipt, storageBytes: receipt.diskBytes - 1 }).valid, 'storage cannot understate disk bytes')
})

test('accounting receipt requires an OS-grounded disk measurement', (t) => {
  t.exception(() => accountingFieldsFromStorageSummary({
    totalBytes: 64,
    diskBytes: null,
    perEntryBytes: 64,
    measuredEntries: 1
  }), /diskBytes required/)

  t.exception(() => createAccountingReceipt(keypairFromSeed('02'.repeat(32)), {
    storageBytes: 10,
    diskBytes: 20,
    perEntryBytes: 10,
    nonce: '22'.repeat(16)
  }), /storageBytes below diskBytes/)
})