OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / client-custody-crossimpl.test.js

458 lines · javascript ref:
// Cross-implementation agreement: CLIENT signer ↔ CORE verifier.
//
// The publisher-side custody signer lives in the client SDK
// (packages/client/custody.js); the relay-side verifier lives in core
// (packages/core/core/custody-signing.js). They are independently-versioned
// wire peers that never share code — the client pins a frozen
// p2p-hiverelay@0.7.2 that predates custody-signing.js, so it carries its own
// self-contained copy. A shared import would be false comfort; the real
// interop contract is that the two agree byte-for-byte on the SIGNED PAYLOAD.
//
// These tests pin that contract: an entry signed by the client must verify and
// validate unchanged in core, the signatures must be identical given identical
// inputs, and tampering is caught. The v2 case specifically pins the nested
// shareAssignments key order — custodySignablePayload stringifies those objects
// positionally (JSON.stringify, not stableStringify), so a key-order drift
// between the two impls would silently break v2 interop.

import test from 'brittle'
import b4a from 'b4a'
import sodium from 'sodium-universal'
import { keygen, split } from 'p2p-hiverelay-client/secret-sharing.js'
import {
  createCustodyIntent as clientIntent,
  createCustodyCommit as clientCommit,
  createSourceRetired as clientRetired,
  verifyCustodyEntry as clientVerify,
  validateCustodyTransition as clientValidate,
  hashHex as clientHashHex,
  summarizeCustodyStatus as clientSummary
} from 'p2p-hiverelay-client/custody.js'
import {
  createCustodyIntent as coreIntent,
  createCustodyReceipt as coreReceipt,
  computeReceiptRoot,
  verifyCustodyEntry,
  validateCustodyTransition,
  hashHex,
  summarizeCustodyStatus as coreSummary
} from 'p2p-hiverelay/core/custody-signing.js'
import { shareCommitmentAt } from 'p2p-hiverelay/core/pvss.js'

// Custody entries are signed with sodium ed25519 keys.
function keyPair () {
  const publicKey = b4a.alloc(sodium.crypto_sign_PUBLICKEYBYTES)
  const secretKey = b4a.alloc(sodium.crypto_sign_SECRETKEYBYTES)
  sodium.crypto_sign_keypair(publicKey, secretKey)
  return { publicKey, secretKey }
}

async function shareholders (n) {
  const keys = []
  for (let i = 0; i < n; i++) keys.push(await keygen())
  return keys
}

// relays[i] custodies share i+1 — the dealer's split-to-relay assignment.
function assignmentsFor (relays) {
  return relays.map((r, i) => ({ relayPubkey: b4a.toString(r.publicKey, 'hex'), shareIndex: i + 1 }))
}

// shareManifest binds each shareIndex to a content-addressed shard:<hash> plus
// the same per-index public commitment point the receipt pins. `res` is the
// split() result carrying the Feldman commitment vector.
function manifestFor (n, res) {
  return Array.from({ length: n }, (_, i) => ({
    shareIndex: i + 1,
    shard: 'shard:' + hashHex('xm-shard-' + (i + 1)),
    shareCommitment: shareCommitmentAt(res.public.commitments, i + 1)
  }))
}

test('cross-impl: client + core hashHex agree', (t) => {
  t.is(clientHashHex('hiverelay-cross-impl'), hashHex('hiverelay-cross-impl'), 'identical BLAKE2b digest')
})

