From aec3548b26e4612bbe2f2ed7a21895bc1d3f6acf Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Mon, 22 Aug 2016 17:55:44 -0700 Subject: [PATCH] walletdb: fix reorg handling. --- lib/bcoin/chain.js | 14 +++--- lib/bcoin/miner.js | 46 +++++++++-------- lib/bcoin/txdb.js | 5 +- lib/bcoin/walletdb.js | 33 ++++++++---- test/chain-test.js | 113 +++++++++++++++++++++++------------------- 5 files changed, 120 insertions(+), 91 deletions(-) diff --git a/lib/bcoin/chain.js b/lib/bcoin/chain.js index c156ad25..59952542 100644 --- a/lib/bcoin/chain.js +++ b/lib/bcoin/chain.js @@ -939,19 +939,19 @@ Chain.prototype.disconnect = function disconnect(entry, callback) { if (err) return callback(err); - entry.getPrevious(function(err, entry) { + entry.getPrevious(function(err, prev) { if (err) return callback(err); - assert(entry); + assert(prev); - self.tip = entry; - self.height = entry.height; + self.tip = prev; + self.height = prev.height; - self.bestHeight = entry.height; - self.network.updateHeight(entry.height); + self.bestHeight = prev.height; + self.network.updateHeight(prev.height); - self.emit('tip', entry); + self.emit('tip', prev); self.emit('disconnect', entry, block); callback(); diff --git a/lib/bcoin/miner.js b/lib/bcoin/miner.js index 45b2109b..d15cd126 100644 --- a/lib/bcoin/miner.js +++ b/lib/bcoin/miner.js @@ -42,6 +42,7 @@ function Miner(options) { this.options = options; this.address = bcoin.address(this.options.address); this.coinbaseFlags = this.options.coinbaseFlags || 'mined by bcoin'; + this.version = null; this.pool = options.pool; this.chain = options.chain; @@ -168,7 +169,7 @@ Miner.prototype._close = function close(callback) { * @param {Number?} version - Custom block version. */ -Miner.prototype.start = function start(version) { +Miner.prototype.start = function start() { var self = this; this.stop(); @@ -176,7 +177,7 @@ Miner.prototype.start = function start(version) { this.running = true; // Create a new block and start hashing - this.createBlock(version, function(err, attempt) { + this.createBlock(function(err, attempt) { if (err) return self.emit('error', err); @@ -241,35 +242,39 @@ Miner.prototype.stop = function stop() { * @param {Function} callback - Returns [Error, {@link MinerBlock}]. */ -Miner.prototype.createBlock = function createBlock(version, callback) { +Miner.prototype.createBlock = function createBlock(tip, callback) { var self = this; - var ts = Math.max(bcoin.now(), this.chain.tip.ts + 1); - var i, attempt, txs, tx; + var i, ts, attempt, txs, tx; - if (typeof version === 'function') { - callback = version; - version = null; + if (typeof tip === 'function') { + callback = tip; + tip = null; } + if (!tip) + tip = this.chain.tip; + + ts = Math.max(bcoin.now(), tip.ts + 1); + function computeVersion(callback) { - if (version != null) - return callback(null, version); - self.chain.computeBlockVersion(self.chain.tip, callback); + if (self.version != null) + return callback(null, self.version); + self.chain.computeBlockVersion(tip, callback); } if (!this.loaded) { this.open(function(err) { if (err) return callback(err); - self.createBlock(version, callback); + self.createBlock(tip, callback); }); return; } - assert(this.chain.tip); + assert(tip); // Find target - this.chain.getTargetAsync(ts, this.chain.tip, function(err, target) { + this.chain.getTargetAsync(ts, tip, function(err, target) { if (err) return callback(err); @@ -280,7 +285,7 @@ Miner.prototype.createBlock = function createBlock(version, callback) { attempt = new MinerBlock({ workerPool: self.workerPool, - tip: self.chain.tip, + tip: tip, version: version, target: target, address: self.address, @@ -311,14 +316,14 @@ Miner.prototype.createBlock = function createBlock(version, callback) { * @param {Function} callback - Returns [Error, [{@link Block}]]. */ -Miner.prototype.mineBlock = function mineBlock(version, callback) { - if (typeof version === 'function') { - callback = version; - version = null; +Miner.prototype.mineBlock = function mineBlock(tip, callback) { + if (typeof tip === 'function') { + callback = tip; + tip = null; } // Create a new block and start hashing - this.createBlock(version, function(err, attempt) { + this.createBlock(tip, function(err, attempt) { if (err) return callback(err); @@ -564,6 +569,7 @@ MinerBlock.prototype.findNonce = function findNonce() { while (block.nonce <= 0xffffffff) { // Hash and test against the next target. if (rcmp(utils.hash256(data), target) <= 0) { + this.coinbase.mutable = false; this.block.mutable = false; return true; } diff --git a/lib/bcoin/txdb.js b/lib/bcoin/txdb.js index a1edc1aa..1e1043c5 100644 --- a/lib/bcoin/txdb.js +++ b/lib/bcoin/txdb.js @@ -594,6 +594,9 @@ TXDB.prototype._verify = function _verify(tx, info, callback) { return callback(null, false); } + self.logger.warning('Removing conflicting tx: %s.', + utils.revHex(spent.hash)); + self._removeConflict(spent.hash, tx, function(err, tx, info) { if (err) return next(err); @@ -1226,7 +1229,7 @@ TXDB.prototype.unconfirm = function unconfirm(hash, callback, force) { return callback(err); if (!tx) - return callback(null, true); + return callback(null, false); self.getInfo(tx, function(err, info) { if (err) diff --git a/lib/bcoin/walletdb.js b/lib/bcoin/walletdb.js index 973b55c5..a5a97226 100644 --- a/lib/bcoin/walletdb.js +++ b/lib/bcoin/walletdb.js @@ -1374,7 +1374,7 @@ WalletDB.prototype.unwriteBlock = function unwriteBlock(block, callback) { */ WalletDB.prototype.getBlock = function getBlock(hash, callback) { - this.db.fetch(layout.b(hash), function(err, data) { + this.db.fetch(layout.b(hash), function(data) { return WalletBlock.fromRaw(hash, data); }, callback); }; @@ -1438,6 +1438,11 @@ WalletDB.prototype.addBlock = function addBlock(entry, txs, callback) { if (err) return callback(err); + if (block.hashes.length > 0) { + self.logger.info('Connecting block %s (%d txs).', + utils.revHex(block.hash), block.hashes.length); + } + self.writeBlock(block, matches, callback); }); }; @@ -1451,12 +1456,15 @@ WalletDB.prototype.addBlock = function addBlock(entry, txs, callback) { WalletDB.prototype.removeBlock = function removeBlock(entry, callback) { var self = this; + var block; callback = this._lockTX(removeBlock, [entry, callback]); if (!callback) return; + block = WalletBlock.fromEntry(entry); + // Note: // If we crash during a reorg, there's not much to do. // Reorgs cannot be rescanned. The database will be @@ -1464,14 +1472,17 @@ WalletDB.prototype.removeBlock = function removeBlock(entry, callback) { // when they shouldn't be. That being said, this // should eventually resolve itself when a new block // comes in. - this.getBlock(entry.hash, function(err, block) { + this.getBlock(block.hash, function(err, data) { if (err) return callback(err); - // Not a saved block, but we - // still want to reset the tip. - if (!block) - block = WalletBlock.fromEntry(entry); + if (data) + block.hashes = data.hashes; + + if (block.hashes.length > 0) { + self.logger.warning('Disconnecting block %s (%d txs).', + utils.revHex(block.hash), block.hashes.length); + } // Unwrite the tip as fast as we can. self.unwriteBlock(block, function(err) { @@ -1503,10 +1514,10 @@ WalletDB.prototype.removeBlock = function removeBlock(entry, callback) { self.tip = block.hash; self.height = block.height; - callback(); + next(); }); }); - }); + }, callback); }); }); }; @@ -2128,21 +2139,21 @@ WalletBlock.fromJSON = function fromJSON(json) { }; WalletBlock.fromRaw = function fromRaw(hash, data) { - return new WalletBlock().fromTip(hash, data); + return new WalletBlock().fromRaw(hash, data); }; WalletBlock.fromTip = function fromTip(data) { return new WalletBlock().fromTip(data); }; -WalletBlock.prototype.toTip = function toTip(data) { +WalletBlock.prototype.toTip = function toTip() { var p = new BufferWriter(); p.writeHash(this.hash); p.writeU32(this.height); return p.render(); }; -WalletBlock.prototype.toRaw = function toRaw(data) { +WalletBlock.prototype.toRaw = function toRaw() { var p = new BufferWriter(); var i; diff --git a/test/chain-test.js b/test/chain-test.js index 76ccefa0..15225593 100644 --- a/test/chain-test.js +++ b/test/chain-test.js @@ -10,38 +10,28 @@ var opcodes = constants.opcodes; constants.tx.COINBASE_MATURITY = 0; describe('Chain', function() { - var chain, wallet, miner, walletdb; - var competingTip, oldTip, ch1, ch2, cb1, cb2; + var chain, wallet, node, miner, walletdb; + var competingTip, oldTip, tip1, tip2, cb1, cb2; this.timeout(5000); - chain = new bcoin.chain({ name: 'chain-test', db: 'memory' }); - walletdb = new bcoin.walletdb({ name: 'chain-test-wdb', db: 'memory' }); - miner = new bcoin.miner({ - chain: chain - }); + node = new bcoin.fullnode({ db: 'memory' }); + chain = node.chain; + walletdb = node.walletdb; + miner = node.miner; + node.on('error', function() {}); - 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; + function mineBlock(tip, tx, callback) { + miner.createBlock(tip, function(err, attempt) { assert.ifError(err); if (tx) { var redeemer = bcoin.mtx(); redeemer.addOutput({ - address: wallet.getAddress(), + address: wallet.receiveAddress.getAddress(), value: utils.satoshi('25.0') }); redeemer.addOutput({ - address: wallet.account.deriveAddress(false, 100).getAddress(), + address: wallet.changeAddress.getAddress(), value: utils.satoshi('5.0') }); redeemer.addInput(tx, 0); @@ -71,18 +61,16 @@ describe('Chain', function() { } it('should open chain and miner', function(cb) { - miner.open(cb); + miner.mempool = null; + node.open(cb); }); it('should open walletdb', function(cb) { - walletdb.open(function(err) { + walletdb.create({}, function(err, w) { assert.ifError(err); - walletdb.create({}, function(err, w) { - assert.ifError(err); - wallet = w; - miner.address = wallet.getAddress(); - cb(); - }); + wallet = w; + miner.address = wallet.getAddress(); + cb(); }); }); @@ -96,29 +84,29 @@ describe('Chain', function() { it('should mine competing chains', function(cb) { utils.forRangeSerial(0, 10, function(i, next) { - mineBlock(ch1, cb1, function(err, chain1) { + mineBlock(tip1, cb1, function(err, block1) { assert.ifError(err); - cb1 = chain1.txs[0]; - mineBlock(ch2, cb2, function(err, chain2) { + cb1 = block1.txs[0]; + mineBlock(tip2, cb2, function(err, block2) { assert.ifError(err); - cb2 = chain2.txs[0]; - deleteCoins(chain1); - chain.add(chain1, function(err) { + cb2 = block2.txs[0]; + deleteCoins(block1); + chain.add(block1, function(err) { assert.ifError(err); - deleteCoins(chain2); - chain.add(chain2, function(err) { + deleteCoins(block2); + chain.add(block2, function(err) { assert.ifError(err); - assert(chain.tip.hash === chain1.hash('hex')); - competingTip = chain2.hash('hex'); - chain.db.get(chain1.hash('hex'), function(err, entry1) { + assert(chain.tip.hash === block1.hash('hex')); + competingTip = block2.hash('hex'); + chain.db.get(block1.hash('hex'), function(err, entry1) { assert.ifError(err); - chain.db.get(chain2.hash('hex'), function(err, entry2) { + chain.db.get(block2.hash('hex'), function(err, entry2) { assert.ifError(err); assert(entry1); assert(entry2); - ch1 = entry1; - ch2 = entry2; - chain.db.isMainChain(chain2.hash('hex'), function(err, result) { + tip1 = entry1; + tip2 = entry2; + chain.db.isMainChain(block2.hash('hex'), function(err, result) { assert.ifError(err); assert(!result); next(); @@ -133,25 +121,25 @@ describe('Chain', function() { }); it('should handle a reorg', function(cb) { + assert.equal(walletdb.height, chain.height); + assert.equal(chain.height, 10); oldTip = chain.tip; chain.db.get(competingTip, function(err, entry) { assert.ifError(err); assert(entry); assert(chain.height === entry.height); - chain.tip = entry; - miner.mineBlock(function(err, reorg) { + miner.mineBlock(entry, function(err, block) { assert.ifError(err); - assert(reorg); - chain.tip = oldTip; + assert(block); var forked = false; chain.once('reorganize', function() { forked = true; }); - deleteCoins(reorg); - chain.add(reorg, function(err) { + deleteCoins(block); + chain.add(block, function(err) { assert.ifError(err); assert(forked); - assert(chain.tip.hash === reorg.hash('hex')); + assert(chain.tip.hash === block.hash('hex')); assert(chain.tip.chainwork.cmp(oldTip.chainwork) > 0); cb(); }); @@ -209,7 +197,8 @@ describe('Chain', function() { assert.ifError(err); chain.db.getCoin(block.txs[1].hash('hex'), 1, function(err, coin) { assert.ifError(err); - assert.deepEqual(coin.toRaw(), bcoin.coin.fromTX(block.txs[1], 1).toRaw()); + var output = bcoin.coin.fromTX(block.txs[1], 1); + assert.deepEqual(coin.toRaw(), output.toRaw()); cb(); }); }); @@ -218,6 +207,26 @@ describe('Chain', function() { }); }); + it('should get balance', function(cb) { + setTimeout(function() { + wallet.getBalance(function(err, balance) { + assert.ifError(err); + assert.equal(balance.unconfirmed, 23000000000); + assert.equal(balance.confirmed, 97000000000); + assert.equal(balance.total, 120000000000); + assert.equal(wallet.account.receiveDepth, 8); + assert.equal(wallet.account.changeDepth, 7); + assert.equal(walletdb.height, chain.height); + assert.equal(walletdb.tip, chain.tip.hash); + wallet.getHistory(function(err, txs) { + assert.ifError(err); + assert.equal(txs.length, 44); + cb(); + }); + }); + }, 100); + }); + it('should rescan for transactions', function(cb) { var txs = []; walletdb.getAddressHashes(function(err, hashes) {