OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-overlay / test / overlay-v2-corestore-mux.test.js

170 lines · javascript ref:
import test from 'brittle'
import Protomux from 'protomux'
import SecretStream from '@hyperswarm/secret-stream'
import { Duplex } from 'streamx'
import crypto from 'hypercore-crypto'
import { tmpStore, tmpDir, fs, path } from './helpers.js'
import { HyperOverlayV2 } from '../lib/overlay-v2.js'
import { OverlayProtocolV2 } from '../lib/protocol-v2.js'
import { FileIndex } from '../lib/file-index.js'
import { TransferManager } from '../lib/transfer.js'

// Attach a SECOND OverlayProtocolV2 to the same mux, exactly how hyper-deploy's
// DeployServer does on the shared forge swarm (same protocol 'hyper-overlay/v2',
// keyed to a topic). This is the production mux condition the daemon's own
// HyperOverlayV2 (id=null) coexists with — the variable the clean E2E never had.
async function attachDeployStyleOverlay (store, mux, topic) {
  const idx = new FileIndex(store.namespace('deploy-' + topic.toString('hex').slice(0, 6)))
  await idx.ready()
  const tm = new TransferManager(idx)
  const stub = { _fileIndex: idx, feedKey: '0'.repeat(64), feed: { length: 0, async get () { return null } }, async markSynced () {}, async logPut () {} }
  const proto = new OverlayProtocolV2(stub, tm, { key: topic, autoSync: false })
  proto.attach(mux)
  return proto
}

// #66 root-cause probe: the existing overlay-v2-fetch tests attach the overlay
// channel on a BARE duplex (Protomux.from(stream) with nothing else on it). The
// LIVE hyper-svc forge path is different (transports/swarm.js _onConnection):
//
//   1. corestore.replicate(conn)        ← claims a Protomux on the stream +
//                                          opens corestore's own channels FIRST
//   2. mux = Protomux.from(conn)         ← REUSES that same protomux
//   3. instance.attachProtocol(mux)      ← overlay channel created on the reused mux
//
// On the live forge swarm, stortron's overlayV2 ends up with ZERO peers (no
// `[proto] channel opened`, fetchFile bails at the no-peer check) while
// tabertron's overlayV2 is active. This test reproduces the live ORDERING in
// memory to prove/disprove that "corestore.replicate claims the mux first"
// breaks overlay channel pairing.

function makeDuplex () {
  let aWrite, bWrite
  const a = new Duplex({ write (d, cb) { bWrite(d); cb() }, read () {} })
  const b = new Duplex({ write (d, cb) { aWrite(d); cb() }, read () {} })
  aWrite = (d) => a.push(d)
  bWrite = (d) => b.push(d)
  return [a, b]
}

const settle = (ms = 400) => new Promise(r => setTimeout(r, ms))

test('CONTROL: bare-mux attach pairs + fetches (matches existing passing tests)', async (t) => {
  const storeA = tmpStore('ov2-ctl-a'); const storeB = tmpStore('ov2-ctl-b')
  const alice = new HyperOverlayV2(storeA, { namespace: 'overlay-v2', destDir: tmpDir('ov2-ctl-a-dest') })
  const bob = new HyperOverlayV2(storeB, { namespace: 'overlay-v2', destDir: tmpDir('ov2-ctl-b-dest') })
  await alice.ready(); await bob.ready()
  t.teardown(async () => { try { await alice.close() } catch {}; try { await bob.close() } catch {} })

  const [sa, sb] = makeDuplex()
  alice.attachProtocol(Protomux.from(sa))
  bob.attachProtocol(Protomux.from(sb))
  await settle()

  t.is(alice.peerCount, 1, 'alice has 1 overlay peer')
  t.is(bob.peerCount, 1, 'bob has 1 overlay peer')

  const content = crypto.randomBytes(200 * 1024)
  const diskPath = path.join(tmpDir('ov2-ctl-files'), 'f.bin')
  fs.writeFileSync(diskPath, content)
  const oid = crypto.data(content).toString('hex')
  await alice.registerFile('/attachments/' + oid, diskPath, { contentHash: oid, size: content.length })

  const got = await bob.fetchFile(oid)
  t.ok(got && got.local === false, 'bob fetched from peer')
  t.is(Buffer.compare(got.data, content), 0, 'bytes match')
})

