miner: classify.

This commit is contained in:
Christopher Jeffrey 2017-11-16 19:10:26 -08:00
parent 5c8a755d63
commit 8be995bd78
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
3 changed files with 968 additions and 957 deletions

View File

@ -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);
}
/**

File diff suppressed because it is too large Load Diff

View File

@ -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