// 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)
})