diff --git a/lib/bcoin/chain.js b/lib/bcoin/chain.js index 9cc4be66..3e6f9fdc 100644 --- a/lib/bcoin/chain.js +++ b/lib/bcoin/chain.js @@ -753,9 +753,6 @@ Chain.prototype._checkInputs = function _checkInputs(block, prev, flags, callbac if (err) return callback(err); - if (block.getClaimed().cmp(block.getReward()) > 0) - return callback(new VerifyError(block, 'invalid', 'bad-cb-amount', 100)); - // Check all transactions for (i = 0; i < block.txs.length; i++) { tx = block.txs[i]; @@ -780,13 +777,6 @@ Chain.prototype._checkInputs = function _checkInputs(block, prev, flags, callbac if (tx.isCoinbase()) continue; - if (!tx.checkInputs(height, ret)) { - return callback(new VerifyError(block, - 'invalid', - ret.reason, - ret.score)); - } - for (j = 0; j < tx.inputs.length; j++) { input = tx.inputs[j]; @@ -814,8 +804,18 @@ Chain.prototype._checkInputs = function _checkInputs(block, prev, flags, callbac 100)); } } + + if (!tx.checkInputs(height, ret)) { + return callback(new VerifyError(block, + 'invalid', + ret.reason, + ret.score)); + } } + if (block.getClaimed().cmp(block.getReward()) > 0) + return callback(new VerifyError(block, 'invalid', 'bad-cb-amount', 100)); + if (self.options.verifySync === true) return callback(); @@ -915,8 +915,9 @@ Chain.prototype._findFork = function _findFork(fork, longer, callback) { Chain.prototype._reorganize = function _reorganize(entry, block, callback) { var self = this; + var tip = this.tip; - return this._findFork(this.tip, entry, function(err, fork) { + return this._findFork(tip, entry, function(err, fork) { if (err) return callback(err); @@ -926,6 +927,8 @@ Chain.prototype._reorganize = function _reorganize(entry, block, callback) { function disconnect(callback) { var entries = []; + entries.push(tip); + (function collect(entry) { entry.getPrevious(function(err, entry) { if (err) @@ -933,20 +936,20 @@ Chain.prototype._reorganize = function _reorganize(entry, block, callback) { assert(entry); - entries.push(entry); - if (entry.hash === fork.hash) return finish(); + entries.push(entry); + collect(entry); }); - })(self.tip); + })(tip); function finish() { assert(entries.length > 0); utils.forEachSerial(entries, function(entry, next) { - self.db.disconnect(entry, next); + self.disconnect(entry, next); }, callback); } } @@ -962,11 +965,11 @@ Chain.prototype._reorganize = function _reorganize(entry, block, callback) { assert(entry); - entries.push(entry); - if (entry.hash === fork.hash) return finish(); + entries.push(entry); + collect(entry); }); })(entry); @@ -976,7 +979,7 @@ Chain.prototype._reorganize = function _reorganize(entry, block, callback) { assert(entries.length > 0); utils.forEachSerial(entries, function(entry, next) { - self.db.connect(entry, next); + self.connect(entry, next); }, callback); } } @@ -991,7 +994,7 @@ Chain.prototype._reorganize = function _reorganize(entry, block, callback) { self.emit('fork', block, { height: fork.height, - expected: self.tip.hash, + expected: tip.hash, received: entry.hash, checkpoint: false }); @@ -1002,6 +1005,85 @@ Chain.prototype._reorganize = function _reorganize(entry, block, callback) { }); }; +/** + * Disconnect an entry from the chain (updates the tip). + * @param {ChainBlock} entry + * @param {Function} callback + */ + +Chain.prototype.disconnect = function disconnect(entry, callback) { + var self = this; + + this.db.disconnect(entry, function(err) { + if (err) + return callback(err); + + entry.getPrevious(function(err, entry) { + if (err) + return callback(err); + + assert(entry); + + self.tip = entry; + self.height = entry.height; + + if (self.bestHeight === -1) + network.height = entry.height; + + self.emit('tip', entry); + + return callback(); + }); + }); +}; + +/** + * Connect an entry to the chain (updates the tip). + * This will do contextual-verification on the block + * (necessary because we cannot validate the inputs + * in side chains when they come in). + * @param {ChainBlock} entry + * @param {Function} callback + */ + +Chain.prototype.connect = function connect(entry, callback) { + var self = this; + + this.db.getBlock(entry.hash, function(err, block) { + if (err) + return callback(err); + + assert(block); + + entry.getPrevious(function(err, prev) { + if (err) + return callback(err); + + assert(prev); + + self._verifyContext(block, prev, function(err) { + if (err) + return callback(err); + + self.db.connect(entry, block, function(err) { + if (err) + return callback(err); + + self.tip = entry; + self.height = entry.height; + + if (self.bestHeight === -1) + network.height = entry.height; + + self.emit('tip', entry); + + return callback(); + }); + }); + }); + }); +}; + /** * Set the best chain. This is called on every valid block * that comes in. It may add and connect the block (main chain), @@ -1009,32 +1091,52 @@ Chain.prototype._reorganize = function _reorganize(entry, block, callback) { * reorganize the chain (a higher fork). * @private * @param {ChainBlock} entry + * @param {ChainBlock} prev * @param {Block|MerkleBlock} block * @param {Function} callback - Returns [{@link VerifyError}]. */ -Chain.prototype._setBestChain = function _setBestChain(entry, block, callback) { +Chain.prototype._setBestChain = function _setBestChain(entry, prev, block, callback) { var self = this; function done(err) { if (err) return callback(err); - // Save block and connect inputs. - self.db.save(entry, block, true, function(err) { - if (err) + self._verifyContext(block, prev, function(err) { + if (err) { + // Couldn't verify block. + // Revert the height. + block.setHeight(-1); + + if (err.type === 'VerifyError') { + self.invalid[entry.hash] = true; + self.emit('invalid', block, { + height: entry.height, + hash: entry.hash, + seen: false, + chain: false + }); + } + return callback(err); + } - self.tip = entry; - self.height = entry.height; + // Save block and connect inputs. + self.db.save(entry, block, true, function(err) { + if (err) + return callback(err); - if (self.bestHeight === -1) - network.height = entry.height; + self.tip = entry; + self.height = entry.height; - self.emit('tip', entry); + if (self.bestHeight === -1) + network.height = entry.height; - // Return true (added to the main chain) - return callback(null, true); + self.emit('tip', entry); + + return callback(); + }); }); } @@ -1054,21 +1156,10 @@ Chain.prototype._setBestChain = function _setBestChain(entry, block, callback) { return done(); } - // The block is on a side chain if the - // chainwork is less than or equal to - // our tip's. Add the block but do _not_ - // connect the inputs. - if (entry.chainwork.cmp(this.tip.chainwork) <= 0) { - return this.db.save(entry, block, false, function(err) { - if (err) - return callback(err); - - // Return false (added to side chain) - return callback(null, false); - }); - } - // Everything is in order. + // Do "contextual" verification on our block + // now that we're certain its previous + // block is in the chain. if (entry.prevBlock === this.tip.hash) return done(); @@ -1169,7 +1260,22 @@ Chain.prototype.add = function add(block, callback, force) { (function next(block, initial) { var hash = block.hash('hex'); var prevHash = block.prevBlock; - var height, checkpoint, orphan; + var height, checkpoint, orphan, entry; + + function handleOrphans() { + // No orphan chain. + if (!self.orphan.map[hash]) + return done(); + + // An orphan chain was found, start resolving. + block = self.orphan.map[hash]; + delete self.orphan.bmap[block.hash('hex')]; + delete self.orphan.map[hash]; + self.orphan.count--; + self.orphan.size -= block.getSize(); + + next(block); + } // Do not revalidate known invalid blocks. if (self.invalid[hash] || self.invalid[prevHash]) { @@ -1344,76 +1450,55 @@ Chain.prototype.add = function add(block, callback, force) { // need access to height on txs. block.setHeight(height); - // Do "contextual" verification on our block - // now that we're certain its previous - // block is in the chain. - self._verifyContext(block, prev, function(err) { - var entry; + // Create a new chain entry. + entry = new bcoin.chainblock(self, { + hash: hash, + version: block.version, + prevBlock: block.prevBlock, + merkleRoot: block.merkleRoot, + ts: block.ts, + bits: block.bits, + nonce: block.nonce, + height: height + }, prev); - if (err) { - // Couldn't verify block. - // Revert the height. - block.setHeight(-1); - - if (err.type === 'VerifyError') { - self.invalid[hash] = true; - self.emit('invalid', block, { - height: height, - hash: hash, - seen: false, - chain: false - }); - } - - return done(err); - } - - // Create a new chain entry. - entry = new bcoin.chainblock(self, { - hash: hash, - version: block.version, - prevBlock: block.prevBlock, - merkleRoot: block.merkleRoot, - ts: block.ts, - bits: block.bits, - nonce: block.nonce, - height: height - }, prev); - - // Attempt to add block to the chain index. - self._setBestChain(entry, block, function(err, mainChain) { + // The block is on a side chain if the + // chainwork is less than or equal to + // our tip's. Add the block but do _not_ + // connect the inputs. + if (entry.chainwork.cmp(self.tip.chainwork) <= 0) { + return self.db.save(entry, block, false, function(err) { if (err) - return done(err); - - // Keep track of the number of blocks we - // added and the number of orphans resolved. - total++; + return callback(err); // Emit our block (and potentially resolved // orphan) only if it is on the main chain. - if (mainChain) { - self.emit('block', block, entry); - if (!initial) - self.emit('resolved', block, entry); - } else { - self.emit('competitor', block, entry); - if (!initial) - self.emit('competitor resolved', block, entry); - } + self.emit('competitor', block, entry); - // No orphan chain. - if (!self.orphan.map[hash]) - return done(); + if (!initial) + self.emit('competitor resolved', block, entry); - // An orphan chain was found, start resolving. - block = self.orphan.map[hash]; - delete self.orphan.bmap[block.hash('hex')]; - delete self.orphan.map[hash]; - self.orphan.count--; - self.orphan.size -= block.getSize(); - - next(block); + handleOrphans(); }); + } + + // Attempt to add block to the chain index. + self._setBestChain(entry, prev, block, function(err) { + if (err) + return done(err); + + // Keep track of the number of blocks we + // added and the number of orphans resolved. + total++; + + // Emit our block (and potentially resolved + // orphan) only if it is on the main chain. + self.emit('block', block, entry); + + if (!initial) + self.emit('resolved', block, entry); + + handleOrphans(); }); }); }); diff --git a/lib/bcoin/chaindb.js b/lib/bcoin/chaindb.js index 84e1a34a..c51bf12d 100644 --- a/lib/bcoin/chaindb.js +++ b/lib/bcoin/chaindb.js @@ -505,37 +505,28 @@ ChainDB.prototype.getTip = function getTip(callback) { * @param {Function} callback - Returns [Error, {@link ChainBlock}]. */ -ChainDB.prototype.connect = function connect(block, callback) { +ChainDB.prototype.connect = function connect(entry, block, callback) { var self = this; - var batch, hash; + var batch = this.db.batch(); + var hash = new Buffer(entry.hash, 'hex'); - this._ensureEntry(block, function(err, entry) { + batch.put('c/n/' + entry.prevBlock, hash); + batch.put('c/h/' + pad32(entry.height), hash); + batch.put('c/t', hash); + + this.cacheHash.set(entry.hash, entry); + this.cacheHeight.set(entry.height, entry); + + this.emit('add entry', entry); + + this.connectBlock(block, batch, function(err) { if (err) return callback(err); - if (!entry) - return callback(); - - batch = self.db.batch(); - hash = new Buffer(entry.hash, 'hex'); - - batch.put('c/n/' + entry.prevBlock, hash); - batch.put('c/h/' + pad32(entry.height), hash); - batch.put('c/t', hash); - - self.cacheHeight.set(entry.height, entry); - - self.emit('add entry', entry); - - self.connectBlock(entry.hash, batch, function(err) { + batch.write(function(err) { if (err) return callback(err); - - batch.write(function(err) { - if (err) - return callback(err); - return callback(null, entry); - }); + return callback(null, entry); }); }); }; @@ -555,7 +546,7 @@ ChainDB.prototype.disconnect = function disconnect(block, callback) { return callback(err); if (!entry) - return callback(); + return callback(new Error('Entry not found.')); batch = self.db.batch(); @@ -869,14 +860,14 @@ ChainDB.prototype.disconnectBlock = function disconnectBlock(hash, batch, callba return callback(err); if (!block) - return callback(); + return callback(new Error('Block not found.')); if (self.options.paranoid) { if (typeof hash === 'string') assert(block.hash('hex') === hash, 'Database is corrupt.'); } - block.txs.forEach(function(tx) { + block.txs.slice().reverse().forEach(function(tx) { var hash = tx.hash('hex'); var uniq = {}; @@ -1448,7 +1439,7 @@ ChainDB.prototype.getBlock = function getBlock(hash, callback) { key = 'b/b/' + hash; self.db.get(key, function(err, data) { - if (err && errr.type !== 'NotFoundError') + if (err && err.type !== 'NotFoundError') return callback(err); if (!data) diff --git a/lib/bcoin/miner.js b/lib/bcoin/miner.js index e7af3f90..b887ea17 100644 --- a/lib/bcoin/miner.js +++ b/lib/bcoin/miner.js @@ -247,7 +247,7 @@ Miner.prototype.createBlock = function createBlock(callback) { target: target, address: self.address, coinbaseFlags: self.coinbaseFlags, - segwit: self.chain.segwitActive, + witness: self.chain.segwitActive, dsha256: self.dsha256 }); @@ -310,7 +310,7 @@ Miner.prototype.mineBlock = function mineBlock(callback) { * @param {Number} options.target - Compact form. * @param {Function} options.dsha256 * @param {Base58Address} options.address - Payout address. - * @param {Boolean} options.segwit - Allow witness + * @param {Boolean} options.witness - Allow witness * transactions, mine a witness block. * @property {Block} block * @property {TX} coinbase @@ -378,7 +378,7 @@ function MinerBlock(options) { this.block.txs.push(this.coinbase); - if (options.segwit) { + if (options.witness) { // Set up the witness nonce and // commitment output for segwit. this.witness = true; @@ -444,13 +444,13 @@ MinerBlock.prototype.addTX = function addTX(tx) { var size = this.block.getVirtualSize(true) + tx.getVirtualSize(); // Deliver me from the block size debate, please - if (size > constants.blocks.maxSize) + if (size > constants.block.maxSize) return false; if (this.block.hasTX(tx)) return false; - if (!this.block.witness && tx.hasWitness()) + if (!this.witness && tx.hasWitness()) return false; // Add the tx to our block diff --git a/lib/bcoin/workers.js b/lib/bcoin/workers.js index 7e64d4b8..9f457b9d 100644 --- a/lib/bcoin/workers.js +++ b/lib/bcoin/workers.js @@ -159,7 +159,7 @@ bcoin.miner.minerblock.prototype.mineAsync = function mineAsync(callback) { target: this.block.bits, address: this.options.address, coinbaseFlags: this.options.coinbaseFlags, - segwit: this.options.segwit + witness: this.options.witness }; return workers.call('mine', [attempt], callback); }; @@ -220,7 +220,7 @@ workers.mine = function mine(attempt) { target: attempt.target, address: attempt.address, coinbaseFlags: attempt.coinbaseFlags, - segwit: attempt.segwit, + witness: attempt.witness, dsha256: utils.dsha256 }); attempt.on('status', function(stat) { diff --git a/test/chain-test.js b/test/chain-test.js new file mode 100644 index 00000000..d2ea8dd0 --- /dev/null +++ b/test/chain-test.js @@ -0,0 +1,180 @@ +var bn = require('bn.js'); +delete process.env.BCOIN_NETWORK; +var bcoin = require('../')({ network: 'regtest', db: 'memory' }); +process.env.BCOIN_NETWORK = 'main'; +var constants = bcoin.protocol.constants; +var utils = bcoin.utils; +var assert = utils.assert; +var opcodes = constants.opcodes; + +constants.tx.coinbaseMaturity = 0; + +describe('Chain', function() { + var chain, wallet, miner; + var competingTip, oldTip, ch1, ch2, cb1, cb2; + + chain = new bcoin.chain(); + wallet = new bcoin.wallet(); + miner = new bcoin.miner({ + chain: chain, + address: wallet.getAddress() + }); + + chain.on('error', function() {}); + miner.on('error', function() {}); + + function mineBlock(entry, tx, callback) { + var realTip; + if (entry) { + realTip = chain.tip; + chain.tip = entry; + } + miner.createBlock(function(err, attempt) { + if (realTip) + chain.tip = realTip; + assert.noError(err); + if (tx) { + var redeemer = bcoin.mtx(); + redeemer.addOutput({ + address: wallet.getAddress(), + value: utils.satoshi('25.0') + }); + redeemer.addInput(tx, 0); + wallet.sign(redeemer); + attempt.addTX(redeemer); + } + callback(null, attempt.mineSync()); + }); + } + + function deleteCoins(tx) { + if (Array.isArray(tx)) { + tx.forEach(deleteCoins); + return; + } + tx.inputs.forEach(function(input) { + delete input.coin; + }); + } + + it('should open chain and miner', function(cb) { + miner.open(cb); + }); + + it('should mine a block', function(cb) { + miner.mineBlock(function(err, block) { + assert.noError(err); + assert(block); + cb(); + }); + }); + + it('should mine competing chains', function(cb) { + utils.forRangeSerial(0, 10, function(i, next) { + mineBlock(ch1, cb1, function(err, chain1) { + assert.noError(err); + cb1 = chain1.txs[0]; + mineBlock(ch2, cb2, function(err, chain2) { + assert.noError(err); + cb2 = chain2.txs[0]; + deleteCoins(chain1.txs); + chain.add(chain1, function(err) { + assert.noError(err); + deleteCoins(chain2.txs); + chain.add(chain2, function(err) { + assert.noError(err); + assert(chain.tip.hash === chain1.hash('hex')); + competingTip = chain2.hash('hex'); + chain.db.get(chain1.hash('hex'), function(err, entry1) { + assert.noError(err); + chain.db.get(chain2.hash('hex'), function(err, entry2) { + assert.noError(err); + assert(entry1); + assert(entry2); + ch1 = entry1; + ch2 = entry2; + chain.db.isMainChain(chain2.hash('hex'), function(err, result) { + assert.noError(err); + assert(!result); + next(); + }); + }); + }); + }); + }); + }); + }); + }, cb); + }); + + it('should handle a reorg', function(cb) { + oldTip = chain.tip; + chain.db.get(competingTip, function(err, entry) { + assert.noError(err); + assert(entry); + assert(chain.height === entry.height); + chain.tip = entry; + miner.mineBlock(function(err, reorg) { + assert.noError(err); + assert(reorg); + chain.tip = oldTip; + var forked = false; + chain.once('fork', function() { + forked = true; + }); + deleteCoins(reorg.txs); + chain.add(reorg, function(err) { + assert.noError(err); + assert(forked); + assert(chain.tip.hash === reorg.hash('hex')); + assert(chain.tip.chainwork.cmp(oldTip.chainwork) > 0); + cb(); + }); + }); + }); + }); + + it('should check main chain', function(cb) { + chain.db.isMainChain(oldTip, function(err, result) { + assert.noError(err); + assert(!result); + cb(); + }); + }); + + it('should mine a block after a reorg', function(cb) { + mineBlock(null, cb2, function(err, block) { + assert.noError(err); + deleteCoins(block.txs); + chain.add(block, function(err) { + assert.noError(err); + chain.db.get(block.hash('hex'), function(err, entry) { + assert.noError(err); + assert(entry); + assert(chain.tip.hash === entry.hash); + chain.db.isMainChain(entry.hash, function(err, result) { + assert.noError(err); + assert(result); + cb(); + }); + }); + }); + }); + }); + + it('should fail to mine a block with coins on a side chain', function(cb) { + mineBlock(null, cb1, function(err, block) { + assert.noError(err); + deleteCoins(block.txs); + chain.add(block, function(err) { + assert(err); + cb(); + }); + }); + }); + + it('should cleanup', function(cb) { + constants.tx.coinbaseMaturity = 100; + cb(); + }); +}); diff --git a/test/node-test.js b/test/node-test.js index 7a584d21..2a3a3a15 100644 --- a/test/node-test.js +++ b/test/node-test.js @@ -5,7 +5,7 @@ var utils = bcoin.utils; var assert = utils.assert; var opcodes = constants.opcodes; -describe('Wallet', function() { +describe('Node', function() { var node = new bcoin.fullnode(); node.on('error', function() {});