OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / test / test_crossystem.js

404 lines · javascript ref:
#!/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)
})