test('cross-impl: expiry witness quorum summaries agree and require publisher-selected policy', (t) => {
  const now = Date.now()
  const publisher = keyPair()
  const relay = keyPair()
  const witnessA = keyPair()
  const witnessB = keyPair()
  const witnessC = keyPair()
  const outsider = keyPair()
  const relayPubkey = b4a.toString(relay.publicKey, 'hex')
  const publisherPubkey = b4a.toString(publisher.publicKey, 'hex')
  const intent = {
    intentId: hashHex('witness-policy-intent'),
    custodyMode: 'blind',
    blindContentId: hashHex('witness-policy-blind'),
    requiredReplicas: 1,
    retainUntil: now
  }
  const proof = {
    intentId: intent.intentId,
    blindContentId: intent.blindContentId,
    relayPubkey,
    retainUntil: intent.retainUntil,
    timestamp: now + 1,
    notServing: true,
    catalogPresent: false,
    activeSwarmServing: false
  }
  const witnessEntry = (kp, overrides = {}) => ({
    type: 'custody-expiry-witness',
    intentId: intent.intentId,
    blindContentId: intent.blindContentId,
    relayPubkey,
    witnessPubkey: b4a.toString(kp.publicKey, 'hex'),
    timestamp: now + 2,
    nonServingProofHash: hashHex(proof),
    catalogPresent: false,
    gatewayServing: false,
    activeSwarmObserved: false,
    ...overrides
  })
  const policy = {
    kind: 'hivemesh-witness-quorum-policy',
    version: 1,
    intentId: intent.intentId,
    subjectRelayPubkey: relayPubkey,
    publisherPubkey,
    selectedBy: 'publisher',
    witnessCount: 3,
    requiredWitnesses: 2,
    minOperators: 2,
    minRegions: 2,
    witnesses: [
      { witnessPubkey: b4a.toString(witnessA.publicKey, 'hex'), operator: 'op-a', region: 'us-east' },
      { witnessPubkey: b4a.toString(witnessB.publicKey, 'hex'), operator: 'op-b', region: 'eu-west' },
      { witnessPubkey: b4a.toString(witnessC.publicKey, 'hex'), operator: 'op-c', region: 'ap-south' }
    ]
  }
  const entries = [
    witnessEntry(witnessA),
    witnessEntry(witnessB),
    witnessEntry(witnessB),
    witnessEntry(outsider),
    witnessEntry(witnessC, { gatewayServing: true })
  ]

  const client = clientSummary(intent, [], null, null, [], [proof], entries, policy)
  const core = coreSummary(intent, [], null, null, [], [proof], entries, policy)
  t.alike(client, core, 'client and core summary agree')
  t.is(client.expiryWitnessCount, 5)
  t.is(client.validExpiryWitnessCount, 4, 'active-serving witness is excluded before quorum evaluation')
  t.is(client.expiryWitnessQuorum.valid, true)
  t.is(client.expiryWitnessQuorum.count, 2)
  t.is(client.expiryWitnessQuorum.required, 2)
  t.alike(client.expiryWitnessQuorum.accepted, [
    b4a.toString(witnessA.publicKey, 'hex'),
    b4a.toString(witnessB.publicKey, 'hex')
  ])
  t.ok(client.expiryWitnessQuorum.rejected.find(row => row.reason === 'duplicate witness'))
  t.ok(client.expiryWitnessQuorum.rejected.find(row => row.reason === 'witness not publisher-selected'))

  const invalidPolicy = { ...policy, selectedBy: 'relay' }
  const invalid = coreSummary(intent, [], null, null, [], [proof], entries, invalidPolicy)
  t.absent(invalid.expiryWitnessQuorum.valid)
  t.is(invalid.expiryWitnessQuorum.reason, 'invalid-policy')
  t.ok(invalid.expiryWitnessQuorum.errors.includes('witness set must be publisher-selected'))
})

test('cross-impl: a client-signed v1 intent is byte-identical to core and verifies in core', (t) => {
  const now = Date.now()
  const publisher = keyPair()
  const fields = {
    blindContentId: hashHex('xi-blind'),
    ciphertextRoot: hashHex('xi-cipher'),
    contentVersion: 1,
    requiredReplicas: 3,
    deadline: now + 60_000,
    retainUntil: now + 120_000
  }
  const ci = clientIntent({ ...fields }, publisher, { timestamp: now })
  const ki = coreIntent({ ...fields }, publisher, { timestamp: now })
  t.is(ci.version, 1, 'defaults to v1')
  t.is(ci.signature, ki.signature, 'client + core sign the v1 intent identically')
  t.alike(ci, ki, 'normalized entries are identical')
  t.ok(verifyCustodyEntry(ci, { now }).valid, 'core verifies the client-signed v1 intent')
})

