diff --git a/lib/bcoin/chain.js b/lib/bcoin/chain.js index f8c9c3ba..20014ee4 100644 --- a/lib/bcoin/chain.js +++ b/lib/bcoin/chain.js @@ -45,13 +45,13 @@ var VerifyError = bcoin.errors.VerifyError; * @emits Chain#resolved * @emits Chain#checkpoint * @emits Chain#fork + * @emits Chain#reorganize * @emits Chain#invalid * @emits Chain#exists * @emits Chain#purge - * @emits Chain#add entry - * @emits Chain#remove entry - * @emits Chain#add block - * @emits Chain#remove block + * @emits Chain#connect + * @emits Chain#reconnect + * @emits Chain#disconnect */ function Chain(options) { @@ -152,6 +152,15 @@ Chain.prototype._init = function _init() { ); }); + this.on('reorganize', function(block, height, expected) { + self.logger.warning( + 'Reorg at height %d: old=%s new=%s', + height, + utils.revHex(expected), + block.rhash + ); + }); + this.on('invalid', function(block, height) { self.logger.warning('Invalid block at height %d: hash=%s', height, block.rhash); @@ -169,22 +178,6 @@ Chain.prototype._init = function _init() { self.logger.debug('Warning: %d (%dmb) orphans cleared!', count, utils.mb(size)); }); - - this.db.on('add entry', function(entry) { - self.emit('add entry', entry); - }); - - this.db.on('remove entry', function(entry) { - self.emit('remove entry', entry); - }); - - this.db.on('add block', function(block) { - self.emit('add block', block); - }); - - this.db.on('remove block', function(block) { - self.emit('remove block', block); - }); }; /** @@ -1017,7 +1010,7 @@ Chain.prototype.reorganize = function reorganize(entry, block, callback) { } // Connect blocks/txs. - function connect(callback) { + function reconnect(callback) { var entries = []; (function collect(entry) { @@ -1044,7 +1037,7 @@ Chain.prototype.reorganize = function reorganize(entry, block, callback) { entries.pop(); utils.forEachSerial(entries, function(entry, next) { - self.connect(entry, next); + self.reconnect(entry, next); }, callback); } } @@ -1053,11 +1046,11 @@ Chain.prototype.reorganize = function reorganize(entry, block, callback) { if (err) return callback(err); - return connect(function(err) { + return reconnect(function(err) { if (err) return callback(err); - self.emit('fork', block, tip.height, tip.hash); + self.emit('reorganize', block, tip.height, tip.hash); return callback(); }); @@ -1074,7 +1067,7 @@ Chain.prototype.reorganize = function reorganize(entry, block, callback) { Chain.prototype.disconnect = function disconnect(entry, callback) { var self = this; - this.db.disconnect(entry, function(err) { + this.db.disconnect(entry, function(err, entry, block) { if (err) return callback(err); @@ -1091,6 +1084,7 @@ Chain.prototype.disconnect = function disconnect(entry, callback) { self.network.updateHeight(entry.height); self.emit('tip', entry); + self.emit('disconnect', entry, block); return callback(); }); @@ -1098,7 +1092,7 @@ Chain.prototype.disconnect = function disconnect(entry, callback) { }; /** - * Connect an entry to the chain (updates the tip). + * Reconnect an entry to the chain (updates the tip). * This will do contextual-verification on the block * (necessary because we cannot validate the inputs * in alternate chains when they come in). @@ -1106,14 +1100,17 @@ Chain.prototype.disconnect = function disconnect(entry, callback) { * @param {Function} callback */ -Chain.prototype.connect = function connect(entry, callback) { +Chain.prototype.reconnect = function reconnect(entry, callback) { var self = this; this.db.getBlock(entry.hash, function(err, block) { if (err) return callback(err); - assert(block); + if (!block) { + assert(self.options.spv); + block = entry.toHeaders(); + } entry.getPrevious(function(err, prev) { if (err) @@ -1130,7 +1127,7 @@ Chain.prototype.connect = function connect(entry, callback) { return callback(err); } - self.db.connect(entry, block, view, function(err) { + self.db.reconnect(entry, block, view, function(err) { if (err) return callback(err); @@ -1141,6 +1138,8 @@ Chain.prototype.connect = function connect(entry, callback) { self.network.updateHeight(entry.height); self.emit('tip', entry); + self.emit('reconnect', entry, block); + self.emit('connect', entry, block); return callback(); }); @@ -1516,6 +1515,7 @@ Chain.prototype.add = function add(block, callback, force) { // Emit our block (and potentially resolved // orphan) only if it is on the main chain. self.emit('block', block, entry); + self.emit('connect', entry, block); if (!initial) self.emit('resolved', block, entry); diff --git a/lib/bcoin/chaindb.js b/lib/bcoin/chaindb.js index 0c365097..23d5a807 100644 --- a/lib/bcoin/chaindb.js +++ b/lib/bcoin/chaindb.js @@ -161,10 +161,6 @@ var layout = { * @property {Number} keepBlocks * @emits ChainDB#open * @emits ChainDB#error - * @emits ChainDB#add block - * @emits ChainDB#remove block - * @emits ChainDB#add entry - * @emits ChainDB#remove entry */ function ChainDB(chain, options) { @@ -501,6 +497,7 @@ ChainDB.prototype.get = function get(hash, callback) { * instead performed in {@link Chain#add}. * @param {ChainEntry} entry * @param {Block} block + * @param {CoinView} view * @param {Boolean} connect - Whether to connect the * block's inputs and add it as a tip. * @param {Function} callback @@ -532,8 +529,6 @@ ChainDB.prototype.save = function save(entry, block, view, connect, callback) { batch.put(layout.H(entry.height), hash); batch.put(layout.R, hash); - this.emit('add entry', entry); - this.saveBlock(block, view, batch, true, function(err) { if (err) return callback(err); @@ -563,12 +558,15 @@ ChainDB.prototype.getTip = function getTip(callback) { }; /** - * Connect the block to the chain. - * @param {ChainEntry|Hash|Height} block - entry, height, or hash. - * @param {Function} callback - Returns [Error, {@link ChainEntry}]. + * Reconnect the block to the chain. + * @param {ChainEntry} entry + * @param {Block} block + * @param {CoinView} view + * @param {Function} callback - + * Returns [Error, {@link ChainEntry}, {@link Block}]. */ -ChainDB.prototype.connect = function connect(entry, block, view, callback) { +ChainDB.prototype.reconnect = function reconnect(entry, block, view, callback) { var batch = this.db.batch(); var hash = block.hash(); @@ -579,7 +577,13 @@ ChainDB.prototype.connect = function connect(entry, block, view, callback) { this.cacheHash.set(entry.hash, entry); this.cacheHeight.set(entry.height, entry); - this.emit('add entry', entry); + if (this.options.spv) { + return batch.write(function(err) { + if (err) + return callback(err); + return callback(null, entry, block); + }); + } this.connectBlock(block, view, batch, function(err) { if (err) @@ -588,15 +592,16 @@ ChainDB.prototype.connect = function connect(entry, block, view, callback) { batch.write(function(err) { if (err) return callback(err); - return callback(null, entry); + return callback(null, entry, block); }); }); }; /** * Disconnect block from the chain. - * @param {ChainEntry|Hash|Height} block - Entry, height, or hash. - * @param {Function} callback - Returns [Error, {@link ChainEntry}]. + * @param {ChainEntry} entry + * @param {Function} callback - + * Returns [Error, {@link ChainEntry}, {@link Block}]. */ ChainDB.prototype.disconnect = function disconnect(entry, callback) { @@ -609,7 +614,13 @@ ChainDB.prototype.disconnect = function disconnect(entry, callback) { this.cacheHeight.remove(entry.height); - this.emit('remove entry', entry); + if (this.options.spv) { + return batch.write(function(err) { + if (err) + return callback(err); + return callback(null, entry, entry.toHeaders()); + }); + } this.getBlock(entry.hash, function(err, block) { if (err) @@ -625,7 +636,7 @@ ChainDB.prototype.disconnect = function disconnect(entry, callback) { batch.write(function(err) { if (err) return callback(err); - return callback(null, entry); + return callback(null, entry, block); }); }); }); @@ -724,8 +735,6 @@ ChainDB.prototype.reset = function reset(block, callback) { batch.del(layout.e(tip.hash)); batch.del(layout.n(tip.prevBlock)); - self.emit('remove entry', tip); - self.removeBlock(tip.hash, batch, function(err) { if (err) return callback(err); @@ -819,10 +828,8 @@ ChainDB.prototype.connectBlock = function connectBlock(block, view, batch, callb var undo = new BufferWriter(); var i, j, tx, input, output, prev, addresses, address, hash, coins, raw; - if (this.options.spv) { - this.emit('add block', block); + if (this.options.spv) return utils.asyncify(callback)(null, block); - } // Genesis block's coinbase is unspendable. if (this.chain.isGenesis(block)) @@ -893,8 +900,6 @@ ChainDB.prototype.connectBlock = function connectBlock(block, view, batch, callb if (undo.written > 0) batch.put(layout.u(block.hash()), undo.render()); - this.emit('add block', block); - this._pruneBlock(block, batch, function(err) { if (err) return callback(err); @@ -990,8 +995,6 @@ ChainDB.prototype.disconnectBlock = function disconnectBlock(block, batch, callb batch.del(layout.u(block.hash())); - self.emit('remove block', block); - return callback(null, block); }); }; diff --git a/lib/bcoin/fullnode.js b/lib/bcoin/fullnode.js index e9d6fa23..5c047123 100644 --- a/lib/bcoin/fullnode.js +++ b/lib/bcoin/fullnode.js @@ -179,35 +179,34 @@ Fullnode.prototype._init = function _init() { self.emit('alert', details); }); - this.on('tx', function(tx) { + this.mempool.on('tx', function(tx) { + self.emit('tx', tx); self.walletdb.addTX(tx, function(err) { if (err) self._error(err); }); }); - // Emit events for valid blocks and TXs. this.chain.on('block', function(block) { self.emit('block', block); - block.txs.forEach(function(tx) { - self.emit('tx', tx, block); + }); + + this.chain.on('connect', function(entry, block) { + self.walletdb.addBlock(block, function(err) { + if (err) + self._error(err); }); - }); - this.mempool.on('tx', function(tx) { - self.emit('tx', tx); - }); - - this.chain.on('add block', function(block) { if (!self.chain.isFull()) return; + self.mempool.addBlock(block, function(err) { if (err) self._error(err); }); }); - this.chain.on('remove block', function(block) { + this.chain.on('disconnect', function(entry, block) { self.walletdb.removeBlock(block, function(err) { if (err) self._error(err); diff --git a/lib/bcoin/spvnode.js b/lib/bcoin/spvnode.js index a3aa3737..179797c4 100644 --- a/lib/bcoin/spvnode.js +++ b/lib/bcoin/spvnode.js @@ -119,27 +119,17 @@ SPVNode.prototype._init = function _init() { self.emit('alert', details); }); - this.on('tx', function(tx) { + this.pool.on('tx', function(tx) { + self.emit('tx', tx); self.walletdb.addTX(tx, function(err) { if (err) self._error(err); }); }); - // Emit events for valid blocks and TXs. this.chain.on('block', function(block) { self.emit('block', block); - block.txs.forEach(function(tx) { - self.emit('tx', tx, block); - }); - }); - - this.pool.on('tx', function(tx) { - self.emit('tx', tx); - }); - - this.chain.on('remove entry', function(entry) { - self.walletdb.removeBlockSPV(entry, function(err) { + self.walletdb.addBlock(block, function(err) { if (err) self._error(err); }); diff --git a/lib/bcoin/txdb.js b/lib/bcoin/txdb.js index 4ed89df4..ac007faa 100644 --- a/lib/bcoin/txdb.js +++ b/lib/bcoin/txdb.js @@ -281,6 +281,93 @@ TXDB.prototype._getOrphans = function _getOrphans(key, callback) { }); }; +/** + * Add a block's transactions and write the new best hash. + * @param {Block} block + * @param {Function} callback + */ + +TXDB.prototype.addBlock = function addBlock(block, callback, force) { + var self = this; + var unlock; + + unlock = this._lock(addBlock, [block, callback], force); + + if (!unlock) + return; + + callback = utils.wrap(callback, unlock); + + utils.forEachSerial(block.txs, function(tx, next) { + self.add(tx, next, true); + }, function(err) { + if (err) + return callback(err); + + self.db.put('R', block.hash(), callback); + }); +}; + +/** + * Unconfirm a block's transactions and write the new best hash. + * @param {Block} block + * @param {Function} callback + */ + +TXDB.prototype.removeBlock = function removeBlock(block, callback, force) { + var self = this; + var unlock; + + unlock = this._lock(removeBlock, [block, callback], force); + + if (!unlock) + return; + + callback = utils.wrap(callback, unlock); + + utils.forEachSerial(block.txs, function(tx, next) { + self.unconfirm(tx.hash('hex'), next, true); + }, function(err) { + if (err) + return callback(err); + + self.db.put('R', new Buffer(block.prevBlock, 'hex'), callback) + }); +}; + +/** + * Unconfirm a block's transactions + * and write the new best hash (SPV version). + * @param {Block} block + * @param {Function} callback + */ + +TXDB.prototype.removeBlockSPV = function removeBlockSPV(block, callback, force) { + var self = this; + var unlock; + + unlock = this._lock(removeBlock, [block, callback], force); + + if (!unlock) + return; + + callback = utils.wrap(callback, unlock); + + this.tx.getHeightHashes(block.height, function(err, hashes) { + if (err) + return callback(err); + + utils.forEachSerial(hashes, function(hash, next) { + self.unconfirm(hash, next, true); + }, function(err) { + if (err) + return callback(err); + + self.db.put('R', new Buffer(block.prevBlock, 'hex'), callback) + }); + }); +}; + /** * Add a transaction to the database, map addresses * to wallet IDs, potentially store orphans, resolve diff --git a/lib/bcoin/walletdb.js b/lib/bcoin/walletdb.js index 92e89e8d..4da8f0cd 100644 --- a/lib/bcoin/walletdb.js +++ b/lib/bcoin/walletdb.js @@ -1084,36 +1084,23 @@ WalletDB.prototype._getKey = function _getKey(id, account, errback, callback) { }; /** - * Notify the database that a block has been - * removed (reorg). Unconfirms transactions by height. - * @param {MerkleBlock|Block} block + * Add a block's transactions and write the new best hash. + * @param {Block} block * @param {Function} callback */ -WalletDB.prototype.removeBlockSPV = function removeBlockSPV(block, callback) { - var self = this; - this.tx.getHeightHashes(block.height, function(err, hashes) { - if (err) - return callback(err); - - utils.forEachSerial(hashes, function(hash, next) { - self.tx.unconfirm(hash, next); - }, callback); - }); +WalletDB.prototype.addBlock = function addBlock(block, callback) { + this.tx.addBlock(block, callback); }; /** - * Notify the database that a block has been - * removed (reorg). Unconfirms transactions. + * Unconfirm a block's transactions and write the new best hash. * @param {Block} block * @param {Function} callback */ WalletDB.prototype.removeBlock = function removeBlock(block, callback) { - var self = this; - utils.forEachSerial(block.txs, function(tx, next) { - self.tx.unconfirm(tx.hash('hex'), next); - }, callback); + this.tx.removeBlock(block, callback); }; /** diff --git a/test/chain-test.js b/test/chain-test.js index e070e980..5b36376c 100644 --- a/test/chain-test.js +++ b/test/chain-test.js @@ -144,7 +144,7 @@ describe('Chain', function() { assert(reorg); chain.tip = oldTip; var forked = false; - chain.once('fork', function() { + chain.once('reorganize', function() { forked = true; }); deleteCoins(reorg);