Source: mining/cpuminer.js

/*!
 * cpuminer.js - inefficient cpu miner for hsd (because we can)
 * Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
 * https://github.com/handshake-org/hsd
 */

'use strict';

const assert = require('bsert');
const bio = require('bufio');
const EventEmitter = require('events');
const {Lock} = require('bmutex');
const util = require('../utils/util');
const mine = require('./mine');
const consensus = require('../protocol/consensus');

/**
 * CPU miner.
 * @alias module:mining.CPUMiner
 */

class CPUMiner extends EventEmitter {
  /**
   * Create a CPU miner.
   * @constructor
   * @param {Miner} miner
   */

  constructor(miner) {
    super();

    this.opened = false;
    this.miner = miner;
    this.network = this.miner.network;
    this.logger = this.miner.logger.context('cpuminer');
    this.workers = this.miner.workers;
    this.chain = this.miner.chain;
    this.locker = new Lock();

    this.running = false;
    this.stopping = false;
    this.job = null;
    this.stopJob = null;

    this.init();
  }

  /**
   * Initialize the miner.
   * @private
   */

  init() {
    this.chain.on('tip', (tip) => {
      if (!this.job)
        return;

      if (this.job.attempt.prevBlock.equals(tip.prevBlock))
        this.job.destroy();
    });
  }

  /**
   * Open the miner.
   * @returns {Promise}
   */

  async open() {
    assert(!this.opened, 'CPUMiner is already open.');
    this.opened = true;
  }

  /**
   * Close the miner.
   * @returns {Promise}
   */

  async close() {
    assert(this.opened, 'CPUMiner is not open.');
    this.opened = false;
    return this.stop();
  }

  /**
   * Start mining.
   * @method
   */

  start() {
    assert(!this.running, 'Miner is already running.');
    this._start().catch(() => {});
  }

  /**
   * Start mining.
   * @method
   * @private
   * @returns {Promise}
   */

  async _start() {
    assert(!this.running, 'Miner is already running.');

    this.running = true;
    this.stopping = false;

    for (;;) {
      this.job = null;

      try {
        this.job = await this.createJob();
      } catch (e) {
        if (this.stopping)
          break;
        this.emit('error', e);
        break;
      }

      if (this.stopping)
        break;

      let block;
      try {
        block = await this.mineAsync(this.job);
      } catch (e) {
        if (this.stopping)
          break;
        this.emit('error', e);
        break;
      }

      if (this.stopping)
        break;

      if (!block)
        continue;

      let entry;
      try {
        entry = await this.chain.add(block);
      } catch (e) {
        if (this.stopping)
          break;

        if (e.type === 'VerifyError') {
          this.logger.warning('Mined an invalid block!');
          this.logger.error(e);
          continue;
        }

        this.emit('error', e);
        break;
      }

      if (!entry) {
        this.logger.warning('Mined a bad-prevblk (race condition?)');
        continue;
      }

      if (this.stopping)
        break;

      this.logger.info('Found block: %d (%x).', entry.height, entry.hash);

      this.emit('block', block, entry);
    }

    const job = this.stopJob;

    if (job) {
      this.stopJob = null;
      job.resolve();
    }
  }

  /**
   * Stop mining.
   * @method
   * @returns {Promise}
   */

  async stop() {
    const unlock = await this.locker.lock();
    try {
      return await this._stop();
    } finally {
      unlock();
    }
  }

  /**
   * Stop mining (without a lock).
   * @method
   * @returns {Promise}
   */

  async _stop() {
    if (!this.running)
      return;

    assert(this.running, 'Miner is not running.');
    assert(!this.stopping, 'Miner is already stopping.');

    this.stopping = true;

    if (this.job) {
      this.job.destroy();
      this.job = null;
    }

    await this.wait();

    this.running = false;
    this.stopping = false;
    this.job = null;
  }

  /**
   * Wait for `done` event.
   * @private
   * @returns {Promise}
   */