test('cross-impl: a client-signed v2 PVSS intent is byte-identical to core, verifies, and binds a core receipt', async (t) => {
  const now = Date.now()
  const n = 3
  const threshold = 2
  const keys = await shareholders(n)
  const res = await split({ threshold, shareholders: keys.map(k => k.publicKey) })
  const publisher = keyPair()
  const relays = Array.from({ length: n }, () => keyPair())
  const fields = {
    version: 2,
    blindContentId: hashHex('xv2-blind'),
    ciphertextRoot: hashHex('xv2-cipher'),
    contentVersion: 1,
    requiredReplicas: n,
    deadline: now + 60_000,
    retainUntil: now + 120_000,
    shareScheme: 'pvss-secp256k1-v1',
    shareThreshold: threshold,
    commitmentRoot: res.public.commitmentRoot,
    shareBundleKey: hashHex('xv2-bundle'),
    shareAssignments: assignmentsFor(relays)
  }
  const ci = clientIntent({ ...fields }, publisher, { timestamp: now })
  const ki = coreIntent({ ...fields }, publisher, { timestamp: now })
  // THE keystone assertion: identical signatures prove the two impls serialize
  // the nested shareAssignments objects in the same key order. A divergence
  // here silently breaks v2 interop, and no other test would catch it.
  t.is(ci.signature, ki.signature, 'client + core sign the v2 intent identically (nested key order pinned)')
  t.alike(ci.shareAssignments, ki.shareAssignments, 'shareAssignments normalize identically')
  t.ok(verifyCustodyEntry(ci, { now }).valid, 'core verifies the client-signed v2 intent')

  // A core (relay) receipt for the client-signed intent binds via the
  // transition check — relays[0] is assigned shareIndex 1.
  const receipt = coreReceipt({
    version: 2,
    intentId: ci.intentId,
    blindContentId: ci.blindContentId,
    ciphertextRoot: ci.ciphertextRoot,
    contentVersion: ci.contentVersion,
    retainUntil: ci.retainUntil,
    relayRegion: 'xi',
    shardIds: [0],
    shareScheme: 'pvss-secp256k1-v1',
    commitmentRoot: ci.commitmentRoot,
    shareIndex: 1,
    shareCommitment: shareCommitmentAt(res.public.commitments, 1),
    shareVerified: true
  }, relays[0], { timestamp: now + 1000 })
  t.ok(verifyCustodyEntry(receipt, { now: now + 1000 }).valid, 'core relay receipt verifies')
  t.ok(validateCustodyTransition(receipt, { intent: ci }).valid, 'core receipt binds to the client-signed v2 intent')
})

test('cross-impl: a client-signed v2 intent WITH a shareManifest is byte-identical to core, verifies, and binds a core receipt', async (t) => {
  const now = Date.now()
  const n = 3
  const threshold = 2
  const keys = await shareholders(n)
  const res = await split({ threshold, shareholders: keys.map(k => k.publicKey) })
  const publisher = keyPair()
  const relays = Array.from({ length: n }, () => keyPair())
  const manifest = manifestFor(n, res)
  const fields = {
    version: 2,
    blindContentId: hashHex('xm-blind'),
    ciphertextRoot: hashHex('xm-cipher'),
    contentVersion: 1,
    requiredReplicas: n,
    deadline: now + 60_000,
    retainUntil: now + 120_000,
    shareScheme: 'pvss-secp256k1-v1',
    shareThreshold: threshold,
    commitmentRoot: res.public.commitmentRoot,
    shareBundleKey: hashHex('xm-bundle'),
    shareAssignments: assignmentsFor(relays),
    shareManifest: manifest
  }
  const ci = clientIntent({ ...fields }, publisher, { timestamp: now })
  const ki = coreIntent({ ...fields }, publisher, { timestamp: now })
  // Keystone: the client can now author a manifest-bearing v2 intent and the
  // signed bytes match core exactly — proves shareManifest (a nested array of
  // objects stringified positionally) serializes identically in both impls.
  t.is(ci.signature, ki.signature, 'client + core sign the manifest-bearing v2 intent identically')
  t.alike(ci.shareManifest, ki.shareManifest, 'shareManifest normalizes identically')
  t.is(ci.shareManifest.length, n, 'manifest survives normalization')
  t.ok(verifyCustodyEntry(ci, { now }).valid, 'core verifies the client-signed manifest-bearing v2 intent')

  // And the reverse: a core-signed manifest-bearing intent verifies + validates
  // against the CLIENT verifier, and a core receipt binds to the client intent's
  // manifest via the transition check (relays[0] custodies shareIndex 1).
  t.ok(clientVerify(ki, { now }).valid, 'client verifies the core-signed manifest-bearing v2 intent')
  const receipt = coreReceipt({
    version: 2,
    intentId: ci.intentId,
    blindContentId: ci.blindContentId,
    ciphertextRoot: ci.ciphertextRoot,
    contentVersion: ci.contentVersion,
    retainUntil: ci.retainUntil,
    relayRegion: 'xm',
    shardIds: [0],
    shareScheme: 'pvss-secp256k1-v1',
    commitmentRoot: ci.commitmentRoot,
    shareIndex: 1,
    shareCommitment: shareCommitmentAt(res.public.commitments, 1),
    shareVerified: true
  }, relays[0], { timestamp: now + 1000 })
  t.ok(verifyCustodyEntry(receipt, { now: now + 1000 }).valid, 'core relay receipt verifies')
  t.ok(validateCustodyTransition(receipt, { intent: ci }).valid, 'core receipt binds to the client-signed manifest-bearing intent (core transition)')
  t.ok(clientValidate(receipt, { intent: ci }).valid, 'core receipt binds via the client transition check too')

  // A receipt whose shareCommitment contradicts the manifest binding is rejected
  // by BOTH transition checks — the manifest genuinely constrains the receipt.
  const wrongCommitment = { ...receipt, shareCommitment: shareCommitmentAt(res.public.commitments, 2) }
  t.absent(validateCustodyTransition(wrongCommitment, { intent: ci }).valid, 'core rejects a manifest-binding mismatch')
  t.absent(clientValidate(wrongCommitment, { intent: ci }).valid, 'client rejects a manifest-binding mismatch')

  // Manifest-ABSENT parity is not regressed: a v2 intent with no manifest still
  // signs byte-identically between client and core (the optional field is
  // dropped from both payloads).
  const noManifest = { ...fields }
  delete noManifest.shareManifest
  const ciNo = clientIntent({ ...noManifest }, publisher, { timestamp: now })
  const kiNo = coreIntent({ ...noManifest }, publisher, { timestamp: now })
  t.is(ciNo.signature, kiNo.signature, 'manifest-absent v2 intents remain byte-identical (no regression)')
  t.absent(ciNo.shareManifest, 'absent manifest is omitted, not null')
})

