/*! * miner.js - inefficient miner for bcoin (because we can) * Copyright (c) 2014-2015, Fedor Indutny (MIT License) * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). * https://github.com/bcoin-org/bcoin */ 'use strict'; var assert = require('assert'); var util = require('../utils/util'); var co = require('../utils/co'); var AsyncObject = require('../utils/async'); var Address = require('../primitives/address'); var MinerBlock = require('./minerblock'); var Network = require('../protocol/network'); var consensus = require('../protocol/consensus'); var policy = require('../protocol/policy'); var BlockEntry = MinerBlock.BlockEntry; /** * A bitcoin miner (supports mining witness blocks). * @exports Miner * @constructor * @param {Object} options * @param {Base58Address} options.address - Payout address. * @param {String?} [options.coinbaseFlags="mined by bcoin"] * @property {Boolean} running * @property {MinerBlock} attempt * @emits Miner#block * @emits Miner#status */ function Miner(options) { if (!(this instanceof Miner)) return new Miner(options); AsyncObject.call(this); this.options = new MinerOptions(options); this.network = this.options.network; this.logger = this.options.logger; this.chain = this.options.chain; this.mempool = this.options.mempool; this.addresses = this.options.addresses; this.locker = this.chain.locker; this.running = false; this.stopping = false; this.attempt = null; this.since = 0; this._init(); } util.inherits(Miner, AsyncObject); /** * Initialize the miner. * @private */ Miner.prototype._init = function _init() { var self = this; this.chain.on('tip', function(tip) { if (!self.attempt) return; if (self.attempt.block.prevBlock === tip.prevBlock) self.attempt.destroy(); }); this.on('block', function(block, entry) { // Emit the block hex as a failsafe (in case we can't send it) self.logger.info('Found block: %d (%s).', entry.height, entry.rhash()); self.logger.debug('Raw: %s', block.toRaw().toString('hex')); }); this.on('status', function(stat) { self.logger.info( 'Miner: hashrate=%dkhs hashes=%d target=%d height=%d best=%s', stat.hashrate / 1000 | 0, stat.hashes, stat.target, stat.height, stat.best); }); }; /** * Open the miner, wait for the chain and mempool to load. * @alias Miner#open * @returns {Promise} */ Miner.prototype._open = co(function* open() { yield this.chain.open(); if (this.mempool) yield this.mempool.open(); this.logger.info('Miner loaded (flags=%s).', this.options.coinbaseFlags.toString('utf8')); }); /** * Close the miner. * @alias Miner#close * @returns {Promise} */ Miner.prototype._close = co(function* close() { if (!this.running) return; if (this.stopping) { yield this._onStop(); return; } yield this.stop(); }); /** * Start mining. * @param {Number?} version - Custom block version. */ Miner.prototype.start = co(function* start() { var self = this; var block, entry; assert(!this.running, 'Miner is already running.'); this.running = true; this.stopping = false; for (;;) { this.attempt = null; try { this.attempt = yield this.createBlock(); } catch (e) { if (this.stopping) break; this.emit('error', e); continue; } if (this.stopping) break; this.attempt.on('status', function(status) { self.emit('status', status); }); try { block = yield this.attempt.mineAsync(); } catch (e) { if (this.stopping) break; this.emit('error', e); continue; } if (this.stopping) break; if (!block) continue; try { entry = yield this.chain.add(block); } catch (e) { if (this.stopping) break; this.emit('error', e); continue; } if (this.stopping) break; this.emit('block', block, entry); } this.emit('done'); }); /** * Stop mining. */ Miner.prototype.stop = co(function* stop() { assert(this.running, 'Miner is not running.'); assert(!this.stopping, 'Miner is already stopping.'); this.stopping = true; if (this.attempt) this.attempt.destroy(); yield this._onDone(); this.running = false; this.stopping = false; this.attempt = null; this.emit('stop'); }); /** * Wait for `done` event. */ Miner.prototype._onDone = function _onDone() { var self = this; return new Promise(function(resolve, reject) { self.once('done', resolve); }); }; /** * Wait for `stop` event. */ Miner.prototype._onStop = function _onStop() { var self = this; return new Promise(function(resolve, reject) { self.once('stop', resolve); }); }; /** * Create a block "attempt". * @param {ChainEntry} tip * @returns {Promise} - Returns {@link MinerBlock}. */ Miner.prototype.createBlock = co(function* createBlock(tip, address) { var unlock = yield this.locker.lock(); try { return yield this._createBlock(tip, address); } finally { unlock(); } }); /** * Create a block "attempt" (without a lock). * @param {ChainEntry} tip * @returns {Promise} - Returns {@link MinerBlock}. */ Miner.prototype._createBlock = co(function* createBlock(tip, address) { var version = this.options.version; var ts, locktime, target, attempt; if (!tip) tip = this.chain.tip; assert(tip); if (version === -1) version = yield this.chain.computeBlockVersion(tip); ts = Math.max(this.network.now(), tip.ts + 1); locktime = ts; target = yield this.chain.getTargetAsync(ts, tip); if (this.chain.state.hasMTP()) locktime = yield tip.getMedianTimeAsync(); if (!address) address = this.getAddress(); attempt = new MinerBlock({ tip: tip, version: version, bits: target, locktime: locktime, flags: this.chain.state.flags, address: address, coinbaseFlags: this.options.coinbaseFlags, witness: this.chain.state.hasWitness(), network: this.network }); this.build(attempt); return attempt; }); /** * Mine a single block. * @param {ChainEntry} tip * @returns {Promise} - Returns [{@link Block}]. */ Miner.prototype.mineBlock = co(function* mineBlock(tip, address) { var attempt = yield this.createBlock(tip, address); return yield attempt.mineAsync(); }); /** * Notify the miner that a new tx has entered the mempool. * @param {MempoolEntry} entry */ Miner.prototype.notifyEntry = function notifyEntry() { if (!this.running) return; if (!this.attempt) return; if (++this.since > 20) { this.since = 0; this.attempt.destroy(); } }; /** * Add an address to the address list. * @param {Address} address */ Miner.prototype.addAddress = function addAddress(address) { this.addresses.push(Address(address)); }; /** * Get a random address from the address list. * @returns {Address} */ Miner.prototype.getAddress = function getAddress() { assert(this.addresses.length !== 0, 'No address passed in for miner.'); return this.addresses[Math.random() * this.addresses.length | 0]; }; /** * Get mempool entries, sort by dependency order. * Prioritize by priority and fee rates. * @returns {MempoolEntry[]} */ Miner.prototype.build = function build(attempt) { var depMap = {}; var block = attempt.block; var queue = new Queue(cmpPriority); var priority = true; var i, j, entry, item, tx, hash, input; var prev, deps, hashes, weight, sigops; if (!this.mempool) return []; hashes = this.mempool.getSnapshot(); for (i = 0; i < hashes.length; i++) { hash = hashes[i]; entry = this.mempool.getEntry(hash); item = BlockEntry.fromEntry(entry, attempt); tx = item.tx; if (tx.isCoinbase()) throw new Error('Cannot add coinbase to block.'); for (j = 0; j < tx.inputs.length; j++) { input = tx.inputs[j]; prev = input.prevout.hash; if (!this.mempool.hasTX(prev)) continue; item.depCount += 1; if (!depMap[prev]) depMap[prev] = []; depMap[prev].push(item); } if (item.depCount > 0) continue; queue.push(item); } while (queue.size() > 0) { item = queue.pop(); tx = item.tx; hash = item.hash; weight = attempt.weight; 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.minPriority) { // Todo: Compare descendant rate with // cumulative fees and cumulative vsize. queue.cmp = cmpRate; priority = false; queue.push(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); block.txs.push(tx); deps = depMap[hash]; if (!deps) continue; for (j = 0; j < deps.length; j++) { item = deps[j]; if (--item.depCount === 0) queue.push(item); } } attempt.updateCoinbase(); attempt.updateMerkle(); assert(block.getWeight() <= attempt.weight); }; /** * MinerOptions * @constructor * @param {Object} */ function MinerOptions(options) { if (!(this instanceof MinerOptions)) return new MinerOptions(options); this.network = Network.primary; this.logger = null; this.chain = null; this.mempool = null; this.version = -1; this.addresses = []; this.coinbaseFlags = new Buffer('mined by bcoin', 'ascii'); this.minWeight = policy.MIN_BLOCK_WEIGHT; this.maxWeight = policy.MAX_BLOCK_WEIGHT; this.priorityWeight = policy.PRIORITY_BLOCK_WEIGHT; this.minPriority = policy.MIN_BLOCK_PRIORITY; this.maxSigops = consensus.MAX_BLOCK_SIGOPS_COST; this.fromOptions(options); } /** * Inject properties from object. * @private * @param {Object} options * @returns {MinerOptions} */ MinerOptions.prototype.fromOptions = function fromOptions(options) { var i, flags; assert(options, 'Miner requires options.'); assert(options.chain && typeof options.chain === 'object', 'Miner requires a blockchain.'); this.chain = options.chain; this.network = options.chain.network; this.logger = options.chain.logger; if (options.logger != null) { assert(typeof options.logger === 'object'); this.logger = options.logger; } if (options.mempool != null) { assert(typeof options.mempool === 'object'); this.mempool = options.mempool; } if (options.version != null) { assert(util.isNumber(options.version)); this.version = options.version; } if (options.address) this.addresses.push(new Address(options.address)); if (options.addresses) { assert(Array.isArray(options.addresses)); for (i = 0; i < options.addresses.length; i++) this.addresses.push(new Address(options.addresses[i])); } if (options.coinbaseFlags) { flags = options.coinbaseFlags; if (typeof flags === 'string') flags = new Buffer(flags, 'utf8'); assert(Buffer.isBuffer(flags)); this.coinbaseFlags = flags; } if (options.minWeight != null) { assert(util.isNumber(options.minWeight)); this.minWeight = options.minWeight; } if (options.maxWeight != null) { assert(util.isNumber(options.maxWeight)); this.maxWeight = options.maxWeight; } if (options.maxSigops != null) { assert(util.isNumber(options.maxSigops)); assert(options.maxSigops <= consensus.MAX_BLOCK_SIGOPS_COST); this.maxSigops = options.maxSigops; } if (options.priorityWeight != null) { assert(util.isNumber(options.priorityWeight)); this.priorityWeight = options.priorityWeight; } if (options.minPriority != null) { assert(util.isNumber(options.minPriority)); this.minPriority = options.minPriority; } return this; }; /** * Instantiate miner options from object. * @param {Object} options * @returns {MinerOptions} */ MinerOptions.fromOptions = function fromOptions(options) { return new MinerOptions().fromOptions(options); }; /** * Queue * @constructor */ function Queue(cmp) { this.cmp = cmp; this.items = []; } Queue.prototype.size = function size() { return this.items.length; }; Queue.prototype.push = function push(item) { util.binaryInsert(this.items, item, this.cmp); }; Queue.prototype.pop = function pop() { return this.items.pop(); }; /* * Helpers */ function cmpPriority(a, b) { return a.priority - b.priority; } function cmpRate(a, b) { return a.rate - b.rate; } /* * Expose */ module.exports = Miner;