OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-registry / lib / proof-of-work.js

144 lines · javascript ref:
/**
 * Proof-of-Work for Hyper-Registry namespace claims
 *
 * STATUS: Implementation sketch - not yet functional
 */

import crypto from 'hypercore-crypto'

/**
 * Get required difficulty for a name
 * Shorter names require more work (Sybil resistance)
 */
export function getDifficulty(name) {
  const len = name.length
  if (len <= 2) return 26   // ~67M hashes
  if (len <= 4) return 22   // ~4M hashes
  if (len <= 8) return 18   // ~262K hashes
  if (len <= 16) return 14  // ~16K hashes
  return 10                  // ~1K hashes
}

/**
 * Mine a proof-of-work for a namespace claim
 *
 * @param {string} name - Namespace name
 * @param {string} pubkey - Root public key (hex)
 * @param {number} difficulty - Required leading zero bits
 * @returns {Promise<Object>} - { nonce, difficulty, hash, attempts, timeMs }
 */
export async function mineProof(name, pubkey, difficulty) {
  const startTime = Date.now()
  let nonce = 0
  let hash

  while (true) {
    hash = hashProof(name, pubkey, nonce)

    if (countLeadingZeroBits(hash) >= difficulty) {
      return {
        nonce,
        difficulty,
        hash: hash.toString('hex'),
        attempts: nonce + 1,
        timeMs: Date.now() - startTime
      }
    }

    nonce++

    // Yield to event loop periodically
    if (nonce % 10000 === 0) {
      await new Promise(resolve => setImmediate(resolve))
    }

    // Progress logging
    if (nonce % 500000 === 0) {
      const elapsed = ((Date.now() - startTime) / 1000).toFixed(1)
      const rate = (nonce / 1000 / parseFloat(elapsed)).toFixed(0)
      console.log(`  ...${(nonce / 1000000).toFixed(1)}M hashes (${elapsed}s, ${rate}k/s)`)
    }
  }
}

/**
 * Validate a proof-of-work
 *
 * @param {string} name - Namespace name
 * @param {string} pubkey - Root public key (hex)
 * @param {Object} proof - { nonce, difficulty, hash }
 * @param {number} requiredDifficulty - Minimum required difficulty
 * @returns {boolean}
 */
export function validateProof(name, pubkey, proof, requiredDifficulty) {
  // Check difficulty meets requirement
  if (proof.difficulty < requiredDifficulty) {
    return false
  }

  // Recompute hash
  const hash = hashProof(name, pubkey, proof.nonce)

  // Verify hash matches
  if (hash.toString('hex') !== proof.hash) {
    return false
  }

  // Verify leading zeros
  if (countLeadingZeroBits(hash) < requiredDifficulty) {
    return false
  }

  return true
}

/**
 * Hash a proof attempt
 */
function hashProof(name, pubkey, nonce) {
  const data = Buffer.from(`hyper-registry:${name}:${pubkey}:${nonce}`)
  return crypto.hash(data)
}

/**
 * Count leading zero bits in a buffer
 */
function countLeadingZeroBits(buffer) {
  let zeros = 0

  for (const byte of buffer) {
    if (byte === 0) {
      zeros += 8
    } else {
      // Count leading zeros in this byte
      for (let i = 7; i >= 0; i--) {
        if ((byte & (1 << i)) === 0) {
          zeros++
        } else {
          return zeros
        }
      }
    }
  }

  return zeros
}

/**
 * Estimate time to mine at given difficulty
 * Assumes ~500k hashes/second on typical hardware
 */
export function estimateMiningTime(difficulty) {
  const expectedHashes = Math.pow(2, difficulty)
  const hashesPerSecond = 500000
  const seconds = expectedHashes / hashesPerSecond

  if (seconds < 60) {
    return `~${seconds.toFixed(0)} seconds`
  } else if (seconds < 3600) {
    return `~${(seconds / 60).toFixed(0)} minutes`
  } else {
    return `~${(seconds / 3600).toFixed(1)} hours`
  }
}