UNPKG

hsd

Version:
678 lines (532 loc) 16.3 kB
/*! * miner.js - block generator for hsd * Copyright (c) 2017-2018, Christopher Jeffrey (MIT License). * https://github.com/handshake-org/hsd */ 'use strict'; const assert = require('bsert'); const EventEmitter = require('events'); const Heap = require('bheep'); const {BufferMap} = require('buffer-map'); const rng = require('bcrypto/lib/random'); const Amount = require('../ui/amount'); const Address = require('../primitives/address'); const BlockTemplate = require('./template'); const Network = require('../protocol/network'); const consensus = require('../protocol/consensus'); const policy = require('../protocol/policy'); const rules = require('../covenants/rules'); const CPUMiner = require('./cpuminer'); const pkg = require('../pkg'); const {BlockEntry, BlockClaim, BlockAirdrop} = BlockTemplate; /** * Miner * A handshake miner and block generator. * @alias module:mining.Miner * @extends EventEmitter */ class Miner extends EventEmitter { /** * Create a handshake miner. * @constructor * @param {Object} options */ 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.init(); } /** * Initialize the miner. */ init() { this.cpu.on('error', (err) => { this.emit('error', err); }); } /** * Open the miner, wait for the chain and mempool to load. * @returns {Promise} */ async 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} */ async 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}. */ async createBlock(tip, address) { const unlock = await this.locker.lock(); try { 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 root = this.chain.db.treeRoot(); const attempt = new BlockTemplate({ prevBlock: tip.hash, treeRoot: root, reservedRoot: consensus.ZERO_HASH, height: tip.height + 1, version: version, time: time, bits: target, mtp: mtp, flags: state.flags, address: address, coinbaseFlags: this.options.coinbaseFlags, interval: this.network.halvingInterval, weight: this.options.reservedWeight, sigops: this.options.reservedSigops }); this.assemble(attempt); this.logger.debug( 'Created block tmpl' + ' (height=%d, weight=%d, fees=%d, txs=%s, diff=%d, bits=%d).', attempt.height, attempt.weight, Amount.coin(attempt.fees), attempt.items.length + 1, attempt.getDifficulty(), target); if (this.options.preverify) { const block = attempt.toBlock(); 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; } this.logger.debug( 'Preverified block %d successfully!', attempt.height); } return attempt; } /** * Update block timestamp. * @param {BlockTemplate} attempt */ updateTime(attempt) { const pow = this.network.pow; attempt.time = Math.max(this.network.now(), attempt.mtp + 1); if (!pow.targetReset) return; const prev = this.chain.tip; if (!attempt.prevBlock.equals(prev.hash)) return; if (attempt.time > prev.time + pow.targetSpacing * 2) attempt.setBits(pow.bits); } /** * Create a cpu miner job. * @method * @param {ChainEntry?} tip * @param {Address?} address * @returns {Promise} Returns {@link CPUJob}. */ 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[rng.randomRange(0, this.addresses.length)]; } /** * 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.equals(this.chain.tip.hash), 'Mempool/chain tip mismatch! Unsafe to create block.'); const pq = new Heap(cmpRateClaim); for (const entry of this.mempool.claims.values()) { const item = BlockClaim.fromEntry(entry); pq.insert(item); } while (pq.size() > 0) { if (attempt.claims.length >= 10) break; const item = pq.shift(); const weight = item.getWeight(); if (attempt.weight + weight > this.options.maxWeight) continue; if (attempt.updates + 1 > this.options.maxUpdates) continue; if (item.commitHeight === 1) attempt.fees += item.fee; attempt.weight += weight; attempt.updates += 1; attempt.claims.push(item); } const pqa = new Heap(cmpRateAirdrop); for (const entry of this.mempool.airdrops.values()) { const item = BlockAirdrop.fromEntry(entry); pqa.insert(item); } while (pqa.size() > 0) { if (attempt.airdrops.length >= 10) break; const item = pqa.shift(); const weight = item.getWeight(); if (attempt.weight + weight > this.options.maxWeight) continue; if (attempt.updates + 1 > this.options.maxUpdates) continue; attempt.fees += item.fee; attempt.weight += weight; attempt.updates += 1; attempt.airdrops.push(item); } const depMap = new BufferMap(); 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; let opens = attempt.opens; let updates = attempt.updates; let renewals = attempt.renewals; if (!tx.isFinal(attempt.height, attempt.mtp)) continue; weight += tx.getWeight(); if (weight > this.options.maxWeight) continue; sigops += item.sigops; if (sigops > this.options.maxSigops) continue; opens += rules.countOpens(tx); if (opens > this.options.maxOpens) continue; updates += rules.countUpdates(tx); if (updates > this.options.maxUpdates) continue; renewals += rules.countRenewals(tx); if (renewals > this.options.maxRenewals) 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.opens = opens; attempt.updates = updates; attempt.renewals = renewals; 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!'); 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.'); } } } /** * Miner Options * @alias module:mining.MinerOptions */ class MinerOptions { /** * Create miner options. * @constructor * @param {Object} */ constructor(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 ${pkg.name}`, '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; this.maxOpens = consensus.MAX_BLOCK_OPENS; this.maxUpdates = consensus.MAX_BLOCK_UPDATES; this.maxRenewals = consensus.MAX_BLOCK_RENEWALS; this.reservedWeight = 4000; this.reservedSigops = 400; this.fromOptions(options); } /** * Inject properties from object. * @private * @param {Object} options * @returns {MinerOptions} */ fromOptions(options) { 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; this.workers = options.chain.workers; 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, 'Max sigops must be below MAX_BLOCK_SIGOPS'); 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; } if (options.maxOpens != null) { assert((options.maxOpens >>> 0) === options.maxOpens); assert(options.maxOpens <= consensus.MAX_BLOCK_OPENS, 'Max opens must be below MAX_BLOCK_OPENS'); this.maxOpens = options.maxOpens; } if (options.maxUpdates != null) { assert((options.maxUpdates >>> 0) === options.maxUpdates); assert(options.maxUpdates <= consensus.MAX_BLOCK_UPDATES, 'Max updates must be below MAX_BLOCK_UPDATES'); this.maxUpdates = options.maxUpdates; } if (options.maxRenewals != null) { assert((options.maxRenewals >>> 0) === options.maxRenewals); assert(options.maxRenewals <= consensus.MAX_BLOCK_RENEWALS, 'Max renewals must be below MAX_BLOCK_RENEWALS'); this.maxRenewals = options.maxRenewals; } return this; } /** * Instantiate miner options from object. * @param {Object} options * @returns {MinerOptions} */ static fromOptions(options) { return new this().fromOptions(options); } } /* * Helpers */ function cmpPriority(a, b) { if (a.priority === b.priority) return cmpRate(a, b); return b.priority - a.priority; } function cmpRate(a, b) { let x = a.rate; let y = b.rate; if (a.descRate > a.rate) x = a.descRate; if (b.descRate > b.rate) y = b.descRate; if (x === y) { x = a.priority; y = b.priority; } return y - x; } function cmpRateClaim(a, b) { const x = a.rate; const y = b.rate; return y - x; } function cmpRateAirdrop(a, b) { const x = a.rate; const y = b.rate; return y - x; } /* * Expose */ module.exports = Miner;