fcoin/lib/mining/minerblock.js
2017-02-28 09:32:17 -08:00

639 lines
14 KiB
JavaScript

/*!
* minerblock.js - miner block object for bcoin (because we can)
* Copyright (c) 2014-2015, Fedor Indutny (MIT License)
* Copyright (c) 2014-2017, Christopher Jeffrey (MIT License).
* https://github.com/bcoin-org/bcoin
*/
'use strict';
var assert = require('assert');
var EventEmitter = require('events').EventEmitter;
var BN = require('bn.js');
var util = require('../utils/util');
var co = require('../utils/co');
var StaticWriter = require('../utils/staticwriter');
var Network = require('../protocol/network');
var TX = require('../primitives/tx');
var Block = require('../primitives/block');
var Input = require('../primitives/input');
var Output = require('../primitives/output');
var mine = require('./mine');
var workerPool = require('../workers/workerpool').pool;
var consensus = require('../protocol/consensus');
var policy = require('../protocol/policy');
var encoding = require('../utils/encoding');
/**
* MinerBlock (block attempt)
* @alias module:mining.MinerBlock
* @constructor
* @param {Object} options
* @param {ChainEntry} options.tip
* @param {Number} options.height
* @param {Number} options.target - Compact form.
* @param {Base58Address} options.address - Payout address.
* @param {Boolean} options.witness - Allow witness
* transactions, mine a witness block.
* @param {String} options.coinbaseFlags
* @property {Block} block
* @property {TX} coinbase
* @property {BN} hashes - Number of hashes attempted.
* @property {Number} rate - Hash rate.
* @emits MinerBlock#status
*/
function MinerBlock(options) {
if (!(this instanceof MinerBlock))
return new MinerBlock(options);
EventEmitter.call(this);
this.network = Network.get(options.network);
this.tip = options.tip;
this.version = options.version;
this.height = options.tip.height + 1;
this.ts = options.ts;
this.bits = options.bits;
this.target = consensus.fromCompact(this.bits).toArrayLike(Buffer, 'le', 32);
this.locktime = options.locktime;
this.flags = options.flags;
this.nonce1 = 0;
this.nonce2 = 0;
this.iterations = 0;
this.coinbaseFlags = options.coinbaseFlags;
this.witness = options.witness;
this.address = options.address;
this.reward = consensus.getReward(this.height, this.network.halvingInterval);
this.destroyed = false;
this.committed = false;
this.sigops = options.sigops;
this.weight = options.weight;
this.fees = 0;
this.items = [];
this.coinbase = new TX();
this.coinbase.mutable = true;
this.block = new Block();
this.block.mutable = true;
this._init();
}
util.inherits(MinerBlock, EventEmitter);
/**
* Nonce range interval.
* @const {Number}
* @default
*/
MinerBlock.INTERVAL = 0xffffffff / 1500 | 0;
/**
* Calculate number of hashes.
* @returns {Number}
*/
MinerBlock.prototype.getHashes = function() {
return this.iterations * 0xffffffff + this.block.nonce;
};
/**
* Calculate hashrate.
* @returns {Number}
*/
MinerBlock.prototype.getRate = function() {
return (this.block.nonce / (util.now() - this.begin)) | 0;
};
/**
* Initialize the block.
* @private
*/
MinerBlock.prototype._init = function _init() {
var scale = consensus.WITNESS_SCALE_FACTOR;
var hash = encoding.ZERO_HASH;
var block = this.block;
var cb = this.coinbase;
var weight = 0;
var sigops = 0;
var input, output, commit, padding;
assert(this.coinbaseFlags.length <= 20);
// Setup our block.
block.version = this.version;
block.prevBlock = this.tip.hash;
block.merkleRoot = encoding.NULL_HASH;
block.ts = this.ts;
block.bits = this.bits;
block.nonce = 0;
// Coinbase input.
input = new Input();
// Height (required in v2+ blocks)
input.script.set(0, new BN(this.height));
// Let the world know this little
// miner succeeded.
input.script.set(1, encoding.ZERO_HASH160);
// Smaller nonce for good measure.
input.script.set(2, util.nonce().slice(0, 4));
// extraNonce - incremented when
// the nonce overflows.
input.script.set(3, this.extraNonce());
input.script.compile();
// Set up the witness nonce.
if (this.witness) {
input.witness.set(0, block.createWitnessNonce());
input.witness.compile();
}
cb.inputs.push(input);
// Reward output.
output = new Output();
output.script.fromPubkeyhash(encoding.ZERO_HASH160);
cb.outputs.push(output);
// If we're using segwit, we
// need to set up the commitment.
if (this.witness) {
// Commitment output.
commit = new Output();
commit.script.fromCommitment(hash);
cb.outputs.push(commit);
}
block.txs.push(cb);
// Initialize weight.
weight = block.getWeight();
// 4 extra bytes for varint tx count.
weight += 4 * scale;
// Padding for the CB height (constant size).
padding = 5 - input.script.code[0].getSize();
assert(padding >= 0);
weight += padding * scale;
// Reserved size.
// Without segwit:
// Block weight = 840
// Block stripped size = 210
// Block size = 210
// CB weight = 500
// CB stripped size = 125
// CB size = 125
// Sigops cost = 4
// With segwit:
// Block weight = 1064
// Block stripped size = 257
// Block size = 293
// CB weight = 724
// CB stripped size = 172
// CB size = 208
// Sigops cost = 4
if (!this.witness) {
assert.equal(weight, 840);
assert.equal(block.getBaseSize() + 4 + padding, 210);
assert.equal(block.getSize() + 4 + padding, 210);
assert.equal(cb.getWeight() + padding * scale, 500);
assert.equal(cb.getBaseSize() + padding, 125);
assert.equal(cb.getSize() + padding, 125);
} else {
assert.equal(weight, 1064);
assert.equal(block.getBaseSize() + 4 + padding, 257);
assert.equal(block.getSize() + 4 + padding, 293);
assert.equal(cb.getWeight() + padding * scale, 724);
assert.equal(cb.getBaseSize() + padding, 172);
assert.equal(cb.getSize() + padding, 208);
}
// Initialize sigops weight.
sigops = 4;
// Setup coinbase flags (variable size).
input.script.set(1, this.coinbaseFlags);
input.script.compile();
// Setup output script (variable size).
output.script.clear();
output.script.fromAddress(this.address);
// Update commitments.
this.refresh();
// Ensure the variable size
// stuff didn't break anything.
assert(block.getWeight() <= weight,
'Coinbase exceeds reserved size!');
assert(cb.getSigopsCost(null, this.flags) <= sigops,
'Coinbase exceeds reserved sigops!');
assert(this.weight >= weight,
'Coinbase exceeds reserved size!');
assert(this.sigops >= sigops,
'Coinbase exceeds reserved sigops!');
};
/**
* Update coinbase, witness
* commitment, and merkle root.
*/
MinerBlock.prototype.refresh = function refresh() {
// Update coinbase.
this.updateCoinbase();
// Witness commitment.
if (this.witness)
this.updateCommitment();
// Create our merkle root.
this.updateMerkle();
};
/**
* Update the commitment output for segwit.
*/
MinerBlock.prototype.updateCommitment = function updateCommitment() {
var output = this.coinbase.outputs[1];
var hash;
// Recalculate witness merkle root.
hash = this.block.createCommitmentHash();
// Update commitment.
output.script.clear();
output.script.fromCommitment(hash);
};
/**
* Update the extra nonce and coinbase reward.
*/
MinerBlock.prototype.updateCoinbase = function updateCoinbase() {
var input = this.coinbase.inputs[0];
var output = this.coinbase.outputs[0];
// Update extra nonce.
input.script.set(3, this.extraNonce());
input.script.compile();
// Update reward.
output.value = this.reward + this.fees;
};
/**
* Increment the extraNonce.
*/
MinerBlock.prototype.updateNonce = function updateNonce() {
// Overflow the nonce and increment the extraNonce.
this.block.nonce = 0;
this.nonce1++;
// Wrap at 4 bytes.
if (this.nonce1 === 0xffffffff) {
this.nonce1 = 0;
this.nonce2++;
}
// We incremented the extraNonce, need to update coinbase.
this.updateCoinbase();
// We changed the coinbase, need to update merkleRoot.
this.updateMerkle();
};
/**
* Rebuild the merkle tree and update merkle root.
*/
MinerBlock.prototype.updateMerkle = function updateMerkle() {
// Recalculate merkle root.
this.block.merkleRoot = this.block.createMerkleRoot('hex');
};
/**
* Render extraNonce.
* @returns {Buffer}
*/
MinerBlock.prototype.extraNonce = function extraNonce() {
var bw = new StaticWriter(8);
bw.writeU32BE(this.nonce1);
bw.writeU32BE(this.nonce2);
return bw.render();
};
/**
* Add a transaction to the block. Rebuilds the merkle tree,
* updates coinbase and commitment.
* @param {TX} tx
* @returns {Boolean} Whether the transaction was successfully added.
*/
MinerBlock.prototype.addTX = function addTX(tx, view) {
var hash = tx.hash('hex');
var item, weight, sigops;
assert(!tx.mutable, 'Cannot add mutable TX to block.');
if (this.block.hasTX(hash))
return false;
item = BlockEntry.fromTX(tx, view, this);
weight = item.tx.getWeight();
sigops = item.sigops;
if (!tx.isFinal(this.height, this.locktime))
return false;
if (this.weight + weight > consensus.MAX_BLOCK_WEIGHT)
return false;
if (this.sigops + sigops > consensus.MAX_BLOCK_SIGOPS_COST)
return false;
if (!this.witness && tx.hasWitness())
return false;
this.weight += weight;
this.sigops += sigops;
this.fees += item.fee;
// Add the tx to our block
this.block.txs.push(tx);
this.items.push(item);
// Update commitments.
this.refresh();
return true;
};
/**
* Hash until the nonce overflows.
* @returns {Boolean} Whether the nonce was found.
*/
MinerBlock.prototype.findNonce = function findNonce() {
var block = this.block;
var target = this.target;
var data = block.abbr();
var interval = MinerBlock.INTERVAL;
var min = 0;
var max = interval;
var nonce;
while (max <= 0xffffffff) {
nonce = mine(data, target, min, max);
if (nonce !== -1)
break;
block.nonce = max;
min += interval;
max += interval;
this.sendStatus();
}
return nonce;
};
/**
* Hash until the nonce overflows.
* @method
* @returns {Promise} - Returns Boolean.
*/
MinerBlock.prototype.findNonceAsync = co(function* findNonceAsync() {
var block = this.block;
var target = this.target;
var interval = MinerBlock.INTERVAL;
var min = 0;
var max = interval;
var data, nonce;
while (max <= 0xffffffff) {
data = block.abbr();
nonce = yield workerPool.mine(data, target, min, max);
if (nonce !== -1)
break;
if (this.destroyed)
return nonce;
block.nonce = max;
min += interval;
max += interval;
this.sendStatus();
}
return nonce;
});
/**
* Mine synchronously until the block is found.
* @returns {Block}
*/
MinerBlock.prototype.mine = function mine() {
var nonce;
// Track how long we've been at it.
this.begin = util.now();
for (;;) {
nonce = this.findNonce();
if (nonce !== -1)
break;
this.iterate();
}
this.commit(nonce);
return this.block;
};
/**
* Mine asynchronously until the block is found.
* @method
* @returns {Promise} - Returns {@link Block}.
*/
MinerBlock.prototype.mineAsync = co(function* mineAsync() {
var nonce;
// Track how long we've been at it.
this.begin = util.now();
for (;;) {
nonce = yield this.findNonceAsync();
if (nonce !== -1)
break;
if (this.destroyed)
return;
this.iterate();
}
this.commit(nonce);
return this.block;
});
/**
* Increment extraNonce, rebuild merkletree.
*/
MinerBlock.prototype.iterate = function iterate() {
// Keep track of our iterations.
this.iterations++;
// Send progress report.
this.sendStatus();
// Overflow the nonce and increment the extraNonce.
this.updateNonce();
};
/**
* Commit and finalize mined block.
* @returns {Block}
*/
MinerBlock.prototype.commit = function commit(nonce) {
assert(!this.committed, 'Block is already committed.');
this.committed = true;
this.block.nonce = nonce;
this.block.mutable = false;
this.coinbase.mutable = false;
return this.block;
};
/**
* Send a progress report (emits `status`).
*/
MinerBlock.prototype.sendStatus = function sendStatus() {
this.emit('status', {
block: this.block,
target: this.block.bits,
hashes: this.getHashes(),
hashrate: this.getRate(),
height: this.height,
best: util.revHex(this.tip.hash)
});
};
/**
* Destroy the minerblock. Stop mining.
*/
MinerBlock.prototype.destroy = function destroy() {
this.destroyed = true;
};
/**
* BlockEntry
* @alias module:mining.BlockEntry
* @constructor
* @param {TX} tx
* @property {TX} tx
* @property {Hash} hash
* @property {Amount} fee
* @property {Rate} rate
* @property {Number} priority
* @property {Boolean} free
* @property {Sigops} sigops
* @property {Number} depCount
*/
function BlockEntry(tx) {
this.tx = tx;
this.hash = tx.hash('hex');
this.fee = 0;
this.rate = 0;
this.priority = 0;
this.free = false;
this.sigops = 0;
this.descRate = 0;
this.depCount = 0;
}
/**
* Instantiate block entry from transaction.
* @param {TX} tx
* @param {CoinView} view
* @param {MinerBlock} attempt
* @returns {BlockEntry}
*/
BlockEntry.fromTX = function fromTX(tx, view, attempt) {
var item = new BlockEntry(tx);
item.fee = tx.getFee(view);
item.rate = tx.getRate(view);
item.priority = tx.getPriority(view, attempt.height);
item.free = false;
item.sigops = tx.getSigopsCost(view, attempt.flags);
item.descRate = item.rate;
return item;
};
/**
* Instantiate block entry from mempool entry.
* @param {MempoolEntry} entry
* @param {MinerBlock} attempt
* @returns {BlockEntry}
*/
BlockEntry.fromEntry = function fromEntry(entry, attempt) {
var item = new BlockEntry(entry.tx);
item.fee = entry.getFee();
item.rate = entry.getRate();
item.priority = entry.getPriority(attempt.height);
item.free = item.fee < policy.getMinFee(entry.size);
item.sigops = entry.sigops;
item.descRate = entry.getDescRate();
return item;
};
/*
* Expose
*/
exports = MinerBlock;
exports.MinerBlock = MinerBlock;
exports.BlockEntry = BlockEntry;
module.exports = exports;