From 5d9f106e2a4938028b64d21c68d73293002fb899 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Wed, 17 Feb 2016 15:30:33 -0800 Subject: [PATCH] event based system for chain error handling. --- lib/bcoin/chain.js | 121 ++++++++++++++++--- lib/bcoin/coin.js | 2 +- lib/bcoin/mempool.js | 2 +- lib/bcoin/peer.js | 4 +- lib/bcoin/pool.js | 268 +++++++++++++------------------------------ lib/bcoin/tx.js | 4 +- 6 files changed, 187 insertions(+), 214 deletions(-) diff --git a/lib/bcoin/chain.js b/lib/bcoin/chain.js index 6837e236..0b413099 100644 --- a/lib/bcoin/chain.js +++ b/lib/bcoin/chain.js @@ -39,11 +39,13 @@ function Chain(options) { this.request = new utils.RequestCache(); this.loading = false; this.tip = null; + this.height = -1; this.mempool = options.mempool; this.blockdb = options.blockdb; this.locked = false; this.pending = []; this.orphanLimit = options.orphanLimit || 20 * 1024 * 1024; + this.invalid = {}; this.orphan = { map: {}, @@ -86,6 +88,47 @@ Chain.msg = function msg(code) { Chain.prototype._init = function _init() { var self = this; + // Hook into events for debugging + this.on('fork', function(data, peer) { + var host = peer ? peer.host : 'unknown'; + utils.debug( + 'Fork at height %d: expected=%s received=%s checkpoint=%s peer=%s', + data.height, + utils.revHex(data.expected), + utils.revHex(data.received), + data.checkpoint, + host + ); + }); + + this.on('invalid', function(data, peer) { + var host = peer ? peer.host : 'unknown'; + utils.debug( + 'Invalid block at height %d: hash=%s peer=%s', + data.height, + utils.revHex(data.hash), + host + ); + if (data.chain) { + utils.debug( + 'Peer is sending an invalid continuation chain (%s)', + host); + } else if (data.seen) { + utils.debug('Peer is sending an invalid chain (%s)', host); + } + }); + + this.on('exists', function(data, peer) { + var host = peer ? peer.host : 'unknown'; + utils.debug('Already have block %s (%s)', + data.height, host); + }); + + this.on('orphan', function(data, peer) { + var host = peer ? peer.host : 'unknown'; + utils.debug('Handled orphan %s (%s)', utils.revHex(data.hash), host); + }); + this.loading = true; utils.debug('Chain is loading.'); @@ -725,6 +768,7 @@ Chain.prototype._saveEntry = function _saveEntry(entry, save, callback) { if (!this.tip || entry.height > this.tip.height) { this.tip = entry; + this.height = this.tip.height; this.emit('tip', this.tip); } @@ -772,6 +816,8 @@ Chain.prototype.resetHeight = function resetHeight(height) { } this.tip = this.db.get(height); + assert(this.tip); + this.height = this.tip.height; this.emit('tip', this.tip); }; @@ -784,6 +830,7 @@ Chain.prototype.resetTime = function resetTime(ts) { Chain.prototype.add = function add(initial, peer, callback) { var self = this; + var host = peer ? peer.host : 'unknown'; var code = Chain.codes.unchanged; var total = 0; @@ -797,7 +844,23 @@ Chain.prototype.add = function add(initial, peer, callback) { (function next(block) { var hash = block.hash('hex'); var prevHash = block.prevBlock; - var prevHeight, entry, existing, checkpoint, prev; + var prevHeight, entry, existing, checkpoint, prev, orphan; + + // Special case for genesis block. + if (block.isGenesis()) + return done(null, code); + + // Do not revalidate known invalid blocks. + if (self.invalid[hash] || self.invalid[prevHash]) { + code = Chain.codes.invalid; + self.emit('invalid', { + height: -1, + hash: hash, + seen: true, + chain: self.invalid[prevHash] + }, peer); + return done(null, code);; + } // Find the previous block height/index. prevHeight = self.heightLookup[prevHash]; @@ -808,32 +871,41 @@ Chain.prototype.add = function add(initial, peer, callback) { // orphans. if (block === initial && !block.verify()) { code = Chain.codes.invalid; + self.invalid[hash] = true; self.emit('invalid', { height: prevHeight + 1, - hash: hash + hash: hash, + seen: false, + chain: false }, peer); return done(null, code); } // If the block is already known to be // an orphan, ignore it. - if (self.orphan.map[prevHash]) { + orphan = self.orphan.map[prevHash]; + if (orphan) { // If the orphan chain forked, simply // reset the orphans and find a new peer. - if (self.orphan.map[prevHash].hash('hex') !== hash) { + if (orphan.hash('hex') !== hash) { self.orphan.map = {}; self.orphan.bmap = {}; self.orphan.count = 0; self.orphan.size = 0; self.emit('fork', { height: -1, - expected: self.orphan.map[prevHash].hash('hex'), + expected: orphan.hash('hex'), received: hash, checkpoint: false }, peer); code = Chain.codes.forked; return done(null, code); } + self.emit('orphan', { + height: -1, + hash: hash, + seen: true + }, peer); code = Chain.codes.knownOrphan; return done(null, code); } @@ -846,6 +918,11 @@ Chain.prototype.add = function add(initial, peer, callback) { self.orphan.map[prevHash] = block; self.orphan.bmap[hash] = block; code = Chain.codes.newOrphan; + self.emit('orphan', { + height: -1, + hash: hash, + seen: false + }, peer); return done(null, code); } @@ -870,7 +947,11 @@ Chain.prototype.add = function add(initial, peer, callback) { // who isn't trying to fool us. checkpoint = network.checkpoints[entry.height]; if (checkpoint) { - self.emit('checkpoint', entry.height, entry.hash, checkpoint); + self.emit('checkpoint', { + height: entry.height, + hash: entry.hash, + checkpoint: checkpoint + }); if (hash !== checkpoint) { // Resetting to the last checkpoint _really_ isn't // necessary (even bitcoind doesn't do it), but it @@ -883,7 +964,7 @@ Chain.prototype.add = function add(initial, peer, callback) { expected: network.checkpoints[entry.height], received: entry.hash, checkpoint: true - }); + }, peer); return done(null, code); } } @@ -898,8 +979,13 @@ Chain.prototype.add = function add(initial, peer, callback) { // NOTE: Wrap this in a nextTick to avoid // a stack overflow if there are a lot of // existing blocks. - if (existing.hash === hash) + if (existing.hash === hash) { + self.emit('exists', { + height: entry.height, + hash: entry.hash + }, peer); return utils.nextTick(handleOrphans); + } // A valid block with an already existing // height came in, that spells fork. We @@ -928,7 +1014,7 @@ Chain.prototype.add = function add(initial, peer, callback) { self.emit('fork', { height: existing.height, expected: existing.hash, - received: hash, + received: entry.hash, checkpoint: false }, peer); @@ -954,9 +1040,12 @@ Chain.prototype.add = function add(initial, peer, callback) { if (!verified) { code = Chain.codes.invalid; + self.invalid[entry.hash] = true; self.emit('invalid', { - height: prevHeight + 1, - hash: hash + height: entry.height, + hash: entry.hash, + seen: false, + chain: false }, peer); return done(null, code); } @@ -1057,7 +1146,7 @@ Chain.prototype.add = function add(initial, peer, callback) { item = self.pending.shift(); - self.chain.add(item[0], item[1], item[2]); + self.add(item[0], item[1], item[2]); }); } }; @@ -1174,7 +1263,7 @@ Chain.prototype.hashRange = function hashRange(start, end) { Chain.prototype.getLocator = function getLocator(start) { var hashes = []; - var top = this.height(); + var top = this.height; var step = 1; var i, existing; @@ -1269,12 +1358,6 @@ Chain.prototype.getSize = function getSize() { // Legacy Chain.prototype.size = Chain.prototype.getSize; -Chain.prototype.height = function height() { - if (!this.tip) - return -1; - return this.tip.height; -}; - Chain.prototype.getCurrentTarget = function getCurrentTarget() { if (!this.tip) return utils.toCompact(network.powLimit); diff --git a/lib/bcoin/coin.js b/lib/bcoin/coin.js index 0d4cb0e0..1daddd0e 100644 --- a/lib/bcoin/coin.js +++ b/lib/bcoin/coin.js @@ -85,7 +85,7 @@ Coin.prototype.getConfirmations = function getConfirmations(height) { if (!this.chain) return 0; - top = this.chain.height(); + top = this.chain.height; } else { top = height; } diff --git a/lib/bcoin/mempool.js b/lib/bcoin/mempool.js index 672adb24..9a5656df 100644 --- a/lib/bcoin/mempool.js +++ b/lib/bcoin/mempool.js @@ -251,7 +251,7 @@ Mempool.prototype.addTX = function addTX(tx, peer, callback) { return callback(new Error('TX is spending coins that it does not have.')); } - height = self.pool.chain.height() + 1; + height = self.pool.chain.height + 1; ts = utils.now(); if (!tx.isFinal(height, ts)) { return callback(new Error('TX is not final.')); diff --git a/lib/bcoin/peer.js b/lib/bcoin/peer.js index 5f9037fc..3765f45f 100644 --- a/lib/bcoin/peer.js +++ b/lib/bcoin/peer.js @@ -148,7 +148,7 @@ Peer.prototype._init = function init() { this.once('version', function() { utils.debug( 'Sent version (%s): height=%s', - self.host, this.pool.chain.height()); + self.host, this.pool.chain.height); }); this._ping.timer = setInterval(function() { @@ -176,7 +176,7 @@ Peer.prototype._init = function init() { // Send hello this._write(this.framer.version({ - height: this.pool.chain.height(), + height: this.pool.chain.height, relay: this.options.relay })); }; diff --git a/lib/bcoin/pool.js b/lib/bcoin/pool.js index d12dfa4c..5a586758 100644 --- a/lib/bcoin/pool.js +++ b/lib/bcoin/pool.js @@ -84,9 +84,6 @@ function Pool(options) { interval: options.loadInterval || 5000 }; - this._pendingBlocks = []; - this._locked = false; - this.requestTimeout = options.requestTimeout || 600000; this.chain = new bcoin.chain({ @@ -192,16 +189,7 @@ Pool.prototype._init = function _init() { }); this.chain.on('fork', function(data, peer) { - utils.debug( - 'Fork at height %d: expected=%s received=%s checkpoint=%s peer=%s', - data.height, - utils.revHex(data.expected), - utils.revHex(data.received), - data.checkpoint, - peer ? peer.host : '' - ); - - self.emit('fork', data.expected, data.received); + self.emit('fork', data, peer); if (!peer) return; @@ -217,21 +205,47 @@ Pool.prototype._init = function _init() { }); this.chain.on('invalid', function(data, peer) { - utils.debug( - 'Invalid block at height %d: hash=%s peer=%s', - data.height, - utils.revHex(data.hash), - peer ? peer.host : '' - ); - - self.block.invalid[data.hash] = true; - if (!peer) return; self.setMisbehavior(peer, 100); }); + this.chain.on('exists', function(data, peer) { + if (!peer) + return; + + self.setMisbehavior(peer, 1); + }); + + this.chain.on('orphan', function(data, peer) { + var host = peer ? peer.host : 'unknown'; + + if (!peer) + return; + + // Resolve orphan chain + if (!self.options.headers) { + // Make sure the peer doesn't send us + // more than 200 orphans every 3 minutes. + if (self.isOrphaning(peer)) { + utils.debug('Peer is orphaning (%s)', host); + self.setMisbehavior(peer, 100); + return; + } + + // Resolve orphan chain. + self.peers.load.loadBlocks( + self.chain.getLocator(), + self.chain.getOrphanRoot(data.hash) + ); + } else { + // Increase banscore by 10 if we're using getheaders. + if (!self.options.multiplePeers) + self.setMisbehavior(peer, 10); + } + }); + this.options.wallets.forEach(function(w) { self.addWallet(w); }); @@ -240,7 +254,7 @@ Pool.prototype._init = function _init() { if (this.chain.isFull()) { this.synced = true; this.emit('full'); - utils.debug('Chain is fully synced (height=%d).', this.chain.height()); + utils.debug('Chain is fully synced (height=%d).', this.chain.height); } this.startServer(); @@ -299,7 +313,7 @@ Pool.prototype._startTimer = function _startTimer() { self._stopInterval(); self.synced = true; self.emit('full'); - utils.debug('Chain is fully synced (height=%d).', self.chain.height()); + utils.debug('Chain is fully synced (height=%d).', self.chain.height); return; } @@ -608,176 +622,53 @@ Pool.prototype._prehandleBlock = function _prehandleBlock(block, peer, callback) Pool.prototype._handleBlock = function _handleBlock(block, peer, callback) { var self = this; + var requested; - if (this._locked) { - this._pendingBlocks.push([block, peer, callback]); - return; - } + callback = utils.asyncify(callback); - this._locked = true; + // Fulfill our request. + requested = self._response(block); - function done(err, result) { - var item; - - utils.nextTick(function() { - callback(err, result); - - self._locked = false; - - if (self._pendingBlocks.length === 0) - return; - - item = self._pendingBlocks.shift(); - - self._handleBlock(item[0], item[1], item[2]); - }); + // Someone is sending us blocks without + // us requesting them. + if (!requested) { + utils.debug( + 'Recieved unrequested block: %s (%s)', + block.rhash, peer.host); } this._prehandleBlock(block, peer, function(err) { - var requested; - - if (err) - return done(err); - - // Fulfill our request. - requested = self._response(block); - - // Ensure the block was not invalid last time. - // Someone might be sending us bad blocks to DoS us. - if (self.block.invalid[block.hash('hex')]) { - utils.debug('Peer is sending an invalid chain (%s)', peer.host); - self.setMisbehavior(peer, 100); - return done(null, false); - } - - // Ensure this is not a continuation - // of an invalid chain. - if (self.block.invalid[block.prevBlock]) { - utils.debug( - 'Peer is sending an invalid continuation chain (%s)', - peer.host); - self.setMisbehavior(peer, 100); - return done(null, false); - } - - // Ignore if we already have. - if (self.chain.has(block)) { - utils.debug('Already have block %s (%s)', - self.chain.getHeight(block), peer.host); - self.setMisbehavior(peer, 1); - return done(null, false); - } - - // Make sure the block is valid. - if (!block.verify()) { - utils.debug( - 'Block verification failed for %s (%s)', - block.rhash, peer.host); - self.block.invalid[block.hash('hex')] = true; - self.setMisbehavior(peer, 100); - return done(null, false); - } - - // Someone is sending us blocks without - // us requesting them. - if (!requested) { - utils.debug( - 'Recieved unrequested block: %s (%s)', - block.rhash, peer.host); - } - - // Resolve orphan chain - if (!self.options.headers) { - if (!self.chain.hasBlock(block.prevBlock)) { - // Special case for genesis block. - if (block.isGenesis()) - return done(null, false); - - // Make sure the peer doesn't send us - // more than 200 orphans every 3 minutes. - if (self.isOrphaning(peer)) { - utils.debug('Peer is orphaning (%s)', peer.host); - self.setMisbehavior(peer, 100); - return done(null, false); - } - - // NOTE: If we were to emit new orphans here, we - // would not need to store full blocks as orphans. - // However, the listener would not be able to see - // the height until later. - self._addIndex(block, peer, function(err, added) { - if (err) - return done(err); - - // Resolve orphan chain. - self.peers.load.loadBlocks( - self.chain.getLocator(), - self.chain.getOrphanRoot(block) - ); - - utils.debug('Handled orphan %s (%s)', block.rhash, peer.host); - - return done(null, false); - }); - - return; - } - } else { - if (!self.chain.hasBlock(block.prevBlock)) { - // Special case for genesis block. - if (block.isGenesis()) - return done(null, false); - - // Increase banscore by 10 if we're using getheaders. - if (!self.options.multiplePeers) { - if (self.setMisbehavior(peer, 10)) - return done(null, false); - } - } - } - - // Add to index and emit/save - self._addIndex(block, peer, function(err, added) { - if (err) - return done(err); - - if (added) - return done(null, true); - - return done(null, false); - }); - }); -}; - -Pool.prototype._addIndex = function _addIndex(block, peer, callback) { - var self = this; - this.chain.add(block, peer, function(err, added) { if (err) return callback(err); - if (added === 0) - return callback(null, false); + self.chain.add(block, peer, function(err, added) { + if (err) + return callback(err); - self.emit('chain-progress', self.chain.fillPercent(), peer); + if (added === 0) + return callback(null, false); - self.block.total += added; + self.emit('chain-progress', self.chain.fillPercent(), peer); - if (self.chain.height() % 20 === 0) { - utils.debug( - 'Got: %s from %s chain len %d blocks %d orp %d act %d queue %d target %s peers %d pending %d', - block.rhash, - new Date(block.ts * 1000).toString(), - self.chain.height(), - self.block.total, - self.chain.orphan.count, - self.request.active, - peer._blockQueue.length, - self.chain.currentTarget(), - self.peers.all.length, - self._pendingBlocks.length); - } + self.block.total += added; - return callback(null, true); + if (self.chain.height % 20 === 0) { + utils.debug( + 'Got: %s from %s chain len %d blocks %d orp %d act %d queue %d target %s peers %d pending %d', + block.rhash, + new Date(block.ts * 1000).toString(), + self.chain.height, + self.block.total, + self.chain.orphan.count, + self.request.activeBlocks, + peer._blockQueue.length, + self.chain.currentTarget(), + self.peers.all.length, + self.chain.pending.length); + } + + return callback(null, true); + }); }); }; @@ -887,9 +778,13 @@ Pool.prototype._createPeer = function _createPeer(options) { Pool.prototype._handleTX = function _handleTX(tx, peer, callback) { var self = this; + var requested, added; callback = utils.asyncify(callback); + requested = self._response(tx); + added = self._addTX(tx, 1); + function addMempool(tx, peer, callback) { if (!self.mempool) return callback(); @@ -903,14 +798,9 @@ Pool.prototype._handleTX = function _handleTX(tx, peer, callback) { return callback(err); addMempool(tx, peer, function(err) { - var requested, added; - if (err && self.synced) utils.debug('Mempool error: %s', err.message); - requested = self._response(tx); - added = self._addTX(tx, 1); - if (added || tx.block) self.emit('tx', tx, peer); @@ -1403,13 +1293,13 @@ Pool.prototype.searchWallet = function(w, h) { if (height > 0) { // Back one week if (!height || height === -1) - height = this.chain.height() - (7 * 24 * 6); + height = this.chain.height - (7 * 24 * 6); utils.nextTick(function() { utils.debug('Wallet height: %s', height); utils.debug( 'Reverted chain to height=%d', - self.chain.height() + self.chain.height ); }); @@ -1425,7 +1315,7 @@ Pool.prototype.searchWallet = function(w, h) { utils.debug('Wallet time: %s', new Date(ts * 1000)); utils.debug( 'Reverted chain to height=%d (%s)', - self.chain.height(), + self.chain.height, new Date(self.chain.tip.ts * 1000) ); }); diff --git a/lib/bcoin/tx.js b/lib/bcoin/tx.js index 1dfeba5d..74c6a30a 100644 --- a/lib/bcoin/tx.js +++ b/lib/bcoin/tx.js @@ -1406,7 +1406,7 @@ TX.prototype.avoidFeeSnipping = function avoidFeeSnipping(height) { if (!this.chain) return; - height = this.chain.height(); + height = this.chain.height; } if (height === -1) @@ -1704,7 +1704,7 @@ TX.prototype.getConfirmations = function getConfirmations(height) { if (!this.chain) return 0; - top = this.chain.height(); + top = this.chain.height; } else { top = height; }