From 8be995bd788a98a351600df38a6d4cab7aada836 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Thu, 16 Nov 2017 19:10:26 -0800 Subject: [PATCH] miner: classify. --- lib/mempool/fees.js | 4 +- lib/mining/cpuminer.js | 1045 ++++++++++++++++++++-------------------- lib/mining/miner.js | 876 ++++++++++++++++----------------- 3 files changed, 968 insertions(+), 957 deletions(-) diff --git a/lib/mempool/fees.js b/lib/mempool/fees.js index 94a17ced..51ab5f25 100644 --- a/lib/mempool/fees.js +++ b/lib/mempool/fees.js @@ -388,7 +388,7 @@ class ConfirmStats { */ static fromRaw(data, type, logger) { - return new ConfirmStats(type, logger).fromRaw(data); + return new this(type, logger).fromRaw(data); } } @@ -813,7 +813,7 @@ class PolicyEstimator { */ static fromRaw(data, logger) { - return new PolicyEstimator(logger).fromRaw(data); + return new this(logger).fromRaw(data); } /** diff --git a/lib/mining/cpuminer.js b/lib/mining/cpuminer.js index 5e86e076..0834c80f 100644 --- a/lib/mining/cpuminer.js +++ b/lib/mining/cpuminer.js @@ -17,36 +17,407 @@ const mine = require('./mine'); /** * CPU miner. * @alias module:mining.CPUMiner - * @constructor - * @param {Miner} miner - * @emits CPUMiner#block - * @emits CPUMiner#status */ -function CPUMiner(miner) { - if (!(this instanceof CPUMiner)) - return new CPUMiner(miner); +class CPUMiner extends EventEmitter { + /** + * Create a CPU miner. + * @constructor + * @param {Miner} miner + */ - EventEmitter.call(this); + 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.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.running = false; + this.stopping = false; + this.job = null; + this.stopJob = null; - this.init(); + this.init(); + } + + /** + * Initialize the miner. + * @private + */ + + init() { + this.chain.on('tip', (tip) => { + if (!this.job) + return; + + if (this.job.attempt.prevBlock === 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; + + // Log the block hex as a failsafe (in case we can't send it). + this.logger.info('Found block: %d (%s).', entry.height, entry.rhash()); + this.logger.debug('Raw: %s', block.toRaw().toString('hex')); + + 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 data = job.getHeader(); + const target = job.attempt.target; + const interval = CPUMiner.INTERVAL; + + let min = 0; + let max = interval; + let nonce; + + while (max <= 0xffffffff) { + nonce = mine(data, target, min, max); + + if (nonce !== -1) + break; + + this.sendStatus(job, max); + + min += interval; + max += interval; + } + + return nonce; + } + + /** + * Hash until the nonce overflows. + * @method + * @param {CPUJob} job + * @returns {Promise} Returns Number. + */ + + async findNonceAsync(job) { + if (!this.workers) + return this.findNonce(job); + + const data = job.getHeader(); + const target = job.attempt.target; + const interval = CPUMiner.INTERVAL; + + let min = 0; + let max = interval; + let nonce; + + while (max <= 0xffffffff) { + nonce = await this.workers.mine(data, target, min, max); + + if (nonce !== -1) + break; + + if (job.destroyed) + return nonce; + + this.sendStatus(job, max); + + min += interval; + max += interval; + } + + return nonce; + } + + /** + * Mine synchronously until the block is found. + * @param {CPUJob} job + * @returns {Block} + */ + + mine(job) { + job.start = util.now(); + + let nonce; + for (;;) { + nonce = this.findNonce(job); + + if (nonce !== -1) + break; + + job.updateNonce(); + + this.sendStatus(job, 0); + } + + return job.commit(nonce); + } + + /** + * Mine asynchronously until the block is found. + * @method + * @param {CPUJob} job + * @returns {Promise} - Returns {@link Block}. + */ + + async mineAsync(job) { + let nonce; + + job.start = util.now(); + + for (;;) { + nonce = await this.findNonceAsync(job); + + if (nonce !== -1) + break; + + if (job.destroyed) + return null; + + job.updateNonce(); + + this.sendStatus(job, 0); + } + + return job.commit(nonce); + } + + /** + * Send a progress report (emits `status`). + * @param {CPUJob} job + * @param {Number} nonce + */ + + sendStatus(job, nonce) { + const attempt = job.attempt; + const tip = encoding.revHex(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=%s', + Math.floor(hashrate / 1000), + hashes, + attempt.bits, + attempt.height, + tip); + + this.emit('status', job, hashes, hashrate); + } } -Object.setPrototypeOf(CPUMiner.prototype, EventEmitter.prototype); - /** * Nonce range interval. * @const {Number} @@ -55,522 +426,156 @@ Object.setPrototypeOf(CPUMiner.prototype, EventEmitter.prototype); CPUMiner.INTERVAL = 0xffffffff / 1500 | 0; -/** - * Initialize the miner. - * @private - */ - -CPUMiner.prototype.init = function init() { - this.chain.on('tip', (tip) => { - if (!this.job) - return; - - if (this.job.attempt.prevBlock === tip.prevBlock) - this.job.destroy(); - }); -}; - -/** - * Open the miner. - * @returns {Promise} - */ - -CPUMiner.prototype.open = async function open() { - assert(!this.opened, 'CPUMiner is already open.'); - this.opened = true; -}; - -/** - * Close the miner. - * @returns {Promise} - */ - -CPUMiner.prototype.close = async function close() { - assert(this.opened, 'CPUMiner is not open.'); - this.opened = false; - return this.stop(); -}; - -/** - * Start mining. - * @method - */ - -CPUMiner.prototype.start = function start() { - assert(!this.running, 'Miner is already running.'); - this._start().catch(() => {}); -}; - -/** - * Start mining. - * @method - * @private - * @returns {Promise} - */ - -CPUMiner.prototype._start = async function _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; - - // Log the block hex as a failsafe (in case we can't send it). - this.logger.info('Found block: %d (%s).', entry.height, entry.rhash()); - this.logger.debug('Raw: %s', block.toRaw().toString('hex')); - - this.emit('block', block, entry); - } - - const job = this.stopJob; - - if (job) { - this.stopJob = null; - job.resolve(); - } -}; - -/** - * Stop mining. - * @method - * @returns {Promise} - */ - -CPUMiner.prototype.stop = async function stop() { - const unlock = await this.locker.lock(); - try { - return await this._stop(); - } finally { - unlock(); - } -}; - -/** - * Stop mining (without a lock). - * @method - * @returns {Promise} - */ - -CPUMiner.prototype._stop = async function _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} - */ - -CPUMiner.prototype.wait = function 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}. - */ - -CPUMiner.prototype.createJob = async function 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}]. - */ - -CPUMiner.prototype.mineBlock = async function 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. - */ - -CPUMiner.prototype.notifyEntry = function 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 - */ - -CPUMiner.prototype.findNonce = function findNonce(job) { - const data = job.getHeader(); - const target = job.attempt.target; - const interval = CPUMiner.INTERVAL; - - let min = 0; - let max = interval; - let nonce; - - while (max <= 0xffffffff) { - nonce = mine(data, target, min, max); - - if (nonce !== -1) - break; - - this.sendStatus(job, max); - - min += interval; - max += interval; - } - - return nonce; -}; - -/** - * Hash until the nonce overflows. - * @method - * @param {CPUJob} job - * @returns {Promise} Returns Number. - */ - -CPUMiner.prototype.findNonceAsync = async function findNonceAsync(job) { - if (!this.workers) - return this.findNonce(job); - - const data = job.getHeader(); - const target = job.attempt.target; - const interval = CPUMiner.INTERVAL; - - let min = 0; - let max = interval; - let nonce; - - while (max <= 0xffffffff) { - nonce = await this.workers.mine(data, target, min, max); - - if (nonce !== -1) - break; - - if (job.destroyed) - return nonce; - - this.sendStatus(job, max); - - min += interval; - max += interval; - } - - return nonce; -}; - -/** - * Mine synchronously until the block is found. - * @param {CPUJob} job - * @returns {Block} - */ - -CPUMiner.prototype.mine = function mine(job) { - job.start = util.now(); - - let nonce; - for (;;) { - nonce = this.findNonce(job); - - if (nonce !== -1) - break; - - job.updateNonce(); - - this.sendStatus(job, 0); - } - - return job.commit(nonce); -}; - -/** - * Mine asynchronously until the block is found. - * @method - * @param {CPUJob} job - * @returns {Promise} - Returns {@link Block}. - */ - -CPUMiner.prototype.mineAsync = async function mineAsync(job) { - let nonce; - - job.start = util.now(); - - for (;;) { - nonce = await this.findNonceAsync(job); - - if (nonce !== -1) - break; - - if (job.destroyed) - return null; - - job.updateNonce(); - - this.sendStatus(job, 0); - } - - return job.commit(nonce); -}; - -/** - * Send a progress report (emits `status`). - * @param {CPUJob} job - * @param {Number} nonce - */ - -CPUMiner.prototype.sendStatus = function sendStatus(job, nonce) { - const attempt = job.attempt; - const tip = encoding.revHex(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=%s', - Math.floor(hashrate / 1000), - hashes, - attempt.bits, - attempt.height, - tip); - - this.emit('status', job, hashes, hashrate); -}; - /** * Mining Job - * @constructor * @ignore - * @param {CPUMiner} miner - * @param {BlockTemplate} attempt */ -function CPUJob(miner, attempt) { - this.miner = miner; - this.attempt = attempt; - this.destroyed = false; - this.committed = false; - this.start = util.now(); - this.nonce1 = 0; - this.nonce2 = 0; - this.refresh(); -} +class CPUJob { + /** + * Create a mining job. + * @constructor + * @param {CPUMiner} miner + * @param {BlockTemplate} attempt + */ -/** - * Get the raw block header. - * @param {Number} nonce - * @returns {Buffer} - */ - -CPUJob.prototype.getHeader = function getHeader() { - const attempt = this.attempt; - const n1 = this.nonce1; - const n2 = this.nonce2; - const time = attempt.time; - const root = attempt.getRoot(n1, n2); - const data = attempt.getHeader(root, time, 0); - return data; -}; - -/** - * Commit job and return a block. - * @param {Number} nonce - * @returns {Block} - */ - -CPUJob.prototype.commit = function commit(nonce) { - const attempt = this.attempt; - const n1 = this.nonce1; - const n2 = this.nonce2; - const time = attempt.time; - - assert(!this.committed, 'Job already committed.'); - this.committed = true; - - const proof = attempt.getProof(n1, n2, time, nonce); - - return attempt.commit(proof); -}; - -/** - * Mine block synchronously. - * @returns {Block} - */ - -CPUJob.prototype.mine = function mine() { - return this.miner.mine(this); -}; - -/** - * Mine block asynchronously. - * @returns {Promise} - */ - -CPUJob.prototype.mineAsync = function mineAsync() { - return this.miner.mineAsync(this); -}; - -/** - * Refresh the block template. - */ - -CPUJob.prototype.refresh = function refresh() { - return this.attempt.refresh(); -}; - -/** - * Increment the extraNonce. - */ - -CPUJob.prototype.updateNonce = function updateNonce() { - if (++this.nonce2 === 0x100000000) { + constructor(miner, attempt) { + this.miner = miner; + this.attempt = attempt; + this.destroyed = false; + this.committed = false; + this.start = util.now(); + this.nonce1 = 0; this.nonce2 = 0; - this.nonce1++; + this.refresh(); } -}; -/** - * Destroy the job. - */ + /** + * Get the raw block header. + * @param {Number} nonce + * @returns {Buffer} + */ -CPUJob.prototype.destroy = function destroy() { - assert(!this.destroyed, 'Job already destroyed.'); - this.destroyed = true; -}; + getHeader() { + const attempt = this.attempt; + const n1 = this.nonce1; + const n2 = this.nonce2; + const time = attempt.time; + const root = attempt.getRoot(n1, n2); + const data = attempt.getHeader(root, time, 0); + return data; + } -/** - * Calculate number of hashes computed. - * @param {Number} nonce - * @returns {Number} - */ + /** + * Commit job and return a block. + * @param {Number} nonce + * @returns {Block} + */ -CPUJob.prototype.getHashes = function getHashes(nonce) { - const extra = this.nonce1 * 0x100000000 + this.nonce2; - return extra * 0xffffffff + nonce; -}; + commit(nonce) { + const attempt = this.attempt; + const n1 = this.nonce1; + const n2 = this.nonce2; + const time = attempt.time; -/** - * Calculate hashrate. - * @param {Number} nonce - * @returns {Number} - */ + assert(!this.committed, 'Job already committed.'); + this.committed = true; -CPUJob.prototype.getRate = function getRate(nonce) { - const hashes = this.getHashes(nonce); - const seconds = util.now() - this.start; - return Math.floor(hashes / Math.max(1, seconds)); -}; + const proof = attempt.getProof(n1, n2, time, nonce); -/** - * Add a transaction to the block. - * @param {TX} tx - * @param {CoinView} view - */ + return attempt.commit(proof); + } -CPUJob.prototype.addTX = function addTX(tx, view) { - return this.attempt.addTX(tx, view); -}; + /** + * Mine block synchronously. + * @returns {Block} + */ -/** - * Add a transaction to the block - * (less verification than addTX). - * @param {TX} tx - * @param {CoinView?} view - */ + mine() { + return this.miner.mine(this); + } -CPUJob.prototype.pushTX = function pushTX(tx, view) { - return this.attempt.pushTX(tx, view); -}; + /** + * Mine block asynchronously. + * @returns {Promise} + */ + + mineAsync() { + return this.miner.mineAsync(this); + } + + /** + * Refresh the block template. + */ + + refresh() { + return this.attempt.refresh(); + } + + /** + * Increment the extraNonce. + */ + + updateNonce() { + if (++this.nonce2 === 0x100000000) { + this.nonce2 = 0; + this.nonce1++; + } + } + + /** + * 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 extra = this.nonce1 * 0x100000000 + this.nonce2; + 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); + } +} /* * Expose diff --git a/lib/mining/miner.js b/lib/mining/miner.js index 7b6a7f84..a5412a3c 100644 --- a/lib/mining/miner.js +++ b/lib/mining/miner.js @@ -17,495 +17,501 @@ const Network = require('../protocol/network'); const consensus = require('../protocol/consensus'); const policy = require('../protocol/policy'); const CPUMiner = require('./cpuminer'); -const BlockEntry = BlockTemplate.BlockEntry; +const {BlockEntry} = BlockTemplate; /** + * Miner * A bitcoin miner and block generator. * @alias module:mining.Miner - * @constructor - * @param {Object} options + * @extends EventEmitter */ -function Miner(options) { - if (!(this instanceof Miner)) - return new Miner(options); +class Miner extends EventEmitter { + /** + * Create a bitcoin miner. + * @constructor + * @param {Object} options + */ - EventEmitter.call(this); + constructor(options) { + super(); - this.opened = false; - this.options = new MinerOptions(options); - this.network = this.options.network; - this.logger = this.options.logger.context('miner'); - this.workers = this.options.workers; - this.chain = this.options.chain; - this.mempool = this.options.mempool; - this.addresses = this.options.addresses; - this.locker = this.chain.locker; - this.cpu = new CPUMiner(this); + this.opened = false; + this.options = new MinerOptions(options); + this.network = this.options.network; + this.logger = this.options.logger.context('miner'); + this.workers = this.options.workers; + this.chain = this.options.chain; + this.mempool = this.options.mempool; + this.addresses = this.options.addresses; + this.locker = this.chain.locker; + this.cpu = new CPUMiner(this); - this.init(); -} - -Object.setPrototypeOf(Miner.prototype, EventEmitter.prototype); - -/** - * Initialize the miner. - */ - -Miner.prototype.init = function init() { - this.cpu.on('error', (err) => { - this.emit('error', err); - }); -}; - -/** - * Open the miner, wait for the chain and mempool to load. - * @returns {Promise} - */ - -Miner.prototype.open = async function open() { - assert(!this.opened, 'Miner is already open.'); - this.opened = true; - - await this.cpu.open(); - - this.logger.info('Miner loaded (flags=%s).', - this.options.coinbaseFlags.toString('utf8')); - - if (this.addresses.length === 0) - this.logger.warning('No reward address is set for miner!'); -}; - -/** - * Close the miner. - * @returns {Promise} - */ - -Miner.prototype.close = async function close() { - assert(this.opened, 'Miner is not open.'); - this.opened = false; - return this.cpu.close(); -}; - -/** - * Create a block template. - * @method - * @param {ChainEntry?} tip - * @param {Address?} address - * @returns {Promise} - Returns {@link BlockTemplate}. - */ - -Miner.prototype.createBlock = async function createBlock(tip, address) { - const unlock = await this.locker.lock(); - try { - return await this._createBlock(tip, address); - } finally { - unlock(); + this.init(); } -}; -/** - * Create a block template (without a lock). - * @method - * @private - * @param {ChainEntry?} tip - * @param {Address?} address - * @returns {Promise} - Returns {@link BlockTemplate}. - */ + /** + * Initialize the miner. + */ -Miner.prototype._createBlock = async function _createBlock(tip, address) { - let version = this.options.version; + init() { + this.cpu.on('error', (err) => { + this.emit('error', err); + }); + } - if (!tip) - tip = this.chain.tip; + /** + * Open the miner, wait for the chain and mempool to load. + * @returns {Promise} + */ - if (!address) - address = this.getAddress(); + async open() { + assert(!this.opened, 'Miner is already open.'); + this.opened = true; - if (version === -1) - version = await this.chain.computeBlockVersion(tip); + await this.cpu.open(); - const mtp = await this.chain.getMedianTime(tip); - const time = Math.max(this.network.now(), mtp + 1); + this.logger.info('Miner loaded (flags=%s).', + this.options.coinbaseFlags.toString('utf8')); - const state = await this.chain.getDeployments(time, tip); - const target = await this.chain.getTarget(time, tip); + if (this.addresses.length === 0) + this.logger.warning('No reward address is set for miner!'); + } - const locktime = state.hasMTP() ? mtp : time; + /** + * Close the miner. + * @returns {Promise} + */ - const attempt = new BlockTemplate({ - prevBlock: tip.hash, - height: tip.height + 1, - version: version, - time: time, - bits: target, - locktime: locktime, - mtp: mtp, - flags: state.flags, - address: address, - coinbaseFlags: this.options.coinbaseFlags, - witness: state.hasWitness(), - interval: this.network.halvingInterval, - weight: this.options.reservedWeight, - sigops: this.options.reservedSigops - }); + async close() { + assert(this.opened, 'Miner is not open.'); + this.opened = false; + return this.cpu.close(); + } - this.assemble(attempt); - - this.logger.debug( - 'Created block template (height=%d, weight=%d, fees=%d, txs=%s, diff=%d).', - attempt.height, - attempt.weight, - Amount.btc(attempt.fees), - attempt.items.length + 1, - attempt.getDifficulty()); - - if (this.options.preverify) { - const block = attempt.toBlock(); + /** + * Create a block template. + * @method + * @param {ChainEntry?} tip + * @param {Address?} address + * @returns {Promise} - Returns {@link BlockTemplate}. + */ + async createBlock(tip, address) { + const unlock = await this.locker.lock(); try { - await this.chain._verifyBlock(block); - } catch (e) { - if (e.type === 'VerifyError') { - this.logger.warning('Miner created invalid block!'); - this.logger.error(e); - throw new Error('BUG: Miner created invalid block.'); - } - throw e; + return await this._createBlock(tip, address); + } finally { + unlock(); } + } + + /** + * Create a block template (without a lock). + * @method + * @private + * @param {ChainEntry?} tip + * @param {Address?} address + * @returns {Promise} - Returns {@link BlockTemplate}. + */ + + async _createBlock(tip, address) { + let version = this.options.version; + + if (!tip) + tip = this.chain.tip; + + if (!address) + address = this.getAddress(); + + if (version === -1) + version = await this.chain.computeBlockVersion(tip); + + const mtp = await this.chain.getMedianTime(tip); + const time = Math.max(this.network.now(), mtp + 1); + + const state = await this.chain.getDeployments(time, tip); + const target = await this.chain.getTarget(time, tip); + + const locktime = state.hasMTP() ? mtp : time; + + const attempt = new BlockTemplate({ + prevBlock: tip.hash, + height: tip.height + 1, + version: version, + time: time, + bits: target, + locktime: locktime, + mtp: mtp, + flags: state.flags, + address: address, + coinbaseFlags: this.options.coinbaseFlags, + witness: state.hasWitness(), + interval: this.network.halvingInterval, + weight: this.options.reservedWeight, + sigops: this.options.reservedSigops + }); + + this.assemble(attempt); this.logger.debug( - 'Preverified block %d successfully!', - attempt.height); - } + 'Created block template (height=%d, weight=%d, fees=%d, txs=%s, diff=%d).', + attempt.height, + attempt.weight, + Amount.btc(attempt.fees), + attempt.items.length + 1, + attempt.getDifficulty()); - return attempt; -}; + if (this.options.preverify) { + const block = attempt.toBlock(); -/** - * Update block timestamp. - * @param {BlockTemplate} attempt - */ - -Miner.prototype.updateTime = function updateTime(attempt) { - attempt.time = Math.max(this.network.now(), attempt.mtp + 1); -}; - -/** - * Create a cpu miner job. - * @method - * @param {ChainEntry?} tip - * @param {Address?} address - * @returns {Promise} Returns {@link CPUJob}. - */ - -Miner.prototype.createJob = function createJob(tip, address) { - return this.cpu.createJob(tip, address); -}; - -/** - * Mine a single block. - * @method - * @param {ChainEntry?} tip - * @param {Address?} address - * @returns {Promise} Returns {@link Block}. - */ - -Miner.prototype.mineBlock = function mineBlock(tip, address) { - return this.cpu.mineBlock(tip, address); -}; - -/** - * Add an address to the address list. - * @param {Address} address - */ - -Miner.prototype.addAddress = function addAddress(address) { - this.addresses.push(new Address(address)); -}; - -/** - * Get a random address from the address list. - * @returns {Address} - */ - -Miner.prototype.getAddress = function getAddress() { - if (this.addresses.length === 0) - return new Address(); - return this.addresses[Math.random() * this.addresses.length | 0]; -}; - -/** - * Get mempool entries, sort by dependency order. - * Prioritize by priority and fee rates. - * @param {BlockTemplate} attempt - * @returns {MempoolEntry[]} - */ - -Miner.prototype.assemble = function assemble(attempt) { - if (!this.mempool) { - attempt.refresh(); - return; - } - - assert(this.mempool.tip === this.chain.tip.hash, - 'Mempool/chain tip mismatch! Unsafe to create block.'); - - const depMap = new Map(); - const queue = new Heap(cmpRate); - - let priority = this.options.priorityWeight > 0; - - if (priority) - queue.set(cmpPriority); - - for (const entry of this.mempool.map.values()) { - const item = BlockEntry.fromEntry(entry, attempt); - const tx = item.tx; - - if (tx.isCoinbase()) - throw new Error('Cannot add coinbase to block.'); - - for (const {prevout} of tx.inputs) { - const hash = prevout.hash; - - if (!this.mempool.hasEntry(hash)) - continue; - - item.depCount += 1; - - if (!depMap.has(hash)) - depMap.set(hash, []); - - depMap.get(hash).push(item); - } - - if (item.depCount > 0) - continue; - - queue.insert(item); - } - - while (queue.size() > 0) { - const item = queue.shift(); - const tx = item.tx; - const hash = item.hash; - - let weight = attempt.weight; - let sigops = attempt.sigops; - - if (!tx.isFinal(attempt.height, attempt.locktime)) - continue; - - if (!attempt.witness && tx.hasWitness()) - continue; - - weight += tx.getWeight(); - - if (weight > this.options.maxWeight) - continue; - - sigops += item.sigops; - - if (sigops > this.options.maxSigops) - continue; - - if (priority) { - if (weight > this.options.priorityWeight - || item.priority < this.options.priorityThreshold) { - priority = false; - queue.set(cmpRate); - queue.init(); - queue.insert(item); - continue; + try { + await this.chain._verifyBlock(block); + } catch (e) { + if (e.type === 'VerifyError') { + this.logger.warning('Miner created invalid block!'); + this.logger.error(e); + throw new Error('BUG: Miner created invalid block.'); + } + throw e; } - } else { - if (item.free && weight >= this.options.minWeight) - continue; + + this.logger.debug( + 'Preverified block %d successfully!', + attempt.height); } - attempt.weight = weight; - attempt.sigops = sigops; - attempt.fees += item.fee; - attempt.items.push(item); - - const deps = depMap.get(hash); - - if (!deps) - continue; - - for (const item of deps) { - if (--item.depCount === 0) - queue.insert(item); - } + return attempt; } - attempt.refresh(); + /** + * Update block timestamp. + * @param {BlockTemplate} attempt + */ - assert(attempt.weight <= consensus.MAX_BLOCK_WEIGHT, - 'Block exceeds reserved weight!'); + updateTime(attempt) { + attempt.time = Math.max(this.network.now(), attempt.mtp + 1); + } - if (this.options.preverify) { - const block = attempt.toBlock(); + /** + * Create a cpu miner job. + * @method + * @param {ChainEntry?} tip + * @param {Address?} address + * @returns {Promise} Returns {@link CPUJob}. + */ - assert(block.getWeight() <= attempt.weight, + createJob(tip, address) { + return this.cpu.createJob(tip, address); + } + + /** + * Mine a single block. + * @method + * @param {ChainEntry?} tip + * @param {Address?} address + * @returns {Promise} Returns {@link Block}. + */ + + mineBlock(tip, address) { + return this.cpu.mineBlock(tip, address); + } + + /** + * Add an address to the address list. + * @param {Address} address + */ + + addAddress(address) { + this.addresses.push(new Address(address)); + } + + /** + * Get a random address from the address list. + * @returns {Address} + */ + + getAddress() { + if (this.addresses.length === 0) + return new Address(); + return this.addresses[Math.random() * this.addresses.length | 0]; + } + + /** + * Get mempool entries, sort by dependency order. + * Prioritize by priority and fee rates. + * @param {BlockTemplate} attempt + * @returns {MempoolEntry[]} + */ + + assemble(attempt) { + if (!this.mempool) { + attempt.refresh(); + return; + } + + assert(this.mempool.tip === this.chain.tip.hash, + 'Mempool/chain tip mismatch! Unsafe to create block.'); + + const depMap = new Map(); + const queue = new Heap(cmpRate); + + let priority = this.options.priorityWeight > 0; + + if (priority) + queue.set(cmpPriority); + + for (const entry of this.mempool.map.values()) { + const item = BlockEntry.fromEntry(entry, attempt); + const tx = item.tx; + + if (tx.isCoinbase()) + throw new Error('Cannot add coinbase to block.'); + + for (const {prevout} of tx.inputs) { + const hash = prevout.hash; + + if (!this.mempool.hasEntry(hash)) + continue; + + item.depCount += 1; + + if (!depMap.has(hash)) + depMap.set(hash, []); + + depMap.get(hash).push(item); + } + + if (item.depCount > 0) + continue; + + queue.insert(item); + } + + while (queue.size() > 0) { + const item = queue.shift(); + const tx = item.tx; + const hash = item.hash; + + let weight = attempt.weight; + let sigops = attempt.sigops; + + if (!tx.isFinal(attempt.height, attempt.locktime)) + continue; + + if (!attempt.witness && tx.hasWitness()) + continue; + + weight += tx.getWeight(); + + if (weight > this.options.maxWeight) + continue; + + sigops += item.sigops; + + if (sigops > this.options.maxSigops) + continue; + + if (priority) { + if (weight > this.options.priorityWeight + || item.priority < this.options.priorityThreshold) { + priority = false; + queue.set(cmpRate); + queue.init(); + queue.insert(item); + continue; + } + } else { + if (item.free && weight >= this.options.minWeight) + continue; + } + + attempt.weight = weight; + attempt.sigops = sigops; + attempt.fees += item.fee; + attempt.items.push(item); + + const deps = depMap.get(hash); + + if (!deps) + continue; + + for (const item of deps) { + if (--item.depCount === 0) + queue.insert(item); + } + } + + attempt.refresh(); + + assert(attempt.weight <= consensus.MAX_BLOCK_WEIGHT, 'Block exceeds reserved weight!'); - assert(block.getBaseSize() <= consensus.MAX_BLOCK_SIZE, - 'Block exceeds max block size.'); + if (this.options.preverify) { + const block = attempt.toBlock(); + + assert(block.getWeight() <= attempt.weight, + 'Block exceeds reserved weight!'); + + assert(block.getBaseSize() <= consensus.MAX_BLOCK_SIZE, + 'Block exceeds max block size.'); + } } -}; - -/** - * MinerOptions - * @alias module:mining.MinerOptions - * @constructor - * @param {Object} - */ - -function MinerOptions(options) { - if (!(this instanceof MinerOptions)) - return new MinerOptions(options); - - this.network = Network.primary; - this.logger = null; - this.workers = null; - this.chain = null; - this.mempool = null; - - this.version = -1; - this.addresses = []; - this.coinbaseFlags = Buffer.from('mined by bcoin', 'ascii'); - this.preverify = false; - - this.minWeight = policy.MIN_BLOCK_WEIGHT; - this.maxWeight = policy.MAX_BLOCK_WEIGHT; - this.priorityWeight = policy.BLOCK_PRIORITY_WEIGHT; - this.priorityThreshold = policy.BLOCK_PRIORITY_THRESHOLD; - this.maxSigops = consensus.MAX_BLOCK_SIGOPS_COST; - this.reservedWeight = 4000; - this.reservedSigops = 400; - - this.fromOptions(options); } /** - * Inject properties from object. - * @private - * @param {Object} options - * @returns {MinerOptions} + * Miner Options + * @alias module:mining.MinerOptions */ -MinerOptions.prototype.fromOptions = function fromOptions(options) { - assert(options, 'Miner requires options.'); - assert(options.chain && typeof options.chain === 'object', - 'Miner requires a blockchain.'); +class MinerOptions { + /** + * Create miner options. + * @constructor + * @param {Object} + */ - this.chain = options.chain; - this.network = options.chain.network; - this.logger = options.chain.logger; - this.workers = options.chain.workers; + constructor(options) { + this.network = Network.primary; + this.logger = null; + this.workers = null; + this.chain = null; + this.mempool = null; - if (options.logger != null) { - assert(typeof options.logger === 'object'); - this.logger = options.logger; + this.version = -1; + this.addresses = []; + this.coinbaseFlags = Buffer.from('mined by bcoin', 'ascii'); + this.preverify = false; + + this.minWeight = policy.MIN_BLOCK_WEIGHT; + this.maxWeight = policy.MAX_BLOCK_WEIGHT; + this.priorityWeight = policy.BLOCK_PRIORITY_WEIGHT; + this.priorityThreshold = policy.BLOCK_PRIORITY_THRESHOLD; + this.maxSigops = consensus.MAX_BLOCK_SIGOPS_COST; + this.reservedWeight = 4000; + this.reservedSigops = 400; + + this.fromOptions(options); } - if (options.workers != null) { - assert(typeof options.workers === 'object'); - this.workers = options.workers; - } + /** + * Inject properties from object. + * @private + * @param {Object} options + * @returns {MinerOptions} + */ - if (options.mempool != null) { - assert(typeof options.mempool === 'object'); - this.mempool = options.mempool; - } + fromOptions(options) { + assert(options, 'Miner requires options.'); + assert(options.chain && typeof options.chain === 'object', + 'Miner requires a blockchain.'); - if (options.version != null) { - assert((options.version >>> 0) === options.version); - this.version = options.version; - } + this.chain = options.chain; + this.network = options.chain.network; + this.logger = options.chain.logger; + this.workers = options.chain.workers; - if (options.address) { - if (Array.isArray(options.address)) { - for (const item of options.address) - this.addresses.push(new Address(item)); - } else { - this.addresses.push(new Address(options.address)); + if (options.logger != null) { + assert(typeof options.logger === 'object'); + this.logger = options.logger; } + + if (options.workers != null) { + assert(typeof options.workers === 'object'); + this.workers = options.workers; + } + + if (options.mempool != null) { + assert(typeof options.mempool === 'object'); + this.mempool = options.mempool; + } + + if (options.version != null) { + assert((options.version >>> 0) === options.version); + this.version = options.version; + } + + if (options.address) { + if (Array.isArray(options.address)) { + for (const item of options.address) + this.addresses.push(new Address(item)); + } else { + this.addresses.push(new Address(options.address)); + } + } + + if (options.addresses) { + assert(Array.isArray(options.addresses)); + for (const item of options.addresses) + this.addresses.push(new Address(item)); + } + + if (options.coinbaseFlags) { + let flags = options.coinbaseFlags; + if (typeof flags === 'string') + flags = Buffer.from(flags, 'utf8'); + assert(Buffer.isBuffer(flags)); + assert(flags.length <= 20, 'Coinbase flags > 20 bytes.'); + this.coinbaseFlags = flags; + } + + if (options.preverify != null) { + assert(typeof options.preverify === 'boolean'); + this.preverify = options.preverify; + } + + if (options.minWeight != null) { + assert((options.minWeight >>> 0) === options.minWeight); + this.minWeight = options.minWeight; + } + + if (options.maxWeight != null) { + assert((options.maxWeight >>> 0) === options.maxWeight); + assert(options.maxWeight <= consensus.MAX_BLOCK_WEIGHT, + 'Max weight must be below MAX_BLOCK_WEIGHT'); + this.maxWeight = options.maxWeight; + } + + if (options.maxSigops != null) { + assert((options.maxSigops >>> 0) === options.maxSigops); + assert(options.maxSigops <= consensus.MAX_BLOCK_SIGOPS_COST, + 'Max sigops must be below MAX_BLOCK_SIGOPS_COST'); + this.maxSigops = options.maxSigops; + } + + if (options.priorityWeight != null) { + assert((options.priorityWeight >>> 0) === options.priorityWeight); + this.priorityWeight = options.priorityWeight; + } + + if (options.priorityThreshold != null) { + assert((options.priorityThreshold >>> 0) === options.priorityThreshold); + this.priorityThreshold = options.priorityThreshold; + } + + if (options.reservedWeight != null) { + assert((options.reservedWeight >>> 0) === options.reservedWeight); + this.reservedWeight = options.reservedWeight; + } + + if (options.reservedSigops != null) { + assert((options.reservedSigops >>> 0) === options.reservedSigops); + this.reservedSigops = options.reservedSigops; + } + + return this; } - if (options.addresses) { - assert(Array.isArray(options.addresses)); - for (const item of options.addresses) - this.addresses.push(new Address(item)); + /** + * Instantiate miner options from object. + * @param {Object} options + * @returns {MinerOptions} + */ + + static fromOptions(options) { + return new this().fromOptions(options); } - - if (options.coinbaseFlags) { - let flags = options.coinbaseFlags; - if (typeof flags === 'string') - flags = Buffer.from(flags, 'utf8'); - assert(Buffer.isBuffer(flags)); - assert(flags.length <= 20, 'Coinbase flags > 20 bytes.'); - this.coinbaseFlags = flags; - } - - if (options.preverify != null) { - assert(typeof options.preverify === 'boolean'); - this.preverify = options.preverify; - } - - if (options.minWeight != null) { - assert((options.minWeight >>> 0) === options.minWeight); - this.minWeight = options.minWeight; - } - - if (options.maxWeight != null) { - assert((options.maxWeight >>> 0) === options.maxWeight); - assert(options.maxWeight <= consensus.MAX_BLOCK_WEIGHT, - 'Max weight must be below MAX_BLOCK_WEIGHT'); - this.maxWeight = options.maxWeight; - } - - if (options.maxSigops != null) { - assert((options.maxSigops >>> 0) === options.maxSigops); - assert(options.maxSigops <= consensus.MAX_BLOCK_SIGOPS_COST, - 'Max sigops must be below MAX_BLOCK_SIGOPS_COST'); - this.maxSigops = options.maxSigops; - } - - if (options.priorityWeight != null) { - assert((options.priorityWeight >>> 0) === options.priorityWeight); - this.priorityWeight = options.priorityWeight; - } - - if (options.priorityThreshold != null) { - assert((options.priorityThreshold >>> 0) === options.priorityThreshold); - this.priorityThreshold = options.priorityThreshold; - } - - if (options.reservedWeight != null) { - assert((options.reservedWeight >>> 0) === options.reservedWeight); - this.reservedWeight = options.reservedWeight; - } - - if (options.reservedSigops != null) { - assert((options.reservedSigops >>> 0) === options.reservedSigops); - this.reservedSigops = options.reservedSigops; - } - - return this; -}; - -/** - * Instantiate miner options from object. - * @param {Object} options - * @returns {MinerOptions} - */ - -MinerOptions.fromOptions = function fromOptions(options) { - return new MinerOptions().fromOptions(options); -}; +} /* * Helpers