639 lines
14 KiB
JavaScript
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;
|