OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / scripts / test-zk-local.js

203 lines · javascript ref:
/**
 * Local ZK Service Test — spins up a testnet relay + client
 * and tests all ZK primitives over P2P via callService().
 *
 * Usage: node scripts/test-zk-local.js
 */

import Hyperswarm from 'hyperswarm'
import Corestore from 'corestore'
import { RelayNode } from '../core/relay-node/index.js'
import { HiveRelayClient } from '../client/index.js'
import createTestnet from '@hyperswarm/testnet'
import { tmpdir } from 'os'
import { join } from 'path'
import { randomBytes } from 'crypto'
import { mkdir } from 'fs/promises'

console.log('=== Local ZK P2P Service Test ===\n')

const testnet = await createTestnet(3)
const relayStorage = join(tmpdir(), 'zk-relay-' + randomBytes(4).toString('hex'))
await mkdir(relayStorage, { recursive: true })

// Start relay with services + router
const node = new RelayNode({
  storage: relayStorage,
  bootstrapNodes: testnet.bootstrap,
  enableAPI: false,
  enableMetrics: false,
  enableServices: true,
  enableRouter: true
})

await node.start()
await node.swarm.flush()
console.log('Relay started')
console.log('  Services registered:', node.serviceRegistry?.services?.size || 0)
console.log('  Router routes:', node.router?.routes()?.length || 0)

// Start client with its own swarm (matches working integration test pattern)
const clientSwarm = new Hyperswarm({ bootstrap: testnet.bootstrap })
const clientStore = new Corestore(join(tmpdir(), 'zk-client-' + randomBytes(4).toString('hex')))
const client = new HiveRelayClient({ swarm: clientSwarm, store: clientStore })

await client.start()

// Poll for relay connection (DHT discovery can take a few seconds)
for (let i = 0; i < 20; i++) {
  if (client.relays.size > 0) break
  await new Promise(resolve => setTimeout(resolve, 500))
  await clientSwarm.flush()
}

console.log('Client connected to', client.getRelays().length, 'relay(s)')

// Wait for service channel to open
await new Promise(resolve => {
  for (const [, relay] of client.relays) {
    if (relay.channels?.service) return resolve()
  }
  client.on('service-channel-open', () => resolve())
  setTimeout(resolve, 3000)
})

let passed = 0
let failed = 0

async function test (name, fn) {
  try {
    await fn()
    passed++
    console.log(`  PASS  ${name}`)
  } catch (err) {
    failed++
    console.log(`  FAIL  ${name}: ${err.message}`)
  }
}

// ─── Pedersen Commitments ───────────────────────────────────────────

console.log('\n--- Pedersen Commitments (over P2P) ---')

await test('commit to bet amount (5000 sats)', async () => {
  const r = await client.callService('zk', 'commit', { value: 5000 })
  if (!r.commitment || r.commitment.length !== 66) throw new Error('bad commitment')
  if (!r.blindingFactor || r.blindingFactor.length !== 64) throw new Error('bad blinding')
})

await test('verify commitment opens correctly', async () => {
  const { commitment, blindingFactor } = await client.callService('zk', 'commit', { value: 'royal-flush' })
  const v = await client.callService('zk', 'verify-commit', { commitment, value: 'royal-flush', blindingFactor })
  if (!v.valid) throw new Error('valid commitment should verify')
})

await test('wrong value rejected (binding property)', async () => {
  const { commitment, blindingFactor } = await client.callService('zk', 'commit', { value: 100 })
  const v = await client.callService('zk', 'verify-commit', { commitment, value: 999, blindingFactor })
  if (v.valid) throw new Error('wrong value should fail')
})

// ─── Schnorr Proofs ─────────────────────────────────────────────────

console.log('\n--- Schnorr NIZK Proofs (over P2P) ---')

await test('prove + verify knowledge of secret', async () => {
  const secret = '0a' + '0'.repeat(62)
  const { proof, publicPoint } = await client.callService('zk', 'prove-knowledge', { secret })
  const v = await client.callService('zk', 'verify-knowledge', { proof, publicPoint })
  if (!v.valid) throw new Error('valid proof should verify')
})