test('cross-impl: a client-signed custody-commit verifies and validates against core receipts', async (t) => {
  const now = Date.now()
  const n = 3
  const threshold = 2
  const keys = await shareholders(n)
  const res = await split({ threshold, shareholders: keys.map(k => k.publicKey) })
  const publisher = keyPair()
  const relays = Array.from({ length: n }, () => keyPair())
  const intent = clientIntent({
    version: 2,
    blindContentId: hashHex('xc-blind'),
    ciphertextRoot: hashHex('xc-cipher'),
    contentVersion: 1,
    requiredReplicas: n,
    deadline: now + 60_000,
    retainUntil: now + 120_000,
    shareScheme: 'pvss-secp256k1-v1',
    shareThreshold: threshold,
    commitmentRoot: res.public.commitmentRoot,
    shareBundleKey: hashHex('xc-bundle'),
    shareAssignments: assignmentsFor(relays)
  }, publisher, { timestamp: now })

  const receipts = relays.map((relay, i) => coreReceipt({
    version: 2,
    intentId: intent.intentId,
    blindContentId: intent.blindContentId,
    ciphertextRoot: intent.ciphertextRoot,
    contentVersion: intent.contentVersion,
    retainUntil: intent.retainUntil,
    relayRegion: 'xi',
    shardIds: [i],
    shareScheme: 'pvss-secp256k1-v1',
    commitmentRoot: intent.commitmentRoot,
    shareIndex: i + 1,
    shareCommitment: shareCommitmentAt(res.public.commitments, i + 1),
    shareVerified: true
  }, relay, { timestamp: now + 1000 }))

  for (const r of receipts) {
    t.ok(validateCustodyTransition(r, { intent }).valid, `receipt #${r.shareIndex} binds to the client intent`)
  }

  const commit = clientCommit({
    intentId: intent.intentId,
    blindContentId: intent.blindContentId,
    ciphertextRoot: intent.ciphertextRoot,
    contentVersion: intent.contentVersion,
    relayQuorum: receipts.map(r => r.relayPubkey).sort(),
    receiptRoot: computeReceiptRoot(receipts)
  }, publisher, { timestamp: now + 2000 })

  t.ok(verifyCustodyEntry(commit, { now: now + 2000 }).valid, 'core verifies the client-signed commit')
  t.ok(validateCustodyTransition(commit, { intent, receipts }).valid, 'client commit validates against the core receipts')
})