  wait() {
    return new Promise((resolve, reject) => {
      assert(!this.stopJob);
      this.stopJob = { resolve, reject };
    });
  }

  /**
   * Create a mining job.
   * @method
   * @param {ChainEntry?} tip
   * @param {Address?} address
   * @returns {Promise} - Returns {@link Job}.
   */

  async createJob(tip, address) {
    const attempt = await this.miner.createBlock(tip, address);
    return new CPUJob(this, attempt);
  }

  /**
   * Mine a single block.
   * @method
   * @param {ChainEntry?} tip
   * @param {Address?} address
   * @returns {Promise} - Returns [{@link Block}].
   */

  async mineBlock(tip, address) {
    const job = await this.createJob(tip, address);
    return await this.mineAsync(job);
  }

  /**
   * Notify the miner that a new
   * tx has entered the mempool.
   */

  notifyEntry() {
    if (!this.running)
      return;

    if (!this.job)
      return;

    if (util.now() - this.job.start > 10) {
      this.job.destroy();
      this.job = null;
    }
  }

  /**
   * Hash until the nonce overflows.
   * @param {CPUJob} job
   * @returns {Number} nonce
   */

  findNonce(job) {
    const hdr = job.getHeader();
    const target = job.attempt.target;
    const interval = CPUMiner.INTERVAL;

    let nonce = 0;
    let solved = false;

    for (;;) {
      [nonce, solved] = mine(hdr, target, interval);

      if (solved)
        break;

      this.sendStatus(job, nonce);
    }

    return [nonce, solved];
  }

  /**
   * Hash until the nonce overflows.
   * @method
   * @param {CPUJob} job
   * @returns {Promise} Returns Number.
   */

  async findNonceAsync(job) {
    if (!this.workers)
      return this.findNonce(job);

    const hdr = job.getHeader();
    const target = job.attempt.target;
    const interval = CPUMiner.INTERVAL;

    let nonce = 0;
    let solved = false;

    for (;;) {
      [nonce, solved] = await this.workers.mine(hdr, target, interval);

      if (solved)
        break;

      if (job.destroyed)
        return [nonce, solved];

      this.sendStatus(job, nonce);
    }

    return [nonce, solved];
  }

  /**
   * Mine synchronously until the block is found.
   * @param {CPUJob} job
   * @returns {Block}
   */

  mine(job) {
    job.start = util.now();

    let nonce, solved;

    for (;;) {
      [nonce, solved] = this.findNonce(job);

      if (solved)
        break;

      job.updateNonce();

      this.sendStatus(job, 0);
    }

    return job.commit(nonce, solved);
  }

  /**
   * Mine asynchronously until the block is found.
   * @method
   * @param {CPUJob} job
   * @returns {Promise} - Returns {@link Block}.
   */

  async mineAsync(job) {
    let nonce, solved;

    job.start = util.now();

    for (;;) {
      [nonce, solved] = await this.findNonceAsync(job);

      if (solved)
        break;

      if (job.destroyed)
        return null;

      job.updateNonce();

      this.sendStatus(job, 0);
    }

    return job.commit(nonce, solved);
  }

  /**
   * Send a progress report (emits `status`).
   * @param {CPUJob} job
   * @param {Number} nonce
   */

  sendStatus(job, nonce) {
    const attempt = job.attempt;
    const tip = attempt.prevBlock;
    const hashes = job.getHashes(nonce);
    const hashrate = job.getRate(nonce);

    this.logger.info(
      'Status: hashrate=%dkhs hashes=%d target=%d height=%d tip=%x',
      Math.floor(hashrate / 1000),
      hashes,
      attempt.bits,
      attempt.height,
      tip);

    this.emit('status', job, hashes, hashrate);
  }
}

/**
 * Nonce range interval.
 * @const {Number}
 * @default
 */

CPUMiner.INTERVAL = 0xffffffff / 1500 | 0;

/**
 * Mining Job
 * @ignore
 */

