#!/usr/bin/env node
// Cross-system tests: JS HyperDHT ↔ C++ client wire interop.
//
// Verifies real wire compatibility by running a JS testnet and spawning
// the C++ test binary against it.
//
// Usage: node test_crossystem.js
//
// Requires: npm install hyperdht (in test/ directory)
const { spawn } = require('child_process')
const path = require('path')
const crypto = require('crypto')
const sodium = require('sodium-native')
let DHT, createTestnet
try {
DHT = require('hyperdht')
createTestnet = require('hyperdht/testnet')
} catch (e) {
console.error('Missing hyperdht. Run: cd test && npm install hyperdht')
process.exit(1)
}
const CLIENT_BIN = path.join(__dirname, 'test_crossystem_client')
const TIMEOUT_MS = 20000
let passed = 0
let failed = 0
let total = 0
// ---- Helpers ----
function toHex (buf) {
return Buffer.isBuffer(buf) ? buf.toString('hex') : Buffer.from(buf).toString('hex')
}
function fromHex (hex) {
return Buffer.from(hex, 'hex')
}
function pass (name) {
total++
passed++
console.log(` PASS: ${name}`)
}
function fail (name, reason) {
total++
failed++
console.log(` FAIL: ${name} — ${reason}`)
}
function runClient (args, timeoutMs = TIMEOUT_MS) {
return new Promise((resolve) => {
const child = spawn(CLIENT_BIN, args, {
stdio: ['ignore', 'pipe', 'pipe'],
timeout: timeoutMs
})
let stdout = ''
let stderr = ''
child.stdout.on('data', (d) => { stdout += d.toString() })
child.stderr.on('data', (d) => { stderr += d.toString() })
child.on('close', (code) => {
resolve({ code, stdout: stdout.trim(), stderr: stderr.trim() })
})
child.on('error', (err) => {
resolve({ code: -1, stdout, stderr: err.message })
})
// Hard kill after timeout.
setTimeout(() => {
child.kill('SIGKILL')
}, timeoutMs)
})
}
function getBootstrapStr (testnet) {
return testnet.bootstrap.map(b => `${b.host}:${b.port}`).join(',')
}
function parseLine (stdout, prefix) {
const line = stdout.split('\n').find(l => l.startsWith(prefix))
return line ? line.slice(prefix.length) : null
}
// ---- Tests ----
async function testBootstrap (testnet) {
console.log('\n--- Cross-System: Bootstrap ---')
const bootstrap = getBootstrapStr(testnet)
const result = await runClient(['bootstrap', bootstrap])
if (result.code !== 0) {
fail('Bootstrap exits 0', `exit code ${result.code}: ${result.stderr}`)
return
}
pass('Bootstrap exits 0')
const sizeStr = parseLine(result.stdout, 'ROUTING_TABLE_SIZE=')
if (!sizeStr) {
fail('Bootstrap reports routing table size', 'no ROUTING_TABLE_SIZE output')
return
}
const size = parseInt(sizeStr)
if (size >= 1) {
pass(`Bootstrap routing table has ${size} nodes (>= 1)`)
} else {
fail('Bootstrap routing table has nodes', `only ${size} nodes`)
}
}
async function testImmutableRoundTrip (testnet) {
console.log('\n--- Cross-System: Immutable Storage Round-Trip ---')
const bootstrap = getBootstrapStr(testnet)
// Step 1: JS puts a value.
const client = testnet.createNode()
const value = Buffer.from('hello from JS to C++ crossystem test')
const putResult = await client.immutablePut(value)
const keyHex = toHex(putResult.hash)
pass(`JS immutablePut succeeded (key=${keyHex.slice(0, 16)}...)`)
// Step 2: C++ gets the value.
const getResult = await runClient(['immutable_get', bootstrap, keyHex])
if (getResult.code !== 0) {
fail('C++ immutableGet exits 0', `exit code ${getResult.code}: ${getResult.stderr}`)
await client.destroy()
return
}
pass('C++ immutableGet exits 0')
const valueHex = parseLine(getResult.stdout, 'IMMUTABLE_VALUE=')
if (!valueHex) {
fail('C++ got value', 'no IMMUTABLE_VALUE output')
await client.destroy()
return
}
const gotValue = fromHex(valueHex)
if (gotValue.equals(value)) {
pass('C++ got correct value from JS')
} else {
fail('C++ got correct value', `got ${valueHex.slice(0, 32)}... expected ${toHex(value).slice(0, 32)}...`)
}
await client.destroy()
}
async function testImmutableCppToJs (testnet) {
console.log('\n--- Cross-System: C++ Puts, JS Gets ---')
const bootstrap = getBootstrapStr(testnet)
// Step 1: C++ puts a value.
const value = Buffer.from('hello from C++ to JS crossystem test')
const valueHex = toHex(value)
const putResult = await runClient(['immutable_put', bootstrap, valueHex])
if (putResult.code !== 0) {
fail('C++ immutablePut exits 0', `exit code ${putResult.code}: ${putResult.stderr}`)
return
}
pass('C++ immutablePut exits 0')
const keyHex = parseLine(putResult.stdout, 'IMMUTABLE_KEY=')
if (!keyHex) {
fail('C++ reported key', 'no IMMUTABLE_KEY output')
return
}
// Verify the key matches BLAKE2b(value).
const expectedHash = Buffer.alloc(32)
sodium.crypto_generichash(expectedHash, value)
if (keyHex === toHex(expectedHash)) {
pass('C++ key matches BLAKE2b(value)')
} else {
fail('C++ key matches hash', `got ${keyHex.slice(0, 16)} expected ${toHex(expectedHash).slice(0, 16)}`)
}
// Step 2: JS gets the value.
const client = testnet.createNode()
const getResult = await client.immutableGet(fromHex(keyHex))
if (getResult && getResult.value) {
if (getResult.value.equals(value)) {
pass('JS got correct value from C++')
} else {
fail('JS got correct value', `got ${toHex(getResult.value).slice(0, 32)}...`)
}
} else {
fail('JS immutableGet found value', 'returned null')
}
await client.destroy()
}
async function testMutableRoundTrip (testnet) {
console.log('\n--- Cross-System: Mutable Storage Round-Trip ---')
const bootstrap = getBootstrapStr(testnet)
// Generate a keypair in JS.
const publicKey = Buffer.alloc(32)
const secretKey = Buffer.alloc(64)
sodium.crypto_sign_keypair(publicKey, secretKey)
const pkHex = toHex(publicKey)
const skHex = toHex(secretKey)
const value = Buffer.from('mutable crossystem test data')
const valueHex = toHex(value)
const seq = 1
// Step 1: JS puts a mutable value.
const client = testnet.createNode()
const putResult = await client.mutablePut({ publicKey, secretKey }, value, { seq })
pass(`JS mutablePut succeeded (seq=${seq})`)
// Step 2: C++ gets the mutable value.
const getResult = await runClient(['mutable_get', bootstrap, pkHex])
if (getResult.code !== 0) {
fail('C++ mutableGet exits 0', `exit code ${getResult.code}: ${getResult.stderr}`)
await client.destroy()
return
}
pass('C++ mutableGet exits 0')
const gotValueHex = parseLine(getResult.stdout, 'MUTABLE_VALUE=')
const gotSeqStr = parseLine(getResult.stdout, 'MUTABLE_SEQ=')
if (!gotValueHex) {
fail('C++ got mutable value', 'no MUTABLE_VALUE output')
await client.destroy()
return
}
const gotValue = fromHex(gotValueHex)
if (gotValue.equals(value)) {
pass('C++ got correct mutable value')
} else {
fail('C++ got correct mutable value', `got ${gotValueHex.slice(0, 32)}...`)
}
if (gotSeqStr && parseInt(gotSeqStr) === seq) {
pass(`C++ got correct seq (${seq})`)
} else {
fail('C++ got correct seq', `got ${gotSeqStr}`)
}
await client.destroy()
}
async function testMutableCppToJs (testnet) {
console.log('\n--- Cross-System: C++ Mutable Put, JS Gets ---')
const bootstrap = getBootstrapStr(testnet)
// Generate keypair in JS (shared between C++ put and JS get).
const publicKey = Buffer.alloc(32)
const secretKey = Buffer.alloc(64)
sodium.crypto_sign_keypair(publicKey, secretKey)
const pkHex = toHex(publicKey)
const skHex = toHex(secretKey)
const value = Buffer.from('mutable from C++ to JS')
const valueHex = toHex(value)
const seq = 42
// Step 1: C++ puts a mutable value.
const putResult = await runClient([
'mutable_put', bootstrap, pkHex, skHex, valueHex, String(seq)
])
if (putResult.code !== 0) {
fail('C++ mutablePut exits 0', `exit code ${putResult.code}: ${putResult.stderr}`)
return
}
const okLine = parseLine(putResult.stdout, 'MUTABLE_PUT_OK')
if (okLine !== null) {
pass('C++ mutablePut succeeded')
} else {
fail('C++ mutablePut reported OK', 'no MUTABLE_PUT_OK output')
return
}
// Step 2: JS gets the mutable value.
const client = testnet.createNode()
const getResult = await client.mutableGet(publicKey)
if (getResult && getResult.value) {
if (getResult.value.equals(value)) {
pass('JS got correct mutable value from C++')
} else {
fail('JS got correct mutable value', `got ${toHex(getResult.value).slice(0, 32)}...`)
}
if (getResult.seq === seq) {
pass(`JS got correct seq from C++ (${seq})`)
} else {
fail('JS got correct seq', `got ${getResult.seq}`)
}
} else {
fail('JS mutableGet found value', 'returned null')
}
await client.destroy()
}
async function testTokenRoundTrip (testnet) {
console.log('\n--- Cross-System: Token Round-Trip ---')
// This test verifies that:
// 1. C++ queries the testnet and receives tokens in responses
// 2. C++ uses those tokens in the commit phase
// 3. JS testnet nodes accept the commit (validate the token)
//
// We test this indirectly: if immutable_put succeeds (C++ → JS),
// it means tokens were received, stored, and sent back correctly
// in the commit phase.
const bootstrap = getBootstrapStr(testnet)
const value = Buffer.from('token validation test payload ' + Date.now())
const valueHex = toHex(value)
const result = await runClient(['immutable_put', bootstrap, valueHex])
if (result.code !== 0) {
fail('Token round-trip (via immutable_put)', `exit code ${result.code}: ${result.stderr}`)
return
}
const keyHex = parseLine(result.stdout, 'IMMUTABLE_KEY=')
if (!keyHex) {
fail('Token round-trip produced key', 'no IMMUTABLE_KEY')
return
}
// Verify the value is actually stored on the JS testnet.
const client = testnet.createNode()
const getResult = await client.immutableGet(fromHex(keyHex))
if (getResult && getResult.value && getResult.value.equals(value)) {
pass('Token round-trip: C++ commit with tokens accepted by JS nodes')
} else {
fail('Token round-trip', 'JS could not retrieve value stored by C++')
}
await client.destroy()
}
// ---- Main ----
async function main () {
console.log('=== Cross-System Tests: JS HyperDHT ↔ C++ Client ===\n')
// Check that the C++ binary exists.
const fs = require('fs')
if (!fs.existsSync(CLIENT_BIN)) {
console.error(`ERROR: ${CLIENT_BIN} not found. Run: cd test && bash build_tests.sh`)
process.exit(1)
}
// Create testnet.
console.log('Creating JS testnet (3 persistent nodes)...')
const testnet = await createTestnet(3, { teardown: () => {} })
const bootstrap = getBootstrapStr(testnet)
console.log(`Testnet bootstrap: ${bootstrap}`)
// Wait a moment for the testnet to stabilize.
await new Promise(r => setTimeout(r, 500))
try {
// Run tests sequentially.
await testBootstrap(testnet)
await testImmutableRoundTrip(testnet)
await testImmutableCppToJs(testnet)
await testMutableRoundTrip(testnet)
await testMutableCppToJs(testnet)
await testTokenRoundTrip(testnet)
} catch (err) {
console.error(`\nUnexpected error: ${err.stack || err}`)
failed++
}
// Summary.
console.log(`\n=== Cross-System Results: ${passed}/${total} passed, ${failed} failed ===`)
// Cleanup.
await testnet.destroy()
process.exit(failed > 0 ? 1 : 0)
}
main().catch(err => {
console.error(err)
process.exit(1)
})