OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / integration / test_dht_compat.js

230 lines · javascript ref:
// Test: DHT announce/lookup compatibility between JS HyperDHT and C++ peer.
//
// Phase 1: JS-only baseline (proves testnet works)
// Phase 2: JS announce → verify from JS (proves announce format)
// Phase 3: Start Godot peer that announces, verify from JS (proves C++ encoding)
//
// Usage: node test_dht_compat.js [--phase 1|2|3]

import createTestnet from 'hyperdht/testnet.js'
import HyperDHT from 'hyperdht'
import crypto from 'crypto'
import { writeFileSync, readFileSync, existsSync, unlinkSync } from 'fs'

const phase = process.argv.includes('--phase')
  ? parseInt(process.argv[process.argv.indexOf('--phase') + 1])
  : 0  // 0 = all phases

const topic = crypto.createHash('sha256').update('test-p2p-layer2').digest()

async function sleep(ms) {
  return new Promise(r => setTimeout(r, ms))
}

// Phase 1: JS testnet baseline
async function phase1() {
  console.log('\n=== Phase 1: JS-only baseline ===')
  const testnet = await createTestnet(3)
  const bootstrap = testnet.bootstrap
  console.log('  testnet bootstrap:', bootstrap.map(n => `${n.host}:${n.port}`))

  // Announcer
  const server = new HyperDHT({ bootstrap, firewalled: false })
  await server.fullyBootstrapped()
  const serverKp = HyperDHT.keyPair()
  console.log('  server pk:', serverKp.publicKey.toString('hex').slice(0, 16))
  console.log('  server addr:', server.address())

  const serverNode = server.createServer()
  await serverNode.listen(serverKp)
  console.log('  server listening')

  // Announce topic
  const ann = server.announce(topic, serverKp)
  await ann.finished()
  console.log('  server announced topic')

  // Discoverer
  const client = new HyperDHT({ bootstrap, firewalled: false })
  await client.fullyBootstrapped()
  console.log('  client bootstrapped, addr:', client.address())

  let found = false
  console.log('  looking up topic:', topic.toString('hex').slice(0, 16))

  // Method 1: iterate stream
  const lookup = client.lookup(topic)
  let replyCount = 0
  for await (const data of lookup) {
    replyCount++
    console.log('  lookup reply #' + replyCount + ':',
      'peers=' + (data.peers ? data.peers.length : 'null'),
      'from=' + (data.from ? `${data.from.host}:${data.from.port}` : 'null'))
    if (data.peers && data.peers.length > 0) {
      for (const peer of data.peers) {
        console.log('    found peer:', peer.publicKey.toString('hex').slice(0, 16))
        if (peer.publicKey.equals(serverKp.publicKey)) {
          found = true
        }
      }
    }
  }
  console.log('  stream replies:', replyCount)

  // Method 2: try findPeer
  if (!found) {
    console.log('  trying findPeer...')
    const fp = client.findPeer(serverKp.publicKey)
    let fpCount = 0
    for await (const data of fp) {
      fpCount++
      console.log('  findPeer reply #' + fpCount + ':', data.peer ?
        'pk=' + data.peer.publicKey.toString('hex').slice(0, 16) : 'null')
      if (data.peer && data.peer.publicKey.equals(serverKp.publicKey)) {
        found = true
      }
    }
    console.log('  findPeer replies:', fpCount)
  }

  console.log('  Phase 1:', found ? 'PASS' : 'FAIL', '(JS announce → JS lookup)')

  await serverNode.close()
  await server.destroy()
  await client.destroy()
  await testnet.destroy()

  return found
}