// ─── Mental Poker ───────────────────────────────────────────────────

console.log('\n--- Mental Poker Cards (over P2P) ---')

const sk = '07' + '0'.repeat(62)
let pk, encrypted

await test('encrypt card 7 (Eight of Spades)', async () => {
  const kp = await client.callService('zk', 'prove-knowledge', { secret: sk })
  pk = kp.publicPoint
  const r = await client.callService('zk', 'encrypt-card', { card: 7, publicKey: pk })
  encrypted = r.encrypted
  if (!encrypted.c1 || !encrypted.c2) throw new Error('bad encryption')
})

await test('reveal token + DLEQ proof', async () => {
  const r = await client.callService('zk', 'create-reveal-token', { encrypted, secretKey: sk })
  if (!r.token || !r.proof) throw new Error('missing token or proof')
})

await test('unmask → card 7', async () => {
  const { token } = await client.callService('zk', 'create-reveal-token', { encrypted, secretKey: sk })
  const r = await client.callService('zk', 'unmask-card', { encrypted, tokens: [token] })
  if (r.card !== 7) throw new Error('expected card 7, got ' + r.card)
  console.log('    Dealt card: 7 (Eight of Spades)')
})

await test('full deal: encrypt card 0 → unmask → Ace of Spades', async () => {
  const { encrypted: enc } = await client.callService('zk', 'encrypt-card', { card: 0, publicKey: pk })
  const { token } = await client.callService('zk', 'create-reveal-token', { encrypted: enc, secretKey: sk })
  const { card } = await client.callService('zk', 'unmask-card', { encrypted: enc, tokens: [token] })
  if (card !== 0) throw new Error('expected card 0, got ' + card)
  console.log('    Dealt card: 0 (Ace of Spades)')
})

// ─── Fair Randomness ────────────────────────────────────────────────

console.log('\n--- Fair Coin Flip (over P2P) ---')

await test('3-player fair random', async () => {
  const p1 = await client.callService('zk', 'commit-random')
  const p2 = await client.callService('zk', 'commit-random')
  const p3 = await client.callService('zk', 'commit-random')
  const r = await client.callService('zk', 'combine-reveals', {
    reveals: [
      { secret: p1.secret, commitment: p1.commitment },
      { secret: p2.secret, commitment: p2.commitment },
      { secret: p3.secret, commitment: p3.commitment }
    ]
  })
  if (!r.valid) throw new Error('reveals should be valid')
  console.log('    Random: ' + r.randomValue.slice(0, 16) + '...')
})

// ─── Membership + Range ─────────────────────────────────────────────

console.log('\n--- ZK Proofs (over P2P) ---')

await test('membership proof (no plaintext in verify)', async () => {
  const p = await client.callService('zk', 'prove-membership', { value: 'bob', set: ['alice', 'bob', 'carol'] })
  const v = await client.callService('zk', 'verify-membership', {
    leafHash: p.leafHash, merkleRoot: p.merkleRoot, proof: p.proof, leafIndex: p.leafIndex
  })
  if (!v.valid) throw new Error('membership proof should verify')
})

await test('range proof (no plaintext in verify)', async () => {
  const p = await client.callService('zk', 'prove-range', { value: 25, min: 18, max: 65 })
  const v = await client.callService('zk', 'verify-range', { commitment: p.commitment, rangeProof: p.rangeProof })
  if (!v.valid) throw new Error('range proof should verify')
})

await test('list circuits', async () => {
  const c = await client.callService('zk', 'circuits')
  if (c.available.length !== 7) throw new Error('expected 7 circuits, got ' + c.available.length)
  if (c.curve !== 'secp256k1') throw new Error('expected secp256k1')
  console.log('    Circuits: ' + c.available.map(a => a.name).join(', '))
})

// ─── Summary ────────────────────────────────────────────────────────

console.log('\n' + '='.repeat(50))
console.log(`Results: ${passed} passed, ${failed} failed`)
console.log('All calls made over Hyperswarm P2P via callService()')
console.log('='.repeat(50))

await client.destroy()
await clientSwarm.destroy()
await node.stop()
await testnet.destroy()
process.exit(failed > 0 ? 1 : 0)