event based system for chain error handling.

This commit is contained in:
Christopher Jeffrey 2016-02-17 15:30:33 -08:00
parent 7a04ac25d0
commit 5d9f106e2a
6 changed files with 187 additions and 214 deletions

View File

@ -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);

View File

@ -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;
}

View File

@ -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.'));

View File

@ -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
}));
};

View File

@ -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)
);
});

View File

@ -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;
}