test('LIVE ORDERING: corestore.replicate(conn) BEFORE attachProtocol — does overlay still pair?', async (t) => {
  const storeA = tmpStore('ov2-live-a'); const storeB = tmpStore('ov2-live-b')
  const alice = new HyperOverlayV2(storeA, { namespace: 'overlay-v2', destDir: tmpDir('ov2-live-a-dest') })
  const bob = new HyperOverlayV2(storeB, { namespace: 'overlay-v2', destDir: tmpDir('ov2-live-b-dest') })
  await alice.ready(); await bob.ready()
  t.teardown(async () => { try { await alice.close() } catch {}; try { await bob.close() } catch {} })

  // ── reproduce hyper-svc _onConnection ordering FAITHFULLY ──
  // conn is a @hyperswarm/secret-stream — EXACTLY what Hyperswarm hands
  // _onConnection. Then, per swarm.js: corestore.replicate(conn) FIRST, then
  // Protomux.from(conn) for the overlay channel.
  const [da, db] = makeDuplex()
  const sa = new SecretStream(true, da)
  const sb = new SecretStream(false, db)
  await Promise.all([
    new Promise(r => sa.on('connect', r)),
    new Promise(r => sb.on('connect', r))
  ])
  storeA.replicate(sa)              // corestore claims the conn's mux first
  storeB.replicate(sb)
  alice.attachProtocol(Protomux.from(sa))   // overlay on Protomux.from(conn)
  bob.attachProtocol(Protomux.from(sb))
  await settle()

  // The decisive assertions: do the overlay channels pair under this ordering?
  t.is(alice.peerCount, 1, 'alice overlay peer present under corestore-first ordering')
  t.is(bob.peerCount, 1, 'bob overlay peer present under corestore-first ordering')

  const content = crypto.randomBytes(200 * 1024)
  const diskPath = path.join(tmpDir('ov2-live-files'), 'f.bin')
  fs.writeFileSync(diskPath, content)
  const oid = crypto.data(content).toString('hex')
  await alice.registerFile('/attachments/' + oid, diskPath, { contentHash: oid, size: content.length })

  // Channels register a peer, but with corestore sharing the mux the overlay
  // channel never OPENS (no sync-state exchange), so the fetch can't complete.
  // Bounded so the test fails fast instead of hanging. This currently FAILS —
  // it documents the bug + guards the fix (it should pass once channel-open
  // works under the shared-mux ordering).
  const got = await bob.fetchFile(oid, { timeout: 6000 })
  t.ok(got, 'bob resolved the file under live (corestore-replicate-first) ordering')
  if (got) t.is(Buffer.compare(got.data, content), 0, 'bytes match')
})

// PRODUCTION MUX: the daemon's HyperOverlayV2 (id=null) coexists on the same
// forge connection with hyper-deploy's OverlayProtocolV2 (SAME protocol
// 'hyper-overlay/v2', keyed to a topic). Reproduce that second same-protocol
// channel and see whether the daemon overlay (id=null) still pairs + fetches.
test('LOADED MUX: a 2nd hyper-overlay/v2 channel (hyper-deploy-style) on the same mux', async (t) => {
  const storeA = tmpStore('ov2-load-a'); const storeB = tmpStore('ov2-load-b')
  const alice = new HyperOverlayV2(storeA, { namespace: 'overlay-v2', destDir: tmpDir('ov2-load-a-dest') })
  const bob = new HyperOverlayV2(storeB, { namespace: 'overlay-v2', destDir: tmpDir('ov2-load-b-dest') })
  await alice.ready(); await bob.ready()
  const extra = []
  t.teardown(async () => {
    for (const p of extra) { try { p.destroy() } catch {} }
    try { await alice.close() } catch {}; try { await bob.close() } catch {}
  })

  const [da, db] = makeDuplex()
  const sa = new SecretStream(true, da)
  const sb = new SecretStream(false, db)
  await Promise.all([new Promise(r => sa.on('connect', r)), new Promise(r => sb.on('connect', r))])
  storeA.replicate(sa); storeB.replicate(sb)
  const muxA = Protomux.from(sa)
  const muxB = Protomux.from(sb)

  // Each daemon, like production, runs BOTH its own HyperOverlayV2 (id=null) AND
  // a hyper-deploy DeployServer overlay (id=topic) on the SAME connection. The
  // two daemons' deploy overlays use DIFFERENT topics (each = its own identity),
  // matching prod (tabertron's deploy topic ≠ stortron's).
  const topicA = crypto.randomBytes(32)
  const topicB = crypto.randomBytes(32)
  extra.push(await attachDeployStyleOverlay(storeA, muxA, topicA)) // alice's deploy overlay
  extra.push(await attachDeployStyleOverlay(storeB, muxB, topicB)) // bob's deploy overlay
  alice.attachProtocol(muxA) // alice's daemon overlay (id=null)
  bob.attachProtocol(muxB)   // bob's daemon overlay (id=null)
  await settle()

  t.is(alice.peerCount, 1, 'alice daemon-overlay peer present with a 2nd same-protocol channel on the mux')
  t.is(bob.peerCount, 1, 'bob daemon-overlay peer present with a 2nd same-protocol channel on the mux')

  const content = crypto.randomBytes(2 * 1024 * 1024)
  const diskPath = path.join(tmpDir('ov2-load-files'), 'f.bin')
  fs.writeFileSync(diskPath, content)
  const oid = crypto.data(content).toString('hex')
  await alice.registerFile('/attachments/' + oid, diskPath, { contentHash: oid, size: content.length })

  const gotN = await bob.fetchFile(oid, { timeout: 8000 })
  t.ok(gotN, 'bob daemon-overlay (id=null) fetched DESPITE a 2nd hyper-overlay/v2 channel on the mux')
  if (gotN) t.is(Buffer.compare(gotN.data, content), 0, 'bytes match')
})