diff --git a/README.md b/README.md index 2493de40..aba11db9 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,37 @@ $ node bin/bcoin-cli wallet primary --passphrase=node $ node bin/bcoin-cli mempool ``` +### Creating a blockchain and mempool + +``` js +var bcoin = require('bcoin'); +bcoin.protocol.network.set('regtest'); +var chain = new bcoin.chain({ db: 'memory' }); +var mempool = new bcoin.mempool({ chain: chain, db: 'memory' }); +var miner = new bcoin.miner({ chain: chain, mempool: mempool }); + +// Create a block "attempt" +miner.createBlock(function(err, attempt) { + if (err) + throw err; + + // Mine the block on the worker pool (use mine() for the master process) + attempt.mineAsync(function(err, block) { + if (err) + throw err; + + // Add the block to the chain + chain.add(block, function(err) { + if (err) + throw err; + + console.log('Added %s to the blockchain.', block.rhash); + console.log(block); + }); + }); +}); +``` + ### TX creation TODO diff --git a/lib/bcoin/block.js b/lib/bcoin/block.js index 0a8e74bf..9e5ea0ee 100644 --- a/lib/bcoin/block.js +++ b/lib/bcoin/block.js @@ -276,8 +276,6 @@ Block.prototype.getReward = function getReward() { var reward = Block.reward(this.height); var i; - assert(this.height !== -1); - for (i = 1; i < this.txs.length; i++) reward.iadd(this.txs[i].getFee()); diff --git a/lib/bcoin/miner.js b/lib/bcoin/miner.js index f85fe4a9..5b6aa2fe 100644 --- a/lib/bcoin/miner.js +++ b/lib/bcoin/miner.js @@ -44,8 +44,6 @@ function Miner(options) { this.timeout = null; this.block = null; - this.iterations = 0; - this._begin = utils.now(); this._init(); } @@ -68,13 +66,13 @@ Miner.prototype._init = function _init() { this.mempool.on('tx', function(tx) { if (!self.running) return; - self.addTX(tx); + self.attempt.addTX(tx); }); } else if (this.pool) { this.pool.on('tx', function(tx) { if (!self.running) return; - self.addTX(tx); + self.attempt.addTX(tx); }); } @@ -122,41 +120,40 @@ Miner.prototype.start = function start() { this.running = true; - // Reset iterations - this.iterations = 0; - // Create a new block and start hashing - this.createBlock(function(err, block) { + this.createBlock(function(err, attempt) { if (err) return self.emit('error', err); - self.block = block; + if (!self.running) + return; - if (!self.mempool) - return self.iterate(); + self.attempt = attempt; - self.mempool.getSnapshot(function(err, hashes) { - if (err) - return self.emit('error', err); + attempt.on('status', function(status) { + self.emit('status', status); + }); - utils.forEachSerial(hashes, function(hash, next) { - self.mempool.getTX(hash, function(err, tx) { - if (err) - return next(err); + attempt.mineAsync(function(err, block) { + if (err) { + self.emit('error', err); + return self.start(); + } - self.mempool.fillAllCoins(tx, function(err) { - if (err) - return next(err); + // Add our block to the chain + self.chain.add(block, function(err) { + if (err) { + if (err.type === 'VerifyError') + utils.debug('%s could not be added to chain.', block.rhash); + self.emit('error', err); + return self.start(); + } - self.addTX(tx); - next(); - }); - }); - }, function(err) { - if (err) - return self.emit('error', err); + // Emit our newly found block + self.emit('block', block); - self.iterate(); + // `tip` will now be emitted by chain + // and the whole process starts over. }); }); }); @@ -168,11 +165,185 @@ Miner.prototype.stop = function stop() { this.running = false; - clearTimeout(this.timeout); - this.timeout = null; + if (this.attempt) { + this.attempt.destroy(); + this.attempt = null; + } }; -Miner.prototype.addTX = function addTX(tx) { +Miner.prototype.createBlock = function createBlock(callback) { + var self = this; + var ts = Math.max(utils.now(), this.chain.tip.ts + 1); + var attempt; + + // Find target + this.chain.getTargetAsync(this.chain.tip, ts, function(err, target) { + if (err) + return callback(err); + + // Calculate version with versionbits + self.chain.computeBlockVersion(self.chain.tip, function(err, version) { + if (err) + return callback(err); + + attempt = new MinerBlock({ + tip: self.chain.tip, + version: version, + target: target, + address: self.address, + coinbaseFlags: self.coinbaseFlags, + segwit: self.chain.segwitActive, + dsha256: self.dsha256 + }); + + if (!self.mempool) + return callback(null, attempt); + + self.mempool.getSnapshot(function(err, hashes) { + if (err) + return callback(err); + + utils.forEachSerial(hashes, function(hash, next) { + self.mempool.getTX(hash, function(err, tx) { + if (err) + return next(err); + + self.mempool.fillAllCoins(tx, function(err) { + if (err) + return next(err); + + attempt.addTX(tx); + + next(); + }); + }); + }, function(err) { + if (err) + return callback(err); + + return callback(null, attempt); + }); + }); + }); + }); +}; + +Miner.prototype.mineBlock = function mineBlock(callback) { + var self = this; + + // Create a new block and start hashing + this.createBlock(function(err, attempt) { + if (err) + return callback(err); + + attempt.mineAsync(callback); + }); +}; + +/** + * MinerBlock + */ + +function MinerBlock(options) { + if (!(this instanceof MinerBlock)) + return new MinerBlock(options); + + this.options = options; + this.tip = options.tip; + this.height = options.tip.height + 1; + this.target = utils.fromCompact(options.target).toBuffer('le', 32); + this.extraNonce = new bn(0); + this.iterations = 0; + this.dsha256 = options.dsha256; + + // Create a coinbase + this.coinbase = new bcoin.mtx(); + + this.coinbase.addInput({ + prevout: { + hash: utils.toHex(constants.zeroHash), + index: 0xffffffff + }, + coin: null, + script: new bcoin.script([ + // Height (required in v2+ blocks) + bcoin.script.array(this.height), + // extraNonce - incremented when + // the nonce overflows. + bcoin.script.array(0), + // Add a nonce to ensure we don't + // collide with a previous coinbase + // of ours. This isn't really + // necessary nowdays due to bip34 + // (used above). + bcoin.script.array(utils.nonce()), + // Let the world know this little + // miner succeeded. + new Buffer(options.coinbaseFlags, 'ascii') + ]), + witness: new bcoin.script.witness([]), + sequence: 0xffffffff + }); + + this.coinbase.addOutput({ + address: options.address, + value: new bn(0) + }); + + // Create our block + this.block = new bcoin.block({ + version: options.version, + prevBlock: this.tip.hash, + merkleRoot: constants.zeroHash, + ts: Math.max(utils.now(), this.tip.ts + 1), + bits: options.target, + nonce: 0 + }); + + this.block.txs.push(this.coinbase); + + if (options.segwit) { + // Set up the witness nonce and + // commitment output for segwit. + this.witness = true; + this.witnessNonce = utils.dsha256(new Buffer(this.tip.hash, 'hex')); + this.coinbase.inputs[0].witness.items[0] = this.witnessNonce; + this.coinbase.addOutput({ + script: new bcoin.script([]), + value: new bn(0) + }); + } + + // Update coinbase since our coinbase was added. + this.updateCoinbase(); + + // Create our merkle root. + this.updateMerkle(); +} + +utils.inherits(MinerBlock, EventEmitter); + +MinerBlock.prototype.updateCommitment = function updateCommitment() { + var hash = this.block.getCommitmentHash(); + this.coinbase.outputs[1].script = bcoin.script.createCommitment(hash); +}; + +MinerBlock.prototype.updateCoinbase = function updateCoinbase() { + this.coinbase.inputs[0].script[1] = this.extraNonce.toBuffer(); + this.coinbase.outputs[0].value = this.block.getReward(); +}; + +MinerBlock.prototype.updateMerkle = function updateMerkle() { + // Always update commitment before updating merkle root. + // The updated commitment output will change the merkle root. + if (this.witness) + this.updateCommitment(); + + this.block.ts = Math.max(utils.now(), this.tip.ts + 1); + this.block.merkleRoot = this.block.getMerkleRoot('hex'); +}; + +MinerBlock.prototype.addTX = function addTX(tx) { var size = this.block.getVirtualSize() + tx.getVirtualSize(); // Deliver me from the block size debate, please @@ -197,206 +368,33 @@ Miner.prototype.addTX = function addTX(tx) { return true; }; -Miner.prototype.createBlock = function createBlock(callback) { - var self = this; - var ts = Math.max(utils.now(), this.chain.tip.ts + 1); - var coinbase, headers, block; - - // Find target - this.chain.getTargetAsync(this.chain.tip, ts, function(err, target) { - if (err) - return callback(err); - - // Calculate version with versionbits - self.chain.computeBlockVersion(self.chain.tip, function(err, version) { - if (err) - return callback(err); - - // Create a coinbase - coinbase = bcoin.mtx(); - - coinbase.addInput({ - prevout: { - hash: utils.toHex(constants.zeroHash), - index: 0xffffffff - }, - coin: null, - script: new bcoin.script([ - // Height (required in v2+ blocks) - bcoin.script.array(self.chain.height + 1), - // extraNonce - incremented when - // the nonce overflows. - bcoin.script.array(0), - // Add a nonce to ensure we don't - // collide with a previous coinbase - // of ours. This isn't really - // necessary nowdays due to bip34 - // (used above). - bcoin.script.array(utils.nonce()), - // Let the world know this little - // miner succeeded. - new Buffer(self.coinbaseFlags, 'ascii') - ]), - witness: new bcoin.script.witness([]), - sequence: 0xffffffff - }); - - coinbase.addOutput({ - address: self.address, - value: new bn(0) - }); - - // Create our block - headers = { - version: version, - prevBlock: self.chain.tip.hash, - merkleRoot: constants.zeroHash, - ts: ts, - bits: target, - nonce: 0 - }; - - block = bcoin.block(headers); - - block.txs.push(coinbase); - - block.height = self.chain.height + 1; - block.target = utils.fromCompact(target).toBuffer('le', 32); - block.extraNonce = new bn(0); - - if (self.chain.segwitActive) { - // Set up the witness nonce and - // commitment output for segwit. - block.witness = true; - block.witnessNonce = utils.nonce().toBuffer('le', 8); - coinbase.inputs[0].witness.items[0] = block.witnessNonce; - coinbase.addOutput({ - script: new bcoin.script([]), - value: new bn(0) - }); - } - - // Update coinbase since our coinbase was added. - self.updateCoinbase(block); - - // Create our merkle root. - self.updateMerkle(block); - - return callback(null, block); - }); - }); -}; - -Miner.prototype.updateCommitment = function updateCommitment(block) { - var coinbase = block.txs[0]; - var hash; - - assert(coinbase); - - if (!block) - block = this.block; - - hash = block.getCommitmentHash(); - coinbase.outputs[1].script = bcoin.script.createCommitment(hash); -}; - -Miner.prototype.updateCoinbase = function updateCoinbase(block) { - var coinbase = block.txs[0]; - - assert(coinbase); - - if (!block) - block = this.block; - - coinbase.inputs[0].script[1] = block.extraNonce.toBuffer(); - coinbase.outputs[0].value = block.getReward(); -}; - -Miner.prototype.updateMerkle = function updateMerkle(block) { - if (!block) - block = this.block; - - // Always update commitment before updating merkle root. - // The updated commitment output will change the merkle root. - if (block.witness) - this.updateCommitment(block); - - block.ts = Math.max(utils.now(), this.chain.tip.ts + 1); - block.merkleRoot = block.getMerkleRoot('hex'); -}; - -Miner.prototype.iterate = function iterate() { - var self = this; - - this.timeout = setTimeout(function() { - // Try to find a block: do one iteration of extraNonce - if (!self.findNonce()) - return self.iterate(); - - // Add our block to the chain - self.chain.add(self.block, function(err) { - if (err) { - if (err.type === 'VerifyError') - utils.debug('%s could not be added to chain.', self.block.rhash); - self.emit('error', err); - return self.start(); - } - - // Emit our newly found block - self.emit('block', self.block); - - // `tip` will now be emitted by chain - // and the whole process starts over. - }); - }, 100); -}; - -Miner.prototype.__defineGetter__('hashes', function() { - return new bn(this.iterations).mul(utils.U32).addn(this.block.nonce); -}); - -Miner.prototype.__defineGetter__('rate', function() { - if (!this.block.nonce) - return 0; - // Calculate our terrible hashrate - return (this.block.nonce / (utils.now() - this._begin)) * 2 | 0; -}); - -Miner.prototype.sendStatus = function sendStatus() { - this.emit('status', { - block: this.block, - target: this.block.bits, - hashes: this.hashes.toString(10), - hashrate: this.rate, - height: this.chain.height + 1, - best: utils.revHex(this.chain.tip.hash) - }); -}; - -Miner.prototype.findNonce = function findNonce() { - var data = this.block.abbr(); +MinerBlock.prototype.findNonce = function findNonce() { + var tip = this.tip; + var block = this.block; + var target = this.target; + var data = block.abbr(); var now; // Track how long we've been at it. - this._begin = utils.now(); + this.begin = utils.now(); - assert(this.block.ts > this.chain.tip.ts); + assert(block.ts > tip.ts); // The heart and soul of the miner: match the target. - while (this.block.nonce <= 0xffffffff) { + while (block.nonce <= 0xffffffff) { // Hash and test against the next target - if (rcmp(this.dsha256(data), this.block.target) < 0) + if (rcmp(this.dsha256(data), target) < 0) return true; // Increment the nonce to get a different hash - this.block.nonce++; + block.nonce++; // Update the raw buffer (faster than // constantly serializing the block) - utils.writeU32(data, this.block.nonce, 76); + utils.writeU32(data, block.nonce, 76); // Send progress report every so often - if (this.block.nonce % 100000 === 0) + if (block.nonce % 100000 === 0) this.sendStatus(); } @@ -412,16 +410,16 @@ Miner.prototype.findNonce = function findNonce() { // performance because we do not have to // recalculate the merkle root. now = utils.now(); - if (now > this.block.ts && now > this.chain.tip.ts) { - this.block.ts = now; + if (now > block.ts && now > tip.ts) { + block.ts = now; // Overflow the nonce - this.block.nonce = 0; + block.nonce = 0; return false; } // Overflow the nonce and increment the extraNonce. - this.block.nonce = 0; - this.block.extraNonce.iaddn(1); + block.nonce = 0; + block.extraNonce.iaddn(1); // We incremented the extraNonce, need to update coinbase. this.updateCoinbase(); @@ -432,6 +430,61 @@ Miner.prototype.findNonce = function findNonce() { return false; }; +MinerBlock.prototype.__defineGetter__('hashes', function() { + return new bn(this.iterations) + .mul(utils.U32) + .addn(this.block.nonce); +}); + +MinerBlock.prototype.__defineGetter__('rate', function() { + if (!this.block.nonce) + return 0; + // Calculate our terrible hashrate + return (this.block.nonce / (utils.now() - this.begin)) * 2 | 0; +}); + +MinerBlock.prototype.sendStatus = function sendStatus() { + this.emit('status', { + block: this.block, + target: this.block.bits, + hashes: this.hashes.toString(10), + hashrate: this.rate, + height: this.height, + best: utils.revHex(this.tip.hash) + }); +}; + +MinerBlock.prototype.mine = function mine(callback) { + var self = this; + + this.timeout = setTimeout(function() { + // Try to find a block: do one iteration of extraNonce + if (!self.findNonce()) + return self.mine(callback); + + return callback(null, self.block); + }, 100); +}; + +MinerBlock.prototype.mineSync = function mineSync() { + while (!this.findNonce()); + return this.block; +}; + +MinerBlock.prototype.mineAsync = function mine(callback) { + return this.mine(callback); +}; + +MinerBlock.prototype.destroy = function destroy() { + clearTimeout(this.timeout); + this.timeout = null; + this.block = null; +}; + +/** + * Helpers + */ + function rcmp(a, b) { var i; @@ -451,4 +504,6 @@ function rcmp(a, b) { * Expose */ -module.exports = Miner; +exports = Miner; +exports.minerblock = MinerBlock; +module.exports = exports; diff --git a/lib/bcoin/workers.js b/lib/bcoin/workers.js index 3c9b4791..4ed3ac50 100644 --- a/lib/bcoin/workers.js +++ b/lib/bcoin/workers.js @@ -81,17 +81,21 @@ workers.call = function call(method, args, callback) { child = workers.alloc(id); function listener(err, result) { - clearTimeout(timeout); - timeout = null; + if (timeout) { + clearTimeout(timeout); + timeout = null; + } callback(err, result); } child.once(event, listener); - timeout = setTimeout(function() { - child.removeListener(event, listener); - return callback(new Error('Worker timed out.')); - }, workers.TIMEOUT); + if (method !== 'mine') { + timeout = setTimeout(function() { + child.removeListener(event, listener); + return callback(new Error('Worker timed out.')); + }, workers.TIMEOUT); + } child.stdin.write(createPacket(id, method, args)); }; @@ -115,13 +119,25 @@ bcoin.tx.prototype.verifyAsync = function verifyAsync(index, force, flags, callb return workers.call('verify', [this, index, force, flags], callback); }; +bcoin.miner.minerblock.prototype.mineAsync = function mineAsync(callback) { + var attempt = { + tip: this.tip.toRaw(), + version: this.block.version, + target: this.block.bits, + address: this.options.address, + coinbaseFlags: this.options.coinbaseFlags, + segwit: this.options.segwit + }; + return workers.call('mine', [attempt], callback); +}; + /** * Child */ workers.listen = function listen() { bcoin.debug = function debug() { - process.stderr.write('Worker ' + process.env.BCOIN_WORKER_ID + ':'); + process.stderr.write('Worker ' + process.env.BCOIN_WORKER_ID + ': '); return console.error.apply(console.error, arguments); }; @@ -155,6 +171,28 @@ workers.verify = function verify(tx, index, force, flags) { return tx.verify(index, force, flags); }; +workers.mine = function mine(attempt) { + attempt = new bcoin.miner.minerblock({ + tip: bcoin.chainblock.fromRaw(null, attempt.tip), + version: attempt.version, + target: attempt.target, + address: attempt.address, + coinbaseFlags: attempt.coinbaseFlags, + segwit: attempt.segwit, + dsha256: utils.dsha256 + }); + attempt.on('status', function(stat) { + utils.debug( + 'hashrate=%dkhs hashes=%d target=%d height=%d best=%s', + stat.hashrate / 1000 | 0, + stat.hashes, + stat.target, + stat.height, + stat.best); + }); + return attempt.mineSync(); +}; + /** * Helpers */