class CPUJob {
  /**
   * Create a mining job.
   * @constructor
   * @param {CPUMiner} miner
   * @param {BlockTemplate} attempt
   */

  constructor(miner, attempt) {
    this.miner = miner;
    this.attempt = attempt;
    this.destroyed = false;
    this.committed = false;
    this.start = util.now();
    this.extraNonce = Buffer.alloc(consensus.NONCE_SIZE, 0x00);
    this.mask = consensus.ZERO_HASH;
    this.refresh();
  }

  /**
   * Get the raw block header.
   * @returns {Buffer}
   */

  getHeader() {
    const attempt = this.attempt;
    const time = attempt.time;
    const extraNonce = this.extraNonce;
    const mask = this.mask;
    const hdr = attempt.getHeader(0, time, extraNonce, mask);
    return hdr;
  }

  /**
   * Commit job and return a block.
   * @param {Number} nonce
   * @returns {Block}
   */

  commit(nonce) {
    const attempt = this.attempt;
    const time = attempt.time;
    const extraNonce = this.extraNonce;
    const mask = this.mask;

    assert(!this.committed, 'Job already committed.');
    this.committed = true;

    const proof = attempt.getProof(nonce, time, extraNonce, mask);

    return attempt.commit(proof);
  }

  /**
   * Mine block synchronously.
   * @returns {Block}
   */

  mine() {
    return this.miner.mine(this);
  }

  /**
   * Mine block asynchronously.
   * @returns {Promise}
   */

  mineAsync() {
    return this.miner.mineAsync(this);
  }

  /**
   * Refresh the block template.
   */

  refresh() {
    return this.attempt.refresh();
  }

  /**
   * Increment the extraNonce.
   */

  updateNonce() {
    for (let i = 0; i < consensus.NONCE_SIZE; i++) {
      this.extraNonce[i] += 1;

      if (this.extraNonce[i] !== 0)
        break;
    }
  }

  /**
   * Destroy the job.
   */

  destroy() {
    assert(!this.destroyed, 'Job already destroyed.');
    this.destroyed = true;
  }

  /**
   * Calculate number of hashes computed.
   * @param {Number} nonce
   * @returns {Number}
   */

  getHashes(nonce) {
    const nonce1 = bio.readU32(this.extraNonce, 0);
    const nonce2 = bio.readU32(this.extraNonce, 4);
    const extra = nonce2 * 0x100000000 + nonce1;
    return extra * 0xffffffff + nonce;
  }

  /**
   * Calculate hashrate.
   * @param {Number} nonce
   * @returns {Number}
   */

  getRate(nonce) {
    const hashes = this.getHashes(nonce);
    const seconds = util.now() - this.start;
    return Math.floor(hashes / Math.max(1, seconds));
  }

  /**
   * Add a transaction to the block.
   * @param {TX} tx
   * @param {CoinView} view
   */

  addTX(tx, view) {
    return this.attempt.addTX(tx, view);
  }

  /**
   * Add a transaction to the block
   * (less verification than addTX).
   * @param {TX} tx
   * @param {CoinView?} view
   */

  pushTX(tx, view) {
    return this.attempt.pushTX(tx, view);
  }

  /**
   * Add a claim to the block.
   * @param {Claim} claim
   * @param {Object} data
   */

  addClaim(claim, data) {
    return this.attempt.addClaim(claim, data);
  }

  /**
   * Add a claim to the block.
   * @param {Claim} claim
   * @param {Network} network
   */

  pushClaim(claim, network) {
    const data = claim.getData(network);
    assert(data);
    return this.addClaim(claim, data);
  }

  /**
   * Add a airdrop proof to the block.
   * @param {AirdropProof} proof
   */

  addAirdrop(proof) {
    return this.attempt.addAirdrop(proof);
  }

  /**
   * Add a airdrop proof to the block.
   * @param {AirdropProof} proof
   */

  pushAirdrop(proof) {
    return this.addAirdrop(proof);
  }
}

/*
 * Expose
 */

module.exports = CPUMiner;