// Phase 2: JS announce, check C++ peer can find it
async function phase2() {
  console.log('\n=== Phase 2: JS announce → C++ lookup ===')
  const testnet = await createTestnet(3)
  const bootstrap = testnet.bootstrap
  const bstr = bootstrap.map(n => `${n.host}:${n.port}`).join(',')
  console.log('  testnet bootstrap:', bstr)

  // Write bootstrap for Godot
  writeFileSync('/tmp/layer_bootstrap.txt', bstr)

  // JS announcer
  const server = new HyperDHT({ bootstrap })
  const serverKp = HyperDHT.keyPair()
  console.log('  JS server pk:', serverKp.publicKey.toString('hex').slice(0, 16))
  writeFileSync('/tmp/test_dht_js_pk.txt', serverKp.publicKey.toString('hex'))

  const serverNode = server.createServer()
  await serverNode.listen(serverKp)
  await server.announce(topic, serverKp).finished
  console.log('  JS server announced, waiting for C++ peer to discover...')
  console.log('  (Run: godot4 --headless --path project --script ../test_dht_lookup.gd)')
  console.log('  Waiting 30s for /tmp/test_dht_lookup_result.txt ...')

  // Wait for C++ peer to write result
  const start = Date.now()
  while (Date.now() - start < 30000) {
    if (existsSync('/tmp/test_dht_lookup_result.txt')) {
      const result = readFileSync('/tmp/test_dht_lookup_result.txt', 'utf8').trim()
      console.log('  C++ lookup result:', result)

      await serverNode.close()
      await server.destroy()
      await testnet.destroy()
      return result === 'PASS'
    }
    await sleep(500)
  }

  console.log('  TIMEOUT — C++ peer never wrote result')
  await serverNode.close()
  await server.destroy()
  await testnet.destroy()
  return false
}

// Phase 3: C++ announce, check JS can find it
async function phase3() {
  console.log('\n=== Phase 3: C++ announce → JS lookup ===')
  const testnet = await createTestnet(3)
  const bootstrap = testnet.bootstrap
  const bstr = bootstrap.map(n => `${n.host}:${n.port}`).join(',')
  console.log('  testnet bootstrap:', bstr)

  writeFileSync('/tmp/layer_bootstrap.txt', bstr)

  console.log('  Waiting for C++ peer to announce...')
  console.log('  (Run: godot4 --headless --path project --script ../test_dht_announce.gd)')
  console.log('  Waiting for /tmp/test_dht_announce_ready.txt ...')

  const start = Date.now()
  while (Date.now() - start < 30000) {
    if (existsSync('/tmp/test_dht_announce_ready.txt')) {
      break
    }
    await sleep(500)
  }

  if (!existsSync('/tmp/test_dht_announce_ready.txt')) {
    console.log('  TIMEOUT — C++ peer never announced')
    await testnet.destroy()
    return false
  }

  const cppPk = readFileSync('/tmp/test_dht_announce_pk.txt', 'utf8').trim()
  console.log('  C++ peer announced, pk:', cppPk.slice(0, 16))

  // JS lookup
  const client = new HyperDHT({ bootstrap })
  let found = false
  const lookup = client.lookup(topic)
  for await (const data of lookup) {
    if (data.peers && data.peers.length > 0) {
      for (const peer of data.peers) {
        const peerHex = peer.publicKey.toString('hex')
        console.log('  found peer:', peerHex.slice(0, 16))
        if (peerHex === cppPk) {
          found = true
        }
      }
    }
  }

  console.log('  Phase 3:', found ? 'PASS' : 'FAIL', '(C++ announce → JS lookup)')

  await client.destroy()
  await testnet.destroy()
  return found
}

// Main
async function main() {
  // Cleanup
  for (const f of ['/tmp/test_dht_lookup_result.txt', '/tmp/test_dht_announce_ready.txt',
                    '/tmp/test_dht_announce_pk.txt', '/tmp/test_dht_js_pk.txt']) {
    try { unlinkSync(f) } catch {}
  }

  let results = []

  if (phase === 0 || phase === 1) {
    results.push(['Phase 1 (JS baseline)', await phase1()])
  }
  if (phase === 0 || phase === 2) {
    results.push(['Phase 2 (JS→C++ lookup)', await phase2()])
  }
  if (phase === 0 || phase === 3) {
    results.push(['Phase 3 (C++→JS lookup)', await phase3()])
  }

  console.log('\n=== Results ===')
  for (const [name, ok] of results) {
    console.log(`  ${ok ? 'PASS' : 'FAIL'}: ${name}`)
  }
}

main().catch(err => {
  console.error(err)
  process.exit(1)
})