test('cross-impl: core rejects a tampered client-signed intent', (t) => {
  const now = Date.now()
  const publisher = keyPair()
  const intent = clientIntent({
    blindContentId: hashHex('xt-blind'),
    ciphertextRoot: hashHex('xt-cipher'),
    contentVersion: 1,
    requiredReplicas: 3,
    deadline: now + 60_000,
    retainUntil: now + 120_000
  }, publisher, { timestamp: now })

  t.ok(verifyCustodyEntry(intent, { now }).valid, 'pristine client intent verifies')
  t.is(verifyCustodyEntry({ ...intent, requiredReplicas: 99 }, { now }).valid, false, 'mutated field breaks the signature')
})

test('cross-impl: custody allowlist rejects top-level and nested smuggling fields', (t) => {
  const now = Date.now()
  const publisher = keyPair()
  const relays = Array.from({ length: 3 }, () => keyPair())
  const fields = {
    version: 2,
    blindContentId: hashHex('allowlist-blind'),
    ciphertextRoot: hashHex('allowlist-cipher'),
    contentVersion: 1,
    requiredReplicas: 3,
    deadline: now + 60_000,
    retainUntil: now + 120_000,
    shareScheme: 'pvss-secp256k1-v1',
    shareThreshold: 2,
    commitmentRoot: hashHex('allowlist-commitment'),
    shareBundleKey: hashHex('allowlist-bundle'),
    shareAssignments: assignmentsFor(relays)
  }
  const intent = clientIntent({ ...fields }, publisher, { timestamp: now })

  const topLevel = verifyCustodyEntry({ ...intent, caption: 'plaintext should not ride along' }, { now })
  t.absent(topLevel.valid, 'unknown top-level field rejected')
  t.is(topLevel.reason, 'unknown custody field: caption')

  const topLevelSecret = verifyCustodyEntry({ ...intent, dataKey: 'plaintext should not ride along' }, { now })
  t.absent(topLevelSecret.valid, 'secret-looking top-level field rejected by allowlist')
  t.is(topLevelSecret.reason, 'unknown custody field: dataKey')

  const nestedIntent = {
    ...intent,
    shareAssignments: intent.shareAssignments.map((assignment, index) => index === 0
      ? { ...assignment, caption: 'plaintext should not ride along' }
      : assignment)
  }
  const nested = verifyCustodyEntry(nestedIntent, { now })
  t.absent(nested.valid, 'unknown nested assignment field rejected')
  t.is(nested.reason, 'unknown shareAssignment field: caption')

  t.exception(
    () => coreIntent({
      ...fields,
      shareAssignments: [{ ...fields.shareAssignments[0], caption: 'plaintext' }, ...fields.shareAssignments.slice(1)]
    }, publisher, { timestamp: now }),
    /unknown shareAssignment field: caption/,
    'core signer refuses to create a smuggled assignment'
  )
  t.exception(
    () => coreIntent({ ...fields, dataKey: 'plaintext' }, publisher, { timestamp: now }),
    /unknown custody field: dataKey/,
    'core signer refuses secret-looking top-level fields via the allowlist'
  )
  t.exception(
    () => clientIntent({
      ...fields,
      shareAssignments: [{ ...fields.shareAssignments[0], caption: 'plaintext' }, ...fields.shareAssignments.slice(1)]
    }, publisher, { timestamp: now }),
    /unknown shareAssignment field: caption/,
    'client signer refuses to create a smuggled assignment'
  )
  t.exception(
    () => clientIntent({ ...fields, dataKey: 'plaintext' }, publisher, { timestamp: now }),
    /unknown custody field: dataKey/,
    'client signer refuses secret-looking top-level fields via the allowlist'
  )
})

test('cross-impl: a client-signed source-retired verifies in core', (t) => {
  const now = Date.now()
  const publisher = keyPair()
  const retired = clientRetired({
    intentId: hashHex('xr-intent'),
    blindContentId: hashHex('xr-blind'),
    retiredAtVersion: 1
  }, publisher, { timestamp: now })
  const res = verifyCustodyEntry(retired, { now })
  t.ok(res.valid, 'core verifies the client-signed source-retired')
  t.is(res.entry.publisherPubkey, b4a.toString(publisher.publicKey, 'hex'), 'publisher pubkey echoed')
})