OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / integration / blind-dispersal-fleet-e2e.test.js

131 lines · javascript ref:
/**
 * The live fleet run, in miniature: blind-shard dispersal across FOUR real
 * RelayNode instances over their HTTP APIs — NO stub anywhere.
 *
 * Each relay runs the shard-store service, ingests a published custody intent
 * into its real seeding registry, and authorizes each PUT through the PRODUCTION
 * resolver (RelayNode._resolveShardCustodyAssignment). This is exactly what
 * scripts/blind-dispersal-live.mjs does against the fleet — same code, real
 * nodes, on a local testnet DHT.
 *
 *   planDispersal → createCustodyIntent → POST /api/custody/intent (each relay)
 *     → PUT /api/v1/shard (custody-pin, real auth) → GET → reconstruct
 */
import test from 'brittle'
import createTestnet from '@hyperswarm/testnet'
import b4a from 'b4a'
import sodium from 'sodium-universal'
import { randomBytes } from 'crypto'
import { mkdir, rm } from 'fs/promises'
import { join } from 'path'
import { tmpdir } from 'os'
import { RelayNode } from 'p2p-hiverelay/core/relay-node/index.js'
import { planDispersal, recoverSecret } from '../../packages/client/blind-shards.js'
import { createHttpShardPut, createHttpShardFetch } from '../../packages/client/shard-transport.js'
import { signShardPin, normalizeShardAddress } from '../../packages/services/builtin/shard-store/index.js'
import { createCustodyIntent } from '../../packages/core/core/custody-signing.js'

async function bringUpRelay (baseDir, testnet, i) {
  const apiKey = 'shard-e2e-' + randomBytes(6).toString('hex')
  await mkdir(join(baseDir, 'relay-' + i), { recursive: true })
  const node = new RelayNode({
    storage: join(baseDir, 'relay-' + i),
    bootstrapNodes: testnet.bootstrap,
    enableAPI: true,
    apiPort: 0,
    apiHost: '127.0.0.1',
    apiKey,
    enableSeeding: true,
    enableServices: true,
    plugins: ['shard-store'],
    enableNetworkDiscovery: false,
    enableHolesail: false,
    gatewayServeBlind: false
  })
  await node.start()
  return {
    node,
    apiKey,
    baseUrl: 'http://127.0.0.1:' + node.api.server.address().port,
    pubkey: b4a.toString(node.swarm.keyPair.publicKey, 'hex')
  }
}

async function publishIntent (baseUrl, apiKey, intent) {
  const res = await fetch(baseUrl + '/api/custody/intent', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + apiKey },
    body: JSON.stringify(intent)
  })
  if (!res.ok) throw new Error('publish intent failed: ' + res.status + ' ' + (await res.text()).slice(0, 200))
}

test('live blind-shard dispersal across 4 real RelayNodes (no stub)', { timeout: 120000 }, async (t) => {
  const baseDir = join(tmpdir(), 'shard-fleet-e2e-' + randomBytes(4).toString('hex'))
  await mkdir(baseDir, { recursive: true })
  const testnet = await createTestnet(3)

  const N = 4
  const K = 3
  const relays = []
  t.teardown(async () => {
    let firstError = null
    for (const relay of relays) {
      try { await relay.node.stop() } catch (err) { if (!firstError) firstError = err }
    }
    try { await testnet.destroy() } catch (err) { if (!firstError) firstError = err }
    try { await rm(baseDir, { recursive: true, force: true }) } catch (err) { if (!firstError) firstError = err }
    if (firstError) throw firstError
  })
  for (let i = 0; i < N; i++) relays.push(await bringUpRelay(baseDir, testnet, i))
  t.pass('brought up ' + N + ' real relay nodes with shard-store enabled')

  const publisher = (() => { const pk = b4a.alloc(32); const sk = b4a.alloc(64); sodium.crypto_sign_seed_keypair(pk, sk, b4a.alloc(32, 7)); return { publicKey: pk, secretKey: sk } })()

  // 1. plan (no storage) — learn addresses up front
  const plan = await planDispersal({ count: N, threshold: K })
  const shareAssignments = plan.shares.map((s) => ({ relayPubkey: relays[s.shareIndex - 1].pubkey, shareIndex: s.shareIndex }))
  const shareManifest = plan.shares.map((s) => ({ shareIndex: s.shareIndex, shard: s.shard, shareCommitment: s.shareCommitment }))

  // 2. sign the custody intent binding relay -> shareIndex -> shard
  const H = (c) => c.repeat(64)
  const intent = createCustodyIntent({
    version: 2,
    blindContentId: H('a'),
    ciphertextRoot: plan.commitmentRoot,
    contentVersion: 1,
    requiredReplicas: N,
    candidateRelays: relays.map((r) => r.pubkey),
    shareScheme: 'pvss-secp256k1-v1',
    shareThreshold: K,
    commitmentRoot: plan.commitmentRoot,
    shareBundleKey: H('d'),
    shareAssignments,
    shareManifest
  }, publisher)

  // 3. publish it to EVERY relay's real seeding registry
  for (const r of relays) await publishIntent(r.baseUrl, r.apiKey, intent)
  t.pass('custody intent ' + intent.intentId.slice(0, 12) + '… published to all relays')

  // 4. PUT each opaque shard to its assigned relay over /api/v1/shard (real auth)
  for (const s of plan.shares) {
    const r = relays[s.shareIndex - 1]
    const put = createHttpShardPut({
      baseUrl: r.baseUrl,
      signPin: ({ hash }) => signShardPin({ reason: 'custody', hash: normalizeShardAddress(hash), custodyIntentId: intent.intentId, shareIndex: s.shareIndex, retainUntil: Date.now() + 3600000, nonce: s.shareIndex.toString(16).padStart(32, '0') }, publisher)
    })
    const addr = await put(s.bytes, { shareIndex: s.shareIndex })
    t.is(addr, s.shard, 'share ' + s.shareIndex + ' stored at its content address on relay ' + s.shareIndex)
  }

  // 5. reconstruct from ANY k relays over the wire
  const rec = await recoverSecret({ shareManifest, threshold: K, fetch: createHttpShardFetch({ baseUrls: [relays[0].baseUrl, relays[2].baseUrl, relays[3].baseUrl] }) })
  t.is(rec.ok, true, 'reader gathered k shards from real relays')
  t.is(rec.key, plan.key, 'reconstructed the exact secret across the live relay set')

  // 6. below threshold cannot reconstruct
  const under = await recoverSecret({ shareManifest, threshold: K, fetch: createHttpShardFetch({ baseUrls: [relays[0].baseUrl, relays[1].baseUrl] }) })
  t.is(under.ok, false, 'k-1 relays cannot reconstruct')
})